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

//! **GPU picking — the colour-encoded ID pass** (GFX_V2 §7 item 8, feature `wgpu`).
//!
//! §7 calls this *"the one deck.gl capability we simply lack"*. Deck.gl renders every
//! pickable primitive a second time into an offscreen target where the **colour IS
//! the id**, then reads back the one texel under the cursor. That answers, in one
//! device round-trip and independent of how the geometry was built, the question a
//! CPU picker can only answer by re-deriving the geometry on the host: *what is under
//! this pixel?*
//!
//! ## This is the shader half. The id space already existed.
//!
//! [`crate::engine::pick`] defined [`PickId`] and the CPU lane, and said so:
//! *"Pass 1 defines the id encoding and the CPU lane; the offscreen target is the
//! shader pass."* This module is that pass, and it **reuses that encoding
//! unchanged** — there is exactly ONE `PickId`, one `to_rgba`/`from_rgba`, one
//! reserved miss sentinel, shared by the CPU lane, the 2-D lane and the 3-D lane
//! (LAW #5). A second id encoding for the GPU would be the twin the law forbids, and
//! worse: two encodings cannot be tested against each other for *agreement*, only
//! for self-consistency, which is the shape of a guard that cannot go red.
//!
//! ## The id space, stated exactly
//!
//! `PickId` is a `u32`: **`layer << 24 | feature`**, `layer` in `1..=255`, `feature`
//! in `0..=`[`MAX_FEATURE`] (`2^24 - 1` = 16 777 215). So:
//!
//! | | value |
//! |---|---|
//! | miss sentinel | `0` — [`PickId::NOTHING`], unreachable from `PickId::new` |
//! | smallest real id | `0x0100_0000` (layer 1, feature 0) |
//! | largest real id | `0xFFFF_FFFF` (layer 255, feature 16 777 215) |
//! | distinct real ids | `255 × 2^24` = 4 278 190 080 |
//!
//! Out-of-range inputs encode as `NOTHING`, never as an alias onto a different
//! feature — a silent alias is a wrong click, forever.
//!
//! **A miss is distinguishable from feature 0 by construction, not by convention.**
//! `layer == 0` is reserved, so the lowest id any real feature can carry is
//! `0x0100_0000`. A cleared target reads `0`; nothing that draws can write `0`. This
//! is why the sentinel lives in the *layer* bits rather than being "feature
//! `u32::MAX`" or an alpha flag: it costs no channel, and it is the natural value of
//! an untouched texel.
//!
//! ## Why `R32Uint`, and why that is not a detail
//!
//! [`PICK_FORMAT`] is `R32Uint`. Three corruptions destroy an id pass, and this
//! format makes all three **impossible rather than merely unused** — WebGPU declares
//! `r32uint` non-filterable, non-blendable and non-multisampleable, so a wrong
//! pipeline descriptor is a validation error at build time instead of an id that
//! silently reads as a neighbour's:
//!
//! * **filtering** — a bilinear tap between id `0x0100_0002` and `0x0100_0004`
//!   yields `0x0100_0003`: a *third* object that was never under the cursor.
//! * **blending** — the same, via the colour attachment's blend state.
//! * **MSAA resolve** — averages ids across an edge, so every silhouette becomes a
//!   ring of wrong answers.
//!
//! `r32uint` also has no sRGB variant, so no transfer function can touch the value.
//! [`pick_format_cannot_filter_blend_or_msaa`](self#tests) asserts all of that
//! against wgpu's own format table rather than trusting this comment.
//!
//! The 32-bit id therefore crosses the wire in ONE channel — the 8-bit-channel carry
//! problem does not arise. But the *readback* is still four little-endian bytes per
//! texel, and those bytes are decoded by the very same [`PickId::from_rgba`] the CPU
//! lane's byte view uses, so the RGBA byte order is load-bearing and is tested at
//! every carry boundary (`0xFF → 0x100`, `0xFFFF → 0x10000`, `0xFFFFFF → layer`).
//!
//! ## Shared with accessibility?  Deliberately not the same id — the same *discipline*
//!
//! [`crate::a11y`] gives a painted element an AccessKit node under a **stable domain
//! key**, and picking answers the same question ("what is under here"). They cannot
//! share one integer: an AccessKit id must be stable across frames and layout
//! changes, while a `PickId`'s `feature` is an index into *this frame's* draw list,
//! and `layer` is a slot in *this frame's* layer stack. Forcing one id space would
//! make the pick target's width a function of the whole application's element count.
//!
//! What they do share is the rule that makes a11y ids trustworthy: **the id is
//! derived, never invented at the call site**. A caller hands
//! [`PickBatch::push_quad`] its own feature index and layer slot and gets a checked
//! encode back; it never composes bits itself. Mapping a resolved `PickId` onto a
//! stable domain key is the host's one-line lookup into the same list it drew from —
//! and keeping that translation at the host is what lets one pick target serve a map
//! layer and a graph overlay in one frame (`engine::pick`'s use case 7).

use crate::engine::pick::PickId;
use egui::{Pos2, Rect};
use wgpu::TextureFormat;

/// The id attachment format: **`R32Uint`**. Non-filterable, non-blendable,
/// non-multisampleable and non-sRGB per the WebGPU format table — see the module
/// docs for why each of those matters, and
/// `pick_format_cannot_filter_blend_or_msaa` for the assertion.
pub const PICK_FORMAT: TextureFormat = TextureFormat::R32Uint;

/// The depth attachment used when the pick pass resolves occlusion. Matches
/// [`super::offscreen::DEPTH_FORMAT`] so a 3-D skin's pick pass declares the same
/// depth format its colour pass does.
pub const PICK_DEPTH_FORMAT: TextureFormat = super::offscreen::DEPTH_FORMAT;

/// The id-writing vertex/fragment shader. Needs the [`super::COMMON_WGSL`] prelude
/// (it takes `px_to_ndc` from there rather than re-spelling it) — compose with
/// [`super::wgsl`].
pub use crate::render::wgsl::PICK_WGSL;

/// One pick vertex: physical-pixel position, clip depth, encoded id. 16 bytes,
/// matching `PickVertexIn` in `pick.wgsl` (Float32x2 @ 0, Float32 @ 8, Uint32 @ 12).
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PickVertex {
    /// Position in physical pixels, origin top-left.
    pub pos_px: [f32; 2],
    /// Clip-space depth, `0.0` nearest … `1.0` farthest.
    pub depth: f32,
    /// The encoded [`PickId`] as a raw `u32`.
    pub id: u32,
}

/// Byte stride of [`PickVertex`] — the vertex buffer layout's `array_stride`.
pub const PICK_VERTEX_STRIDE: u64 = std::mem::size_of::<PickVertex>() as u64;

/// A frame's pickable geometry, in physical pixels. Push shapes, then upload once.
///
/// This is the ONE place a `PickId` is composed for the GPU lane: a caller hands its
/// own `(layer, feature)` and gets [`PickId::new`]'s checked encode, so an
/// out-of-range feature becomes the miss sentinel instead of aliasing onto another
/// object. A caller that built raw `u32`s itself would reintroduce exactly the
/// silent-alias bug `PickId::new` exists to prevent.
#[derive(Clone, Debug, Default)]
pub struct PickBatch {
    verts: Vec<PickVertex>,
}

impl PickBatch {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Forget last frame's geometry, keeping the allocation.
    pub fn clear(&mut self) {
        self.verts.clear();
    }

    /// **The writer.** Add an axis-aligned rect (two triangles) carrying `id` at
    /// clip depth `depth`.
    ///
    /// Depth is what makes a 3-D pick return the object a viewer can *see*: with a
    /// depth attachment attached and `Less` compare, the nearest id wins regardless
    /// of submission order. Without it the answer is "whatever drew last", which
    /// happens to be right for a painter-ordered 2-D scene and wrong for a camera.
    pub fn push_quad_at_depth(&mut self, rect: Rect, id: PickId, depth: f32) {
        let (l, t, r, b) = (rect.left(), rect.top(), rect.right(), rect.bottom());
        let raw = id.0;
        let v = |x: f32, y: f32| PickVertex { pos_px: [x, y], depth, id: raw };
        self.verts.extend_from_slice(&[
            v(l, t),
            v(r, t),
            v(r, b),
            v(l, t),
            v(r, b),
            v(l, b),
        ]);
    }

    /// [`push_quad_at_depth`](Self::push_quad_at_depth) at depth `0.0` — the 2-D
    /// lane, where painter order alone decides and everything sits on the near
    /// plane. Delegates so there is one quad-winding writer (LAW #5); a second
    /// six-vertex expansion differing only in a `0.0` is exactly the twin.
    pub fn push_quad(&mut self, rect: Rect, id: PickId) {
        self.push_quad_at_depth(rect, id, 0.0);
    }

    /// Add a rect for `(layer, feature)`, encoding through [`PickId::new`].
    pub fn push_feature(&mut self, rect: Rect, layer: u8, feature: u32) {
        self.push_quad(rect, PickId::new(layer, feature));
    }

    /// Add a rect at its **painter-order depth**, so a depth-tested pick pass
    /// resolves overlaps the way [`PickIndex`](crate::engine::pick::PickIndex)'s
    /// `Box` lane does.
    ///
    /// This exists because the two lanes' tie-breaks are **opposite by default**, and
    /// that is a wrong click rather than a cosmetic difference. `PickIndex`'s `Box`
    /// shape documents *"the LOWEST feature index that contains the probe wins —
    /// paint order"*; a colour-encoded pass with no depth gives the answer *"whichever
    /// quad was submitted LAST"*. For overlapping chips those are different objects,
    /// which is exactly the failure `PickShape`'s own doc warns about: *"a picker that
    /// silently swaps one for the other is wrong in a way no smoke test notices."*
    ///
    /// Mapping paint index → depth (lower index = nearer) makes the `Less` depth test
    /// reproduce the CPU rule, so agreement is by construction and does not depend on
    /// the caller's submission order at all.
    pub fn push_quad_in_painter_order(&mut self, rect: Rect, id: PickId, paint_index: usize, paint_count: usize) {
        self.push_quad_at_depth(rect, id, painter_depth(paint_index, paint_count));
    }

    /// Add an arbitrary triangle — the escape hatch for tessellated geometry (a road
    /// quad strip, an extruded building cap) that is not an axis-aligned rect.
    pub fn push_tri(&mut self, a: Pos2, b: Pos2, c: Pos2, id: PickId, depth: f32) {
        let raw = id.0;
        for p in [a, b, c] {
            self.verts.push(PickVertex { pos_px: [p.x, p.y], depth, id: raw });
        }
    }

    /// The raw vertices, for a caller that uploads them itself.
    #[must_use]
    pub fn vertices(&self) -> &[PickVertex] {
        &self.verts
    }

    /// Vertex count — what [`PickPass::record`] draws.
    #[must_use]
    pub fn len(&self) -> usize {
        self.verts.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.verts.is_empty()
    }
}

/// The offscreen id target + the one-texel readback. Owns an [`PICK_FORMAT`]
/// texture, optionally a depth companion, and (re)allocates on resize.
pub struct PickTarget {
    tex: Option<wgpu::Texture>,
    view: Option<wgpu::TextureView>,
    depth_view: Option<wgpu::TextureView>,
    size: (u32, u32),
    with_depth: bool,
}

impl PickTarget {
    /// A colour-only id target — the 2-D lane, where painter order decides.
    #[must_use]
    pub fn new() -> Self {
        Self { tex: None, view: None, depth_view: None, size: (0, 0), with_depth: false }
    }

    /// An id target with a [`PICK_DEPTH_FORMAT`] companion, so the pass resolves
    /// occlusion and the **nearest** id wins — the 3-D lane.
    #[must_use]
    pub fn with_depth() -> Self {
        Self { tex: None, view: None, depth_view: None, size: (0, 0), with_depth: true }
    }

    /// Whether this target carries a depth attachment.
    #[must_use]
    pub fn has_depth(&self) -> bool {
        self.with_depth
    }

    /// (Re)create the target for `w × h` physical pixels. Idempotent within a size.
    ///
    /// `sample_count` is hard-wired to 1: MSAA averages ids across silhouettes. It
    /// is not merely unset here — `R32Uint` cannot be multisampled at all, so a
    /// future edit that raised it would fail wgpu validation rather than quietly
    /// smear the id space.
    pub fn ensure(&mut self, device: &wgpu::Device, w: u32, h: u32) {
        let w = w.max(1);
        let h = h.max(1);
        if self.size == (w, h) && self.view.is_some() {
            return;
        }
        let size = wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 };
        let tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("l0_pick_id_target"),
            size,
            mip_level_count: 1,
            sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: PICK_FORMAT,
            // COPY_SRC is the readback; no TEXTURE_BINDING, because nothing may ever
            // SAMPLE this target — sampling is how filtering gets in.
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });
        let view = tex.create_view(&Default::default());
        self.depth_view = self.with_depth.then(|| {
            device
                .create_texture(&wgpu::TextureDescriptor {
                    label: Some("l0_pick_depth"),
                    size,
                    mip_level_count: 1,
                    sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
                    dimension: wgpu::TextureDimension::D2,
                    format: PICK_DEPTH_FORMAT,
                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
                    view_formats: &[],
                })
                .create_view(&Default::default())
        });
        self.tex = Some(tex);
        self.view = Some(view);
        self.size = (w, h);
    }

    /// Whether targets are allocated.
    #[must_use]
    pub fn ready(&self) -> bool {
        self.view.is_some()
    }

    /// Current size in physical pixels.
    #[must_use]
    pub fn size(&self) -> (u32, u32) {
        self.size
    }

    /// The id attachment view, for a caller recording its own pass.
    #[must_use]
    pub fn view(&self) -> Option<&wgpu::TextureView> {
        self.view.as_ref()
    }

    /// The depth attachment view (`None` unless built with [`Self::with_depth`]).
    #[must_use]
    pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
        self.depth_view.as_ref()
    }

    /// Begin the id pass, **clearing to [`PickId::NOTHING`]**.
    ///
    /// The clear value is `0` and nothing else, on purpose. A render-pass clear
    /// colour is a `wgpu::Color` of `f64`s reinterpreted per the attachment's sample
    /// type; `0` is the one value that means the same thing under every
    /// interpretation. That the reserved miss sentinel is exactly `0` is therefore
    /// not a coincidence — it is what lets "no geometry here" and "the cleared
    /// target" be the same bit pattern with no conversion risk.
    ///
    /// Depth clears to `1.0` (far) so the `Less` compare admits the first fragment.
    pub fn begin_pass<'a>(&'a self, enc: &'a mut wgpu::CommandEncoder) -> Option<wgpu::RenderPass<'a>> {
        let view = self.view.as_ref()?;
        Some(enc.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("l0_pick_id_pass"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view,
                resolve_target: None,
                depth_slice: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), // == PickId::NOTHING
                    store: wgpu::StoreOp::Store,
                },
            })],
            depth_stencil_attachment: self.depth_view.as_ref().map(|dv| wgpu::RenderPassDepthStencilAttachment {
                view: dv,
                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,
        }))
    }

    /// **The click.** Read the id at physical texel `(x, y)`.
    ///
    /// Out of bounds is [`PickId::NOTHING`] — a cursor outside the pane is a miss,
    /// not a clamp onto the edge feature.
    #[must_use]
    pub fn read_id(&self, device: &wgpu::Device, queue: &wgpu::Queue, x: u32, y: u32) -> PickId {
        self.read_region(device, queue, x, y, 1, 1).first().copied().unwrap_or(PickId::NOTHING)
    }

    /// [`read_id`](Self::read_id) from a **logical** cursor position relative to the
    /// widget's top-left, at `pixels_per_point`. The conversion is
    /// [`logical_to_texel`], shared with the CPU lane's probe so a hover highlight
    /// and a click cannot land on different texels.
    #[must_use]
    pub fn read_id_logical(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        local: Pos2,
        pixels_per_point: f32,
    ) -> PickId {
        match logical_to_texel(local, pixels_per_point) {
            Some((x, y)) => self.read_id(device, queue, x, y),
            None => PickId::NOTHING,
        }
    }

    /// Read a `w × h` block of ids at `(x, y)`, row-major. **One writer** for the
    /// readback: [`read_id`](Self::read_id) is this with `1 × 1`, so the one-texel
    /// click and a neighbourhood probe cannot decode bytes two different ways.
    ///
    /// Returns empty when the region does not intersect the target, or when no
    /// target is allocated.
    #[must_use]
    pub fn read_region(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        x: u32,
        y: u32,
        w: u32,
        h: u32,
    ) -> Vec<PickId> {
        let Some(tex) = &self.tex else { return Vec::new() };
        let (tw, th) = self.size;
        if x >= tw || y >= th || w == 0 || h == 0 {
            return Vec::new();
        }
        let w = w.min(tw - x);
        let h = h.min(th - y);

        // The copy + row-padding strip + `map_async` poll now come from
        // `render::gpu::readback`, the ONE writer of that dance (LAW #5). What stays
        // here is the only part that is about *ids*: the decode.
        let data = super::readback::read_texture_region(device, queue, tex, 4, x, y, w, h);
        let mut ids = Vec::with_capacity((w * h) as usize);
        for chunk in data.as_chunks::<4>().0 {
            // The SAME byte view the CPU lane uses. `copy_texture_to_buffer`
            // hands back the texel's 4 little-endian bytes, which IS
            // `PickId::to_rgba`, so the two lanes decode one number one way.
            ids.push(PickId::from_rgba([chunk[0], chunk[1], chunk[2], chunk[3]]));
        }
        ids
    }
}

impl Default for PickTarget {
    fn default() -> Self {
        Self::new()
    }
}

/// Paint index → clip depth, **lower index nearer**, strictly inside `0..1` so no
/// object lands on the depth clear value.
///
/// The one writer of the painter-order → depth rule (see
/// [`PickBatch::push_quad_in_painter_order`]). Monotonic and injective for
/// `index < count`, which is what makes the `Less` test a total order over the paint
/// list rather than a coin flip between two objects at equal depth.
#[must_use]
pub fn painter_depth(index: usize, count: usize) -> f32 {
    let n = count.max(1) as f32;
    ((index as f32 + 0.5) / n).clamp(0.0, 1.0)
}

/// A logical cursor position relative to a widget's top-left → the physical texel to
/// probe in that widget's pick target. `None` when the position is negative (the
/// cursor is outside the widget), which reads as a miss rather than clamping onto
/// the edge feature.
///
/// Plain arithmetic, no device — so the coordinate step of a click is testable
/// without a GPU, and the GPU test can assert the same function it ships.
#[must_use]
pub fn logical_to_texel(local: Pos2, pixels_per_point: f32) -> Option<(u32, u32)> {
    let ppp = if pixels_per_point > 0.0 { pixels_per_point } else { 1.0 };
    let x = local.x * ppp;
    let y = local.y * ppp;
    if x.is_nan() || x < 0.0 || y.is_nan() || y < 0.0 {
        return None; // also rejects NaN
    }
    Some((x as u32, y as u32))
}

/// The id pass pipeline + its viewport uniform. Build once from the device; call
/// [`set_viewport`](Self::set_viewport) when the pane resizes,
/// [`upload`](Self::upload) with the frame's [`PickBatch`], then
/// [`record`](Self::record) inside [`PickTarget::begin_pass`].
pub struct PickPass {
    pipeline: wgpu::RenderPipeline,
    ubo: wgpu::Buffer,
    bind: wgpu::BindGroup,
    verts: Option<wgpu::Buffer>,
    vert_cap: u64,
    vert_count: u32,
}

impl PickPass {
    /// Build the pick pipeline. `depth` must be `Some(PICK_DEPTH_FORMAT)` exactly
    /// when the [`PickTarget`] it will draw into has a depth attachment — the
    /// pipeline's depth state and the pass's attachments must agree or wgpu rejects
    /// the pass.
    ///
    /// One constructor for both lanes, parameterised: the 2-D pick pass and the 3-D
    /// pick pass differ ONLY in this `Option`, and a second `new_with_depth` copying
    /// forty lines of descriptor to change it would be the twin LAW #5 forbids.
    pub fn new(device: &wgpu::Device, depth: Option<TextureFormat>) -> Self {
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("l0_pick"),
            source: wgpu::ShaderSource::Wgsl(super::wgsl(PICK_WGSL).into()),
        });
        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("l0_pick_bgl"),
            entries: &[wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::VERTEX,
                ty: wgpu::BindingType::Buffer {
                    ty: wgpu::BufferBindingType::Uniform,
                    has_dynamic_offset: false,
                    min_binding_size: None,
                },
                count: None,
            }],
        });
        let ubo = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("l0_pick_ubo"),
            size: 16, // vec2<f32> viewport + vec2<f32> pad
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("l0_pick_bind"),
            layout: &bgl,
            entries: &[wgpu::BindGroupEntry { binding: 0, resource: ubo.as_entire_binding() }],
        });
        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("l0_pick_pipeline"),
            layout: Some(&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("l0_pick_pll"),
                bind_group_layouts: &[Some(&bgl)],
                immediate_size: 0,
            })),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("pick_vs"),
                compilation_options: Default::default(),
                buffers: &[Some(wgpu::VertexBufferLayout {
                    array_stride: PICK_VERTEX_STRIDE,
                    step_mode: wgpu::VertexStepMode::Vertex,
                    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::Uint32, offset: 12, shader_location: 2 },
                    ],
                })],
            },
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                // No back-face culling: a pickable quad's winding is the caller's
                // business, and a silently unpickable object is the worst failure
                // this pass can have.
                cull_mode: None,
                ..Default::default()
            },
            depth_stencil: depth.map(|format| wgpu::DepthStencilState {
                format,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: Default::default(),
                bias: Default::default(),
            }),
            // sample_count 1. `R32Uint` cannot be multisampled, so this is enforced
            // by wgpu validation, not by this line staying put.
            multisample: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("pick_fs"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: PICK_FORMAT,
                    // `None`, and it could not be anything else: `R32Uint` is not
                    // blendable, so a blend state here is a validation error. An id
                    // must arrive whole or not at all.
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            multiview_mask: None,
            cache: None,
        });
        Self { pipeline, ubo, bind, verts: None, vert_cap: 0, vert_count: 0 }
    }

    /// Tell the shader the target size in physical pixels — the space
    /// [`PickVertex::pos_px`] is in.
    pub fn set_viewport(&self, queue: &wgpu::Queue, w: u32, h: u32) {
        let u: [f32; 4] = [w.max(1) as f32, h.max(1) as f32, 0.0, 0.0];
        queue.write_buffer(&self.ubo, 0, bytemuck::cast_slice(&u));
    }

    /// Upload a frame's pickable geometry, growing the vertex buffer as needed.
    pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, batch: &PickBatch) {
        let verts = batch.vertices();
        self.vert_count = verts.len() as u32;
        if verts.is_empty() {
            return;
        }
        let bytes: &[u8] = bytemuck::cast_slice(verts);
        let need = bytes.len() as u64;
        if self.verts.is_none() || self.vert_cap < need {
            let cap = need.next_power_of_two().max(1024);
            self.verts = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("l0_pick_verts"),
                size: cap,
                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
            self.vert_cap = cap;
        }
        queue.write_buffer(self.verts.as_ref().expect("just allocated"), 0, bytes);
    }

    /// Record the id draw into a pass from [`PickTarget::begin_pass`]. Returns the
    /// vertex count submitted — `0` means nothing was drawn, so a caller (or a test)
    /// can tell "everything missed" from "the pass never ran", which is the
    /// distinction a `bool` loses.
    pub fn record(&self, pass: &mut wgpu::RenderPass<'_>) -> u32 {
        if self.vert_count == 0 {
            return 0;
        }
        let Some(vb) = &self.verts else { return 0 };
        pass.set_pipeline(&self.pipeline);
        pass.set_bind_group(0, &self.bind, &[]);
        pass.set_vertex_buffer(0, vb.slice(..));
        pass.draw(0..self.vert_count, 0..1);
        self.vert_count
    }

    /// Vertex count currently uploaded.
    #[must_use]
    pub fn vertex_count(&self) -> u32 {
        self.vert_count
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::pick::MAX_FEATURE;

    /// **The format is the guard.** Asserted against wgpu's own format table, not
    /// against a comment: `R32Uint` cannot filter, cannot blend, cannot multisample
    /// and is not sRGB — the four ways an id gets corrupted into a neighbour's.
    ///
    /// RED-PROVEN on a real device (2026-08-01): `PICK_FORMAT = Rgba8Unorm` — the
    /// obvious "colour-encoded" choice — fails here with *"Rgba8Unorm permits
    /// FILTERABLE — filtering blends two ids into a third"*, and `Rgba8UnormSrgb`
    /// fails identically. Measured, not assumed: FILTERABLE is checked first, so the
    /// `is_srgb` assertion below has NOT been observed red on its own — it guards a
    /// format that would have to be non-filterable *and* sRGB to reach it, which no
    /// current wgpu format is. It is a belt on braces, and saying so is the point.
    #[test]
    fn pick_format_cannot_filter_blend_or_msaa() {
        let f = PICK_FORMAT.guaranteed_format_features(wgpu::Features::empty());
        assert!(
            f.allowed_usages.contains(wgpu::TextureUsages::RENDER_ATTACHMENT),
            "the id target must be renderable"
        );
        assert!(f.allowed_usages.contains(wgpu::TextureUsages::COPY_SRC), "the id target must be readable back");
        let bad = [
            (wgpu::TextureFormatFeatureFlags::FILTERABLE, "filtering blends two ids into a third"),
            (wgpu::TextureFormatFeatureFlags::BLENDABLE, "blending blends two ids into a third"),
            (wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4, "MSAA averages ids across every silhouette"),
            (wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X2, "MSAA averages ids across every silhouette"),
        ];
        for (flag, why) in bad {
            assert!(!f.flags.contains(flag), "{PICK_FORMAT:?} permits {flag:?} — {why}");
        }
        assert!(!PICK_FORMAT.is_srgb(), "an sRGB transfer function would rewrite the id");
        assert_eq!(PICK_FORMAT.target_pixel_byte_cost(), Some(4), "a 32-bit id needs 4 bytes/texel");
    }

    /// The wire format: the four bytes `copy_texture_to_buffer` returns for an
    /// `R32Uint` texel are little-endian, which is exactly `PickId::to_rgba`. So the
    /// GPU readback decode and the CPU lane's byte view are ONE function — checked
    /// here at every **channel-carry boundary**, the ids a hand-rolled shift/mask
    /// decode gets wrong.
    #[test]
    fn every_carry_boundary_round_trips_through_the_rgba_byte_view() {
        // feature values that straddle a byte boundary, plus the ceiling.
        let features = [
            0,
            1,
            2,
            0xFE,
            0xFF,
            0x100, // R carries into G
            0x101,
            0xFFFE,
            0xFFFF,
            0x1_0000, // G carries into B
            0x1_0001,
            0xFF_FFFE,
            MAX_FEATURE, // 0xFF_FFFF — B full, next carry would hit the layer bits
        ];
        for layer in [1u8, 2, 0x7F, 0x80, 0xFE, 0xFF] {
            for feat in features {
                let id = PickId::new(layer, feat);
                assert!(!id.is_nothing(), "layer {layer} feature {feat:#x} must be a real id");
                assert_eq!(id.layer(), layer, "layer lost for feature {feat:#x}");
                assert_eq!(id.feature(), feat, "feature {feat:#x} lost for layer {layer}");
                let bytes = id.to_rgba();
                assert_eq!(bytes, id.0.to_le_bytes(), "the byte view must BE the wire bytes");
                assert_eq!(PickId::from_rgba(bytes), id, "round trip failed at layer {layer} feature {feat:#x}");
            }
        }
        // The ceiling, stated: 0xFFFF_FFFF is reachable and is the largest id.
        let top = PickId::new(0xFF, MAX_FEATURE);
        assert_eq!(top.0, u32::MAX, "the id space tops out at 0xFFFF_FFFF");
        assert_eq!(PickId::from_rgba([0xFF; 4]), top);
    }

    /// The miss sentinel is unreachable from a real feature — **by construction**,
    /// not by convention. The lowest id any drawable feature can carry is
    /// `0x0100_0000`, so a cleared texel (`0`) can never be confused with
    /// "feature 0", which is the trap in every naive id pass.
    #[test]
    fn the_miss_sentinel_is_below_every_real_id() {
        assert_eq!(PickId::NOTHING.0, 0);
        let lowest = PickId::new(1, 0);
        assert_eq!(lowest.0, 0x0100_0000, "layer 1 feature 0 is the floor of the real id space");
        assert!(lowest.0 > PickId::NOTHING.0);
        for layer in 1..=255u8 {
            assert!(!PickId::new(layer, 0).is_nothing(), "layer {layer} feature 0 must not read as a miss");
        }
        // A batch cannot smuggle a 0 in for a real feature either.
        let mut b = PickBatch::new();
        b.push_feature(Rect::from_min_size(Pos2::ZERO, egui::vec2(4.0, 4.0)), 1, 0);
        assert!(b.vertices().iter().all(|v| v.id == 0x0100_0000), "push_feature must encode, not pass through");
    }

    /// The vertex layout matches `pick.wgsl`'s attribute offsets. A stride or offset
    /// drift silently reads the id out of the position bytes.
    #[test]
    fn vertex_layout_matches_the_shader() {
        assert_eq!(PICK_VERTEX_STRIDE, 16);
        assert_eq!(std::mem::offset_of!(PickVertex, pos_px), 0);
        assert_eq!(std::mem::offset_of!(PickVertex, depth), 8);
        assert_eq!(std::mem::offset_of!(PickVertex, id), 12);
        assert!(PICK_WGSL.contains("@location(2) id: u32"), "shader must read the id at location 2");
        assert!(PICK_WGSL.contains("@interpolate(flat)"), "an id must never be interpolated");
        assert!(PICK_WGSL.contains("-> @location(0) u32"), "the fragment stage writes a raw u32 id");
        assert!(!PICK_WGSL.contains("fn px_to_ndc"), "pick.wgsl must take px_to_ndc from the ONE prelude");
        assert!(super::super::wgsl(PICK_WGSL).contains("fn px_to_ndc"), "the composer supplies it");
    }

    /// A quad is six vertices covering the rect, all carrying the same id.
    #[test]
    fn a_quad_is_six_vertices_of_one_id_over_its_rect() {
        let r = Rect::from_min_max(Pos2::new(10.0, 20.0), Pos2::new(30.0, 50.0));
        let id = PickId::new(2, 7);
        let mut b = PickBatch::new();
        b.push_quad(r, id);
        assert_eq!(b.len(), 6);
        assert!(b.vertices().iter().all(|v| v.id == id.0 && v.depth == 0.0));
        for c in [(10.0, 20.0), (30.0, 20.0), (30.0, 50.0), (10.0, 50.0)] {
            assert!(
                b.vertices().iter().any(|v| v.pos_px == [c.0, c.1]),
                "corner {c:?} missing — the quad does not cover its rect"
            );
        }
        // push_quad delegates to push_quad_at_depth: same winding, one writer.
        let mut d = PickBatch::new();
        d.push_quad_at_depth(r, id, 0.0);
        assert_eq!(b.vertices(), d.vertices());
    }

    /// The logical→texel step, and the out-of-widget miss.
    #[test]
    fn logical_to_texel_scales_and_rejects_outside() {
        assert_eq!(logical_to_texel(Pos2::new(10.0, 20.0), 1.0), Some((10, 20)));
        assert_eq!(logical_to_texel(Pos2::new(10.0, 20.0), 2.0), Some((20, 40)));
        assert_eq!(logical_to_texel(Pos2::new(10.4, 20.9), 1.5), Some((15, 31)));
        assert_eq!(logical_to_texel(Pos2::new(-0.5, 20.0), 1.0), None, "left of the widget is a miss");
        assert_eq!(logical_to_texel(Pos2::new(10.0, -3.0), 1.0), None, "above the widget is a miss");
        assert_eq!(logical_to_texel(Pos2::new(f32::NAN, 0.0), 1.0), None, "NaN is a miss, not texel 0");
        // ppp <= 0 would collapse every position onto texel 0; treat it as 1.
        assert_eq!(logical_to_texel(Pos2::new(7.0, 9.0), 0.0), Some((7, 9)));
    }
}