Skip to main content

mittens_engine/engine/graphics/
vulkano_renderer.rs

1use crate::engine::graphics::MeshUploader;
2use crate::engine::graphics::MsaaMode;
3use crate::engine::graphics::TextureUploader;
4use crate::engine::graphics::mesh::CpuMesh;
5use crate::engine::graphics::primitives::MeshHandle;
6use crate::engine::graphics::primitives::TextureHandle;
7use crate::engine::graphics::visual_world::VisualWorld;
8use std::sync::Arc;
9use winit::window::Window;
10
11mod vulkano_backend {
12    use std::collections::HashMap;
13    use std::mem::size_of;
14    use std::sync::Arc;
15    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
16
17    use crate::engine::ecs::ComponentId;
18    use crate::engine::ecs::system::render_to_texture_system::INTERNAL_RENDERER_STENCIL_CLIP_DEBUG_SELECTOR;
19    use crate::engine::graphics::MsaaMode;
20    use crate::engine::graphics::mesh::{CpuMesh, CpuVertex};
21    use crate::engine::graphics::pipeline_descriptor_set_layouts::PipelineDescriptorSetLayouts;
22    use crate::engine::graphics::post_processing::{
23        PostProcessFrameTargets, PostProcessingConfig, PostProcessingRenderer,
24    };
25    use crate::engine::graphics::primitives::MeshHandle;
26    use crate::engine::graphics::primitives::TextureHandle;
27    use crate::engine::graphics::visual_world::{TextureFiltering, VisualWorld};
28    use crate::engine::graphics::vulkano_swapchain::VulkanoSwapchainState;
29    use crate::engine::graphics::vulkano_texture_upload;
30    use vulkano::buffer::{Buffer, BufferContents, BufferCreateInfo, BufferUsage, Subbuffer};
31    use vulkano::command_buffer::{
32        AutoCommandBufferBuilder, CommandBufferUsage, CopyBufferInfo, CopyImageInfo,
33        PrimaryCommandBufferAbstract, allocator::StandardCommandBufferAllocator,
34    };
35    use vulkano::command_buffer::{
36        ClearAttachment, ClearRect, RenderingAttachmentInfo, RenderingAttachmentResolveInfo,
37        RenderingInfo,
38    };
39    use vulkano::descriptor_set::allocator::StandardDescriptorSetAllocator;
40    use vulkano::descriptor_set::{DescriptorSet, WriteDescriptorSet};
41    use vulkano::format::ClearValue;
42    use vulkano::image::view::{ImageView, ImageViewCreateInfo};
43    use vulkano::image::{
44        Image, ImageAspects, ImageCreateInfo, ImageSubresourceRange, ImageType, ImageUsage,
45        SampleCount, SampleCounts,
46    };
47    use vulkano::memory::allocator::{
48        AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator,
49    };
50    use vulkano::pipeline::graphics::color_blend::{
51        AttachmentBlend, BlendFactor, BlendOp, ColorBlendAttachmentState, ColorBlendState,
52        ColorComponents,
53    };
54    use vulkano::pipeline::graphics::depth_stencil::{
55        CompareOp, DepthState, DepthStencilState, StencilOp, StencilOpState, StencilOps,
56        StencilState,
57    };
58    use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
59    use vulkano::pipeline::graphics::multisample::MultisampleState;
60    use vulkano::pipeline::graphics::rasterization::RasterizationState;
61    use vulkano::pipeline::graphics::subpass::PipelineRenderingCreateInfo;
62    use vulkano::pipeline::graphics::subpass::PipelineSubpassType;
63    use vulkano::pipeline::graphics::vertex_input::{
64        VertexInputAttributeDescription, VertexInputBindingDescription, VertexInputRate,
65        VertexInputState,
66    };
67    use vulkano::pipeline::graphics::viewport::{Scissor, Viewport, ViewportState};
68    use vulkano::pipeline::layout::{PipelineLayout, PipelineLayoutCreateInfo};
69
70    use vulkano::DeviceSize;
71    use vulkano::Version;
72    use vulkano::VulkanObject;
73    use vulkano::format::Format;
74    use vulkano::image::sampler::{
75        Filter, Sampler, SamplerAddressMode, SamplerCreateInfo, SamplerMipmapMode,
76    };
77    use vulkano::pipeline::{
78        DynamicState, GraphicsPipeline, Pipeline, PipelineBindPoint, PipelineShaderStageCreateInfo,
79    };
80    use vulkano::render_pass::{AttachmentLoadOp, AttachmentStoreOp};
81    use vulkano::swapchain::{self, SwapchainPresentInfo};
82    use vulkano::sync::{self, GpuFuture};
83    use vulkano::{Validated, VulkanError};
84    use vulkano_util::context::{VulkanoConfig, VulkanoContext};
85    use winit::window::Window;
86
87    fn env_flag(name: &str) -> bool {
88        std::env::var(name)
89            .ok()
90            .map(|s| {
91                let s = s.trim().to_ascii_lowercase();
92                s == "1" || s == "true" || s == "on" || s == "yes"
93            })
94            .unwrap_or(false)
95    }
96
97    fn env_usize(name: &str) -> Option<usize> {
98        std::env::var(name)
99            .ok()
100            .and_then(|s| s.trim().parse::<usize>().ok())
101    }
102
103    use vulkano::device::DeviceExtensions;
104
105    // Split out command-buffer recording helpers to keep this file manageable.
106    //
107    // `vulkano_backend` is an inline module, so `#[path = "..."]` would be resolved relative to
108    // a *virtual* module directory (`.../vulkano_renderer/vulkano_backend/`) that doesn't exist
109    // on disk. Using `include!` lets us keep the helpers in a normal file next to the renderer.
110    mod vulkano_cbb {
111        include!(concat!(
112            env!("CARGO_MANIFEST_DIR"),
113            "/src/engine/graphics/vulkano_cbb.rs"
114        ));
115    }
116
117    mod toon_mesh_vs {
118        vulkano_shaders::shader! {
119            ty: "vertex",
120            path: "assets/shaders/toon-mesh.vert",
121        }
122    }
123
124    mod mirror_mesh_vs {
125        vulkano_shaders::shader! {
126            ty: "vertex",
127            path: "assets/shaders/mirror-mesh.vert",
128        }
129    }
130
131    mod toon_mesh_fs {
132        vulkano_shaders::shader! {
133            ty: "fragment",
134            path: "assets/shaders/toon-mesh.frag",
135        }
136    }
137
138    mod emissive_toon_mesh_fs {
139        vulkano_shaders::shader! {
140            ty: "fragment",
141            path: "assets/shaders/emissive-toon-mesh.frag",
142        }
143    }
144
145    mod mirror_mesh_fs {
146        vulkano_shaders::shader! {
147            ty: "fragment",
148            path: "assets/shaders/mirror-mesh.frag",
149        }
150    }
151
152    mod skinned_toon_mesh_vs {
153        vulkano_shaders::shader! {
154            ty: "vertex",
155            path: "assets/shaders/skinned-toon-mesh.vert",
156        }
157    }
158
159    mod grid_mesh_vs {
160        vulkano_shaders::shader! {
161            ty: "vertex",
162            path: "assets/shaders/grid.vert",
163        }
164    }
165
166    mod grid_square_mesh_fs {
167        vulkano_shaders::shader! {
168            ty: "fragment",
169            path: "assets/shaders/grid-square.frag",
170        }
171    }
172
173    #[derive(BufferContents, Clone, Copy, Debug, Default)]
174    #[repr(C, align(16))]
175    pub struct CameraUBO {
176        pub view: [[f32; 4]; 4],
177        pub proj: [[f32; 4]; 4],
178        // std140 mat3 = 3x vec4 columns.
179        pub camera2d: [[f32; 4]; 3],
180        // Swapchain size in pixels (width, height). Used for aspect correction in 2D.
181        pub viewport: [f32; 2],
182        pub _pad0: [f32; 2],
183
184        // Linear RGB ambient light in 0..1.
185        pub ambient_light: [f32; 3],
186        pub _pad1: f32,
187    }
188
189    #[derive(BufferContents, Clone, Copy, Debug, Default)]
190    #[repr(C, align(16))]
191    struct MaterialUBO {
192        base_color: [f32; 4],
193        quant_steps: f32,
194        emissive: u32,
195        _pad0: u32,
196        _pad1: u32,
197    }
198
199    #[derive(BufferContents, Clone, Copy, Debug, Default)]
200    #[repr(C, align(16))]
201    struct GpuMat4 {
202        cols: [[f32; 4]; 4],
203    }
204
205    #[derive(BufferContents, Clone, Copy, Debug, Default)]
206    #[repr(C, align(16))]
207    struct DummyPerInstanceLightingSSBO {
208        _pad0: [u32; 4],
209    }
210
211    #[derive(
212        BufferContents,
213        vulkano::pipeline::graphics::vertex_input::Vertex,
214        Clone,
215        Copy,
216        Debug,
217        Default,
218    )]
219    #[repr(C)]
220    pub struct InstanceData {
221        #[format(R32G32B32A32_SFLOAT)]
222        pub i_model_c0: [f32; 4],
223        #[format(R32G32B32A32_SFLOAT)]
224        pub i_model_c1: [f32; 4],
225        #[format(R32G32B32A32_SFLOAT)]
226        pub i_model_c2: [f32; 4],
227        #[format(R32G32B32A32_SFLOAT)]
228        pub i_model_c3: [f32; 4],
229
230        #[format(R32G32B32A32_SFLOAT)]
231        pub i_color: [f32; 4],
232        #[format(R32_SFLOAT)]
233        pub i_emissive: f32,
234
235        #[format(R32_SFLOAT)]
236        pub i_opacity: f32,
237
238        // For skinned meshes: base index/count into the shared bones palette SSBO.
239        // For non-skinned instances these are 0.
240        #[format(R32_UINT)]
241        pub i_bones_base: u32,
242        #[format(R32_UINT)]
243        pub i_bones_count: u32,
244    }
245
246    /// GPU-uploadable per-vertex skinning attributes (separate vertex buffer).
247    #[derive(BufferContents, Debug, Clone, Copy, Default)]
248    #[repr(C)]
249    pub struct GpuSkinVertex {
250        pub joints0: [u16; 4],
251        pub weights0: [f32; 4],
252    }
253
254    #[derive(Debug, Clone)]
255    pub enum RenderViewKind {
256        Window,
257        XrEye {
258            eye: usize,
259        },
260        Mirror {
261            mirror_component: ComponentId,
262            family: crate::engine::graphics::visual_world::MirrorViewerFamily,
263            view_index: usize,
264            plane_origin: [f32; 3],
265            plane_normal: [f32; 3],
266            excluded_instance: Option<crate::engine::graphics::primitives::InstanceHandle>,
267        },
268    }
269
270    #[derive(Debug, Clone)]
271    pub struct RenderView {
272        pub view: [[f32; 4]; 4],
273        pub proj: [[f32; 4]; 4],
274        pub viewport: [f32; 2],
275        pub kind: RenderViewKind,
276    }
277
278    pub struct VulkanoGpuMesh {
279        #[allow(dead_code)]
280        pub vertices: Subbuffer<[CpuVertex]>,
281        #[allow(dead_code)]
282        pub skin_vertices: Option<Subbuffer<[GpuSkinVertex]>>,
283        #[allow(dead_code)]
284        pub indices: Subbuffer<[u32]>,
285        #[allow(dead_code)]
286        pub index_count: u32,
287    }
288
289    pub struct VulkanoGpuTexture {
290        pub view: Arc<ImageView>,
291        pub extent: [u32; 2],
292        pub format: Format,
293    }
294
295    pub struct VulkanoState {
296        #[allow(dead_code)]
297        pub context: VulkanoContext,
298        #[allow(dead_code)]
299        pub window: Arc<Window>,
300
301        #[allow(dead_code)]
302        pub swapchain_state: VulkanoSwapchainState,
303
304        #[allow(dead_code)]
305        pub command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
306
307        #[allow(dead_code)]
308        pub descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
309
310        pub post_processing_renderer: PostProcessingRenderer,
311
312        #[allow(dead_code)]
313        pub set_layouts: PipelineDescriptorSetLayouts,
314
315        #[allow(dead_code)]
316        pub meshes: HashMap<MeshHandle, VulkanoGpuMesh>,
317
318        pub textures: HashMap<TextureHandle, VulkanoGpuTexture>,
319        pub sampler_linear: Arc<Sampler>,
320        pub sampler_nearest: Arc<Sampler>,
321        pub sampler_nearest_mag: Arc<Sampler>,
322        pub default_white_texture: TextureHandle,
323
324        pub pipeline_toon_mesh: Arc<GraphicsPipeline>,
325        pub pipeline_toon_mesh_transparent: Arc<GraphicsPipeline>,
326        pub pipeline_toon_mesh_cutout: Arc<GraphicsPipeline>,
327        pub pipeline_toon_mesh_transparent_clipped: Arc<GraphicsPipeline>,
328        pub pipeline_toon_mesh_cutout_clipped: Arc<GraphicsPipeline>,
329
330        pub pipeline_mirror_mesh: Arc<GraphicsPipeline>,
331        pub pipeline_mirror_mesh_transparent: Arc<GraphicsPipeline>,
332        pub pipeline_mirror_mesh_cutout: Arc<GraphicsPipeline>,
333        pub pipeline_mirror_mesh_clipped: Arc<GraphicsPipeline>,
334        pub pipeline_mirror_mesh_transparent_clipped: Arc<GraphicsPipeline>,
335        pub pipeline_mirror_mesh_cutout_clipped: Arc<GraphicsPipeline>,
336
337        pub pipeline_grid_mesh: Arc<GraphicsPipeline>,
338        pub pipeline_grid_mesh_transparent: Arc<GraphicsPipeline>,
339        pub pipeline_grid_mesh_transparent_clipped: Arc<GraphicsPipeline>,
340
341        pub pipeline_emissive_toon_mesh: Arc<GraphicsPipeline>,
342        pub pipeline_emissive_toon_mesh_transparent: Arc<GraphicsPipeline>,
343        pub pipeline_emissive_toon_mesh_cutout: Arc<GraphicsPipeline>,
344        pub pipeline_emissive_toon_mesh_transparent_clipped: Arc<GraphicsPipeline>,
345        pub pipeline_emissive_toon_mesh_cutout_clipped: Arc<GraphicsPipeline>,
346        pub pipeline_emissive_prepass_toon_mesh: Arc<GraphicsPipeline>,
347        pub pipeline_emissive_prepass_toon_mesh_cutout: Arc<GraphicsPipeline>,
348        pub pipeline_emissive_prepass_depth_write_toon_mesh: Arc<GraphicsPipeline>,
349
350        pub pipeline_skinned_toon_mesh: Arc<GraphicsPipeline>,
351        pub pipeline_skinned_toon_mesh_transparent: Arc<GraphicsPipeline>,
352        pub pipeline_skinned_toon_mesh_cutout: Arc<GraphicsPipeline>,
353
354        pub pipeline_skinned_emissive_toon_mesh: Arc<GraphicsPipeline>,
355        pub pipeline_skinned_emissive_toon_mesh_transparent: Arc<GraphicsPipeline>,
356        pub pipeline_skinned_emissive_toon_mesh_cutout: Arc<GraphicsPipeline>,
357        pub pipeline_skinned_emissive_prepass_toon_mesh: Arc<GraphicsPipeline>,
358        pub pipeline_skinned_emissive_prepass_toon_mesh_cutout: Arc<GraphicsPipeline>,
359        pub pipeline_skinned_emissive_prepass_depth_write_toon_mesh: Arc<GraphicsPipeline>,
360
361        /// Writes stencil INCR (enter clip region). Color write off, depth test off.
362        pub pipeline_stencil_incr: Arc<GraphicsPipeline>,
363        /// Writes stencil DECR (exit clip region). Color write off, depth test off.
364        pub pipeline_stencil_decr: Arc<GraphicsPipeline>,
365        /// Overlay draw gated by stencil EQUAL. For non-emissive materials.
366        pub pipeline_overlay_clipped: Arc<GraphicsPipeline>,
367        /// Overlay draw gated by stencil EQUAL. For emissive materials.
368        pub pipeline_emissive_overlay_clipped: Arc<GraphicsPipeline>,
369        /// Opaque draw gated by stencil EQUAL. Depth write ON. For non-emissive materials.
370        pub pipeline_opaque_clipped: Arc<GraphicsPipeline>,
371        /// Opaque draw gated by stencil EQUAL. Depth write ON. For emissive materials.
372        pub pipeline_emissive_opaque_clipped: Arc<GraphicsPipeline>,
373
374        pub msaa_samples: SampleCount,
375
376        // --- Per-frame CPU work reduction ---
377        cached_instance_buffer: Option<Subbuffer<[InstanceData]>>,
378        cached_instance_count: usize,
379
380        cached_background_instance_buffer: Option<Subbuffer<[InstanceData]>>,
381        cached_background_instance_count: usize,
382
383        cached_background_occluded_lit_instance_buffer: Option<Subbuffer<[InstanceData]>>,
384        cached_background_occluded_lit_instance_count: usize,
385
386        cached_cutout_instance_buffer: Option<Subbuffer<[InstanceData]>>,
387        cached_cutout_instance_count: usize,
388
389        cached_overlay_instance_buffer: Option<Subbuffer<[InstanceData]>>,
390        cached_overlay_instance_count: usize,
391        cached_material_sets: HashMap<
392            (
393                crate::engine::graphics::MaterialHandle,
394                TextureHandle,
395                TextureFiltering,
396                u32,
397            ),
398            Arc<DescriptorSet>,
399        >,
400        pending_runtime_texture_updates: HashMap<TextureHandle, VulkanoGpuTexture>,
401        window_runtime_debug_targets: Option<WindowRuntimeDebugTargets>,
402
403        // Cached bones palette SSBOs (set=2 binding=1).
404        //
405        // These are per-frame slots (swapchain image index + optional XR eye slots) to avoid
406        // writing a buffer while the GPU is still reading it from a previous frame.
407        cached_bones_buffers: Vec<Subbuffer<[GpuMat4]>>,
408        cached_bones_slot_valid: Vec<bool>,
409        cached_bones_capacity: usize,
410
411        xr_offscreen: Option<XrOffscreenTargets>,
412        mirror_offscreen: std::collections::HashMap<String, MirrorOffscreenTargets>,
413
414        pub window_resized: bool,
415        pub recreate_swapchain: bool,
416        pub images_in_flight: Vec<Option<Box<dyn GpuFuture>>>,
417    }
418
419    struct XrOffscreenTargets {
420        extent: [u32; 2],
421        color_format: Format,
422        color_images: Vec<Arc<vulkano::image::Image>>,
423        msaa_color_views: Vec<Arc<ImageView>>,
424        color_views: Vec<Arc<ImageView>>,
425        /// Combined depth+stencil views (same view used for both attachments).
426        depth_views: Vec<Arc<ImageView>>,
427    }
428
429    struct MirrorOffscreenTargets {
430        extent: [u32; 2],
431        color_format: Format,
432        color_images: Vec<Arc<vulkano::image::Image>>,
433        msaa_color_views: Vec<Arc<ImageView>>,
434        color_views: Vec<Arc<ImageView>>,
435        /// Combined depth+stencil views (same view used for both attachments).
436        depth_views: Vec<Arc<ImageView>>,
437    }
438
439    struct WindowRuntimeDebugTargets {
440        extent: [u32; 2],
441        color_format: Format,
442        msaa_color_views: Vec<Arc<ImageView>>,
443        color_views: Vec<Arc<ImageView>>,
444    }
445
446    #[derive(Clone)]
447    struct PostProcessInvocation {
448        final_output_view: Arc<ImageView>,
449        final_color_format: Format,
450        config: PostProcessingConfig,
451        targets: PostProcessFrameTargets,
452    }
453
454    const MAX_LIGHTS: usize = 64;
455
456    const LIGHT_TYPE_POINT: u32 = 1;
457    const LIGHT_TYPE_DIRECTIONAL: u32 = 2;
458
459    #[derive(BufferContents, Clone, Copy, Debug, Default)]
460    #[repr(C, align(16))]
461    struct GpuLight {
462        // xyz position (world), w intensity
463        pos_intensity: [f32; 4],
464        // rgb color, w distance
465        color_distance: [f32; 4],
466        // Light metadata (matches `uvec4 meta` on the shader side).
467        // meta.x = light_type (1=point, 2=directional)
468        meta: [u32; 4],
469    }
470
471    #[derive(BufferContents, Clone, Copy, Debug)]
472    #[repr(C, align(16))]
473    struct LightsSSBO {
474        count: u32,
475        _pad0: [u32; 3],
476        lights: [GpuLight; MAX_LIGHTS],
477    }
478
479    impl Default for LightsSSBO {
480        fn default() -> Self {
481            Self {
482                count: 0,
483                _pad0: [0, 0, 0],
484                lights: [GpuLight::default(); MAX_LIGHTS],
485            }
486        }
487    }
488
489    impl VulkanoState {
490        fn sampler_for(&self, filtering: TextureFiltering) -> &Arc<Sampler> {
491            match filtering {
492                TextureFiltering::Linear => &self.sampler_linear,
493                TextureFiltering::Nearest => &self.sampler_nearest,
494                TextureFiltering::NearestMagnification => &self.sampler_nearest_mag,
495            }
496        }
497
498        fn create_material_ubo(
499            material: crate::engine::graphics::MaterialHandle,
500            quant_steps: f32,
501        ) -> MaterialUBO {
502            let quant_steps = if quant_steps.is_finite() {
503                quant_steps.clamp(1.0, 64.0)
504            } else {
505                3.0
506            };
507
508            match material {
509                crate::engine::graphics::MaterialHandle::TOON_MESH => MaterialUBO {
510                    base_color: [1.0, 1.0, 1.0, 1.0],
511                    quant_steps,
512                    emissive: 0,
513                    _pad0: 0,
514                    _pad1: 0,
515                },
516                crate::engine::graphics::MaterialHandle::SKINNED_TOON_MESH => MaterialUBO {
517                    base_color: [1.0, 1.0, 1.0, 1.0],
518                    quant_steps,
519                    emissive: 0,
520                    _pad0: 0,
521                    _pad1: 0,
522                },
523                crate::engine::graphics::MaterialHandle::EMISSIVE_TOON_MESH => MaterialUBO {
524                    base_color: [1.0, 1.0, 1.0, 1.0],
525                    quant_steps,
526                    emissive: 0,
527                    _pad0: 0,
528                    _pad1: 0,
529                },
530                crate::engine::graphics::MaterialHandle::SKINNED_EMISSIVE_TOON_MESH => {
531                    MaterialUBO {
532                        base_color: [1.0, 1.0, 1.0, 1.0],
533                        quant_steps,
534                        emissive: 0,
535                        _pad0: 0,
536                        _pad1: 0,
537                    }
538                }
539                crate::engine::graphics::MaterialHandle::GRID_MESH => MaterialUBO {
540                    base_color: [1.0, 1.0, 1.0, 1.0],
541                    quant_steps: 1.0,
542                    emissive: 1,
543                    _pad0: 0,
544                    _pad1: 0,
545                },
546                // While migrating, treat UNLIT as a simple toon material too.
547                crate::engine::graphics::MaterialHandle::UNLIT_MESH => MaterialUBO {
548                    base_color: [1.0, 1.0, 1.0, 1.0],
549                    quant_steps,
550                    emissive: 1,
551                    _pad0: 0,
552                    _pad1: 0,
553                },
554                crate::engine::graphics::MaterialHandle::MIRROR => MaterialUBO {
555                    base_color: [1.0, 1.0, 1.0, 1.0],
556                    quant_steps: 1.0,
557                    emissive: 1,
558                    _pad0: 0,
559                    _pad1: 0,
560                },
561                _ => MaterialUBO::default(),
562            }
563        }
564
565        pub fn new(
566            window: Arc<Window>,
567            xr_required: Option<(&[String], &[String])>,
568            msaa_mode: MsaaMode,
569        ) -> Result<Self, Box<dyn std::error::Error>> {
570            // Prefer the helper context while we're migrating: it enables surface extensions
571            // and sets up graphics/compute queues and allocators.
572            let context = {
573                let mut config = VulkanoConfig::default();
574
575                // SteamVR's OpenXR Vulkan requirements commonly report a max API of 1.2.0.
576                // Some runtimes appear to validate the VkInstance API version against this.
577                config.instance_create_info.max_api_version = Some(Version::V1_2);
578
579                // Dynamic rendering: required so we can record the same draw-batch code against
580                // non-swapchain targets (e.g. OpenXR swapchain images) without per-target
581                // RenderPass/Framebuffer objects.
582                //
583                // On Vulkan 1.2 this is provided via VK_KHR_dynamic_rendering.
584                config.device_extensions.khr_dynamic_rendering = true;
585                config.device_features.dynamic_rendering = true;
586
587                if let Some((instance_exts, device_exts)) = xr_required {
588                    let mut enabled_instance_exts = config.instance_create_info.enabled_extensions;
589                    let mut enabled_device_exts = config.device_extensions;
590
591                    let mut unknown_instance_exts: Vec<&str> = Vec::new();
592                    for name in instance_exts {
593                        let ok = match name.as_str() {
594                            "VK_KHR_get_physical_device_properties2" => {
595                                enabled_instance_exts.khr_get_physical_device_properties2 = true;
596                                true
597                            }
598                            "VK_KHR_external_memory_capabilities" => {
599                                enabled_instance_exts.khr_external_memory_capabilities = true;
600                                true
601                            }
602                            "VK_KHR_external_fence_capabilities" => {
603                                enabled_instance_exts.khr_external_fence_capabilities = true;
604                                true
605                            }
606                            "VK_KHR_external_semaphore_capabilities" => {
607                                enabled_instance_exts.khr_external_semaphore_capabilities = true;
608                                true
609                            }
610                            "VK_KHR_surface" => {
611                                // Needed by winit surface creation anyway, but we mark it explicitly.
612                                enabled_instance_exts.khr_surface = true;
613                                true
614                            }
615                            _ => false,
616                        };
617                        if !ok {
618                            unknown_instance_exts.push(name);
619                        }
620                    }
621
622                    let mut unknown_device_exts: Vec<&str> = Vec::new();
623                    for name in device_exts {
624                        let ok = match name.as_str() {
625                            "VK_KHR_external_memory" => {
626                                enabled_device_exts.khr_external_memory = true;
627                                true
628                            }
629                            "VK_KHR_external_memory_fd" => {
630                                enabled_device_exts.khr_external_memory_fd = true;
631                                true
632                            }
633                            "VK_KHR_external_fence" => {
634                                enabled_device_exts.khr_external_fence = true;
635                                true
636                            }
637                            "VK_KHR_external_fence_fd" => {
638                                enabled_device_exts.khr_external_fence_fd = true;
639                                true
640                            }
641                            "VK_KHR_external_semaphore" => {
642                                enabled_device_exts.khr_external_semaphore = true;
643                                true
644                            }
645                            "VK_KHR_external_semaphore_fd" => {
646                                enabled_device_exts.khr_external_semaphore_fd = true;
647                                true
648                            }
649                            "VK_KHR_get_memory_requirements2" => {
650                                enabled_device_exts.khr_get_memory_requirements2 = true;
651                                true
652                            }
653                            "VK_KHR_dedicated_allocation" => {
654                                enabled_device_exts.khr_dedicated_allocation = true;
655                                true
656                            }
657                            "VK_KHR_bind_memory2" => {
658                                enabled_device_exts.khr_bind_memory2 = true;
659                                true
660                            }
661                            "VK_KHR_timeline_semaphore" => {
662                                enabled_device_exts.khr_timeline_semaphore = true;
663                                true
664                            }
665                            "VK_KHR_image_format_list" => {
666                                enabled_device_exts.khr_image_format_list = true;
667                                true
668                            }
669                            _ => false,
670                        };
671                        if !ok {
672                            unknown_device_exts.push(name);
673                        }
674                    }
675
676                    config.instance_create_info.enabled_extensions = enabled_instance_exts;
677                    config.device_extensions = enabled_device_exts;
678
679                    // Keep the device selection filter in sync with the extensions we require.
680                    let required_dev_exts: DeviceExtensions = enabled_device_exts;
681                    config.device_filter_fn =
682                        Arc::new(move |p| p.supported_extensions().contains(&required_dev_exts));
683
684                    if !unknown_instance_exts.is_empty() || !unknown_device_exts.is_empty() {
685                        // These might still be satisfied by Vulkan API version or be irrelevant to Vulkano;
686                        // we log them so we can extend the mapping as needed.
687                        eprintln!(
688                            "[VulkanoRenderer] Note: some OpenXR-required Vulkan extensions were not mapped: instance={:?} device={:?}",
689                            unknown_instance_exts, unknown_device_exts
690                        );
691                    }
692                }
693
694                VulkanoContext::new(config)
695            };
696            let device = context.device().clone();
697
698            // Global toggle: either 4x MSAA (if supported) or no multisampling.
699            let msaa_samples = match msaa_mode {
700                MsaaMode::Off => SampleCount::Sample1,
701                MsaaMode::Msaa4x => {
702                    let props = device.physical_device().properties();
703                    let counts = props.framebuffer_color_sample_counts
704                        & props.framebuffer_depth_sample_counts;
705                    if counts.intersects(SampleCounts::SAMPLE_4) {
706                        SampleCount::Sample4
707                    } else {
708                        SampleCount::Sample1
709                    }
710                }
711            };
712
713            match msaa_samples {
714                SampleCount::Sample4 => println!("[VulkanoRenderer] MSAA enabled: 4x"),
715                _ => println!("[VulkanoRenderer] MSAA disabled"),
716            }
717
718            let swapchain_state =
719                VulkanoSwapchainState::new(&context, window.clone(), msaa_samples)?;
720            let framebuffer_count = swapchain_state.swapchain_views.len();
721
722            let set_layouts = PipelineDescriptorSetLayouts::new(device.clone())?;
723
724            let vs = toon_mesh_vs::load(device.clone())?;
725            let mirror_vs = mirror_mesh_vs::load(device.clone())?;
726            let fs = toon_mesh_fs::load(device.clone())?;
727            let emissive_fs = emissive_toon_mesh_fs::load(device.clone())?;
728            let mirror_fs = mirror_mesh_fs::load(device.clone())?;
729            let grid_vs = grid_mesh_vs::load(device.clone())?;
730            let grid_fs = grid_square_mesh_fs::load(device.clone())?;
731
732            let skinned_vs = skinned_toon_mesh_vs::load(device.clone())?;
733
734            let stages = vec![
735                PipelineShaderStageCreateInfo::new(
736                    vs.entry_point("main")
737                        .ok_or("missing toon-mesh.vert entry point")?,
738                ),
739                PipelineShaderStageCreateInfo::new(
740                    fs.entry_point("main")
741                        .ok_or("missing toon-mesh.frag entry point")?,
742                ),
743            ];
744
745            let grid_stages = vec![
746                PipelineShaderStageCreateInfo::new(
747                    grid_vs
748                        .entry_point("main")
749                        .ok_or("missing grid.vert entry point")?,
750                ),
751                PipelineShaderStageCreateInfo::new(
752                    grid_fs
753                        .entry_point("main")
754                        .ok_or("missing grid-square.frag entry point")?,
755                ),
756            ];
757
758            let skinned_stages = vec![
759                PipelineShaderStageCreateInfo::new(
760                    skinned_vs
761                        .entry_point("main")
762                        .ok_or("missing skinned-toon-mesh.vert entry point")?,
763                ),
764                PipelineShaderStageCreateInfo::new(
765                    fs.entry_point("main")
766                        .ok_or("missing toon-mesh.frag entry point")?,
767                ),
768            ];
769
770            let emissive_stages = vec![
771                PipelineShaderStageCreateInfo::new(
772                    vs.entry_point("main")
773                        .ok_or("missing toon-mesh.vert entry point")?,
774                ),
775                PipelineShaderStageCreateInfo::new(
776                    emissive_fs
777                        .entry_point("main")
778                        .ok_or("missing emissive-toon-mesh.frag entry point")?,
779                ),
780            ];
781
782            let mirror_stages = vec![
783                PipelineShaderStageCreateInfo::new(
784                    mirror_vs
785                        .entry_point("main")
786                        .ok_or("missing mirror-mesh.vert entry point")?,
787                ),
788                PipelineShaderStageCreateInfo::new(
789                    mirror_fs
790                        .entry_point("main")
791                        .ok_or("missing mirror-mesh.frag entry point")?,
792                ),
793            ];
794
795            let skinned_emissive_stages = vec![
796                PipelineShaderStageCreateInfo::new(
797                    skinned_vs
798                        .entry_point("main")
799                        .ok_or("missing skinned-toon-mesh.vert entry point")?,
800                ),
801                PipelineShaderStageCreateInfo::new(
802                    emissive_fs
803                        .entry_point("main")
804                        .ok_or("missing emissive-toon-mesh.frag entry point")?,
805                ),
806            ];
807
808            let layout = PipelineLayout::new(
809                device.clone(),
810                PipelineLayoutCreateInfo {
811                    set_layouts: vec![
812                        set_layouts.global.clone(),
813                        set_layouts.material.clone(),
814                        set_layouts.rig.clone(),
815                    ],
816                    ..Default::default()
817                },
818            )?;
819
820            // Important: `CpuVertex` contains more than just position (e.g. UV).
821            // We explicitly declare which attributes are consumed by the shader.
822            // Instance data occupies locations 1-4 (+ per-instance color/emissive).
823            let vertex_input_state_static = VertexInputState::new()
824                .binding(
825                    0,
826                    VertexInputBindingDescription {
827                        stride: size_of::<CpuVertex>() as u32,
828                        input_rate: VertexInputRate::Vertex,
829                        ..Default::default()
830                    },
831                )
832                .binding(
833                    1,
834                    VertexInputBindingDescription {
835                        stride: size_of::<InstanceData>() as u32,
836                        input_rate: VertexInputRate::Instance { divisor: 1 },
837                        ..Default::default()
838                    },
839                )
840                .attribute(
841                    0,
842                    VertexInputAttributeDescription {
843                        binding: 0,
844                        format: Format::R32G32B32_SFLOAT,
845                        offset: 0,
846                        ..Default::default()
847                    },
848                )
849                .attribute(
850                    5,
851                    VertexInputAttributeDescription {
852                        binding: 0,
853                        format: Format::R32G32_SFLOAT,
854                        offset: 12,
855                        ..Default::default()
856                    },
857                )
858                .attribute(
859                    8,
860                    VertexInputAttributeDescription {
861                        binding: 0,
862                        format: Format::R32G32B32_SFLOAT,
863                        offset: 20,
864                        ..Default::default()
865                    },
866                )
867                .attribute(
868                    1,
869                    VertexInputAttributeDescription {
870                        binding: 1,
871                        format: Format::R32G32B32A32_SFLOAT,
872                        offset: 0,
873                        ..Default::default()
874                    },
875                )
876                .attribute(
877                    2,
878                    VertexInputAttributeDescription {
879                        binding: 1,
880                        format: Format::R32G32B32A32_SFLOAT,
881                        offset: 16,
882                        ..Default::default()
883                    },
884                )
885                .attribute(
886                    3,
887                    VertexInputAttributeDescription {
888                        binding: 1,
889                        format: Format::R32G32B32A32_SFLOAT,
890                        offset: 32,
891                        ..Default::default()
892                    },
893                )
894                .attribute(
895                    4,
896                    VertexInputAttributeDescription {
897                        binding: 1,
898                        format: Format::R32G32B32A32_SFLOAT,
899                        offset: 48,
900                        ..Default::default()
901                    },
902                )
903                .attribute(
904                    6,
905                    VertexInputAttributeDescription {
906                        binding: 1,
907                        format: Format::R32G32B32A32_SFLOAT,
908                        offset: 64,
909                        ..Default::default()
910                    },
911                )
912                .attribute(
913                    7,
914                    VertexInputAttributeDescription {
915                        binding: 1,
916                        format: Format::R32_SFLOAT,
917                        offset: 80,
918                        ..Default::default()
919                    },
920                )
921                .attribute(
922                    9,
923                    VertexInputAttributeDescription {
924                        binding: 1,
925                        format: Format::R32_SFLOAT,
926                        offset: 84,
927                        ..Default::default()
928                    },
929                )
930                .attribute(
931                    10,
932                    VertexInputAttributeDescription {
933                        binding: 1,
934                        format: Format::R32_UINT,
935                        offset: 88,
936                        ..Default::default()
937                    },
938                )
939                .attribute(
940                    11,
941                    VertexInputAttributeDescription {
942                        binding: 1,
943                        format: Format::R32_UINT,
944                        offset: 92,
945                        ..Default::default()
946                    },
947                );
948
949            // Skinned pipeline: add a separate per-vertex skinning buffer (binding=1),
950            // and move per-instance data to binding=2.
951            let vertex_input_state_skinned = VertexInputState::new()
952                .binding(
953                    0,
954                    VertexInputBindingDescription {
955                        stride: size_of::<CpuVertex>() as u32,
956                        input_rate: VertexInputRate::Vertex,
957                        ..Default::default()
958                    },
959                )
960                .binding(
961                    1,
962                    VertexInputBindingDescription {
963                        stride: size_of::<GpuSkinVertex>() as u32,
964                        input_rate: VertexInputRate::Vertex,
965                        ..Default::default()
966                    },
967                )
968                .binding(
969                    2,
970                    VertexInputBindingDescription {
971                        stride: size_of::<InstanceData>() as u32,
972                        input_rate: VertexInputRate::Instance { divisor: 1 },
973                        ..Default::default()
974                    },
975                )
976                // Base vertex attributes.
977                .attribute(
978                    0,
979                    VertexInputAttributeDescription {
980                        binding: 0,
981                        format: Format::R32G32B32_SFLOAT,
982                        offset: 0,
983                        ..Default::default()
984                    },
985                )
986                .attribute(
987                    5,
988                    VertexInputAttributeDescription {
989                        binding: 0,
990                        format: Format::R32G32_SFLOAT,
991                        offset: 12,
992                        ..Default::default()
993                    },
994                )
995                .attribute(
996                    8,
997                    VertexInputAttributeDescription {
998                        binding: 0,
999                        format: Format::R32G32B32_SFLOAT,
1000                        offset: 20,
1001                        ..Default::default()
1002                    },
1003                )
1004                // Skinning attributes.
1005                .attribute(
1006                    12,
1007                    VertexInputAttributeDescription {
1008                        binding: 1,
1009                        format: Format::R16G16B16A16_UINT,
1010                        offset: 0,
1011                        ..Default::default()
1012                    },
1013                )
1014                .attribute(
1015                    13,
1016                    VertexInputAttributeDescription {
1017                        binding: 1,
1018                        format: Format::R32G32B32A32_SFLOAT,
1019                        offset: 8,
1020                        ..Default::default()
1021                    },
1022                )
1023                // Per-instance attributes (binding=2).
1024                .attribute(
1025                    1,
1026                    VertexInputAttributeDescription {
1027                        binding: 2,
1028                        format: Format::R32G32B32A32_SFLOAT,
1029                        offset: 0,
1030                        ..Default::default()
1031                    },
1032                )
1033                .attribute(
1034                    2,
1035                    VertexInputAttributeDescription {
1036                        binding: 2,
1037                        format: Format::R32G32B32A32_SFLOAT,
1038                        offset: 16,
1039                        ..Default::default()
1040                    },
1041                )
1042                .attribute(
1043                    3,
1044                    VertexInputAttributeDescription {
1045                        binding: 2,
1046                        format: Format::R32G32B32A32_SFLOAT,
1047                        offset: 32,
1048                        ..Default::default()
1049                    },
1050                )
1051                .attribute(
1052                    4,
1053                    VertexInputAttributeDescription {
1054                        binding: 2,
1055                        format: Format::R32G32B32A32_SFLOAT,
1056                        offset: 48,
1057                        ..Default::default()
1058                    },
1059                )
1060                .attribute(
1061                    6,
1062                    VertexInputAttributeDescription {
1063                        binding: 2,
1064                        format: Format::R32G32B32A32_SFLOAT,
1065                        offset: 64,
1066                        ..Default::default()
1067                    },
1068                )
1069                .attribute(
1070                    7,
1071                    VertexInputAttributeDescription {
1072                        binding: 2,
1073                        format: Format::R32_SFLOAT,
1074                        offset: 80,
1075                        ..Default::default()
1076                    },
1077                )
1078                .attribute(
1079                    9,
1080                    VertexInputAttributeDescription {
1081                        binding: 2,
1082                        format: Format::R32_SFLOAT,
1083                        offset: 84,
1084                        ..Default::default()
1085                    },
1086                )
1087                .attribute(
1088                    10,
1089                    VertexInputAttributeDescription {
1090                        binding: 2,
1091                        format: Format::R32_UINT,
1092                        offset: 88,
1093                        ..Default::default()
1094                    },
1095                )
1096                .attribute(
1097                    11,
1098                    VertexInputAttributeDescription {
1099                        binding: 2,
1100                        format: Format::R32_UINT,
1101                        offset: 92,
1102                        ..Default::default()
1103                    },
1104                );
1105
1106            let color_format = swapchain_state.swapchain.image_format();
1107            let mut pipeline_ci =
1108                vulkano::pipeline::graphics::GraphicsPipelineCreateInfo::layout(layout);
1109            pipeline_ci.stages = stages.into();
1110            pipeline_ci.vertex_input_state = Some(vertex_input_state_static);
1111            pipeline_ci.input_assembly_state = Some(InputAssemblyState::default());
1112            pipeline_ci.viewport_state = Some(ViewportState::default());
1113            pipeline_ci.rasterization_state = Some(RasterizationState::default());
1114            pipeline_ci.multisample_state = Some(MultisampleState {
1115                rasterization_samples: msaa_samples,
1116                ..Default::default()
1117            });
1118            // Enable depth testing so 3D geometry occludes correctly.
1119            pipeline_ci.depth_stencil_state = Some(DepthStencilState {
1120                depth: Some(DepthState::simple()),
1121                ..Default::default()
1122            });
1123            // Enable alpha blending so textures with transparency (e.g. PNG alpha) render correctly.
1124            // Uses straight alpha: out.rgb = src.rgb * src.a + dst.rgb * (1-src.a)
1125            pipeline_ci.color_blend_state = Some(ColorBlendState::with_attachment_states(
1126                1,
1127                ColorBlendAttachmentState {
1128                    blend: Some(AttachmentBlend {
1129                        src_color_blend_factor: BlendFactor::SrcAlpha,
1130                        dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
1131                        color_blend_op: BlendOp::Add,
1132                        src_alpha_blend_factor: BlendFactor::One,
1133                        dst_alpha_blend_factor: BlendFactor::OneMinusSrcAlpha,
1134                        alpha_blend_op: BlendOp::Add,
1135                    }),
1136                    color_write_enable: true,
1137                    color_write_mask: ColorComponents::all(),
1138                },
1139            ));
1140            pipeline_ci.dynamic_state = [DynamicState::Viewport, DynamicState::Scissor]
1141                .into_iter()
1142                .collect();
1143            // Dynamic rendering so we can reuse the same draw code for non-swapchain targets (OpenXR).
1144            // The pipeline is keyed by attachment formats rather than a specific RenderPass.
1145            let mut pipeline_rendering = PipelineRenderingCreateInfo::default();
1146            pipeline_rendering.color_attachment_formats = vec![Some(color_format)];
1147            pipeline_rendering.depth_attachment_format = Some(VulkanoSwapchainState::DEPTH_FORMAT);
1148            pipeline_rendering.stencil_attachment_format =
1149                Some(VulkanoSwapchainState::DEPTH_FORMAT);
1150
1151            pipeline_ci.subpass = Some(PipelineSubpassType::BeginRendering(pipeline_rendering));
1152
1153            let pipeline_toon_mesh =
1154                GraphicsPipeline::new(device.clone(), None, pipeline_ci.clone())?;
1155            let mut pipeline_ci_mirror = pipeline_ci.clone();
1156            pipeline_ci_mirror.stages = mirror_stages.clone().into();
1157            let pipeline_mirror_mesh =
1158                GraphicsPipeline::new(device.clone(), None, pipeline_ci_mirror.clone())?;
1159            let mut pipeline_ci_grid = pipeline_ci.clone();
1160            pipeline_ci_grid.stages = grid_stages.clone().into();
1161            let pipeline_grid_mesh =
1162                GraphicsPipeline::new(device.clone(), None, pipeline_ci_grid.clone())?;
1163
1164            let mut pipeline_ci_emissive = pipeline_ci.clone();
1165            pipeline_ci_emissive.stages = emissive_stages.clone().into();
1166            let pipeline_emissive_toon_mesh =
1167                GraphicsPipeline::new(device.clone(), None, pipeline_ci_emissive.clone())?;
1168
1169            let mut pipeline_ci_emissive_prepass = pipeline_ci_emissive.clone();
1170            pipeline_ci_emissive_prepass.depth_stencil_state = Some(DepthStencilState {
1171                depth: Some(DepthState {
1172                    write_enable: false,
1173                    compare_op: CompareOp::LessOrEqual,
1174                    ..DepthState::simple()
1175                }),
1176                ..Default::default()
1177            });
1178            let pipeline_emissive_prepass_toon_mesh =
1179                GraphicsPipeline::new(device.clone(), None, pipeline_ci_emissive_prepass)?;
1180
1181            let mut pipeline_ci_emissive_prepass_depth_write = pipeline_ci_emissive.clone();
1182            pipeline_ci_emissive_prepass_depth_write.depth_stencil_state =
1183                Some(DepthStencilState {
1184                    depth: Some(DepthState {
1185                        write_enable: true,
1186                        compare_op: CompareOp::LessOrEqual,
1187                        ..DepthState::simple()
1188                    }),
1189                    ..Default::default()
1190                });
1191            let pipeline_emissive_prepass_depth_write_toon_mesh = GraphicsPipeline::new(
1192                device.clone(),
1193                None,
1194                pipeline_ci_emissive_prepass_depth_write,
1195            )?;
1196
1197            // Transparent variant: depth test ON, depth write OFF.
1198            let mut pipeline_ci_transparent = pipeline_ci.clone();
1199            pipeline_ci_transparent.depth_stencil_state = Some(DepthStencilState {
1200                depth: Some(DepthState {
1201                    write_enable: false,
1202                    ..DepthState::simple()
1203                }),
1204                ..Default::default()
1205            });
1206            let pipeline_toon_mesh_transparent =
1207                GraphicsPipeline::new(device.clone(), None, pipeline_ci_transparent.clone())?;
1208            let mut pipeline_ci_mirror_transparent = pipeline_ci_transparent.clone();
1209            pipeline_ci_mirror_transparent.stages = mirror_stages.clone().into();
1210            let pipeline_mirror_mesh_transparent =
1211                GraphicsPipeline::new(device.clone(), None, pipeline_ci_mirror_transparent)?;
1212            let mut pipeline_ci_grid_transparent = pipeline_ci_transparent.clone();
1213            pipeline_ci_grid_transparent.stages = grid_stages.clone().into();
1214            let pipeline_grid_mesh_transparent =
1215                GraphicsPipeline::new(device.clone(), None, pipeline_ci_grid_transparent.clone())?;
1216
1217            let mut pipeline_ci_emissive_transparent = pipeline_ci_transparent.clone();
1218            pipeline_ci_emissive_transparent.stages = emissive_stages.clone().into();
1219            let pipeline_emissive_toon_mesh_transparent = GraphicsPipeline::new(
1220                device.clone(),
1221                None,
1222                pipeline_ci_emissive_transparent.clone(),
1223            )?;
1224
1225            // Transparent cutout variant:
1226            // - depth test/write ON
1227            // - alpha-to-coverage enabled (requires MSAA)
1228            // - blending disabled (coverage handles edges)
1229            let mut pipeline_ci_cutout = pipeline_ci.clone();
1230            pipeline_ci_cutout.multisample_state = Some(MultisampleState {
1231                rasterization_samples: msaa_samples,
1232                alpha_to_coverage_enable: msaa_samples != SampleCount::Sample1,
1233                ..Default::default()
1234            });
1235            pipeline_ci_cutout.color_blend_state = Some(ColorBlendState::with_attachment_states(
1236                1,
1237                ColorBlendAttachmentState {
1238                    blend: None,
1239                    color_write_enable: true,
1240                    color_write_mask: ColorComponents::all(),
1241                },
1242            ));
1243            let pipeline_toon_mesh_cutout =
1244                GraphicsPipeline::new(device.clone(), None, pipeline_ci_cutout.clone())?;
1245            let mut pipeline_ci_mirror_cutout = pipeline_ci_cutout.clone();
1246            pipeline_ci_mirror_cutout.stages = mirror_stages.clone().into();
1247            let pipeline_mirror_mesh_cutout =
1248                GraphicsPipeline::new(device.clone(), None, pipeline_ci_mirror_cutout)?;
1249
1250            let mut pipeline_ci_emissive_cutout = pipeline_ci_cutout.clone();
1251            pipeline_ci_emissive_cutout.stages = emissive_stages.clone().into();
1252            let pipeline_emissive_toon_mesh_cutout =
1253                GraphicsPipeline::new(device.clone(), None, pipeline_ci_emissive_cutout.clone())?;
1254
1255            let mut pipeline_ci_emissive_prepass_cutout = pipeline_ci_emissive_cutout.clone();
1256            pipeline_ci_emissive_prepass_cutout.depth_stencil_state = Some(DepthStencilState {
1257                depth: Some(DepthState {
1258                    write_enable: false,
1259                    compare_op: CompareOp::LessOrEqual,
1260                    ..DepthState::simple()
1261                }),
1262                ..Default::default()
1263            });
1264            let pipeline_emissive_prepass_toon_mesh_cutout =
1265                GraphicsPipeline::new(device.clone(), None, pipeline_ci_emissive_prepass_cutout)?;
1266
1267            // Skinned variants: same state, different vertex shader.
1268            let mut pipeline_ci_skinned = pipeline_ci.clone();
1269            pipeline_ci_skinned.stages = skinned_stages.clone().into();
1270            pipeline_ci_skinned.vertex_input_state = Some(vertex_input_state_skinned.clone());
1271            let pipeline_skinned_toon_mesh =
1272                GraphicsPipeline::new(device.clone(), None, pipeline_ci_skinned.clone())?;
1273
1274            let mut pipeline_ci_skinned_emissive = pipeline_ci.clone();
1275            pipeline_ci_skinned_emissive.stages = skinned_emissive_stages.clone().into();
1276            pipeline_ci_skinned_emissive.vertex_input_state =
1277                Some(vertex_input_state_skinned.clone());
1278            let pipeline_skinned_emissive_toon_mesh =
1279                GraphicsPipeline::new(device.clone(), None, pipeline_ci_skinned_emissive.clone())?;
1280
1281            let mut pipeline_ci_skinned_emissive_prepass = pipeline_ci_skinned_emissive.clone();
1282            pipeline_ci_skinned_emissive_prepass.depth_stencil_state = Some(DepthStencilState {
1283                depth: Some(DepthState {
1284                    write_enable: false,
1285                    compare_op: CompareOp::LessOrEqual,
1286                    ..DepthState::simple()
1287                }),
1288                ..Default::default()
1289            });
1290            let pipeline_skinned_emissive_prepass_toon_mesh =
1291                GraphicsPipeline::new(device.clone(), None, pipeline_ci_skinned_emissive_prepass)?;
1292
1293            let mut pipeline_ci_skinned_emissive_prepass_depth_write =
1294                pipeline_ci_skinned_emissive.clone();
1295            pipeline_ci_skinned_emissive_prepass_depth_write.depth_stencil_state =
1296                Some(DepthStencilState {
1297                    depth: Some(DepthState {
1298                        write_enable: true,
1299                        compare_op: CompareOp::LessOrEqual,
1300                        ..DepthState::simple()
1301                    }),
1302                    ..Default::default()
1303                });
1304            let pipeline_skinned_emissive_prepass_depth_write_toon_mesh = GraphicsPipeline::new(
1305                device.clone(),
1306                None,
1307                pipeline_ci_skinned_emissive_prepass_depth_write,
1308            )?;
1309
1310            let mut pipeline_ci_skinned_transparent = pipeline_ci_transparent.clone();
1311            pipeline_ci_skinned_transparent.stages = skinned_stages.clone().into();
1312            pipeline_ci_skinned_transparent.vertex_input_state =
1313                Some(vertex_input_state_skinned.clone());
1314            let pipeline_skinned_toon_mesh_transparent =
1315                GraphicsPipeline::new(device.clone(), None, pipeline_ci_skinned_transparent)?;
1316
1317            let mut pipeline_ci_skinned_emissive_transparent = pipeline_ci_transparent.clone();
1318            pipeline_ci_skinned_emissive_transparent.stages =
1319                skinned_emissive_stages.clone().into();
1320            pipeline_ci_skinned_emissive_transparent.vertex_input_state =
1321                Some(vertex_input_state_skinned.clone());
1322            let pipeline_skinned_emissive_toon_mesh_transparent = GraphicsPipeline::new(
1323                device.clone(),
1324                None,
1325                pipeline_ci_skinned_emissive_transparent,
1326            )?;
1327
1328            let mut pipeline_ci_skinned_cutout = pipeline_ci_cutout.clone();
1329            pipeline_ci_skinned_cutout.stages = skinned_stages.into();
1330            pipeline_ci_skinned_cutout.vertex_input_state =
1331                Some(vertex_input_state_skinned.clone());
1332            let pipeline_skinned_toon_mesh_cutout =
1333                GraphicsPipeline::new(device.clone(), None, pipeline_ci_skinned_cutout)?;
1334
1335            let mut pipeline_ci_skinned_emissive_cutout = pipeline_ci_cutout.clone();
1336            pipeline_ci_skinned_emissive_cutout.stages = skinned_emissive_stages.clone().into();
1337            pipeline_ci_skinned_emissive_cutout.vertex_input_state =
1338                Some(vertex_input_state_skinned.clone());
1339            let pipeline_skinned_emissive_toon_mesh_cutout = GraphicsPipeline::new(
1340                device.clone(),
1341                None,
1342                pipeline_ci_skinned_emissive_cutout.clone(),
1343            )?;
1344
1345            let mut pipeline_ci_skinned_emissive_prepass_cutout =
1346                pipeline_ci_skinned_emissive_cutout.clone();
1347            pipeline_ci_skinned_emissive_prepass_cutout.depth_stencil_state =
1348                Some(DepthStencilState {
1349                    depth: Some(DepthState {
1350                        write_enable: false,
1351                        compare_op: CompareOp::LessOrEqual,
1352                        ..DepthState::simple()
1353                    }),
1354                    ..Default::default()
1355                });
1356            let pipeline_skinned_emissive_prepass_toon_mesh_cutout = GraphicsPipeline::new(
1357                device.clone(),
1358                None,
1359                pipeline_ci_skinned_emissive_prepass_cutout,
1360            )?;
1361
1362            // Stencil clip pipelines — color write off, no depth test.
1363            // Shared helper: stencil ops for enter/exit clip.
1364            let stencil_ops_incr = StencilOps {
1365                compare_op: CompareOp::Equal,
1366                pass_op: StencilOp::IncrementAndClamp,
1367                fail_op: StencilOp::Keep,
1368                depth_fail_op: StencilOp::Keep,
1369            };
1370            let stencil_ops_decr = StencilOps {
1371                compare_op: CompareOp::Equal,
1372                pass_op: StencilOp::DecrementAndClamp,
1373                fail_op: StencilOp::Keep,
1374                depth_fail_op: StencilOp::Keep,
1375            };
1376            let stencil_ops_test = StencilOps {
1377                compare_op: CompareOp::Equal,
1378                pass_op: StencilOp::Keep,
1379                fail_op: StencilOp::Keep,
1380                depth_fail_op: StencilOp::Keep,
1381            };
1382            let stencil_write_color_blend = ColorBlendState::with_attachment_states(
1383                1,
1384                ColorBlendAttachmentState {
1385                    blend: None,
1386                    color_write_enable: true,
1387                    color_write_mask: ColorComponents::empty(),
1388                },
1389            );
1390            let mut stencil_dynamic_state = pipeline_ci.dynamic_state.clone();
1391            stencil_dynamic_state.insert(DynamicState::StencilReference);
1392
1393            let mut pipeline_ci_stencil_incr = pipeline_ci.clone();
1394            pipeline_ci_stencil_incr.color_blend_state = Some(stencil_write_color_blend.clone());
1395            pipeline_ci_stencil_incr.depth_stencil_state = Some(DepthStencilState {
1396                depth: None,
1397                stencil: Some(StencilState {
1398                    front: StencilOpState {
1399                        ops: stencil_ops_incr,
1400                        ..Default::default()
1401                    },
1402                    back: StencilOpState {
1403                        ops: stencil_ops_incr,
1404                        ..Default::default()
1405                    },
1406                }),
1407                ..Default::default()
1408            });
1409            pipeline_ci_stencil_incr.dynamic_state = stencil_dynamic_state.clone();
1410            let pipeline_stencil_incr =
1411                GraphicsPipeline::new(device.clone(), None, pipeline_ci_stencil_incr)?;
1412
1413            let mut pipeline_ci_stencil_decr = pipeline_ci.clone();
1414            pipeline_ci_stencil_decr.color_blend_state = Some(stencil_write_color_blend);
1415            pipeline_ci_stencil_decr.depth_stencil_state = Some(DepthStencilState {
1416                depth: None,
1417                stencil: Some(StencilState {
1418                    front: StencilOpState {
1419                        ops: stencil_ops_decr,
1420                        ..Default::default()
1421                    },
1422                    back: StencilOpState {
1423                        ops: stencil_ops_decr,
1424                        ..Default::default()
1425                    },
1426                }),
1427                ..Default::default()
1428            });
1429            pipeline_ci_stencil_decr.dynamic_state = stencil_dynamic_state.clone();
1430            let pipeline_stencil_decr =
1431                GraphicsPipeline::new(device.clone(), None, pipeline_ci_stencil_decr)?;
1432
1433            // Overlay variants that require stencil == reference to pass.
1434            let clipped_depth_stencil = DepthStencilState {
1435                depth: Some(DepthState::simple()),
1436                stencil: Some(StencilState {
1437                    front: StencilOpState {
1438                        ops: stencil_ops_test,
1439                        ..Default::default()
1440                    },
1441                    back: StencilOpState {
1442                        ops: stencil_ops_test,
1443                        ..Default::default()
1444                    },
1445                }),
1446                ..Default::default()
1447            };
1448
1449            let mut pipeline_ci_overlay_clipped = pipeline_ci.clone();
1450            pipeline_ci_overlay_clipped.depth_stencil_state = Some(clipped_depth_stencil.clone());
1451            pipeline_ci_overlay_clipped.dynamic_state = stencil_dynamic_state.clone();
1452            let pipeline_overlay_clipped =
1453                GraphicsPipeline::new(device.clone(), None, pipeline_ci_overlay_clipped)?;
1454
1455            let mut pipeline_ci_emissive_overlay_clipped = pipeline_ci_emissive.clone();
1456            pipeline_ci_emissive_overlay_clipped.depth_stencil_state =
1457                Some(clipped_depth_stencil.clone());
1458            pipeline_ci_emissive_overlay_clipped.dynamic_state = stencil_dynamic_state.clone();
1459            let pipeline_emissive_overlay_clipped =
1460                GraphicsPipeline::new(device.clone(), None, pipeline_ci_emissive_overlay_clipped)?;
1461
1462            // Opaque clipped: identical depth_stencil (depth write ON + stencil EQUAL),
1463            // based on the opaque pipeline_ci so blend state matches opaque phase.
1464            let mut pipeline_ci_opaque_clipped = pipeline_ci.clone();
1465            pipeline_ci_opaque_clipped.depth_stencil_state = Some(clipped_depth_stencil.clone());
1466            pipeline_ci_opaque_clipped.dynamic_state = stencil_dynamic_state.clone();
1467            let pipeline_opaque_clipped =
1468                GraphicsPipeline::new(device.clone(), None, pipeline_ci_opaque_clipped)?;
1469
1470            let mut pipeline_ci_mirror_clipped = pipeline_ci_mirror.clone();
1471            pipeline_ci_mirror_clipped.depth_stencil_state = Some(clipped_depth_stencil.clone());
1472            pipeline_ci_mirror_clipped.dynamic_state = stencil_dynamic_state.clone();
1473            let pipeline_mirror_mesh_clipped =
1474                GraphicsPipeline::new(device.clone(), None, pipeline_ci_mirror_clipped)?;
1475
1476            let mut pipeline_ci_emissive_opaque_clipped = pipeline_ci_emissive.clone();
1477            pipeline_ci_emissive_opaque_clipped.depth_stencil_state = Some(clipped_depth_stencil);
1478            pipeline_ci_emissive_opaque_clipped.dynamic_state = stencil_dynamic_state.clone();
1479            let pipeline_emissive_opaque_clipped =
1480                GraphicsPipeline::new(device.clone(), None, pipeline_ci_emissive_opaque_clipped)?;
1481
1482            let transparent_clipped_depth_stencil = DepthStencilState {
1483                depth: Some(DepthState {
1484                    write_enable: false,
1485                    ..DepthState::simple()
1486                }),
1487                stencil: Some(StencilState {
1488                    front: StencilOpState {
1489                        ops: stencil_ops_test,
1490                        ..Default::default()
1491                    },
1492                    back: StencilOpState {
1493                        ops: stencil_ops_test,
1494                        ..Default::default()
1495                    },
1496                }),
1497                ..Default::default()
1498            };
1499
1500            let mut pipeline_ci_transparent_clipped = pipeline_ci_transparent.clone();
1501            pipeline_ci_transparent_clipped.depth_stencil_state =
1502                Some(transparent_clipped_depth_stencil.clone());
1503            pipeline_ci_transparent_clipped.dynamic_state = stencil_dynamic_state.clone();
1504            let pipeline_toon_mesh_transparent_clipped =
1505                GraphicsPipeline::new(device.clone(), None, pipeline_ci_transparent_clipped)?;
1506            let mut pipeline_ci_mirror_transparent_clipped = pipeline_ci_transparent.clone();
1507            pipeline_ci_mirror_transparent_clipped.stages = mirror_stages.clone().into();
1508            pipeline_ci_mirror_transparent_clipped.depth_stencil_state =
1509                Some(transparent_clipped_depth_stencil.clone());
1510            pipeline_ci_mirror_transparent_clipped.dynamic_state = stencil_dynamic_state.clone();
1511            let pipeline_mirror_mesh_transparent_clipped = GraphicsPipeline::new(
1512                device.clone(),
1513                None,
1514                pipeline_ci_mirror_transparent_clipped,
1515            )?;
1516            let mut pipeline_ci_grid_transparent_clipped = pipeline_ci_grid_transparent.clone();
1517            pipeline_ci_grid_transparent_clipped.depth_stencil_state =
1518                Some(transparent_clipped_depth_stencil.clone());
1519            pipeline_ci_grid_transparent_clipped.dynamic_state = stencil_dynamic_state.clone();
1520            let pipeline_grid_mesh_transparent_clipped =
1521                GraphicsPipeline::new(device.clone(), None, pipeline_ci_grid_transparent_clipped)?;
1522
1523            let mut pipeline_ci_emissive_transparent_clipped =
1524                pipeline_ci_emissive_transparent.clone();
1525            pipeline_ci_emissive_transparent_clipped.depth_stencil_state =
1526                Some(transparent_clipped_depth_stencil);
1527            pipeline_ci_emissive_transparent_clipped.dynamic_state = stencil_dynamic_state.clone();
1528            let pipeline_emissive_toon_mesh_transparent_clipped = GraphicsPipeline::new(
1529                device.clone(),
1530                None,
1531                pipeline_ci_emissive_transparent_clipped,
1532            )?;
1533
1534            let mut pipeline_ci_cutout_clipped = pipeline_ci_cutout.clone();
1535            pipeline_ci_cutout_clipped.depth_stencil_state = Some(DepthStencilState {
1536                depth: Some(DepthState::simple()),
1537                stencil: Some(StencilState {
1538                    front: StencilOpState {
1539                        ops: stencil_ops_test,
1540                        ..Default::default()
1541                    },
1542                    back: StencilOpState {
1543                        ops: stencil_ops_test,
1544                        ..Default::default()
1545                    },
1546                }),
1547                ..Default::default()
1548            });
1549            pipeline_ci_cutout_clipped.dynamic_state = stencil_dynamic_state.clone();
1550            let pipeline_toon_mesh_cutout_clipped =
1551                GraphicsPipeline::new(device.clone(), None, pipeline_ci_cutout_clipped)?;
1552            let mut pipeline_ci_mirror_cutout_clipped = pipeline_ci_cutout.clone();
1553            pipeline_ci_mirror_cutout_clipped.stages = mirror_stages.into();
1554            pipeline_ci_mirror_cutout_clipped.depth_stencil_state = Some(DepthStencilState {
1555                depth: Some(DepthState::simple()),
1556                stencil: Some(StencilState {
1557                    front: StencilOpState {
1558                        ops: stencil_ops_test,
1559                        ..Default::default()
1560                    },
1561                    back: StencilOpState {
1562                        ops: stencil_ops_test,
1563                        ..Default::default()
1564                    },
1565                }),
1566                ..Default::default()
1567            });
1568            pipeline_ci_mirror_cutout_clipped.dynamic_state = stencil_dynamic_state.clone();
1569            let pipeline_mirror_mesh_cutout_clipped =
1570                GraphicsPipeline::new(device.clone(), None, pipeline_ci_mirror_cutout_clipped)?;
1571
1572            let mut pipeline_ci_emissive_cutout_clipped = pipeline_ci_emissive_cutout.clone();
1573            pipeline_ci_emissive_cutout_clipped.depth_stencil_state = Some(DepthStencilState {
1574                depth: Some(DepthState::simple()),
1575                stencil: Some(StencilState {
1576                    front: StencilOpState {
1577                        ops: stencil_ops_test,
1578                        ..Default::default()
1579                    },
1580                    back: StencilOpState {
1581                        ops: stencil_ops_test,
1582                        ..Default::default()
1583                    },
1584                }),
1585                ..Default::default()
1586            });
1587            pipeline_ci_emissive_cutout_clipped.dynamic_state = stencil_dynamic_state;
1588            let pipeline_emissive_toon_mesh_cutout_clipped =
1589                GraphicsPipeline::new(device.clone(), None, pipeline_ci_emissive_cutout_clipped)?;
1590
1591            let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new(
1592                device.clone(),
1593                Default::default(),
1594            ));
1595
1596            let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
1597                device.clone(),
1598                Default::default(),
1599            ));
1600
1601            let post_processing_renderer = PostProcessingRenderer::new(
1602                device.clone(),
1603                context.memory_allocator().clone(),
1604                descriptor_set_allocator.clone(),
1605            )?;
1606
1607            let sampler_linear =
1608                Sampler::new(device.clone(), SamplerCreateInfo::simple_repeat_linear())?;
1609
1610            let sampler_nearest = Sampler::new(
1611                device.clone(),
1612                SamplerCreateInfo {
1613                    mag_filter: Filter::Nearest,
1614                    min_filter: Filter::Nearest,
1615                    mipmap_mode: SamplerMipmapMode::Nearest,
1616                    address_mode: [SamplerAddressMode::Repeat; 3],
1617                    ..Default::default()
1618                },
1619            )?;
1620
1621            let sampler_nearest_mag = Sampler::new(
1622                device.clone(),
1623                SamplerCreateInfo {
1624                    mag_filter: Filter::Nearest,
1625                    min_filter: Filter::Linear,
1626                    mipmap_mode: SamplerMipmapMode::Nearest,
1627                    address_mode: [SamplerAddressMode::Repeat; 3],
1628                    ..Default::default()
1629                },
1630            )?;
1631
1632            let mut state = Self {
1633                context,
1634                window,
1635
1636                swapchain_state,
1637
1638                command_buffer_allocator,
1639                descriptor_set_allocator,
1640                post_processing_renderer,
1641                meshes: HashMap::new(),
1642
1643                textures: HashMap::new(),
1644                sampler_linear,
1645                sampler_nearest,
1646                sampler_nearest_mag,
1647                default_white_texture: TextureHandle(0),
1648
1649                set_layouts,
1650
1651                pipeline_toon_mesh,
1652                pipeline_toon_mesh_transparent,
1653                pipeline_toon_mesh_cutout,
1654                pipeline_toon_mesh_transparent_clipped,
1655                pipeline_toon_mesh_cutout_clipped,
1656                pipeline_mirror_mesh,
1657                pipeline_mirror_mesh_transparent,
1658                pipeline_mirror_mesh_cutout,
1659                pipeline_mirror_mesh_clipped,
1660                pipeline_mirror_mesh_transparent_clipped,
1661                pipeline_mirror_mesh_cutout_clipped,
1662
1663                pipeline_grid_mesh,
1664                pipeline_grid_mesh_transparent,
1665                pipeline_grid_mesh_transparent_clipped,
1666
1667                pipeline_emissive_toon_mesh,
1668                pipeline_emissive_toon_mesh_transparent,
1669                pipeline_emissive_toon_mesh_cutout,
1670                pipeline_emissive_toon_mesh_transparent_clipped,
1671                pipeline_emissive_toon_mesh_cutout_clipped,
1672                pipeline_emissive_prepass_toon_mesh,
1673                pipeline_emissive_prepass_toon_mesh_cutout,
1674                pipeline_emissive_prepass_depth_write_toon_mesh,
1675
1676                pipeline_skinned_toon_mesh,
1677                pipeline_skinned_toon_mesh_transparent,
1678                pipeline_skinned_toon_mesh_cutout,
1679
1680                pipeline_skinned_emissive_toon_mesh,
1681                pipeline_skinned_emissive_toon_mesh_transparent,
1682                pipeline_skinned_emissive_toon_mesh_cutout,
1683                pipeline_skinned_emissive_prepass_toon_mesh,
1684                pipeline_skinned_emissive_prepass_toon_mesh_cutout,
1685                pipeline_skinned_emissive_prepass_depth_write_toon_mesh,
1686
1687                pipeline_stencil_incr,
1688                pipeline_stencil_decr,
1689                pipeline_overlay_clipped,
1690                pipeline_emissive_overlay_clipped,
1691                pipeline_opaque_clipped,
1692                pipeline_emissive_opaque_clipped,
1693
1694                msaa_samples,
1695
1696                cached_instance_buffer: None,
1697                cached_instance_count: 0,
1698
1699                cached_background_instance_buffer: None,
1700                cached_background_instance_count: 0,
1701
1702                cached_background_occluded_lit_instance_buffer: None,
1703                cached_background_occluded_lit_instance_count: 0,
1704
1705                cached_cutout_instance_buffer: None,
1706                cached_cutout_instance_count: 0,
1707
1708                cached_overlay_instance_buffer: None,
1709                cached_overlay_instance_count: 0,
1710                cached_material_sets: HashMap::new(),
1711                pending_runtime_texture_updates: HashMap::new(),
1712                window_runtime_debug_targets: None,
1713
1714                cached_bones_buffers: Vec::new(),
1715                cached_bones_slot_valid: Vec::new(),
1716                cached_bones_capacity: 0,
1717
1718                xr_offscreen: None,
1719                mirror_offscreen: HashMap::new(),
1720
1721                window_resized: false,
1722                recreate_swapchain: false,
1723                images_in_flight: (0..framebuffer_count).map(|_| None).collect(),
1724            };
1725
1726            // Default texture: 1x1 white so untextured materials can still bind a sampler.
1727            state.upload_texture_rgba8(TextureHandle(0), &[255, 255, 255, 255], 1, 1)?;
1728
1729            Ok(state)
1730        }
1731
1732        pub fn window_color_format(&self) -> Format {
1733            self.swapchain_state.swapchain.image_format()
1734        }
1735
1736        fn ensure_xr_offscreen_targets(
1737            &mut self,
1738            view_count: usize,
1739            extent: [u32; 2],
1740        ) -> Result<(), Box<dyn std::error::Error>> {
1741            let color_format = self.swapchain_state.swapchain.image_format();
1742
1743            let needs_recreate = self.xr_offscreen.as_ref().is_none_or(|t| {
1744                t.extent != extent
1745                    || t.color_format != color_format
1746                    || t.color_views.len() != view_count
1747                    || (self.msaa_samples == SampleCount::Sample1) != t.msaa_color_views.is_empty()
1748                    || (self.msaa_samples != SampleCount::Sample1
1749                        && t.msaa_color_views.len() != view_count)
1750            });
1751
1752            if !needs_recreate {
1753                return Ok(());
1754            }
1755
1756            let memory_allocator = self.context.memory_allocator().clone();
1757
1758            let mut color_images = Vec::with_capacity(view_count);
1759            let mut msaa_color_views = Vec::with_capacity(view_count);
1760            let mut color_views = Vec::with_capacity(view_count);
1761            let mut depth_views = Vec::with_capacity(view_count);
1762
1763            for _ in 0..view_count {
1764                // Resolve target (single-sampled): used for transfer/copy out.
1765                let color_image = vulkano::image::Image::new(
1766                    memory_allocator.clone(),
1767                    vulkano::image::ImageCreateInfo {
1768                        image_type: vulkano::image::ImageType::Dim2d,
1769                        format: color_format,
1770                        extent: [extent[0], extent[1], 1],
1771                        samples: SampleCount::Sample1,
1772                        // OpenXR copies from these images into the XR swapchain target, so keep
1773                        // them usable for both transfer and sampling.
1774                        usage: vulkano::image::ImageUsage::COLOR_ATTACHMENT
1775                            | vulkano::image::ImageUsage::SAMPLED
1776                            | vulkano::image::ImageUsage::TRANSFER_SRC,
1777                        ..Default::default()
1778                    },
1779                    AllocationCreateInfo {
1780                        memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
1781                        ..Default::default()
1782                    },
1783                )?;
1784
1785                let color_view = ImageView::new_default(color_image.clone())
1786                    .map_err(|e| -> Box<dyn std::error::Error> { format!("{e:?}").into() })?;
1787
1788                // Multisampled color attachment (optional): resolved into `color_view`.
1789                if self.msaa_samples != SampleCount::Sample1 {
1790                    let msaa_color_image = vulkano::image::Image::new(
1791                        memory_allocator.clone(),
1792                        vulkano::image::ImageCreateInfo {
1793                            image_type: vulkano::image::ImageType::Dim2d,
1794                            format: color_format,
1795                            extent: [extent[0], extent[1], 1],
1796                            samples: self.msaa_samples,
1797                            usage: vulkano::image::ImageUsage::COLOR_ATTACHMENT
1798                                | vulkano::image::ImageUsage::TRANSIENT_ATTACHMENT,
1799                            ..Default::default()
1800                        },
1801                        AllocationCreateInfo {
1802                            memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
1803                            ..Default::default()
1804                        },
1805                    )?;
1806
1807                    let msaa_color_view = ImageView::new_default(msaa_color_image)
1808                        .map_err(|e| -> Box<dyn std::error::Error> { format!("{e:?}").into() })?;
1809                    msaa_color_views.push(msaa_color_view);
1810                }
1811
1812                let depth_image = vulkano::image::Image::new(
1813                    memory_allocator.clone(),
1814                    vulkano::image::ImageCreateInfo {
1815                        image_type: vulkano::image::ImageType::Dim2d,
1816                        format: VulkanoSwapchainState::DEPTH_FORMAT,
1817                        extent: [extent[0], extent[1], 1],
1818                        samples: self.msaa_samples,
1819                        usage: vulkano::image::ImageUsage::DEPTH_STENCIL_ATTACHMENT,
1820                        ..Default::default()
1821                    },
1822                    AllocationCreateInfo {
1823                        memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
1824                        ..Default::default()
1825                    },
1826                )?;
1827
1828                let depth_view = ImageView::new(
1829                    depth_image.clone(),
1830                    ImageViewCreateInfo {
1831                        subresource_range: ImageSubresourceRange {
1832                            aspects: ImageAspects::DEPTH | ImageAspects::STENCIL,
1833                            ..depth_image.subresource_range()
1834                        },
1835                        ..ImageViewCreateInfo::from_image(&depth_image)
1836                    },
1837                )
1838                .map_err(|e| -> Box<dyn std::error::Error> { format!("{e:?}").into() })?;
1839
1840                color_images.push(color_image);
1841                color_views.push(color_view);
1842                depth_views.push(depth_view);
1843            }
1844
1845            self.xr_offscreen = Some(XrOffscreenTargets {
1846                extent,
1847                color_format,
1848                color_images,
1849                msaa_color_views,
1850                color_views,
1851                depth_views,
1852            });
1853
1854            Ok(())
1855        }
1856
1857        pub fn ensure_mirror_offscreen_targets(
1858            &mut self,
1859            mirror_key: &str,
1860            view_count: usize,
1861            extent: [u32; 2],
1862            color_format: Format,
1863        ) -> Result<&MirrorOffscreenTargets, Box<dyn std::error::Error>> {
1864            let needs_recreate = self
1865                .mirror_offscreen
1866                .get(mirror_key)
1867                .map_or(true, |targets| {
1868                    targets.extent != extent
1869                        || targets.color_format != color_format
1870                        || targets.color_images.len() != view_count
1871                });
1872
1873            if needs_recreate {
1874                let mut color_images = Vec::new();
1875                let mut msaa_color_views = Vec::new();
1876                let mut color_views = Vec::new();
1877                let mut depth_views = Vec::new();
1878
1879                let memory_allocator = self.context.memory_allocator().clone();
1880                for _ in 0..view_count {
1881                    let color_image = Image::new(
1882                        memory_allocator.clone(),
1883                        ImageCreateInfo {
1884                            image_type: ImageType::Dim2d,
1885                            format: color_format,
1886                            extent: [extent[0], extent[1], 1],
1887                            samples: SampleCount::Sample1,
1888                            usage: ImageUsage::COLOR_ATTACHMENT
1889                                | ImageUsage::SAMPLED
1890                                | ImageUsage::TRANSFER_SRC,
1891                            ..Default::default()
1892                        },
1893                        AllocationCreateInfo {
1894                            memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
1895                            ..Default::default()
1896                        },
1897                    )?;
1898
1899                    let color_view = ImageView::new_default(color_image.clone())
1900                        .map_err(|e| -> Box<dyn std::error::Error> { format!("{e:?}").into() })?;
1901
1902                    if self.msaa_samples != SampleCount::Sample1 {
1903                        let msaa_color_image = Image::new(
1904                            memory_allocator.clone(),
1905                            ImageCreateInfo {
1906                                image_type: ImageType::Dim2d,
1907                                format: color_format,
1908                                extent: [extent[0], extent[1], 1],
1909                                samples: self.msaa_samples,
1910                                usage: ImageUsage::COLOR_ATTACHMENT
1911                                    | ImageUsage::TRANSIENT_ATTACHMENT,
1912                                ..Default::default()
1913                            },
1914                            AllocationCreateInfo {
1915                                memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
1916                                ..Default::default()
1917                            },
1918                        )?;
1919
1920                        let msaa_color_view = ImageView::new_default(msaa_color_image).map_err(
1921                            |e| -> Box<dyn std::error::Error> { format!("{e:?}").into() },
1922                        )?;
1923                        msaa_color_views.push(msaa_color_view);
1924                    }
1925
1926                    let depth_image = Image::new(
1927                        memory_allocator.clone(),
1928                        ImageCreateInfo {
1929                            image_type: ImageType::Dim2d,
1930                            format: VulkanoSwapchainState::DEPTH_FORMAT,
1931                            extent: [extent[0], extent[1], 1],
1932                            samples: self.msaa_samples,
1933                            usage: ImageUsage::DEPTH_STENCIL_ATTACHMENT,
1934                            ..Default::default()
1935                        },
1936                        AllocationCreateInfo {
1937                            memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
1938                            ..Default::default()
1939                        },
1940                    )?;
1941
1942                    let depth_view = ImageView::new(
1943                        depth_image.clone(),
1944                        ImageViewCreateInfo {
1945                            subresource_range: ImageSubresourceRange {
1946                                aspects: ImageAspects::DEPTH | ImageAspects::STENCIL,
1947                                ..depth_image.subresource_range()
1948                            },
1949                            ..ImageViewCreateInfo::from_image(&depth_image)
1950                        },
1951                    )
1952                    .map_err(|e| -> Box<dyn std::error::Error> { format!("{e:?}").into() })?;
1953
1954                    color_images.push(color_image);
1955                    color_views.push(color_view);
1956                    depth_views.push(depth_view);
1957                }
1958
1959                self.mirror_offscreen.insert(
1960                    mirror_key.to_string(),
1961                    MirrorOffscreenTargets {
1962                        extent,
1963                        color_format,
1964                        color_images,
1965                        msaa_color_views,
1966                        color_views,
1967                        depth_views,
1968                    },
1969                );
1970            }
1971
1972            Ok(self.mirror_offscreen.get(mirror_key).unwrap())
1973        }
1974
1975        pub fn render_xr_eye_offscreen(
1976            &mut self,
1977            visual_world: &mut VisualWorld,
1978            eye: usize,
1979            extent: [u32; 2],
1980        ) -> Result<(), Box<dyn std::error::Error>> {
1981            if !self.pending_runtime_texture_updates.is_empty() {
1982                unsafe {
1983                    self.context.device().wait_idle().map_err(
1984                        |e| -> Box<dyn std::error::Error> {
1985                            format!("wait_idle failed before runtime texture swap: {e}").into()
1986                        },
1987                    )?;
1988                }
1989            }
1990            self.apply_pending_runtime_texture_updates();
1991
1992            // MVP: assume PRIMARY_STEREO (2 eyes).
1993            let view_count = 2;
1994            self.ensure_xr_offscreen_targets(view_count, extent)?;
1995            let color_format = self
1996                .xr_offscreen
1997                .as_ref()
1998                .ok_or("XR offscreen targets missing")?
1999                .color_format;
2000
2001            let post_process_config = visual_world.post_processing().clone();
2002            let post_process_active = post_process_config.is_active();
2003            if post_process_active {
2004                self.post_processing_renderer.ensure_xr_targets(
2005                    view_count,
2006                    extent,
2007                    color_format,
2008                    self.msaa_samples,
2009                    &post_process_config,
2010                )?;
2011            }
2012
2013            self.render_mirror_captures(visual_world, color_format)?;
2014
2015            let Some(targets) = self.xr_offscreen.as_ref() else {
2016                return Err("XR offscreen targets missing".into());
2017            };
2018
2019            let resolve_view = targets
2020                .color_views
2021                .get(eye)
2022                .ok_or("XR offscreen eye out of range")?
2023                .clone();
2024
2025            let post_process = if post_process_active {
2026                let pp_targets = self
2027                    .post_processing_renderer
2028                    .xr_frame_targets(eye)
2029                    .ok_or("missing XR post-processing targets")?
2030                    .clone();
2031                Some(PostProcessInvocation {
2032                    final_output_view: resolve_view.clone(),
2033                    final_color_format: targets.color_format,
2034                    config: post_process_config,
2035                    targets: pp_targets,
2036                })
2037            } else {
2038                None
2039            };
2040
2041            let (color_attachment_view, color_resolve_view, depth_view) =
2042                if let Some(post) = post_process.as_ref() {
2043                    (
2044                        post.targets
2045                            .main_msaa_color
2046                            .clone()
2047                            .unwrap_or_else(|| post.targets.main_color.clone()),
2048                        if post.targets.main_msaa_color.is_some() {
2049                            Some(post.targets.main_color.clone())
2050                        } else {
2051                            None
2052                        },
2053                        post.targets.depth.clone(),
2054                    )
2055                } else if self.msaa_samples != SampleCount::Sample1 {
2056                    let msaa_view = targets
2057                        .msaa_color_views
2058                        .get(eye)
2059                        .ok_or("XR MSAA color eye out of range")?
2060                        .clone();
2061                    let depth_view = targets
2062                        .depth_views
2063                        .get(eye)
2064                        .ok_or("XR depth eye out of range")?
2065                        .clone();
2066                    (msaa_view, Some(resolve_view.clone()), depth_view)
2067                } else {
2068                    let depth_view = targets
2069                        .depth_views
2070                        .get(eye)
2071                        .ok_or("XR depth eye out of range")?
2072                        .clone();
2073                    (resolve_view.clone(), None, depth_view)
2074                };
2075
2076            let xr_camera = visual_world
2077                .visual_camera(crate::engine::graphics::CameraTarget::Xr)
2078                .ok_or("missing XR camera")?;
2079            let eye_data = xr_camera
2080                .eyes
2081                .get(eye)
2082                .ok_or("XR camera eye out of range")?;
2083            let render_view = RenderView {
2084                view: eye_data.view,
2085                proj: eye_data.proj,
2086                viewport: [extent[0] as f32, extent[1] as f32],
2087                kind: RenderViewKind::XrEye { eye },
2088            };
2089
2090            let window_slots = self.swapchain_state.swapchain_views.len().max(1);
2091            let bones_slots_total = window_slots + view_count;
2092            let bones_slot = window_slots + eye;
2093
2094            let cb = self.build_draw_batches_command_buffer(
2095                visual_world,
2096                &render_view,
2097                bones_slot,
2098                bones_slots_total,
2099                color_attachment_view,
2100                color_resolve_view,
2101                depth_view,
2102                extent,
2103                post_process,
2104                None,
2105            )?;
2106
2107            let device = self.context.device().clone();
2108            let queue = self.context.graphics_queue().clone();
2109
2110            sync::now(device)
2111                .then_execute(queue, cb)?
2112                .then_signal_fence_and_flush()?
2113                .wait(None)?;
2114
2115            Ok(())
2116        }
2117
2118        fn render_mirror_captures(
2119            &mut self,
2120            visual_world: &mut VisualWorld,
2121            color_format: Format,
2122        ) -> Result<(), Box<dyn std::error::Error>> {
2123            let device = self.context.device().clone();
2124            let queue = self.context.graphics_queue().clone();
2125            let mirrors = visual_world.mirrors().to_vec();
2126            let msaa_samples = self.msaa_samples;
2127
2128            for mirror in mirrors {
2129                if mirror.captures.is_empty() {
2130                    continue;
2131                }
2132                let capture_count = mirror.captures.len();
2133
2134                let aspect = mirror.aspect_ratio.max(1e-6);
2135                let base_extent = (1024.0 * mirror.resolution_scale).max(1.0).floor() as u32;
2136                let mirror_extent = if aspect >= 1.0 {
2137                    [
2138                        ((base_extent as f32) * aspect).max(1.0).floor() as u32,
2139                        base_extent.max(1),
2140                    ]
2141                } else {
2142                    [
2143                        base_extent.max(1),
2144                        ((base_extent as f32) / aspect).max(1.0).floor() as u32,
2145                    ]
2146                };
2147                if mirror_extent[0] == 0 || mirror_extent[1] == 0 {
2148                    continue;
2149                }
2150
2151                let (color_views, msaa_color_views, depth_views) = {
2152                    let mirror_offscreen_key = mirror
2153                        .captures
2154                        .first()
2155                        .map(|capture| capture.target_key.as_str())
2156                        .ok_or("mirror capture key missing")?;
2157                    let targets = self.ensure_mirror_offscreen_targets(
2158                        mirror_offscreen_key,
2159                        capture_count,
2160                        mirror_extent,
2161                        color_format,
2162                    )?;
2163                    (
2164                        targets.color_views.clone(),
2165                        targets.msaa_color_views.clone(),
2166                        targets.depth_views.clone(),
2167                    )
2168                };
2169
2170                for (capture_slot, capture) in mirror.captures.iter().enumerate() {
2171                    let eye_data = &capture.camera;
2172
2173                    let render_view = RenderView {
2174                        view: eye_data.view,
2175                        proj: eye_data.proj,
2176                        viewport: [mirror_extent[0] as f32, mirror_extent[1] as f32],
2177                        kind: RenderViewKind::Mirror {
2178                            mirror_component: mirror.mirror_component,
2179                            family: capture.family,
2180                            view_index: capture.view_index,
2181                            plane_origin: mirror.plane_origin,
2182                            plane_normal: mirror.plane_normal,
2183                            excluded_instance: Some(mirror.source_instance),
2184                        },
2185                    };
2186
2187                    let (color_attachment_view, color_resolve_view) =
2188                        if msaa_samples != SampleCount::Sample1 {
2189                            (
2190                                msaa_color_views[capture_slot].clone(),
2191                                Some(color_views[capture_slot].clone()),
2192                            )
2193                        } else {
2194                            (color_views[capture_slot].clone(), None)
2195                        };
2196                    let depth_view = depth_views[capture_slot].clone();
2197
2198                    let runtime_texture_publication = visual_world
2199                        .runtime_texture_handle(&capture.target_key)
2200                        .map(|handle| {
2201                            let src_view = color_resolve_view
2202                                .clone()
2203                                .unwrap_or_else(|| color_attachment_view.clone());
2204                            (handle, src_view)
2205                        });
2206
2207                    let cb = self.build_draw_batches_command_buffer(
2208                        visual_world,
2209                        &render_view,
2210                        0,
2211                        1,
2212                        color_attachment_view,
2213                        color_resolve_view,
2214                        depth_view,
2215                        mirror_extent,
2216                        None,
2217                        runtime_texture_publication,
2218                    )?;
2219
2220                    sync::now(device.clone())
2221                        .then_execute(queue.clone(), cb)?
2222                        .then_signal_fence_and_flush()?
2223                        .wait(None)?;
2224                }
2225            }
2226
2227            Ok(())
2228        }
2229
2230        pub fn xr_offscreen_vk_image(&self, eye: usize) -> Option<ash::vk::Image> {
2231            let targets = self.xr_offscreen.as_ref()?;
2232            let img = targets.color_images.get(eye)?;
2233            Some(img.handle())
2234        }
2235
2236        pub fn upload_texture_rgba8(
2237            &mut self,
2238            handle: TextureHandle,
2239            rgba: &[u8],
2240            width: u32,
2241            height: u32,
2242        ) -> Result<(), Box<dyn std::error::Error>> {
2243            if self.textures.contains_key(&handle) {
2244                return Ok(());
2245            }
2246
2247            let view = vulkano_texture_upload::upload_texture_rgba8(
2248                &self.context,
2249                &self.command_buffer_allocator,
2250                rgba,
2251                width,
2252                height,
2253            )?;
2254
2255            self.textures.insert(
2256                handle,
2257                VulkanoGpuTexture {
2258                    view,
2259                    extent: [width, height],
2260                    format: Format::R8G8B8A8_UNORM,
2261                },
2262            );
2263            Ok(())
2264        }
2265
2266        pub fn upload_texture_bc7(
2267            &mut self,
2268            handle: TextureHandle,
2269            bc7_blocks: &[u8],
2270            width: u32,
2271            height: u32,
2272            srgb: bool,
2273        ) -> Result<(), Box<dyn std::error::Error>> {
2274            if self.textures.contains_key(&handle) {
2275                return Ok(());
2276            }
2277
2278            let view = vulkano_texture_upload::upload_texture_bc7(
2279                &self.context,
2280                &self.command_buffer_allocator,
2281                bc7_blocks,
2282                width,
2283                height,
2284                srgb,
2285            )?;
2286
2287            self.textures.insert(
2288                handle,
2289                VulkanoGpuTexture {
2290                    view,
2291                    extent: [width, height],
2292                    format: if srgb {
2293                        Format::BC7_SRGB_BLOCK
2294                    } else {
2295                        Format::BC7_UNORM_BLOCK
2296                    },
2297                },
2298            );
2299            Ok(())
2300        }
2301
2302        fn ensure_runtime_texture_target(
2303            &mut self,
2304            handle: TextureHandle,
2305            src_view: &Arc<ImageView>,
2306        ) -> Result<Arc<ImageView>, Box<dyn std::error::Error>> {
2307            let src_image = src_view.image().clone();
2308            let src_extent = src_image.extent();
2309            let extent = [src_extent[0], src_extent[1]];
2310            let format = src_image.format();
2311
2312            let image = Image::new(
2313                self.context.memory_allocator().clone(),
2314                ImageCreateInfo {
2315                    image_type: ImageType::Dim2d,
2316                    format,
2317                    extent: [extent[0], extent[1], 1],
2318                    usage: ImageUsage::TRANSFER_DST | ImageUsage::SAMPLED,
2319                    ..Default::default()
2320                },
2321                AllocationCreateInfo {
2322                    memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
2323                    ..Default::default()
2324                },
2325            )?;
2326
2327            let view = ImageView::new_default(image)
2328                .map_err(|e| -> Box<dyn std::error::Error> { format!("{e:?}").into() })?;
2329
2330            self.pending_runtime_texture_updates.insert(
2331                handle,
2332                VulkanoGpuTexture {
2333                    view: view.clone(),
2334                    extent,
2335                    format,
2336                },
2337            );
2338
2339            Ok(view)
2340        }
2341
2342        fn create_color_target_view(
2343            memory_allocator: Arc<StandardMemoryAllocator>,
2344            format: Format,
2345            extent: [u32; 2],
2346            samples: SampleCount,
2347            usage: ImageUsage,
2348        ) -> Result<Arc<ImageView>, Box<dyn std::error::Error>> {
2349            let image = Image::new(
2350                memory_allocator,
2351                ImageCreateInfo {
2352                    image_type: ImageType::Dim2d,
2353                    format,
2354                    extent: [extent[0], extent[1], 1],
2355                    samples,
2356                    usage,
2357                    ..Default::default()
2358                },
2359                AllocationCreateInfo {
2360                    memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
2361                    ..Default::default()
2362                },
2363            )?;
2364
2365            Ok(ImageView::new_default(image)?)
2366        }
2367
2368        fn ensure_window_runtime_debug_targets(
2369            &mut self,
2370            frame_count: usize,
2371            extent: [u32; 2],
2372            color_format: Format,
2373        ) -> Result<(), Box<dyn std::error::Error>> {
2374            let needs_recreate = self
2375                .window_runtime_debug_targets
2376                .as_ref()
2377                .is_none_or(|targets| {
2378                    targets.extent != extent
2379                        || targets.color_format != color_format
2380                        || targets.color_views.len() != frame_count
2381                        || (self.msaa_samples == SampleCount::Sample1)
2382                            != targets.msaa_color_views.is_empty()
2383                        || (self.msaa_samples != SampleCount::Sample1
2384                            && targets.msaa_color_views.len() != frame_count)
2385                });
2386
2387            if !needs_recreate {
2388                return Ok(());
2389            }
2390
2391            let mut msaa_color_views = Vec::with_capacity(frame_count);
2392            let mut color_views = Vec::with_capacity(frame_count);
2393            for _ in 0..frame_count {
2394                if self.msaa_samples != SampleCount::Sample1 {
2395                    msaa_color_views.push(Self::create_color_target_view(
2396                        self.context.memory_allocator().clone(),
2397                        color_format,
2398                        extent,
2399                        self.msaa_samples,
2400                        ImageUsage::COLOR_ATTACHMENT | ImageUsage::TRANSIENT_ATTACHMENT,
2401                    )?);
2402                }
2403
2404                color_views.push(Self::create_color_target_view(
2405                    self.context.memory_allocator().clone(),
2406                    color_format,
2407                    extent,
2408                    SampleCount::Sample1,
2409                    ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC,
2410                )?);
2411            }
2412
2413            self.window_runtime_debug_targets = Some(WindowRuntimeDebugTargets {
2414                extent,
2415                color_format,
2416                msaa_color_views,
2417                color_views,
2418            });
2419
2420            Ok(())
2421        }
2422
2423        fn build_stencil_clip_debug_batches(
2424            visual_world: &VisualWorld,
2425        ) -> Vec<crate::engine::graphics::visual_world::DrawBatch> {
2426            visual_world
2427                .stencil_clip_order()
2428                .iter()
2429                .enumerate()
2430                .filter_map(|(slot, &instance_index)| {
2431                    let instance = visual_world.instances().get(instance_index as usize)?;
2432                    Some(crate::engine::graphics::visual_world::DrawBatch {
2433                        material: crate::engine::graphics::MaterialHandle::UNLIT_MESH,
2434                        mesh: instance.renderable.mesh,
2435                        texture: None,
2436                        texture_filtering: TextureFiltering::Nearest,
2437                        quant_steps: 1.0,
2438                        stencil_ref: 0,
2439                        start: slot,
2440                        count: 1,
2441                    })
2442                })
2443                .collect()
2444        }
2445
2446        fn retarget_mirror_surface_textures_for_render_view(
2447            visual_world: &mut VisualWorld,
2448            kind: &RenderViewKind,
2449        ) {
2450            let (family, view_index) = match *kind {
2451                RenderViewKind::Window => (
2452                    crate::engine::graphics::visual_world::MirrorViewerFamily::Monoscopic,
2453                    0,
2454                ),
2455                RenderViewKind::XrEye { eye } => (
2456                    crate::engine::graphics::visual_world::MirrorViewerFamily::Stereoscopic,
2457                    eye,
2458                ),
2459                RenderViewKind::Mirror {
2460                    family, view_index, ..
2461                } => (family, view_index),
2462            };
2463
2464            let updates: Vec<_> = visual_world
2465                .mirrors()
2466                .iter()
2467                .filter_map(|mirror| {
2468                    let key = visual_world.mirror_texture_key_for_view(
2469                        mirror.mirror_component,
2470                        family,
2471                        view_index,
2472                    )?;
2473                    let handle = visual_world.runtime_texture_handle(key)?;
2474                    Some((mirror.source_instance, handle))
2475                })
2476                .collect();
2477
2478            for (instance, handle) in updates {
2479                let _ = visual_world.update_texture(instance, Some(handle));
2480            }
2481        }
2482
2483        fn hsv_debug_color_for_stencil_ref(stencil_ref: u8) -> [f32; 4] {
2484            if stencil_ref == 0 {
2485                return [0.08, 0.08, 0.08, 1.0];
2486            }
2487
2488            let hue_deg = (((stencil_ref - 1) as f32) * 100.0) % 360.0;
2489            let saturation = 0.9;
2490            let value = 1.0;
2491            let chroma = value * saturation;
2492            let hue_sector = hue_deg / 60.0;
2493            let x = chroma * (1.0 - ((hue_sector % 2.0) - 1.0).abs());
2494
2495            let (r1, g1, b1) = if hue_sector < 1.0 {
2496                (chroma, x, 0.0)
2497            } else if hue_sector < 2.0 {
2498                (x, chroma, 0.0)
2499            } else if hue_sector < 3.0 {
2500                (0.0, chroma, x)
2501            } else if hue_sector < 4.0 {
2502                (0.0, x, chroma)
2503            } else if hue_sector < 5.0 {
2504                (x, 0.0, chroma)
2505            } else {
2506                (chroma, 0.0, x)
2507            };
2508
2509            let match_value = value - chroma;
2510            [r1 + match_value, g1 + match_value, b1 + match_value, 1.0]
2511        }
2512
2513        fn build_stencil_clip_debug_instance_buffer(
2514            &self,
2515            visual_world: &VisualWorld,
2516            order: &[u32],
2517        ) -> Result<Option<Subbuffer<[InstanceData]>>, Box<dyn std::error::Error>> {
2518            if order.is_empty() {
2519                return Ok(None);
2520            }
2521
2522            let instances_ref = visual_world.instances();
2523            let instance_data_iter = order.iter().map(|&idx| {
2524                let inst = instances_ref[idx as usize];
2525                let m = inst.transform.model;
2526                InstanceData {
2527                    i_model_c0: m[0],
2528                    i_model_c1: m[1],
2529                    i_model_c2: m[2],
2530                    i_model_c3: m[3],
2531                    i_color: Self::hsv_debug_color_for_stencil_ref(
2532                        inst.stencil_ref.saturating_add(1),
2533                    ),
2534                    i_emissive: 1.0,
2535                    i_opacity: 1.0,
2536                    i_bones_base: inst.bones_base,
2537                    i_bones_count: inst.bones_count,
2538                }
2539            });
2540
2541            let buf: Subbuffer<[InstanceData]> = Buffer::from_iter(
2542                self.context.memory_allocator().clone(),
2543                BufferCreateInfo {
2544                    usage: BufferUsage::VERTEX_BUFFER,
2545                    ..Default::default()
2546                },
2547                AllocationCreateInfo {
2548                    memory_type_filter: MemoryTypeFilter::PREFER_HOST
2549                        | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
2550                    ..Default::default()
2551                },
2552                instance_data_iter,
2553            )?;
2554
2555            Ok(Some(buf))
2556        }
2557
2558        fn apply_pending_runtime_texture_updates(&mut self) {
2559            if self.pending_runtime_texture_updates.is_empty() {
2560                return;
2561            }
2562
2563            for (handle, texture) in self.pending_runtime_texture_updates.drain() {
2564                self.textures.insert(handle, texture);
2565                self.cached_material_sets
2566                    .retain(|(_, texture_handle, _, _), _| *texture_handle != handle);
2567            }
2568        }
2569
2570        fn collect_runtime_texture_publications(
2571            &self,
2572            visual_world: &VisualWorld,
2573            post_process: &PostProcessInvocation,
2574        ) -> Vec<(TextureHandle, Arc<ImageView>)> {
2575            let mut publications = Vec::new();
2576
2577            if let (Some(emissive_pass), Some(view)) = (
2578                post_process.config.emissive_pass.as_ref(),
2579                post_process.targets.bloom_source.clone(),
2580            ) {
2581                if let Some(key) = emissive_pass.output_texture.as_deref() {
2582                    if let Some(handle) = visual_world.runtime_texture_handle(key) {
2583                        publications.push((handle, view));
2584                    }
2585                }
2586            }
2587
2588            if let Some(bloom) = post_process.config.bloom.as_ref() {
2589                if let (Some(key), Some(view)) = (
2590                    bloom.output_texture.as_deref(),
2591                    post_process.targets.bloom_a.clone(),
2592                ) {
2593                    if let Some(handle) = visual_world.runtime_texture_handle(key) {
2594                        publications.push((handle, view));
2595                    }
2596                }
2597            }
2598
2599            publications
2600        }
2601
2602        fn recreate_swapchain_if_needed(&mut self) -> Result<(), Box<dyn std::error::Error>> {
2603            if !(self.window_resized || self.recreate_swapchain) {
2604                return Ok(());
2605            }
2606
2607            // Swapchain recreation can race with in-flight frames during rapid resize/fullscreen
2608            // transitions. Ensure the GPU is idle before we rebuild swapchain-dependent
2609            // resources (framebuffers/depth images).
2610            unsafe {
2611                self.context
2612                    .device()
2613                    .wait_idle()
2614                    .map_err(|e| -> Box<dyn std::error::Error> {
2615                        format!("wait_idle failed: {e}").into()
2616                    })?;
2617
2618                // IMPORTANT: Vulkano's internal resource tracking is tied to futures. If we drop
2619                // futures without telling Vulkano they've finished, it can permanently believe a
2620                // resource is still in use (even if the GPU is idle).
2621                for slot in self.images_in_flight.iter_mut() {
2622                    if let Some(mut fut) = slot.take() {
2623                        fut.signal_finished();
2624                        fut.cleanup_finished();
2625                    }
2626                }
2627            }
2628
2629            self.recreate_swapchain = false;
2630
2631            if let Err(e) = self.swapchain_state.recreate(&self.context, &self.window) {
2632                self.recreate_swapchain = true;
2633                println!("[VulkanoRenderer] failed to recreate swapchain: {}", e);
2634                return Ok(());
2635            }
2636
2637            // After swapchain recreation, all old swapchain images/depth attachments are gone.
2638            // Reset per-image in-flight tracking.
2639            self.images_in_flight = (0..self.swapchain_state.swapchain_views.len())
2640                .map(|_| None)
2641                .collect();
2642
2643            // Swapchain image count may have changed; rebuild per-slot bones buffers lazily.
2644            self.cached_bones_buffers.clear();
2645            self.cached_bones_slot_valid.clear();
2646            self.cached_bones_capacity = 0;
2647
2648            self.window_resized = false;
2649            Ok(())
2650        }
2651
2652        fn build_instance_buffer_for_order_or_dummy(
2653            &self,
2654            visual_world: &VisualWorld,
2655            order: &[u32],
2656        ) -> Result<Subbuffer<[InstanceData]>, Box<dyn std::error::Error>> {
2657            static DID_LOG_SKIN_INSTANCE_RANGES: AtomicBool = AtomicBool::new(false);
2658
2659            if !order.is_empty() && env_flag("CAT_DEBUG_SKIN_INSTANCE_RANGES") {
2660                let instances_ref = visual_world.instances();
2661                let skinned_count = order
2662                    .iter()
2663                    .filter(|&&idx| instances_ref[idx as usize].bones_count > 0)
2664                    .count();
2665
2666                if skinned_count > 0 && !DID_LOG_SKIN_INSTANCE_RANGES.swap(true, Ordering::Relaxed)
2667                {
2668                    let mut skinned = Vec::new();
2669                    for &idx in order.iter() {
2670                        let inst = instances_ref[idx as usize];
2671                        if inst.bones_count > 0 {
2672                            skinned.push((idx, inst.renderable, inst.bones_base, inst.bones_count));
2673                            if skinned.len() >= 24 {
2674                                break;
2675                            }
2676                        }
2677                    }
2678
2679                    let total = order.len();
2680                    println!(
2681                        "[VulkanoRenderer] instances: total={} with_bones={} (showing up to {})",
2682                        total,
2683                        skinned_count,
2684                        skinned.len()
2685                    );
2686                    for (i, (idx, renderable, base, count)) in skinned.iter().enumerate() {
2687                        println!(
2688                            "  skinned[{i:02}] instance_index={idx} renderable={renderable:?} bones_base={base} bones_count={count}"
2689                        );
2690                    }
2691                }
2692            }
2693
2694            // `Buffer::from_iter` with an empty iterator can panic inside Vulkano.
2695            let buf: Subbuffer<[InstanceData]> = if order.is_empty() {
2696                Buffer::from_iter(
2697                    self.context.memory_allocator().clone(),
2698                    BufferCreateInfo {
2699                        usage: BufferUsage::VERTEX_BUFFER,
2700                        ..Default::default()
2701                    },
2702                    AllocationCreateInfo {
2703                        memory_type_filter: MemoryTypeFilter::PREFER_HOST
2704                            | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
2705                        ..Default::default()
2706                    },
2707                    std::iter::once(InstanceData::default()),
2708                )?
2709            } else {
2710                let instances_ref = visual_world.instances();
2711                let instance_data_iter = order.iter().map(|&idx| {
2712                    let inst = instances_ref[idx as usize];
2713                    let m = inst.transform.model;
2714                    InstanceData {
2715                        i_model_c0: m[0],
2716                        i_model_c1: m[1],
2717                        i_model_c2: m[2],
2718                        i_model_c3: m[3],
2719                        i_color: inst.color,
2720                        i_emissive: inst.emissive,
2721                        i_opacity: inst.opacity,
2722                        i_bones_base: inst.bones_base,
2723                        i_bones_count: inst.bones_count,
2724                    }
2725                });
2726
2727                Buffer::from_iter(
2728                    self.context.memory_allocator().clone(),
2729                    BufferCreateInfo {
2730                        usage: BufferUsage::VERTEX_BUFFER,
2731                        ..Default::default()
2732                    },
2733                    AllocationCreateInfo {
2734                        memory_type_filter: MemoryTypeFilter::PREFER_HOST
2735                            | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
2736                        ..Default::default()
2737                    },
2738                    instance_data_iter,
2739                )?
2740            };
2741
2742            Ok(buf)
2743        }
2744
2745        fn build_instance_buffer_for_order_opt(
2746            &self,
2747            visual_world: &VisualWorld,
2748            order: &[u32],
2749        ) -> Result<Option<Subbuffer<[InstanceData]>>, Box<dyn std::error::Error>> {
2750            if order.is_empty() {
2751                return Ok(None);
2752            }
2753
2754            let instances_ref = visual_world.instances();
2755            let instance_data_iter = order.iter().map(|&idx| {
2756                let inst = instances_ref[idx as usize];
2757                let m = inst.transform.model;
2758                InstanceData {
2759                    i_model_c0: m[0],
2760                    i_model_c1: m[1],
2761                    i_model_c2: m[2],
2762                    i_model_c3: m[3],
2763                    i_color: inst.color,
2764                    i_emissive: inst.emissive,
2765                    i_opacity: inst.opacity,
2766                    i_bones_base: inst.bones_base,
2767                    i_bones_count: inst.bones_count,
2768                }
2769            });
2770
2771            let buf: Subbuffer<[InstanceData]> = Buffer::from_iter(
2772                self.context.memory_allocator().clone(),
2773                BufferCreateInfo {
2774                    usage: BufferUsage::VERTEX_BUFFER,
2775                    ..Default::default()
2776                },
2777                AllocationCreateInfo {
2778                    memory_type_filter: MemoryTypeFilter::PREFER_HOST
2779                        | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
2780                    ..Default::default()
2781                },
2782                instance_data_iter,
2783            )?;
2784
2785            Ok(Some(buf))
2786        }
2787
2788        fn get_or_create_material_set(
2789            &mut self,
2790            material: crate::engine::graphics::MaterialHandle,
2791            texture_handle: TextureHandle,
2792            filtering: TextureFiltering,
2793            quant_steps: f32,
2794        ) -> Result<Option<Arc<DescriptorSet>>, Box<dyn std::error::Error>> {
2795            match material {
2796                crate::engine::graphics::MaterialHandle::TOON_MESH
2797                | crate::engine::graphics::MaterialHandle::UNLIT_MESH
2798                | crate::engine::graphics::MaterialHandle::SKINNED_TOON_MESH
2799                | crate::engine::graphics::MaterialHandle::EMISSIVE_TOON_MESH
2800                | crate::engine::graphics::MaterialHandle::SKINNED_EMISSIVE_TOON_MESH
2801                | crate::engine::graphics::MaterialHandle::GRID_MESH
2802                | crate::engine::graphics::MaterialHandle::MIRROR => {}
2803                _ => return Ok(None),
2804            }
2805
2806            let Some(tex) = self.textures.get(&texture_handle) else {
2807                return Ok(None);
2808            };
2809
2810            let quant_bits = quant_steps.to_bits();
2811            let material_key = (material, texture_handle, filtering, quant_bits);
2812            if let Some(set) = self.cached_material_sets.get(&material_key) {
2813                return Ok(Some(set.clone()));
2814            }
2815
2816            let material_ubo = Self::create_material_ubo(material, quant_steps);
2817            let material_buffer: Subbuffer<MaterialUBO> = Buffer::from_data(
2818                self.context.memory_allocator().clone(),
2819                BufferCreateInfo {
2820                    usage: BufferUsage::UNIFORM_BUFFER,
2821                    ..Default::default()
2822                },
2823                AllocationCreateInfo {
2824                    memory_type_filter: MemoryTypeFilter::PREFER_HOST
2825                        | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
2826                    ..Default::default()
2827                },
2828                material_ubo,
2829            )?;
2830
2831            let sampler = self.sampler_for(filtering).clone();
2832            let set = DescriptorSet::new(
2833                self.descriptor_set_allocator.clone(),
2834                self.set_layouts.material.clone(),
2835                [
2836                    WriteDescriptorSet::buffer(0, material_buffer),
2837                    WriteDescriptorSet::image_view_sampler(1, tex.view.clone(), sampler),
2838                ],
2839                [],
2840            )?;
2841
2842            self.cached_material_sets.insert(material_key, set.clone());
2843            Ok(Some(set))
2844        }
2845
2846        fn build_draw_batches_command_buffer(
2847            &mut self,
2848            visual_world: &mut VisualWorld,
2849            render_view: &RenderView,
2850            bones_slot: usize,
2851            bones_slots_total: usize,
2852            color_attachment_view: Arc<ImageView>,
2853            color_resolve_view: Option<Arc<ImageView>>,
2854            depth_view: Arc<ImageView>,
2855            extent: [u32; 2],
2856            post_process: Option<PostProcessInvocation>,
2857            runtime_texture_publication: Option<(TextureHandle, Arc<ImageView>)>,
2858        ) -> Result<
2859            Arc<vulkano::command_buffer::PrimaryAutoCommandBuffer>,
2860            Box<dyn std::error::Error>,
2861        > {
2862            Self::retarget_mirror_surface_textures_for_render_view(visual_world, &render_view.kind);
2863
2864            let (camera_target, eye) = match render_view.kind {
2865                RenderViewKind::Window => (crate::engine::graphics::CameraTarget::Window, 0),
2866                RenderViewKind::XrEye { eye } => (crate::engine::graphics::CameraTarget::Xr, eye),
2867                RenderViewKind::Mirror {
2868                    family, view_index, ..
2869                } => match family {
2870                    crate::engine::graphics::visual_world::MirrorViewerFamily::Monoscopic => {
2871                        (crate::engine::graphics::CameraTarget::Window, view_index)
2872                    }
2873                    crate::engine::graphics::visual_world::MirrorViewerFamily::Stereoscopic => {
2874                        (crate::engine::graphics::CameraTarget::Xr, view_index)
2875                    }
2876                },
2877            };
2878            let excluded_instance = match &render_view.kind {
2879                RenderViewKind::Mirror {
2880                    excluded_instance, ..
2881                } => *excluded_instance,
2882                _ => None,
2883            };
2884
2885            let queue = self.context.graphics_queue().clone();
2886
2887            // Always rebuild draw cache cheaply.
2888            let draw_cache_rebuilt = visual_world.prepare_draw_cache();
2889
2890            // Multi-layer transparency sort is always per-view.
2891            visual_world.prepare_transparent_multi_draw_cache_for_view(render_view.view);
2892
2893            // Consume dirty flags so they reflect "changed since last render".
2894            // For multi-eye (XR) rendering, only consume on the first eye.
2895            let instance_data_dirty = if eye == 0 {
2896                visual_world.take_instance_data_dirty()
2897            } else {
2898                visual_world.instance_data_dirty()
2899            };
2900
2901            // Only consume the bones palette dirty flag on the first eye.
2902            let bones_palette_dirty = if eye == 0 {
2903                visual_world.take_bones_palette_dirty()
2904            } else {
2905                false
2906            };
2907
2908            // --- Opaque pass ---
2909            // Buffer indexed by opaque_stream().1; use its length for cache invalidation.
2910            let (opaque_ops, opaque_instances) = if excluded_instance.is_some() {
2911                visual_world.opaque_stream_excluding(excluded_instance)
2912            } else {
2913                let (ops, instances) = visual_world.opaque_stream();
2914                (ops.to_vec(), instances.to_vec())
2915            };
2916            let instance_count = opaque_instances.len();
2917
2918            // --- Background pass ---
2919            // Background instances are stored in their own draw order/batches.
2920            let background_instance_count = visual_world.background_order().len();
2921            let background_occluded_lit_instance_count =
2922                visual_world.background_occluded_lit_order().len();
2923            let any_background =
2924                background_instance_count > 0 || background_occluded_lit_instance_count > 0;
2925
2926            // --- Cutout pass ---
2927            let (cutout_ops, cutout_instances) = if excluded_instance.is_some() {
2928                visual_world.cutout_stream_excluding(excluded_instance)
2929            } else {
2930                let (ops, instances) = visual_world.cutout_stream();
2931                (ops.to_vec(), instances.to_vec())
2932            };
2933            let cutout_instance_count = cutout_instances.len();
2934
2935            // --- Overlay pass ---
2936            // Buffer indexed by overlay_stream().1; use its length for cache invalidation.
2937            let (overlay_ops, overlay_instances) = if excluded_instance.is_some() {
2938                visual_world.overlay_stream_excluding(excluded_instance)
2939            } else {
2940                let (ops, instances) = visual_world.overlay_stream();
2941                (ops.to_vec(), instances.to_vec())
2942            };
2943            let overlay_instance_count = overlay_instances.len();
2944
2945            let stencil_clip_debug_requested = camera_target
2946                == crate::engine::graphics::CameraTarget::Window
2947                && eye == 0
2948                && visual_world.stencil_clip_debug_requested();
2949            let stencil_clip_debug_handle = if stencil_clip_debug_requested {
2950                visual_world.runtime_texture_handle(INTERNAL_RENDERER_STENCIL_CLIP_DEBUG_SELECTOR)
2951            } else {
2952                None
2953            };
2954            let stencil_clip_debug_enabled =
2955                stencil_clip_debug_requested && stencil_clip_debug_handle.is_some();
2956            let stencil_clip_debug_batches = if stencil_clip_debug_enabled {
2957                Self::build_stencil_clip_debug_batches(visual_world)
2958            } else {
2959                Vec::new()
2960            };
2961            let stencil_clip_debug_instance_count = stencil_clip_debug_batches.len();
2962
2963            // --- Emissive-only post-process source passes ---
2964            let emissive_instance_count = visual_world.emissive_draw_order().len();
2965            let emissive_cutout_instance_count = visual_world.emissive_cutout_order().len();
2966
2967            let need_instance_buffer = instance_data_dirty
2968                || draw_cache_rebuilt
2969                || self.cached_instance_buffer.is_none()
2970                || self.cached_instance_count != instance_count;
2971
2972            // Opaque instance buffer indexed by opaque_stream().1 (not draw_order) so the
2973            // stream's batch.start offsets address the correct slot in the buffer.
2974            let instance_buffer: Subbuffer<[InstanceData]> = if !need_instance_buffer {
2975                self.cached_instance_buffer
2976                    .as_ref()
2977                    .expect("cached_instance_buffer")
2978                    .clone()
2979            } else {
2980                let buf = self
2981                    .build_instance_buffer_for_order_or_dummy(&*visual_world, &opaque_instances)?;
2982
2983                self.cached_instance_count = instance_count;
2984                self.cached_instance_buffer = Some(buf.clone());
2985                buf
2986            };
2987
2988            let need_background_instance_buffer = instance_data_dirty
2989                || draw_cache_rebuilt
2990                || self.cached_background_instance_count != background_instance_count;
2991            let background_instance_buffer = if !need_background_instance_buffer {
2992                self.cached_background_instance_buffer.clone()
2993            } else {
2994                let buf = self.build_instance_buffer_for_order_opt(
2995                    &*visual_world,
2996                    visual_world.background_order(),
2997                )?;
2998                self.cached_background_instance_count = background_instance_count;
2999                self.cached_background_instance_buffer = buf.clone();
3000                buf
3001            };
3002
3003            let need_background_occluded_lit_instance_buffer = instance_data_dirty
3004                || draw_cache_rebuilt
3005                || self.cached_background_occluded_lit_instance_count
3006                    != background_occluded_lit_instance_count;
3007            let background_occluded_lit_instance_buffer =
3008                if !need_background_occluded_lit_instance_buffer {
3009                    self.cached_background_occluded_lit_instance_buffer.clone()
3010                } else {
3011                    let buf = self.build_instance_buffer_for_order_opt(
3012                        &*visual_world,
3013                        visual_world.background_occluded_lit_order(),
3014                    )?;
3015                    self.cached_background_occluded_lit_instance_count =
3016                        background_occluded_lit_instance_count;
3017                    self.cached_background_occluded_lit_instance_buffer = buf.clone();
3018                    buf
3019                };
3020
3021            let need_cutout_instance_buffer = instance_data_dirty
3022                || draw_cache_rebuilt
3023                || self.cached_cutout_instance_count != cutout_instance_count;
3024            let cutout_instance_buffer = if !need_cutout_instance_buffer {
3025                self.cached_cutout_instance_buffer.clone()
3026            } else {
3027                let buf =
3028                    self.build_instance_buffer_for_order_opt(&*visual_world, &cutout_instances)?;
3029                self.cached_cutout_instance_count = cutout_instance_count;
3030                self.cached_cutout_instance_buffer = buf.clone();
3031                buf
3032            };
3033
3034            let need_overlay_instance_buffer = instance_data_dirty
3035                || draw_cache_rebuilt
3036                || self.cached_overlay_instance_count != overlay_instance_count;
3037            // Overlay buffer indexed by overlay_stream().1 so batch.start offsets are correct.
3038            let overlay_instance_buffer = if !need_overlay_instance_buffer {
3039                self.cached_overlay_instance_buffer.clone()
3040            } else {
3041                let buf =
3042                    self.build_instance_buffer_for_order_opt(&*visual_world, &overlay_instances)?;
3043                self.cached_overlay_instance_count = overlay_instance_count;
3044                self.cached_overlay_instance_buffer = buf.clone();
3045                buf
3046            };
3047
3048            let stencil_clip_debug_instance_buffer = if stencil_clip_debug_enabled {
3049                self.build_stencil_clip_debug_instance_buffer(
3050                    &*visual_world,
3051                    visual_world.stencil_clip_order(),
3052                )?
3053            } else {
3054                None
3055            };
3056            let emissive_instance_buffer = self.build_instance_buffer_for_order_opt(
3057                &*visual_world,
3058                visual_world.emissive_draw_order(),
3059            )?;
3060            let background_occluded_lit_emissive_instance_count =
3061                visual_world.background_occluded_lit_emissive_order().len();
3062            let background_occluded_lit_emissive_instance_buffer =
3063                if background_occluded_lit_emissive_instance_count > 0 {
3064                    self.build_instance_buffer_for_order_opt(
3065                        &*visual_world,
3066                        visual_world.background_occluded_lit_emissive_order(),
3067                    )?
3068                } else {
3069                    None
3070                };
3071
3072            let emissive_cutout_instance_buffer = self.build_instance_buffer_for_order_opt(
3073                &*visual_world,
3074                visual_world.emissive_cutout_order(),
3075            )?;
3076
3077            let clear_color = visual_world.clear_color();
3078            let defer_overlay_until_before_final_composite =
3079                post_process.is_some() && overlay_instance_count > 0;
3080
3081            let mut color_attachment_clear = RenderingAttachmentInfo {
3082                load_op: AttachmentLoadOp::Clear,
3083                store_op: AttachmentStoreOp::Store,
3084                clear_value: Some(ClearValue::from(clear_color)),
3085                ..RenderingAttachmentInfo::image_view(color_attachment_view.clone())
3086            };
3087
3088            if let Some(resolve_view) = color_resolve_view.clone() {
3089                color_attachment_clear.resolve_info =
3090                    Some(RenderingAttachmentResolveInfo::image_view(resolve_view));
3091                // The multisampled attachment doesn't need to be stored when resolve is used,
3092                // except when post-process is active and we plan to reopen the scene color
3093                // attachment for a deferred overlay pass before final composite.
3094                color_attachment_clear.store_op = if defer_overlay_until_before_final_composite {
3095                    AttachmentStoreOp::Store
3096                } else {
3097                    AttachmentStoreOp::DontCare
3098                };
3099            }
3100
3101            let depth_attachment_clear = RenderingAttachmentInfo {
3102                load_op: AttachmentLoadOp::Clear,
3103                store_op: if post_process.is_some() {
3104                    AttachmentStoreOp::Store
3105                } else {
3106                    AttachmentStoreOp::DontCare
3107                },
3108                clear_value: Some(ClearValue::Depth(1.0)),
3109                ..RenderingAttachmentInfo::image_view(depth_view.clone())
3110            };
3111
3112            let rendering_info_clear_color_and_depth = RenderingInfo {
3113                render_area_offset: [0, 0],
3114                render_area_extent: [extent[0], extent[1]],
3115                layer_count: 1,
3116                color_attachments: vec![Some(color_attachment_clear)],
3117                depth_attachment: Some(depth_attachment_clear.clone()),
3118                stencil_attachment: Some(RenderingAttachmentInfo {
3119                    load_op: AttachmentLoadOp::Clear,
3120                    store_op: if defer_overlay_until_before_final_composite {
3121                        AttachmentStoreOp::Store
3122                    } else {
3123                        AttachmentStoreOp::DontCare
3124                    },
3125                    clear_value: Some(ClearValue::Stencil(0)),
3126                    ..RenderingAttachmentInfo::image_view(depth_view.clone())
3127                }),
3128                ..Default::default()
3129            };
3130
3131            // Engine convention: +Y is up in clip space.
3132            // Vulkan's default viewport maps NDC Y with opposite direction, so we flip the
3133            // viewport by using a negative height.
3134            let viewport = Viewport {
3135                offset: [0.0, extent[1] as f32],
3136                extent: [extent[0] as f32, -(extent[1] as f32)],
3137                depth_range: 0.0..=1.0,
3138                ..Default::default()
3139            };
3140
3141            // Camera uniform buffer (set=0, binding=0) for foreground.
3142            let camera_ubo_fg = CameraUBO {
3143                view: render_view.view,
3144                proj: render_view.proj,
3145                camera2d: visual_world.camera_2d(),
3146                viewport: [extent[0] as f32, extent[1] as f32],
3147                _pad0: [0.0, 0.0],
3148
3149                ambient_light: visual_world.ambient_light(),
3150                _pad1: 0.0,
3151            };
3152
3153            let camera_buffer_fg: Subbuffer<CameraUBO> = Buffer::from_data(
3154                self.context.memory_allocator().clone(),
3155                BufferCreateInfo {
3156                    usage: BufferUsage::UNIFORM_BUFFER,
3157                    ..Default::default()
3158                },
3159                AllocationCreateInfo {
3160                    memory_type_filter: MemoryTypeFilter::PREFER_HOST
3161                        | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
3162                    ..Default::default()
3163                },
3164                camera_ubo_fg,
3165            )?;
3166
3167            // Lights storage buffer (set=0, binding=1).
3168            let mut lights_ssbo = LightsSSBO::default();
3169            let lights = visual_world.point_lights();
3170            let count = (lights.len()).min(MAX_LIGHTS);
3171            lights_ssbo.count = count as u32;
3172            for (i, l) in lights.iter().take(count).enumerate() {
3173                let light_type = match l.light_type {
3174                    LIGHT_TYPE_POINT => LIGHT_TYPE_POINT,
3175                    LIGHT_TYPE_DIRECTIONAL => LIGHT_TYPE_DIRECTIONAL,
3176                    // Default to point for legacy/unknown values.
3177                    _ => LIGHT_TYPE_POINT,
3178                };
3179
3180                lights_ssbo.lights[i] = GpuLight {
3181                    pos_intensity: [
3182                        l.position_ws[0],
3183                        l.position_ws[1],
3184                        l.position_ws[2],
3185                        l.intensity,
3186                    ],
3187                    color_distance: [l.color[0], l.color[1], l.color[2], l.distance],
3188                    meta: [light_type, 0, 0, 0],
3189                };
3190            }
3191
3192            let lights_buffer: Subbuffer<LightsSSBO> = Buffer::from_data(
3193                self.context.memory_allocator().clone(),
3194                BufferCreateInfo {
3195                    usage: BufferUsage::STORAGE_BUFFER,
3196                    ..Default::default()
3197                },
3198                AllocationCreateInfo {
3199                    memory_type_filter: MemoryTypeFilter::PREFER_HOST
3200                        | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
3201                    ..Default::default()
3202                },
3203                lights_ssbo,
3204            )?;
3205
3206            // Global descriptor set (set=0): per-frame camera + lights.
3207            //
3208            // `global_set_fg` is the *foreground* variant used for normal scene rendering
3209            // (opaque + transparent passes). Its camera UBO uses the full view matrix,
3210            // so camera translation causes normal parallax.
3211            let global_set_fg = DescriptorSet::new(
3212                self.descriptor_set_allocator.clone(),
3213                self.set_layouts.global.clone(),
3214                [
3215                    WriteDescriptorSet::buffer(0, camera_buffer_fg),
3216                    WriteDescriptorSet::buffer(1, lights_buffer.clone()),
3217                ],
3218                [],
3219            )?;
3220
3221            // Background global set: same layout + lights, but view translation removed.
3222            //
3223            // This makes backgrounds behave like a skybox: they rotate with the camera but
3224            // do not appear to move when the camera translates.
3225            let global_set_bg: Option<Arc<DescriptorSet>> = if !any_background {
3226                None
3227            } else {
3228                let mut view_bg = render_view.view;
3229                view_bg[3] = [0.0, 0.0, 0.0, 1.0];
3230
3231                let camera_ubo_bg = CameraUBO {
3232                    view: view_bg,
3233                    proj: render_view.proj,
3234                    camera2d: visual_world.camera_2d(),
3235                    viewport: [extent[0] as f32, extent[1] as f32],
3236                    _pad0: [0.0, 0.0],
3237
3238                    ambient_light: visual_world.ambient_light(),
3239                    _pad1: 0.0,
3240                };
3241
3242                let camera_buffer_bg: Subbuffer<CameraUBO> = Buffer::from_data(
3243                    self.context.memory_allocator().clone(),
3244                    BufferCreateInfo {
3245                        usage: BufferUsage::UNIFORM_BUFFER,
3246                        ..Default::default()
3247                    },
3248                    AllocationCreateInfo {
3249                        memory_type_filter: MemoryTypeFilter::PREFER_HOST
3250                            | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
3251                        ..Default::default()
3252                    },
3253                    camera_ubo_bg,
3254                )?;
3255
3256                let set = DescriptorSet::new(
3257                    self.descriptor_set_allocator.clone(),
3258                    self.set_layouts.global.clone(),
3259                    [
3260                        WriteDescriptorSet::buffer(0, camera_buffer_bg),
3261                        WriteDescriptorSet::buffer(1, lights_buffer.clone()),
3262                    ],
3263                    [],
3264                )?;
3265                Some(set)
3266            };
3267
3268            // Rig descriptor set (set=2): shared bones palette + placeholder per-instance lighting.
3269            // Layout is defined in `PipelineDescriptorSetLayouts::rig`.
3270            let rig_set: Arc<DescriptorSet> = {
3271                static DID_LOG_BONES_PALETTE_UPLOAD: AtomicBool = AtomicBool::new(false);
3272
3273                let want_len = visual_world.bones_palette().len().max(1);
3274
3275                let want_slots = bones_slots_total.max(1);
3276                let slot = bones_slot.min(want_slots - 1);
3277
3278                let needs_realloc = self.cached_bones_buffers.len() != want_slots
3279                    || self.cached_bones_capacity < want_len;
3280
3281                if needs_realloc {
3282                    let new_cap = want_len.next_power_of_two().max(1);
3283
3284                    let mut buffers = Vec::with_capacity(want_slots);
3285                    for _ in 0..want_slots {
3286                        let buffer: Subbuffer<[GpuMat4]> = Buffer::new_slice(
3287                            self.context.memory_allocator().clone(),
3288                            BufferCreateInfo {
3289                                usage: BufferUsage::STORAGE_BUFFER,
3290                                ..Default::default()
3291                            },
3292                            AllocationCreateInfo {
3293                                memory_type_filter: MemoryTypeFilter::PREFER_HOST
3294                                    | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
3295                                ..Default::default()
3296                            },
3297                            new_cap as DeviceSize,
3298                        )?;
3299                        buffers.push(buffer);
3300                    }
3301
3302                    self.cached_bones_buffers = buffers;
3303                    self.cached_bones_slot_valid = vec![false; want_slots];
3304                    self.cached_bones_capacity = new_cap;
3305                }
3306
3307                if bones_palette_dirty {
3308                    for v in self.cached_bones_slot_valid.iter_mut() {
3309                        *v = false;
3310                    }
3311                }
3312
3313                let slot_needs_upload = !self
3314                    .cached_bones_slot_valid
3315                    .get(slot)
3316                    .copied()
3317                    .unwrap_or(false);
3318
3319                if slot_needs_upload {
3320                    let bones_src = visual_world.bones_palette();
3321
3322                    if env_flag("CAT_DEBUG_BONES_PALETTE")
3323                        && bones_src.len() > 1
3324                        && !DID_LOG_BONES_PALETTE_UPLOAD.swap(true, Ordering::Relaxed)
3325                    {
3326                        println!(
3327                            "[VulkanoRenderer] bones palette upload: dirty={} realloc={} want_len={} cached_cap={} src_len={}",
3328                            bones_palette_dirty,
3329                            needs_realloc,
3330                            want_len,
3331                            self.cached_bones_capacity,
3332                            bones_src.len()
3333                        );
3334
3335                        for (i, m) in bones_src.iter().take(3).enumerate() {
3336                            println!("  bone[{i:03}]={m:?}");
3337                        }
3338                    }
3339
3340                    let identity = [
3341                        [1.0, 0.0, 0.0, 0.0],
3342                        [0.0, 1.0, 0.0, 0.0],
3343                        [0.0, 0.0, 1.0, 0.0],
3344                        [0.0, 0.0, 0.0, 1.0],
3345                    ];
3346
3347                    let mut dst = self.cached_bones_buffers[slot].write()?;
3348
3349                    // Write current palette, then fill remainder with identity.
3350                    if bones_src.is_empty() {
3351                        dst[0] = GpuMat4 { cols: identity };
3352                        for slot in dst.iter_mut().skip(1) {
3353                            *slot = GpuMat4 { cols: identity };
3354                        }
3355                    } else {
3356                        for (i, m) in bones_src.iter().copied().enumerate() {
3357                            dst[i] = GpuMat4 { cols: m };
3358                        }
3359                        for slot in dst.iter_mut().skip(bones_src.len()) {
3360                            *slot = GpuMat4 { cols: identity };
3361                        }
3362                    }
3363
3364                    if let Some(v) = self.cached_bones_slot_valid.get_mut(slot) {
3365                        *v = true;
3366                    }
3367                }
3368
3369                let bones_buffer = self.cached_bones_buffers[slot].clone();
3370
3371                let per_instance_lighting_buffer: Subbuffer<DummyPerInstanceLightingSSBO> =
3372                    Buffer::from_data(
3373                        self.context.memory_allocator().clone(),
3374                        BufferCreateInfo {
3375                            usage: BufferUsage::STORAGE_BUFFER,
3376                            ..Default::default()
3377                        },
3378                        AllocationCreateInfo {
3379                            memory_type_filter: MemoryTypeFilter::PREFER_HOST
3380                                | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
3381                            ..Default::default()
3382                        },
3383                        DummyPerInstanceLightingSSBO::default(),
3384                    )?;
3385
3386                DescriptorSet::new(
3387                    self.descriptor_set_allocator.clone(),
3388                    self.set_layouts.rig.clone(),
3389                    [
3390                        WriteDescriptorSet::buffer(0, per_instance_lighting_buffer),
3391                        WriteDescriptorSet::buffer(1, bones_buffer),
3392                    ],
3393                    [],
3394                )?
3395            };
3396
3397            let mut cbb = AutoCommandBufferBuilder::primary(
3398                self.command_buffer_allocator.clone(),
3399                queue.queue_family_index(),
3400                CommandBufferUsage::OneTimeSubmit,
3401            )?;
3402
3403            // Single dynamic-rendering scope. This keeps MSAA resolve straightforward.
3404            cbb.begin_rendering(rendering_info_clear_color_and_depth)?;
3405
3406            cbb.set_viewport(0, vec![viewport.clone()].into())?;
3407            cbb.set_scissor(
3408                0,
3409                vec![Scissor {
3410                    offset: [0, 0],
3411                    extent: [extent[0], extent[1]],
3412                    ..Default::default()
3413                }]
3414                .into(),
3415            )?;
3416
3417            if any_background {
3418                // Background phase: draw:
3419                // 1) plain background (no depth write)
3420                // 2) occluded+lit background (depth write ON for self-occlusion)
3421                if let Some(global_set_bg) = global_set_bg.as_ref() {
3422                    if let Some(background_instance_buffer) = background_instance_buffer.as_ref() {
3423                        self.record_background_draws(
3424                            &mut cbb,
3425                            visual_world,
3426                            global_set_bg,
3427                            &rig_set,
3428                            background_instance_buffer,
3429                            background_instance_count,
3430                        )?;
3431                    }
3432
3433                    if let Some(background_occluded_lit_instance_buffer) =
3434                        background_occluded_lit_instance_buffer.as_ref()
3435                    {
3436                        self.record_background_occluded_lit_draws(
3437                            &mut cbb,
3438                            visual_world,
3439                            global_set_bg,
3440                            &rig_set,
3441                            background_occluded_lit_instance_buffer,
3442                            background_occluded_lit_instance_count,
3443                        )?;
3444                    }
3445                }
3446
3447                // Foreground phase: clear depth so background doesn't occlude.
3448                // NOTE: `clear_attachments` requires a bound graphics pipeline.
3449                cbb.bind_pipeline_graphics(self.pipeline_toon_mesh.clone())?;
3450                cbb.clear_attachments(
3451                    smallvec::smallvec![ClearAttachment::Depth(1.0)],
3452                    smallvec::smallvec![ClearRect {
3453                        offset: [0, 0],
3454                        extent: [extent[0], extent[1]],
3455                        array_layers: 0..1,
3456                    }],
3457                )?;
3458            }
3459
3460            self.record_opaque_draws(
3461                &mut cbb,
3462                visual_world,
3463                &global_set_fg,
3464                &rig_set,
3465                &instance_buffer,
3466                instance_count,
3467                Some((&opaque_ops, &opaque_instances)),
3468            )?;
3469
3470            if let Some(cutout_instance_buffer) = cutout_instance_buffer.as_ref() {
3471                self.record_cutout_draws(
3472                    &mut cbb,
3473                    visual_world,
3474                    &global_set_fg,
3475                    &rig_set,
3476                    cutout_instance_buffer,
3477                    cutout_instance_count,
3478                    Some((&cutout_ops, &cutout_instances)),
3479                )?;
3480            }
3481
3482            self.record_transparent_single_draws(
3483                &mut cbb,
3484                visual_world,
3485                &global_set_fg,
3486                &rig_set,
3487                eye,
3488                excluded_instance,
3489            )?;
3490
3491            self.record_transparent_multi_draws(
3492                &mut cbb,
3493                visual_world,
3494                &global_set_fg,
3495                &rig_set,
3496                camera_target,
3497                eye,
3498                excluded_instance,
3499            )?;
3500
3501            // Overlay phase: when post-process is disabled, clear depth here so overlay draws on
3502            // top of the scene immediately. When post-process is enabled, defer overlay until
3503            // after emissive extraction so opaque/cutout depth can occlude the emissive pass.
3504            if overlay_instance_count > 0 && !defer_overlay_until_before_final_composite {
3505                // NOTE: `clear_attachments` requires a bound graphics pipeline.
3506                cbb.bind_pipeline_graphics(self.pipeline_toon_mesh.clone())?;
3507                cbb.clear_attachments(
3508                    smallvec::smallvec![ClearAttachment::Depth(1.0)],
3509                    smallvec::smallvec![ClearRect {
3510                        offset: [0, 0],
3511                        extent: [extent[0], extent[1]],
3512                        array_layers: 0..1,
3513                    }],
3514                )?;
3515
3516                if let Some(overlay_instance_buffer) = overlay_instance_buffer.as_ref() {
3517                    self.record_overlay_draws(
3518                        &mut cbb,
3519                        visual_world,
3520                        &global_set_fg,
3521                        &rig_set,
3522                        overlay_instance_buffer,
3523                        overlay_instance_count,
3524                        Some((&overlay_ops, &overlay_instances)),
3525                    )?;
3526                }
3527            }
3528
3529            cbb.end_rendering()?;
3530
3531            if let Some((handle, src_view)) = runtime_texture_publication {
3532                let dst_view = self.ensure_runtime_texture_target(handle, &src_view)?;
3533                cbb.copy_image(CopyImageInfo::images(
3534                    src_view.image().clone(),
3535                    dst_view.image().clone(),
3536                ))?;
3537            }
3538
3539            if let Some(post_process) = post_process {
3540                let bloom_radius_pixels = post_process
3541                    .config
3542                    .effective_blur_radius_pixels(post_process.targets.bloom_extent[0]);
3543
3544                let mut blurred_bloom: Option<Arc<ImageView>> = None;
3545                if let (
3546                    Some(bloom_cfg),
3547                    Some(bloom_source),
3548                    Some(bloom_a),
3549                    Some(bloom_b),
3550                    Some(radius_pixels),
3551                ) = (
3552                    post_process.config.bloom.as_ref(),
3553                    post_process.targets.bloom_source.clone(),
3554                    post_process.targets.bloom_a.clone(),
3555                    post_process.targets.bloom_b.clone(),
3556                    bloom_radius_pixels,
3557                ) {
3558                    let has_foreground_emissive_content =
3559                        emissive_instance_count > 0 || emissive_cutout_instance_count > 0;
3560                    let has_background_occluded_lit_emissive_content =
3561                        background_occluded_lit_emissive_instance_count > 0;
3562
3563                    if has_foreground_emissive_content
3564                        || has_background_occluded_lit_emissive_content
3565                    {
3566                        let begin_bloom_extraction =
3567                            |cbb: &mut AutoCommandBufferBuilder<
3568                                vulkano::command_buffer::PrimaryAutoCommandBuffer,
3569                            >,
3570                             load_op: AttachmentLoadOp,
3571                             store_msaa_for_followup: bool|
3572                             -> Result<(), Box<dyn std::error::Error>> {
3573                                let mut bloom_attachment = RenderingAttachmentInfo {
3574                                    load_op,
3575                                    store_op: AttachmentStoreOp::Store,
3576                                    clear_value: match load_op {
3577                                        AttachmentLoadOp::Clear => {
3578                                            Some(ClearValue::from([0.0, 0.0, 0.0, 0.0]))
3579                                        }
3580                                        _ => None,
3581                                    },
3582                                    ..RenderingAttachmentInfo::image_view(
3583                                        post_process
3584                                            .targets
3585                                            .bloom_source_msaa
3586                                            .clone()
3587                                            .unwrap_or_else(|| bloom_source.clone()),
3588                                    )
3589                                };
3590
3591                                if post_process.targets.bloom_source_msaa.is_some() {
3592                                    bloom_attachment.resolve_info =
3593                                        Some(RenderingAttachmentResolveInfo::image_view(
3594                                            bloom_source.clone(),
3595                                        ));
3596                                    bloom_attachment.store_op = if store_msaa_for_followup {
3597                                        AttachmentStoreOp::Store
3598                                    } else {
3599                                        AttachmentStoreOp::DontCare
3600                                    };
3601                                }
3602
3603                                cbb.begin_rendering(RenderingInfo {
3604                                    render_area_offset: [0, 0],
3605                                    render_area_extent: [extent[0], extent[1]],
3606                                    layer_count: 1,
3607                                    color_attachments: vec![Some(bloom_attachment)],
3608                                    depth_attachment: Some(RenderingAttachmentInfo {
3609                                        load_op: AttachmentLoadOp::Load,
3610                                        store_op: AttachmentStoreOp::DontCare,
3611                                        ..RenderingAttachmentInfo::image_view(
3612                                            post_process.targets.depth.clone(),
3613                                        )
3614                                    }),
3615                                    ..Default::default()
3616                                })?;
3617
3618                                let bloom_viewport = Viewport {
3619                                    offset: [0.0, extent[1] as f32],
3620                                    extent: [extent[0] as f32, -(extent[1] as f32)],
3621                                    depth_range: 0.0..=1.0,
3622                                    ..Default::default()
3623                                };
3624
3625                                cbb.set_viewport(0, vec![bloom_viewport].into())?;
3626                                cbb.set_scissor(
3627                                    0,
3628                                    vec![Scissor {
3629                                        offset: [0, 0],
3630                                        extent: [extent[0], extent[1]],
3631                                        ..Default::default()
3632                                    }]
3633                                    .into(),
3634                                )?;
3635
3636                                Ok(())
3637                            };
3638
3639                        if has_foreground_emissive_content {
3640                            begin_bloom_extraction(
3641                                &mut cbb,
3642                                AttachmentLoadOp::Clear,
3643                                has_background_occluded_lit_emissive_content,
3644                            )?;
3645
3646                            if let Some(emissive_instance_buffer) =
3647                                emissive_instance_buffer.as_ref()
3648                            {
3649                                self.record_instanced_draws_for_batches(
3650                                    &mut cbb,
3651                                    &global_set_fg,
3652                                    &rig_set,
3653                                    emissive_instance_buffer,
3654                                    emissive_instance_count,
3655                                    visual_world.emissive_draw_batches(),
3656                                    self.pipeline_emissive_prepass_toon_mesh.clone(),
3657                                    self.pipeline_emissive_prepass_toon_mesh.clone(),
3658                                    self.pipeline_emissive_prepass_toon_mesh.clone(),
3659                                    self.pipeline_emissive_prepass_toon_mesh.clone(),
3660                                    self.pipeline_skinned_emissive_prepass_toon_mesh.clone(),
3661                                    self.pipeline_skinned_emissive_prepass_toon_mesh.clone(),
3662                                )?;
3663                            }
3664
3665                            if let Some(emissive_cutout_instance_buffer) =
3666                                emissive_cutout_instance_buffer.as_ref()
3667                            {
3668                                self.record_instanced_draws_for_batches(
3669                                    &mut cbb,
3670                                    &global_set_fg,
3671                                    &rig_set,
3672                                    emissive_cutout_instance_buffer,
3673                                    emissive_cutout_instance_count,
3674                                    visual_world.emissive_cutout_batches(),
3675                                    self.pipeline_emissive_prepass_toon_mesh_cutout.clone(),
3676                                    self.pipeline_emissive_prepass_toon_mesh_cutout.clone(),
3677                                    self.pipeline_emissive_prepass_toon_mesh_cutout.clone(),
3678                                    self.pipeline_emissive_prepass_toon_mesh_cutout.clone(),
3679                                    self.pipeline_skinned_emissive_prepass_toon_mesh_cutout
3680                                        .clone(),
3681                                    self.pipeline_skinned_emissive_prepass_toon_mesh_cutout
3682                                        .clone(),
3683                                )?;
3684                            }
3685
3686                            cbb.end_rendering()?;
3687                        }
3688
3689                        if has_background_occluded_lit_emissive_content {
3690                            begin_bloom_extraction(
3691                                &mut cbb,
3692                                if has_foreground_emissive_content {
3693                                    AttachmentLoadOp::Load
3694                                } else {
3695                                    AttachmentLoadOp::Clear
3696                                },
3697                                false,
3698                            )?;
3699
3700                            if let Some(background_occluded_lit_emissive_instance_buffer) =
3701                                background_occluded_lit_emissive_instance_buffer.as_ref()
3702                            {
3703                                let global_set_bg = global_set_bg.as_ref().expect(
3704                                    "background emissive extraction requires bg camera set",
3705                                );
3706                                self.record_instanced_draws_for_batches(
3707                                    &mut cbb,
3708                                    global_set_bg,
3709                                    &rig_set,
3710                                    background_occluded_lit_emissive_instance_buffer,
3711                                    background_occluded_lit_emissive_instance_count,
3712                                    visual_world.background_occluded_lit_emissive_batches(),
3713                                    self.pipeline_emissive_prepass_depth_write_toon_mesh.clone(),
3714                                    self.pipeline_emissive_prepass_depth_write_toon_mesh.clone(),
3715                                    self.pipeline_emissive_prepass_depth_write_toon_mesh.clone(),
3716                                    self.pipeline_emissive_prepass_depth_write_toon_mesh.clone(),
3717                                    self.pipeline_skinned_emissive_prepass_depth_write_toon_mesh
3718                                        .clone(),
3719                                    self.pipeline_skinned_emissive_prepass_depth_write_toon_mesh
3720                                        .clone(),
3721                                )?;
3722                            }
3723
3724                            cbb.end_rendering()?;
3725                        }
3726
3727                        let bloom_format = post_process.final_color_format;
3728                        let blur_h_dir = [1.0 / post_process.targets.bloom_extent[0] as f32, 0.0];
3729                        let blur_v_dir = [0.0, 1.0 / post_process.targets.bloom_extent[1] as f32];
3730
3731                        self.post_processing_renderer.record_final_pass(
3732                            &mut cbb,
3733                            bloom_format,
3734                            bloom_a.clone(),
3735                            post_process.targets.bloom_extent,
3736                            bloom_source,
3737                            None,
3738                            &post_process.config,
3739                        )?;
3740
3741                        self.post_processing_renderer.record_blur_pass(
3742                            &mut cbb,
3743                            bloom_format,
3744                            bloom_a.clone(),
3745                            bloom_b.clone(),
3746                            post_process.targets.bloom_extent,
3747                            blur_h_dir,
3748                            radius_pixels,
3749                        )?;
3750                        self.post_processing_renderer.record_blur_pass(
3751                            &mut cbb,
3752                            bloom_format,
3753                            bloom_b,
3754                            bloom_a.clone(),
3755                            post_process.targets.bloom_extent,
3756                            blur_v_dir,
3757                            radius_pixels,
3758                        )?;
3759
3760                        if bloom_cfg.intensity > 0.0 {
3761                            blurred_bloom = Some(bloom_a);
3762                        }
3763                    }
3764                }
3765
3766                let final_output_view = post_process.final_output_view.clone();
3767
3768                if overlay_instance_count > 0 {
3769                    cbb.begin_rendering(RenderingInfo {
3770                        render_area_offset: [0, 0],
3771                        render_area_extent: [extent[0], extent[1]],
3772                        layer_count: 1,
3773                        color_attachments: vec![Some({
3774                            let mut color_attachment_load = RenderingAttachmentInfo {
3775                                load_op: AttachmentLoadOp::Load,
3776                                store_op: AttachmentStoreOp::Store,
3777                                ..RenderingAttachmentInfo::image_view(color_attachment_view.clone())
3778                            };
3779                            if let Some(resolve_view) = color_resolve_view.clone() {
3780                                color_attachment_load.resolve_info =
3781                                    Some(RenderingAttachmentResolveInfo::image_view(resolve_view));
3782                                color_attachment_load.store_op = AttachmentStoreOp::DontCare;
3783                            }
3784                            color_attachment_load
3785                        })],
3786                        depth_attachment: Some(RenderingAttachmentInfo {
3787                            load_op: AttachmentLoadOp::Clear,
3788                            store_op: AttachmentStoreOp::DontCare,
3789                            clear_value: Some(ClearValue::Depth(1.0)),
3790                            ..RenderingAttachmentInfo::image_view(depth_view.clone())
3791                        }),
3792                        stencil_attachment: Some(RenderingAttachmentInfo {
3793                            load_op: AttachmentLoadOp::Load,
3794                            store_op: AttachmentStoreOp::DontCare,
3795                            ..RenderingAttachmentInfo::image_view(depth_view.clone())
3796                        }),
3797                        ..Default::default()
3798                    })?;
3799
3800                    cbb.set_viewport(0, vec![viewport.clone()].into())?;
3801                    cbb.set_scissor(
3802                        0,
3803                        vec![Scissor {
3804                            offset: [0, 0],
3805                            extent: [extent[0], extent[1]],
3806                            ..Default::default()
3807                        }]
3808                        .into(),
3809                    )?;
3810
3811                    if let Some(overlay_instance_buffer) = overlay_instance_buffer.as_ref() {
3812                        self.record_overlay_draws(
3813                            &mut cbb,
3814                            visual_world,
3815                            &global_set_fg,
3816                            &rig_set,
3817                            overlay_instance_buffer,
3818                            overlay_instance_count,
3819                            Some((&overlay_ops, &overlay_instances)),
3820                        )?;
3821                    }
3822                    cbb.end_rendering()?;
3823                }
3824
3825                self.post_processing_renderer.record_final_pass(
3826                    &mut cbb,
3827                    post_process.final_color_format,
3828                    final_output_view.clone(),
3829                    extent,
3830                    post_process.targets.main_color.clone(),
3831                    blurred_bloom,
3832                    &post_process.config,
3833                )?;
3834
3835                for (handle, src_view) in
3836                    self.collect_runtime_texture_publications(visual_world, &post_process)
3837                {
3838                    let dst_view = self.ensure_runtime_texture_target(handle, &src_view)?;
3839                    cbb.copy_image(CopyImageInfo::images(
3840                        src_view.image().clone(),
3841                        dst_view.image().clone(),
3842                    ))?;
3843                }
3844            }
3845
3846            if let (Some(handle), Some(debug_instance_buffer)) = (
3847                stencil_clip_debug_handle,
3848                stencil_clip_debug_instance_buffer.as_ref(),
3849            ) {
3850                self.ensure_window_runtime_debug_targets(
3851                    self.swapchain_state.swapchain_views.len(),
3852                    extent,
3853                    color_attachment_view.image().format(),
3854                )?;
3855
3856                let debug_view = self
3857                    .window_runtime_debug_targets
3858                    .as_ref()
3859                    .and_then(|targets| targets.color_views.get(bones_slot))
3860                    .cloned()
3861                    .ok_or("missing window runtime debug target")?;
3862
3863                let debug_msaa_view = self
3864                    .window_runtime_debug_targets
3865                    .as_ref()
3866                    .and_then(|targets| targets.msaa_color_views.get(bones_slot))
3867                    .cloned();
3868
3869                let mut debug_color_attachment = RenderingAttachmentInfo {
3870                    load_op: AttachmentLoadOp::Clear,
3871                    store_op: AttachmentStoreOp::Store,
3872                    clear_value: Some(ClearValue::from([1.0, 0.0, 1.0, 1.0])),
3873                    ..RenderingAttachmentInfo::image_view(
3874                        debug_msaa_view
3875                            .clone()
3876                            .unwrap_or_else(|| debug_view.clone()),
3877                    )
3878                };
3879                if debug_msaa_view.is_some() {
3880                    debug_color_attachment.resolve_info = Some(
3881                        RenderingAttachmentResolveInfo::image_view(debug_view.clone()),
3882                    );
3883                    debug_color_attachment.store_op = AttachmentStoreOp::DontCare;
3884                }
3885
3886                cbb.begin_rendering(RenderingInfo {
3887                    render_area_offset: [0, 0],
3888                    render_area_extent: [extent[0], extent[1]],
3889                    layer_count: 1,
3890                    color_attachments: vec![Some(debug_color_attachment)],
3891                    depth_attachment: Some(RenderingAttachmentInfo {
3892                        load_op: AttachmentLoadOp::Clear,
3893                        store_op: AttachmentStoreOp::DontCare,
3894                        clear_value: Some(ClearValue::Depth(1.0)),
3895                        ..RenderingAttachmentInfo::image_view(depth_view.clone())
3896                    }),
3897                    stencil_attachment: Some(RenderingAttachmentInfo {
3898                        load_op: AttachmentLoadOp::Clear,
3899                        store_op: AttachmentStoreOp::DontCare,
3900                        clear_value: Some(ClearValue::Stencil(0)),
3901                        ..RenderingAttachmentInfo::image_view(depth_view.clone())
3902                    }),
3903                    ..Default::default()
3904                })?;
3905
3906                cbb.set_viewport(0, vec![viewport.clone()].into())?;
3907                cbb.set_scissor(
3908                    0,
3909                    vec![Scissor {
3910                        offset: [0, 0],
3911                        extent: [extent[0], extent[1]],
3912                        ..Default::default()
3913                    }]
3914                    .into(),
3915                )?;
3916
3917                self.record_instanced_draws_for_batches(
3918                    &mut cbb,
3919                    &global_set_fg,
3920                    &rig_set,
3921                    debug_instance_buffer,
3922                    stencil_clip_debug_instance_count,
3923                    &stencil_clip_debug_batches,
3924                    self.pipeline_toon_mesh.clone(),
3925                    self.pipeline_mirror_mesh.clone(),
3926                    self.pipeline_grid_mesh.clone(),
3927                    self.pipeline_emissive_toon_mesh.clone(),
3928                    self.pipeline_skinned_toon_mesh.clone(),
3929                    self.pipeline_skinned_emissive_toon_mesh.clone(),
3930                )?;
3931
3932                cbb.end_rendering()?;
3933
3934                let dst_view = self.ensure_runtime_texture_target(handle, &debug_view)?;
3935                cbb.copy_image(CopyImageInfo::images(
3936                    debug_view.image().clone(),
3937                    dst_view.image().clone(),
3938                ))?;
3939            }
3940
3941            let cb = cbb.build()?;
3942
3943            Ok(cb)
3944        }
3945
3946        pub fn render_visual_world(
3947            &mut self,
3948            visual_world: &mut VisualWorld,
3949        ) -> Result<(), Box<dyn std::error::Error>> {
3950            self.recreate_swapchain_if_needed()?;
3951            if !self.pending_runtime_texture_updates.is_empty() {
3952                unsafe {
3953                    self.context.device().wait_idle().map_err(
3954                        |e| -> Box<dyn std::error::Error> {
3955                            format!("wait_idle failed before runtime texture swap: {e}").into()
3956                        },
3957                    )?;
3958                }
3959            }
3960            self.apply_pending_runtime_texture_updates();
3961
3962            // Let Vulkano drop finished per-frame resources incrementally.
3963            for fut in self.images_in_flight.iter_mut() {
3964                if let Some(fut) = fut.as_mut() {
3965                    fut.cleanup_finished();
3966                }
3967            }
3968
3969            let (image_i, suboptimal, acquire_future) =
3970                match swapchain::acquire_next_image(self.swapchain_state.swapchain.clone(), None)
3971                    .map_err(Validated::unwrap)
3972                {
3973                    Ok(r) => r,
3974                    Err(VulkanError::OutOfDate) => {
3975                        self.recreate_swapchain = true;
3976                        return Ok(());
3977                    }
3978                    Err(e) => return Err(Box::new(e)),
3979                };
3980
3981            if suboptimal {
3982                self.recreate_swapchain = true;
3983            }
3984
3985            let extent = self.swapchain_state.swapchain.image_extent();
3986
3987            // Keep VisualWorld informed of the current output size so camera systems can
3988            // build aspect-correct projection matrices.
3989            visual_world.set_viewport([extent[0] as f32, extent[1] as f32]);
3990
3991            let post_process_config = visual_world.post_processing().clone();
3992            let post_process_active = post_process_config.is_active();
3993
3994            if post_process_active {
3995                self.post_processing_renderer.ensure_window_targets(
3996                    self.swapchain_state.swapchain_views.len(),
3997                    extent,
3998                    self.swapchain_state.swapchain.image_format(),
3999                    self.swapchain_state.msaa_samples,
4000                    &post_process_config,
4001                )?;
4002            }
4003
4004            let resolve_view = self.swapchain_state.swapchain_views[image_i as usize].clone();
4005            let post_process = if post_process_active {
4006                let targets = self
4007                    .post_processing_renderer
4008                    .window_frame_targets(image_i as usize)
4009                    .ok_or("missing window post-processing targets")?
4010                    .clone();
4011
4012                Some(PostProcessInvocation {
4013                    final_output_view: resolve_view.clone(),
4014                    final_color_format: self.swapchain_state.swapchain.image_format(),
4015                    config: post_process_config.clone(),
4016                    targets,
4017                })
4018            } else {
4019                None
4020            };
4021
4022            let device = self.context.device().clone();
4023            let queue = self.context.graphics_queue().clone();
4024
4025            self.render_mirror_captures(
4026                visual_world,
4027                self.swapchain_state.swapchain.image_format(),
4028            )?;
4029
4030            let (color_attachment_view, color_resolve_view, depth_view) =
4031                if let Some(post) = post_process.as_ref() {
4032                    (
4033                        post.targets
4034                            .main_msaa_color
4035                            .clone()
4036                            .unwrap_or_else(|| post.targets.main_color.clone()),
4037                        if post.targets.main_msaa_color.is_some() {
4038                            Some(post.targets.main_color.clone())
4039                        } else {
4040                            None
4041                        },
4042                        post.targets.depth.clone(),
4043                    )
4044                } else if self.swapchain_state.msaa_samples != SampleCount::Sample1 {
4045                    (
4046                        self.swapchain_state.msaa_color_views[image_i as usize].clone(),
4047                        Some(resolve_view.clone()),
4048                        self.swapchain_state.depth_views[image_i as usize].clone(),
4049                    )
4050                } else {
4051                    (
4052                        resolve_view.clone(),
4053                        None,
4054                        self.swapchain_state.depth_views[image_i as usize].clone(),
4055                    )
4056                };
4057
4058            let window_camera = visual_world
4059                .visual_camera(crate::engine::graphics::CameraTarget::Window)
4060                .ok_or("missing window camera")?;
4061            let window_eye = window_camera
4062                .eyes
4063                .get(0)
4064                .ok_or("window camera has no eyes")?;
4065            let render_view = RenderView {
4066                view: window_eye.view,
4067                proj: window_eye.proj,
4068                viewport: [extent[0] as f32, extent[1] as f32],
4069                kind: RenderViewKind::Window,
4070            };
4071
4072            let cb = self.build_draw_batches_command_buffer(
4073                visual_world,
4074                &render_view,
4075                image_i as usize,
4076                self.swapchain_state.swapchain_views.len().max(1),
4077                color_attachment_view,
4078                color_resolve_view,
4079                depth_view,
4080                extent,
4081                post_process,
4082                None,
4083            )?;
4084
4085            // Ensure we never render into a swapchain image (and its paired depth attachment)
4086            // while a previous frame that used that image is still in flight.
4087            let image_i_usize = image_i as usize;
4088            if self.images_in_flight.len() != self.swapchain_state.swapchain_views.len() {
4089                // Defensive: should only happen if swapchain recreation failed partially.
4090                self.images_in_flight = (0..self.swapchain_state.swapchain_views.len())
4091                    .map(|_| None)
4092                    .collect();
4093            }
4094
4095            let image_future: Box<dyn GpuFuture> = self.images_in_flight[image_i_usize]
4096                .take()
4097                .unwrap_or_else(|| sync::now(device.clone()).boxed());
4098
4099            // Wayland windows don't participate correctly in frame-callback scheduling unless
4100            // winit is notified just before we submit/present the rendered buffer.
4101            self.window.pre_present_notify();
4102
4103            let execution = image_future
4104                .join(acquire_future)
4105                .then_execute(queue.clone(), cb)?
4106                .then_swapchain_present(
4107                    queue.clone(),
4108                    SwapchainPresentInfo::swapchain_image_index(
4109                        self.swapchain_state.swapchain.clone(),
4110                        image_i,
4111                    ),
4112                )
4113                .then_signal_fence_and_flush();
4114
4115            match execution.map_err(Validated::unwrap) {
4116                Ok(future) => {
4117                    // Track this swapchain image as in flight; this also keeps per-frame
4118                    // resources alive until the GPU is done.
4119                    self.images_in_flight[image_i_usize] = Some(future.boxed());
4120                }
4121                Err(VulkanError::OutOfDate) => {
4122                    self.recreate_swapchain = true;
4123                    // During resize/out-of-date thrash, the command buffer may have been
4124                    // submitted but we might not get a usable future back. Ensure the GPU
4125                    // is idle before we drop tracking; otherwise resources (e.g. depth
4126                    // attachments) can appear "already in use" next frame.
4127                    unsafe {
4128                        println!("[VulkanoRenderer] swapchain out of date during flush");
4129                        let _ = device.wait_idle();
4130                        for slot in self.images_in_flight.iter_mut() {
4131                            if let Some(mut fut) = slot.take() {
4132                                fut.signal_finished();
4133                                fut.cleanup_finished();
4134                            }
4135                        }
4136                    }
4137                }
4138                Err(e) => {
4139                    println!("[VulkanoRenderer] failed to flush future: {e}");
4140
4141                    unsafe {
4142                        println!("[VulkanoRenderer] waiting for device idle after flush failure");
4143                        let _ = device.wait_idle();
4144                        for slot in self.images_in_flight.iter_mut() {
4145                            if let Some(mut fut) = slot.take() {
4146                                fut.signal_finished();
4147                                fut.cleanup_finished();
4148                            }
4149                        }
4150                    }
4151                }
4152            }
4153
4154            Ok(())
4155        }
4156
4157        pub fn upload_mesh(
4158            &mut self,
4159            handle: MeshHandle,
4160            mesh: &CpuMesh,
4161        ) -> Result<(), Box<dyn std::error::Error>> {
4162            static SKIN_UPLOAD_LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
4163
4164            if self.meshes.contains_key(&handle) {
4165                return Ok(());
4166            }
4167
4168            if mesh.vertices.is_empty() {
4169                return Err("mesh has no vertices".into());
4170            }
4171            if mesh.indices_u32.is_empty() {
4172                return Err("mesh has no indices".into());
4173            }
4174
4175            let memory_allocator = self.context.memory_allocator().clone();
4176            let queue = self.context.graphics_queue().clone();
4177
4178            // Host-visible staging buffers.
4179            let vertices_src = Buffer::from_iter(
4180                memory_allocator.clone(),
4181                BufferCreateInfo {
4182                    usage: BufferUsage::TRANSFER_SRC,
4183                    ..Default::default()
4184                },
4185                AllocationCreateInfo {
4186                    memory_type_filter: MemoryTypeFilter::PREFER_HOST
4187                        | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
4188                    ..Default::default()
4189                },
4190                mesh.vertices.iter().copied(),
4191            )?;
4192
4193            let skin_src: Option<Subbuffer<[GpuSkinVertex]>> = match (&mesh.joints0, &mesh.weights0)
4194            {
4195                (Some(joints0), Some(weights0))
4196                    if joints0.len() == mesh.vertices.len()
4197                        && weights0.len() == mesh.vertices.len() =>
4198                {
4199                    if env_flag("CAT_DEBUG_SKIN_UPLOAD") {
4200                        let limit = env_usize("CAT_DEBUG_SKIN_UPLOAD_LIMIT").unwrap_or(3);
4201                        let n = SKIN_UPLOAD_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
4202                        if n < limit {
4203                            println!(
4204                                "[VulkanoRenderer] skin upload: mesh={handle:?} verts={} indices={} joints0_verts={} weights0_verts={}",
4205                                mesh.vertices.len(),
4206                                mesh.indices_u32.len(),
4207                                joints0.len(),
4208                                weights0.len()
4209                            );
4210                            for vi in 0..mesh.vertices.len().min(8) {
4211                                let j = joints0[vi];
4212                                let w = weights0[vi];
4213                                let sum = w[0] + w[1] + w[2] + w[3];
4214                                println!("  v[{vi:04}] joints={j:?} weights={w:?} sum={sum:.6}",);
4215                            }
4216
4217                            if env_flag("CAT_DEBUG_SKIN_HIST") {
4218                                let mut joint_weight: HashMap<u16, f32> = HashMap::new();
4219                                for (j, w) in joints0.iter().copied().zip(weights0.iter().copied())
4220                                {
4221                                    for lane in 0..4 {
4222                                        let jw = w[lane];
4223                                        if jw > 0.0 {
4224                                            *joint_weight.entry(j[lane]).or_insert(0.0) += jw;
4225                                        }
4226                                    }
4227                                }
4228
4229                                let mut entries: Vec<(u16, f32)> =
4230                                    joint_weight.into_iter().collect();
4231                                entries.sort_by(|a, b| b.1.total_cmp(&a.1));
4232                                println!(
4233                                    "[VulkanoRenderer] skin joint histogram (top 12 by total weight):"
4234                                );
4235                                for (rank, (joint, total)) in
4236                                    entries.into_iter().take(12).enumerate()
4237                                {
4238                                    println!("  #{rank:02} joint={joint} total_weight={total:.3}");
4239                                }
4240                            }
4241                        }
4242                    }
4243
4244                    let skin_iter =
4245                        joints0
4246                            .iter()
4247                            .copied()
4248                            .zip(weights0.iter().copied())
4249                            .map(|(j, w)| GpuSkinVertex {
4250                                joints0: j,
4251                                weights0: w,
4252                            });
4253
4254                    Some(Buffer::from_iter(
4255                        memory_allocator.clone(),
4256                        BufferCreateInfo {
4257                            usage: BufferUsage::TRANSFER_SRC,
4258                            ..Default::default()
4259                        },
4260                        AllocationCreateInfo {
4261                            memory_type_filter: MemoryTypeFilter::PREFER_HOST
4262                                | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
4263                            ..Default::default()
4264                        },
4265                        skin_iter,
4266                    )?)
4267                }
4268                (Some(joints0), Some(weights0)) => {
4269                    if env_flag("CAT_DEBUG_SKIN_UPLOAD") {
4270                        println!(
4271                            "[VulkanoRenderer] skin upload skipped (len mismatch): mesh={handle:?} verts={} joints0_len={} weights0_len={}",
4272                            mesh.vertices.len(),
4273                            joints0.len(),
4274                            weights0.len()
4275                        );
4276                    }
4277                    None
4278                }
4279                (Some(joints0), None) => {
4280                    if env_flag("CAT_DEBUG_SKIN_UPLOAD") {
4281                        println!(
4282                            "[VulkanoRenderer] skin upload skipped (missing weights0): mesh={handle:?} verts={} joints0_len={}",
4283                            mesh.vertices.len(),
4284                            joints0.len(),
4285                        );
4286                    }
4287                    None
4288                }
4289                (None, Some(weights0)) => {
4290                    if env_flag("CAT_DEBUG_SKIN_UPLOAD") {
4291                        println!(
4292                            "[VulkanoRenderer] skin upload skipped (missing joints0): mesh={handle:?} verts={} weights0_len={}",
4293                            mesh.vertices.len(),
4294                            weights0.len(),
4295                        );
4296                    }
4297                    None
4298                }
4299                (None, None) => None,
4300            };
4301
4302            let indices_src = Buffer::from_iter(
4303                memory_allocator.clone(),
4304                BufferCreateInfo {
4305                    usage: BufferUsage::TRANSFER_SRC,
4306                    ..Default::default()
4307                },
4308                AllocationCreateInfo {
4309                    memory_type_filter: MemoryTypeFilter::PREFER_HOST
4310                        | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
4311                    ..Default::default()
4312                },
4313                mesh.indices_u32.iter().copied(),
4314            )?;
4315
4316            // Device-local destination buffers.
4317            let vertices_dst = Buffer::new_slice::<CpuVertex>(
4318                memory_allocator.clone(),
4319                BufferCreateInfo {
4320                    usage: BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
4321                    ..Default::default()
4322                },
4323                AllocationCreateInfo {
4324                    memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
4325                    ..Default::default()
4326                },
4327                mesh.vertices.len() as DeviceSize,
4328            )?;
4329
4330            let skin_dst: Option<Subbuffer<[GpuSkinVertex]>> = skin_src
4331                .as_ref()
4332                .map(|_| {
4333                    Buffer::new_slice::<GpuSkinVertex>(
4334                        memory_allocator.clone(),
4335                        BufferCreateInfo {
4336                            usage: BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
4337                            ..Default::default()
4338                        },
4339                        AllocationCreateInfo {
4340                            memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
4341                            ..Default::default()
4342                        },
4343                        mesh.vertices.len() as DeviceSize,
4344                    )
4345                })
4346                .transpose()?;
4347
4348            let indices_dst = Buffer::new_slice::<u32>(
4349                memory_allocator.clone(),
4350                BufferCreateInfo {
4351                    usage: BufferUsage::INDEX_BUFFER | BufferUsage::TRANSFER_DST,
4352                    ..Default::default()
4353                },
4354                AllocationCreateInfo {
4355                    memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
4356                    ..Default::default()
4357                },
4358                mesh.indices_u32.len() as DeviceSize,
4359            )?;
4360
4361            // Copy staging -> device-local.
4362            let mut cbb = AutoCommandBufferBuilder::primary(
4363                self.command_buffer_allocator.clone(),
4364                queue.queue_family_index(),
4365                CommandBufferUsage::OneTimeSubmit,
4366            )?;
4367
4368            cbb.copy_buffer(CopyBufferInfo::buffers(vertices_src, vertices_dst.clone()))?;
4369            if let (Some(src), Some(dst)) = (skin_src, skin_dst.as_ref()) {
4370                cbb.copy_buffer(CopyBufferInfo::buffers(src, dst.clone()))?;
4371            }
4372            cbb.copy_buffer(CopyBufferInfo::buffers(indices_src, indices_dst.clone()))?;
4373
4374            let cb = cbb.build()?;
4375
4376            cb.execute(queue.clone())?
4377                .then_signal_fence_and_flush()?
4378                .wait(None)?;
4379
4380            self.meshes.insert(
4381                handle,
4382                VulkanoGpuMesh {
4383                    vertices: vertices_dst,
4384                    skin_vertices: skin_dst,
4385                    indices: indices_dst,
4386                    index_count: mesh.index_count(),
4387                },
4388            );
4389
4390            Ok(())
4391        }
4392    }
4393}
4394
4395/// Vulkano-only renderer.
4396pub struct VulkanoRenderer {
4397    vulkano: Option<vulkano_backend::VulkanoState>,
4398    next_mesh_handle: u32,
4399    next_texture_handle: u32,
4400    did_enable_present_loop_log: bool,
4401    msaa_mode_override: Option<MsaaMode>,
4402}
4403
4404impl VulkanoRenderer {
4405    pub fn new() -> Self {
4406        Self {
4407            vulkano: None,
4408            next_mesh_handle: 0,
4409            // Reserve handle 0 for the default white texture.
4410            next_texture_handle: 1,
4411            did_enable_present_loop_log: false,
4412            msaa_mode_override: None,
4413        }
4414    }
4415
4416    pub fn msaa_mode_override(&self) -> Option<MsaaMode> {
4417        self.msaa_mode_override
4418    }
4419
4420    pub fn set_msaa_mode(&mut self, mode: MsaaMode) -> Result<(), &'static str> {
4421        if self.vulkano.is_some() {
4422            return Err("cannot change MSAA mode after renderer initialization");
4423        }
4424        self.msaa_mode_override = Some(mode);
4425        Ok(())
4426    }
4427
4428    pub fn init_for_window(
4429        &mut self,
4430        window: &Arc<Window>,
4431        xr_required: Option<(&[String], &[String])>,
4432    ) -> Result<(), Box<dyn std::error::Error>> {
4433        if self.vulkano.is_none() {
4434            let msaa_mode = self.msaa_mode_override.unwrap_or_default();
4435            self.vulkano = Some(vulkano_backend::VulkanoState::new(
4436                window.clone(),
4437                xr_required,
4438                msaa_mode,
4439            )?);
4440            println!("[VulkanoRenderer] Vulkano swapchain/render-pass initialized");
4441        }
4442
4443        Ok(())
4444    }
4445
4446    pub fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
4447        let _ = size;
4448        if let Some(vulkano) = self.vulkano.as_mut() {
4449            vulkano.window_resized = true;
4450        }
4451    }
4452
4453    pub fn upload_mesh(
4454        &mut self,
4455        mesh: &CpuMesh,
4456    ) -> Result<MeshHandle, Box<dyn std::error::Error>> {
4457        let Some(vulkano) = self.vulkano.as_mut() else {
4458            return Err("VulkanoRenderer not initialized (call init_for_window first)".into());
4459        };
4460
4461        let handle = MeshHandle(self.next_mesh_handle);
4462        self.next_mesh_handle = self.next_mesh_handle.wrapping_add(1);
4463
4464        vulkano.upload_mesh(handle, mesh)?;
4465        Ok(handle)
4466    }
4467
4468    pub fn render_visual_world(
4469        &mut self,
4470        visual_world: &mut VisualWorld,
4471    ) -> Result<(), Box<dyn std::error::Error>> {
4472        let Some(vulkano) = self.vulkano.as_mut() else {
4473            return Err("VulkanoRenderer not initialized (call init_for_window first)".into());
4474        };
4475
4476        if !self.did_enable_present_loop_log {
4477            self.did_enable_present_loop_log = true;
4478            println!("[VulkanoRenderer] Present loop enabled");
4479        }
4480
4481        vulkano.render_visual_world(visual_world)
4482    }
4483
4484    pub fn window_vk_format_raw(&self) -> Option<u32> {
4485        let vulkano = self.vulkano.as_ref()?;
4486        let format = vulkano.window_color_format();
4487        let vk: ash::vk::Format = format.into();
4488        Some(vk.as_raw() as u32)
4489    }
4490
4491    pub fn render_xr_eye_offscreen(
4492        &mut self,
4493        visual_world: &mut VisualWorld,
4494        eye: usize,
4495        extent: [u32; 2],
4496    ) -> Result<(), Box<dyn std::error::Error>> {
4497        let Some(vulkano) = self.vulkano.as_mut() else {
4498            return Err("VulkanoRenderer not initialized (call init_for_window first)".into());
4499        };
4500
4501        vulkano.render_xr_eye_offscreen(visual_world, eye, extent)
4502    }
4503
4504    pub fn xr_offscreen_vk_image(&self, eye: usize) -> Option<ash::vk::Image> {
4505        self.vulkano.as_ref()?.xr_offscreen_vk_image(eye)
4506    }
4507
4508    /// Returns raw Vulkan handles suitable for `openxr::Instance::create_session::<openxr::Vulkan>()`.
4509    ///
4510    /// Note: OpenXR expects these as opaque pointers; we cast from `ash` raw handles.
4511    pub fn xr_vulkan_graphics(&self) -> Option<crate::engine::graphics::XrVulkanGraphics> {
4512        use ash::vk::Handle as _;
4513        use std::ffi::c_void;
4514        use vulkano::VulkanObject;
4515
4516        let vulkano = self.vulkano.as_ref()?;
4517
4518        let device = vulkano.context.device().clone();
4519        let queue = vulkano.context.graphics_queue().clone();
4520        let instance = device.instance().clone();
4521        let physical_device = device.physical_device();
4522
4523        let vk_instance = instance.handle().as_raw() as usize as *const c_void;
4524        let vk_physical_device = physical_device.handle().as_raw() as usize as *const c_void;
4525        let vk_device = device.handle().as_raw() as usize as *const c_void;
4526        let vk_queue = queue.handle().as_raw() as usize as *const c_void;
4527
4528        Some(crate::engine::graphics::XrVulkanGraphics {
4529            vk_instance,
4530            vk_physical_device,
4531            vk_device,
4532            vk_queue,
4533            queue_family_index: queue.queue_family_index(),
4534            // Vulkano doesn't currently expose a stable “queue index within family” API here.
4535            // Using 0 is correct for the common single-queue case.
4536            queue_index: 0,
4537        })
4538    }
4539}
4540
4541impl MeshUploader for VulkanoRenderer {
4542    fn upload_mesh(&mut self, mesh: &CpuMesh) -> Result<MeshHandle, Box<dyn std::error::Error>> {
4543        self.upload_mesh(mesh)
4544    }
4545}
4546
4547impl TextureUploader for VulkanoRenderer {
4548    fn upload_texture_rgba8(
4549        &mut self,
4550        rgba: &[u8],
4551        width: u32,
4552        height: u32,
4553    ) -> Result<TextureHandle, Box<dyn std::error::Error>> {
4554        let Some(vulkano) = self.vulkano.as_mut() else {
4555            return Err("VulkanoRenderer not initialized (call init_for_window first)".into());
4556        };
4557
4558        let handle = TextureHandle(self.next_texture_handle);
4559        self.next_texture_handle = self.next_texture_handle.wrapping_add(1);
4560
4561        vulkano.upload_texture_rgba8(handle, rgba, width, height)?;
4562        Ok(handle)
4563    }
4564
4565    fn upload_texture_bc7(
4566        &mut self,
4567        bc7_blocks: &[u8],
4568        width: u32,
4569        height: u32,
4570        srgb: bool,
4571    ) -> Result<TextureHandle, Box<dyn std::error::Error>> {
4572        let Some(vulkano) = self.vulkano.as_mut() else {
4573            return Err("VulkanoRenderer not initialized (call init_for_window first)".into());
4574        };
4575
4576        let handle = TextureHandle(self.next_texture_handle);
4577        self.next_texture_handle = self.next_texture_handle.wrapping_add(1);
4578
4579        vulkano.upload_texture_bc7(handle, bc7_blocks, width, height, srgb)?;
4580        Ok(handle)
4581    }
4582}