Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-core/src/render/gpu/mod.rs

//! **The L0 GPU lane** (feature `wgpu`) — the extracted shared scaffold the three
//! skin kernels (facett-map `GpuMapRenderer`, facett-map3d `Map3dRenderer`,
//! facett-graph3d `LogoRenderer`) will be re-pointed at in Phase C.
//!
//! All three share the identical `egui_wgpu::CallbackTrait` plumbing: a `*Renderer`
//! stored in `callback_resources`, **installed once** via an `install_renderer`,
//! driven by a paint callback. Phase A extracts:
//!
//! - [`types`] — the bytemuck `Vertex`/`Uniform`/`DrawIndirectArgs` structs +
//!   `color32_to_f32` (byte-identical to the skins' copies).
//! - [`buffer`] — the upload-once + **chunking** algorithm (fix #12), verbatim.
//! - [`install_renderer`] — the install-once lifecycle each kernel hand-rolls.
//! - [`CULL_WGSL`] / [`DRAW_WGSL`] — the cull + draw shaders, **byte-identical** to
//!   `facett-map/src/gpu/*.wgsl` (do not change their semantics).
//!
//! Nothing in the skins consumes this yet — it is the scaffold for Phase B/C.

/// **wgpu glue for the adapter-selection policy** — `wgpu::AdapterInfo` →
/// [`AdapterFacts`](crate::render::adapter::AdapterFacts), the live selector, the
/// `request_best_adapter` replacement for the naive headless probes, and the
/// `egui_wgpu::WgpuConfiguration` an eframe host installs. See [`adapter_wgpu`].
pub mod adapter_wgpu;
pub mod blur;
pub mod buffer;
/// **THE shared instanced graph-cloud renderer** — screen-space emissive node discs
/// + edge filaments as GPU instances, plus the live `egui_wgpu` paint callback and
/// its install-once lifecycle. Consumed by `facett-graph3d` (which re-exports it as
/// `graph_gpu`) AND by the 2-D graph family (`facett-graphpan` / `facett-graphview` /
/// `facett-graphnav`). See [`graphcloud`].
pub mod graphcloud;
/// **Final-look colour grade** (AgX tonemap + vignette + film grain + CAS sharpen +
/// blue-noise-style dither) as an optional post over the HDR offscreen colour — a
/// new, composable seam that does **not** touch the map3d ACES tonemap. See
/// [`colorgrade`].
pub mod colorgrade;
/// **GPU label collision + the indirect label draw** (GFX_V2 §3.B item 4) — three
/// compute passes over a fixed spatial grid whose third pass *produces the draw* by
/// `atomicAdd`ing onto a `DrawIndirectArgs`. The collision rule itself lives in
/// [`crate::label_grid`]; the WGSL here is its device transcription. See
/// [`label_collide`].
pub mod label_collide;
/// **MSDF text / label rendering** — crisp glowing labels at any zoom from a
/// multi-channel signed-distance-field atlas. See [`msdf`].
pub mod msdf;
pub mod offscreen;
/// **The in-GPU pass clock** — TIMESTAMP_QUERY around a lane's encoded passes,
/// resolved through a non-blocking readback ring into the per-lane clock stats
/// (`crate::render::lane::clock_stats_of`). Feature-detected; a device without the
/// feature reports `None`, never `0`. See [`pass_clock`].
pub mod pass_clock;
/// **OIT — order-independent transparency via per-pixel fragment linked lists**
/// (GFX_V2 §3.C item 7): gather every translucent fragment onto its pixel's list,
/// then sort and composite per pixel in one fullscreen resolve. See [`oit`].
pub mod oit;
/// **GPU picking — the colour-encoded ID pass** (GFX_V2 §7 item 8): render pickable
/// geometry into an `R32Uint` target where the value IS the [`PickId`](crate::engine::pick::PickId),
/// then read back the texel under the cursor. See [`picking`].
pub mod picking;
/// **The host lane for GPU picking** — install-once + the one `pick_at` entry point a
/// widget calls to turn a click into a `PickId`. See [`pick_host`].
pub mod pick_host;
/// **GPU compute particles** — storage-buffer boids stepped in a compute shader and
/// drawn as soft additive points into the HDR target. See [`particles`].
pub mod particles;
/// **THE one GPU→CPU readback** — the `map_async` + row-padding dance, spelled once
/// instead of in seventeen hand-rolled copies. See [`readback`].
pub mod readback;
/// **Scene3D, the GPU lane** — flat quads, screen-width lines and SDF text on world
/// planes, depth-tested in one pass and blitted into egui. See [`scene3d`].
pub mod scene3d;
pub mod sdf_pipeline;
/// **TAA — Halton jitter + a velocity-reprojecting temporal resolve** (GFX_V2 §5
/// item 7): the fix for sub-pixel vector lines shimmering under camera motion, which
/// MSAA cannot solve. See [`taa`].
pub mod taa;
pub mod types;

pub use adapter_wgpu::{
    decide_for, facett_wgpu_options, facts_of, facts_of_all, kind_of, preferred_backends,
    refuse_software_adapter, request_best_adapter, select_adapter,
};
pub use blur::{gaussian_taps, GaussianBlur, BLUR_WGSL};
pub use graphcloud::{
    render_cloud_offscreen, render_graph_offscreen, BloomParams, CloudBuffers, CloudCamera,
    CloudEdge, CloudPoint, EdgeInstance, GraphGpu, GraphRender, GraphScene, NodeInstance,
};
pub use graphcloud::cloud::CloudModelPaintCallback;
pub use buffer::{
    ChunkSpan, chunk_budget, chunk_budget_stride, partition_chunks, partition_chunks_stride,
    rebase_chunk_meta,
};
pub use colorgrade::{
    agx_tonemap, ign, srgb_oetf, vignette_factor, ColorGrade, ColorGradeParams, COLORGRADE_WGSL,
};
pub use label_collide::{
    LabelCollider, LabelStats, LABEL_COLLIDE_WGSL, LABEL_DRAW_WGSL, LABEL_GLYPH_CAPACITY,
};
pub use msdf::{
    coverage, disk_msdf_atlas, median3, screen_px_range, GlyphInstance, MsdfText, MSDF_WGSL,
};
pub use particles::{
    advect_cpu, step_cpu, DrawParams, FlowGeometry, GpuParticles, Particle, ParticleBuffers,
    SimParams, PARTICLES_WGSL,
};
pub use offscreen::{
    gpu_scissor_px, scissor_intersection, OffscreenColorDepth, ScissorPx, BLIT_WGSL, DEPTH_FORMAT,
    OFFSCREEN_FORMAT,
};
pub use oit::{
    OitBatch, OitPass, OitVertex, OIT_DEFAULT_LAYERS, OIT_MAX_LAYERS, OIT_NODE_SIZE,
    OIT_VERTEX_STRIDE, OIT_WGSL,
};
pub use pass_clock::{PassClock, PassClockSlot, PASS_CLOCK_FEATURES};
pub use pick_host::{install_pick_host, pick_at, pick_host_installed, pick_passes_run, PickHost};
pub use readback::{map_read_all, read_buffer_range, read_texture_region, strip_row_padding};
pub use taa::{
    halton, history_weight, jitter_px, TaaPass, TAA_FEEDBACK_CAP, TAA_PHASES, TAA_WGSL,
};
pub use picking::{
    logical_to_texel, painter_depth, PickBatch, PickPass, PickTarget, PickVertex,
    PICK_DEPTH_FORMAT, PICK_FORMAT, PICK_VERTEX_STRIDE, PICK_WGSL,
};
pub use sdf_pipeline::{
    GpuSdfHeadless, GpuSdfRenderer, SdfUniforms, offscreen_render, LINE_WGSL, SDF_WGSL,
};
pub use types::{
    DrawIndirectArgs, DrawUniforms, GPU_WAY_FLAG_FILL, GpuWayMeta, Vertex, ViewportCull,
    color32_to_f32, rebase_vertex_origin,
};

/// **The shader TEXT lives in [`crate::render::wgsl`]**, which is NOT behind this
/// feature — a `.wgsl` source string needs `include_str!` and nothing else, and
/// keeping it here put the shader-CONTRACT guards (`cull.wgsl` is the shader every
/// road on the map goes through) out of reach of a default `cargo test`. Re-exported
/// so every existing `facett_core::render::gpu::*_WGSL` path still resolves.
pub use crate::render::wgsl::{COMMON_WGSL, CULL_WGSL, DRAW_WGSL, wgsl};

/// **THE RULING: MSAA is not facett's anti-aliasing scheme, and this is the one writer of
/// that.** (Since 2026-09-05 there is exactly ONE multisampled target in the workspace,
/// and it is not there for anti-aliasing — see *The ONE exception* below.)
/// TAA is the 3D anti-aliasing scheme ([`taa`]); the analytic feather in
/// `facett-map/src/gpu/line.wgsl` is the 2D one. Every render pipeline in every facett
/// crate names [`msaa_state()`] and every render target names [`NO_MSAA_SAMPLES`],
/// instead of the 26 + 26 scattered `MultisampleState::default()` / `sample_count: 1`
/// literals that used to state the same thing fifty-two times over.
///
/// # Why not MSAA
///
/// * **Our own shaders already beat it at its own job.** `line.wgsl` extends the
///   geometry by `feather_px` and takes fragment alpha from `coverage_inside(edge_px,
///   feather_px)` — true analytic coverage, resolution-independent, costing no
///   framebuffer. That is the technique the professional GL map renderers use, for the
///   content that dominates a map frame.
/// * **MSAA does not fix what actually looks bad.** A 1 px road at 4× has four coverage
///   steps and still crawls when the camera rotates, because *which* subsamples it
///   covers changes discontinuously. That sentence is in [`taa`]'s own header and is
///   why TAA was written.
/// * **It does not compose with what map3d already runs.** A deferred SSAO G-buffer and
///   a screen-space post stack are exactly the category MSAA is bad for: it multiplies
///   colour+depth bandwidth and breaks screen-space effects.
/// * **And the failure mode is HARD.** `MultisampleState::default()` is `count: 1`. A
///   pipeline left on the default while its target goes to 4× is a wgpu **validation
///   failure**, not a soft downgrade. Fifty-two literals is fifty-two chances to miss
///   one on a path nobody exercises — so if MSAA is ever genuinely wanted, the LAW-5
///   form is to change these two items and let every site follow.
/// * **Two AA schemes composed by accident is worse than either.** facett ships TAA.
///
/// # The ONE exception, and why it does not erode any of the above
///
/// Since lane GG #2 (2026-09-05) the map's offscreen colour+depth pair is **4×** —
/// `facett_map::gpu::MAP_MSAA_SAMPLES` / `map_msaa_state()`, and nothing else in the
/// workspace is. Read the reasons above again and none of them is what it buys: it buys
/// per-sample **DEPTH**, and no other mechanism here can supply that.
///
/// Lane MP-a gave the map its own depth buffer so the declared paint rank could survive
/// being split across tiles. With one depth value per PIXEL, a partially covered edge
/// fragment must choose between compositing and owning the pixel — and it owns it, so the
/// lower-ranked way underneath is depth-rejected rather than blended and what shows is the
/// winner over bare paper. MEASURED on oden / RTX 4090: across a motorway's whole 7 px
/// footprint the frame with a residential beneath it was bit-identical to the frame
/// without. `MAP_MSAA_SAMPLES` carries the numbers.
///
/// Note also what the exception is NOT: 4× alone changes nothing there, and was measured
/// not to. The road shader rasterises its quad one feather wider than the road, so the
/// rasteriser already sees full coverage at exactly those pixels; the coverage lives in
/// the fragment alpha and `line.wgsl` hands it back through `@builtin(sample_mask)`. MSAA
/// is the substrate that makes a sample mask mean something, not the fix.
///
/// It is a second named writer rather than a bump of [`NO_MSAA_SAMPLES`] for the reason
/// this whole item exists: raising the constant would put all fifty-odd sites on 4×,
/// including [`picking`], whose `R32Uint` target cannot be multisampled at all.
///
/// `tests/one_msaa_writer.rs` scans the source tree and fails if a naked
/// `MultisampleState` literal or a bare `sample_count:` reappears outside this file — and
/// pins the exception to one declaration in one file, so it cannot spread.
#[must_use]
pub const fn msaa_state() -> wgpu::MultisampleState {
    wgpu::MultisampleState { count: NO_MSAA_SAMPLES, mask: !0, alpha_to_coverage_enabled: false }
}

/// **The sample count every facett render target is allocated at** — see
/// [`msaa_state()`] for the ruling this encodes. `1` is not a placeholder awaiting a
/// bump: raising it here alone would leave every pipeline on `count: 1` and turn the
/// next frame into a wgpu validation failure.
pub const NO_MSAA_SAMPLES: u32 = 1;

/// **A texture size the device can make**: `w × h` (at least 1 × 1) with each side
/// clamped to `max` (`device.limits().max_texture_dimension_2d`). A pane wider than
/// the limit renders into the clamped target and the blit scales it back over the
/// pane — a softer picture, never a wgpu validation panic. MEASURED 2026-10-16 on loki
/// (RTX 5080, limit 8192): `l0_offscreen_color` asked 10 604 px wide and killed
/// dwarves-ui-gpu.
#[must_use]
pub fn clamp_extent(max: u32, w: u32, h: u32) -> (u32, u32) {
    let max = max.max(1);
    (w.clamp(1, max), h.clamp(1, max))
}

/// [`clamp_extent`] against `device`'s own limit.
#[must_use]
pub fn texture_extent(device: &wgpu::Device, w: u32, h: u32) -> (u32, u32) {
    clamp_extent(device.limits().max_texture_dimension_2d, w, h)
}

/// **Install-once** lifecycle, generic over the renderer type — the pattern all
/// three skin kernels hand-roll (`install_renderer` in facett-map / facett-map3d /
/// facett-graph3d). If a renderer of type `R` is already in the egui-wgpu
/// `RenderState`'s `callback_resources`, this is a no-op; otherwise `make` builds
/// one (from the device + target format) and inserts it. Hosts call this once at
/// startup so the paint callback can `resources.get::<R>()`.
///
/// Returns `true` if a renderer was newly installed, `false` if one already existed.
///
/// It also **registers the lane** with [`crate::render::lane`] under
/// `std::any::type_name::<R>()`, whether or not this call did the inserting. That is
/// how an About box can say "a GPU lane is installed" without every lane wiring
/// itself up: the one install writer is the one place that knows. A lane that also
/// counts frames declares it next to its own install
/// ([`lane::note_reports_frames`](crate::render::lane::note_reports_frames)).
///
/// It opens NO error scope: a pipeline `make` built invalid goes to the device's
/// uncaptured-error handler. [`install_renderer_checked`] is the same install under a
/// validation scope, with the verdict reported through the lane registry.
pub fn install_renderer<R, F>(render_state: &egui_wgpu::RenderState, make: F) -> bool
where
    R: 'static + Send + Sync,
    F: FnOnce(&wgpu::Device, wgpu::TextureFormat) -> R,
{
    crate::render::lane::note_installed(std::any::type_name::<R>());
    if render_state.renderer.read().callback_resources.get::<R>().is_some() {
        return false; // already installed
    }
    let renderer = make(&render_state.device, render_state.target_format);
    render_state.renderer.write().callback_resources.insert(renderer);
    true
}

/// **[`install_renderer`] under a wgpu VALIDATION error scope** — the same synchronous
/// install-once, plus the device's verdict on everything `make` created.
///
/// `make` builds shader modules, pipelines and buffers, and wgpu does not report an
/// invalid one as a return value: the error goes to the device's uncaptured-error
/// handler (native: a panic; web: a console line and a pane that draws nothing), and
/// [`install_renderer`] opens no scope, so that error had no owner. This pushes
/// `ErrorFilter::Validation`, installs, pops the scope, and routes a captured error to
/// [`lane::note_gpu_error`](crate::render::lane::note_gpu_error) — and UNDOES the
/// install: the renderer leaves `callback_resources` and
/// [`lane::is_installed`](crate::render::lane::is_installed) goes `false`, so the pane's
/// CPU painter draws instead of a pipeline the device refused.
///
/// **Synchronous, never blocking.** Every facett host installs inside eframe's
/// `CreationContext` (facet-wrapped `wasm.rs`, facett-demo `main.rs`,
/// facett-wasm-demo `lib.rs`), which cannot `await`. The popped scope is a future:
///
/// * **native** — wgpu-core resolves it when the scope is popped, so ONE poll with a
///   no-op waker settles it before this returns. Should a backend ever answer
///   `Pending`, the future goes to a detached thread that parks until woken — never
///   the caller's thread, and no `pollster`.
/// * **wasm32** — the browser answers asynchronously, so the verdict is settled on
///   `wasm_bindgen_futures::spawn_local` and lands a few frames later; until then the
///   renderer is installed and draws, exactly as with [`install_renderer`].
///
/// Returns `true` if this call installed the renderer and no refusal is known when it
/// returns (on wasm32 a refusal arrives later — read `lane::is_installed` /
/// `lane::gpu_error`). `false` = it was already installed (`make` never ran), or the
/// device refused it (native).
pub fn install_renderer_checked<R, F>(render_state: &egui_wgpu::RenderState, make: F) -> bool
where
    R: 'static + Send + Sync,
    F: FnOnce(&wgpu::Device, wgpu::TextureFormat) -> R,
{
    let name = std::any::type_name::<R>();
    let scope = render_state.device.push_error_scope(wgpu::ErrorFilter::Validation);
    let installed = install_renderer::<R, F>(render_state, make);
    let verdict = scope.pop();
    let renderer = std::sync::Arc::clone(&render_state.renderer);
    let on_verdict = move |error: Option<wgpu::Error>| -> bool {
        let Some(error) = error else { return false };
        if installed {
            renderer.write().callback_resources.remove::<R>();
        }
        let short = name.rsplit("::").next().unwrap_or(name);
        crate::render::lane::note_install_refused(
            name,
            &format!("{short}: install refused by the device: {error}"),
        );
        true
    };
    match settle_error_scope(verdict, on_verdict) {
        Some(refused) => installed && !refused,
        None => installed,
    }
}

/// Settle a popped error scope WITHOUT blocking the caller: `Some(on_verdict(..))` when
/// the verdict was ready on the first poll, `None` when it was handed on.
#[cfg(not(target_arch = "wasm32"))]
fn settle_error_scope<V, G>(verdict: V, on_verdict: G) -> Option<bool>
where
    V: std::future::Future<Output = Option<wgpu::Error>> + Send + 'static,
    G: FnOnce(Option<wgpu::Error>) -> bool + Send + 'static,
{
    use std::task::{Context, Poll, Waker};
    let mut verdict = Box::pin(verdict);
    match verdict.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
        Poll::Ready(error) => Some(on_verdict(error)),
        Poll::Pending => {
            std::thread::spawn(move || {
                /// Wakes the parked settling thread.
                struct Unpark(std::thread::Thread);
                impl std::task::Wake for Unpark {
                    fn wake(self: std::sync::Arc<Self>) {
                        self.0.unpark();
                    }
                }
                let waker = Waker::from(std::sync::Arc::new(Unpark(std::thread::current())));
                let mut cx = Context::from_waker(&waker);
                let error = loop {
                    if let Poll::Ready(error) = verdict.as_mut().poll(&mut cx) {
                        break error;
                    }
                    std::thread::park();
                };
                let _ = on_verdict(error);
            });
            None
        }
    }
}

/// Settle a popped error scope WITHOUT blocking the caller — on the browser's event
/// loop; the verdict always lands later, so this always returns `None`.
#[cfg(target_arch = "wasm32")]
fn settle_error_scope<V, G>(verdict: V, on_verdict: G) -> Option<bool>
where
    V: std::future::Future<Output = Option<wgpu::Error>> + 'static,
    G: FnOnce(Option<wgpu::Error>) -> bool + 'static,
{
    wasm_bindgen_futures::spawn_local(async move {
        let _ = on_verdict(verdict.await);
    });
    None
}