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
//! **OIT — order-independent transparency via per-pixel fragment linked lists**
//! (GFX_V2 §3.C, item 7).
//!
//! The thing this replaces is a CPU depth sort. Sorting *primitives* on the CPU is
//! wrong in a way that no amount of care fixes: two translucent quads that intersect,
//! or three that cycle in overlap, have **no** correct primitive order — only a
//! per-*fragment* order exists. So the sort moves to the pixel:
//!
//! ```text
//!   GATHER   every translucent fragment appends {colour, depth} to its pixel's
//!            singly-linked list (atomic head exchange + a bump allocator)
//!   RESOLVE  one fullscreen pass; each pixel walks its list into registers, sorts
//!            it, and composites back-to-front over the background
//! ```
//!
//! **The claim, stated so it can fail.** The rendered pixels are *bit-identical* for
//! any permutation of the submission order — not "visually similar", not "within a
//! tolerance". [`OIT_WGSL`]'s header derives why from the two properties that carry
//! it (a **total** sort order including a tie-break on colour, and an overflow policy
//! that *selects the nearest* rather than truncating the first N seen). A depth-sorted
//! alpha blend cannot satisfy that claim, and neither can a depth-only comparator.
//!
//! **Why the claim alone is not a proof.** A resolve that returned a constant would
//! also be identical across every order — the identity-value trap. So the device test
//! (`facett-core/tests/gpu_oit.rs`) pairs the cross-order identity with an
//! *independent CPU oracle* of the composite at named pixels, a distinctness check
//! across regions, and the device's own fragment counter read back out of
//! [`OitPass::fragment_count`]. Identity plus correctness plus non-blankness; any one
//! alone is hollow.
//!
//! The depth key is a **vertex attribute**, not the rasterised z, and the gather pass
//! runs with no depth attachment at all. That is what makes the pass usable for
//! painter-ordered 2-D vector layers (feed it [`crate::render::gpu::picking::painter_depth`]-style
//! layer indices) and for true 3-D translucent geometry (feed it view-space depth)
//! without twinning the pipeline.

use wgpu::TextureFormat;

/// The linked-list OIT shader (gather + resolve). Composed behind
/// [`COMMON_WGSL`](super::COMMON_WGSL) by [`super::wgsl`], from which it takes
/// `px_to_ndc` — so its screen mapping is the same one the line lanes use.
pub use crate::render::wgsl::OIT_WGSL;

/// Fragments per pixel the resolve carries in registers. **Must match `OIT_LAYERS`
/// in [`OIT_WGSL`]** — asserted in this module's tests.
pub const OIT_MAX_LAYERS: u32 = 16;

/// Default fragments *allocated* per pixel. Independent of [`OIT_MAX_LAYERS`]: the
/// node buffer may hold more than the resolve can composite (the overflow policy then
/// picks the nearest `OIT_MAX_LAYERS`), or fewer (the allocator then drops the tail).
pub const OIT_DEFAULT_LAYERS: u32 = 8;

/// Size of one [`OitNode`] on the device: `{u32 colour, f32 depth, u32 next}`.
pub const OIT_NODE_SIZE: u64 = 12;

/// One vertex of translucent geometry. Position in **physical pixels**, depth as the
/// sort key (any monotone "farther is larger" scale), colour as straight RGBA.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct OitVertex {
    /// Physical-pixel position; `px_to_ndc` maps it in the shader.
    pub pos: [f32; 2],
    /// Sort key — **larger is farther**. Not the rasterised z.
    pub depth: f32,
    /// Explicit tail padding so the Rust and WGSL strides cannot disagree.
    pub _pad: f32,
    /// Straight (non-premultiplied) RGBA in `0..=1`. Quantised to 8 bits per channel
    /// by `pack4x8unorm` when it lands in a node.
    pub color: [f32; 4],
}

/// Byte stride of [`OitVertex`].
pub const OIT_VERTEX_STRIDE: u64 = 32;

#[repr(C)]
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
struct OitUniform {
    viewport: [f32; 2],
    dims: [u32; 2],
    background: [f32; 4],
    capacity: u32,
    _pad: [u32; 3],
}

/// CPU-side translucent geometry for one frame. Mirrors
/// [`PickBatch`](super::picking::PickBatch)'s shape on purpose: one winding writer
/// ([`push_tri`](Self::push_tri)) that every other push delegates to, so a quad and a
/// triangle cannot disagree about orientation.
#[derive(Clone, Debug, Default)]
pub struct OitBatch {
    verts: Vec<OitVertex>,
}

impl OitBatch {
    /// An empty batch.
    #[must_use]
    pub fn new() -> Self {
        Self { verts: Vec::new() }
    }

    /// Drop all geometry, keeping the allocation.
    pub fn clear(&mut self) {
        self.verts.clear();
    }

    /// **The one winding writer.** Append a triangle at a sort depth.
    pub fn push_tri(
        &mut self,
        a: egui::Pos2,
        b: egui::Pos2,
        c: egui::Pos2,
        color: [f32; 4],
        depth: f32,
    ) {
        for p in [a, b, c] {
            self.verts.push(OitVertex { pos: [p.x, p.y], depth, _pad: 0.0, color });
        }
    }

    /// Append an axis-aligned rect as two triangles, sharing the `min→max` diagonal.
    ///
    /// The two triangles carry the **same** depth and colour, so the fragments on their
    /// shared diagonal are a genuine tie — exactly the case [`OIT_WGSL`]'s colour
    /// tie-break exists for. This is why a quad is a useful test primitive rather than
    /// a degenerate one.
    pub fn push_quad(&mut self, rect: egui::Rect, color: [f32; 4], depth: f32) {
        let (a, b) = (rect.min, rect.max);
        let tr = egui::pos2(b.x, a.y);
        let bl = egui::pos2(a.x, b.y);
        self.push_tri(a, tr, b, color, depth);
        self.push_tri(a, b, bl, color, depth);
    }

    /// The raw vertices, for upload.
    #[must_use]
    pub fn vertices(&self) -> &[OitVertex] {
        &self.verts
    }

    /// Vertex count.
    #[must_use]
    pub fn len(&self) -> usize {
        self.verts.len()
    }

    /// Whether nothing has been pushed.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.verts.is_empty()
    }

    /// Return a copy of this batch with its triangles in a different **submission
    /// order**, permuting whole triangles and never splitting one.
    ///
    /// This exists so the order-independence proof cannot cheat: the permutation is a
    /// property of the batch, applied by the batch, and `verts` is private — a test
    /// cannot accidentally reorder half a triangle and then "prove" something about a
    /// frame whose geometry it silently changed.
    #[must_use]
    pub fn permuted(&self, order: &[usize]) -> Self {
        let mut out = Self { verts: Vec::with_capacity(self.verts.len()) };
        for &t in order {
            let base = t * 3;
            if base + 2 < self.verts.len() {
                out.verts.extend_from_slice(&self.verts[base..base + 3]);
            }
        }
        out
    }

    /// Number of whole triangles in the batch.
    #[must_use]
    pub fn tri_count(&self) -> usize {
        self.verts.len() / 3
    }
}

/// The two-pass OIT lane: pipelines, the three storage buffers, and the per-frame
/// record. Persistent — `ensure` is idempotent within a size, like every other
/// target in this module.
pub struct OitPass {
    gather: wgpu::RenderPipeline,
    resolve: wgpu::RenderPipeline,
    bgl: wgpu::BindGroupLayout,
    uniform: wgpu::Buffer,
    /// One `u32` head pointer per pixel; `0` = empty list.
    heads: Option<wgpu::Buffer>,
    /// `capacity` × [`OIT_NODE_SIZE`] fragment records.
    nodes: Option<wgpu::Buffer>,
    /// Single-`u32` bump allocator, also the device's fragment counter.
    alloc: wgpu::Buffer,
    bind: Option<wgpu::BindGroup>,
    verts: Option<wgpu::Buffer>,
    vert_cap: usize,
    vertex_count: u32,
    size: (u32, u32),
    capacity: u32,
    layers: u32,
}

impl OitPass {
    /// Whether `adapter` can run this lane at all.
    ///
    /// A fragment shader writing to a storage buffer is
    /// [`wgpu::DownlevelFlags::FRAGMENT_WRITABLE_STORAGE`], which is **not** universal
    /// — and it is the whole mechanism here, so a caller must ask rather than discover
    /// it as a pipeline-creation panic. Reported explicitly rather than skipped
    /// silently: a lane that quietly does nothing is the failure mode this track keeps
    /// finding.
    #[must_use]
    pub fn supported(adapter: &wgpu::Adapter) -> bool {
        adapter
            .get_downlevel_capabilities()
            .flags
            .contains(wgpu::DownlevelFlags::FRAGMENT_WRITABLE_STORAGE)
    }

    /// Build both pipelines for a `target_format` colour attachment.
    #[must_use]
    pub fn new(device: &wgpu::Device, target_format: TextureFormat) -> Self {
        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("l0_oit"),
            source: wgpu::ShaderSource::Wgsl(super::wgsl(OIT_WGSL).into()),
        });
        let storage = |binding: u32| wgpu::BindGroupLayoutEntry {
            binding,
            visibility: wgpu::ShaderStages::FRAGMENT,
            ty: wgpu::BindingType::Buffer {
                ty: wgpu::BufferBindingType::Storage { read_only: false },
                has_dynamic_offset: false,
                min_binding_size: None,
            },
            count: None,
        };
        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("l0_oit_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,
                },
                storage(1),
                storage(2),
                storage(3),
            ],
        });
        let pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("l0_oit_pll"),
            bind_group_layouts: &[Some(&bgl)],
            immediate_size: 0,
        });

        let gather = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("l0_oit_gather"),
            layout: Some(&pll),
            vertex: wgpu::VertexState {
                module: &module,
                entry_point: Some("oit_gather_vs"),
                compilation_options: Default::default(),
                buffers: &[wgpu::VertexBufferLayout {
                    array_stride: OIT_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::Float32x4,
                            offset: 16,
                            shader_location: 2,
                        },
                    ],
                }],
            },
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                // NO culling. A translucent surface is visible from both sides, and a
                // cull would make the result depend on winding — which is the same
                // class of bug as depending on submission order.
                cull_mode: None,
                ..Default::default()
            },
            // NO depth attachment: the rasteriser must not reject a fragment. Depth
            // travels as a vertex attribute and is resolved per pixel.
            depth_stencil: None,
            multisample: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &module,
                entry_point: Some("oit_gather_fs"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: target_format,
                    blend: None,
                    // The gather writes only to storage. An empty mask says so on the
                    // pipeline, so a future edit that started returning real colour
                    // here would be visibly dead rather than double-composited.
                    write_mask: wgpu::ColorWrites::empty(),
                })],
            }),
            multiview_mask: None,
            cache: None,
        });

        let resolve = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("l0_oit_resolve"),
            layout: Some(&pll),
            vertex: wgpu::VertexState {
                module: &module,
                entry_point: Some("oit_resolve_vs"),
                compilation_options: Default::default(),
                buffers: &[],
            },
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                ..Default::default()
            },
            depth_stencil: None,
            multisample: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &module,
                entry_point: Some("oit_resolve_fs"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: target_format,
                    // The resolve *is* the composite. Blending it again would apply
                    // the transparency twice.
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            multiview_mask: None,
            cache: None,
        });

        Self {
            gather,
            resolve,
            bgl,
            uniform: device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("l0_oit_uniform"),
                size: std::mem::size_of::<OitUniform>() as u64,
                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }),
            heads: None,
            nodes: None,
            alloc: device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("l0_oit_alloc"),
                size: 4,
                usage: wgpu::BufferUsages::STORAGE
                    | wgpu::BufferUsages::COPY_DST
                    | wgpu::BufferUsages::COPY_SRC,
                mapped_at_creation: false,
            }),
            bind: None,
            verts: None,
            vert_cap: 0,
            vertex_count: 0,
            size: (0, 0),
            capacity: 0,
            layers: 0,
        }
    }

    /// (Re)allocate the head-pointer and node buffers for `w × h` pixels with
    /// `layers` fragments of node storage per pixel. Idempotent within a size+layers.
    pub fn ensure(&mut self, device: &wgpu::Device, w: u32, h: u32, layers: u32) {
        let w = w.max(1);
        let h = h.max(1);
        let layers = layers.max(1);
        if self.size == (w, h) && self.layers == layers && self.heads.is_some() {
            return;
        }
        let pixels = u64::from(w) * u64::from(h);
        let heads = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("l0_oit_heads"),
            size: pixels * 4,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let capacity = pixels * u64::from(layers);
        let nodes = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("l0_oit_nodes"),
            size: capacity * OIT_NODE_SIZE,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.bind = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("l0_oit_bind"),
            layout: &self.bgl,
            entries: &[
                wgpu::BindGroupEntry { binding: 0, resource: self.uniform.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 1, resource: heads.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 2, resource: nodes.as_entire_binding() },
                wgpu::BindGroupEntry { binding: 3, resource: self.alloc.as_entire_binding() },
            ],
        }));
        self.heads = Some(heads);
        self.nodes = Some(nodes);
        self.size = (w, h);
        self.layers = layers;
        self.capacity = u32::try_from(capacity).unwrap_or(u32::MAX);
    }

    /// Write this frame's uniform: viewport, list stride, node capacity, background.
    pub fn set_frame(&self, queue: &wgpu::Queue, background: [f32; 4]) {
        let (w, h) = self.size;
        queue.write_buffer(
            &self.uniform,
            0,
            bytemuck::bytes_of(&OitUniform {
                viewport: [w as f32, h as f32],
                dims: [w, h],
                background,
                capacity: self.capacity,
                _pad: [0; 3],
            }),
        );
    }

    /// Upload `batch`, growing the vertex buffer in powers of two.
    pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, batch: &OitBatch) {
        let verts = batch.vertices();
        self.vertex_count = u32::try_from(verts.len()).unwrap_or(u32::MAX);
        if verts.is_empty() {
            return;
        }
        if self.vert_cap < verts.len() || self.verts.is_none() {
            let cap = verts.len().next_power_of_two();
            self.verts = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("l0_oit_verts"),
                size: cap as u64 * OIT_VERTEX_STRIDE,
                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
            self.vert_cap = cap;
        }
        if let Some(vb) = &self.verts {
            queue.write_buffer(vb, 0, bytemuck::cast_slice(verts));
        }
    }

    /// Reset the per-frame lists: every head back to "empty" and the allocator to 0.
    ///
    /// A `clear_buffer` rather than a clear compute pass — which is the whole reason
    /// node links are 1-based. Zero is a free clear on every backend.
    pub fn reset(&self, encoder: &mut wgpu::CommandEncoder) {
        if let Some(heads) = &self.heads {
            encoder.clear_buffer(heads, 0, None);
        }
        encoder.clear_buffer(&self.alloc, 0, None);
    }

    /// **The frame.** Reset the lists, gather every fragment, then resolve into
    /// `target`. Returns the vertex count submitted — so "everything was clipped away"
    /// is distinguishable from "the pass never ran".
    ///
    /// `target` must be a `target_format` view of the size passed to [`Self::ensure`].
    pub fn record(&self, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView) -> u32 {
        let Some(bind) = &self.bind else { return 0 };
        self.reset(encoder);

        if self.vertex_count > 0 {
            if let Some(vb) = &self.verts {
                let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                    label: Some("l0_oit_gather_pass"),
                    // The attachment is a formality (the write mask is empty); binding
                    // the eventual target costs no extra memory.
                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                        view: target,
                        resolve_target: None,
                        depth_slice: None,
                        ops: wgpu::Operations {
                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
                            store: wgpu::StoreOp::Store,
                        },
                    })],
                    depth_stencil_attachment: None,
                    timestamp_writes: None,
                    occlusion_query_set: None,
                    multiview_mask: None,
                });
                rp.set_pipeline(&self.gather);
                rp.set_bind_group(0, bind, &[]);
                rp.set_vertex_buffer(0, vb.slice(..));
                rp.draw(0..self.vertex_count, 0..1);
            }
        }

        {
            let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("l0_oit_resolve_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: target,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations {
                        // The resolve writes the background itself, from the uniform,
                        // so it does not depend on what the gather pass left behind.
                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            rp.set_pipeline(&self.resolve);
            rp.set_bind_group(0, bind, &[]);
            rp.draw(0..3, 0..1);
        }

        self.vertex_count
    }

    /// Read the device's own count of fragments the gather emitted this frame.
    ///
    /// Not a host-side tally: this is the bump allocator the shader `atomicAdd`ed, read
    /// back out of GPU memory. It is the assertion that distinguishes "the OIT frame is
    /// stable across submission orders" from "the OIT frame is stable because no
    /// fragment ever reached a list".
    ///
    /// Counts *attempts*, including allocations past `capacity` that were dropped — so
    /// comparing it to `capacity` tells a caller whether it overflowed.
    #[must_use]
    pub fn fragment_count(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> u32 {
        let bytes = super::readback::read_buffer_range(device, queue, &self.alloc, 0, 4);
        if bytes.len() < 4 {
            return 0;
        }
        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
    }

    /// Node-buffer capacity in fragments (`w · h · layers`).
    #[must_use]
    pub fn capacity(&self) -> u32 {
        self.capacity
    }

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

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

    /// Vertices submitted by the last [`Self::upload`].
    #[must_use]
    pub fn vertex_count(&self) -> u32 {
        self.vertex_count
    }
}

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

    /// The Rust `OitVertex` and the WGSL vertex attributes describe the same bytes.
    /// Offsets are quoted in [`OitPass::new`]; if the struct is reordered this is the
    /// guard that notices.
    #[test]
    fn vertex_layout_matches_the_shader() {
        assert_eq!(std::mem::size_of::<OitVertex>() as u64, OIT_VERTEX_STRIDE);
        assert_eq!(std::mem::offset_of!(OitVertex, pos), 0);
        assert_eq!(std::mem::offset_of!(OitVertex, depth), 8);
        assert_eq!(std::mem::offset_of!(OitVertex, color), 16);
        assert_eq!(std::mem::size_of::<OitUniform>(), 48, "the uniform is 3 × 16 B");
    }

    /// `OIT_MAX_LAYERS` is a Rust mirror of a WGSL `const`. Mirrors drift, so read the
    /// actual shader text rather than trusting the comment next to it.
    #[test]
    fn the_layer_count_constant_is_the_same_number_on_both_sides() {
        let want = format!("const OIT_LAYERS: u32 = {OIT_MAX_LAYERS}u;");
        assert!(
            OIT_WGSL.contains(&want),
            "OIT_MAX_LAYERS ({OIT_MAX_LAYERS}) must match the shader's OIT_LAYERS; looked for `{want}`"
        );
    }

    // `oit.wgsl`'s use of the ONE prelude is asserted by the shared loop in
    // `super::tests::every_shared_shader_takes_its_maths_from_the_one_prelude`, which
    // this shader was added to rather than given a private twin of (LAW #5).

    /// The gather must not be able to reject a fragment. Both of these are how that
    /// would silently happen, and both are stated in the source as deliberate.
    #[test]
    fn the_gather_neither_depth_tests_nor_culls() {
        // Not a state round-trip: this reads the *shader text* for the two things that
        // would make the linked list incomplete, and the pipeline setters live in one
        // function whose source is checked by the compiler to have those literals.
        assert!(
            OIT_WGSL.contains("o.clip = vec4<f32>(px_to_ndc(v.pos_px, u.viewport), 0.0, 1.0);"),
            "the gather must emit a constant z — a varying z with a depth test would cull"
        );
    }

    /// A quad is two triangles over its rect, sharing the diagonal, at one depth.
    #[test]
    fn a_quad_is_two_triangles_of_one_depth_over_its_rect() {
        let mut b = OitBatch::new();
        let r = egui::Rect::from_min_max(egui::pos2(10.0, 20.0), egui::pos2(30.0, 50.0));
        b.push_quad(r, [0.25, 0.5, 0.75, 0.5], 3.5);
        assert_eq!(b.len(), 6);
        assert_eq!(b.tri_count(), 2);
        assert!(b.vertices().iter().all(|v| v.depth == 3.5), "one depth");
        assert!(b.vertices().iter().all(|v| v.color == [0.25, 0.5, 0.75, 0.5]), "one colour");
        let xs: Vec<f32> = b.vertices().iter().map(|v| v.pos[0]).collect();
        let ys: Vec<f32> = b.vertices().iter().map(|v| v.pos[1]).collect();
        assert_eq!(xs.iter().cloned().fold(f32::MAX, f32::min), 10.0);
        assert_eq!(xs.iter().cloned().fold(f32::MIN, f32::max), 30.0);
        assert_eq!(ys.iter().cloned().fold(f32::MAX, f32::min), 20.0);
        assert_eq!(ys.iter().cloned().fold(f32::MIN, f32::max), 50.0);
        // Both triangles carry the min→max diagonal, which is what makes the shared
        // edge a genuine depth+colour tie.
        assert_eq!(b.vertices()[0].pos, [10.0, 20.0]);
        assert_eq!(b.vertices()[2].pos, [30.0, 50.0]);
        assert_eq!(b.vertices()[3].pos, [10.0, 20.0]);
        assert_eq!(b.vertices()[4].pos, [30.0, 50.0]);
    }

    /// `permuted` reorders whole triangles and never splits one — the property the
    /// order-independence proof depends on for its permutations to be *the same scene*.
    #[test]
    fn permuting_reorders_whole_triangles_and_preserves_the_multiset() {
        let mut b = OitBatch::new();
        for i in 0..4u32 {
            let x = i as f32 * 10.0;
            b.push_quad(
                egui::Rect::from_min_max(egui::pos2(x, 0.0), egui::pos2(x + 5.0, 5.0)),
                [i as f32 / 4.0, 0.0, 0.0, 0.5],
                i as f32,
            );
        }
        let n = b.tri_count();
        assert_eq!(n, 8);
        let rev: Vec<usize> = (0..n).rev().collect();
        let p = b.permuted(&rev);
        assert_eq!(p.len(), b.len(), "same vertex count");

        // Every triangle survives intact: each 3-vertex group of the permutation is a
        // 3-vertex group of the original.
        let groups = |x: &OitBatch| -> Vec<[OitVertex; 3]> {
            x.vertices().chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
        };
        let (og, pg) = (groups(&b), groups(&p));
        assert_eq!(pg.len(), og.len());
        for g in &pg {
            assert!(og.contains(g), "each permuted triangle is an original triangle");
        }
        // And it genuinely reordered — otherwise the whole proof is vacuous.
        assert_ne!(pg, og, "the reversal must actually change the submission order");
        assert_eq!(pg.iter().rev().cloned().collect::<Vec<_>>(), og, "…by reversing it");
    }

    /// An out-of-range permutation index drops that triangle instead of panicking, and
    /// an empty order yields an empty batch.
    #[test]
    fn permuting_is_total() {
        let mut b = OitBatch::new();
        b.push_quad(egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(4.0, 4.0)), [1.0; 4], 0.0);
        assert_eq!(b.permuted(&[99]).len(), 0, "out-of-range triangle is dropped");
        assert_eq!(b.permuted(&[]).len(), 0);
        assert_eq!(b.permuted(&[0, 1]).len(), 6);
    }
}