Skip to main content

mittens_engine/engine/graphics/
visual_world.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::Transform;
3use crate::engine::graphics::GpuRenderable;
4use crate::engine::graphics::MsaaMode;
5use crate::engine::graphics::post_processing::PostProcessingConfig;
6use crate::engine::graphics::primitives::InstanceHandle;
7use crate::engine::graphics::primitives::TransformMatrix;
8use crate::engine::graphics::{Skin, SkinId};
9use slotmap::{Key, SlotMap};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum TextureFiltering {
14    /// Default: linear filtering for both minification and magnification.
15    #[default]
16    Linear,
17    /// Nearest-neighbor filtering for both minification and magnification.
18    Nearest,
19    /// Nearest for magnification, linear for minification.
20    NearestMagnification,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum CameraTarget {
25    Window,
26    Xr,
27}
28
29#[derive(Debug, Clone, Copy)]
30pub struct CameraData {
31    pub view: [[f32; 4]; 4],
32    pub proj: [[f32; 4]; 4],
33    pub transform: Transform,
34}
35
36#[derive(Debug, Clone)]
37pub struct VisualCamera {
38    pub target: CameraTarget,
39    pub eyes: Vec<CameraData>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum MirrorViewerFamily {
44    Monoscopic,
45    Stereoscopic,
46}
47
48impl MirrorViewerFamily {
49    pub fn key_segment(self) -> &'static str {
50        match self {
51            Self::Monoscopic => "mono",
52            Self::Stereoscopic => "stereo",
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58pub struct MirrorCaptureRequest {
59    pub family: MirrorViewerFamily,
60    pub view_index: usize,
61    pub camera: CameraData,
62    pub target_key: String,
63}
64
65#[derive(Debug, Clone)]
66pub struct VisualMirror {
67    pub mirror_component: ComponentId,
68    pub captures: Vec<MirrorCaptureRequest>,
69    pub plane_origin: [f32; 3],
70    pub plane_normal: [f32; 3],
71    pub aspect_ratio: f32,
72    pub source_instance: InstanceHandle,
73    pub resolution_scale: f32,
74}
75
76#[derive(Debug, Clone, Copy)]
77pub struct DrawBatch {
78    pub material: crate::engine::graphics::MaterialHandle,
79    pub mesh: crate::engine::graphics::primitives::MeshHandle,
80    pub texture: Option<crate::engine::graphics::TextureHandle>,
81    pub texture_filtering: TextureFiltering,
82    pub quant_steps: f32,
83    /// Effective stencil reference value for this batch.
84    /// 0 = unclipped; >0 = draw only where stencil == this value.
85    /// For clip-source instances this is `parent_ref + 1` (their visual draw is inside their own region).
86    pub stencil_ref: u8,
87    /// Start index into the phase's instance-index array (e.g. `overlay_stream_instances`).
88    pub start: usize,
89    pub count: usize,
90}
91
92/// One entry in a per-phase DFS render stream.
93///
94/// The overlay stream is a sequence of these ops that the renderer executes in order,
95/// binding different pipelines for stencil writes vs. clipped color draws.
96#[derive(Debug, Clone, Copy)]
97pub enum RenderOp {
98    /// Write stencil INCR for a clip region entry.
99    /// Render `instance_index`'s mesh with `pipeline_stencil_incr`, stencil reference = `parent_ref`.
100    /// After this op, pixels under the mesh have stencil value `new_ref`.
101    EnterClip {
102        instance_index: u32,
103        parent_ref: u8,
104        new_ref: u8,
105    },
106    /// Draw a batch of instances.
107    /// `batch.stencil_ref == 0` → use normal overlay pipeline.
108    /// `batch.stencil_ref > 0` → use clipped overlay pipeline with that reference.
109    /// Instance indices live in `[batch.start .. batch.start + batch.count)` of the stream's instance array.
110    DrawBatch(DrawBatch),
111    /// Write stencil DECR to close a clip region.
112    /// Render `instance_index`'s mesh with `pipeline_stencil_decr`, stencil reference = `ref_value`.
113    ExitClip { instance_index: u32, ref_value: u8 },
114}
115
116pub struct VisualWorld {
117    instances: Vec<VisualInstance>,
118    clear_color: [f32; 4],
119    renderer_msaa_mode: MsaaMode,
120    preferred_window_size: Option<[u32; 2]>,
121    post_processing: PostProcessingConfig,
122
123    mirrors: Vec<VisualMirror>,
124
125    // Frame timing stats captured from the main loop (window) and the XR render path.
126    // These are best-effort diagnostics and are not used for simulation.
127    window_frame_dt_sec: f32,
128    xr_frame_dt_sec: Option<f32>,
129
130    // Shared bones palette for all skinned instances.
131    // Instances reference a subrange via (bones_base, bones_count).
132    //
133    // This is *persistent* across frames. We update only the subranges for rigs
134    // that become dirty (via transform changes), and we keep offsets stable via a
135    // tiny free-list allocator.
136    bones_palette: Vec<TransformMatrix>,
137    bones_free_ranges: Vec<(u32, u32)>,
138    dirty_bones_palette: bool,
139
140    // Shared skin definitions (glTF skins), keyed by (uri, skin_index).
141    skins: SlotMap<SkinId, Skin>,
142    skin_id_by_key: std::collections::HashMap<(String, usize), SkinId>,
143
144    ambient_light: [f32; 3],
145
146    point_lights: Vec<VisualPointLight>,
147    point_light_index_by_component: std::collections::HashMap<ComponentId, usize>,
148    dirty_lights: bool,
149
150    // Target-scoped camera state. Window is typically mono; XR is stereo.
151    visual_cameras: Vec<VisualCamera>,
152
153    // Which CameraXRComponent (by ComponentId) is currently active for XR rig transforms.
154    active_xr_camera: Option<ComponentId>,
155    // Most recent render target size in pixels (width, height).
156    viewport: [f32; 2],
157    runtime_texture_handles: HashMap<String, crate::engine::graphics::TextureHandle>,
158    stencil_clip_debug_requested: bool,
159    // 2D camera view transform for translation/scale/rotation.
160    // Stored as mat3 column vectors padded to vec4 columns (std140 friendly).
161    camera_2d: [[f32; 4]; 3],
162    dirty_camera: bool,
163
164    next_handle: u32,
165    handle_to_index: std::collections::HashMap<InstanceHandle, usize>,
166    component_to_handle: std::collections::HashMap<ComponentId, InstanceHandle>,
167
168    // Cached draw data (rebuilt when dirty)
169    dirty_draw_cache: bool,
170    /// True when per-instance data (e.g. model matrices) changed and any cached GPU instance
171    /// buffer should be rebuilt/uploaded.
172    dirty_instance_data: bool,
173
174    // Background draw data (rebuilt when dirty)
175    background_order: Vec<u32>, // indices into `instances`
176    background_batches: Vec<DrawBatch>,
177    background_occluded_lit_order: Vec<u32>,
178    background_occluded_lit_batches: Vec<DrawBatch>,
179    background_occluded_lit_emissive_order: Vec<u32>,
180    background_occluded_lit_emissive_batches: Vec<DrawBatch>,
181    draw_order: Vec<u32>, // indices into `instances`
182    draw_batches: Vec<DrawBatch>,
183
184    // Emissive-only opaque draw data (rebuilt when dirty).
185    emissive_draw_order: Vec<u32>,
186    emissive_draw_batches: Vec<DrawBatch>,
187
188    // Alpha-to-coverage cutout draw data (rebuilt when dirty).
189    cutout_order: Vec<u32>,
190    cutout_batches: Vec<DrawBatch>,
191
192    // Emissive-only alpha-to-coverage draw data (rebuilt when dirty).
193    emissive_cutout_order: Vec<u32>,
194    emissive_cutout_batches: Vec<DrawBatch>,
195
196    // DFS-ordered render stream for the cutout phase.
197    cutout_stream: Vec<RenderOp>,
198    cutout_stream_instances: Vec<u32>,
199
200    // Overlay draw data (rebuilt when dirty).
201    // Overlay is drawn on top of all other phases.
202    overlay_order: Vec<u32>,
203    overlay_batches: Vec<DrawBatch>,
204
205    // Stencil clip sources: indices into `instances` where `is_stencil_clip=true`,
206    // sorted ascending by stencil_ref (outer clips first). Rebuilt with draw cache.
207    stencil_clip_order: Vec<u32>,
208
209    // DFS-ordered render stream for the overlay phase.
210    // overlay_stream_instances holds VisualInstance indices referenced by DrawBatch ops.
211    // Rebuilt with draw cache whenever stencil clip state or overlay membership changes.
212    overlay_stream: Vec<RenderOp>,
213    overlay_stream_instances: Vec<u32>,
214
215    // DFS-ordered render stream for the opaque phase (mirrors overlay_stream).
216    opaque_stream: Vec<RenderOp>,
217    opaque_stream_instances: Vec<u32>,
218
219    // Transparent draw data.
220    // - Single-layer: cached (order does not depend on view), instanced.
221    transparent_single_draw_order: Vec<u32>,
222    transparent_single_draw_batches: Vec<DrawBatch>,
223    transparent_single_stream: Vec<RenderOp>,
224    transparent_single_stream_instances: Vec<u32>,
225    // - Multi-layer: rebuilt per-eye (ordering depends on view), sorted + drawn one-by-one.
226    transparent_multi_draw_order: Vec<u32>,
227    transparent_multi_draw_batches: Vec<DrawBatch>,
228}
229
230#[derive(Debug, Clone, Copy)]
231pub struct VisualInstance {
232    pub renderable: GpuRenderable,
233    pub transform: Transform,
234    pub color: [f32; 4],
235    pub opacity: f32,
236    pub multiple_layers: bool,
237    pub transparent_cutout: bool,
238    pub background: bool,
239    pub background_occluded_lit: bool,
240    pub overlay: bool,
241    pub emissive: f32,
242    pub texture: Option<crate::engine::graphics::TextureHandle>,
243    pub texture_filtering: TextureFiltering,
244    pub quant_steps: f32,
245
246    /// Base index into `VisualWorld::bones_palette`.
247    pub bones_base: u32,
248    /// Number of bone matrices for this instance.
249    pub bones_count: u32,
250
251    /// Which clip region this instance is inside. 0 = unclipped.
252    pub stencil_ref: u8,
253    /// When true, this instance writes stencil before its descendant subtree draws.
254    /// It also draws normally in the color pass (double duty: clip source + background quad).
255    pub is_stencil_clip: bool,
256}
257
258fn sanitize_quant_steps(steps: f32) -> f32 {
259    if !steps.is_finite() {
260        3.0
261    } else {
262        steps.clamp(1.0, 64.0)
263    }
264}
265
266impl Default for VisualWorld {
267    fn default() -> Self {
268        let ident4 = [
269            [1.0, 0.0, 0.0, 0.0],
270            [0.0, 1.0, 0.0, 0.0],
271            [0.0, 0.0, 1.0, 0.0],
272            [0.0, 0.0, 0.0, 1.0],
273        ];
274        let mut t = Transform::default();
275        t.model = ident4;
276        t.matrix_world = ident4;
277
278        Self {
279            instances: Vec::new(),
280            clear_color: [0.0, 0.0, 0.0, 1.0],
281            renderer_msaa_mode: MsaaMode::default(),
282            preferred_window_size: None,
283            post_processing: PostProcessingConfig::default(),
284            mirrors: Vec::new(),
285            window_frame_dt_sec: 0.0,
286            xr_frame_dt_sec: None,
287
288            bones_palette: vec![ident4],
289            bones_free_ranges: Vec::new(),
290            dirty_bones_palette: true,
291
292            skins: SlotMap::with_key(),
293            skin_id_by_key: std::collections::HashMap::new(),
294
295            ambient_light: [0.0, 0.0, 0.0],
296
297            point_lights: Vec::new(),
298            point_light_index_by_component: std::collections::HashMap::new(),
299            dirty_lights: true,
300
301            visual_cameras: vec![VisualCamera {
302                target: CameraTarget::Window,
303                eyes: vec![CameraData {
304                    view: ident4,
305                    proj: ident4,
306                    transform: t,
307                }],
308            }],
309
310            active_xr_camera: None,
311            viewport: [1.0, 1.0],
312            runtime_texture_handles: HashMap::new(),
313            stencil_clip_debug_requested: false,
314            camera_2d: [
315                [1.0, 0.0, 0.0, 0.0],
316                [0.0, 1.0, 0.0, 0.0],
317                [0.0, 0.0, 1.0, 0.0],
318            ],
319            dirty_camera: true,
320
321            next_handle: 0,
322            handle_to_index: std::collections::HashMap::new(),
323            component_to_handle: std::collections::HashMap::new(),
324
325            dirty_draw_cache: true,
326            dirty_instance_data: true,
327            background_order: Vec::new(),
328            background_batches: Vec::new(),
329            background_occluded_lit_order: Vec::new(),
330            background_occluded_lit_batches: Vec::new(),
331            background_occluded_lit_emissive_order: Vec::new(),
332            background_occluded_lit_emissive_batches: Vec::new(),
333            draw_order: Vec::new(),
334            draw_batches: Vec::new(),
335            emissive_draw_order: Vec::new(),
336            emissive_draw_batches: Vec::new(),
337
338            cutout_order: Vec::new(),
339            cutout_batches: Vec::new(),
340            emissive_cutout_order: Vec::new(),
341            emissive_cutout_batches: Vec::new(),
342            cutout_stream: Vec::new(),
343            cutout_stream_instances: Vec::new(),
344
345            overlay_order: Vec::new(),
346            overlay_batches: Vec::new(),
347
348            stencil_clip_order: Vec::new(),
349
350            overlay_stream: Vec::new(),
351            overlay_stream_instances: Vec::new(),
352
353            opaque_stream: Vec::new(),
354            opaque_stream_instances: Vec::new(),
355
356            transparent_single_draw_order: Vec::new(),
357            transparent_single_draw_batches: Vec::new(),
358            transparent_single_stream: Vec::new(),
359            transparent_single_stream_instances: Vec::new(),
360            transparent_multi_draw_order: Vec::new(),
361            transparent_multi_draw_batches: Vec::new(),
362        }
363    }
364}
365
366impl VisualWorld {
367    fn is_emissive_material(material: crate::engine::graphics::MaterialHandle) -> bool {
368        matches!(
369            material,
370            crate::engine::graphics::MaterialHandle::EMISSIVE_TOON_MESH
371                | crate::engine::graphics::MaterialHandle::SKINNED_EMISSIVE_TOON_MESH
372        )
373    }
374
375    pub fn instance(&self, handle: InstanceHandle) -> Option<&VisualInstance> {
376        let idx = *self.handle_to_index.get(&handle)?;
377        self.instances.get(idx)
378    }
379
380    pub fn skin(&self, id: SkinId) -> Option<&Skin> {
381        self.skins.get(id)
382    }
383
384    pub fn skin_id_for(&self, uri: &str, skin_index: usize) -> Option<SkinId> {
385        self.skin_id_by_key
386            .get(&(uri.to_string(), skin_index))
387            .copied()
388    }
389
390    pub fn upsert_skin(
391        &mut self,
392        uri: &str,
393        skin_index: usize,
394        joint_node_indices: Vec<usize>,
395        inverse_bind_matrices: Vec<TransformMatrix>,
396    ) -> SkinId {
397        let key = (uri.to_string(), skin_index);
398        if let Some(existing) = self.skin_id_by_key.get(&key).copied() {
399            if let Some(skin) = self.skins.get_mut(existing) {
400                skin.uri = uri.to_string();
401                skin.skin_index = skin_index;
402                skin.joint_node_indices = joint_node_indices;
403                skin.inverse_bind_matrices = inverse_bind_matrices;
404                return existing;
405            }
406        }
407
408        let id = self.skins.insert(Skin {
409            id: SkinId::null(),
410            uri: uri.to_string(),
411            skin_index,
412            joint_node_indices,
413            inverse_bind_matrices,
414        });
415
416        if let Some(s) = self.skins.get_mut(id) {
417            s.id = id;
418        }
419
420        self.skin_id_by_key.insert(key, id);
421        id
422    }
423
424    fn bones_identity() -> TransformMatrix {
425        [
426            [1.0, 0.0, 0.0, 0.0],
427            [0.0, 1.0, 0.0, 0.0],
428            [0.0, 0.0, 1.0, 0.0],
429            [0.0, 0.0, 0.0, 1.0],
430        ]
431    }
432
433    fn bones_free_coalesce(&mut self) {
434        if self.bones_free_ranges.len() <= 1 {
435            return;
436        }
437
438        self.bones_free_ranges.sort_by_key(|(b, _)| *b);
439        let mut out: Vec<(u32, u32)> = Vec::with_capacity(self.bones_free_ranges.len());
440        for (base, len) in self.bones_free_ranges.drain(..) {
441            if let Some((prev_base, prev_len)) = out.last_mut() {
442                let prev_end = *prev_base + *prev_len;
443                if prev_end == base {
444                    *prev_len += len;
445                    continue;
446                }
447            }
448            out.push((base, len));
449        }
450        self.bones_free_ranges = out;
451    }
452
453    fn bones_alloc_range(&mut self, len: u32) -> u32 {
454        // Never allocate index 0 (reserved identity).
455        debug_assert!(!self.bones_palette.is_empty());
456        debug_assert_eq!(self.bones_palette[0], Self::bones_identity());
457
458        if len == 0 {
459            return 0;
460        }
461
462        // First-fit from free list.
463        for i in 0..self.bones_free_ranges.len() {
464            let (base, free_len) = self.bones_free_ranges[i];
465            if free_len >= len {
466                let alloc_base = base;
467                if free_len == len {
468                    self.bones_free_ranges.swap_remove(i);
469                } else {
470                    self.bones_free_ranges[i] = (base + len, free_len - len);
471                }
472                return alloc_base;
473            }
474        }
475
476        // Otherwise grow the palette.
477        let base = self.bones_palette.len() as u32;
478        self.bones_palette.resize(
479            self.bones_palette.len() + len as usize,
480            Self::bones_identity(),
481        );
482        base
483    }
484
485    fn bones_free_range(&mut self, base: u32, len: u32) {
486        if len == 0 {
487            return;
488        }
489
490        // Never free the reserved identity element.
491        if base == 0 {
492            return;
493        }
494
495        // Fill freed region with identity so sampling a freed slot is benign.
496        let start = base as usize;
497        let end = (base + len) as usize;
498        if end <= self.bones_palette.len() {
499            for slot in &mut self.bones_palette[start..end] {
500                *slot = Self::bones_identity();
501            }
502        }
503
504        self.bones_free_ranges.push((base, len));
505        self.bones_free_coalesce();
506        self.dirty_bones_palette = true;
507    }
508
509    /// Assigns the skin matrices for an instance into the shared palette.
510    ///
511    /// This keeps `bones_base` stable for an instance unless its bone count changes.
512    pub fn set_skin_matrices(&mut self, handle: InstanceHandle, bones: &[TransformMatrix]) -> bool {
513        let debug_skin_set = std::env::var("CAT_DEBUG_SKIN_SET")
514            .ok()
515            .map(|s| {
516                let s = s.trim().to_ascii_lowercase();
517                s == "1" || s == "true" || s == "on" || s == "yes"
518            })
519            .unwrap_or(false);
520
521        let Some(&idx) = self.handle_to_index.get(&handle) else {
522            if debug_skin_set {
523                println!(
524                    "[VisualWorld] set_skin_matrices: unknown handle={handle:?} bones_len={}",
525                    bones.len()
526                );
527            }
528            return false;
529        };
530
531        if bones.is_empty() {
532            // Disable skinning for this instance and free its allocation.
533            let old_base = self.instances[idx].bones_base;
534            let old_count = self.instances[idx].bones_count;
535            if old_count != 0 {
536                self.bones_free_range(old_base, old_count);
537            }
538            if self.instances[idx].bones_base != 0 || self.instances[idx].bones_count != 0 {
539                self.instances[idx].bones_base = 0;
540                self.instances[idx].bones_count = 0;
541                self.dirty_instance_data = true;
542            }
543            return true;
544        }
545
546        let want_count = bones.len() as u32;
547        let mut base = self.instances[idx].bones_base;
548        let old_count = self.instances[idx].bones_count;
549
550        if old_count == 0 {
551            base = self.bones_alloc_range(want_count);
552            self.instances[idx].bones_base = base;
553            self.instances[idx].bones_count = want_count;
554            self.dirty_instance_data = true;
555        } else if old_count != want_count {
556            // Reallocate with new size.
557            self.bones_free_range(base, old_count);
558            base = self.bones_alloc_range(want_count);
559            self.instances[idx].bones_base = base;
560            self.instances[idx].bones_count = want_count;
561            self.dirty_instance_data = true;
562        }
563
564        // Write into the palette.
565        let start = base as usize;
566        let end = start + bones.len();
567        if end > self.bones_palette.len() {
568            self.bones_palette.resize(end, Self::bones_identity());
569        }
570        self.bones_palette[start..end].copy_from_slice(bones);
571        self.dirty_bones_palette = true;
572
573        if debug_skin_set {
574            println!(
575                "[VisualWorld] set_skin_matrices: handle={handle:?} idx={idx} bones_base={} bones_count={} bones_len={}",
576                self.instances[idx].bones_base,
577                self.instances[idx].bones_count,
578                bones.len(),
579            );
580        }
581        true
582    }
583
584    pub fn bones_palette(&self) -> &[TransformMatrix] {
585        &self.bones_palette
586    }
587
588    /// Returns whether the bones palette changed since the last call, and clears the dirty flag.
589    pub fn take_bones_palette_dirty(&mut self) -> bool {
590        let dirty = self.dirty_bones_palette;
591        self.dirty_bones_palette = false;
592        dirty
593    }
594
595    /// Compatibility helper: updates the skin palette range for an instance.
596    ///
597    /// Prefer `set_skin_matrices()` for stable allocation.
598    pub fn update_skin_range(
599        &mut self,
600        handle: InstanceHandle,
601        bones_base: u32,
602        bones_count: u32,
603    ) -> bool {
604        if let Some(&idx) = self.handle_to_index.get(&handle) {
605            if self.instances[idx].bones_base == bones_base
606                && self.instances[idx].bones_count == bones_count
607            {
608                return true;
609            }
610            // If the caller is forcing a range, free the old allocation (best-effort).
611            let old_base = self.instances[idx].bones_base;
612            let old_count = self.instances[idx].bones_count;
613            if old_count != 0 && (old_base != bones_base || old_count != bones_count) {
614                self.bones_free_range(old_base, old_count);
615            }
616            self.instances[idx].bones_base = bones_base;
617            self.instances[idx].bones_count = bones_count;
618            self.dirty_instance_data = true;
619            true
620        } else {
621            false
622        }
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::{RenderOp, VisualWorld};
629    use crate::engine::ecs::ComponentId;
630    use crate::engine::graphics::primitives::{
631        GpuRenderable, MaterialHandle, MeshHandle, Transform,
632    };
633    use slotmap::KeyData;
634
635    fn cid(n: u64) -> ComponentId {
636        KeyData::from_ffi(n).into()
637    }
638
639    fn dummy_renderable() -> GpuRenderable {
640        GpuRenderable::new(MeshHandle::SQUARE, MaterialHandle::TOON_MESH)
641    }
642
643    fn dummy_emissive_renderable() -> GpuRenderable {
644        GpuRenderable::new(MeshHandle::SQUARE, MaterialHandle::EMISSIVE_TOON_MESH)
645    }
646
647    #[test]
648    fn background_occluded_lit_emissive_subset_includes_only_eligible_instances() {
649        let mut visuals = VisualWorld::default();
650
651        let _ = visuals.register(
652            cid(100),
653            dummy_emissive_renderable(),
654            Transform::default(),
655            [1.0, 0.8, 0.4, 1.0],
656            1.0,
657            false,
658            false,
659            true,
660            true,
661            false,
662            1.0,
663            None,
664            3.0,
665        );
666        let _ = visuals.register(
667            cid(101),
668            dummy_emissive_renderable(),
669            Transform::default(),
670            [1.0, 0.8, 0.4, 1.0],
671            1.0,
672            false,
673            false,
674            true,
675            false,
676            false,
677            1.0,
678            None,
679            3.0,
680        );
681        let _ = visuals.register(
682            cid(102),
683            dummy_renderable(),
684            Transform::default(),
685            [0.6, 0.6, 0.6, 1.0],
686            1.0,
687            false,
688            false,
689            true,
690            true,
691            false,
692            0.0,
693            None,
694            3.0,
695        );
696
697        visuals.prepare_draw_cache();
698
699        assert!(visuals.has_background_occluded_lit_emissive());
700        assert_eq!(visuals.background_occluded_lit_order().len(), 2);
701        assert_eq!(visuals.background_occluded_lit_emissive_order().len(), 1);
702        assert_eq!(visuals.background_occluded_lit_emissive_batches().len(), 1);
703    }
704
705    #[test]
706    fn background_occluded_lit_non_emissive_keeps_subset_empty() {
707        let mut visuals = VisualWorld::default();
708
709        let _ = visuals.register(
710            cid(110),
711            dummy_renderable(),
712            Transform::default(),
713            [0.6, 0.6, 0.6, 1.0],
714            1.0,
715            false,
716            false,
717            true,
718            true,
719            false,
720            0.0,
721            None,
722            3.0,
723        );
724
725        visuals.prepare_draw_cache();
726
727        assert_eq!(visuals.background_occluded_lit_order().len(), 1);
728        assert!(!visuals.has_background_occluded_lit_emissive());
729        assert!(visuals.background_occluded_lit_emissive_order().is_empty());
730        assert!(
731            visuals
732                .background_occluded_lit_emissive_batches()
733                .is_empty()
734        );
735    }
736
737    #[test]
738    fn opaque_stream_enters_and_exits_single_root_clip() {
739        let mut visuals = VisualWorld::default();
740
741        let clip_handle = visuals.register(
742            cid(1),
743            dummy_renderable(),
744            Transform::default(),
745            [1.0, 1.0, 1.0, 1.0],
746            1.0,
747            false,
748            false,
749            false,
750            false,
751            false,
752            0.0,
753            None,
754            3.0,
755        );
756        let content_handle = visuals.register(
757            cid(2),
758            dummy_renderable(),
759            Transform::default(),
760            [0.8, 0.8, 0.8, 1.0],
761            1.0,
762            false,
763            false,
764            false,
765            false,
766            false,
767            0.0,
768            None,
769            3.0,
770        );
771
772        let _ = visuals.register_stencil_clip(clip_handle, 0);
773        let _ = visuals.update_stencil_ref(content_handle, 1);
774        visuals.prepare_draw_cache();
775
776        let (ops, instance_indices) = visuals.opaque_stream();
777        assert_eq!(ops.len(), 4);
778        assert_eq!(instance_indices.len(), 2);
779
780        match ops[0] {
781            RenderOp::EnterClip {
782                parent_ref,
783                new_ref,
784                ..
785            } => {
786                assert_eq!(parent_ref, 0);
787                assert_eq!(new_ref, 1);
788            }
789            other => panic!("expected EnterClip, got {other:?}"),
790        }
791
792        match ops[1] {
793            RenderOp::DrawBatch(batch) => {
794                assert_eq!(batch.stencil_ref, 1);
795                assert_eq!(batch.count, 1);
796                assert_eq!(instance_indices[batch.start], 0);
797            }
798            other => panic!("expected clip-source DrawBatch, got {other:?}"),
799        }
800
801        match ops[2] {
802            RenderOp::DrawBatch(batch) => {
803                assert_eq!(batch.stencil_ref, 1);
804                assert_eq!(batch.count, 1);
805                assert_eq!(instance_indices[batch.start], 1);
806            }
807            other => panic!("expected content DrawBatch, got {other:?}"),
808        }
809
810        match ops[3] {
811            RenderOp::ExitClip { ref_value, .. } => assert_eq!(ref_value, 1),
812            other => panic!("expected ExitClip, got {other:?}"),
813        }
814    }
815
816    #[test]
817    fn overlay_stream_nests_clip_regions_in_dfs_order() {
818        let mut visuals = VisualWorld::default();
819
820        let outer_clip = visuals.register(
821            cid(10),
822            dummy_renderable(),
823            Transform::default(),
824            [1.0, 1.0, 1.0, 1.0],
825            1.0,
826            false,
827            false,
828            false,
829            false,
830            true,
831            0.0,
832            None,
833            3.0,
834        );
835        let outer_content = visuals.register(
836            cid(11),
837            dummy_renderable(),
838            Transform::default(),
839            [0.8, 0.8, 0.8, 1.0],
840            1.0,
841            false,
842            false,
843            false,
844            false,
845            true,
846            0.0,
847            None,
848            3.0,
849        );
850        let inner_clip = visuals.register(
851            cid(12),
852            dummy_renderable(),
853            Transform::default(),
854            [1.0, 1.0, 1.0, 1.0],
855            1.0,
856            false,
857            false,
858            false,
859            false,
860            true,
861            0.0,
862            None,
863            3.0,
864        );
865        let inner_content = visuals.register(
866            cid(13),
867            dummy_renderable(),
868            Transform::default(),
869            [0.6, 0.6, 0.6, 1.0],
870            1.0,
871            false,
872            false,
873            false,
874            false,
875            true,
876            0.0,
877            None,
878            3.0,
879        );
880
881        let _ = visuals.register_stencil_clip(outer_clip, 0);
882        let _ = visuals.update_stencil_ref(outer_content, 1);
883        let _ = visuals.register_stencil_clip(inner_clip, 1);
884        let _ = visuals.update_stencil_ref(inner_content, 2);
885        visuals.prepare_draw_cache();
886
887        let (ops, instance_indices) = visuals.overlay_stream();
888        assert_eq!(ops.len(), 8);
889        assert_eq!(instance_indices.len(), 4);
890
891        match ops[0] {
892            RenderOp::EnterClip {
893                parent_ref,
894                new_ref,
895                ..
896            } => {
897                assert_eq!(parent_ref, 0);
898                assert_eq!(new_ref, 1);
899            }
900            other => panic!("expected outer EnterClip, got {other:?}"),
901        }
902        match ops[1] {
903            RenderOp::DrawBatch(batch) => {
904                assert_eq!(batch.stencil_ref, 1);
905                assert_eq!(batch.count, 1);
906                assert_eq!(instance_indices[batch.start], 0);
907            }
908            other => panic!("expected outer clip-source DrawBatch, got {other:?}"),
909        }
910        match ops[2] {
911            RenderOp::DrawBatch(batch) => {
912                assert_eq!(batch.stencil_ref, 1);
913                assert_eq!(batch.count, 1);
914                assert_eq!(instance_indices[batch.start], 1);
915            }
916            other => panic!("expected outer content DrawBatch, got {other:?}"),
917        }
918        match ops[3] {
919            RenderOp::EnterClip {
920                parent_ref,
921                new_ref,
922                ..
923            } => {
924                assert_eq!(parent_ref, 1);
925                assert_eq!(new_ref, 2);
926            }
927            other => panic!("expected inner EnterClip, got {other:?}"),
928        }
929        match ops[4] {
930            RenderOp::DrawBatch(batch) => {
931                assert_eq!(batch.stencil_ref, 2);
932                assert_eq!(batch.count, 1);
933                assert_eq!(instance_indices[batch.start], 2);
934            }
935            other => panic!("expected inner clip-source DrawBatch, got {other:?}"),
936        }
937        match ops[5] {
938            RenderOp::DrawBatch(batch) => {
939                assert_eq!(batch.stencil_ref, 2);
940                assert_eq!(batch.count, 1);
941                assert_eq!(instance_indices[batch.start], 3);
942            }
943            other => panic!("expected inner content DrawBatch, got {other:?}"),
944        }
945        match ops[6] {
946            RenderOp::ExitClip { ref_value, .. } => assert_eq!(ref_value, 2),
947            other => panic!("expected inner ExitClip, got {other:?}"),
948        }
949        match ops[7] {
950            RenderOp::ExitClip { ref_value, .. } => assert_eq!(ref_value, 1),
951            other => panic!("expected outer ExitClip, got {other:?}"),
952        }
953    }
954
955    #[test]
956    fn cutout_stream_enters_and_exits_single_root_clip() {
957        let mut visuals = VisualWorld::default();
958
959        let clip_handle = visuals.register(
960            cid(20),
961            dummy_renderable(),
962            Transform::default(),
963            [1.0, 1.0, 1.0, 1.0],
964            1.0,
965            false,
966            true,
967            false,
968            false,
969            false,
970            0.0,
971            None,
972            3.0,
973        );
974        let content_handle = visuals.register(
975            cid(21),
976            dummy_renderable(),
977            Transform::default(),
978            [0.8, 0.8, 0.8, 1.0],
979            1.0,
980            false,
981            true,
982            false,
983            false,
984            false,
985            0.0,
986            None,
987            3.0,
988        );
989
990        let _ = visuals.register_stencil_clip(clip_handle, 0);
991        let _ = visuals.update_stencil_ref(content_handle, 1);
992        visuals.prepare_draw_cache();
993
994        let (ops, instance_indices) = visuals.cutout_stream();
995        assert_eq!(ops.len(), 4);
996        assert_eq!(instance_indices.len(), 2);
997
998        match ops[0] {
999            RenderOp::EnterClip {
1000                parent_ref,
1001                new_ref,
1002                ..
1003            } => {
1004                assert_eq!(parent_ref, 0);
1005                assert_eq!(new_ref, 1);
1006            }
1007            other => panic!("expected EnterClip, got {other:?}"),
1008        }
1009        match ops[1] {
1010            RenderOp::DrawBatch(batch) => {
1011                assert_eq!(batch.stencil_ref, 1);
1012                assert_eq!(batch.count, 1);
1013                assert_eq!(instance_indices[batch.start], 0);
1014            }
1015            other => panic!("expected clip-source DrawBatch, got {other:?}"),
1016        }
1017        match ops[2] {
1018            RenderOp::DrawBatch(batch) => {
1019                assert_eq!(batch.stencil_ref, 1);
1020                assert_eq!(batch.count, 1);
1021                assert_eq!(instance_indices[batch.start], 1);
1022            }
1023            other => panic!("expected content DrawBatch, got {other:?}"),
1024        }
1025        match ops[3] {
1026            RenderOp::ExitClip { ref_value, .. } => assert_eq!(ref_value, 1),
1027            other => panic!("expected ExitClip, got {other:?}"),
1028        }
1029    }
1030
1031    #[test]
1032    fn transparent_single_stream_enters_and_exits_single_root_clip() {
1033        let mut visuals = VisualWorld::default();
1034
1035        let clip_handle = visuals.register(
1036            cid(30),
1037            dummy_renderable(),
1038            Transform::default(),
1039            [1.0, 1.0, 1.0, 0.5],
1040            1.0,
1041            false,
1042            false,
1043            false,
1044            false,
1045            false,
1046            0.0,
1047            None,
1048            3.0,
1049        );
1050        let content_handle = visuals.register(
1051            cid(31),
1052            dummy_renderable(),
1053            Transform::default(),
1054            [0.8, 0.8, 0.8, 0.5],
1055            1.0,
1056            false,
1057            false,
1058            false,
1059            false,
1060            false,
1061            0.0,
1062            None,
1063            3.0,
1064        );
1065
1066        let _ = visuals.register_stencil_clip(clip_handle, 0);
1067        let _ = visuals.update_stencil_ref(content_handle, 1);
1068        visuals.prepare_draw_cache();
1069
1070        let (ops, instance_indices) = visuals.transparent_single_stream();
1071        assert_eq!(ops.len(), 4);
1072        assert_eq!(instance_indices.len(), 2);
1073
1074        match ops[0] {
1075            RenderOp::EnterClip {
1076                parent_ref,
1077                new_ref,
1078                ..
1079            } => {
1080                assert_eq!(parent_ref, 0);
1081                assert_eq!(new_ref, 1);
1082            }
1083            other => panic!("expected EnterClip, got {other:?}"),
1084        }
1085        match ops[1] {
1086            RenderOp::DrawBatch(batch) => {
1087                assert_eq!(batch.stencil_ref, 1);
1088                assert_eq!(batch.count, 1);
1089                assert_eq!(instance_indices[batch.start], 0);
1090            }
1091            other => panic!("expected clip-source DrawBatch, got {other:?}"),
1092        }
1093        match ops[2] {
1094            RenderOp::DrawBatch(batch) => {
1095                assert_eq!(batch.stencil_ref, 1);
1096                assert_eq!(batch.count, 1);
1097                assert_eq!(instance_indices[batch.start], 1);
1098            }
1099            other => panic!("expected content DrawBatch, got {other:?}"),
1100        }
1101        match ops[3] {
1102            RenderOp::ExitClip { ref_value, .. } => assert_eq!(ref_value, 1),
1103            other => panic!("expected ExitClip, got {other:?}"),
1104        }
1105    }
1106
1107    #[test]
1108    fn opaque_stream_uses_transparent_clip_source_for_stencil_ops() {
1109        let mut visuals = VisualWorld::default();
1110
1111        let clip_handle = visuals.register(
1112            cid(40),
1113            dummy_renderable(),
1114            Transform::default(),
1115            [1.0, 1.0, 1.0, 0.0],
1116            1.0,
1117            false,
1118            false,
1119            false,
1120            false,
1121            false,
1122            0.0,
1123            None,
1124            3.0,
1125        );
1126        let content_handle = visuals.register(
1127            cid(41),
1128            dummy_renderable(),
1129            Transform::default(),
1130            [0.8, 0.8, 0.8, 1.0],
1131            1.0,
1132            false,
1133            false,
1134            false,
1135            false,
1136            false,
1137            0.0,
1138            None,
1139            3.0,
1140        );
1141
1142        let _ = visuals.register_stencil_clip(clip_handle, 0);
1143        let _ = visuals.update_stencil_ref(content_handle, 1);
1144        visuals.prepare_draw_cache();
1145
1146        let (ops, instance_indices) = visuals.opaque_stream();
1147        assert_eq!(ops.len(), 3);
1148        assert_eq!(instance_indices, &[0, 1]);
1149
1150        match ops[0] {
1151            RenderOp::EnterClip {
1152                parent_ref,
1153                new_ref,
1154                ..
1155            } => {
1156                assert_eq!(parent_ref, 0);
1157                assert_eq!(new_ref, 1);
1158            }
1159            other => panic!("expected EnterClip, got {other:?}"),
1160        }
1161        match ops[1] {
1162            RenderOp::DrawBatch(batch) => {
1163                assert_eq!(batch.stencil_ref, 1);
1164                assert_eq!(batch.count, 1);
1165                assert_eq!(instance_indices[batch.start], 1);
1166            }
1167            other => panic!("expected content DrawBatch, got {other:?}"),
1168        }
1169        match ops[2] {
1170            RenderOp::ExitClip { ref_value, .. } => assert_eq!(ref_value, 1),
1171            other => panic!("expected ExitClip, got {other:?}"),
1172        }
1173    }
1174
1175    #[test]
1176    fn cutout_stream_uses_opaque_clip_source_for_stencil_ops() {
1177        let mut visuals = VisualWorld::default();
1178
1179        let clip_handle = visuals.register(
1180            cid(50),
1181            dummy_renderable(),
1182            Transform::default(),
1183            [1.0, 1.0, 1.0, 1.0],
1184            1.0,
1185            false,
1186            false,
1187            false,
1188            false,
1189            false,
1190            0.0,
1191            None,
1192            3.0,
1193        );
1194        let content_handle = visuals.register(
1195            cid(51),
1196            dummy_renderable(),
1197            Transform::default(),
1198            [0.8, 0.8, 0.8, 1.0],
1199            1.0,
1200            false,
1201            true,
1202            false,
1203            false,
1204            false,
1205            0.0,
1206            None,
1207            3.0,
1208        );
1209
1210        let _ = visuals.register_stencil_clip(clip_handle, 0);
1211        let _ = visuals.update_stencil_ref(content_handle, 1);
1212        visuals.prepare_draw_cache();
1213
1214        let (ops, instance_indices) = visuals.cutout_stream();
1215        assert_eq!(ops.len(), 3);
1216        assert_eq!(instance_indices, &[0, 1]);
1217
1218        match ops[0] {
1219            RenderOp::EnterClip {
1220                parent_ref,
1221                new_ref,
1222                ..
1223            } => {
1224                assert_eq!(parent_ref, 0);
1225                assert_eq!(new_ref, 1);
1226            }
1227            other => panic!("expected EnterClip, got {other:?}"),
1228        }
1229        match ops[1] {
1230            RenderOp::DrawBatch(batch) => {
1231                assert_eq!(batch.stencil_ref, 1);
1232                assert_eq!(batch.count, 1);
1233                assert_eq!(instance_indices[batch.start], 1);
1234            }
1235            other => panic!("expected content DrawBatch, got {other:?}"),
1236        }
1237        match ops[2] {
1238            RenderOp::ExitClip { ref_value, .. } => assert_eq!(ref_value, 1),
1239            other => panic!("expected ExitClip, got {other:?}"),
1240        }
1241    }
1242
1243    #[test]
1244    fn opaque_stream_excluding_source_instance_skips_clip_ops_for_that_source() {
1245        let mut visuals = VisualWorld::default();
1246
1247        let clip_handle = visuals.register(
1248            cid(60),
1249            dummy_renderable(),
1250            Transform::default(),
1251            [1.0, 1.0, 1.0, 0.0],
1252            1.0,
1253            false,
1254            false,
1255            false,
1256            false,
1257            false,
1258            0.0,
1259            None,
1260            3.0,
1261        );
1262        let content_handle = visuals.register(
1263            cid(61),
1264            dummy_renderable(),
1265            Transform::default(),
1266            [0.8, 0.8, 0.8, 1.0],
1267            1.0,
1268            false,
1269            false,
1270            false,
1271            false,
1272            false,
1273            0.0,
1274            None,
1275            3.0,
1276        );
1277
1278        let _ = visuals.register_stencil_clip(clip_handle, 0);
1279        let _ = visuals.update_stencil_ref(content_handle, 1);
1280        visuals.prepare_draw_cache();
1281
1282        let (ops, instance_indices) = visuals.opaque_stream_excluding(Some(clip_handle));
1283        assert_eq!(instance_indices, &[1]);
1284        assert_eq!(ops.len(), 1);
1285        match ops[0] {
1286            RenderOp::DrawBatch(batch) => {
1287                assert_eq!(batch.count, 1);
1288                assert_eq!(batch.stencil_ref, 1);
1289                assert_eq!(instance_indices[batch.start], 1);
1290            }
1291            other => panic!("expected DrawBatch, got {other:?}"),
1292        }
1293    }
1294
1295    #[test]
1296    fn phase_exclusion_helpers_drop_source_from_overlay_cutout_and_transparent_passes() {
1297        let mut visuals = VisualWorld::default();
1298
1299        let overlay_handle = visuals.register(
1300            cid(70),
1301            dummy_renderable(),
1302            Transform::default(),
1303            [1.0, 1.0, 1.0, 1.0],
1304            1.0,
1305            false,
1306            false,
1307            false,
1308            false,
1309            true,
1310            0.0,
1311            None,
1312            3.0,
1313        );
1314        let cutout_handle = visuals.register(
1315            cid(71),
1316            dummy_renderable(),
1317            Transform::default(),
1318            [1.0, 1.0, 1.0, 1.0],
1319            1.0,
1320            false,
1321            true,
1322            false,
1323            false,
1324            false,
1325            0.0,
1326            None,
1327            3.0,
1328        );
1329        let transparent_single_handle = visuals.register(
1330            cid(72),
1331            dummy_renderable(),
1332            Transform::default(),
1333            [1.0, 1.0, 1.0, 0.5],
1334            1.0,
1335            false,
1336            false,
1337            false,
1338            false,
1339            false,
1340            0.0,
1341            None,
1342            3.0,
1343        );
1344        let transparent_multi_handle = visuals.register(
1345            cid(73),
1346            dummy_renderable(),
1347            Transform::default(),
1348            [1.0, 1.0, 1.0, 0.5],
1349            1.0,
1350            true,
1351            false,
1352            false,
1353            false,
1354            false,
1355            0.0,
1356            None,
1357            3.0,
1358        );
1359        visuals.prepare_draw_cache();
1360        visuals.prepare_transparent_multi_draw_cache_for_view(Transform::default().model);
1361
1362        let (_, overlay_instances) = visuals.overlay_stream_excluding(Some(overlay_handle));
1363        let (_, cutout_instances) = visuals.cutout_stream_excluding(Some(cutout_handle));
1364        let (_, transparent_single_instances) =
1365            visuals.transparent_single_stream_excluding(Some(transparent_single_handle));
1366        let (transparent_multi_order, _) =
1367            visuals.transparent_multi_draw_excluding(Some(transparent_multi_handle));
1368
1369        assert!(!overlay_instances.contains(&0));
1370        assert!(!cutout_instances.contains(&1));
1371        assert!(!transparent_single_instances.contains(&2));
1372        assert!(!transparent_multi_order.contains(&3));
1373    }
1374}
1375#[derive(Debug, Clone, Copy, Default)]
1376pub struct VisualPointLight {
1377    /// Light type discriminator for GPU shading.
1378    ///
1379    /// Matches shader constants in `assets/shaders/toon-mesh.frag`:
1380    /// - 1 = point
1381    /// - 2 = directional
1382    pub light_type: u32,
1383    pub position_ws: [f32; 3],
1384    pub intensity: f32,
1385    pub distance: f32,
1386    pub color: [f32; 3],
1387}
1388
1389impl VisualWorld {
1390    pub fn new() -> Self {
1391        Self::default()
1392    }
1393
1394    fn is_transparent(inst: &VisualInstance) -> bool {
1395        // Conservative: any non-1 alpha/opacity is treated as transparent.
1396        // (Texture alpha is not considered here; that would require texture metadata.)
1397        inst.opacity < 0.999 || inst.color[3] < 0.999
1398    }
1399
1400    fn view_space_z(view: TransformMatrix, model: TransformMatrix) -> f32 {
1401        // Matrices are stored as column vectors (shader uses mat4(i_model_c0..c3)).
1402        // Translation is column 3: (tx, ty, tz).
1403        let tx = model[3][0];
1404        let ty = model[3][1];
1405        let tz = model[3][2];
1406
1407        // Multiply view * vec4(t,1) and return z.
1408        view[0][2] * tx + view[1][2] * ty + view[2][2] * tz + view[3][2]
1409    }
1410
1411    fn build_draw_batches_for_order(
1412        instances: &[VisualInstance],
1413        order: &[u32],
1414        out: &mut Vec<DrawBatch>,
1415    ) {
1416        out.clear();
1417        let mut cursor = 0usize;
1418        while cursor < order.len() {
1419            let idx0 = order[cursor] as usize;
1420            let inst0 = instances[idx0];
1421            let r0 = inst0.renderable;
1422            let material = r0.material;
1423            let mesh = r0.mesh;
1424            let texture = inst0.texture;
1425            let texture_filtering = inst0.texture_filtering;
1426            let quant_steps = sanitize_quant_steps(inst0.quant_steps);
1427            let stencil_ref = inst0.stencil_ref;
1428
1429            let start = cursor;
1430            cursor += 1;
1431
1432            while cursor < order.len() {
1433                let idx = order[cursor] as usize;
1434                let inst = instances[idx];
1435                let r = inst.renderable;
1436                if r.material == material
1437                    && r.mesh == mesh
1438                    && inst.texture == texture
1439                    && inst.texture_filtering == texture_filtering
1440                    && sanitize_quant_steps(inst.quant_steps).to_bits() == quant_steps.to_bits()
1441                    && inst.stencil_ref == stencil_ref
1442                {
1443                    cursor += 1;
1444                } else {
1445                    break;
1446                }
1447            }
1448
1449            out.push(DrawBatch {
1450                material,
1451                mesh,
1452                texture,
1453                texture_filtering,
1454                quant_steps,
1455                stencil_ref,
1456                start,
1457                count: cursor - start,
1458            });
1459        }
1460    }
1461
1462    fn filtered_phase_order(
1463        &self,
1464        order: &[u32],
1465        excluded_instance: Option<InstanceHandle>,
1466    ) -> Vec<u32> {
1467        let Some(excluded_index) =
1468            excluded_instance.and_then(|handle| self.handle_to_index.get(&handle).copied())
1469        else {
1470            return order.to_vec();
1471        };
1472        order
1473            .iter()
1474            .copied()
1475            .filter(|&idx| idx as usize != excluded_index)
1476            .collect()
1477    }
1478
1479    fn filtered_clip_sources_by_depth(
1480        &self,
1481        max_depth: u8,
1482        excluded_instance: Option<InstanceHandle>,
1483    ) -> Vec<Vec<u32>> {
1484        let mut clip_sources_by_depth = vec![Vec::new(); max_depth as usize + 1];
1485        let excluded_index =
1486            excluded_instance.and_then(|handle| self.handle_to_index.get(&handle).copied());
1487        for (index, inst) in self.instances.iter().enumerate() {
1488            if !inst.is_stencil_clip {
1489                continue;
1490            }
1491            if Some(index) == excluded_index {
1492                continue;
1493            }
1494            clip_sources_by_depth[inst.stencil_ref as usize].push(index as u32);
1495        }
1496        clip_sources_by_depth
1497    }
1498
1499    /// Build the per-phase DFS render stream for the overlay pass.
1500    ///
1501    /// `overlay_order` must already be sorted by `(stencil_ref, material, tex, mesh, ...)`.
1502    ///
1503    /// Algorithm (one level at a time):
1504    /// - Non-clip instances at depth D → `DrawBatch` at stencil_ref D.
1505    /// - Clip sources at depth D → `EnterClip`, then their visual draw at stencil_ref D+1,
1506    ///   then all content at depth D+1 (recursed), then `ExitClip`.
1507    fn build_phase_render_stream(
1508        instances: &[VisualInstance],
1509        phase_order: &[u32],
1510        out_ops: &mut Vec<RenderOp>,
1511        out_instances: &mut Vec<u32>,
1512    ) {
1513        out_ops.clear();
1514        out_instances.clear();
1515
1516        if phase_order.is_empty() {
1517            return;
1518        }
1519
1520        let max_depth = phase_order
1521            .iter()
1522            .map(|&i| instances[i as usize].stencil_ref)
1523            .chain(
1524                instances
1525                    .iter()
1526                    .filter(|inst| inst.is_stencil_clip)
1527                    .map(|inst| inst.stencil_ref),
1528            )
1529            .max()
1530            .unwrap_or(0);
1531
1532        // Split into per-depth groups preserving sort order within each group.
1533        // Group[d] = (non_clip indices, clip_source indices) at stencil_ref == d.
1534        let depth_count = max_depth as usize + 1;
1535        let mut non_clip_by_depth: Vec<Vec<u32>> = vec![Vec::new(); depth_count];
1536        let mut phase_clip_sources_by_depth: Vec<Vec<u32>> = vec![Vec::new(); depth_count];
1537        let mut all_clip_sources_by_depth: Vec<Vec<u32>> = vec![Vec::new(); depth_count];
1538
1539        for (index, inst) in instances.iter().enumerate() {
1540            if inst.is_stencil_clip {
1541                all_clip_sources_by_depth[inst.stencil_ref as usize].push(index as u32);
1542            }
1543        }
1544
1545        for &idx in phase_order {
1546            let inst = &instances[idx as usize];
1547            let d = inst.stencil_ref as usize;
1548            if inst.is_stencil_clip {
1549                phase_clip_sources_by_depth[d].push(idx);
1550            } else {
1551                non_clip_by_depth[d].push(idx);
1552            }
1553        }
1554
1555        Self::build_overlay_level(
1556            0,
1557            max_depth,
1558            instances,
1559            &non_clip_by_depth,
1560            &all_clip_sources_by_depth,
1561            &phase_clip_sources_by_depth,
1562            out_ops,
1563            out_instances,
1564        );
1565    }
1566
1567    fn build_phase_render_stream_excluding(
1568        &self,
1569        phase_order: &[u32],
1570        excluded_instance: Option<InstanceHandle>,
1571        out_ops: &mut Vec<RenderOp>,
1572        out_instances: &mut Vec<u32>,
1573    ) {
1574        let filtered_order = self.filtered_phase_order(phase_order, excluded_instance);
1575        out_ops.clear();
1576        out_instances.clear();
1577
1578        if filtered_order.is_empty() {
1579            return;
1580        }
1581
1582        let excluded_index =
1583            excluded_instance.and_then(|handle| self.handle_to_index.get(&handle).copied());
1584        let max_depth = filtered_order
1585            .iter()
1586            .map(|&i| self.instances[i as usize].stencil_ref)
1587            .chain(
1588                self.instances
1589                    .iter()
1590                    .enumerate()
1591                    .filter(|inst| {
1592                        let (index, inst) = inst;
1593                        inst.is_stencil_clip && Some(*index) != excluded_index
1594                    })
1595                    .map(|(_, inst)| inst.stencil_ref),
1596            )
1597            .max()
1598            .unwrap_or(0);
1599
1600        let depth_count = max_depth as usize + 1;
1601        let mut non_clip_by_depth: Vec<Vec<u32>> = vec![Vec::new(); depth_count];
1602        let mut phase_clip_sources_by_depth: Vec<Vec<u32>> = vec![Vec::new(); depth_count];
1603        let all_clip_sources_by_depth =
1604            self.filtered_clip_sources_by_depth(max_depth, excluded_instance);
1605
1606        for &idx in &filtered_order {
1607            let inst = &self.instances[idx as usize];
1608            let d = inst.stencil_ref as usize;
1609            if inst.is_stencil_clip {
1610                phase_clip_sources_by_depth[d].push(idx);
1611            } else {
1612                non_clip_by_depth[d].push(idx);
1613            }
1614        }
1615
1616        Self::build_overlay_level(
1617            0,
1618            max_depth,
1619            &self.instances,
1620            &non_clip_by_depth,
1621            &all_clip_sources_by_depth,
1622            &phase_clip_sources_by_depth,
1623            out_ops,
1624            out_instances,
1625        );
1626    }
1627
1628    fn build_overlay_level(
1629        depth: usize,
1630        max_depth: u8,
1631        instances: &[VisualInstance],
1632        non_clip_by_depth: &[Vec<u32>],
1633        all_clip_sources_by_depth: &[Vec<u32>],
1634        phase_clip_sources_by_depth: &[Vec<u32>],
1635        out_ops: &mut Vec<RenderOp>,
1636        out_instances: &mut Vec<u32>,
1637    ) {
1638        if depth >= non_clip_by_depth.len() {
1639            return;
1640        }
1641
1642        // Draw non-clip instances at this depth.
1643        let non_clip = &non_clip_by_depth[depth];
1644        if !non_clip.is_empty() {
1645            Self::append_stream_batches(instances, non_clip, depth as u8, out_ops, out_instances);
1646        }
1647
1648        let all_clip_sources = &all_clip_sources_by_depth[depth];
1649        if !all_clip_sources.is_empty() {
1650            let parent_ref = depth as u8;
1651            let new_ref = parent_ref.saturating_add(1);
1652            let phase_clip_sources = &phase_clip_sources_by_depth[depth];
1653
1654            // Emit EnterClip for every clip source at this depth.
1655            for &src_idx in all_clip_sources {
1656                if !phase_clip_sources.contains(&src_idx) {
1657                    Self::append_clip_stream_instance(out_instances, src_idx);
1658                }
1659                out_ops.push(RenderOp::EnterClip {
1660                    instance_index: src_idx,
1661                    parent_ref,
1662                    new_ref,
1663                });
1664            }
1665
1666            // Clip sources also draw as normal color instances, but at new_ref
1667            // (they are inside their own clip region after the INCR).
1668            if !phase_clip_sources.is_empty() {
1669                Self::append_stream_batches(
1670                    instances,
1671                    phase_clip_sources,
1672                    new_ref,
1673                    out_ops,
1674                    out_instances,
1675                );
1676            }
1677
1678            // Recurse: all content at the next depth is inside the clip region.
1679            if depth + 1 <= max_depth as usize {
1680                Self::build_overlay_level(
1681                    depth + 1,
1682                    max_depth,
1683                    instances,
1684                    non_clip_by_depth,
1685                    all_clip_sources_by_depth,
1686                    phase_clip_sources_by_depth,
1687                    out_ops,
1688                    out_instances,
1689                );
1690            }
1691
1692            // Emit ExitClip in reverse order (innermost-first, which here means
1693            // the last clip source entered is the first exited).
1694            for &src_idx in all_clip_sources.iter().rev() {
1695                out_ops.push(RenderOp::ExitClip {
1696                    instance_index: src_idx,
1697                    ref_value: new_ref,
1698                });
1699            }
1700        } else if depth + 1 <= max_depth as usize {
1701            // No clips here but deeper levels exist; keep recursing.
1702            Self::build_overlay_level(
1703                depth + 1,
1704                max_depth,
1705                instances,
1706                non_clip_by_depth,
1707                all_clip_sources_by_depth,
1708                phase_clip_sources_by_depth,
1709                out_ops,
1710                out_instances,
1711            );
1712        }
1713    }
1714
1715    fn append_clip_stream_instance(out_instances: &mut Vec<u32>, instance_index: u32) {
1716        if !out_instances.contains(&instance_index) {
1717            out_instances.push(instance_index);
1718        }
1719    }
1720
1721    /// Append `DrawBatch` ops for a pre-sorted slice of instance indices.
1722    ///
1723    /// `effective_ref` overrides the per-instance `stencil_ref` — necessary for clip
1724    /// sources that are visually drawn inside their own region (`new_ref`).
1725    fn append_stream_batches(
1726        instances: &[VisualInstance],
1727        indices: &[u32],
1728        effective_ref: u8,
1729        out_ops: &mut Vec<RenderOp>,
1730        out_instances: &mut Vec<u32>,
1731    ) {
1732        let mut cursor = 0usize;
1733        while cursor < indices.len() {
1734            let idx0 = indices[cursor] as usize;
1735            let inst0 = &instances[idx0];
1736            let r0 = inst0.renderable;
1737            let material = r0.material;
1738            let mesh = r0.mesh;
1739            let texture = inst0.texture;
1740            let texture_filtering = inst0.texture_filtering;
1741            let quant_steps = sanitize_quant_steps(inst0.quant_steps);
1742
1743            let start = out_instances.len();
1744            out_instances.push(indices[cursor]);
1745            cursor += 1;
1746
1747            while cursor < indices.len() {
1748                let idx = indices[cursor] as usize;
1749                let inst = &instances[idx];
1750                let r = inst.renderable;
1751                if r.material == material
1752                    && r.mesh == mesh
1753                    && inst.texture == texture
1754                    && inst.texture_filtering == texture_filtering
1755                    && sanitize_quant_steps(inst.quant_steps).to_bits() == quant_steps.to_bits()
1756                {
1757                    out_instances.push(indices[cursor]);
1758                    cursor += 1;
1759                } else {
1760                    break;
1761                }
1762            }
1763
1764            out_ops.push(RenderOp::DrawBatch(DrawBatch {
1765                material,
1766                mesh,
1767                texture,
1768                texture_filtering,
1769                quant_steps,
1770                stencil_ref: effective_ref,
1771                start,
1772                count: out_instances.len() - start,
1773            }));
1774        }
1775    }
1776
1777    pub fn clear_color(&self) -> [f32; 4] {
1778        self.clear_color
1779    }
1780
1781    pub fn set_clear_color(&mut self, rgba: [f32; 4]) {
1782        self.clear_color = rgba;
1783    }
1784
1785    pub fn renderer_msaa_mode(&self) -> MsaaMode {
1786        self.renderer_msaa_mode
1787    }
1788
1789    pub fn set_renderer_msaa_mode(&mut self, mode: MsaaMode) {
1790        self.renderer_msaa_mode = mode;
1791    }
1792
1793    pub fn preferred_window_size(&self) -> Option<[u32; 2]> {
1794        self.preferred_window_size
1795    }
1796
1797    pub fn set_preferred_window_size(&mut self, size: Option<[u32; 2]>) {
1798        self.preferred_window_size = size.filter(|[w, h]| *w > 0 && *h > 0);
1799    }
1800
1801    pub fn take_preferred_window_size(&mut self) -> Option<[u32; 2]> {
1802        self.preferred_window_size.take()
1803    }
1804
1805    pub fn window_frame_dt_sec(&self) -> f32 {
1806        self.window_frame_dt_sec
1807    }
1808
1809    pub fn window_frame_fps(&self) -> f32 {
1810        if self.window_frame_dt_sec > 0.0 {
1811            1.0 / self.window_frame_dt_sec
1812        } else {
1813            0.0
1814        }
1815    }
1816
1817    pub fn set_window_frame_dt_sec(&mut self, dt_sec: f32) {
1818        self.window_frame_dt_sec = dt_sec.max(0.0);
1819    }
1820
1821    pub fn xr_frame_dt_sec(&self) -> Option<f32> {
1822        self.xr_frame_dt_sec
1823    }
1824
1825    pub fn xr_frame_fps(&self) -> Option<f32> {
1826        let dt = self.xr_frame_dt_sec?;
1827        if dt > 0.0 { Some(1.0 / dt) } else { None }
1828    }
1829
1830    pub fn set_xr_frame_dt_sec(&mut self, dt_sec: Option<f32>) {
1831        self.xr_frame_dt_sec = dt_sec.and_then(|dt| {
1832            if dt.is_finite() {
1833                Some(dt.max(0.0))
1834            } else {
1835                None
1836            }
1837        });
1838    }
1839
1840    pub fn ambient_light(&self) -> [f32; 3] {
1841        self.ambient_light
1842    }
1843
1844    pub fn set_ambient_light(&mut self, rgb: [f32; 3]) {
1845        self.ambient_light = rgb;
1846        // Stored in the global camera UBO for now.
1847        self.dirty_camera = true;
1848    }
1849
1850    pub fn clear(&mut self) {
1851        self.instances.clear();
1852        self.handle_to_index.clear();
1853        self.component_to_handle.clear();
1854        self.next_handle = 0;
1855
1856        self.point_lights.clear();
1857        self.point_light_index_by_component.clear();
1858        self.dirty_lights = true;
1859
1860        self.ambient_light = [0.0, 0.0, 0.0];
1861
1862        self.dirty_draw_cache = true;
1863        self.dirty_instance_data = true;
1864        self.dirty_camera = true;
1865        self.background_order.clear();
1866        self.background_batches.clear();
1867        self.background_occluded_lit_order.clear();
1868        self.background_occluded_lit_batches.clear();
1869        self.background_occluded_lit_emissive_order.clear();
1870        self.background_occluded_lit_emissive_batches.clear();
1871        self.draw_order.clear();
1872        self.draw_batches.clear();
1873        self.emissive_draw_order.clear();
1874        self.emissive_draw_batches.clear();
1875        self.cutout_order.clear();
1876        self.cutout_batches.clear();
1877        self.emissive_cutout_order.clear();
1878        self.emissive_cutout_batches.clear();
1879        self.cutout_stream.clear();
1880        self.cutout_stream_instances.clear();
1881
1882        self.transparent_single_draw_order.clear();
1883        self.transparent_single_draw_batches.clear();
1884        self.transparent_single_stream.clear();
1885        self.transparent_single_stream_instances.clear();
1886        self.transparent_multi_draw_order.clear();
1887        self.transparent_multi_draw_batches.clear();
1888
1889        self.active_xr_camera = None;
1890    }
1891
1892    pub fn active_xr_camera(&self) -> Option<ComponentId> {
1893        self.active_xr_camera
1894    }
1895
1896    pub fn set_active_xr_camera(&mut self, component: Option<ComponentId>) {
1897        self.active_xr_camera = component;
1898    }
1899
1900    pub fn lights_dirty(&self) -> bool {
1901        self.dirty_lights
1902    }
1903
1904    pub fn take_lights_dirty(&mut self) -> bool {
1905        let v = self.dirty_lights;
1906        self.dirty_lights = false;
1907        v
1908    }
1909
1910    pub fn point_lights(&self) -> &[VisualPointLight] {
1911        &self.point_lights
1912    }
1913
1914    pub fn upsert_point_light(&mut self, cid: ComponentId, light: VisualPointLight) {
1915        if let Some(&idx) = self.point_light_index_by_component.get(&cid) {
1916            self.point_lights[idx] = light;
1917        } else {
1918            let idx = self.point_lights.len();
1919            self.point_lights.push(light);
1920            self.point_light_index_by_component.insert(cid, idx);
1921        }
1922        self.dirty_lights = true;
1923    }
1924
1925    pub fn camera_dirty(&self) -> bool {
1926        self.dirty_camera
1927    }
1928
1929    pub fn take_camera_dirty(&mut self) -> bool {
1930        let v = self.dirty_camera;
1931        self.dirty_camera = false;
1932        v
1933    }
1934
1935    pub fn visual_cameras(&self) -> &[VisualCamera] {
1936        &self.visual_cameras
1937    }
1938
1939    pub fn visual_camera(&self, target: CameraTarget) -> Option<&VisualCamera> {
1940        self.visual_cameras.iter().find(|c| c.target == target)
1941    }
1942
1943    fn visual_camera_mut(&mut self, target: CameraTarget) -> &mut VisualCamera {
1944        if let Some(i) = self.visual_cameras.iter().position(|c| c.target == target) {
1945            return &mut self.visual_cameras[i];
1946        }
1947
1948        self.visual_cameras.push(VisualCamera {
1949            target,
1950            eyes: Vec::new(),
1951        });
1952        self.visual_cameras.last_mut().unwrap()
1953    }
1954
1955    /// Window-facing compatibility: returns the first eye's view matrix for the window target.
1956    pub fn camera_view(&self) -> [[f32; 4]; 4] {
1957        self.camera_view_for(CameraTarget::Window)
1958    }
1959
1960    /// Window-facing compatibility: returns the first eye's projection matrix for the window target.
1961    pub fn camera_proj(&self) -> [[f32; 4]; 4] {
1962        self.camera_proj_for(CameraTarget::Window)
1963    }
1964
1965    pub fn camera_view_for(&self, target: CameraTarget) -> [[f32; 4]; 4] {
1966        self.camera_view_for_eye(target, 0)
1967    }
1968
1969    pub fn camera_view_for_eye(&self, target: CameraTarget, eye: usize) -> [[f32; 4]; 4] {
1970        self.visual_camera(target)
1971            .and_then(|c| c.eyes.get(eye))
1972            .map(|e| e.view)
1973            .unwrap_or([
1974                [1.0, 0.0, 0.0, 0.0],
1975                [0.0, 1.0, 0.0, 0.0],
1976                [0.0, 0.0, 1.0, 0.0],
1977                [0.0, 0.0, 0.0, 1.0],
1978            ])
1979    }
1980
1981    pub fn camera_proj_for(&self, target: CameraTarget) -> [[f32; 4]; 4] {
1982        self.camera_proj_for_eye(target, 0)
1983    }
1984
1985    pub fn camera_proj_for_eye(&self, target: CameraTarget, eye: usize) -> [[f32; 4]; 4] {
1986        self.visual_camera(target)
1987            .and_then(|c| c.eyes.get(eye))
1988            .map(|e| e.proj)
1989            .unwrap_or([
1990                [1.0, 0.0, 0.0, 0.0],
1991                [0.0, 1.0, 0.0, 0.0],
1992                [0.0, 0.0, 1.0, 0.0],
1993                [0.0, 0.0, 0.0, 1.0],
1994            ])
1995    }
1996
1997    pub fn viewport(&self) -> [f32; 2] {
1998        self.viewport
1999    }
2000
2001    pub fn set_viewport(&mut self, viewport: [f32; 2]) {
2002        self.viewport = viewport;
2003    }
2004
2005    pub fn camera_2d(&self) -> [[f32; 4]; 3] {
2006        self.camera_2d
2007    }
2008
2009    pub fn set_camera(&mut self, view: [[f32; 4]; 4], proj: [[f32; 4]; 4]) {
2010        self.set_camera_mono_for_target(CameraTarget::Window, view, proj);
2011        // When a 3D camera becomes active, the 2D camera transform should be neutral.
2012        self.camera_2d = [
2013            [1.0, 0.0, 0.0, 0.0],
2014            [0.0, 1.0, 0.0, 0.0],
2015            [0.0, 0.0, 1.0, 0.0],
2016        ];
2017        self.dirty_camera = true;
2018    }
2019
2020    /// Set all eyes for a target.
2021    ///
2022    /// - For `CameraTarget::Window`, pass a 1-element `eyes` vector.
2023    /// - For `CameraTarget::Xr`, pass 2 (or more) eyes.
2024    pub fn set_camera_for_target(&mut self, target: CameraTarget, eyes: Vec<CameraData>) {
2025        let c = self.visual_camera_mut(target);
2026        c.eyes = eyes;
2027        self.dirty_camera = true;
2028    }
2029
2030    /// Convenience: set a single-eye camera for a target.
2031    pub fn set_camera_mono_for_target(
2032        &mut self,
2033        target: CameraTarget,
2034        view: [[f32; 4]; 4],
2035        proj: [[f32; 4]; 4],
2036    ) {
2037        self.set_camera_mono_for_target_with_transform(target, view, proj, Transform::default());
2038    }
2039
2040    pub fn set_camera_mono_for_target_with_transform(
2041        &mut self,
2042        target: CameraTarget,
2043        view: [[f32; 4]; 4],
2044        proj: [[f32; 4]; 4],
2045        transform: Transform,
2046    ) {
2047        self.set_camera_for_target(
2048            target,
2049            vec![CameraData {
2050                view,
2051                proj,
2052                transform,
2053            }],
2054        );
2055    }
2056
2057    /// Convenience: set XR eyes.
2058    pub fn set_xr_camera(&mut self, eyes: Vec<CameraData>) {
2059        self.set_camera_for_target(CameraTarget::Xr, eyes);
2060    }
2061
2062    pub fn set_camera_2d(&mut self, m: [[f32; 4]; 3]) {
2063        if self.camera_2d == m {
2064            return;
2065        }
2066        self.camera_2d = m;
2067        self.dirty_camera = true;
2068    }
2069
2070    pub fn register_mirror(&mut self, mirror: VisualMirror) {
2071        self.mirrors.push(mirror);
2072    }
2073
2074    pub fn clear_mirrors(&mut self) {
2075        self.mirrors.clear();
2076    }
2077
2078    pub fn mirrors(&self) -> &[VisualMirror] {
2079        &self.mirrors
2080    }
2081
2082    pub fn mirror_texture_key_for_view(
2083        &self,
2084        mirror_component: ComponentId,
2085        family: MirrorViewerFamily,
2086        view_index: usize,
2087    ) -> Option<&str> {
2088        self.mirrors
2089            .iter()
2090            .find(|mirror| mirror.mirror_component == mirror_component)
2091            .and_then(|mirror| {
2092                mirror
2093                    .captures
2094                    .iter()
2095                    .find(|capture| capture.family == family && capture.view_index == view_index)
2096                    .map(|capture| capture.target_key.as_str())
2097            })
2098    }
2099
2100    pub fn mirror_count(&self) -> usize {
2101        self.mirrors.len()
2102    }
2103
2104    /// Returns whether any per-instance data has changed since the last time it was consumed.
2105    pub fn instance_data_dirty(&self) -> bool {
2106        self.dirty_instance_data
2107    }
2108
2109    /// Consume the instance-data dirty flag.
2110    pub fn take_instance_data_dirty(&mut self) -> bool {
2111        let v = self.dirty_instance_data;
2112        self.dirty_instance_data = false;
2113        v
2114    }
2115
2116    pub fn instances(&self) -> &[VisualInstance] {
2117        &self.instances
2118    }
2119
2120    pub fn instance_count(&self) -> usize {
2121        self.instances.len()
2122    }
2123
2124    /// Indices into `instances()` in the order they should be drawn (opaque batching).
2125    pub fn background_order(&self) -> &[u32] {
2126        &self.background_order
2127    }
2128
2129    pub fn background_batches(&self) -> &[DrawBatch] {
2130        &self.background_batches
2131    }
2132
2133    pub fn background_occluded_lit_order(&self) -> &[u32] {
2134        &self.background_occluded_lit_order
2135    }
2136
2137    pub fn background_occluded_lit_batches(&self) -> &[DrawBatch] {
2138        &self.background_occluded_lit_batches
2139    }
2140
2141    pub fn background_occluded_lit_emissive_order(&self) -> &[u32] {
2142        &self.background_occluded_lit_emissive_order
2143    }
2144
2145    pub fn background_occluded_lit_emissive_batches(&self) -> &[DrawBatch] {
2146        &self.background_occluded_lit_emissive_batches
2147    }
2148
2149    pub fn has_background_occluded_lit_emissive(&self) -> bool {
2150        !self.background_occluded_lit_emissive_order.is_empty()
2151    }
2152
2153    /// Indices into `instances()` in the order they should be drawn (opaque batching).
2154    pub fn draw_order(&self) -> &[u32] {
2155        &self.draw_order
2156    }
2157
2158    pub fn draw_batches(&self) -> &[DrawBatch] {
2159        &self.draw_batches
2160    }
2161
2162    /// Indices into `instances()` in the order they should be drawn (emissive opaque batching).
2163    pub fn emissive_draw_order(&self) -> &[u32] {
2164        &self.emissive_draw_order
2165    }
2166
2167    pub fn emissive_draw_batches(&self) -> &[DrawBatch] {
2168        &self.emissive_draw_batches
2169    }
2170
2171    /// Indices into `instances()` in the order they should be drawn (alpha-to-coverage cutout pass).
2172    pub fn cutout_order(&self) -> &[u32] {
2173        &self.cutout_order
2174    }
2175
2176    pub fn cutout_batches(&self) -> &[DrawBatch] {
2177        &self.cutout_batches
2178    }
2179
2180    /// DFS-ordered render stream for the alpha-to-coverage cutout phase.
2181    pub fn cutout_stream(&self) -> (&[RenderOp], &[u32]) {
2182        (&self.cutout_stream, &self.cutout_stream_instances)
2183    }
2184
2185    pub fn cutout_stream_excluding(
2186        &self,
2187        excluded_instance: Option<InstanceHandle>,
2188    ) -> (Vec<RenderOp>, Vec<u32>) {
2189        let mut ops = Vec::new();
2190        let mut instances = Vec::new();
2191        self.build_phase_render_stream_excluding(
2192            &self.cutout_order,
2193            excluded_instance,
2194            &mut ops,
2195            &mut instances,
2196        );
2197        (ops, instances)
2198    }
2199
2200    /// Indices into `instances()` in the order they should be drawn (emissive cutout batching).
2201    pub fn emissive_cutout_order(&self) -> &[u32] {
2202        &self.emissive_cutout_order
2203    }
2204
2205    pub fn emissive_cutout_batches(&self) -> &[DrawBatch] {
2206        &self.emissive_cutout_batches
2207    }
2208
2209    /// Indices into `instances()` in the order they should be drawn (overlay pass).
2210    pub fn overlay_order(&self) -> &[u32] {
2211        &self.overlay_order
2212    }
2213
2214    pub fn overlay_batches(&self) -> &[DrawBatch] {
2215        &self.overlay_batches
2216    }
2217
2218    /// DFS-ordered render stream for the single-layer transparent phase.
2219    pub fn transparent_single_stream(&self) -> (&[RenderOp], &[u32]) {
2220        (
2221            &self.transparent_single_stream,
2222            &self.transparent_single_stream_instances,
2223        )
2224    }
2225
2226    pub fn transparent_single_stream_excluding(
2227        &self,
2228        excluded_instance: Option<InstanceHandle>,
2229    ) -> (Vec<RenderOp>, Vec<u32>) {
2230        let mut ops = Vec::new();
2231        let mut instances = Vec::new();
2232        self.build_phase_render_stream_excluding(
2233            &self.transparent_single_draw_order,
2234            excluded_instance,
2235            &mut ops,
2236            &mut instances,
2237        );
2238        (ops, instances)
2239    }
2240
2241    /// DFS-ordered render stream for the overlay phase.
2242    ///
2243    /// Returns `(ops, instance_indices)`.
2244    /// - `ops` is the sequence of `EnterClip`, `DrawBatch`, and `ExitClip` commands.
2245    /// - `instance_indices[batch.start .. batch.start + batch.count]` gives the
2246    ///   `VisualInstance` indices for each `DrawBatch` op.
2247    pub fn overlay_stream(&self) -> (&[RenderOp], &[u32]) {
2248        (&self.overlay_stream, &self.overlay_stream_instances)
2249    }
2250
2251    pub fn overlay_stream_excluding(
2252        &self,
2253        excluded_instance: Option<InstanceHandle>,
2254    ) -> (Vec<RenderOp>, Vec<u32>) {
2255        let mut ops = Vec::new();
2256        let mut instances = Vec::new();
2257        self.build_phase_render_stream_excluding(
2258            &self.overlay_order,
2259            excluded_instance,
2260            &mut ops,
2261            &mut instances,
2262        );
2263        (ops, instances)
2264    }
2265
2266    /// DFS-ordered render stream for the opaque phase.
2267    ///
2268    /// Returns `(ops, instance_indices)`.
2269    pub fn opaque_stream(&self) -> (&[RenderOp], &[u32]) {
2270        (&self.opaque_stream, &self.opaque_stream_instances)
2271    }
2272
2273    pub fn opaque_stream_excluding(
2274        &self,
2275        excluded_instance: Option<InstanceHandle>,
2276    ) -> (Vec<RenderOp>, Vec<u32>) {
2277        let mut ops = Vec::new();
2278        let mut instances = Vec::new();
2279        self.build_phase_render_stream_excluding(
2280            &self.draw_order,
2281            excluded_instance,
2282            &mut ops,
2283            &mut instances,
2284        );
2285        (ops, instances)
2286    }
2287
2288    /// Indices into `instances()` where `is_stencil_clip=true`, sorted by stencil_ref ascending.
2289    /// Used by the renderer to inject stencil write/restore draws around clipped batch groups.
2290    pub fn stencil_clip_order(&self) -> &[u32] {
2291        &self.stencil_clip_order
2292    }
2293
2294    /// Mark an instance as a stencil clip source with the given reference value.
2295    /// Triggers draw cache rebuild.
2296    pub fn register_stencil_clip(&mut self, handle: InstanceHandle, stencil_ref: u8) -> bool {
2297        if let Some(&idx) = self.handle_to_index.get(&handle) {
2298            self.instances[idx].is_stencil_clip = true;
2299            self.instances[idx].stencil_ref = stencil_ref;
2300            self.dirty_draw_cache = true;
2301            true
2302        } else {
2303            false
2304        }
2305    }
2306
2307    /// Remove stencil clip status from an instance.
2308    pub fn unregister_stencil_clip(&mut self, handle: InstanceHandle) -> bool {
2309        if let Some(&idx) = self.handle_to_index.get(&handle) {
2310            self.instances[idx].is_stencil_clip = false;
2311            self.dirty_draw_cache = true;
2312            true
2313        } else {
2314            false
2315        }
2316    }
2317
2318    /// Update the stencil reference value for a clipped instance (not the clip source itself).
2319    pub fn update_stencil_ref(&mut self, handle: InstanceHandle, stencil_ref: u8) -> bool {
2320        if let Some(&idx) = self.handle_to_index.get(&handle) {
2321            if self.instances[idx].stencil_ref == stencil_ref {
2322                return true;
2323            }
2324            self.instances[idx].stencil_ref = stencil_ref;
2325            self.dirty_draw_cache = true;
2326            true
2327        } else {
2328            false
2329        }
2330    }
2331
2332    /// Indices into `instances()` in the order they should be drawn (single-layer transparent pass).
2333    pub fn transparent_single_draw_order(&self) -> &[u32] {
2334        &self.transparent_single_draw_order
2335    }
2336
2337    pub fn transparent_single_draw_batches(&self) -> &[DrawBatch] {
2338        &self.transparent_single_draw_batches
2339    }
2340
2341    /// Indices into `instances()` in the order they should be drawn (multi-layer transparent pass).
2342    pub fn transparent_multi_draw_order(&self) -> &[u32] {
2343        &self.transparent_multi_draw_order
2344    }
2345
2346    pub fn transparent_multi_draw_batches(&self) -> &[DrawBatch] {
2347        &self.transparent_multi_draw_batches
2348    }
2349
2350    pub fn transparent_multi_draw_excluding(
2351        &self,
2352        excluded_instance: Option<InstanceHandle>,
2353    ) -> (Vec<u32>, Vec<DrawBatch>) {
2354        let order =
2355            self.filtered_phase_order(&self.transparent_multi_draw_order, excluded_instance);
2356        let mut batches = Vec::new();
2357        Self::build_draw_batches_for_order(&self.instances, &order, &mut batches);
2358        (order, batches)
2359    }
2360
2361    /// Call once per frame before rendering. Cheap if nothing changed.
2362    ///
2363    /// Returns `true` if the cached draw order/batches were rebuilt this call.
2364    pub fn prepare_draw_cache(&mut self) -> bool {
2365        if !self.dirty_draw_cache {
2366            return false;
2367        }
2368
2369        self.background_order.clear();
2370        self.background_occluded_lit_order.clear();
2371        self.background_occluded_lit_emissive_order.clear();
2372        self.background_occluded_lit_emissive_batches.clear();
2373        self.draw_order.clear();
2374        self.emissive_draw_order.clear();
2375        self.cutout_order.clear();
2376        self.emissive_cutout_order.clear();
2377        self.transparent_single_draw_order.clear();
2378        self.overlay_order.clear();
2379        self.stencil_clip_order.clear();
2380        // Opaque pass: exclude anything that is transparent.
2381        for i in 0..self.instances.len() {
2382            let inst = &self.instances[i];
2383            if inst.is_stencil_clip {
2384                self.stencil_clip_order.push(i as u32);
2385            }
2386            if inst.overlay {
2387                self.overlay_order.push(i as u32);
2388            } else if inst.background {
2389                if inst.background_occluded_lit {
2390                    self.background_occluded_lit_order.push(i as u32);
2391                } else {
2392                    self.background_order.push(i as u32);
2393                }
2394            } else if inst.transparent_cutout {
2395                self.cutout_order.push(i as u32);
2396            } else if !Self::is_transparent(inst) {
2397                self.draw_order.push(i as u32);
2398            } else if !inst.multiple_layers {
2399                self.transparent_single_draw_order.push(i as u32);
2400            }
2401        }
2402        self.stencil_clip_order
2403            .sort_by_key(|&i| self.instances[i as usize].stencil_ref);
2404
2405        // Background pass: batch aggressively (order does not depend on view).
2406        // NOTE: Background instances are excluded from the normal opaque/transparent lists.
2407        self.background_order.sort_by_key(|&i| {
2408            let inst = self.instances[i as usize];
2409            let r = inst.renderable;
2410            let tex = inst.texture.map(|t| t.0).unwrap_or(u32::MAX);
2411            (
2412                r.material.0,
2413                r.mesh.0,
2414                tex,
2415                inst.texture_filtering as u8,
2416                sanitize_quant_steps(inst.quant_steps).to_bits(),
2417            )
2418        });
2419        let instances = &self.instances;
2420        let background_draw_order = &self.background_order;
2421        Self::build_draw_batches_for_order(
2422            instances,
2423            background_draw_order,
2424            &mut self.background_batches,
2425        );
2426
2427        // Background (occluded+lit) pass: same batching strategy.
2428        self.background_occluded_lit_order.sort_by_key(|&i| {
2429            let inst = self.instances[i as usize];
2430            let r = inst.renderable;
2431            let tex = inst.texture.map(|t| t.0).unwrap_or(u32::MAX);
2432            (
2433                r.material.0,
2434                r.mesh.0,
2435                tex,
2436                inst.texture_filtering as u8,
2437                sanitize_quant_steps(inst.quant_steps).to_bits(),
2438            )
2439        });
2440        let background_occluded_lit_draw_order = &self.background_occluded_lit_order;
2441        Self::build_draw_batches_for_order(
2442            instances,
2443            background_occluded_lit_draw_order,
2444            &mut self.background_occluded_lit_batches,
2445        );
2446
2447        if self
2448            .background_occluded_lit_order
2449            .iter()
2450            .any(|&i| Self::is_emissive_material(self.instances[i as usize].renderable.material))
2451        {
2452            self.background_occluded_lit_emissive_order.extend(
2453                self.background_occluded_lit_order
2454                    .iter()
2455                    .copied()
2456                    .filter(|&i| {
2457                        Self::is_emissive_material(self.instances[i as usize].renderable.material)
2458                    }),
2459            );
2460            Self::build_draw_batches_for_order(
2461                instances,
2462                &self.background_occluded_lit_emissive_order,
2463                &mut self.background_occluded_lit_emissive_batches,
2464            );
2465        }
2466
2467        // Sort by (stencil_ref, material, tex, mesh, filtering) so stencil-clipped
2468        // instances group with their clip region. tex before mesh matches overlay convention
2469        // (untextured quads draw before textured glyphs at same depth).
2470        self.draw_order.sort_by_key(|&i| {
2471            let inst = self.instances[i as usize];
2472            let r = inst.renderable;
2473            let tex = inst.texture.map_or(0, |t| t.0.wrapping_add(1));
2474            (
2475                inst.stencil_ref,
2476                r.material.0,
2477                tex,
2478                r.mesh.0,
2479                inst.texture_filtering as u8,
2480                sanitize_quant_steps(inst.quant_steps).to_bits(),
2481            )
2482        });
2483
2484        let draw_order = &self.draw_order;
2485        Self::build_draw_batches_for_order(instances, draw_order, &mut self.draw_batches);
2486
2487        // Build the DFS render stream for the opaque phase.
2488        Self::build_phase_render_stream(
2489            instances,
2490            &self.draw_order,
2491            &mut self.opaque_stream,
2492            &mut self.opaque_stream_instances,
2493        );
2494
2495        self.emissive_draw_order
2496            .extend(self.draw_order.iter().copied().filter(|&i| {
2497                Self::is_emissive_material(self.instances[i as usize].renderable.material)
2498            }));
2499        let emissive_draw_order = &self.emissive_draw_order;
2500        Self::build_draw_batches_for_order(
2501            instances,
2502            emissive_draw_order,
2503            &mut self.emissive_draw_batches,
2504        );
2505
2506        // Cutout pass: batch aggressively (order does not depend on view).
2507        self.cutout_order.sort_by_key(|&i| {
2508            let inst = self.instances[i as usize];
2509            let r = inst.renderable;
2510            let tex = inst.texture.map(|t| t.0).unwrap_or(u32::MAX);
2511            (
2512                inst.stencil_ref,
2513                r.material.0,
2514                r.mesh.0,
2515                tex,
2516                inst.texture_filtering as u8,
2517                sanitize_quant_steps(inst.quant_steps).to_bits(),
2518            )
2519        });
2520        let cutout_order = &self.cutout_order;
2521        Self::build_draw_batches_for_order(instances, cutout_order, &mut self.cutout_batches);
2522
2523        Self::build_phase_render_stream(
2524            instances,
2525            &self.cutout_order,
2526            &mut self.cutout_stream,
2527            &mut self.cutout_stream_instances,
2528        );
2529
2530        self.emissive_cutout_order
2531            .extend(self.cutout_order.iter().copied().filter(|&i| {
2532                Self::is_emissive_material(self.instances[i as usize].renderable.material)
2533            }));
2534        let emissive_cutout_order = &self.emissive_cutout_order;
2535        Self::build_draw_batches_for_order(
2536            instances,
2537            emissive_cutout_order,
2538            &mut self.emissive_cutout_batches,
2539        );
2540
2541        // Single-layer transparent pass: batch aggressively (order does not depend on view).
2542        self.transparent_single_draw_order.sort_by_key(|&i| {
2543            let inst = self.instances[i as usize];
2544            let r = inst.renderable;
2545            let tex = inst.texture.map(|t| t.0).unwrap_or(u32::MAX);
2546            (
2547                inst.stencil_ref,
2548                r.material.0,
2549                r.mesh.0,
2550                tex,
2551                inst.texture_filtering as u8,
2552                sanitize_quant_steps(inst.quant_steps).to_bits(),
2553            )
2554        });
2555        let transparent_single_draw_order = &self.transparent_single_draw_order;
2556        Self::build_draw_batches_for_order(
2557            instances,
2558            transparent_single_draw_order,
2559            &mut self.transparent_single_draw_batches,
2560        );
2561
2562        Self::build_phase_render_stream(
2563            instances,
2564            &self.transparent_single_draw_order,
2565            &mut self.transparent_single_stream,
2566            &mut self.transparent_single_stream_instances,
2567        );
2568
2569        // Overlay pass: no-texture instances (e.g. text backgrounds) must draw before
2570        // textured glyphs so that depth-write from glyph edge pixels does not block
2571        // the background quad.  tex=0 for untextured, tex=handle+1 for textured, so
2572        // placing tex BEFORE mesh ensures untextured always sorts first regardless of
2573        // which MeshHandle was allocated earlier.
2574        self.overlay_order.sort_by_key(|&i| {
2575            let inst = self.instances[i as usize];
2576            let r = inst.renderable;
2577            let tex = inst.texture.map_or(0, |t| t.0.wrapping_add(1));
2578            (
2579                inst.stencil_ref,
2580                r.material.0,
2581                tex,
2582                r.mesh.0,
2583                inst.texture_filtering as u8,
2584                sanitize_quant_steps(inst.quant_steps).to_bits(),
2585            )
2586        });
2587        let overlay_order = &self.overlay_order;
2588        Self::build_draw_batches_for_order(instances, overlay_order, &mut self.overlay_batches);
2589
2590        // Build the DFS render stream for the overlay phase.
2591        Self::build_phase_render_stream(
2592            instances,
2593            &self.overlay_order,
2594            &mut self.overlay_stream,
2595            &mut self.overlay_stream_instances,
2596        );
2597
2598        self.dirty_draw_cache = false;
2599        true
2600    }
2601
2602    pub fn prepare_transparent_multi_draw_cache_for_eye(
2603        &mut self,
2604        target: CameraTarget,
2605        eye: usize,
2606    ) {
2607        let view = self.camera_view_for_eye(target, eye);
2608        self.prepare_transparent_multi_draw_cache_for_view(view);
2609    }
2610
2611    /// Rebuild multi-layer transparent draw order/batches for a specific view matrix.
2612    pub fn prepare_transparent_multi_draw_cache_for_view(&mut self, view: [[f32; 4]; 4]) {
2613        self.transparent_multi_draw_order.clear();
2614
2615        for i in 0..self.instances.len() {
2616            let inst = &self.instances[i];
2617            if inst.overlay {
2618                continue;
2619            }
2620            if inst.background {
2621                continue;
2622            }
2623            if inst.transparent_cutout {
2624                continue;
2625            }
2626            if inst.multiple_layers && Self::is_transparent(inst) {
2627                self.transparent_multi_draw_order.push(i as u32);
2628            }
2629        }
2630
2631        if self.transparent_multi_draw_order.is_empty() {
2632            self.transparent_multi_draw_batches.clear();
2633            return;
2634        }
2635
2636        // Back-to-front for blending.
2637        self.transparent_multi_draw_order.sort_by(|&a, &b| {
2638            let ia = self.instances[a as usize];
2639            let ib = self.instances[b as usize];
2640            let za = Self::view_space_z(view, ia.transform.model);
2641            let zb = Self::view_space_z(view, ib.transform.model);
2642            za.partial_cmp(&zb)
2643                .unwrap_or(std::cmp::Ordering::Equal)
2644                .then_with(|| a.cmp(&b))
2645        });
2646
2647        let instances = &self.instances;
2648        let transparent_draw_order = &self.transparent_multi_draw_order;
2649        Self::build_draw_batches_for_order(
2650            instances,
2651            transparent_draw_order,
2652            &mut self.transparent_multi_draw_batches,
2653        );
2654    }
2655
2656    pub fn register(
2657        &mut self,
2658        cid: ComponentId,
2659        renderable: GpuRenderable,
2660        transform: Transform,
2661        color: [f32; 4],
2662        opacity: f32,
2663        multiple_layers: bool,
2664        transparent_cutout: bool,
2665        background: bool,
2666        background_occluded_lit: bool,
2667        overlay: bool,
2668        emissive: f32,
2669        texture: Option<crate::engine::graphics::TextureHandle>,
2670        quant_steps: f32,
2671    ) -> InstanceHandle {
2672        let handle = InstanceHandle(self.next_handle);
2673        self.next_handle = self.next_handle.wrapping_add(1);
2674
2675        let idx = self.instances.len();
2676        self.instances.push(VisualInstance {
2677            renderable,
2678            transform,
2679            color,
2680            opacity: if opacity.is_finite() {
2681                opacity.clamp(0.0, 1.0)
2682            } else {
2683                1.0
2684            },
2685            multiple_layers,
2686            transparent_cutout,
2687            background,
2688            background_occluded_lit,
2689            overlay,
2690            emissive: if emissive.is_finite() {
2691                emissive.max(0.0)
2692            } else {
2693                0.0
2694            },
2695            texture,
2696            texture_filtering: TextureFiltering::default(),
2697            quant_steps: sanitize_quant_steps(quant_steps),
2698
2699            bones_base: 0,
2700            bones_count: 0,
2701
2702            stencil_ref: 0,
2703            is_stencil_clip: false,
2704        });
2705        self.handle_to_index.insert(handle, idx);
2706        self.component_to_handle.insert(cid, handle);
2707
2708        self.dirty_draw_cache = true;
2709        self.dirty_instance_data = true;
2710        handle
2711    }
2712
2713    pub fn update_quant_steps(&mut self, handle: InstanceHandle, quant_steps: f32) -> bool {
2714        if let Some(&idx) = self.handle_to_index.get(&handle) {
2715            let q = sanitize_quant_steps(quant_steps);
2716            if self.instances[idx].quant_steps.to_bits() == q.to_bits() {
2717                return true;
2718            }
2719            self.instances[idx].quant_steps = q;
2720            // Quantization affects material UBO selection => batching.
2721            self.dirty_draw_cache = true;
2722            true
2723        } else {
2724            false
2725        }
2726    }
2727
2728    pub fn remove(&mut self, handle: InstanceHandle) -> bool {
2729        if let Some(idx) = self.handle_to_index.remove(&handle) {
2730            // Free any skin allocation before removing the instance.
2731            let old_base = self.instances[idx].bones_base;
2732            let old_count = self.instances[idx].bones_count;
2733            if old_count != 0 {
2734                self.bones_free_range(old_base, old_count);
2735            }
2736
2737            self.instances.swap_remove(idx);
2738
2739            if idx < self.instances.len() {
2740                // NOTE: This is O(n). Consider storing index->handle too if it becomes hot.
2741                if let Some((moved_handle, _)) = self
2742                    .handle_to_index
2743                    .iter()
2744                    .find(|(_, i)| **i == self.instances.len())
2745                {
2746                    self.handle_to_index.insert(*moved_handle, idx);
2747                }
2748            }
2749
2750            self.component_to_handle.retain(|_, &mut h| h != handle);
2751
2752            self.dirty_draw_cache = true;
2753            self.dirty_instance_data = true;
2754            true
2755        } else {
2756            false
2757        }
2758    }
2759
2760    pub fn update_transform(&mut self, handle: InstanceHandle, transform: Transform) -> bool {
2761        if let Some(&idx) = self.handle_to_index.get(&handle) {
2762            self.instances[idx].transform = transform;
2763            self.dirty_instance_data = true;
2764            // transform-only doesn’t affect batching by (material, mesh)
2765            true
2766        } else {
2767            false
2768        }
2769    }
2770
2771    pub fn update_model(&mut self, handle: InstanceHandle, model: TransformMatrix) -> bool {
2772        if let Some(&idx) = self.handle_to_index.get(&handle) {
2773            self.instances[idx].transform.model = model;
2774            self.instances[idx].transform.matrix_world = model;
2775            self.dirty_instance_data = true;
2776            // model-only doesn’t affect batching by (material, mesh)
2777            true
2778        } else {
2779            false
2780        }
2781    }
2782
2783    pub fn update_color(&mut self, handle: InstanceHandle, color: [f32; 4]) -> bool {
2784        if let Some(&idx) = self.handle_to_index.get(&handle) {
2785            self.instances[idx].color = color;
2786            self.dirty_instance_data = true;
2787            // Color alpha can change transparent/opaque classification.
2788            self.dirty_draw_cache = true;
2789            true
2790        } else {
2791            false
2792        }
2793    }
2794
2795    pub fn update_opacity(&mut self, handle: InstanceHandle, opacity: f32) -> bool {
2796        self.update_opacity_state(handle, opacity, None)
2797    }
2798
2799    pub fn update_opacity_state(
2800        &mut self,
2801        handle: InstanceHandle,
2802        opacity: f32,
2803        multiple_layers: impl Into<Option<bool>>,
2804    ) -> bool {
2805        if let Some(&idx) = self.handle_to_index.get(&handle) {
2806            let o = if opacity.is_finite() {
2807                opacity.clamp(0.0, 1.0)
2808            } else {
2809                1.0
2810            };
2811            let mut changed = false;
2812
2813            if (self.instances[idx].opacity - o).abs() >= f32::EPSILON {
2814                self.instances[idx].opacity = o;
2815                changed = true;
2816            }
2817
2818            if let Some(ml) = multiple_layers.into() {
2819                if self.instances[idx].multiple_layers != ml {
2820                    self.instances[idx].multiple_layers = ml;
2821                    changed = true;
2822                }
2823            }
2824
2825            if !changed {
2826                return true;
2827            }
2828            self.dirty_instance_data = true;
2829            // Opacity changes can change transparent/opaque classification.
2830            self.dirty_draw_cache = true;
2831            true
2832        } else {
2833            false
2834        }
2835    }
2836
2837    pub fn update_transparent_cutout(&mut self, handle: InstanceHandle, enabled: bool) -> bool {
2838        if let Some(&idx) = self.handle_to_index.get(&handle) {
2839            if self.instances[idx].transparent_cutout == enabled {
2840                return true;
2841            }
2842            self.instances[idx].transparent_cutout = enabled;
2843            // Cutout changes affect pass classification.
2844            self.dirty_draw_cache = true;
2845            true
2846        } else {
2847            false
2848        }
2849    }
2850
2851    pub fn update_emissive(&mut self, handle: InstanceHandle, emissive: f32) -> bool {
2852        if let Some(&idx) = self.handle_to_index.get(&handle) {
2853            self.instances[idx].emissive = if emissive.is_finite() {
2854                emissive.max(0.0)
2855            } else {
2856                0.0
2857            };
2858            self.dirty_instance_data = true;
2859            true
2860        } else {
2861            false
2862        }
2863    }
2864
2865    pub fn update_material(
2866        &mut self,
2867        handle: InstanceHandle,
2868        material: crate::engine::graphics::MaterialHandle,
2869    ) -> bool {
2870        if let Some(&idx) = self.handle_to_index.get(&handle) {
2871            if self.instances[idx].renderable.material == material {
2872                return true;
2873            }
2874            self.instances[idx].renderable.material = material;
2875            self.dirty_draw_cache = true;
2876            true
2877        } else {
2878            false
2879        }
2880    }
2881
2882    pub fn post_processing(&self) -> &PostProcessingConfig {
2883        &self.post_processing
2884    }
2885
2886    pub fn post_processing_mut(&mut self) -> &mut PostProcessingConfig {
2887        &mut self.post_processing
2888    }
2889
2890    pub fn set_post_processing(&mut self, config: PostProcessingConfig) {
2891        self.post_processing = config;
2892    }
2893
2894    pub fn runtime_texture_handle(
2895        &self,
2896        key: &str,
2897    ) -> Option<crate::engine::graphics::TextureHandle> {
2898        self.runtime_texture_handles.get(key).copied()
2899    }
2900
2901    pub fn stencil_clip_debug_requested(&self) -> bool {
2902        self.stencil_clip_debug_requested
2903    }
2904
2905    pub fn set_stencil_clip_debug_requested(&mut self, requested: bool) {
2906        self.stencil_clip_debug_requested = requested;
2907    }
2908
2909    pub fn set_runtime_texture_handle(
2910        &mut self,
2911        key: impl Into<String>,
2912        handle: crate::engine::graphics::TextureHandle,
2913    ) {
2914        self.runtime_texture_handles.insert(key.into(), handle);
2915    }
2916
2917    pub fn update_texture(
2918        &mut self,
2919        handle: InstanceHandle,
2920        texture: Option<crate::engine::graphics::TextureHandle>,
2921    ) -> bool {
2922        if let Some(&idx) = self.handle_to_index.get(&handle) {
2923            self.instances[idx].texture = texture;
2924            // Texture affects batching (descriptor binding), but not instance vertex data.
2925            self.dirty_draw_cache = true;
2926            true
2927        } else {
2928            false
2929        }
2930    }
2931
2932    pub fn update_texture_filtering(
2933        &mut self,
2934        handle: InstanceHandle,
2935        filtering: TextureFiltering,
2936    ) -> bool {
2937        if let Some(&idx) = self.handle_to_index.get(&handle) {
2938            self.instances[idx].texture_filtering = filtering;
2939            // Filtering affects batching (sampler binding), but not instance vertex data.
2940            self.dirty_draw_cache = true;
2941            true
2942        } else {
2943            false
2944        }
2945    }
2946
2947    pub fn update(
2948        &mut self,
2949        handle: InstanceHandle,
2950        renderable: GpuRenderable,
2951        transform: Transform,
2952    ) -> bool {
2953        if let Some(&idx) = self.handle_to_index.get(&handle) {
2954            // Preserve per-instance color when updating renderable/transform.
2955            let color = self.instances[idx].color;
2956            let opacity = self.instances[idx].opacity;
2957            let multiple_layers = self.instances[idx].multiple_layers;
2958            let transparent_cutout = self.instances[idx].transparent_cutout;
2959            let background = self.instances[idx].background;
2960            let background_occluded_lit = self.instances[idx].background_occluded_lit;
2961            let overlay = self.instances[idx].overlay;
2962            let emissive = self.instances[idx].emissive;
2963            let texture = self.instances[idx].texture;
2964            let texture_filtering = self.instances[idx].texture_filtering;
2965            let quant_steps = self.instances[idx].quant_steps;
2966            let bones_base = self.instances[idx].bones_base;
2967            let bones_count = self.instances[idx].bones_count;
2968            let stencil_ref = self.instances[idx].stencil_ref;
2969            let is_stencil_clip = self.instances[idx].is_stencil_clip;
2970            self.instances[idx] = VisualInstance {
2971                renderable,
2972                transform,
2973                color,
2974                opacity,
2975                multiple_layers,
2976                transparent_cutout,
2977                background,
2978                background_occluded_lit,
2979                overlay,
2980                emissive,
2981                texture,
2982                texture_filtering,
2983                quant_steps,
2984                bones_base,
2985                bones_count,
2986                stencil_ref,
2987                is_stencil_clip,
2988            };
2989            self.dirty_draw_cache = true; // renderable changes likely affect sort/batch
2990            self.dirty_instance_data = true;
2991            true
2992        } else {
2993            false
2994        }
2995    }
2996}