concinnity-device 0.19.24

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
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
// src/metal/draw/main.rs
//
// Main pass (off-screen HDR + 4x MSAA). Renders the visible scene into the
// HDR colour + depth attachments, multisample-resolving to `hdr_resolve`.
//
// One geometry path: the GPU-driven bindless pass, issued through the cull's
// indirect command buffers. Static objects, folded instances and the folded
// skinned tail are all cull records, so the encode is one prefix range per
// shader bucket plus the skinned tail. `object_buffer` / `bindless_tex_args`
// are `Some` exactly when the world has something to draw.
//
// The pass is encoded on a single `MTLRenderCommandEncoder`. Two
// `MTLParallelRenderCommandEncoder` attempts (one full-fat with parallel
// encoders on every shadow cascade, one scoped to just this main pass with 3
// sub-encoders + no pass-timing) both tripped G14X (M2/M3 Pro/Max class) into
// an abort inside `IOGPUMetalCommandBufferStorageAllocResourceAtIndex`: the
// crash window scaled with parallel-encoder usage rate (~20 s with shadow
// split, ~90 s with main-only) but never went away. The mechanism appears
// fundamentally incompatible with our usage on this hardware / macOS 26.4
// combo.
#![deny(unsafe_op_in_unsafe_fn)]

use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
    MTLBuffer, MTLClearColor, MTLCommandBuffer as _, MTLCommandEncoder as _, MTLLoadAction,
    MTLRenderCommandEncoder as _, MTLRenderPassDescriptor, MTLStoreAction,
};

use crate::metal::context::{BINDLESS_TEXTURE_ARG_BUFFER_INDEX, MtlContext};
use crate::metal::encode::RenderEncode;
use crate::metal::scoped_encoder::ScopedEncoder;
use concinnity_core::render::uniforms::ViewUniforms;

// Camera state a main-pass encode builds its ViewUniforms from. `view` is
// `self.view_matrix` for the on-screen main pass (and its phase-2 sibling) but
// the caller's face view matrix for a reflection-probe face capture, so it
// travels with the rest of the camera state rather than being read off `self`.
#[derive(Clone, Copy)]
pub(in crate::metal) struct MainPassCamera {
    pub elapsed: f32,
    pub vp: [[f32; 4]; 4],
    pub view: [[f32; 4]; 4],
    pub cam_pos: [f32; 3],
}

// The GPU-driven buffers the main pass consumes. `object_buffer` and
// `bindless_tex_args` are `Some` together exactly when the cull path ran this
// frame; a world with nothing to draw leaves them `None` and the pass stops at
// its clear. `counts` is the record set those buffers were built for, which the
// indirect-draw ranges address; it is the live draw list for a frame pass and an
// earlier snapshot for the reflection-probe bake.
#[derive(Clone, Copy)]
pub(in crate::metal) struct GpuFrameBuffers<'a> {
    pub object_buffer: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
    pub bindless_tex_args: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
    pub deformed_skinned: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
    pub counts: crate::metal::context::DrawRecordCounts,
}

// The reflection-probe face attachments `encode_main_into_face` renders into
// instead of the HDR targets: a square MSAA colour + depth, resolving colour
// into `resolve`.
#[derive(Clone, Copy)]
pub(in crate::metal) struct FaceTargets<'a> {
    pub color_msaa: &'a ProtocolObject<dyn objc2_metal::MTLTexture>,
    pub depth_msaa: &'a ProtocolObject<dyn objc2_metal::MTLTexture>,
    pub resolve: &'a ProtocolObject<dyn objc2_metal::MTLTexture>,
    // Array slice of `resolve` this face resolves into. The probe capture passes
    // the cube face it is rendering; the planar mirror's target is a plain 2D
    // texture, so it passes 0.
    pub resolve_slice: usize,
}

impl MtlContext {
    // 1.0 when a reflection-resolve pass (SSR resolve or RT reflections) will
    // composite over this frame's HDR target, else 0.0. Mirrors the
    // `scene_input` gate in draw/mod.rs (both resolves write `ssr.targets.output`
    // and the graph picks RT over SSR). The forward shader reads it from
    // `ViewUniforms.reflections_enabled` to hand glossy specular to that resolve.
    fn reflection_resolve_active(&self) -> f32 {
        if self.ssr.settings.is_some() || self.rt.accel.is_some() {
            1.0
        } else {
            0.0
        }
    }

    // The frame's unlit flag for ViewUniforms, from the viewport view mode.
    fn shade_mode(&self) -> f32 {
        if self.view.mode == concinnity_core::gfx::view_modes::ViewMode::Unlit {
            1.0
        } else {
            0.0
        }
    }

    // pub(in crate::metal) so the render-graph executor in
    // metal/graph_exec.rs can dispatch this pass from a CompiledGraph.
    pub(in crate::metal) fn encode_main_pass(
        &self,
        cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
        camera: MainPassCamera,
        gpu: GpuFrameBuffers,
        world_hidden: bool,
    ) -> Result<u32, String> {
        let MainPassCamera {
            elapsed,
            vp,
            view: _,
            cam_pos,
        } = camera;
        // Only `object_buffer` gates the descriptor's store action below; the
        // rest of `gpu` travels intact into `encode_main_static_into`.
        let object_buffer = gpu.object_buffer;
        // Build the HDR render pass descriptor. Colour writes into the MSAA
        // attachment and resolves into the single-sample target at end-of-pass;
        // depth lives entirely on the MSAA attachment and is discarded unless
        // a later pass (projected decals, volumetric fog) needs to sample it.
        // Under two-pass occlusion the phase-2 main pass (`Main2`) loads this
        // pass's MSAA colour to draw the disoccluded geometry on top, so the
        // MSAA samples must be stored, not just resolved away. `Main2`
        // performs the final resolve. Without two-pass we resolve-and-discard
        // the MSAA samples as before. The decision mirrors the graph's two-pass
        // gating (`two_pass_occlusion` config AND the bindless cull path active
        // this frame, i.e. an `object_buffer` exists).
        let store_msaa_color = self.cull.two_pass_occlusion
            && object_buffer.is_some()
            && self.cull.pipeline_phase2.is_some();
        let main_pass_desc = MTLRenderPassDescriptor::new();
        let [r, g, b, a] = self.view.clear_color;
        // SAFETY: plain descriptor property setters; the subscripted slots are ones this descriptor
        // declares.
        unsafe {
            let ca = main_pass_desc
                .colorAttachments()
                .objectAtIndexedSubscript(0);
            ca.setTexture(Some(self.hdr_targets.hdr_color.as_ref()));
            ca.setResolveTexture(Some(self.hdr_targets.hdr_resolve.as_ref()));
            ca.setLoadAction(MTLLoadAction::Clear);
            ca.setStoreAction(if store_msaa_color {
                MTLStoreAction::StoreAndMultisampleResolve
            } else {
                MTLStoreAction::MultisampleResolve
            });
            ca.setClearColor(MTLClearColor {
                red: r as f64,
                green: g as f64,
                blue: b as f64,
                alpha: a as f64,
            });

            let da = main_pass_desc.depthAttachment();
            da.setTexture(Some(self.hdr_targets.depth.as_ref()));
            da.setLoadAction(MTLLoadAction::Clear);
            da.setClearDepth(1.0);
            // Always resolve depth into the single-sample
            // `hdr_targets.depth_resolve` sibling. This is the canonical
            // post-rasterise scene depth that the post chain consumes:
            // raymarch writes hit depth into it; water / decal / fog
            // sample it (single-sample is enough since they only ever
            // read sample 0 anyway). `Sample0` filter matches the
            // existing MSAA-sample-0 read pattern bit-for-bit. The MSAA
            // attachment also stays alive (`StoreAndMultisampleResolve`):
            // the raymarch fragment shader samples it as a read-only
            // snapshot to drive the cone-march early-out without
            // aliasing the writable depth target.
            da.setResolveTexture(Some(self.hdr_targets.depth_resolve.as_ref()));
            da.setDepthResolveFilter(objc2_metal::MTLMultisampleDepthResolveFilter::Sample0);
            da.setStoreAction(MTLStoreAction::StoreAndMultisampleResolve);
        }

        if let Some(t) = &self.diagnostics.pass_timing {
            t.attach_render(&main_pass_desc, super::super::pass_timing::PassId::Main);
        }
        // ScopedEncoder ends the main HDR pass (MSAA resolves into hdr_resolve)
        // and pops the debug group when it drops at end of scope.
        let encoder = ScopedEncoder::new(
            cmd_buf
                .renderCommandEncoderWithDescriptor(&main_pass_desc)
                .ok_or("failed to get render encoder")?,
            "main pass",
        );
        // Wireframe view: fill mode is encoder state that indirect commands
        // inherit, so the one call covers the ICB sub-paths too.
        if self.view.mode == concinnity_core::gfx::view_modes::ViewMode::Wireframe {
            encoder.setTriangleFillMode(objc2_metal::MTLTriangleFillMode::Lines);
        }

        let view_uniforms = ViewUniforms {
            vp,
            view: self.view.matrix,
            elapsed,
            reflections_enabled: self.reflection_resolve_active(),
            cam_pos,
            prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
            shade_mode: self.shade_mode(),
            _end_pad: 0.0,
            sky_rot: self.view.sky_rot,
        };

        // While the world is hidden behind an opaque menu, the pass stops at the
        // descriptor's Clear load action. A scene-less world (no main pipeline)
        // takes the same bare-clear shape every frame.
        if world_hidden || self.pipeline_state.is_none() {
            return Ok(0);
        }

        // Main camera: bind the per-cluster light lists once for the pass.
        self.bind_clusters(&encoder, true);
        Ok(self.encode_main_geometry_into(
            &encoder,
            &view_uniforms,
            gpu,
            // Main pass: the main cull ICB (no override).
            None,
        ))
    }

    // Render the main pass into one reflection-probe cube face instead of the
    // HDR targets. A thin sibling of `encode_main_pass`: same three geometry
    // sub-paths and shared bindings, but the render-pass descriptor points at a
    // square MSAA colour + depth (resolving colour into `face_resolve`), and the
    // view + view-projection are the caller's face matrices (not `self.*`), so
    // the capture never disturbs the frame's camera state. Depth is cleared and
    // discarded -- the probe consumes only the resolved colour. Driven by
    // `capture_reflection_probe` (metal/probe.rs), once at first frame.
    pub(in crate::metal) fn encode_main_into_face(
        &self,
        cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
        face_targets: FaceTargets,
        camera: MainPassCamera,
        gpu: GpuFrameBuffers,
        // Bindless ICB to execute instead of the main cull's. The planar mirror
        // render passes its slot's mirror ICB (culled against the reflected
        // frustum); the probe capture passes `None` (reuses the main cull ICB).
        icb_override: Option<&ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>,
    ) -> Result<u32, String> {
        let FaceTargets {
            color_msaa: face_color_msaa,
            depth_msaa: face_depth_msaa,
            resolve: face_resolve,
            resolve_slice,
        } = face_targets;
        let MainPassCamera {
            elapsed,
            vp,
            view,
            cam_pos,
        } = camera;
        let desc = MTLRenderPassDescriptor::new();
        let [r, g, b, a] = self.view.clear_color;
        // SAFETY: plain descriptor property setters; the subscripted slots are ones this descriptor
        // declares.
        unsafe {
            let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
            ca.setTexture(Some(face_color_msaa));
            ca.setResolveTexture(Some(face_resolve));
            ca.setResolveSlice(resolve_slice);
            ca.setLoadAction(MTLLoadAction::Clear);
            ca.setStoreAction(MTLStoreAction::MultisampleResolve);
            ca.setClearColor(MTLClearColor {
                red: r as f64,
                green: g as f64,
                blue: b as f64,
                alpha: a as f64,
            });

            let da = desc.depthAttachment();
            da.setTexture(Some(face_depth_msaa));
            da.setLoadAction(MTLLoadAction::Clear);
            da.setClearDepth(1.0);
            da.setStoreAction(MTLStoreAction::DontCare);
        }

        let encoder = ScopedEncoder::new(
            cmd_buf
                .renderCommandEncoderWithDescriptor(&desc)
                .ok_or("failed to get probe render encoder")?,
            "probe face",
        );

        let view_uniforms = ViewUniforms {
            vp,
            view,
            elapsed,
            // Probe-face bake: no reflection resolve runs over the probe cube, so
            // the forward probe specular is the only reflection source here. Keep
            // it (0.0) so captured glossy surfaces are not flattened.
            reflections_enabled: 0.0,
            cam_pos,
            prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
            // A probe capture is always lit, whatever the viewport shows.
            shade_mode: 0.0,
            _end_pad: 0.0,
            sky_rot: self.view.sky_rot,
        };

        // Planar / probe re-render: the main camera's cluster grid does not match
        // this viewpoint, so iterate every local light instead of the clusters.
        self.bind_clusters(&encoder, false);
        Ok(self.encode_main_geometry_into(&encoder, &view_uniforms, gpu, icb_override))
    }

    // Phase-2 main pass for two-pass occlusion (`Main2`). Loads (does not
    // clear) the HDR colour + depth that `encode_main_pass` (phase 1) wrote
    // and re-runs the bindless indirect draw through `cull_icb_2` (the phase-2
    // cull's output), depth-compositing the disoccluded geometry with phase 1.
    // Folded instances AND folded skinned objects ride the unified cull buffers
    // as cullable records, so both are Hi-Z-culled through both phases: drawn in
    // phase 1 when visible, and redrawn here only when phase 1 Hi-Z-occluded
    // them and the rebuilt pyramid (Cull2) disoccludes them. The shared
    // `execute_bindless_static_icb` issues the static+instance range then the
    // skinned tail of `cull_icb_2` (the phase-2 cull resets every already-drawn
    // slot, so nothing double-draws). Resolves colour + depth at end-of-pass so
    // the post-decoration stack reads the combined result. A no-op (returns 0)
    // when there is nothing to redraw: two-pass off, no bindless geometry, or
    // the phase-2 ICB was not built.
    pub(in crate::metal) fn encode_main_pass_phase2(
        &self,
        cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
        camera: MainPassCamera,
        gpu: GpuFrameBuffers,
    ) -> Result<u32, String> {
        let MainPassCamera {
            elapsed,
            vp,
            view: _,
            cam_pos,
        } = camera;
        let GpuFrameBuffers {
            object_buffer,
            bindless_tex_args,
            deformed_skinned,
            counts,
        } = gpu;
        let (Some(obj_buf), Some(tex_args), false) = (
            object_buffer,
            bindless_tex_args,
            self.cull.icbs_2.is_empty(),
        ) else {
            return Ok(0);
        };

        // Load the phase-1 MSAA colour + depth (phase 1 stored them under
        // two-pass), draw the disoccluded geometry on top, and resolve both at
        // end-of-pass: this resolve is the one the post stack consumes.
        let main_pass_desc = MTLRenderPassDescriptor::new();
        // SAFETY: plain descriptor property setters; the subscripted slots are ones this descriptor
        // declares.
        unsafe {
            let ca = main_pass_desc
                .colorAttachments()
                .objectAtIndexedSubscript(0);
            ca.setTexture(Some(self.hdr_targets.hdr_color.as_ref()));
            ca.setResolveTexture(Some(self.hdr_targets.hdr_resolve.as_ref()));
            ca.setLoadAction(MTLLoadAction::Load);
            ca.setStoreAction(MTLStoreAction::StoreAndMultisampleResolve);

            let da = main_pass_desc.depthAttachment();
            da.setTexture(Some(self.hdr_targets.depth.as_ref()));
            da.setLoadAction(MTLLoadAction::Load);
            da.setResolveTexture(Some(self.hdr_targets.depth_resolve.as_ref()));
            da.setDepthResolveFilter(objc2_metal::MTLMultisampleDepthResolveFilter::Sample0);
            da.setStoreAction(MTLStoreAction::StoreAndMultisampleResolve);
        }

        if let Some(t) = &self.diagnostics.pass_timing {
            t.attach_render(&main_pass_desc, super::super::pass_timing::PassId::Main2);
        }
        let encoder = ScopedEncoder::new(
            cmd_buf
                .renderCommandEncoderWithDescriptor(&main_pass_desc)
                .ok_or("failed to get render encoder")?,
            "main2 pass",
        );
        if self.view.mode == concinnity_core::gfx::view_modes::ViewMode::Wireframe {
            encoder.setTriangleFillMode(objc2_metal::MTLTriangleFillMode::Lines);
        }

        let view_uniforms = ViewUniforms {
            vp,
            view: self.view.matrix,
            elapsed,
            reflections_enabled: self.reflection_resolve_active(),
            cam_pos,
            prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
            shade_mode: self.shade_mode(),
            _end_pad: 0.0,
            sky_rot: self.view.sky_rot,
        };
        self.bind_main_pass_shared(&encoder, &view_uniforms);
        // Main2 is the same main camera as phase 1, so it reads the clusters too.
        self.bind_clusters(&encoder, true);
        let draw_calls = self.execute_bindless_static_icb(
            &encoder,
            obj_buf,
            tex_args,
            &self.cull.icbs_2,
            deformed_skinned,
            counts,
        );

        Ok(draw_calls)
    }

    // Issue the GPU-driven bindless pass: bind the per-object data +
    // bindless-texture argument buffer, declare the index buffer + sampled
    // textures resident (they are reached only through indirect commands /
    // the argument buffer, never bound on the encoder), then execute the `icb`.
    // Shared by the phase-1 main pass (`icb = cull_icb`) and the phase-2 main
    // pass (`icb = cull_icb_2`) so both issue the bindless geometry identically.
    //
    // The ICB is split into two `executeCommandsInBuffer` ranges because Metal
    // bakes the index buffer into each indirect command: the prefix records are
    // static + instances (static u32 index buffer, static vertex buffer already
    // bound at binding 1 by `bind_main_pass_shared`); the tail records are the
    // folded skinned ones, which the cull kernel encoded against the skinned
    // u16 index buffer and which read the compute-deformed vertices, so this
    // rebinds the deformed buffer at binding 1 for that range (inherited by the
    // ICB commands). Both ranges come from `counts`, the record set the caller's
    // buffers were built and culled for, so a snapshot capture never addresses
    // slots the live draw list grew after its ICB was sized. Returns the number
    // of indirect draws issued (1 or 2).
    fn execute_bindless_static_icb(
        &self,
        enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
        obj_buf: &Retained<ProtocolObject<dyn MTLBuffer>>,
        tex_args: &Retained<ProtocolObject<dyn MTLBuffer>>,
        icbs: &[Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>],
        deformed_skinned: Option<&Retained<ProtocolObject<dyn MTLBuffer>>>,
        counts: crate::metal::context::DrawRecordCounts,
    ) -> u32 {
        use objc2_metal::{MTLRenderStages, MTLResourceUsage};
        // Object records (binding 9) + bindless textures (binding 7) are shared
        // by both ranges: the skinned records live in the same object buffer and
        // sample the same flat pool. The object id reaches the shader via each
        // command's [[base_instance]].
        enc.set_vertex_buffer(obj_buf, 0, 9);
        enc.set_fragment_buffer(obj_buf, 0, 9);
        enc.set_fragment_buffer(tex_args, 0, BINDLESS_TEXTURE_ARG_BUFFER_INDEX);
        // The engine sampler block (single-source main program only;
        // world-authored fragments use inline samplers and ignore the
        // slot).
        if let Some(sampler_args) = &self.bindless_sampler_args {
            enc.set_fragment_buffer(
                sampler_args,
                0,
                crate::metal::context::BINDLESS_SAMPLER_ARG_BUFFER_INDEX,
            );
        }
        self.use_bindless_textures(enc);

        let mut draw_calls = 0u32;

        // Static + instance prefix, once per shader bucket: bucket 0 executes
        // under the pipeline `bind_main_pass_shared` bound, each later bucket
        // under its material shader's pipeline. The cull kernel wrote every
        // record's command into exactly one bucket's ICB, so the ranges never
        // double-draw. The static u32 index buffer is referenced only inside the
        // indirect commands (not bound), so make it resident; the static vertex
        // buffer is already bound at binding 1.
        if let Some(prefix) = counts.prefix(0) {
            enc.useResource_usage_stages(
                ProtocolObject::from_ref(&*self.index_buffer),
                MTLResourceUsage::Read,
                MTLRenderStages::Vertex,
            );
            let range = crate::metal::context::ns_range(prefix);
            for (b, icb) in icbs.iter().enumerate() {
                if b > 0 {
                    // A bucket whose Shader is not resident yet (its scene has
                    // not pinned) has no pipeline: skip it until warmup builds
                    // one rather than drawing it with the wrong program.
                    let Some(pso) = self.world_pipeline(b) else {
                        continue;
                    };
                    enc.set_pipeline(pso);
                }
                // SAFETY: the prefix spans the static + instance command slots
                // (`ensure_icb_capacity` sized every ICB for `counts.total`).
                unsafe {
                    enc.executeCommandsInBuffer_withRange(icb, range);
                }
                draw_calls += 1;
            }
            // Restore the default pipeline for the skinned tail below (and for
            // the caller's subsequent sub-paths, which re-bind anyway).
            if icbs.len() > 1
                && let Some(ps) = &self.pipeline_state
            {
                enc.set_pipeline(ps);
            }
        }

        // Skinned tail: bind the deformed vertex buffer (inherited by the ICB
        // commands) and make the skinned index buffer resident (the cull
        // kernel baked it into these commands). `deformed_skinned` is `Some`
        // exactly when the fold is active, which is also when the tail is
        // non-empty. Skinned draws always render under the world default shader,
        // so only bucket 0's ICB executes here.
        if let (Some(deformed), Some(icb0), Some(tail)) =
            (deformed_skinned, icbs.first(), counts.skinned_tail(0))
        {
            enc.set_vertex_buffer(deformed, 0, 1);
            if let Some(skinned_ib) = self.skinned.index_buffer.as_ref() {
                enc.useResource_usage_stages(
                    ProtocolObject::from_ref(&**skinned_ib),
                    MTLResourceUsage::Read,
                    MTLRenderStages::Vertex,
                );
            }
            // SAFETY: the tail spans the folded skinned command slots.
            unsafe {
                enc.executeCommandsInBuffer_withRange(icb0, crate::metal::context::ns_range(tail));
            }
            draw_calls += 1;
        }
        draw_calls
    }

    // Bind the clustered-lighting inputs the forward pass reads: the params at
    // fragment buffer(11) + the per-cluster light-index list at buffer(12). Bound
    // once per pass (the value is pass-level, shared by every geometry sub-path)
    // on the shared encoder. `clustered` = true for the main camera (binds the
    // live params); false for the planar / probe re-renders, which shade from a
    // viewpoint the main camera's grid does not match and so fall back to
    // iterating every local light (use_clusters cleared).
    fn bind_clusters(
        &self,
        enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
        clustered: bool,
    ) {
        let cluster_params = if clustered {
            self.cluster_params
        } else {
            crate::gfx::render_types::ClusterParams {
                use_clusters: 0,
                ..self.cluster_params
            }
        };
        enc.set_fragment_value(&cluster_params, 11);
        enc.set_fragment_buffer(&self.light_cull.cluster_buffer, 0, 12);
    }

    // Apply the encoder state every main-pass encode needs: the pipeline and
    // depth state, the view uniforms, the shared vertex buffer at binding 1,
    // and the light / shadow / probe records the fragment stage reads. All of
    // it is buffers, because the ICB-executed draws inherit encoder buffer
    // bindings but reach every texture and sampler through the argument
    // buffers `execute_bindless_static_icb` binds instead.
    // Returns false without touching the encoder when the world has no main
    // pipeline (no 3D scene content); the caller then skips its draws.
    fn bind_main_pass_shared(
        &self,
        enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
        view_uniforms: &ViewUniforms,
    ) -> bool {
        let Some(pipeline_state) = &self.pipeline_state else {
            return false;
        };
        enc.set_pipeline(pipeline_state);
        enc.set_depth_stencil(&self.depth_state);

        enc.set_vertex_value(view_uniforms, 0);
        enc.set_fragment_value(view_uniforms, 0);
        enc.set_vertex_buffer(&self.vertex_buffer, 0, 1);
        enc.set_fragment_value(&self.light_uniforms, 4);
        // Local-light storage buffer at fragment buffer(8). Encoder-bound
        // buffers are inherited by the ICB-executed bindless draws (the same
        // way the object buffer at binding 9 is), so this single bind covers
        // the main pass and the planar / probe re-renders.
        enc.set_fragment_buffer(&self.local_light_buffer, 0, 8);
        // Per-slice spot shadow projections at fragment buffer(13), inherited
        // by the ICB draws like the local lights above. The matching depth
        // array is not bound here: every texture the pass samples travels in
        // the BindlessTextures argument buffer, because discrete texture
        // bindings break ICB compatibility.
        enc.set_fragment_buffer(&self.spot_shadow.buffer, 0, 13);
        // Rect area-light extents at fragment buffer(14), indexed by
        // GpuLight.data_index. Inherited by the ICB draws like the buffers
        // above, so this one bind covers every main-pass variant.
        enc.set_fragment_buffer(&self.area_light_buffer, 0, 14);
        enc.set_fragment_value(&self.shadow.uniforms, 5);
        // Reflection-probe set (count + per-probe parallax boxes) at fragment
        // buffer(6). `EMPTY` until a bake; the shader weights every box
        // covering the surface.
        enc.set_fragment_value(&self.probe.set, 6);
        true
    }

    // Encode the frame's geometry: the GPU-driven bindless pass, one indirect
    // range per shader bucket plus the folded skinned tail. Returns 0 without
    // touching the encoder when the world has nothing to draw this frame.
    fn encode_main_geometry_into(
        &self,
        enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
        view_uniforms: &ViewUniforms,
        gpu: GpuFrameBuffers,
        // ICB to execute instead of the main cull's `self.cull.icb`: the planar
        // mirror render passes its slot's mirror ICB (culled against the
        // reflected frustum) here; the main + probe paths pass `None` to use the
        // main ICB.
        icb_override: Option<&ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>,
    ) -> u32 {
        let GpuFrameBuffers {
            object_buffer,
            bindless_tex_args,
            deformed_skinned,
            counts,
        } = gpu;
        enc.pushDebugGroup(&objc2_foundation::NSString::from_str("main geometry"));
        if !self.bind_main_pass_shared(enc, view_uniforms) {
            enc.popDebugGroup();
            return 0;
        }

        // The planar mirror override is a single command stream executed under
        // the encoder's default pipeline; the main + probe paths execute the
        // per-bucket main cull set.
        let override_holder;
        let bucket_icbs: &[Retained<_>] = match icb_override {
            Some(icb) => {
                override_holder = [objc2::Message::retain(icb)];
                &override_holder
            }
            None => &self.cull.icbs,
        };
        let mut draw_calls: u32 = 0;
        if let (Some(obj_buf), Some(tex_args), false) =
            (object_buffer, bindless_tex_args, bucket_icbs.is_empty())
        {
            // Shared with the phase-2 main pass under two-pass occlusion: see
            // `execute_bindless_static_icb`.
            draw_calls += self.execute_bindless_static_icb(
                enc,
                obj_buf,
                tex_args,
                bucket_icbs,
                deformed_skinned,
                counts,
            );
        }
        enc.popDebugGroup();
        draw_calls
    }
}