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

//! **MSDF text / label rendering** (feature `wgpu`) — crisp glowing labels at any
//! zoom from a multi-channel signed-distance-field atlas. Net-new (the tree had no
//! GPU glyph pipeline; text was egui galleys only). A consumer bakes an MSDF atlas
//! offline (e.g. `msdf-atlas-gen`) and feeds glyph quads here; the fragment takes
//! the **median** of the three distance channels — the MSDF trick that keeps sharp
//! corners a single-channel SDF would round — and converts it to ~1px coverage via
//! the per-glyph **screen pixel range**, so a label stays razor-sharp whether it's
//! tiny or zoomed across the viewport.
//!
//! To keep the render proof self-contained (no external atlas generator), this
//! module also ships [`disk_msdf_atlas`]: an analytic single-glyph atlas (a filled
//! disk, whose true signed distance is known in closed form) — a valid degenerate
//! MSDF (all three channels equal) that exercises the exact sample → median →
//! coverage path. The coverage math ([`median3`], [`coverage`], [`screen_px_range`])
//! is mirrored on the CPU and unit-tested with no GPU (FC-7).

use wgpu::util::DeviceExt;
use wgpu::TextureFormat;

/// The MSDF glyph shader (`msdf_vs`/`msdf_fs`).
pub use crate::render::wgsl::MSDF_WGSL;

/// One glyph quad: a pixel-space rect, its atlas UV rect, a colour, and the
/// per-glyph **screen pixel range** (atlas field range × glyph-screen-px ÷
/// atlas-glyph-px) that sets the AA width. 64 bytes; the vertex attribute offsets in
/// [`MsdfText::new`] hard-code this layout.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GlyphInstance {
    pub rect_min: [f32; 2],
    pub rect_max: [f32; 2],
    pub uv_min: [f32; 2],
    pub uv_max: [f32; 2],
    pub color: [f32; 4],
    pub px_range: f32,
    pub _pad: [f32; 3],
}

impl GlyphInstance {
    /// A glyph at pixel rect `(x0,y0)-(x1,y1)` sampling atlas UV `(u0,v0)-(u1,v1)`,
    /// in `color` (straight RGBA), with the screen pixel range `px_range`.
    #[allow(clippy::too_many_arguments)]
    #[must_use]
    pub fn new(rect: [f32; 4], uv: [f32; 4], color: [f32; 4], px_range: f32) -> Self {
        Self {
            rect_min: [rect[0], rect[1]],
            rect_max: [rect[2], rect[3]],
            uv_min: [uv[0], uv[1]],
            uv_max: [uv[2], uv[3]],
            color,
            px_range,
            _pad: [0.0; 3],
        }
    }
}

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

/// The median of three values — the MSDF reconstruction (mirrors `median3` in the
/// WGSL). For a degenerate equal-channel atlas it is just that value.
#[must_use]
pub fn median3(a: f32, b: f32, c: f32) -> f32 {
    a.max(b).min(a.min(b).max(c)).max(a.min(b))
}

/// Coverage (alpha, `[0,1]`) from a sampled `median` distance and a `screen_px_range`
/// — mirrors `msdf_fs`. The transition is ~1 screen pixel wide, so the edge is crisp
/// at any zoom; a larger range = a sharper (steeper) edge.
#[must_use]
pub fn coverage(median: f32, screen_px_range: f32) -> f32 {
    (screen_px_range * (median - 0.5) + 0.5).clamp(0.0, 1.0)
}

/// The **screen pixel range** for a glyph: the atlas field range (`px_range_atlas`,
/// in atlas texels) scaled by how many screen pixels each atlas texel covers
/// (`glyph_screen_px / atlas_glyph_px`). This is what keeps coverage ~1px wide
/// independent of zoom.
#[must_use]
pub fn screen_px_range(px_range_atlas: f32, glyph_screen_px: f32, atlas_glyph_px: f32) -> f32 {
    px_range_atlas * (glyph_screen_px / atlas_glyph_px.max(1.0))
}

/// Build an analytic single-glyph MSDF atlas: a filled disk of radius
/// `radius_frac × tile/2`, distance field range `px_range` (texels), as `tile×tile`
/// `Rgba8Unorm` bytes (all three colour channels carry the same encoded signed
/// distance — a valid degenerate MSDF; alpha = 255). The encoding is the standard
/// `0.5 + sd/range`, so the shader's `median - 0.5` recovers the signed distance.
#[must_use]
pub fn disk_msdf_atlas(tile: u32, radius_frac: f32, px_range: f32) -> Vec<u8> {
    let tile = tile.max(2);
    let cx = tile as f32 / 2.0;
    let cy = tile as f32 / 2.0;
    let radius = radius_frac * (tile as f32 / 2.0);
    let range = px_range.max(1e-3);
    let mut out = vec![0u8; (tile * tile * 4) as usize];
    for y in 0..tile {
        for x in 0..tile {
            // Texel centre.
            let px = x as f32 + 0.5;
            let py = y as f32 + 0.5;
            let dist = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
            let sd = radius - dist; // positive inside the disk
            let enc = (0.5 + sd / range).clamp(0.0, 1.0);
            let b = (enc * 255.0).round() as u8;
            let i = ((y * tile + x) * 4) as usize;
            out[i] = b;
            out[i + 1] = b;
            out[i + 2] = b;
            out[i + 3] = 255;
        }
    }
    out
}

/// The MSDF glyph renderer: an instanced quad pipeline sampling an MSDF atlas,
/// built for a `target_format` (the LDR surface the labels composite onto).
pub struct MsdfText {
    pipeline: wgpu::RenderPipeline,
    uniform_bgl: wgpu::BindGroupLayout,
    atlas_bgl: wgpu::BindGroupLayout,
    sampler: wgpu::Sampler,
    uniform: wgpu::Buffer,
    target_format: TextureFormat,
}

impl MsdfText {
    /// Build the glyph pipeline (validates `msdf.wgsl`). `target_format` is the
    /// colour format the labels are drawn into (alpha-blended over it).
    pub fn new(device: &wgpu::Device, target_format: TextureFormat) -> Self {
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("l0_msdf"),
            source: wgpu::ShaderSource::Wgsl(MSDF_WGSL.into()),
        });
        let uniform_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("l0_msdf_uniform_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,
            }],
        });
        let atlas_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("l0_msdf_atlas_bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    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: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
            ],
        });
        let blend = Some(wgpu::BlendState {
            color: wgpu::BlendComponent { src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, operation: wgpu::BlendOperation::Add },
            alpha: wgpu::BlendComponent { src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, operation: wgpu::BlendOperation::Add },
        });
        // Glyph instance attrs (offsets match GlyphInstance, stride 64).
        let attrs = [
            wgpu::VertexAttribute { offset: 0, shader_location: 0, format: wgpu::VertexFormat::Float32x2 },
            wgpu::VertexAttribute { offset: 8, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
            wgpu::VertexAttribute { offset: 16, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
            wgpu::VertexAttribute { offset: 24, shader_location: 3, format: wgpu::VertexFormat::Float32x2 },
            wgpu::VertexAttribute { offset: 32, shader_location: 4, format: wgpu::VertexFormat::Float32x4 },
            wgpu::VertexAttribute { offset: 48, shader_location: 5, format: wgpu::VertexFormat::Float32 },
        ];
        let layout = wgpu::VertexBufferLayout {
            array_stride: std::mem::size_of::<GlyphInstance>() as wgpu::BufferAddress,
            step_mode: wgpu::VertexStepMode::Instance,
            attributes: &attrs,
        };
        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("l0_msdf_pipeline"),
            layout: Some(&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("l0_msdf_pll"),
                bind_group_layouts: &[Some(&uniform_bgl), Some(&atlas_bgl)],
                immediate_size: 0,
            })),
            vertex: wgpu::VertexState { module: &shader, entry_point: Some("msdf_vs"), compilation_options: Default::default(), buffers: &[Some(layout)] },
            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("msdf_fs"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState { format: target_format, blend, write_mask: wgpu::ColorWrites::ALL })],
            }),
            multiview_mask: None,
            cache: None,
        });
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("l0_msdf_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 uniform = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("l0_msdf_uniform"),
            size: std::mem::size_of::<MsdfUniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        Self { pipeline, uniform_bgl, atlas_bgl, sampler, uniform, target_format }
    }

    /// The colour format the pipeline targets.
    #[must_use]
    pub fn target_format(&self) -> TextureFormat {
        self.target_format
    }

    /// Build a bind group for an MSDF `atlas_view`. Reuse it across frames while the
    /// atlas is unchanged.
    pub fn atlas_bind(&self, device: &wgpu::Device, atlas_view: &wgpu::TextureView) -> wgpu::BindGroup {
        device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("l0_msdf_atlas_bind"),
            layout: &self.atlas_bgl,
            entries: &[
                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(atlas_view) },
                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
            ],
        })
    }

    /// Record the glyph draw into `target` (must be `target_format`). `glyphs` is the
    /// per-glyph instance batch; `atlas_bind` comes from [`atlas_bind`](Self::atlas_bind).
    /// `load` keeps the existing target (compose over a scene); `false` clears first.
    #[allow(clippy::too_many_arguments)]
    pub fn render(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        target: &wgpu::TextureView,
        atlas_bind: &wgpu::BindGroup,
        glyphs: &[GlyphInstance],
        load: bool,
        w: u32,
        h: u32,
    ) {
        if glyphs.is_empty() {
            return;
        }
        let u = MsdfUniforms { vp: [w.max(1) as f32, h.max(1) as f32, 0.0, 0.0] };
        queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&u));
        let uni_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("l0_msdf_uni_bind"),
            layout: &self.uniform_bgl,
            entries: &[wgpu::BindGroupEntry { binding: 0, resource: self.uniform.as_entire_binding() }],
        });
        let inst = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("l0_msdf_instances"),
            contents: bytemuck::cast_slice(glyphs),
            usage: wgpu::BufferUsages::VERTEX,
        });
        let load_op = if load { wgpu::LoadOp::Load } else { wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT) };
        let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("l0_msdf_pass"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view: target,
                resolve_target: None,
                depth_slice: None,
                ops: wgpu::Operations { load: load_op, store: wgpu::StoreOp::Store },
            })],
            depth_stencil_attachment: None,
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });
        rp.set_pipeline(&self.pipeline);
        rp.set_bind_group(0, &uni_bind, &[]);
        rp.set_bind_group(1, atlas_bind, &[]);
        rp.set_vertex_buffer(0, inst.slice(..));
        rp.draw(0..6, 0..glyphs.len() as u32);
    }
}

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

    /// INJECT-ASSERT: median3 returns the middle value for any ordering (the MSDF
    /// reconstruction). Pure CPU.
    #[test]
    fn median3_is_the_middle() {
        assert_eq!(median3(0.1, 0.5, 0.9), 0.5);
        assert_eq!(median3(0.9, 0.5, 0.1), 0.5);
        assert_eq!(median3(0.5, 0.9, 0.1), 0.5);
        assert_eq!(median3(0.2, 0.2, 0.8), 0.2);
        assert_eq!(median3(0.7, 0.7, 0.7), 0.7);
    }

    /// INJECT-ASSERT: coverage is 1 well inside (median≫0.5), 0 well outside, ~0.5
    /// at the edge — and a larger screen px range gives a **sharper** edge (steeper
    /// slope through 0.5). This is the crispness guarantee, on data.
    #[test]
    fn coverage_is_an_edge_and_sharper_with_more_range() {
        assert!(coverage(0.9, 8.0) > 0.99, "inside → opaque");
        assert!(coverage(0.1, 8.0) < 0.01, "outside → transparent");
        assert!((coverage(0.5, 8.0) - 0.5).abs() < 1e-6, "edge → 0.5");
        // Just off the edge: more range → moves further from 0.5 (sharper).
        let soft = coverage(0.52, 4.0);
        let sharp = coverage(0.52, 16.0);
        assert!(sharp > soft, "more screen-px-range = crisper edge ({soft} → {sharp})");
    }

    /// INJECT-ASSERT: screen px range scales with the on-screen glyph size — a glyph
    /// drawn 2× larger gets 2× the range (so coverage still spans ~1 screen px → it
    /// stays crisp when zoomed). This is the "any zoom" property as math.
    #[test]
    fn screen_px_range_scales_with_glyph_size() {
        let small = screen_px_range(4.0, 16.0, 32.0);
        let big = screen_px_range(4.0, 64.0, 32.0);
        assert!((big / small - 4.0).abs() < 1e-4, "4× glyph → 4× range");
        assert!(small > 0.0);
    }

    /// INJECT-ASSERT: the analytic disk atlas encodes the field correctly — the
    /// centre texel is well inside (>0.5), a corner is outside (<0.5), and the
    /// encoded value crosses 0.5 at the disk radius. Drives the exact sample path.
    #[test]
    fn disk_atlas_encodes_signed_distance() {
        let tile = 32u32;
        let atlas = disk_msdf_atlas(tile, 0.7, 6.0);
        let at = |x: u32, y: u32| atlas[((y * tile + x) * 4) as usize] as f32 / 255.0;
        assert!(at(tile / 2, tile / 2) > 0.5, "centre is inside the disk");
        assert!(at(0, 0) < 0.5, "corner is outside the disk");
        // Coverage of the centre is opaque; coverage of the corner is transparent.
        assert!(coverage(at(tile / 2, tile / 2), 8.0) > 0.9);
        assert!(coverage(at(1, 1), 8.0) < 0.1);
    }

    /// INJECT-ASSERT: the glyph instance is a tight 64 bytes (its vertex attrs are
    /// hard-coded to these offsets) and the shader has the named entry points.
    #[test]
    fn glyph_layout_and_shader_entry_points() {
        assert_eq!(std::mem::size_of::<GlyphInstance>(), 64);
        assert!(MSDF_WGSL.contains("fn msdf_vs"));
        assert!(MSDF_WGSL.contains("fn msdf_fs"));
        assert!(MSDF_WGSL.contains("fn median3"));
    }

    fn headless_device(
    ) -> Option<(wgpu::Device, wgpu::Queue, crate::render::gputurn::probe::ProbeGuard)> {
        // ONE WRITER for the bring-up — `crate::render::gputurn::probe::open`. This used to
        // be twenty hand-rolled lines here and in seven other places; every copy asked
        // `request_adapter(PowerPreference::default())` and so could be handed llvmpipe with
        // no word said, and none of them took the GPU turn. `open` takes the turn, goes
        // through facett's adapter policy (software ranked last), PRINTS the adapter it got,
        // and refuses a software one. The turn travels out in the returned guard so it covers
        // the whole test body — MEASURED 2026-08-31, serialising only the bring-up still left
        // nine `label_collide` devices alive at once and deadlocked the binary for 13 minutes
        // (20 threads in `futex_do_wait`, GPU at 0 %, 632 MiB held), in BOTH profiles; the
        // same 74 `render::gpu::*` tests pass in 5.2 s serialised. Bind the guard, never `_`.
        use crate::render::gputurn::probe;
        Some(probe::open("l0-msdf-proof", probe::OnSoftware::Refuse, probe::downlevel)?.split())
    }

    /// Upload the disk atlas as an Rgba8Unorm texture and return its view.
    fn upload_atlas(device: &wgpu::Device, queue: &wgpu::Queue, tile: u32, bytes: &[u8]) -> wgpu::TextureView {
        let tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("msdf-proof-atlas"),
            size: wgpu::Extent3d { width: tile, height: tile, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: TextureFormat::Rgba8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        queue.write_texture(
            wgpu::TexelCopyTextureInfo { texture: &tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
            bytes,
            wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(tile * 4), rows_per_image: Some(tile) },
            wgpu::Extent3d { width: tile, height: tile, depth_or_array_layers: 1 },
        );
        tex.create_view(&Default::default())
    }

    /// Render `glyphs` to a `w×h` Rgba8Unorm target and read back the pixels.
    fn render_glyphs(w: u32, h: u32, tile: u32, atlas: &[u8], glyphs: &[GlyphInstance]) -> Option<Vec<u8>> {
        let (device, queue, _gpu) = headless_device()?;
        let atlas_view = upload_atlas(&device, &queue, tile, atlas);
        let msdf = MsdfText::new(&device, TextureFormat::Rgba8Unorm);
        let atlas_bind = msdf.atlas_bind(&device, &atlas_view);
        let target = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("msdf-proof-target"),
            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: TextureFormat::Rgba8Unorm,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });
        let view = target.create_view(&Default::default());
        let mut enc = device.create_command_encoder(&Default::default());
        msdf.render(&device, &queue, &mut enc, &view, &atlas_bind, glyphs, false, w, h);

        let bpp = 4u32;
        let unpadded = w * bpp;
        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
        let padded = unpadded.div_ceil(align) * align;
        let readback = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("msdf-proof-readback"),
            size: (padded * h) as u64,
            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
            mapped_at_creation: false,
        });
        enc.copy_texture_to_buffer(
            wgpu::TexelCopyTextureInfo { texture: &target, 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(enc.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()?.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 s = (row * padded) as usize;
            rgba.extend_from_slice(&data[s..s + unpadded as usize]);
        }
        drop(data);
        readback.unmap();
        Some(rgba)
    }

    /// RENDER PROOF (self-skips without an adapter): the MSDF disk glyph draws a
    /// crisp filled disk — the centre is opaque-coloured, a corner is empty, and the
    /// lit-pixel count is close to the disk's area (the shape is rendered, not a
    /// blurry blob). Plus a **crispness** check: drawing 2× larger grows the lit area
    /// ~4× (area scaling) — the edge stays thin, it does not smear.
    #[test]
    fn msdf_render_proof_crisp_disk_at_any_zoom() {
        let tile = 32u32;
        let px_range = 6.0f32;
        let radius_frac = 0.8f32;
        let atlas = disk_msdf_atlas(tile, radius_frac, px_range);

        // ── base size: a 24px glyph centred in a 48×48 target ──
        let (w, h) = (48u32, 48u32);
        let g = 24.0f32;
        let x0 = (w as f32 - g) / 2.0;
        let y0 = (h as f32 - g) / 2.0;
        let spr = screen_px_range(px_range, g, tile as f32);
        let glyph = GlyphInstance::new([x0, y0, x0 + g, y0 + g], [0.0, 0.0, 1.0, 1.0], [0.2, 0.9, 1.0, 1.0], spr);
        let Some(px) = render_glyphs(w, h, tile, &atlas, &[glyph]) else {
            eprintln!("[msdf] no GPU adapter — skipping render proof");
            return;
        };
        let at = |buf: &[u8], stride: u32, x: u32, y: u32| -> [u8; 4] {
            let i = ((y * stride + x) * 4) as usize;
            [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]
        };
        let centre = at(&px, w, w / 2, h / 2);
        assert!(centre[1] > 180 && centre[2] > 180, "disk centre is opaque tint, got {centre:?}");
        let corner = at(&px, w, 0, 0);
        assert!((corner[0] as u32 + corner[1] as u32 + corner[2] as u32) < 20, "corner empty, got {corner:?}");

        let lit_base = px.as_chunks::<4>().0.iter().filter(|p| p[3] > 128).count();
        // Expected disk area in glyph px: π r², r = radius_frac * g/2.
        let r = radius_frac * g / 2.0;
        let area = std::f32::consts::PI * r * r;
        assert!(
            (lit_base as f32) > area * 0.6 && (lit_base as f32) < area * 1.6,
            "lit area {lit_base} ≈ disk area {area:.0} (a real shape, not a blob)"
        );

        // ── crispness: same atlas, 2× glyph → ~4× lit area (sharp edges, no smear) ──
        let (w2, h2) = (96u32, 96u32);
        let g2 = 48.0f32;
        let x2 = (w2 as f32 - g2) / 2.0;
        let y2 = (h2 as f32 - g2) / 2.0;
        let spr2 = screen_px_range(px_range, g2, tile as f32);
        let glyph2 = GlyphInstance::new([x2, y2, x2 + g2, y2 + g2], [0.0, 0.0, 1.0, 1.0], [0.2, 0.9, 1.0, 1.0], spr2);
        let px2 = render_glyphs(w2, h2, tile, &atlas, &[glyph2]).expect("adapter present (base render succeeded)");
        let lit_2x = px2.as_chunks::<4>().0.iter().filter(|p| p[3] > 128).count();
        let ratio = lit_2x as f32 / lit_base as f32;
        assert!(
            (3.0..5.0).contains(&ratio),
            "2× zoom grows area ~4× ({ratio:.2}×) — edges stay crisp, not blurred"
        );
    }
}