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

//! **Scene3D, the GPU lane** (feature `wgpu`) — ONE depth-tested 3D pass for flat
//! quads, screen-width line ribbons and **text as flat planes in world space**, drawn
//! into the shared offscreen colour+depth target and blitted into egui's pass, like the
//! graph cloud.
//!
//! * **Text** is a single-channel signed-distance-field glyph atlas, baked ONCE from
//!   egui's own monospace font (`epaint_default_fonts::HACK_REGULAR`, outlines through
//!   `ttf-parser`), drawn as textured quads on the [`Text3`]'s plane. The fragment
//!   shader puts the 0.5 iso-line one `fwidth` wide, so a glyph is sharp at every zoom
//!   and has true perspective; it is DEPTH-TESTED (not written), so a nearer card hides
//!   the text behind it. A text whose em projects under [`Scene3D::min_text_px`] is not
//!   drawn (it fades in over the next half of that).
//! * **Quads and lines** write depth, so they occlude each other and the text.
//! * **Sheets** ([`Scene3D::sheets`], a translucent source panel) come last, far to
//!   near: depth-tested, NOT depth-writing, blended — a spline behind a sheet shows
//!   through it — and then the sheets' own words ([`Scene3D::sheet_texts`]) on top.
//!
//! The data types and the camera are the CPU half, [`crate::render::scene3d`] (no
//! feature), re-exported here. A host:
//!
//! 1. installs once at startup: [`install_scene3d_renderer`]`(render_state)`;
//! 2. each frame builds a [`Scene3D`] and calls [`draw_scene3d`]; on `false` (no
//!    renderer, nothing visible) it paints its own CPU fallback from the same data.
//!
//! [`render_scene3d_offscreen`] renders headless (tests, proofs).

use std::collections::HashMap;
use std::sync::Arc;

use egui_wgpu::wgpu;

pub use crate::render::scene3d::{layout_text, Camera3, GlyphAt, Line3, Quad3, Scene3D, Text3, LINE_ADVANCE, MONO_ADVANCE};
/// The Scene3D shader.
pub use crate::render::wgsl::SCENE3D_WGSL;

// ------------------------------------------------------------------ the glyph atlas

/// Atlas px per em.
pub const PX_PER_EM: f32 = 40.0;
/// The distance field's reach either side of an edge, atlas px.
pub const SPREAD: f32 = 6.0;
const ATLAS_W: u32 = 1024;

/// One baked glyph: its atlas rect and its quad on the text plane.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Glyph {
    /// `u0, v0, u1, v1` (v0 = the top edge).
    pub uv: [f32; 4],
    /// `x0, y0, x1, y1` in em from the pen (baseline-left), y up.
    pub plane: [f32; 4],
}

/// The single-channel SDF atlas: `R8`, 0.5 = the outline, larger = inside.
pub struct SdfAtlas {
    pub width: u32,
    pub height: u32,
    pub pixels: Vec<u8>,
    glyphs: HashMap<char, Glyph>,
    /// The font's missing-glyph box, drawn for a character the font does not have.
    notdef: Option<Glyph>,
}

impl SdfAtlas {
    /// Has a baked glyph for `ch` (not the missing-glyph box)?
    #[must_use]
    pub fn has(&self, ch: char) -> bool {
        self.glyphs.contains_key(&ch)
    }

    /// The glyph `ch` is drawn with: its own, else the missing-glyph box.
    #[must_use]
    pub fn glyph(&self, ch: char) -> Option<Glyph> {
        self.glyphs.get(&ch).copied().or(self.notdef)
    }
}

/// The characters baked: printable ASCII and a few the 3D views use.
const EXTRA: &[char] = &['…', '→', '←', '·', '⚠', '│', '─', '▸', '✓', '✗', 'å', 'ä', 'ö', 'Å', 'Ä', 'Ö', 'é', 'ü'];

/// The atlas, baked on first use (≈100 glyphs, a few ms in release).
#[must_use]
pub fn atlas() -> &'static SdfAtlas {
    static A: std::sync::OnceLock<SdfAtlas> = std::sync::OnceLock::new();
    A.get_or_init(bake)
}

/// Outline flattener: segments in atlas px (font units × scale).
struct Flatten {
    s: f32,
    segs: Vec<[f32; 4]>,
    start: [f32; 2],
    cur: [f32; 2],
}

impl Flatten {
    fn to(&mut self, p: [f32; 2]) {
        if p != self.cur {
            self.segs.push([self.cur[0], self.cur[1], p[0], p[1]]);
        }
        self.cur = p;
    }
}

impl ttf_parser::OutlineBuilder for Flatten {
    fn move_to(&mut self, x: f32, y: f32) {
        let p = [x * self.s, y * self.s];
        self.start = p;
        self.cur = p;
    }
    fn line_to(&mut self, x: f32, y: f32) {
        self.to([x * self.s, y * self.s]);
    }
    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
        let (p0, p1, p2) = (self.cur, [x1 * self.s, y1 * self.s], [x * self.s, y * self.s]);
        for i in 1..=8 {
            let t = i as f32 / 8.0;
            let m = 1.0 - t;
            self.to([m * m * p0[0] + 2.0 * m * t * p1[0] + t * t * p2[0], m * m * p0[1] + 2.0 * m * t * p1[1] + t * t * p2[1]]);
        }
    }
    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
        let (p0, p1, p2, p3) = (self.cur, [x1 * self.s, y1 * self.s], [x2 * self.s, y2 * self.s], [x * self.s, y * self.s]);
        for i in 1..=12 {
            let t = i as f32 / 12.0;
            let m = 1.0 - t;
            let (a, b, c, d) = (m * m * m, 3.0 * m * m * t, 3.0 * m * t * t, t * t * t);
            self.to([a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]]);
        }
    }
    fn close(&mut self) {
        let s = self.start;
        self.to(s);
    }
}

/// Signed distance (atlas px, > 0 inside, nonzero winding) from `p` to the outline.
fn signed_distance(segs: &[[f32; 4]], p: [f32; 2]) -> f32 {
    let mut best = f32::MAX;
    let mut winding = 0i32;
    for &[ax, ay, bx, by] in segs {
        let (dx, dy) = (bx - ax, by - ay);
        let len2 = dx * dx + dy * dy;
        let t = if len2 > 0.0 { (((p[0] - ax) * dx + (p[1] - ay) * dy) / len2).clamp(0.0, 1.0) } else { 0.0 };
        let (qx, qy) = (ax + t * dx - p[0], ay + t * dy - p[1]);
        best = best.min(qx * qx + qy * qy);
        // a ray to +x: crossings, signed by direction
        let cross = dx * (p[1] - ay) - dy * (p[0] - ax);
        if ay <= p[1] && by > p[1] && cross > 0.0 {
            winding += 1;
        } else if by <= p[1] && ay > p[1] && cross < 0.0 {
            winding -= 1;
        }
    }
    let d = best.sqrt();
    if winding != 0 { d } else { -d }
}

/// A glyph to bake: its character (`None` = the missing-glyph box), its outline as
/// px segments, its cell `left, bottom, w, h` in px.
type Cell = (Option<char>, Vec<[f32; 4]>, [i32; 4]);

fn bake() -> SdfAtlas {
    let mut atlas = SdfAtlas { width: ATLAS_W, height: 1, pixels: Vec::new(), glyphs: HashMap::new(), notdef: None };
    let Ok(face) = ttf_parser::Face::parse(epaint_default_fonts::HACK_REGULAR, 0) else {
        atlas.pixels = vec![0];
        return atlas;
    };
    let s = PX_PER_EM / f32::from(face.units_per_em().max(1));
    // (char or None for notdef, outline segments, cell left/bottom/w/h in px)
    let mut cells: Vec<Cell> = Vec::new();
    let mut want: Vec<(Option<char>, ttf_parser::GlyphId)> = vec![(None, ttf_parser::GlyphId(0))];
    for ch in (32u8..=126).map(char::from).chain(EXTRA.iter().copied()) {
        if let Some(g) = face.glyph_index(ch) {
            want.push((Some(ch), g));
        }
    }
    let pad = SPREAD.ceil() as i32 + 1;
    for (ch, gid) in want {
        let mut f = Flatten { s, segs: Vec::new(), start: [0.0; 2], cur: [0.0; 2] };
        let Some(bb) = face.outline_glyph(gid, &mut f) else { continue };
        let left = (f32::from(bb.x_min) * s).floor() as i32 - pad;
        let right = (f32::from(bb.x_max) * s).ceil() as i32 + pad;
        let bottom = (f32::from(bb.y_min) * s).floor() as i32 - pad;
        let top = (f32::from(bb.y_max) * s).ceil() as i32 + pad;
        cells.push((ch, f.segs, [left, bottom, right - left, top - bottom]));
    }
    // shelf packing
    let (mut x, mut y, mut row_h) = (1u32, 1u32, 0u32);
    let mut placed: Vec<(u32, u32)> = Vec::with_capacity(cells.len());
    for (_, _, [_, _, w, h]) in &cells {
        let (w, h) = (*w as u32, *h as u32);
        if x + w + 1 > ATLAS_W {
            x = 1;
            y += row_h + 1;
            row_h = 0;
        }
        placed.push((x, y));
        x += w + 1;
        row_h = row_h.max(h);
    }
    let height = (y + row_h + 1).next_power_of_two();
    let mut px = vec![0u8; (ATLAS_W * height) as usize];
    for ((ch, segs, [left, bottom, w, h]), (cx, cy)) in cells.iter().zip(&placed) {
        let top = bottom + h;
        for j in 0..*h {
            for i in 0..*w {
                let p = [*left as f32 + i as f32 + 0.5, top as f32 - j as f32 - 0.5];
                let d = signed_distance(segs, p);
                let v = (0.5 + d / (2.0 * SPREAD)).clamp(0.0, 1.0);
                px[((cy + j as u32) * ATLAS_W + cx + i as u32) as usize] = (v * 255.0).round() as u8;
            }
        }
        let (aw, ah) = (ATLAS_W as f32, height as f32);
        let g = Glyph {
            uv: [*cx as f32 / aw, *cy as f32 / ah, (*cx as f32 + *w as f32) / aw, (*cy as f32 + *h as f32) / ah],
            plane: [*left as f32 / PX_PER_EM, *bottom as f32 / PX_PER_EM, (*left + *w) as f32 / PX_PER_EM, top as f32 / PX_PER_EM],
        };
        match ch {
            Some(c) => {
                atlas.glyphs.insert(*c, g);
            }
            None => atlas.notdef = Some(g),
        }
    }
    atlas.height = height;
    atlas.pixels = px;
    atlas
}

// ------------------------------------------------------------------ vertices

#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct SolidV {
    pos: [f32; 3],
    color: [u8; 4],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct LineV {
    a: [f32; 3],
    b: [f32; 3],
    end_side_width: [f32; 3],
    color: [u8; 4],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct TextV {
    pos: [f32; 3],
    uv: [f32; 2],
    color: [u8; 4],
}

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

struct Vertices {
    solid: Vec<SolidV>,
    lines: Vec<LineV>,
    text: Vec<TextV>,
    sheets: Vec<SolidV>,
    sheet_text: Vec<TextV>,
}

fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}

fn scale(a: [f32; 3], k: f32) -> [f32; 3] {
    [a[0] * k, a[1] * k, a[2] * k]
}

/// The frame's vertices; text sized on a `w × h` px pane (the WHOLE pane, also when only
/// part of it is visible).
fn vertices(scene: &Scene3D, cam: &Camera3, w: f32, h: f32) -> Vertices {
    let mut v = Vertices {
        solid: Vec::with_capacity(scene.quads.len() * 6),
        lines: Vec::with_capacity(scene.lines.len() * 6),
        text: Vec::new(),
        sheets: Vec::with_capacity(scene.sheets.len() * 6),
        sheet_text: Vec::new(),
    };
    let quad = |out: &mut Vec<SolidV>, q: &Quad3| {
        let (p00, p10, p11, p01) = (q.origin, add(q.origin, q.u), add(add(q.origin, q.u), q.v), add(q.origin, q.v));
        for pos in [p00, p10, p11, p00, p11, p01] {
            out.push(SolidV { pos, color: q.color });
        }
    };
    for q in &scene.quads {
        quad(&mut v.solid, q);
    }
    // sheets blend without writing depth: far to near, so a nearer one lies over a farther
    let mut sheets: Vec<(f32, &Quad3)> = scene
        .sheets
        .iter()
        .map(|q| (cam.project(add(q.origin, scale(add(q.u, q.v), 0.5)), w, h).map_or(f32::MAX, |p| p.1), q))
        .collect();
    sheets.sort_by(|a, b| b.0.total_cmp(&a.0));
    for (_, q) in sheets {
        quad(&mut v.sheets, q);
    }
    for l in &scene.lines {
        for (end, side) in [(0.0, -1.0), (0.0, 1.0), (1.0, 1.0), (0.0, -1.0), (1.0, 1.0), (1.0, -1.0)] {
            v.lines.push(LineV { a: l.a, b: l.b, end_side_width: [end, side, l.width_px], color: l.color });
        }
    }
    let atlas = atlas();
    let texts = |out: &mut Vec<TextV>, list: &[Text3]| {
        for t in list {
            let mut alpha = 1.0f32;
            if scene.min_text_px > 0.0 {
                let em = cam.em_px(t.origin, t.up, w, h);
                if em < scene.min_text_px {
                    continue;
                }
                alpha = ((em - scene.min_text_px) / (0.5 * scene.min_text_px)).clamp(0.0, 1.0).max(0.05);
            }
            let color = [t.color[0], t.color[1], t.color[2], (f32::from(t.color[3]) * alpha).round() as u8];
            for g in layout_text(t) {
                let Some(gl) = atlas.glyph(g.ch) else { continue };
                let [x0, y0, x1, y1] = gl.plane;
                let [u0, v0, u1, v1] = gl.uv;
                let at = |x: f32, y: f32| add(g.pen, add(scale(t.right, x), scale(t.up, y)));
                let (bl, br, tr, tl) = (TextV { pos: at(x0, y0), uv: [u0, v1], color }, TextV { pos: at(x1, y0), uv: [u1, v1], color }, TextV { pos: at(x1, y1), uv: [u1, v0], color }, TextV { pos: at(x0, y1), uv: [u0, v0], color });
                out.extend_from_slice(&[bl, br, tr, bl, tr, tl]);
            }
        }
    };
    texts(&mut v.text, &scene.texts);
    texts(&mut v.sheet_text, &scene.sheet_texts);
    v
}

// ------------------------------------------------------------------ pipelines

/// The four pipelines + the bind group (uniforms, atlas, sampler) for one colour format.
pub struct Scene3DGpu {
    solid: wgpu::RenderPipeline,
    line: wgpu::RenderPipeline,
    text: wgpu::RenderPipeline,
    /// A translucent sheet: the solid shader, depth-tested, NOT depth-writing.
    sheet: wgpu::RenderPipeline,
    ubuf: wgpu::Buffer,
    atlas_tex: wgpu::Texture,
    bind: wgpu::BindGroup,
    atlas_uploaded: bool,
}

impl Scene3DGpu {
    /// Pipelines drawing into `color_format` + [`super::DEPTH_FORMAT`].
    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("scene3d"), source: wgpu::ShaderSource::Wgsl(SCENE3D_WGSL.into()) });
        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("scene3d_bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
                    ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None },
            ],
        });
        let ubuf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("scene3d_uniforms"),
            size: std::mem::size_of::<Uniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let a = atlas();
        let atlas_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("scene3d_sdf_atlas"),
            size: wgpu::Extent3d { width: a.width, height: a.height, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::R8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("scene3d_sdf_sampler"),
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            ..Default::default()
        });
        let view = atlas_tex.create_view(&Default::default());
        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("scene3d_bind"),
            layout: &bgl,
            entries: &[
                wgpu::BindGroupEntry { binding: 0, resource: ubuf.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&view) },
                wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&sampler) },
            ],
        });
        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("scene3d_pll"), bind_group_layouts: &[Some(&bgl)], immediate_size: 0 });
        let pipeline = |label: &str, vs: &str, fs: &str, attrs: &[wgpu::VertexAttribute], stride: u64, depth_write: bool, cmp: wgpu::CompareFunction| {
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some(label),
                layout: Some(&layout),
                vertex: wgpu::VertexState {
                    module: &shader,
                    entry_point: Some(vs),
                    compilation_options: Default::default(),
                    buffers: &[Some(wgpu::VertexBufferLayout { array_stride: stride, step_mode: wgpu::VertexStepMode::Vertex, attributes: attrs })],
                },
                primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: None, ..Default::default() },
                depth_stencil: Some(wgpu::DepthStencilState {
                    format: super::DEPTH_FORMAT,
                    depth_write_enabled: Some(depth_write),
                    depth_compare: Some(cmp),
                    stencil: Default::default(),
                    bias: Default::default(),
                }),
                multisample: crate::render::gpu::msaa_state(),
                fragment: Some(wgpu::FragmentState {
                    module: &shader,
                    entry_point: Some(fs),
                    compilation_options: Default::default(),
                    targets: &[Some(wgpu::ColorTargetState { format: color_format, blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL })],
                }),
                multiview_mask: None,
                cache: None,
            })
        };
        let solid = pipeline(
            "scene3d_solid",
            "solid_vs",
            "solid_fs",
            &wgpu::vertex_attr_array![0 => Float32x3, 1 => Unorm8x4],
            std::mem::size_of::<SolidV>() as u64,
            true,
            wgpu::CompareFunction::Less,
        );
        let line = pipeline(
            "scene3d_line",
            "line_vs",
            "solid_fs",
            &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x3, 3 => Unorm8x4],
            std::mem::size_of::<LineV>() as u64,
            true,
            wgpu::CompareFunction::Less,
        );
        let text = pipeline(
            "scene3d_text",
            "text_vs",
            "text_fs",
            &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Unorm8x4],
            std::mem::size_of::<TextV>() as u64,
            false,
            wgpu::CompareFunction::LessEqual,
        );
        let sheet = pipeline(
            "scene3d_sheet",
            "solid_vs",
            "solid_fs",
            &wgpu::vertex_attr_array![0 => Float32x3, 1 => Unorm8x4],
            std::mem::size_of::<SolidV>() as u64,
            false,
            wgpu::CompareFunction::Less,
        );
        Self { solid, line, text, sheet, ubuf, atlas_tex, bind, atlas_uploaded: false }
    }

    /// Encode one frame into `color` + `depth` (both `w_px × h_px`): `view_proj` is the
    /// whole projection (crop included), `viewport` the target size in px.
    #[allow(clippy::too_many_arguments)]
    fn encode(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        color: &wgpu::TextureView,
        depth: &wgpu::TextureView,
        view_proj: [[f32; 4]; 4],
        viewport: [f32; 2],
        clear: [f32; 4],
        verts: &Vertices,
    ) -> Vec<wgpu::Buffer> {
        if !self.atlas_uploaded {
            let a = atlas();
            queue.write_texture(
                wgpu::TexelCopyTextureInfo { texture: &self.atlas_tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
                &a.pixels,
                wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(a.width), rows_per_image: Some(a.height) },
                wgpu::Extent3d { width: a.width, height: a.height, depth_or_array_layers: 1 },
            );
            self.atlas_uploaded = true;
        }
        queue.write_buffer(&self.ubuf, 0, bytemuck::bytes_of(&Uniforms { view_proj, viewport: [viewport[0], viewport[1], 0.0, 0.0] }));
        let lane = std::any::type_name::<Scene3DRenderer>();
        let make = |label: &str, bytes: &[u8]| -> Option<wgpu::Buffer> {
            if bytes.is_empty() {
                return None;
            }
            super::graphcloud::checked_buffer(device, queue, label, bytes, wgpu::BufferUsages::VERTEX).map_err(|why| crate::render::lane::note_gpu_error(lane, &why)).ok()
        };
        let bufs = [
            (make("scene3d_solid", bytemuck::cast_slice(&verts.solid)), verts.solid.len(), &self.solid),
            (make("scene3d_lines", bytemuck::cast_slice(&verts.lines)), verts.lines.len(), &self.line),
            (make("scene3d_text", bytemuck::cast_slice(&verts.text)), verts.text.len(), &self.text),
            (make("scene3d_sheets", bytemuck::cast_slice(&verts.sheets)), verts.sheets.len(), &self.sheet),
            (make("scene3d_sheet_text", bytemuck::cast_slice(&verts.sheet_text)), verts.sheet_text.len(), &self.text),
        ];
        {
            let c = |x: f32| f64::from(x);
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("scene3d_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: color,
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: c(clear[0]), g: c(clear[1]), b: c(clear[2]), a: c(clear[3]) }), store: wgpu::StoreOp::Store },
                })],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: depth,
                    depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store }),
                    stencil_ops: None,
                }),
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_bind_group(0, &self.bind, &[]);
            for (buf, n, pipe) in &bufs {
                if let Some(b) = buf {
                    pass.set_pipeline(pipe);
                    pass.set_vertex_buffer(0, b.slice(..));
                    pass.draw(0..*n as u32, 0..1);
                }
            }
        }
        bufs.into_iter().filter_map(|(b, _, _)| b).collect()
    }
}

// ------------------------------------------------------------------ the live lane

/// The installed renderer: pipelines for the offscreen HDR format + the shared offscreen
/// colour/depth target and its blit.
pub struct Scene3DRenderer {
    gpu: Scene3DGpu,
    offscreen: super::OffscreenColorDepth,
    /// This frame's vertex buffers, kept until egui submits.
    frame: Vec<wgpu::Buffer>,
}

impl Scene3DRenderer {
    pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
        Self { gpu: Scene3DGpu::new(device, super::OFFSCREEN_FORMAT), offscreen: super::OffscreenColorDepth::new(device, target_format), frame: Vec::new() }
    }
}

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::<Scene3DRenderer>()))
}

/// Has a host installed the Scene3D renderer ([`install_scene3d_renderer`])?
#[must_use]
pub fn scene3d_renderer_installed() -> bool {
    installed_flag().load(std::sync::atomic::Ordering::Acquire)
}

/// Install the Scene3D renderer into a `RenderState` (once, at host startup;
/// idempotent). Without it [`draw_scene3d`] returns `false` and the host's CPU fallback
/// paints.
pub fn install_scene3d_renderer(render_state: &egui_wgpu::RenderState) {
    let name = std::any::type_name::<Scene3DRenderer>();
    crate::render::lane::note_reports_frames(name);
    crate::render::lane::note_idle_reason(name, Some(crate::render::lane::PANE_NOT_PAINTED_YET));
    let _ = super::install_renderer(render_state, Scene3DRenderer::new);
}

/// One live frame.
struct Scene3DPaintCallback {
    scene: Arc<Scene3D>,
    cam: Camera3,
    /// The whole pane, physical px (text is sized on it).
    full: [f32; 2],
    /// The visible part's target size, physical px.
    px: [u32; 2],
    /// `view_proj` of the whole pane, cropped to the visible part.
    view_proj: [[f32; 4]; 4],
}

impl egui_wgpu::CallbackTrait for Scene3DPaintCallback {
    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::<Scene3DRenderer>() else { return vec![] };
        r.offscreen.ensure(device, self.px[0], self.px[1]);
        let (Some(color), Some(depth)) = (r.offscreen.color_view(), r.offscreen.depth_view()) else { return vec![] };
        let (w, h) = r.offscreen.size();
        let verts = vertices(&self.scene, &self.cam, self.full[0], self.full[1]);
        let (color, depth) = (color.clone(), depth.clone());
        r.frame = r.gpu.encode(device, queue, encoder, &color, &depth, self.view_proj, [w as f32, h as f32], self.scene.clear, &verts);
        crate::render::lane::note_frame(std::any::type_name::<Scene3DRenderer>());
        vec![]
    }

    fn paint(&self, info: egui::PaintCallbackInfo, pass: &mut wgpu::RenderPass<'static>, resources: &egui_wgpu::CallbackResources) {
        let Some(r) = resources.get::<Scene3DRenderer>() else { return };
        if r.offscreen.ready() {
            let _ = r.offscreen.blit(&info, pass);
        }
    }
}

/// The clip-space crop that maps the whole `full` px pane's projection onto the visible
/// part `vis` (`x0, y0, w, h` px, y down, relative to the pane): a point lands on the
/// same screen pixel whether the pane is drawn whole or only its visible part.
#[must_use]
pub fn crop_matrix(full: [f32; 2], vis: [f32; 4]) -> [[f32; 4]; 4] {
    let [x0, y0, vw, vh] = vis;
    let (vw, vh) = (vw.max(1e-3), vh.max(1e-3));
    let (sx, sy) = (full[0] / vw, full[1] / vh);
    let ox = sx - 1.0 - 2.0 * x0 / vw;
    let oy = 1.0 - sy + 2.0 * y0 / vh;
    [[sx, 0.0, 0.0, 0.0], [0.0, sy, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [ox, oy, 0.0, 1.0]]
}

fn mul(a: [[f32; 4]; 4], b: [[f32; 4]; 4]) -> [[f32; 4]; 4] {
    (glam::Mat4::from_cols_array_2d(&a) * glam::Mat4::from_cols_array_2d(&b)).to_cols_array_2d()
}

/// **Draw a [`Scene3D`] into `rect`** through the GPU lane. `true` when the frame was
/// pushed — the caller then skips its CPU fallback; `false` (no renderer installed, or
/// nothing of `rect` visible) and the caller paints. Only the visible part of `rect` is
/// rendered (a pane laid out bigger than its clip never asks for a texture past the
/// device limit), with the projection cropped so every point lands where it would.
pub fn draw_scene3d(ui: &egui::Ui, rect: egui::Rect, cam: &Camera3, scene: Arc<Scene3D>) -> bool {
    if !scene3d_renderer_installed() {
        return false;
    }
    let ppp = ui.ctx().pixels_per_point();
    let vis = rect.intersect(ui.clip_rect());
    let full = [(rect.width() * ppp).round(), (rect.height() * ppp).round()];
    let px = [(vis.width() * ppp).round(), (vis.height() * ppp).round()];
    if !(full[0] >= 1.0 && full[1] >= 1.0 && px[0] >= 1.0 && px[1] >= 1.0) {
        return false;
    }
    let off = (vis.min - rect.min) * ppp;
    let view_proj = mul(crop_matrix(full, [off.x, off.y, px[0], px[1]]), cam.view_proj(full[0] / full[1]));
    ui.painter().add(egui_wgpu::Callback::new_paint_callback(vis, Scene3DPaintCallback { scene, cam: *cam, full, px: [px[0] as u32, px[1] as u32], view_proj }));
    true
}

/// **Headless**: render `scene` at `w × h` and read it back (RGBA8, row-major, `w*h*4`
/// bytes). `None` when this host has no usable adapter (software rasterisers count as
/// none unless `$FACETT_ALLOW_SOFTWARE_GPU` is set — the shared probe's rule).
#[must_use]
pub fn render_scene3d_offscreen(w: u32, h: u32, cam: &Camera3, scene: &Scene3D) -> Option<Vec<u8>> {
    let probe = crate::render::gputurn::probe::open("scene3d-offscreen", crate::render::gputurn::probe::OnSoftware::Skip, crate::render::gputurn::probe::downlevel)?;
    Some(render_on(&probe.device, &probe.queue, w, h, cam, scene))
}

/// [`render_scene3d_offscreen`] on a device the caller opened.
fn render_on(device: &wgpu::Device, queue: &wgpu::Queue, w: u32, h: u32, cam: &Camera3, scene: &Scene3D) -> Vec<u8> {
    let fmt = wgpu::TextureFormat::Rgba8Unorm;
    let (w, h) = super::texture_extent(device, w, h);
    let tex = |label: &str, format: wgpu::TextureFormat, usage: wgpu::TextureUsages| {
        device.create_texture(&wgpu::TextureDescriptor {
            label: Some(label),
            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,
            usage,
            view_formats: &[],
        })
    };
    let color = tex("scene3d_headless_color", fmt, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC);
    let depth = tex("scene3d_headless_depth", super::DEPTH_FORMAT, wgpu::TextureUsages::RENDER_ATTACHMENT);
    let mut gpu = Scene3DGpu::new(device, fmt);
    let verts = vertices(scene, cam, w as f32, h as f32);
    let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("scene3d_headless") });
    let (cv, dv) = (color.create_view(&Default::default()), depth.create_view(&Default::default()));
    let keep = gpu.encode(device, queue, &mut enc, &cv, &dv, cam.view_proj(w as f32 / h as f32), [w as f32, h as f32], scene.clear, &verts);
    queue.submit(Some(enc.finish()));
    let px = super::read_texture_region(device, queue, &color, 4, 0, 0, w, h);
    drop(keep);
    px
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_atlas_has_the_glyphs_of_code() {
        let a = atlas();
        for ch in "fn main() -> Vec<u8> { x.y(); } 0123456789".chars().filter(|c| *c != ' ') {
            assert!(a.has(ch), "no glyph for {ch:?}");
        }
        assert!(a.width >= 256 && a.pixels.len() == (a.width * a.height) as usize);
        // a distance field, not a bitmap: edge values exist between in and out
        let mid = a.pixels.iter().filter(|p| (64..192).contains(*p)).count();
        assert!(mid > a.pixels.len() / 50, "{mid} mid texels");
    }

    /// The visible-part crop keeps every point on its screen pixel.
    #[test]
    fn the_crop_lands_a_point_where_the_whole_pane_would() {
        let cam = Camera3 { eye: [2.0, 3.0, 9.0], target: [0.5, 0.0, 0.0], ..Camera3::default() };
        let full = [800.0f32, 500.0];
        let vis = [120.0f32, 40.0, 300.0, 200.0];
        let m = glam::Mat4::from_cols_array_2d(&mul(crop_matrix(full, vis), cam.view_proj(full[0] / full[1])));
        for p in [[0.5, 0.0, 0.0], [1.3, 0.4, -0.2], [0.0, 0.5, 0.6]] {
            let whole = cam.project(p, full[0], full[1]).unwrap().0;
            let c = m * glam::Vec4::new(p[0], p[1], p[2], 1.0);
            let (x, y) = (c.x / c.w, c.y / c.w);
            let part = [(x * 0.5 + 0.5) * vis[2] + vis[0], (0.5 - y * 0.5) * vis[3] + vis[1]];
            assert!((whole[0] - part[0]).abs() < 1e-2 && (whole[1] - part[1]).abs() < 1e-2, "{whole:?} vs {part:?}");
        }
    }

    // ------------------------------------------------------------- rendered

    const W: u32 = 480;
    const H: u32 = 240;

    /// Render on THIS box's adapter, software included: these are geometry and
    /// legibility checks (occlusion, edge width, culling), true on llvmpipe as on a
    /// card — not a performance claim. Under the GPU turn, without touching the
    /// process environment (the shared probe's software rule stays as it is for every
    /// other test). `None` only when there is no adapter at all.
    fn render(cam: &Camera3, scene: &Scene3D) -> Option<Vec<u8>> {
        let _turn = crate::render::gputurn::hold();
        let instance = wgpu::Instance::default();
        let Some(adapter) = crate::render::gpu::request_best_adapter(&instance, crate::render::gpu::preferred_backends()) else {
            eprintln!("SKIP: no wgpu adapter at all");
            return None;
        };
        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
            label: Some("scene3d-test"),
            required_limits: wgpu::Limits::downlevel_defaults(),
            ..Default::default()
        }))
        .expect("a device on the adapter");
        Some(render_on(&device, &queue, W, H, cam, scene))
    }

    fn luma(px: &[u8], x: u32, y: u32) -> f32 {
        let i = ((y * W + x) * 4) as usize;
        (0.2126 * f32::from(px[i]) + 0.7152 * f32::from(px[i + 1]) + 0.0722 * f32::from(px[i + 2])) / 255.0
    }

    /// Bright texels (luma > 0.8) inside a px rect.
    fn bright(px: &[u8], x0: u32, y0: u32, x1: u32, y1: u32) -> usize {
        let mut n = 0;
        for y in y0..y1.min(H) {
            for x in x0..x1.min(W) {
                if luma(px, x, y) > 0.8 {
                    n += 1;
                }
            }
        }
        n
    }

    fn white(text: &str, origin: [f32; 3]) -> Text3 {
        Text3 { origin, right: [1.0, 0.0, 0.0], up: [0.0, 1.0, 0.0], text: text.into(), color: [255, 255, 255, 255] }
    }

    fn cam_at(z: f32) -> Camera3 {
        Camera3 { eye: [0.0, 0.0, z], target: [0.0, 0.0, 0.0], ..Camera3::default() }
    }

    /// A nearer opaque quad HIDES the text behind it; text beside it is drawn.
    #[test]
    fn a_nearer_quad_occludes_text_and_uncovered_text_shows() {
        let mut s = Scene3D { min_text_px: 0.0, ..Scene3D::default() };
        s.texts.push(white("HIDDEN", [-5.5, -0.35, 0.0]));
        s.texts.push(white("SHOWN", [1.5, -0.35, 0.0]));
        // a grey card one unit nearer, covering x -6..-1
        s.quads.push(Quad3 { origin: [-6.0, -1.5, 1.0], u: [5.0, 0.0, 0.0], v: [0.0, 3.0, 0.0], color: [90, 90, 90, 255] });
        let cam = cam_at(12.0);
        let Some(px) = render(&cam, &s) else { return };
        let r = |p: [f32; 3]| cam.project(p, W as f32, H as f32).unwrap().0;
        let (a, b) = (r([-6.0, -1.5, 1.0]), r([-1.0, 1.5, 1.0]));
        let hidden = bright(&px, a[0] as u32 + 1, b[1] as u32 + 1, b[0] as u32 - 1, a[1] as u32 - 1);
        assert_eq!(hidden, 0, "text behind the card leaks through");
        let quad_l = luma(&px, ((a[0] + b[0]) * 0.5) as u32, ((a[1] + b[1]) * 0.5) as u32);
        assert!((quad_l - 90.0 / 255.0).abs() < 0.08, "the card is drawn: {quad_l}");
        let (c, d) = (r([1.5, -1.0, 0.0]), r([1.5 + 5.0 * MONO_ADVANCE, 1.0, 0.0]));
        let shown = bright(&px, c[0] as u32, d[1] as u32, d[0] as u32, c[1] as u32);
        assert!(shown > 40, "the uncovered text is drawn: {shown} bright px");
    }

    /// Edge contrast on a scanline through the glyphs: sharp at two zooms (≈24 and
    /// ≈71 px a em) — full contrast, and every ink edge at most 2 px wide (a blurred
    /// distance field gives wide grey ramps, and they grow with the zoom).
    #[test]
    fn text_stays_sharp_at_two_zooms() {
        for z in [12.0f32, 4.0] {
            let mut s = Scene3D { min_text_px: 0.0, ..Scene3D::default() };
            s.texts.push(white("fn main", [-2.0, -0.35, 0.0]));
            let cam = cam_at(z);
            let Some(px) = render(&cam, &s) else { return };
            let p = |x: f32, y: f32| cam.project([x, y, 0.0], W as f32, H as f32).unwrap().0;
            let em = cam.em_px([0.0, -0.35, 0.0], [0.0, 1.0, 0.0], W as f32, H as f32);
            let (l, r) = (p(-2.0, 0.0)[0] as u32, p(-2.0 + 7.0 * MONO_ADVANCE, 0.0)[0] as u32);
            // the x-height band: a scanline a quarter em above the baseline
            let y = p(0.0, -0.35 + 0.25)[1] as u32;
            let row: Vec<f32> = (l..r.min(W)).map(|x| luma(&px, x, y)).collect();
            let (lo, hi) = row.iter().fold((1.0f32, 0.0f32), |(a, b), v| (a.min(*v), b.max(*v)));
            assert!(hi - lo > 0.6, "z={z} ({em} px/em): contrast {lo}..{hi}");
            let ink = row.iter().filter(|v| **v > 0.6).count();
            let mut widest = 0;
            let mut run = 0;
            for v in &row {
                run = if *v > 0.15 && *v < 0.85 { run + 1 } else { 0 };
                widest = widest.max(run);
            }
            assert!(ink >= 6, "z={z}: {ink} ink px on the scanline");
            assert!(widest <= 2, "z={z} ({em} px/em): an edge {widest} px wide — blurred");
        }
    }

    /// Below `min_text_px` a text is not drawn at all.
    #[test]
    fn text_too_small_to_read_is_not_drawn() {
        let mut s = Scene3D { min_text_px: 8.0, ..Scene3D::default() };
        s.texts.push(white("far away", [-2.0, 0.0, 0.0]));
        let near = render(&cam_at(8.0), &s);
        let Some(near) = near else { return };
        assert!(bright(&near, 0, 0, W, H) > 30, "near: drawn");
        let far = render(&cam_at(400.0), &s).unwrap();
        assert_eq!(bright(&far, 0, 0, W, H), 0, "far: under min_text_px, not drawn");
    }

    /// **A sheet lets what stands behind it show through** (the unfolded source panel
    /// over a spline): drawn as a depth-writing quad it cuts the line away; as a sheet the
    /// line shows, dimmed by the sheet's half alpha, while the sheet's own words stay
    /// bright on top and a word BEHIND the sheet is dimmed with the line.
    #[test]
    fn a_sheet_lets_what_is_behind_it_show_through() {
        let line = Line3 { a: [-3.0, 0.0, -1.0], b: [3.0, 0.0, -1.0], width_px: 6.0, color: [255, 255, 255, 255] };
        let panel = Quad3 { origin: [-4.0, -2.5, 0.0], u: [8.0, 0.0, 0.0], v: [0.0, 5.0, 0.0], color: [0x0c, 0x0f, 0x16, 128] };
        let cam = cam_at(10.0);
        let c = cam.project([0.0, 0.0, -1.0], W as f32, H as f32).unwrap().0;
        let (cx, cy) = (c[0] as u32, c[1] as u32);
        let peak = |px: &[u8]| (cy.saturating_sub(4)..cy + 4).map(|y| luma(px, cx, y)).fold(0.0f32, f32::max);
        // as a depth-writing quad (the old road): the line is cut away
        let mut s = Scene3D { min_text_px: 0.0, ..Scene3D::default() };
        s.lines.push(line);
        s.quads.push(panel);
        let Some(px) = render(&cam, &s) else { return };
        assert!(peak(&px) < 0.15, "a depth-writing quad hides the line: {}", peak(&px));
        // as a sheet: the line shows through, dimmed
        let mut s = Scene3D { min_text_px: 0.0, ..Scene3D::default() };
        s.lines.push(line);
        s.sheets.push(panel);
        s.texts.push(white("behind", [-2.0, 1.0, -1.0]));
        s.sheet_texts.push(white("on it", [-2.0, -2.0, 0.03]));
        let px = render(&cam, &s).expect("rendered above");
        let p = peak(&px);
        assert!((0.3..0.8).contains(&p), "the line shows through the half-transparent sheet, dimmed: {p}");
        let box_of = |o: [f32; 3], n: usize| {
            let a = cam.project([o[0], o[1] - 0.2, o[2]], W as f32, H as f32).unwrap().0;
            let b = cam.project([o[0] + n as f32 * MONO_ADVANCE, o[1] + 0.9, o[2]], W as f32, H as f32).unwrap().0;
            (a[0] as u32, b[1] as u32, b[0] as u32, a[1] as u32)
        };
        let (x0, y0, x1, y1) = box_of([-2.0, -2.0, 0.03], 5);
        assert!(bright(&px, x0, y0, x1, y1) > 20, "the sheet's own words are bright on top");
        let (x0, y0, x1, y1) = box_of([-2.0, 1.0, -1.0], 6);
        assert_eq!(bright(&px, x0, y0, x1, y1), 0, "a word behind the sheet is dimmed with the line");
    }

    #[test]
    fn lines_are_drawn_their_width() {
        let mut s = Scene3D::default();
        s.lines.push(Line3 { a: [-3.0, 0.0, 0.0], b: [3.0, 0.0, 0.0], width_px: 6.0, color: [255, 255, 255, 255] });
        let cam = cam_at(10.0);
        let Some(px) = render(&cam, &s) else { return };
        let c = cam.project([0.0; 3], W as f32, H as f32).unwrap().0;
        let col: usize = (0..H).filter(|y| luma(&px, c[0] as u32, *y) > 0.8).count();
        assert!((5..=8).contains(&col), "a 6 px ribbon is {col} px tall");
    }
}