facett-core 0.1.18

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
//! **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.chunks_exact(4) {
            // 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 >= 0.0) || !(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: &[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)));
    }
}