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/graphcloud.rs

//! **THE shared instanced graph-cloud GPU lane** (feature `wgpu`) — an instanced
//! emissive node/edge renderer with real GPU **bloom**, and the ONE renderer every
//! facett graph surface paints its node/edge cloud through.
//!
//! # Why it lives in facett-core (L5)
//!
//! This lane was born inside `facett-graph3d` (as `graph3d::graph_gpu`) to take the
//! measured single-core rotation wall off the CPU: egui tessellates 2.47 M triangles
//! for a 30 000-node cloud on **one** thread (33.75 ms/frame — see
//! `facett/.nornir/graph3d-rotate-perf-design.md`), while the projection that feeds it
//! costs 0.02 ms. The tessellation is the wall, and the cure is to stop tessellating:
//! upload the cloud as **instances** and issue two draw calls.
//!
//! That cure is not 3-D. Every input this module takes is already **screen-space
//! pixels** — a node is a centre + radius, an edge is two endpoints + a half-width. The
//! whole 2-D graph family (`facett-graphpan`, `facett-graphview`, `facett-graphnav`,
//! `facett-graph`) hits exactly the same wall for exactly the same reason, so the
//! renderer moved DOWN here and every graph crate consumes it. `facett_graph3d::graph_gpu`
//! remains as a re-export — the public path is unchanged, and there is still exactly one
//! renderer.
//!
//! It reuses two shared facett-core L0 pieces:
//!
//! - [`OffscreenColorDepth`](crate::render::gpu::OffscreenColorDepth) — the HDR
//!   (`Rgba16Float`) colour+depth offscreen target. Nodes/edges draw into its colour
//!   attachment with **additive blend** (`src = ONE, dst = ONE`), so a fragment that
//!   lands on a texel another already lit **sums** its linear radiance (edge crossings
//!   accumulate past 1.0, which the HDR target keeps).
//! - [`GaussianBlur`](crate::render::gpu::GaussianBlur) — the separable blur
//!   substrate that spreads the bright-pass output for the bloom.
//!
//! The post chain is bright-pass (soft-knee threshold) → blur → composite (scene +
//! bloom·intensity → Reinhard tonemap → LDR). Emissive node/edge colours carry linear
//! radiance **above 1.0**, so the bright pass isolates exactly the glowing graph and
//! dense crossings bloom hottest.
//!
//! Two entry points:
//! - [`render_graph_offscreen`] — the **headless** render-proof: draws a graph and reads
//!   back the LDR framebuffer + the instance draw counts, so the lane is validated as
//!   DATA without a windowing host.
//! - [`cloud`] — the **live** `egui_wgpu` paint callback + the install-once lifecycle.
//!   Read [`cloud::cloud_renderer_installed`] before you skip a CPU painter.

#![allow(clippy::too_many_lines, reason = "self-contained instanced+bloom pipeline set")]

use egui_wgpu::wgpu;
use crate::render::gpu::offscreen::{OffscreenColorDepth, OFFSCREEN_FORMAT};
use crate::render::gpu::GaussianBlur;

/// One instanced emissive node — a screen-space disc. Matches `NodeInst` in
/// `graph_gpu.wgsl`. 32 bytes.
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct NodeInstance {
    /// Centre in physical px (top-left origin, y down).
    pub center: [f32; 2],
    /// Radius in px.
    pub radius: f32,
    pub _pad: f32,
    /// Emissive linear radiance `[r, g, b, a]` (rgb may exceed 1.0 to bloom).
    pub color: [f32; 4],
}

/// One instanced emissive edge — a thick screen-space line segment. Matches
/// `EdgeInst` in `graph_gpu.wgsl`. 40 bytes.
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct EdgeInstance {
    /// Endpoint A in physical px.
    pub a: [f32; 2],
    /// Endpoint B in physical px.
    pub b: [f32; 2],
    /// Half-width in px.
    pub width: f32,
    pub _pad: f32,
    /// Emissive linear radiance `[r, g, b, a]`.
    pub color: [f32; 4],
}

/// **One node of the CLOUD model** — the form the GPU keeps, uploaded once. 16 bytes.
///
/// `pos` is the node in the layout's `~[-1,1]` cube (NOT screen px: the vertex shader
/// projects it), and `color` is the node's RGBA8 packed little-endian exactly as
/// `egui::Color32::to_array()` gives it. Colour rides in the 16th byte because a WGSL
/// `array<vec3<f32>>` has stride 16 anyway — so the padding a `[f32;3]` would waste is
/// spent on the colour instead, and the whole model is ONE buffer.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CloudPoint {
    /// Position in the layout cube (`~[-1,1]³`), y-up.
    pub pos: [f32; 3],
    /// RGBA8, little-endian (`r | g<<8 | b<<16 | a<<24`).
    pub color: u32,
}

impl CloudPoint {
    /// Build from a position and an egui colour.
    #[must_use]
    pub fn new(pos: [f32; 3], color: egui::Color32) -> Self {
        let [r, g, b, a] = color.to_array();
        Self { pos, color: u32::from(r) | (u32::from(g) << 8) | (u32::from(b) << 16) | (u32::from(a) << 24) }
    }
}

/// **One edge of the CLOUD model** — an index pair into [`CloudPoint`]s. 8 bytes,
/// against 40 for a screen-space `EdgeInstance`, and it never changes when the camera
/// moves.
pub type CloudEdge = [u32; 2];

/// The per-frame camera for the cloud pass — **the only thing that moves**. 48 bytes,
/// written once per frame however many million points are on the GPU.
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CloudCamera {
    /// `xy` = pane size px, `zw` = pane centre px.
    pub viewport: [f32; 4],
    /// `x` = yaw (rad), `y` = scale px, `z` = emissive peak, `w` = node radius shrink.
    pub cam: [f32; 4],
    /// `x` = edge half-width px, `y` = edge colour multiplier, `zw` = pad.
    pub edge: [f32; 4],
}

impl CloudCamera {
    /// The camera a `Graph3D` frame implies: pane `w×h`, orbit `yaw`, `emissive` peak,
    /// and the crowd-driven node radius `shrink`. Scale and centre follow the pane the
    /// same way the CPU build did (`w.min(h) * 0.40`, centred).
    #[must_use]
    pub fn frame(w: f32, h: f32, yaw: f32, emissive: f32, shrink: f32) -> Self {
        Self {
            viewport: [w.max(1.0), h.max(1.0), w * 0.5, h * 0.5],
            cam: [yaw, w.min(h) * 0.40, emissive, shrink],
            // 1.5 px half-width and 0.6× radiance — the constants the CPU build used.
            edge: [1.5, 0.6, 0.0, 0.0],
        }
    }

    /// The camera for a FLAT pan/zoom pane (yaw 0, every point at `z = 0`): the pane is
    /// `size_px` physical px, world `(0, 0)` lands at `origin_px` (pane-local physical
    /// px), one world unit is `scale_px` px, every disc has radius `radius_px` and every
    /// edge half-width `edge_half_px`. Feed points as `[x, −y, 0]`: the shader projects
    /// `centre + (x, −y) · scale`, a y-down canvas projects `origin + (x, y) · zoom`.
    #[must_use]
    pub fn plane(size_px: [f32; 2], origin_px: [f32; 2], scale_px: f32, radius_px: f32, edge_half_px: f32, emissive: f32) -> Self {
        Self {
            viewport: [size_px[0].max(1.0), size_px[1].max(1.0), origin_px[0], origin_px[1]],
            // The node vertex shader's disc is `(3 + (z + 1) · 2.5) · shrink` = 5.5 · shrink at z = 0.
            cam: [0.0, scale_px, emissive, radius_px.max(0.5) / 5.5],
            edge: [edge_half_px.max(0.5), 0.6, 0.0, 0.0],
        }
    }
}

/// **A cloud model resident on the GPU** — points + edge indices uploaded once, plus
/// the bind group that reaches them. Hold it across frames; rebuild only when
/// `generation` moves.
///
/// The buffers are kept alive by this struct and reached only through `bg`, hence the
/// underscore names: they are owned, not read.
pub struct CloudBuffers {
    bg: wgpu::BindGroup,
    /// Points in the model (the node draw's instance count).
    pub node_count: u32,
    /// Edges in the model (the edge draw's instance count).
    pub edge_count: u32,
    /// The model version these buffers were built from. A host compares its own
    /// counter against this to decide whether to re-upload — the ONE place staleness
    /// can be caught, and the reason positions are not a public field anywhere.
    pub generation: u64,
    _pts: wgpu::Buffer,
    _edg: wgpu::Buffer,
}

/// Which source feeds the P1 scene pass. Private: it exists so
/// [`GraphGpu::render_src`] can be written once for both lanes instead of twinned.
enum Src<'a> {
    /// Screen-space instances the CPU built this frame.
    Instances(&'a GraphScene),
    /// Model-space points already resident on the GPU.
    Cloud(&'a CloudBuffers),
}

/// A graph to draw on the GPU lane: a background clear colour + the node + edge
/// instance batches (already projected to screen px by the caller).
#[derive(Clone, Default)]
pub struct GraphScene {
    /// Background clear colour (linear, `[r, g, b, a]`).
    pub background: [f32; 4],
    pub nodes: Vec<NodeInstance>,
    pub edges: Vec<EdgeInstance>,
}

/// Bloom post-stack tuning (threshold/knee/intensity/exposure). The defaults keep
/// sub-1.0 scene radiance untouched and bloom only the emissive graph.
#[derive(Clone, Copy, Debug)]
pub struct BloomParams {
    pub threshold: f32,
    pub knee: f32,
    pub intensity: f32,
    pub exposure: f32,
    /// Blur σ (texels) for the glow spread.
    pub sigma: f32,
}

impl Default for BloomParams {
    fn default() -> Self {
        Self { threshold: 1.0, knee: 0.6, intensity: 1.3, exposure: 1.0, sigma: 3.0 }
    }
}

impl BloomParams {
    /// Bloom disabled (intensity 0) — the A/B reference for the render-proof.
    #[must_use]
    pub fn off(self) -> Self {
        Self { intensity: 0.0, ..self }
    }
}

#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct SceneUniforms {
    viewport: [f32; 4],
}

#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct PostUniforms {
    params: [f32; 4],
}

/// The result of a headless GPU graph render — the read-back LDR framebuffer plus the
/// instance draw counts (the render-proof asserts on these as DATA).
pub struct GraphRender {
    pub width: u32,
    pub height: u32,
    /// Straight RGBA8, row-major (length `width*height*4`).
    pub rgba: Vec<u8>,
    /// Node instances actually issued to the draw call.
    pub node_instances: u32,
    /// Edge instances actually issued to the draw call.
    pub edge_instances: u32,
}

impl GraphRender {
    /// Mean per-pixel luminance (Rec.601) over the LDR frame — the energy measure the
    /// bloom A/B oracle compares (bloom on vs off).
    #[must_use]
    pub fn mean_luma(&self) -> f64 {
        let mut sum = 0.0f64;
        for px in self.rgba.as_chunks::<4>().0 {
            sum += 0.299 * px[0] as f64 + 0.587 * px[1] as f64 + 0.114 * px[2] as f64;
        }
        let n = (self.width * self.height) as f64;
        if n > 0.0 { sum / (n * 255.0) } else { 0.0 }
    }

    /// Luminance (0..1) of the pixel at `(x, y)` — for the additive-accumulation probe.
    #[must_use]
    pub fn luma_at(&self, x: u32, y: u32) -> f64 {
        if x >= self.width || y >= self.height {
            return 0.0;
        }
        let i = ((y * self.width + x) * 4) as usize;
        (0.299 * self.rgba[i] as f64 + 0.587 * self.rgba[i + 1] as f64 + 0.114 * self.rgba[i + 2] as f64) / 255.0
    }
}

/// The reusable graph GPU pipelines (node + edge instanced draws + the bloom post
/// stack). Built once from a device + the LDR target format; drive it with
/// [`GraphGpu::render`] (headless) — or, in the follow-up, from an `egui_wgpu`
/// callback.
pub struct GraphGpu {
    node_pipeline: wgpu::RenderPipeline,
    edge_pipeline: wgpu::RenderPipeline,
    bright_pipeline: wgpu::RenderPipeline,
    composite_pipeline: wgpu::RenderPipeline,
    scene_bgl: wgpu::BindGroupLayout,
    sample_bgl: wgpu::BindGroupLayout,
    composite_bgl: wgpu::BindGroupLayout,
    sampler: wgpu::Sampler,
    scene_u: wgpu::Buffer,
    post_u: wgpu::Buffer,
    /// The CLOUD pass: model-space points + edge indices live on the GPU and only the
    /// camera is written per frame. `None` until [`GraphGpu::new`] builds them — they
    /// are always built; the option is only so the layout can be borrowed.
    cloud_node_pipeline: wgpu::RenderPipeline,
    cloud_edge_pipeline: wgpu::RenderPipeline,
    cloud_bgl: wgpu::BindGroupLayout,
    cloud_u: wgpu::Buffer,
    /// Persistent scene bind group (`group(0) = { scene_u }`) for the node/edge passes.
    /// Held so the LIVE `egui_wgpu` callback ([`cloud`]) can encode the instanced draw
    /// without rebuilding a bind group per frame.
    scene_bg: wgpu::BindGroup,
    blur: GaussianBlur,
    ldr_format: wgpu::TextureFormat,
}

impl GraphGpu {
    /// Build the node/edge/bloom pipelines for an `ldr_format` colour target (the
    /// composite's output format — the host surface, or `Rgba8Unorm` headless).
    pub fn new(device: &wgpu::Device, ldr_format: wgpu::TextureFormat) -> Self {
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("graph3d_gpu"),
            source: wgpu::ShaderSource::Wgsl(crate::render::wgsl::GRAPHCLOUD_WGSL.into()),
        });

        // group(0) = { scene uniform } for the node/edge passes.
        let scene_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("graph3d_scene_bgl"),
            entries: &[uniform_entry(0, wgpu::ShaderStages::VERTEX)],
        });
        // group(0) = { post uniform, src tex, sampler } for the bright pass.
        let sample_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("graph3d_sample_bgl"),
            entries: &[
                uniform_entry(0, wgpu::ShaderStages::FRAGMENT),
                tex_entry(1),
                sampler_entry(2),
            ],
        });
        // group(0) = { post uniform, scene tex, sampler, bloom tex } for composite.
        let composite_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("graph3d_composite_bgl"),
            entries: &[
                uniform_entry(0, wgpu::ShaderStages::FRAGMENT),
                tex_entry(1),
                sampler_entry(2),
                tex_entry(3),
            ],
        });

        let additive = wgpu::BlendState {
            color: wgpu::BlendComponent {
                src_factor: wgpu::BlendFactor::One,
                dst_factor: wgpu::BlendFactor::One,
                operation: wgpu::BlendOperation::Add,
            },
            alpha: wgpu::BlendComponent {
                src_factor: wgpu::BlendFactor::One,
                dst_factor: wgpu::BlendFactor::One,
                operation: wgpu::BlendOperation::Add,
            },
        };

        let node_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("graph3d_node_pll"),
            bind_group_layouts: &[Some(&scene_bgl)],
            immediate_size: 0,
        });
        let node_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("graph3d_node_pipeline"),
            layout: Some(&node_pll),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("node_vs"),
                buffers: &[Some(wgpu::VertexBufferLayout {
                    array_stride: std::mem::size_of::<NodeInstance>() as u64,
                    step_mode: wgpu::VertexStepMode::Instance,
                    // EXPLICIT offsets: `NodeInstance` has a `_pad` after `radius`, so `color`
                    // lives at byte 16 — the auto-packed `vertex_attr_array` would place it at
                    // 12 (over the pad), shifting every channel and zeroing red. (Was a latent
                    // bug the luma-only render-proof never caught.)
                    attributes: &[
                        wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 0, shader_location: 0 },
                        wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32, offset: 8, shader_location: 1 },
                        wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x4, offset: 16, shader_location: 2 },
                    ],
                })],
                compilation_options: Default::default(),
            },
            primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
            depth_stencil: None,
            multisample: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("node_fs"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: OFFSCREEN_FORMAT,
                    blend: Some(additive),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            multiview_mask: None,
            cache: None,
        });

        let edge_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("graph3d_edge_pll"),
            bind_group_layouts: &[Some(&scene_bgl)],
            immediate_size: 0,
        });
        let edge_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("graph3d_edge_pipeline"),
            layout: Some(&edge_pll),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("edge_vs"),
                buffers: &[Some(wgpu::VertexBufferLayout {
                    array_stride: std::mem::size_of::<EdgeInstance>() as u64,
                    step_mode: wgpu::VertexStepMode::Instance,
                    // EXPLICIT offsets: `EdgeInstance` has a `_pad` after `width`, so `color`
                    // lives at byte 24 (auto-packing would put it at 20 over the pad).
                    attributes: &[
                        wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 0, shader_location: 0 },
                        wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 8, shader_location: 1 },
                        wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32, offset: 16, shader_location: 2 },
                        wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x4, offset: 24, shader_location: 3 },
                    ],
                })],
                compilation_options: Default::default(),
            },
            primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
            depth_stencil: None,
            multisample: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("edge_fs"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: OFFSCREEN_FORMAT,
                    blend: Some(additive),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            multiview_mask: None,
            cache: None,
        });

        let bright_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("graph3d_bright_pll"),
            bind_group_layouts: &[Some(&sample_bgl)],
            immediate_size: 0,
        });
        let bright_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("graph3d_bright_pipeline"),
            layout: Some(&bright_pll),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("post_vs"),
                buffers: &[],
                compilation_options: Default::default(),
            },
            primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
            depth_stencil: None,
            multisample: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("bright_fs"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: OFFSCREEN_FORMAT,
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            multiview_mask: None,
            cache: None,
        });

        let composite_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("graph3d_composite_pll"),
            bind_group_layouts: &[Some(&composite_bgl)],
            immediate_size: 0,
        });
        let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("graph3d_composite_pipeline"),
            layout: Some(&composite_pll),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("post_vs"),
                buffers: &[],
                compilation_options: Default::default(),
            },
            primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
            depth_stencil: None,
            multisample: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("composite_fs"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: ldr_format,
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            multiview_mask: None,
            cache: None,
        });

        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("graph3d_sampler"),
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            ..Default::default()
        });
        let mkbuf = |label: &str, size: u64| device.create_buffer(&wgpu::BufferDescriptor {
            label: Some(label),
            size,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let scene_u = mkbuf("graph3d_scene_u", std::mem::size_of::<SceneUniforms>() as u64);

        // ── the CLOUD pass: uniform + two read-only STORAGE buffers, vertex stage ──
        // Storage in the vertex stage is what lets the shader read `PTS[instance_index]`
        // and, for an edge, `PTS[EDG[i].x]` — a random gather a vertex buffer cannot do.
        // It is WebGPU-core, not an extension; only the WebGL2 downlevel profile forbids
        // it, and LAW 4 rules WebGL out of this constellation.
        let storage_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
            binding,
            visibility: wgpu::ShaderStages::VERTEX,
            ty: wgpu::BindingType::Buffer {
                ty: wgpu::BufferBindingType::Storage { read_only: true },
                has_dynamic_offset: false,
                min_binding_size: None,
            },
            count: None,
        };
        let cloud_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("graph3d_cloud_bgl"),
            entries: &[uniform_entry(0, wgpu::ShaderStages::VERTEX), storage_entry(1), storage_entry(2)],
        });
        let cloud_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("graph3d_cloud_pll"),
            bind_group_layouts: &[Some(&cloud_bgl)],
            immediate_size: 0,
        });
        // Same fragment shaders, same additive blend, same target as the instance pass —
        // only the VERTEX stage differs, which is the whole point: the picture is the
        // same picture, computed one step earlier.
        let mk_cloud_pipeline = |label: &str, vs: &str, fs: &str| {
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some(label),
                layout: Some(&cloud_pll),
                vertex: wgpu::VertexState {
                    module: &shader,
                    entry_point: Some(vs),
                    buffers: &[],
                    compilation_options: Default::default(),
                },
                fragment: Some(wgpu::FragmentState {
                    module: &shader,
                    entry_point: Some(fs),
                    targets: &[Some(wgpu::ColorTargetState {
                        format: OFFSCREEN_FORMAT,
                        blend: Some(additive),
                        write_mask: wgpu::ColorWrites::ALL,
                    })],
                    compilation_options: Default::default(),
                }),
                primitive: wgpu::PrimitiveState::default(),
                depth_stencil: None,
                // Through the ONE writer, not a fresh `MultisampleState::default()` —
                // `one_msaa_writer.rs` caught exactly that here and is right to: a sample
                // count stated independently per pipeline makes any change an all-sites
                // edit, and a missed site is a wgpu VALIDATION failure, not a downgrade.
                multisample: crate::render::gpu::msaa_state(),
                multiview_mask: None,
                cache: None,
            })
        };
        let cloud_node_pipeline = mk_cloud_pipeline("graph3d_cloud_node_pipeline", "cloud_node_vs", "node_fs");
        let cloud_edge_pipeline = mk_cloud_pipeline("graph3d_cloud_edge_pipeline", "cloud_edge_vs", "edge_fs");
        let cloud_u = mkbuf("graph3d_cloud_u", std::mem::size_of::<CloudCamera>() as u64);
        let scene_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("graph3d_scene_bg"),
            layout: &scene_bgl,
            entries: &[wgpu::BindGroupEntry { binding: 0, resource: scene_u.as_entire_binding() }],
        });
        Self {
            node_pipeline,
            edge_pipeline,
            cloud_node_pipeline,
            cloud_edge_pipeline,
            cloud_bgl,
            cloud_u,
            bright_pipeline,
            composite_pipeline,
            scene_bgl,
            sample_bgl,
            composite_bgl,
            sampler,
            scene_u,
            post_u: mkbuf("graph3d_post_u", std::mem::size_of::<PostUniforms>() as u64),
            scene_bg,
            blur: GaussianBlur::new(device, OFFSCREEN_FORMAT),
            ldr_format,
        }
    }

    /// Write the viewport uniform (physical px) — the live callback ([`cloud`]) calls
    /// this once per frame before [`encode_scene`](Self::encode_scene).
    pub fn write_viewport(&self, queue: &wgpu::Queue, w: u32, h: u32) {
        queue.write_buffer(&self.scene_u, 0, bytemuck::bytes_of(&SceneUniforms { viewport: [w.max(1) as f32, h.max(1) as f32, 0.0, 0.0] }));
    }

    /// **Live-lane scene encode** — record the instanced emissive edge+node draw into
    /// `color_view` (an [`OFFSCREEN_FORMAT`] HDR target) on the caller's `encoder`, with
    /// the additive-blend accumulation the offscreen path uses. This is the SAME
    /// node/edge pipelines the headless [`render`](Self::render) drives (L5 reuse — no
    /// second renderer); the caller (the `egui_wgpu` callback) then blits `color_view`
    /// into egui's pass. Instance buffers are owned by the caller (kept alive until the
    /// frame is submitted). Edges draw first (under the nodes); both accumulate.
    #[allow(clippy::too_many_arguments)]
    pub fn encode_scene(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        color_view: &wgpu::TextureView,
        node_buf: Option<&wgpu::Buffer>,
        node_count: u32,
        edge_buf: Option<&wgpu::Buffer>,
        edge_count: u32,
        background: [f32; 4],
    ) {
        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("graph3d_cloud_scene_pass"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view: color_view,
                resolve_target: None,
                depth_slice: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color {
                        r: background[0] as f64,
                        g: background[1] as f64,
                        b: background[2] as f64,
                        a: background[3] as f64,
                    }),
                    store: wgpu::StoreOp::Store,
                },
            })],
            depth_stencil_attachment: None,
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });
        if let (Some(eb), true) = (edge_buf, edge_count > 0) {
            pass.set_pipeline(&self.edge_pipeline);
            pass.set_bind_group(0, &self.scene_bg, &[]);
            pass.set_vertex_buffer(0, eb.slice(..));
            pass.draw(0..6, 0..edge_count);
        }
        if let (Some(nb), true) = (node_buf, node_count > 0) {
            pass.set_pipeline(&self.node_pipeline);
            pass.set_bind_group(0, &self.scene_bg, &[]);
            pass.set_vertex_buffer(0, nb.slice(..));
            pass.draw(0..6, 0..node_count);
        }
    }

    /// **Upload a cloud model to the GPU** — the once-per-change step. Hold the result
    /// and re-use it every frame; rebuild it only when `generation` moves.
    ///
    /// The buffers are made WITHOUT `mappedAtCreation` and filled through the queue
    /// ([`checked_buffer`]): on SwiftShader (lane L, loki 2026-10-02) Chrome THREW from a
    /// mapped 1.6 MB `createBuffer` ("too large for the implementation") and wgpu's
    /// browser backend unwraps that throw — the page panicked. `Err(why)` when the
    /// device's limits refuse a buffer; the caller draws nothing and says why.
    ///
    /// # Errors
    /// A buffer larger than the device's `max_buffer_size` / `max_storage_buffer_binding_size`.
    pub fn upload_cloud(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        points: &[CloudPoint],
        edges: &[CloudEdge],
        generation: u64,
    ) -> Result<CloudBuffers, String> {
        // wgpu refuses a zero-sized binding, so an empty side gets one dead element and
        // a count of 0 — it is bound but never drawn.
        let dead_pt = [CloudPoint::default()];
        let dead_ed: [CloudEdge; 1] = [[0, 0]];
        let pts = checked_buffer(
            device,
            queue,
            "graph3d_cloud_pts",
            bytemuck::cast_slice(if points.is_empty() { &dead_pt } else { points }),
            wgpu::BufferUsages::STORAGE,
        )?;
        let edg = checked_buffer(
            device,
            queue,
            "graph3d_cloud_edg",
            bytemuck::cast_slice(if edges.is_empty() { &dead_ed } else { edges }),
            wgpu::BufferUsages::STORAGE,
        )?;
        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("graph3d_cloud_bg"),
            layout: &self.cloud_bgl,
            entries: &[
                wgpu::BindGroupEntry { binding: 0, resource: self.cloud_u.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 1, resource: pts.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 2, resource: edg.as_entire_binding() },
            ],
        });
        Ok(CloudBuffers {
            bg,
            node_count: points.len() as u32,
            edge_count: edges.len() as u32,
            generation,
            _pts: pts,
            _edg: edg,
        })
    }

    /// **Write the per-frame camera** — 48 bytes, whatever the model size. This is the
    /// entire per-frame CPU cost of the cloud lane.
    pub fn write_cloud_camera(&self, queue: &wgpu::Queue, cam: &CloudCamera) {
        queue.write_buffer(&self.cloud_u, 0, bytemuck::bytes_of(cam));
    }

    /// **Live-lane CLOUD encode** — the resident-model twin of
    /// [`encode_scene`](Self::encode_scene). Write the camera first
    /// ([`write_cloud_camera`](Self::write_cloud_camera)); the points and edges are
    /// already on the GPU, so this frame sends no geometry at all.
    pub fn encode_cloud(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        color_view: &wgpu::TextureView,
        cloud: &CloudBuffers,
        background: [f32; 4],
    ) {
        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("graph3d_cloud_model_pass"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view: color_view,
                resolve_target: None,
                depth_slice: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color {
                        r: background[0] as f64,
                        g: background[1] as f64,
                        b: background[2] as f64,
                        a: background[3] as f64,
                    }),
                    store: wgpu::StoreOp::Store,
                },
            })],
            depth_stencil_attachment: None,
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });
        if cloud.edge_count > 0 {
            pass.set_pipeline(&self.cloud_edge_pipeline);
            pass.set_bind_group(0, &cloud.bg, &[]);
            pass.draw(0..6, 0..cloud.edge_count);
        }
        if cloud.node_count > 0 {
            pass.set_pipeline(&self.cloud_node_pipeline);
            pass.set_bind_group(0, &cloud.bg, &[]);
            pass.draw(0..6, 0..cloud.node_count);
        }
    }

    /// Render `scene` at `w×h` px into an LDR texture and read it back. `bloom` tunes
    /// the post stack (use [`BloomParams::off`] for the A/B reference). Returns the
    /// LDR frame + the instance draw counts.
    pub fn render(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        w: u32,
        h: u32,
        scene: &GraphScene,
        bloom: BloomParams,
    ) -> GraphRender {
        self.render_src(device, queue, w, h, Src::Instances(scene), scene.background, bloom)
    }

    /// The CLOUD twin of [`render`](Self::render): the same offscreen + bloom chain, fed
    /// by model-space points already on the GPU. Write the camera first
    /// ([`write_cloud_camera`](Self::write_cloud_camera)).
    #[allow(clippy::too_many_arguments)] // public signature hosts call: kept as is
    pub fn render_cloud(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        w: u32,
        h: u32,
        cloud: &CloudBuffers,
        background: [f32; 4],
        bloom: BloomParams,
    ) -> GraphRender {
        self.render_src(device, queue, w, h, Src::Cloud(cloud), background, bloom)
    }

    /// ONE body, two sources (LAW 5). Everything but the P1 draw is identical between
    /// the instance lane and the cloud lane — offscreen, bright pass, blur, composite,
    /// readback — so it is written once and the source is a parameter. A second copy of
    /// this function is exactly the twin that would let the two lanes drift into
    /// rendering different pictures, which is the one thing the parity test must be able
    /// to trust.
    #[allow(clippy::too_many_arguments)] // the private twin of `render_cloud`'s public signature
    fn render_src(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        w: u32,
        h: u32,
        src: Src<'_>,
        background: [f32; 4],
        bloom: BloomParams,
    ) -> GraphRender {
        use wgpu::util::DeviceExt;
        // never a texture larger than the device can make (a panic otherwise)
        let (w, h) = super::texture_extent(device, w, h);

        // Uniforms.
        queue.write_buffer(&self.scene_u, 0, bytemuck::bytes_of(&SceneUniforms { viewport: [w as f32, h as f32, 0.0, 0.0] }));
        queue.write_buffer(
            &self.post_u,
            0,
            bytemuck::bytes_of(&PostUniforms { params: [bloom.threshold, bloom.knee, bloom.intensity, bloom.exposure] }),
        );

        // Shared HDR scene offscreen (facett-core L0).
        let mut offscreen = OffscreenColorDepth::new(device, self.ldr_format);
        offscreen.ensure(device, w, h);
        let scene_view = offscreen.color_view().expect("offscreen ensured");

        // Bright-pass target + LDR present target (owned here).
        let bright = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("graph3d_bright"),
            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: OFFSCREEN_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        let bright_view = bright.create_view(&Default::default());
        let ldr = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("graph3d_ldr"),
            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: self.ldr_format,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });
        let ldr_view = ldr.create_view(&Default::default());

        self.blur.ensure(device, queue, w, h, bloom.sigma);

        // Instance buffers — only the instance lane has any; the cloud lane's points are
        // already resident and were uploaded once.
        let scene = match src {
            Src::Instances(s) => Some(s),
            Src::Cloud(_) => None,
        };
        let node_buf = scene.is_some_and(|s| !s.nodes.is_empty()).then(|| {
            let scene = scene.expect("checked");
            device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("graph3d_nodes"),
                contents: bytemuck::cast_slice(&scene.nodes),
                usage: wgpu::BufferUsages::VERTEX,
            })
        });
        let edge_buf = scene.is_some_and(|s| !s.edges.is_empty()).then(|| {
            let scene = scene.expect("checked");
            device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("graph3d_edges"),
                contents: bytemuck::cast_slice(&scene.edges),
                usage: wgpu::BufferUsages::VERTEX,
            })
        });

        // Bind groups.
        let scene_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("graph3d_scene_bg"),
            layout: &self.scene_bgl,
            entries: &[wgpu::BindGroupEntry { binding: 0, resource: self.scene_u.as_entire_binding() }],
        });
        let bright_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("graph3d_bright_bg"),
            layout: &self.sample_bgl,
            entries: &[
                wgpu::BindGroupEntry { binding: 0, resource: self.post_u.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(scene_view) },
                wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.sampler) },
            ],
        });

        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("graph3d_enc") });

        // ── P1: scene pass — additive emissive nodes + edges into the HDR offscreen ──
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("graph3d_scene_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: scene_view,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: background[0] as f64,
                            g: background[1] as f64,
                            b: background[2] as f64,
                            a: background[3] as f64,
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            // Edges first (under the nodes), then nodes — both accumulate additively, and
            // the ORDER is cosmetic only: the blend is `ONE,ONE`, which commutes.
            match src {
                Src::Instances(sc) => {
                    if let Some(eb) = &edge_buf {
                        pass.set_pipeline(&self.edge_pipeline);
                        pass.set_bind_group(0, &scene_bg, &[]);
                        pass.set_vertex_buffer(0, eb.slice(..));
                        pass.draw(0..6, 0..sc.edges.len() as u32);
                    }
                    if let Some(nb) = &node_buf {
                        pass.set_pipeline(&self.node_pipeline);
                        pass.set_bind_group(0, &scene_bg, &[]);
                        pass.set_vertex_buffer(0, nb.slice(..));
                        pass.draw(0..6, 0..sc.nodes.len() as u32);
                    }
                }
                Src::Cloud(cb) => {
                    // No vertex buffer at all: the shader reads PTS[instance_index].
                    if cb.edge_count > 0 {
                        pass.set_pipeline(&self.cloud_edge_pipeline);
                        pass.set_bind_group(0, &cb.bg, &[]);
                        pass.draw(0..6, 0..cb.edge_count);
                    }
                    if cb.node_count > 0 {
                        pass.set_pipeline(&self.cloud_node_pipeline);
                        pass.set_bind_group(0, &cb.bg, &[]);
                        pass.draw(0..6, 0..cb.node_count);
                    }
                }
            }
        }

        // ── P2: bright pass — isolate >threshold radiance into `bright` ──
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("graph3d_bright_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &bright_view,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_pipeline(&self.bright_pipeline);
            pass.set_bind_group(0, &bright_bg, &[]);
            pass.draw(0..3, 0..1);
        }

        // ── P3: blur the bright pass (separable Gaussian; facett-core L0) ──
        let bloom_view = self.blur.blur(device, &mut encoder, &bright_view).expect("blur ensured");

        // ── P4: composite — scene + bloom·intensity → tonemap → LDR ──
        let composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("graph3d_composite_bg"),
            layout: &self.composite_bgl,
            entries: &[
                wgpu::BindGroupEntry { binding: 0, resource: self.post_u.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(scene_view) },
                wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.sampler) },
                wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(bloom_view) },
            ],
        });
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("graph3d_composite_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &ldr_view,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_pipeline(&self.composite_pipeline);
            pass.set_bind_group(0, &composite_bg, &[]);
            pass.draw(0..3, 0..1);
        }

        // Read back the LDR present target.
        let bytes_per_pixel = 4u32;
        let unpadded = w * bytes_per_pixel;
        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
        let padded = unpadded.div_ceil(align) * align;
        let readback = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("graph3d_readback"),
            size: (padded * h) as u64,
            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
            mapped_at_creation: false,
        });
        encoder.copy_texture_to_buffer(
            wgpu::TexelCopyTextureInfo {
                texture: &ldr,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            wgpu::TexelCopyBufferInfo {
                buffer: &readback,
                layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded), rows_per_image: Some(h) },
            },
            wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
        );
        queue.submit(Some(encoder.finish()));

        let slice = readback.slice(..);
        let (tx, rx) = std::sync::mpsc::channel();
        slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
        device.poll(wgpu::PollType::wait_indefinitely()).ok();
        rx.recv().ok();

        let data = slice.get_mapped_range().expect("wgpu: get_mapped_range on a mapped buffer");
        let mut rgba = Vec::with_capacity((w * h * 4) as usize);
        for row in 0..h {
            let start = (row * padded) as usize;
            rgba.extend_from_slice(&data[start..start + unpadded as usize]);
        }
        drop(data);
        readback.unmap();

        GraphRender {
            width: w,
            height: h,
            rgba,
            // The draw counts, whichever lane produced them — the render-proof asserts on
            // these as DATA, so they must mean the same thing on both.
            node_instances: match src {
                Src::Instances(sc) => sc.nodes.len() as u32,
                Src::Cloud(cb) => cb.node_count,
            },
            edge_instances: match src {
                Src::Instances(sc) => sc.edges.len() as u32,
                Src::Cloud(cb) => cb.edge_count,
            },
        }
    }
}

/// **Why a `bytes`-long buffer cannot be made under `limits`** (`None` = it can) — the
/// pure half of [`checked_buffer`], testable without a device. A `STORAGE` buffer is
/// bound whole, so it must also fit `max_storage_buffer_binding_size`.
#[must_use]
pub fn buffer_refusal(label: &str, bytes: u64, usage: wgpu::BufferUsages, limits: &wgpu::Limits) -> Option<String> {
    if bytes > limits.max_buffer_size {
        return Some(format!(
            "{label}: {bytes} B is over this device's max_buffer_size ({} B) — nothing drawn",
            limits.max_buffer_size
        ));
    }
    if usage.contains(wgpu::BufferUsages::STORAGE) && bytes > limits.max_storage_buffer_binding_size {
        return Some(format!(
            "{label}: {bytes} B is over this device's max_storage_buffer_binding_size ({} B) — nothing drawn",
            limits.max_storage_buffer_binding_size
        ));
    }
    None
}

/// **A buffer holding `contents`, or why not** — created WITHOUT `mappedAtCreation`
/// (the call Chrome throws from on SwiftShader, which wgpu's browser backend turns into
/// a panic) and filled with `queue.write_buffer`, which lands before the next submit's
/// commands. Checked against the device's limits first ([`buffer_refusal`]).
///
/// # Errors
/// [`buffer_refusal`]'s refusal.
pub fn checked_buffer(
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    label: &str,
    contents: &[u8],
    usage: wgpu::BufferUsages,
) -> Result<wgpu::Buffer, String> {
    let align = wgpu::COPY_BUFFER_ALIGNMENT;
    let size = (contents.len() as u64).div_ceil(align).max(1) * align;
    if let Some(why) = buffer_refusal(label, size, usage, &device.limits()) {
        return Err(why);
    }
    let buf = device.create_buffer(&wgpu::BufferDescriptor {
        label: Some(label),
        size,
        usage: usage | wgpu::BufferUsages::COPY_DST,
        mapped_at_creation: false,
    });
    if contents.len() as u64 == size {
        queue.write_buffer(&buf, 0, contents);
    } else {
        let mut padded = contents.to_vec();
        padded.resize(size as usize, 0);
        queue.write_buffer(&buf, 0, &padded);
    }
    Ok(buf)
}

/// **The LIVE `egui_wgpu` lane** — the follow-up the module header promised: wire the
/// same [`GraphGpu`] instanced node/edge pipelines into an `egui_wgpu` paint callback
/// so a host (korp) draws a 30 000-node cloud as GPU **instances** instead of egui
/// tessellating 30 000 discs + edges on ONE CPU core every frame (the measured
/// single-core rotation wall — see `examples/rotate_bench.rs`). Mirrors
/// `facett_graph3d::logo3d::gpu`: a persistent `*Renderer` in `callback_resources`, installed
/// once via [`install_cloud_renderer`], driven by a [`CallbackTrait`](egui_wgpu::CallbackTrait)
/// that draws into the shared [`OffscreenColorDepth`] colour target and blits it into
/// egui's pass. Behind `with_gpu(true)` opt-in — the CPU painter (the snapshot
/// golden path) is the untouched default fallback.
pub mod cloud {
    use super::{GraphGpu, GraphScene};
    use egui_wgpu::wgpu;
    use crate::render::gpu::offscreen::OffscreenColorDepth;

    /// Persistent GPU resources for the live cloud lane: the reused [`GraphGpu`]
    /// pipelines + a shared offscreen colour target (+ blit) + the per-frame instance
    /// buffers (kept here so they outlive the frame's submit).
    pub struct CloudRenderer {
        gpu: GraphGpu,
        offscreen: OffscreenColorDepth,
        node_buf: Option<wgpu::Buffer>,
        edge_buf: Option<wgpu::Buffer>,
        node_count: u32,
        edge_count: u32,
        /// **The RESIDENT model** for the cloud lane — points and edge indices that
        /// live on the GPU across frames. This is the whole optimisation: the instance
        /// buffers above are rebuilt and re-sent every frame, these are not.
        cloud: Option<super::CloudBuffers>,
    }

    impl CloudRenderer {
        pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
            Self {
                gpu: GraphGpu::new(device, target_format),
                offscreen: OffscreenColorDepth::new(device, target_format),
                node_buf: None,
                edge_buf: None,
                node_count: 0,
                edge_count: 0,
                cloud: None,
            }
        }
    }

    /// **One live frame of the CLOUD lane** — the camera, and the model only when it
    /// changed.
    ///
    /// `model` is `Some` on the frames where the host's `generation` moved and `None`
    /// on every other frame, which is nearly all of them: a spin changes the camera and
    /// nothing else. So a steady-state frame carries 48 bytes of `cam` and two integers,
    /// against the ~96 MB of screen-space instances [`CloudPaintCallback`] rebuilds at a
    /// million nodes.
    ///
    /// The host owns the comparison because it owns the model: keep the last
    /// `generation` you uploaded, and pass `Some` exactly when it differs.
    pub struct CloudModelPaintCallback {
        /// The model, present only when it changed. `(points, edge index pairs)`.
        pub model: Option<(Vec<super::CloudPoint>, Vec<super::CloudEdge>)>,
        /// The version `model` corresponds to — stored on upload so a later frame can
        /// tell whether the resident copy is still the right one.
        pub generation: u64,
        /// The per-frame camera. This is the only thing that moves during a spin.
        pub cam: super::CloudCamera,
        pub background: [f32; 4],
        pub px_w: u32,
        pub px_h: u32,
    }

    impl egui_wgpu::CallbackTrait for CloudModelPaintCallback {
        fn prepare(
            &self,
            device: &wgpu::Device,
            queue: &wgpu::Queue,
            sd: &egui_wgpu::ScreenDescriptor,
            encoder: &mut wgpu::CommandEncoder,
            resources: &mut egui_wgpu::CallbackResources,
        ) -> Vec<wgpu::CommandBuffer> {
            let Some(r) = resources.get_mut::<CloudRenderer>() else {
                return vec![];
            };
            let (w, h) = if self.px_w > 0 && self.px_h > 0 {
                (self.px_w, self.px_h)
            } else {
                (sd.size_in_pixels[0], sd.size_in_pixels[1])
            };
            r.offscreen.ensure(device, w, h);

            // UPLOAD ONLY WHEN THE MODEL MOVED. `model` is `None` on a spin frame, and a
            // resident copy at the right generation is reused untouched — the entire
            // point of the lane. A `Some` with an unchanged generation still re-uploads,
            // because the host only sends one when it means it.
            if let Some((pts, eds)) = self.model.as_ref() {
                // FAIL SOFT: a device that refuses the buffers draws nothing for this
                // model and says why in the state (`render::lane::gpu_error`), never a
                // panic and never the previous model under the new one's name.
                match r.gpu.upload_cloud(device, queue, pts, eds, self.generation) {
                    Ok(c) => r.cloud = Some(c),
                    Err(why) => {
                        r.cloud = None;
                        crate::render::lane::note_gpu_error(std::any::type_name::<CloudRenderer>(), &why);
                    }
                }
            }
            let Some(cloud) = r.cloud.as_ref() else {
                // No model has ever been supplied — nothing to draw, and drawing the
                // instance lane's leftovers instead would be worse than an empty pane.
                return vec![];
            };
            r.gpu.write_cloud_camera(queue, &self.cam);
            let Some(color_view) = r.offscreen.color_view() else {
                return vec![];
            };
            r.gpu.encode_cloud(encoder, color_view, cloud, self.background);
            crate::render::lane::note_frame(std::any::type_name::<CloudRenderer>());
            vec![]
        }

        fn paint(
            &self,
            info: egui::PaintCallbackInfo,
            pass: &mut wgpu::RenderPass<'static>,
            resources: &egui_wgpu::CallbackResources,
        ) {
            let Some(r) = resources.get::<CloudRenderer>() else { return };
            if !r.offscreen.ready() {
                return;
            }
            // The same blit the instance lane uses — one offscreen, one composite path.
            let _scissor = r.offscreen.blit(&info, pass);
        }
    }

    /// One live cloud frame: the projected [`GraphScene`] + the pane size in physical px.
    pub struct CloudPaintCallback {
        pub scene: GraphScene,
        pub px_w: u32,
        pub px_h: u32,
    }

    impl egui_wgpu::CallbackTrait for CloudPaintCallback {
        fn prepare(
            &self,
            device: &wgpu::Device,
            queue: &wgpu::Queue,
            sd: &egui_wgpu::ScreenDescriptor,
            encoder: &mut wgpu::CommandEncoder,
            resources: &mut egui_wgpu::CallbackResources,
        ) -> Vec<wgpu::CommandBuffer> {
            let Some(r) = resources.get_mut::<CloudRenderer>() else {
                return vec![];
            };
            let (w, h) = if self.px_w > 0 && self.px_h > 0 { (self.px_w, self.px_h) } else { (sd.size_in_pixels[0], sd.size_in_pixels[1]) };
            r.offscreen.ensure(device, w, h);
            r.gpu.write_viewport(queue, w, h);
            // (Re)build the instance buffers for THIS frame — kept on the persistent
            // renderer so they stay alive until egui submits the encoder.
            // Fail soft, as the resident lane: a refused buffer draws nothing and is said.
            let make = |label: &str, bytes: &[u8]| {
                if bytes.is_empty() {
                    return None;
                }
                super::checked_buffer(device, queue, label, bytes, wgpu::BufferUsages::VERTEX)
                    .map_err(|why| crate::render::lane::note_gpu_error(std::any::type_name::<CloudRenderer>(), &why))
                    .ok()
            };
            r.node_buf = make("graph3d_cloud_nodes", bytemuck::cast_slice(&self.scene.nodes));
            r.edge_buf = make("graph3d_cloud_edges", bytemuck::cast_slice(&self.scene.edges));
            r.node_count = self.scene.nodes.len() as u32;
            r.edge_count = self.scene.edges.len() as u32;
            let Some(color_view) = r.offscreen.color_view() else {
                return vec![];
            };
            // Immutable co-borrows of distinct fields (gpu / offscreen / *_buf) — the
            // instanced draw the CPU no longer tessellates.
            r.gpu.encode_scene(
                encoder,
                color_view,
                r.node_buf.as_ref(),
                r.node_count,
                r.edge_buf.as_ref(),
                r.edge_count,
                self.scene.background,
            );
            // LANE ATTRIBUTION (`crate::render::lane`): the instanced scene is encoded
            // against a real device, after every fail-safe early-return above. Counted
            // so an About box can say IN USE for this lane instead of leaving the whole
            // app's answer at `unknown` — korp installs THIS renderer beside the map's,
            // and one silent lane is enough to make the honest answer unknowable.
            crate::render::lane::note_frame(std::any::type_name::<CloudRenderer>());
            vec![]
        }

        fn paint(
            &self,
            info: egui::PaintCallbackInfo,
            render_pass: &mut wgpu::RenderPass<'static>,
            resources: &egui_wgpu::CallbackResources,
        ) {
            let Some(r) = resources.get::<CloudRenderer>() else { return };
            if !r.offscreen.ready() {
                return;
            }
            // Blit the instanced HDR cloud into egui's pass, scissored to the widget.
            let _scissor = r.offscreen.blit(&info, render_pass);
        }
    }

    /// Has a host installed the cloud renderer in this process?
    ///
    /// This exists because the GPU lane is **fail-dangerous** without it:
    /// `Graph3D::draw_gpu_cloud` returning `true` makes the view SKIP the CPU
    /// tessellation of the whole node/edge cloud. If a host called
    /// `with_gpu(true)` but never called [`install_cloud_renderer`], the paint
    /// callback finds no `CloudRenderer`, draws nothing — and the CPU painter has
    /// already been skipped, so the pane renders **BLANK**. That is a silent
    /// black 3D graph, and neither the pixel nor the state oracle would blame the
    /// missing install.
    ///
    /// So the fast path is gated on this flag: opt in *and* install, or the CPU
    /// painter draws unchanged. Process-global (the renderer lives in the one
    /// `RenderState` a host builds at startup) and monotonic — installing is a
    /// startup act, never undone.
    ///
    /// **The flag is the lane registry's**, not a private copy: this lane's
    /// [`crate::render::lane::installed_flag`], raised by the shared
    /// `crate::render::gpu::install_renderer` like every other lane's — so
    /// `lane::is_installed(type_name::<CloudRenderer>())` and this function cannot
    /// disagree (it used to be a second `AtomicBool` beside the registry, LAW #5).
    /// The flag reference is looked up once and cached, so the per-frame read stays
    /// one atomic load.
    #[must_use]
    pub fn cloud_renderer_installed() -> bool {
        installed_flag().load(std::sync::atomic::Ordering::Acquire)
    }

    /// This lane's installed flag in the [`crate::render::lane`] registry, cached.
    fn installed_flag() -> &'static std::sync::atomic::AtomicBool {
        static FLAG: std::sync::OnceLock<&'static std::sync::atomic::AtomicBool> =
            std::sync::OnceLock::new();
        FLAG.get_or_init(|| {
            crate::render::lane::installed_flag(std::any::type_name::<CloudRenderer>())
        })
    }

    /// The generation of the model the resident lane last uploaded (`0` = none). The
    /// renderer keeps ONE resident model, so a pane must re-send its model whenever this
    /// is not its own generation — another pane (another tab) may have replaced it. A
    /// pane that only remembered its own last upload would draw the OTHER pane's cloud
    /// after a tab switch (2026-10-01: `Graphs` ↔ `lineage` ↔ `Universe` at 100k).
    #[must_use]
    pub fn resident_generation() -> u64 {
        RESIDENT_GENERATION.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// See [`resident_generation`].
    static RESIDENT_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

    /// Above this many nodes the 2-D panes `facett-graphpan` and `facett-graphview`'s
    /// `DecoratedGraphView` paint their GPU cloud on the RESIDENT lane (a [`ResidentSlot`]);
    /// at or below it they keep the per-frame instance lane ([`draw_cloud`]), which colours
    /// each edge by its own colour. The same cut `facett-graphnav` makes at `SCALE_NODES`.
    pub const RESIDENT_ABOVE: usize = 5_000;

    static NEXT_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);

    /// A model generation no other pane in this process holds — for a pane that numbers
    /// its own model changes instead of hashing its model. Bit 62 set, so it cannot equal
    /// the `0` "nothing uploaded" of [`resident_generation`].
    #[must_use]
    pub fn fresh_generation() -> u64 {
        (1u64 << 62) | NEXT_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
    }

    /// **One pane's claim on the resident model** — the upload-once bookkeeping every
    /// resident pane needs, written once (graphnav and the demo's Universe carry their own
    /// copy of the same two lines, from before this existed).
    ///
    /// [`draw`](Self::draw) sends the model exactly when this pane's last upload is not
    /// `generation`, or when another pane replaced the ONE resident model since
    /// ([`resident_generation`]); every other frame sends the 48-byte camera alone. The
    /// counters say what happened, so a test (and a pane's `state_json`) can assert
    /// "a camera-only frame uploaded nothing" instead of trusting it.
    #[derive(Debug, Default)]
    pub struct ResidentSlot {
        uploaded: std::cell::Cell<u64>,
        uploads: std::cell::Cell<u64>,
        frames: std::cell::Cell<u64>,
    }

    impl ResidentSlot {
        /// Would a frame at `generation` send the model?
        #[must_use]
        pub fn needs_upload(&self, generation: u64) -> bool {
            self.uploaded.get() != generation || resident_generation() != generation
        }

        /// Push the resident cloud for `rect`: the camera every frame, `build()` (the
        /// model) only when [`needs_upload`](Self::needs_upload). `false` — and `build`
        /// never runs — when no renderer is installed or the rect has no pixels, so the
        /// caller's CPU painter draws, exactly as with [`draw_cloud`].
        pub fn draw(
            &self,
            ui: &egui::Ui,
            rect: egui::Rect,
            generation: u64,
            build: impl FnOnce() -> (Vec<super::CloudPoint>, Vec<super::CloudEdge>),
            cam: super::CloudCamera,
        ) -> bool {
            if !cloud_renderer_installed() {
                return false;
            }
            let ppp = ui.ctx().pixels_per_point();
            if !((rect.width() * ppp).round() >= 1.0 && (rect.height() * ppp).round() >= 1.0) {
                return false;
            }
            let upload = self.needs_upload(generation);
            let drew = draw_cloud_model(ui, rect, generation, upload.then(build), cam);
            if drew {
                self.frames.set(self.frames.get() + 1);
                if upload {
                    self.uploaded.set(generation);
                    self.uploads.set(self.uploads.get() + 1);
                }
            }
            drew
        }

        /// Models this slot has sent to the GPU.
        #[must_use]
        pub fn uploads(&self) -> u64 {
            self.uploads.get()
        }

        /// Frames this slot has drawn on the resident lane.
        #[must_use]
        pub fn frames(&self) -> u64 {
            self.frames.get()
        }

        /// The generation this slot last uploaded (`0` = none).
        #[must_use]
        pub fn generation(&self) -> u64 {
            self.uploaded.get()
        }
    }

    /// **Headless tests only** (feature `cloud-test-gate`): open the installed-renderer
    /// gate without a device, so a test can drive a pane's resident lane through plain
    /// `egui::Context` frames and count what it sends — the paint callbacks are collected
    /// into the frame's shapes and never executed. A host that called this would paint
    /// BLANK panes (the trap [`cloud_renderer_installed`] exists to prevent), which is why
    /// it is behind a feature that only `[dev-dependencies]` turn on.
    ///
    /// It raises the lane's registry flag only (what [`cloud_renderer_installed`] and
    /// `lane::is_installed` read) and registers no lane RECORD — nothing was installed,
    /// so an About box must not say a lane is.
    #[cfg(feature = "cloud-test-gate")]
    pub fn assume_renderer_installed_for_tests() {
        installed_flag().store(true, std::sync::atomic::Ordering::Release);
    }

    /// Install a [`CloudRenderer`] into an egui-wgpu `RenderState`'s callback resources
    /// (call once at host startup, like `facett-map`/`logo3d`'s `install_renderer`).
    /// Idempotent. Also arms [`cloud_renderer_installed`], which is what unlocks
    /// `Graph3D`'s instanced fast path.
    /// The install-once body is the shared `crate::render::gpu::install_renderer`
    /// (LAW #5 — one writer, not a fourth hand-rolled get-or-insert), which also
    /// registers the lane with `crate::render::lane` so an About box can see it.
    pub fn install_cloud_renderer(render_state: &egui_wgpu::RenderState) {
        // `install_renderer` (last line) raises this lane's registry flag, which is what
        // `cloud_renderer_installed` reads — no second flag to set here.
        // This lane counts its frames (see `CloudPaintCallback::prepare`), so a zero
        // means "installed and never painted" instead of "did not say".
        crate::render::lane::note_reports_frames(std::any::type_name::<CloudRenderer>());
        // ── AND SAY WHY THE ZERO IS THERE. A freshly installed lane has not painted for
        //    exactly one reason: nobody has drawn the pane that owns it. `facett-map`'s
        //    map lane has said so since 2026-08-02; this one stayed silent, and silence
        //    is not free — `GpuStatus::every_idle_lane_is_undrawn` cannot conclude
        //    anything from a lane that never spoke, so ONE silent lane was enough to put
        //    the About box back to "installed but UNUSED … (the CPU painter is doing the
        //    work)" on a host where nothing had drawn a map OR a graph. MEASURED on korp
        //    2026-08-03: `[(CloudRenderer, 0, None), (GpuMapRenderer, 0,
        //    pane_not_painted_yet)]`.
        //
        //    `Graph3D`/`cloud_gpu_scene` overwrite this the first time they run, so the
        //    row only says it while it is true.
        crate::render::lane::note_idle_reason(
            std::any::type_name::<CloudRenderer>(),
            Some(crate::render::lane::PANE_NOT_PAINTED_YET),
        );
        let _ = crate::render::gpu::install_renderer(render_state, CloudRenderer::new);
    }

    /// **The resident-model twin of [`draw_cloud`]**: push a [`CloudModelPaintCallback`]
    /// for `rect` and return `true`, so the caller may skip its CPU cloud. `model` is
    /// `Some` only on the frames where the host's `generation` moved; every other frame
    /// sends the camera alone. The same fail-safe gates as [`draw_cloud`]: no installed
    /// renderer, or a zero-pixel rect, is `false` and the CPU painter draws.
    ///
    /// The renderer keeps ONE resident model, so one pane per `RenderState` should use
    /// this lane at a time; a second would re-upload on every frame it differs.
    pub fn draw_cloud_model(
        ui: &egui::Ui,
        rect: egui::Rect,
        generation: u64,
        model: Option<(Vec<super::CloudPoint>, Vec<super::CloudEdge>)>,
        cam: super::CloudCamera,
    ) -> bool {
        if !cloud_renderer_installed() {
            return false;
        }
        let ppp = ui.ctx().pixels_per_point();
        // Paint only what is on screen. A pane laid out bigger than its clip (a graph
        // inside a scroll area, a canvas sized to its world) handed its WHOLE rect here,
        // and the offscreen was made that size — 10 604 px wide on loki, past the
        // device's 8 192 limit (a wgpu panic). The callback covers the visible part; the
        // camera's centre moves by the same offset, so every point lands where it did.
        let (rect, cam) = visible_part(rect, ui.clip_rect(), ppp, cam);
        let px_w = (rect.width() * ppp).round();
        let px_h = (rect.height() * ppp).round();
        if !(px_w >= 1.0 && px_h >= 1.0) {
            return false;
        }
        if model.is_some() {
            RESIDENT_GENERATION.store(generation, std::sync::atomic::Ordering::Relaxed);
        }
        ui.painter().add(egui_wgpu::Callback::new_paint_callback(
            rect,
            CloudModelPaintCallback {
                model,
                generation,
                cam,
                background: [0.02, 0.022, 0.03, 1.0],
                px_w: px_w as u32,
                px_h: px_h as u32,
            },
        ));
        true
    }

    /// The part of `rect` inside `clip`, and `cam` moved to it: the viewport becomes the
    /// visible size, and the centre (pane-local px) shifts by the visible part's offset,
    /// so a point projects to the same screen pixel. An empty intersection is an empty
    /// rect (the caller draws nothing).
    #[must_use]
    pub fn visible_part(rect: egui::Rect, clip: egui::Rect, ppp: f32, cam: super::CloudCamera) -> (egui::Rect, super::CloudCamera) {
        let vis = rect.intersect(clip);
        if !(vis.width() > 0.0 && vis.height() > 0.0) {
            return (egui::Rect::NOTHING, cam);
        }
        let shift = (vis.min - rect.min) * ppp;
        let mut cam = cam;
        cam.viewport = [vis.width() * ppp, vis.height() * ppp, cam.viewport[2] - shift.x, cam.viewport[3] - shift.y];
        (vis, cam)
    }

    /// **THE one GPU-cloud entry point every graph pane calls.** Returns `true` if the
    /// instanced cloud was pushed for `rect` — and therefore, and only therefore, the
    /// caller may skip its CPU tessellation of the node/edge cloud.
    ///
    /// It performs, in one place, the whole fail-safe protocol that must not be
    /// re-derived per pane:
    ///
    /// 1. `use_gpu` off ⇒ `false`. The CPU painter runs; goldens are untouched.
    /// 2. **Renderer not installed ⇒ `false`.** This is the trap. Opting in is not
    ///    enough: without [`install_cloud_renderer`] the paint callback finds no
    ///    `CloudRenderer` and draws *nothing*, while the caller has already skipped the
    ///    CPU painter — a silently **BLANK** pane. That exact shape shipped once in
    ///    `Graph3D` (proven by tests, installed by no host) and was the whole "the 3D
    ///    graphs are missing" symptom. Falling back here makes it structurally
    ///    impossible for a *new* pane to reintroduce it: the pane cannot skip its CPU
    ///    painter without going through this function, and this function will not say
    ///    yes until a renderer really exists.
    /// 3. A degenerate rect (zero px) ⇒ `false`.
    ///
    /// `build` is only invoked once every gate has passed — so building the instance
    /// scene costs nothing on the fallback path. It receives the pane size in
    /// **physical px** (`rect` × `pixels_per_point`), which is the space
    /// [`NodeInstance::center`] / [`EdgeInstance::a`] live in.
    pub fn draw_cloud(
        ui: &egui::Ui,
        rect: egui::Rect,
        use_gpu: bool,
        build: impl FnOnce(f32, f32) -> GraphScene,
    ) -> bool {
        if !use_gpu || !cloud_renderer_installed() {
            return false;
        }
        let ppp = ui.ctx().pixels_per_point();
        let px_w = (rect.width() * ppp).round();
        let px_h = (rect.height() * ppp).round();
        if !(px_w >= 1.0 && px_h >= 1.0) {
            return false;
        }
        let scene = build(px_w, px_h);
        if scene.nodes.is_empty() && scene.edges.is_empty() {
            return false;
        }
        ui.painter().add(egui_wgpu::Callback::new_paint_callback(
            rect,
            CloudPaintCallback { scene, px_w: px_w as u32, px_h: px_h as u32 },
        ));
        true
    }
}

/// **The shared 2-D lowering** — turn already-projected screen geometry into a
/// [`GraphScene`] of emissive instances. Every 2-D graph pane
/// (`facett-graphpan` / `facett-graphview` / `facett-graphnav`) lowers through this,
/// so the look, the sRGB→linear conversion and the emissive scaling are defined once
/// (L5) rather than re-derived per crate with three slightly different gammas.
///
/// `emissive` scales peak linear radiance; `≈2.5` reads bright over a dark pane with a
/// ~1.0 bright-pass threshold, `1.0` is calm. Edges are emitted first so they sit under
/// the nodes, matching the CPU painters' order.
#[derive(Clone, Copy, Debug)]
pub struct CloudBuilder {
    /// Peak emissive multiplier for node instances.
    pub emissive: f32,
    /// Edge radiance as a fraction of the node peak (crossings still accumulate).
    pub edge_gain: f32,
    /// Clear colour behind the cloud (linear rgba).
    pub background: [f32; 4],
}

impl Default for CloudBuilder {
    fn default() -> Self {
        Self { emissive: 2.5, edge_gain: 0.6, background: [0.02, 0.022, 0.03, 1.0] }
    }
}

impl CloudBuilder {
    /// Linear radiance for one `Color32` at `mul × emissive`. sRGB is approximated by a
    /// square — the same curve `Graph3D::gpu_scene_at` has always used, kept identical
    /// so the 2-D and 3-D clouds cannot drift apart visually.
    #[must_use]
    pub fn radiance(&self, c: egui::Color32, mul: f32) -> [f32; 4] {
        let lin = |b: u8| {
            let s = b as f32 / 255.0;
            s * s
        };
        let k = self.emissive * mul;
        [lin(c.r()) * k, lin(c.g()) * k, lin(c.b()) * k, 1.0]
    }

    /// Lower `nodes` (`centre px`, `radius px`, colour) and `edges` (`a px`, `b px`,
    /// `half-width px`, colour) into an instanced [`GraphScene`]. Pure — no device, no
    /// egui painting — so a headless test can assert the instance counts and positions
    /// without a GPU.
    #[must_use]
    pub fn scene(
        &self,
        nodes: impl IntoIterator<Item = (egui::Pos2, f32, egui::Color32)>,
        edges: impl IntoIterator<Item = (egui::Pos2, egui::Pos2, f32, egui::Color32)>,
    ) -> GraphScene {
        let edges = edges
            .into_iter()
            .map(|(a, b, w, c)| EdgeInstance {
                a: [a.x, a.y],
                b: [b.x, b.y],
                width: w.max(0.5),
                _pad: 0.0,
                color: self.radiance(c, self.edge_gain),
            })
            .collect();
        let nodes = nodes
            .into_iter()
            .map(|(p, r, c)| NodeInstance {
                center: [p.x, p.y],
                radius: r.max(0.5),
                _pad: 0.0,
                color: self.radiance(c, 1.0),
            })
            .collect();
        GraphScene { background: self.background, nodes, edges }
    }
}

fn uniform_entry(binding: u32, vis: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
    wgpu::BindGroupLayoutEntry {
        binding,
        visibility: vis,
        ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
        count: None,
    }
}
fn tex_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
    wgpu::BindGroupLayoutEntry {
        binding,
        visibility: wgpu::ShaderStages::FRAGMENT,
        ty: wgpu::BindingType::Texture {
            sample_type: wgpu::TextureSampleType::Float { filterable: true },
            view_dimension: wgpu::TextureViewDimension::D2,
            multisampled: false,
        },
        count: None,
    }
}
fn sampler_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
    wgpu::BindGroupLayoutEntry {
        binding,
        visibility: wgpu::ShaderStages::FRAGMENT,
        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
        count: None,
    }
}

/// **Headless render-proof entry point.** Spin up an adapter/device, render `scene`
/// at `w×h` through the full instanced + bloom lane, and read back the LDR frame +
/// the instance draw counts. `None` when no wgpu adapter is available (CPU-only CI),
/// so the caller degrades to a skip rather than a false failure.
#[must_use]
pub fn render_graph_offscreen(w: u32, h: u32, scene: &GraphScene, bloom: BloomParams) -> Option<GraphRender> {
    pollster::block_on(render_graph_offscreen_async(w, h, scene, bloom))
}

async fn render_graph_offscreen_async(w: u32, h: u32, scene: &GraphScene, bloom: BloomParams) -> Option<GraphRender> {
    // THE GPU TURN + the adapter policy, via the one bring-up writer
    // (`crate::render::gputurn::probe`). This is the device probe `facett-graph3d`,
    // `facett-helix` and `facett-graphview` all render through, so the turn taken here
    // serialises those crates' pixel tests too. `OnSoftware::Skip`, not `Refuse`: the
    // signature is already `Option` and every caller reads `None` as "no GPU, skip".
    let probe = crate::render::gputurn::probe::open(
        "graph3d-gpu-offscreen",
        crate::render::gputurn::probe::OnSoftware::Skip,
        crate::render::gputurn::probe::downlevel,
    )?;
    let (device, queue) = (&probe.device, &probe.queue);
    let mut gpu = GraphGpu::new(device, wgpu::TextureFormat::Rgba8Unorm);
    Some(gpu.render(device, queue, w, h, scene, bloom))
}

/// **The CLOUD lane, headless** — upload a model-space cloud, write one camera, render.
/// The A/B partner of [`render_graph_offscreen`]: give both the same graph and they must
/// produce the same picture, which is what `tests/cloud_parity.rs` asserts.
#[must_use]
pub fn render_cloud_offscreen(
    w: u32,
    h: u32,
    points: &[CloudPoint],
    edges: &[CloudEdge],
    cam: &CloudCamera,
    background: [f32; 4],
    bloom: BloomParams,
) -> Option<GraphRender> {
    pollster::block_on(render_cloud_offscreen_async(w, h, points, edges, cam, background, bloom))
}

async fn render_cloud_offscreen_async(
    w: u32,
    h: u32,
    points: &[CloudPoint],
    edges: &[CloudEdge],
    cam: &CloudCamera,
    background: [f32; 4],
    bloom: BloomParams,
) -> Option<GraphRender> {
    let probe = crate::render::gputurn::probe::open(
        "graph3d-cloud-offscreen",
        crate::render::gputurn::probe::OnSoftware::Skip,
        crate::render::gputurn::probe::downlevel,
    )?;
    let (device, queue) = (&probe.device, &probe.queue);
    let mut gpu = GraphGpu::new(device, wgpu::TextureFormat::Rgba8Unorm);
    let cloud = gpu.upload_cloud(device, queue, points, edges, 0).ok()?;
    gpu.write_cloud_camera(queue, cam);
    Some(gpu.render_cloud(device, queue, w, h, &cloud, background, bloom))
}

#[cfg(test)]
mod buffer_refusal_tests {
    use super::buffer_refusal;
    use egui_wgpu::wgpu;

    /// The resident cloud at 100k nodes is a 1.6 MB points buffer (lane L's SwiftShader
    /// panic). Over a device's limits it is REFUSED with the reason — `upload_cloud`
    /// returns it, the paint callback draws nothing and records it — never a panic.
    #[test]
    fn a_buffer_over_the_device_limits_is_refused_with_the_reason() {
        let st = wgpu::BufferUsages::STORAGE;
        let mut small = wgpu::Limits::downlevel_defaults();
        small.max_buffer_size = 1 << 20;
        small.max_storage_buffer_binding_size = 1 << 19;
        let why = buffer_refusal("graph3d_cloud_pts", 1_600_000, st, &small).expect("over max_buffer_size");
        assert!(why.contains("max_buffer_size") && why.contains("1600000"), "{why}");
        let why = buffer_refusal("graph3d_cloud_pts", 600_000, st, &small).expect("over the storage binding");
        assert!(why.contains("max_storage_buffer_binding_size"), "{why}");
        assert_eq!(buffer_refusal("graph3d_cloud_nodes", 600_000, wgpu::BufferUsages::VERTEX, &small), None);
        assert_eq!(buffer_refusal("graph3d_cloud_pts", 1_600_000, st, &wgpu::Limits::default()), None, "fits a default device");
    }
}