Skip to main content

viewport_lib/resources/mesh/
instancing.rs

1use crate::resources::*;
2
3/// Instanced-draw pipelines, the shared per-instance storage buffer, and the
4/// per-material bind group cache. Created lazily by `ensure_instanced_pipelines`
5/// and `ensure_hdr_instanced_pipelines`.
6#[derive(Default)]
7pub(crate) struct InstancingResources {
8    /// Bind group layout for the instanced storage buffer + textures (group 1).
9    pub(crate) bind_group_layout: Option<wgpu::BindGroupLayout>,
10    /// Storage buffer for per-instance data.
11    pub(crate) storage_buf: Option<wgpu::Buffer>,
12    /// Current capacity (in number of instances) of the storage buffer.
13    pub(crate) storage_capacity: usize,
14    /// Per-texture-key bind groups for the instanced path.
15    ///
16    /// Each entry combines the shared instance storage buffer (binding 0) with
17    /// one specific texture combination (bindings 1-4). Keyed by
18    /// (albedo_id, normal_map_id, ao_map_id) using u64::MAX for fallback slots.
19    /// Invalidated when the storage buffer is resized.
20    pub(crate) bind_groups: std::collections::HashMap<(u64, u64, u64), wgpu::BindGroup>,
21    /// Instanced solid render pipeline (TriangleList, opaque).
22    pub(crate) solid_pipeline: Option<wgpu::RenderPipeline>,
23    /// Two-sided (`cull_mode: None`) variant of `solid_pipeline` for
24    /// `Identical` backface-policy meshes.
25    pub(crate) solid_two_sided_pipeline: Option<wgpu::RenderPipeline>,
26    /// Instanced transparent render pipeline (TriangleList, alpha blending).
27    pub(crate) transparent_pipeline: Option<wgpu::RenderPipeline>,
28    /// Instanced shadow render pipeline (depth-only).
29    pub(crate) shadow_pipeline: Option<wgpu::RenderPipeline>,
30    /// Two-sided (`cull_mode: None` + two-sided depth bias) variant of
31    /// `shadow_pipeline` for `Identical` backface-policy batches.
32    pub(crate) shadow_two_sided_pipeline: Option<wgpu::RenderPipeline>,
33    /// Per-cascade uniform buffers for the shadow pipeline (64 bytes each, one mat4x4).
34    pub(crate) shadow_cascade_bufs: [Option<wgpu::Buffer>; 4],
35    /// Per-cascade bind groups for the shadow pipeline group 0.
36    pub(crate) shadow_cascade_bgs: [Option<wgpu::BindGroup>; 4],
37    /// HDR-pass instanced solid pipeline (direct draw path).
38    pub(crate) hdr_solid_pipeline: Option<wgpu::RenderPipeline>,
39    /// Two-sided (`cull_mode: None`) variant of `hdr_solid_pipeline`
40    /// for `Identical` backface-policy meshes (direct draw path).
41    pub(crate) hdr_solid_two_sided_pipeline: Option<wgpu::RenderPipeline>,
42    pub(crate) hdr_transparent_pipeline: Option<wgpu::RenderPipeline>,
43    /// Instanced HDR pipeline with additive blend, no depth write. Used by
44    /// `MeshInstanceItem` batches that opt into [`SpriteBlend::Additive`].
45    pub(crate) hdr_additive_pipeline: Option<wgpu::RenderPipeline>,
46    /// Instanced HDR pipeline with premultiplied-alpha blend, no depth write.
47    /// Used by `MeshInstanceItem` batches with [`SpriteBlend::Premultiplied`].
48    pub(crate) hdr_premultiplied_pipeline: Option<wgpu::RenderPipeline>,
49}
50
51/// GPU-culling inputs and pipelines. The per-instance AABBs and per-batch meta
52/// are scene-global (camera-independent); the cull OUTPUTS (visibility indices,
53/// indirect args, counters) are per-viewport and live in `ViewportCullState`.
54/// Pipelines are created lazily by `ensure_cull_instance_pipelines`.
55#[derive(Default)]
56pub(crate) struct CullResources {
57    /// Per-instance world-space AABB buffer. Rebuilt on batch cache miss.
58    pub(crate) aabb_buf: Option<wgpu::Buffer>,
59    pub(crate) aabb_capacity: usize,
60    /// Per-batch metadata buffer. Rebuilt on batch cache miss.
61    pub(crate) batch_meta_buf: Option<wgpu::Buffer>,
62    pub(crate) batch_meta_capacity: usize,
63    /// Bind group layout for instanced cull pipelines (group 1).
64    /// Extends the instance BGL with binding 5: visibility_indices storage buffer.
65    pub(crate) bind_group_layout: Option<wgpu::BindGroupLayout>,
66    /// HDR-pass solid instanced pipeline using `vs_main_cull` (indirect draw path).
67    pub(crate) hdr_solid_pipeline: Option<wgpu::RenderPipeline>,
68    /// Two-sided (`cull_mode: None`) variant of `hdr_solid_pipeline`
69    /// for `Identical` backface-policy meshes (indirect draw path).
70    pub(crate) hdr_solid_two_sided_pipeline: Option<wgpu::RenderPipeline>,
71    /// OIT-pass transparent instanced pipeline using `vs_main_cull` (indirect draw path).
72    pub(crate) oit_pipeline: Option<wgpu::RenderPipeline>,
73    /// Shadow instanced cull pipeline (depth-only, uses `vs_shadow_cull`).
74    pub(crate) shadow_pipeline: Option<wgpu::RenderPipeline>,
75    /// Two-sided (`cull_mode: None` + two-sided depth bias) variant of
76    /// `shadow_pipeline` for `Identical` backface-policy batches.
77    pub(crate) shadow_two_sided_pipeline: Option<wgpu::RenderPipeline>,
78    /// BGL for shadow cull instance group: binding 0 (instances) + binding 5 (visibility_indices).
79    pub(crate) shadow_bgl: Option<wgpu::BindGroupLayout>,
80}
81
82impl DeviceResources {
83    /// Ensure the instanced pipelines and bind group layout are created.
84    /// Called lazily when the instanced draw path is first needed.
85    pub(crate) fn ensure_instanced_pipelines(&mut self, device: &wgpu::Device) {
86        if self.instancing.bind_group_layout.is_some() {
87            return; // Already initialized.
88        }
89
90        // Instanced bind group layout (group 1 for instanced pipelines).
91        // binding 0: instance storage buffer
92        // binding 1-4: albedo texture, sampler, normal map, AO map
93        // Co-located in group 1 to stay within iced's max_bind_groups = 2.
94        let instance_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
95            label: Some("instance_bgl"),
96            entries: &[
97                // binding 0: instance storage buffer
98                wgpu::BindGroupLayoutEntry {
99                    binding: 0,
100                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
101                    ty: wgpu::BindingType::Buffer {
102                        ty: wgpu::BufferBindingType::Storage { read_only: true },
103                        has_dynamic_offset: false,
104                        min_binding_size: None,
105                    },
106                    count: None,
107                },
108                // binding 1: albedo texture
109                wgpu::BindGroupLayoutEntry {
110                    binding: 1,
111                    visibility: wgpu::ShaderStages::FRAGMENT,
112                    ty: wgpu::BindingType::Texture {
113                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
114                        view_dimension: wgpu::TextureViewDimension::D2,
115                        multisampled: false,
116                    },
117                    count: None,
118                },
119                // binding 2: sampler
120                wgpu::BindGroupLayoutEntry {
121                    binding: 2,
122                    visibility: wgpu::ShaderStages::FRAGMENT,
123                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
124                    count: None,
125                },
126                // binding 3: normal map texture
127                wgpu::BindGroupLayoutEntry {
128                    binding: 3,
129                    visibility: wgpu::ShaderStages::FRAGMENT,
130                    ty: wgpu::BindingType::Texture {
131                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
132                        view_dimension: wgpu::TextureViewDimension::D2,
133                        multisampled: false,
134                    },
135                    count: None,
136                },
137                // binding 4: AO map texture
138                wgpu::BindGroupLayoutEntry {
139                    binding: 4,
140                    visibility: wgpu::ShaderStages::FRAGMENT,
141                    ty: wgpu::BindingType::Texture {
142                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
143                        view_dimension: wgpu::TextureViewDimension::D2,
144                        multisampled: false,
145                    },
146                    count: None,
147                },
148            ],
149        });
150
151        // Instanced mesh shader.
152        let instanced_shader = {
153            let base = include_str!(concat!(env!("OUT_DIR"), "/mesh_instanced.wgsl"));
154            let composed = crate::resources::mesh_sidecar::registry::compose_shader(
155                base,
156                &self.deform.registrations,
157            );
158            crate::resources::builders::wgsl_module(device, "mesh_instanced_shader", composed)
159        };
160
161        let instanced_layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
162            device,
163            "instanced_pipeline_layout",
164            &self.camera_bind_group_layout,
165            &instance_bgl,
166            self.deform
167                .enabled
168                .then_some(&self.deform.bind_group_layout),
169        );
170        let ldr_inst = crate::resources::mesh::mesh_pipelines::build_ldr_instanced_mesh_pipelines(
171            device,
172            &instanced_layout,
173            &instanced_shader,
174            self.target_format,
175            self.sample_count,
176        );
177        let solid_instanced = ldr_inst.solid;
178        let solid_two_sided_instanced = ldr_inst.solid_two_sided;
179        let transparent_instanced = ldr_inst.transparent;
180
181        // Shadow instanced pipeline.
182        let shadow_instanced_shader = crate::resources::builders::wgsl_module(
183            device,
184            "shadow_instanced_shader",
185            crate::resources::builders::wgsl_source!("shadow_instanced"),
186        );
187
188        // Shadow instanced uses the shadow bind group layout (group 0) + instance_bgl (group 1).
189        // Re-derive the shadow BGL from the existing shadow_bind_group.
190        let shadow_bgl = crate::resources::builders::uniform_bgl(
191            device,
192            "shadow_bgl_for_instanced",
193            wgpu::ShaderStages::VERTEX,
194        );
195
196        let shadow_instanced_layout =
197            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
198                label: Some("shadow_instanced_pipeline_layout"),
199                bind_group_layouts: &[&shadow_bgl, &instance_bgl],
200                push_constant_ranges: &[],
201            });
202
203        // Front-cull for closed solids; `cull_mode: None` + the two-sided bias for
204        // two-sided (`Identical`) batches, so a single-winding foliage card still
205        // casts when its front face points away from the light. Mirrors the
206        // per-object `shadow_pipeline` / `shadow_pipeline_two_sided` split.
207        let make_shadow_instanced =
208            |label: &str, cull_mode: Option<wgpu::Face>, bias: wgpu::DepthBiasState| {
209                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
210                    label: Some(label),
211                    layout: Some(&shadow_instanced_layout),
212                    vertex: wgpu::VertexState {
213                        module: &shadow_instanced_shader,
214                        entry_point: Some("vs_main"),
215                        buffers: &[Vertex::buffer_layout()],
216                        compilation_options: wgpu::PipelineCompilationOptions::default(),
217                    },
218                    fragment: None,
219                    primitive: wgpu::PrimitiveState {
220                        topology: wgpu::PrimitiveTopology::TriangleList,
221                        cull_mode,
222                        ..Default::default()
223                    },
224                    depth_stencil: Some(wgpu::DepthStencilState {
225                        format: wgpu::TextureFormat::Depth32Float,
226                        depth_write_enabled: true,
227                        depth_compare: wgpu::CompareFunction::Less,
228                        stencil: wgpu::StencilState::default(),
229                        bias,
230                    }),
231                    multisample: wgpu::MultisampleState::default(),
232                    multiview: None,
233                    cache: None,
234                })
235            };
236        let shadow_instanced = make_shadow_instanced(
237            "shadow_instanced_pipeline",
238            Some(wgpu::Face::Front),
239            crate::resources::mesh::mesh_pipelines::CSM_SHADOW_BIAS,
240        );
241        let shadow_instanced_two_sided = make_shadow_instanced(
242            "shadow_instanced_two_sided_pipeline",
243            None,
244            crate::resources::mesh::mesh_pipelines::CSM_SHADOW_BIAS_TWO_SIDED,
245        );
246
247        // Allocate 4 per-cascade uniform buffers (64 bytes each = one mat4x4) and
248        // create bind groups for shadow_instanced_pipeline group 0.
249        // Each cascade has its own small buffer so we can write_buffer(buf, 0, ...) without
250        // dynamic offsets (shadow_instanced.wgsl group 0 binds a single uniform, not an array).
251        let cascade_bufs: [wgpu::Buffer; 4] = std::array::from_fn(|i| {
252            device.create_buffer(&wgpu::BufferDescriptor {
253                label: Some(&format!("shadow_instanced_cascade_buf_{i}")),
254                size: 64, // sizeof(mat4x4<f32>)
255                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
256                mapped_at_creation: false,
257            })
258        });
259        let cascade_bgs: [wgpu::BindGroup; 4] = std::array::from_fn(|i| {
260            device.create_bind_group(&wgpu::BindGroupDescriptor {
261                label: Some(&format!("shadow_instanced_cascade_bg_{i}")),
262                layout: &shadow_bgl,
263                entries: &[wgpu::BindGroupEntry {
264                    binding: 0,
265                    resource: cascade_bufs[i].as_entire_binding(),
266                }],
267            })
268        });
269        self.instancing.shadow_cascade_bufs = cascade_bufs.map(Some);
270        self.instancing.shadow_cascade_bgs = cascade_bgs.map(Some);
271
272        self.instancing.bind_group_layout = Some(instance_bgl);
273        self.instancing.solid_pipeline = Some(solid_instanced);
274        self.instancing.solid_two_sided_pipeline = Some(solid_two_sided_instanced);
275        self.instancing.transparent_pipeline = Some(transparent_instanced);
276        self.instancing.shadow_pipeline = Some(shadow_instanced);
277        self.instancing.shadow_two_sided_pipeline = Some(shadow_instanced_two_sided);
278    }
279
280    /// Ensure the HDR instanced pipelines exist. Called after
281    /// `ensure_instanced_pipelines` so that `instance_bind_group_layout` is
282    /// available. Idempotent: returns immediately if the pipelines already
283    /// exist or if the BGL hasn't been created yet.
284    pub(crate) fn ensure_hdr_instanced_pipelines(&mut self, device: &wgpu::Device) {
285        if self.instancing.hdr_solid_pipeline.is_some() {
286            return;
287        }
288        let Some(ref instance_bgl) = self.instancing.bind_group_layout else {
289            return;
290        };
291
292        let inst_shader = {
293            let base = include_str!(concat!(env!("OUT_DIR"), "/mesh_instanced.wgsl"));
294            let composed = crate::resources::mesh_sidecar::registry::compose_shader(
295                base,
296                &self.deform.registrations,
297            );
298            crate::resources::builders::wgsl_module(device, "mesh_instanced_shader_hdr", composed)
299        };
300        let inst_layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
301            device,
302            "hdr_instanced_pipeline_layout",
303            &self.camera_bind_group_layout,
304            instance_bgl,
305            self.deform
306                .enabled
307                .then_some(&self.deform.bind_group_layout),
308        );
309        let hdr_inst = crate::resources::mesh::mesh_pipelines::build_hdr_instanced_mesh_pipelines(
310            device,
311            &inst_layout,
312            &inst_shader,
313        );
314        self.instancing.hdr_solid_pipeline = Some(hdr_inst.solid);
315        self.instancing.hdr_solid_two_sided_pipeline = Some(hdr_inst.solid_two_sided);
316        self.instancing.hdr_transparent_pipeline = Some(hdr_inst.transparent);
317        self.instancing.hdr_additive_pipeline = Some(hdr_inst.additive);
318        self.instancing.hdr_premultiplied_pipeline = Some(hdr_inst.premultiplied);
319    }
320
321    /// Ensure the OIT instanced pipeline exists. Called after
322    /// `ensure_instanced_pipelines` so that `instance_bind_group_layout` is
323    /// available. Idempotent: returns immediately if the pipeline already
324    /// exists or if the BGL hasn't been created yet.
325    pub(crate) fn ensure_oit_instanced_pipeline(&mut self, device: &wgpu::Device) {
326        if self.oit.instanced_pipeline.is_some() {
327            return;
328        }
329        let Some(ref instance_bgl) = self.instancing.bind_group_layout else {
330            return;
331        };
332
333        let instanced_oit_shader = {
334            let base = include_str!(concat!(env!("OUT_DIR"), "/mesh_instanced_oit.wgsl"));
335            let composed = crate::resources::mesh_sidecar::registry::compose_shader(
336                base,
337                &self.deform.registrations,
338            );
339            crate::resources::builders::wgsl_module(device, "mesh_instanced_oit_shader", composed)
340        };
341        let instanced_oit_layout =
342            crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
343                device,
344                "oit_instanced_pipeline_layout",
345                &self.camera_bind_group_layout,
346                instance_bgl,
347                self.deform
348                    .enabled
349                    .then_some(&self.deform.bind_group_layout),
350            );
351        let pipeline = crate::resources::mesh::mesh_pipelines::build_oit_instanced_pipeline(
352            device,
353            &instanced_oit_layout,
354            &instanced_oit_shader,
355            "oit_instanced_pipeline",
356            "vs_main",
357        );
358
359        self.oit.instanced_pipeline = Some(pipeline);
360    }
361
362    /// Upload instance data to the storage buffer, resizing if needed.
363    /// Returns the bind group for the instance storage buffer.
364    pub(crate) fn upload_instance_data(
365        &mut self,
366        device: &wgpu::Device,
367        queue: &wgpu::Queue,
368        data: &[InstanceData],
369    ) {
370        if data.is_empty() {
371            return;
372        }
373
374        let _bgl = self
375            .instancing
376            .bind_group_layout
377            .as_ref()
378            .expect("ensure_instanced_pipelines must be called first");
379
380        // Clamp to the device's max_storage_buffer_binding_size so bind group
381        // creation never panics regardless of scene size.
382        let max_instances = (device.limits().max_storage_buffer_binding_size as usize)
383            / std::mem::size_of::<InstanceData>();
384        let data = &data[..data.len().min(max_instances)];
385
386        let needed = data.len();
387        if needed > self.instancing.storage_capacity {
388            // Grow with 2x strategy, capped at the device limit.
389            let new_cap = (needed * 2).max(64).min(max_instances);
390            let buf_size = (new_cap * std::mem::size_of::<InstanceData>()) as u64;
391            self.instancing.storage_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
392                label: Some("instance_storage_buf"),
393                size: buf_size,
394                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
395                mapped_at_creation: false,
396            }));
397            self.instancing.storage_capacity = new_cap;
398
399            // Invalidate all per-texture-key bind groups; they reference the old buffer.
400            self.instancing.bind_groups.clear();
401        }
402
403        queue.write_buffer(
404            self.instancing.storage_buf.as_ref().unwrap(),
405            0,
406            bytemuck::cast_slice(data),
407        );
408    }
409
410    /// Upload the shared cull inputs: per-instance AABBs and per-batch metadata.
411    ///
412    /// These are the same for every viewport (they do not depend on the camera),
413    /// so they live on `DeviceResources`. The per-viewport cull outputs are
414    /// allocated separately by `ViewportCullState::ensure_outputs`. Buffers grow
415    /// with the same 2x strategy as `upload_instance_data`. Call on every batch
416    /// cache miss, immediately after `upload_instance_data`.
417    pub(crate) fn upload_cull_inputs(
418        &mut self,
419        device: &wgpu::Device,
420        queue: &wgpu::Queue,
421        aabbs: &[crate::resources::types::InstanceAabb],
422        metas: &[crate::resources::types::BatchMeta],
423    ) {
424        // --- AABB buffer (per-instance) ---
425        let max_instances = (device.limits().max_storage_buffer_binding_size as usize)
426            / std::mem::size_of::<crate::resources::types::InstanceAabb>();
427        let aabbs = &aabbs[..aabbs.len().min(max_instances)];
428
429        if aabbs.len() > self.cull.aabb_capacity {
430            let new_cap = (aabbs.len() * 2).max(64).min(max_instances);
431            self.cull.aabb_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
432                label: Some("instance_aabb_buf"),
433                size: (new_cap * std::mem::size_of::<crate::resources::types::InstanceAabb>())
434                    as u64,
435                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
436                mapped_at_creation: false,
437            }));
438            self.cull.aabb_capacity = new_cap;
439        }
440        if !aabbs.is_empty() {
441            queue.write_buffer(
442                self.cull.aabb_buf.as_ref().unwrap(),
443                0,
444                bytemuck::cast_slice(aabbs),
445            );
446        }
447
448        // --- Batch meta buffer (per-batch) ---
449        let max_batches = (device.limits().max_storage_buffer_binding_size as usize)
450            / std::mem::size_of::<crate::resources::types::BatchMeta>();
451        let metas = &metas[..metas.len().min(max_batches)];
452        let batch_count = metas.len();
453
454        if batch_count > self.cull.batch_meta_capacity {
455            let new_cap = (batch_count * 2).max(16).min(max_batches);
456            let meta_size =
457                (new_cap * std::mem::size_of::<crate::resources::types::BatchMeta>()) as u64;
458            self.cull.batch_meta_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
459                label: Some("batch_meta_buf"),
460                size: meta_size,
461                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
462                mapped_at_creation: false,
463            }));
464            self.cull.batch_meta_capacity = new_cap;
465        }
466
467        if !metas.is_empty() {
468            queue.write_buffer(
469                self.cull.batch_meta_buf.as_ref().unwrap(),
470                0,
471                bytemuck::cast_slice(metas),
472            );
473        }
474    }
475
476    /// Ensure the GPU-driven cull variant pipelines and BGL are created.
477    ///
478    /// Must be called after `ensure_instanced_pipelines`.  Idempotent.
479    pub(crate) fn ensure_cull_instance_pipelines(&mut self, device: &wgpu::Device) {
480        if self.cull.bind_group_layout.is_some() {
481            return;
482        }
483
484        let Some(ref _instance_bgl) = self.instancing.bind_group_layout else {
485            return; // ensure_instanced_pipelines must be called first.
486        };
487
488        // Cull BGL = instance_bgl bindings 0-4 + binding 5: visibility_indices (read, VERTEX).
489        let cull_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
490            label: Some("instance_cull_bgl"),
491            entries: &[
492                wgpu::BindGroupLayoutEntry {
493                    binding: 0,
494                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
495                    ty: wgpu::BindingType::Buffer {
496                        ty: wgpu::BufferBindingType::Storage { read_only: true },
497                        has_dynamic_offset: false,
498                        min_binding_size: None,
499                    },
500                    count: None,
501                },
502                wgpu::BindGroupLayoutEntry {
503                    binding: 1,
504                    visibility: wgpu::ShaderStages::FRAGMENT,
505                    ty: wgpu::BindingType::Texture {
506                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
507                        view_dimension: wgpu::TextureViewDimension::D2,
508                        multisampled: false,
509                    },
510                    count: None,
511                },
512                wgpu::BindGroupLayoutEntry {
513                    binding: 2,
514                    visibility: wgpu::ShaderStages::FRAGMENT,
515                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
516                    count: None,
517                },
518                wgpu::BindGroupLayoutEntry {
519                    binding: 3,
520                    visibility: wgpu::ShaderStages::FRAGMENT,
521                    ty: wgpu::BindingType::Texture {
522                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
523                        view_dimension: wgpu::TextureViewDimension::D2,
524                        multisampled: false,
525                    },
526                    count: None,
527                },
528                wgpu::BindGroupLayoutEntry {
529                    binding: 4,
530                    visibility: wgpu::ShaderStages::FRAGMENT,
531                    ty: wgpu::BindingType::Texture {
532                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
533                        view_dimension: wgpu::TextureViewDimension::D2,
534                        multisampled: false,
535                    },
536                    count: None,
537                },
538                // binding 5: visibility_indices (written by compute cull pass, read in vertex shader)
539                wgpu::BindGroupLayoutEntry {
540                    binding: 5,
541                    visibility: wgpu::ShaderStages::VERTEX,
542                    ty: wgpu::BindingType::Buffer {
543                        ty: wgpu::BufferBindingType::Storage { read_only: true },
544                        has_dynamic_offset: false,
545                        min_binding_size: None,
546                    },
547                    count: None,
548                },
549            ],
550        });
551
552        // HDR solid cull pipeline: Rgba16Float target, vs_main_cull, back-face cull.
553        let instanced_shader = {
554            let base = include_str!(concat!(env!("OUT_DIR"), "/mesh_instanced.wgsl"));
555            let composed = crate::resources::mesh_sidecar::registry::compose_shader(
556                base,
557                &self.deform.registrations,
558            );
559            crate::resources::builders::wgsl_module(device, "mesh_instanced_shader_cull", composed)
560        };
561        let inst_cull_layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
562            device,
563            "hdr_instanced_cull_pipeline_layout",
564            &self.camera_bind_group_layout,
565            &cull_bgl,
566            self.deform
567                .enabled
568                .then_some(&self.deform.bind_group_layout),
569        );
570        let hdr_solid_cull =
571            crate::resources::mesh::mesh_pipelines::build_hdr_instanced_cull_pipeline(
572                device,
573                &inst_cull_layout,
574                &instanced_shader,
575            );
576        let hdr_solid_cull_two_sided =
577            crate::resources::mesh::mesh_pipelines::build_hdr_instanced_cull_two_sided_pipeline(
578                device,
579                &inst_cull_layout,
580                &instanced_shader,
581            );
582
583        // OIT cull pipeline: Rgba16Float + R8Unorm targets, vs_main_cull, no depth write.
584        let oit_shader = {
585            let base = include_str!(concat!(env!("OUT_DIR"), "/mesh_instanced_oit.wgsl"));
586            let composed = crate::resources::mesh_sidecar::registry::compose_shader(
587                base,
588                &self.deform.registrations,
589            );
590            crate::resources::builders::wgsl_module(
591                device,
592                "mesh_instanced_oit_shader_cull",
593                composed,
594            )
595        };
596        let oit_cull_layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
597            device,
598            "oit_instanced_cull_pipeline_layout",
599            &self.camera_bind_group_layout,
600            &cull_bgl,
601            self.deform
602                .enabled
603                .then_some(&self.deform.bind_group_layout),
604        );
605        let oit_cull = crate::resources::mesh::mesh_pipelines::build_oit_instanced_pipeline(
606            device,
607            &oit_cull_layout,
608            &oit_shader,
609            "oit_instanced_cull_pipeline",
610            "vs_main_cull",
611        );
612
613        self.cull.bind_group_layout = Some(cull_bgl);
614        self.cull.hdr_solid_pipeline = Some(hdr_solid_cull);
615        self.cull.hdr_solid_two_sided_pipeline = Some(hdr_solid_cull_two_sided);
616        self.cull.oit_pipeline = Some(oit_cull);
617
618        // Shadow instanced cull pipeline.
619        // Uses a minimal BGL for group 1: binding 0 (instances) + binding 5 (visibility_indices).
620        // Group 0 reuses the existing shadow cascade BGL (single mat4x4 uniform).
621        let shadow_cull_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
622            label: Some("shadow_cull_instance_bgl"),
623            entries: &[
624                wgpu::BindGroupLayoutEntry {
625                    binding: 0,
626                    visibility: wgpu::ShaderStages::VERTEX,
627                    ty: wgpu::BindingType::Buffer {
628                        ty: wgpu::BufferBindingType::Storage { read_only: true },
629                        has_dynamic_offset: false,
630                        min_binding_size: None,
631                    },
632                    count: None,
633                },
634                wgpu::BindGroupLayoutEntry {
635                    binding: 5,
636                    visibility: wgpu::ShaderStages::VERTEX,
637                    ty: wgpu::BindingType::Buffer {
638                        ty: wgpu::BufferBindingType::Storage { read_only: true },
639                        has_dynamic_offset: false,
640                        min_binding_size: None,
641                    },
642                    count: None,
643                },
644            ],
645        });
646        // Recreate the shadow cascade BGL (same definition as in ensure_instanced_pipelines).
647        let shadow_bgl_for_cull = crate::resources::builders::uniform_bgl(
648            device,
649            "shadow_bgl_for_cull",
650            wgpu::ShaderStages::VERTEX,
651        );
652        let shadow_cull_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
653            label: Some("shadow_instanced_cull_pipeline_layout"),
654            bind_group_layouts: &[&shadow_bgl_for_cull, &shadow_cull_bgl],
655            push_constant_ranges: &[],
656        });
657        let shadow_cull_shader = crate::resources::builders::wgsl_module(
658            device,
659            "shadow_instanced_cull_shader",
660            crate::resources::builders::wgsl_source!("shadow_instanced"),
661        );
662        // Front-cull for closed solids; `cull_mode: None` + the two-sided bias for
663        // two-sided (`Identical`) batches (see the direct-path shadow pipelines above).
664        let make_shadow_cull =
665            |label: &str, cull_mode: Option<wgpu::Face>, bias: wgpu::DepthBiasState| {
666                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
667                    label: Some(label),
668                    layout: Some(&shadow_cull_layout),
669                    vertex: wgpu::VertexState {
670                        module: &shadow_cull_shader,
671                        entry_point: Some("vs_shadow_cull"),
672                        buffers: &[Vertex::buffer_layout()],
673                        compilation_options: wgpu::PipelineCompilationOptions::default(),
674                    },
675                    fragment: None,
676                    primitive: wgpu::PrimitiveState {
677                        topology: wgpu::PrimitiveTopology::TriangleList,
678                        cull_mode,
679                        ..Default::default()
680                    },
681                    depth_stencil: Some(wgpu::DepthStencilState {
682                        format: wgpu::TextureFormat::Depth32Float,
683                        depth_write_enabled: true,
684                        depth_compare: wgpu::CompareFunction::Less,
685                        stencil: wgpu::StencilState::default(),
686                        bias,
687                    }),
688                    multisample: wgpu::MultisampleState::default(),
689                    multiview: None,
690                    cache: None,
691                })
692            };
693        let shadow_instanced_cull = make_shadow_cull(
694            "shadow_instanced_cull_pipeline",
695            Some(wgpu::Face::Front),
696            crate::resources::mesh::mesh_pipelines::CSM_SHADOW_BIAS,
697        );
698        let shadow_instanced_cull_two_sided = make_shadow_cull(
699            "shadow_instanced_cull_two_sided_pipeline",
700            None,
701            crate::resources::mesh::mesh_pipelines::CSM_SHADOW_BIAS_TWO_SIDED,
702        );
703        self.cull.shadow_pipeline = Some(shadow_instanced_cull);
704        self.cull.shadow_two_sided_pipeline = Some(shadow_instanced_cull_two_sided);
705        self.cull.shadow_bgl = Some(shadow_cull_bgl);
706    }
707
708    /// Get or create the shadow cull instance bind group for a given cascade index.
709    ///
710    /// Binds `instance_storage_buf` (binding 0) and `shadow_vis_bufs[cascade_idx]` (binding 5).
711    /// Returns `None` if the required buffers or BGL are not yet allocated.
712    pub(crate) fn get_shadow_cull_instance_bind_group<'a>(
713        &self,
714        shadow_cull: &'a mut crate::resources::ShadowCullState,
715        device: &wgpu::Device,
716        cascade_idx: usize,
717    ) -> Option<&'a wgpu::BindGroup> {
718        if shadow_cull.shadow_cull_instance_bgs[cascade_idx].is_none() {
719            let bgl = self.cull.shadow_bgl.as_ref()?;
720            let inst_buf = self.instancing.storage_buf.as_ref()?;
721            let vis_buf = shadow_cull.shadow_vis_bufs[cascade_idx].as_ref()?;
722            let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
723                label: Some(&format!("shadow_cull_instance_bg_{cascade_idx}")),
724                layout: bgl,
725                entries: &[
726                    wgpu::BindGroupEntry {
727                        binding: 0,
728                        resource: inst_buf.as_entire_binding(),
729                    },
730                    wgpu::BindGroupEntry {
731                        binding: 5,
732                        resource: vis_buf.as_entire_binding(),
733                    },
734                ],
735            });
736            shadow_cull.shadow_cull_instance_bgs[cascade_idx] = Some(bg);
737        }
738        shadow_cull.shadow_cull_instance_bgs[cascade_idx].as_ref()
739    }
740
741    /// Get or create a cull-path bind group for the instanced cull pipeline.
742    ///
743    /// Identical to `get_instance_bind_group` but uses `instance_cull_bind_group_layout`
744    /// and includes the `visibility_index_buf` at binding 5.
745    pub(crate) fn get_instance_cull_bind_group<'a>(
746        &self,
747        cull_state: &'a mut crate::resources::ViewportCullState,
748        device: &wgpu::Device,
749        albedo_id: Option<crate::resources::TextureId>,
750        normal_map_id: Option<crate::resources::TextureId>,
751        ao_map_id: Option<crate::resources::TextureId>,
752    ) -> Option<&'a wgpu::BindGroup> {
753        let key = (
754            albedo_id.map(|t| t.raw()).unwrap_or(u64::MAX),
755            normal_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
756            ao_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
757        );
758
759        if !cull_state.instance_cull_bind_groups.contains_key(&key) {
760            let bgl = self.cull.bind_group_layout.as_ref()?;
761            let inst_buf = self.instancing.storage_buf.as_ref()?;
762            let vis_buf = cull_state.visibility_index_buf.as_ref()?;
763
764            let albedo_view = match albedo_id {
765                Some(id) if self.content.textures.get(id).is_some() => {
766                    &self.content.textures.get(id).unwrap().view
767                }
768                _ => &self.fallback_texture.view,
769            };
770            let normal_view = match normal_map_id {
771                Some(id) if self.content.textures.get(id).is_some() => {
772                    &self.content.textures.get(id).unwrap().view
773                }
774                _ => &self.fallback_normal_map_view,
775            };
776            let ao_view = match ao_map_id {
777                Some(id) if self.content.textures.get(id).is_some() => {
778                    &self.content.textures.get(id).unwrap().view
779                }
780                _ => &self.fallback_ao_map_view,
781            };
782
783            let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
784                label: Some("instance_cull_tex_bg"),
785                layout: bgl,
786                entries: &[
787                    wgpu::BindGroupEntry {
788                        binding: 0,
789                        resource: inst_buf.as_entire_binding(),
790                    },
791                    wgpu::BindGroupEntry {
792                        binding: 1,
793                        resource: wgpu::BindingResource::TextureView(albedo_view),
794                    },
795                    wgpu::BindGroupEntry {
796                        binding: 2,
797                        resource: wgpu::BindingResource::Sampler(&self.material_sampler),
798                    },
799                    wgpu::BindGroupEntry {
800                        binding: 3,
801                        resource: wgpu::BindingResource::TextureView(normal_view),
802                    },
803                    wgpu::BindGroupEntry {
804                        binding: 4,
805                        resource: wgpu::BindingResource::TextureView(ao_view),
806                    },
807                    wgpu::BindGroupEntry {
808                        binding: 5,
809                        resource: vis_buf.as_entire_binding(),
810                    },
811                ],
812            });
813            cull_state.instance_cull_bind_groups.insert(key, bg);
814        }
815
816        cull_state.instance_cull_bind_groups.get(&key)
817    }
818
819    /// Get or create a combined instance+texture bind group for the instanced pipeline.
820    ///
821    /// The bind group combines the shared instance storage buffer (binding 0) with the
822    /// texture views for the given material key (bindings 1-4). Results are cached by key.
823    ///
824    /// `u64::MAX` in any key component means "use fallback texture for that slot".
825    pub(crate) fn get_instance_bind_group(
826        &mut self,
827        device: &wgpu::Device,
828        albedo_id: Option<crate::resources::TextureId>,
829        normal_map_id: Option<crate::resources::TextureId>,
830        ao_map_id: Option<crate::resources::TextureId>,
831    ) -> Option<&wgpu::BindGroup> {
832        let key = (
833            albedo_id.map(|t| t.raw()).unwrap_or(u64::MAX),
834            normal_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
835            ao_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
836        );
837
838        if !self.instancing.bind_groups.contains_key(&key) {
839            let bgl = self.instancing.bind_group_layout.as_ref()?;
840            let buf = self.instancing.storage_buf.as_ref()?;
841
842            let albedo_view = match albedo_id {
843                Some(id) if self.content.textures.get(id).is_some() => {
844                    &self.content.textures.get(id).unwrap().view
845                }
846                _ => &self.fallback_texture.view,
847            };
848            let normal_view = match normal_map_id {
849                Some(id) if self.content.textures.get(id).is_some() => {
850                    &self.content.textures.get(id).unwrap().view
851                }
852                _ => &self.fallback_normal_map_view,
853            };
854            let ao_view = match ao_map_id {
855                Some(id) if self.content.textures.get(id).is_some() => {
856                    &self.content.textures.get(id).unwrap().view
857                }
858                _ => &self.fallback_ao_map_view,
859            };
860
861            let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
862                label: Some("instance_tex_bg"),
863                layout: bgl,
864                entries: &[
865                    wgpu::BindGroupEntry {
866                        binding: 0,
867                        resource: buf.as_entire_binding(),
868                    },
869                    wgpu::BindGroupEntry {
870                        binding: 1,
871                        resource: wgpu::BindingResource::TextureView(albedo_view),
872                    },
873                    wgpu::BindGroupEntry {
874                        binding: 2,
875                        resource: wgpu::BindingResource::Sampler(&self.material_sampler),
876                    },
877                    wgpu::BindGroupEntry {
878                        binding: 3,
879                        resource: wgpu::BindingResource::TextureView(normal_view),
880                    },
881                    wgpu::BindGroupEntry {
882                        binding: 4,
883                        resource: wgpu::BindingResource::TextureView(ao_view),
884                    },
885                ],
886            });
887            self.instancing.bind_groups.insert(key, bg);
888        }
889
890        self.instancing.bind_groups.get(&key)
891    }
892
893    /// Upload one [`MeshInstanceItem`] batch and return draw data.
894    ///
895    /// Builds a per-batch instance storage buffer in the layout expected by
896    /// `mesh_instanced.wgsl`'s `InstanceData` struct, allocates a one-shot
897    /// bind group against `instance_bind_group_layout`, and packages it into
898    /// a [`MeshInstanceGpuData`]. The host is expected to rebuild this once
899    /// per frame for moving particle systems.
900    ///
901    /// Lighting flags are filled with unlit defaults: the shader receives the
902    /// per-instance colour with the optional albedo sampled on top, without
903    /// going through shadow or lighting math.
904    pub(crate) fn upload_mesh_instance(
905        &mut self,
906        device: &wgpu::Device,
907        queue: &wgpu::Queue,
908        item: &crate::renderer::MeshInstanceItem,
909    ) -> Option<crate::resources::types::MeshInstanceGpuData> {
910        self.upload_mesh_instance_from(device, queue, item, item.mesh_id, None)
911    }
912
913    /// Build a mesh-instance batch from a subset of an item's instances drawn
914    /// with a chosen mesh. `indices` selects which instances to include (and in
915    /// what order); `None` uses every instance in order. `mesh_id` overrides the
916    /// item's own mesh, which is how LOD draws the same item at several detail
917    /// levels: one call per level with that level's mesh and its instances.
918    pub(crate) fn upload_mesh_instance_from(
919        &mut self,
920        device: &wgpu::Device,
921        queue: &wgpu::Queue,
922        item: &crate::renderer::MeshInstanceItem,
923        mesh_id: crate::resources::mesh::mesh_store::MeshId,
924        indices: Option<&[u32]>,
925    ) -> Option<crate::resources::types::MeshInstanceGpuData> {
926        let instance_count = match indices {
927            Some(idx) => idx.len() as u32,
928            None => item.transforms.len() as u32,
929        };
930        if instance_count == 0 {
931            return None;
932        }
933
934        // Per-instance struct must match `InstanceData` in `mesh_instanced.wgsl`
935        // and `resources::types::InstanceData` (176 bytes).
936        #[repr(C)]
937        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
938        struct GpuInstanceData {
939            model: [[f32; 4]; 4],
940            colour: [f32; 4],
941            selected: u32,
942            wireframe: u32,
943            ambient: f32,
944            diffuse: f32,
945            specular: f32,
946            shininess: f32,
947            has_texture: u32,
948            use_pbr: u32,
949            metallic: f32,
950            roughness: f32,
951            has_normal_map: u32,
952            has_ao_map: u32,
953            unlit: u32,
954            receive_shadows: u32,
955            use_flat: u32,
956            _pad_inst1: u32,
957            uv_transform: [f32; 4],
958            ao_range: [f32; 2],
959            _pad_ao_range: [f32; 2],
960        }
961
962        const _: () = assert!(std::mem::size_of::<GpuInstanceData>() == 176);
963
964        let has_texture = if item
965            .texture_id
966            .is_some_and(|id| self.content.textures.get(id).is_some())
967        {
968            1u32
969        } else {
970            0u32
971        };
972
973        let build = |i: usize| -> GpuInstanceData {
974            GpuInstanceData {
975                model: item.transforms[i],
976                colour: item.colours.get(i).copied().unwrap_or([1.0, 1.0, 1.0, 1.0]),
977                selected: 0,
978                wireframe: 0,
979                ambient: 1.0,
980                diffuse: 0.0,
981                specular: 0.0,
982                shininess: 0.0,
983                has_texture,
984                use_pbr: 0,
985                metallic: 0.0,
986                roughness: 0.0,
987                has_normal_map: 0,
988                has_ao_map: 0,
989                unlit: 1,
990                receive_shadows: 0,
991                use_flat: 1,
992                _pad_inst1: 0,
993                uv_transform: [0.0, 0.0, 1.0, 1.0],
994                ao_range: [0.0, 1.0],
995                _pad_ao_range: [0.0, 0.0],
996            }
997        };
998        let instances: Vec<GpuInstanceData> = match indices {
999            Some(idx) => idx.iter().map(|&i| build(i as usize)).collect(),
1000            None => (0..item.transforms.len()).map(build).collect(),
1001        };
1002
1003        let instance_bytes = bytemuck::cast_slice(&instances);
1004        let instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
1005            label: Some("mesh_instance_buf"),
1006            size: instance_bytes
1007                .len()
1008                .max(std::mem::size_of::<GpuInstanceData>()) as u64,
1009            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1010            mapped_at_creation: false,
1011        });
1012        queue.write_buffer(&instance_buf, 0, instance_bytes);
1013
1014        let bgl = self.instancing.bind_group_layout.as_ref()?;
1015        let albedo_view = match item.texture_id {
1016            Some(id) if self.content.textures.get(id).is_some() => {
1017                &self.content.textures.get(id).unwrap().view
1018            }
1019            _ => &self.fallback_texture.view,
1020        };
1021        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1022            label: Some("mesh_instance_bg"),
1023            layout: bgl,
1024            entries: &[
1025                wgpu::BindGroupEntry {
1026                    binding: 0,
1027                    resource: instance_buf.as_entire_binding(),
1028                },
1029                wgpu::BindGroupEntry {
1030                    binding: 1,
1031                    resource: wgpu::BindingResource::TextureView(albedo_view),
1032                },
1033                wgpu::BindGroupEntry {
1034                    binding: 2,
1035                    resource: wgpu::BindingResource::Sampler(&self.material_sampler),
1036                },
1037                wgpu::BindGroupEntry {
1038                    binding: 3,
1039                    resource: wgpu::BindingResource::TextureView(&self.fallback_normal_map_view),
1040                },
1041                wgpu::BindGroupEntry {
1042                    binding: 4,
1043                    resource: wgpu::BindingResource::TextureView(&self.fallback_ao_map_view),
1044                },
1045            ],
1046        });
1047
1048        Some(crate::resources::types::MeshInstanceGpuData {
1049            mesh_id,
1050            instance_count,
1051            bind_group,
1052            blend: item.blend,
1053            _instance_buf: instance_buf,
1054        })
1055    }
1056}
1057
1058/// Per-object uniform: world transform, material properties, selection state, and wireframe mode.
1059///
1060/// Layout (256 bytes, 16-byte aligned):
1061/// - model:                    [[f32;4];4] = 64 bytes  offset   0
1062/// - colour:                     [f32;4]   = 16 bytes  offset  64  (base_colour.xyz + opacity)
1063/// - selected:                   u32      =  4 bytes  offset  80
1064/// - wireframe:                  u32      =  4 bytes  offset  84
1065/// - ambient:                    f32      =  4 bytes  offset  88
1066/// - diffuse:                    f32      =  4 bytes  offset  92
1067/// - specular:                   f32      =  4 bytes  offset  96
1068/// - shininess:                  f32      =  4 bytes  offset 100
1069/// - has_texture:                u32      =  4 bytes  offset 104
1070/// - use_pbr:                    u32      =  4 bytes  offset 108
1071/// - metallic:                   f32      =  4 bytes  offset 112
1072/// - roughness:                  f32      =  4 bytes  offset 116
1073/// - has_normal_map:             u32      =  4 bytes  offset 120
1074/// - has_ao_map:                 u32      =  4 bytes  offset 124
1075/// - has_attribute:              u32      =  4 bytes  offset 128
1076/// - scalar_min:                 f32      =  4 bytes  offset 132
1077/// - scalar_max:                 f32      =  4 bytes  offset 136
1078/// - _pad_scalar:                u32      =  4 bytes  offset 140
1079/// - nan_colour:                 [f32;4]   = 16 bytes  offset 144
1080/// - use_nan_colour:              u32      =  4 bytes  offset 160
1081/// - use_matcap:                 u32      =  4 bytes  offset 164
1082/// - matcap_blendable:           u32      =  4 bytes  offset 168
1083/// - unlit:                      u32      =  4 bytes  offset 172
1084/// - use_face_colour:             u32      =  4 bytes  offset 176
1085/// - uv_vis_mode:                u32      =  4 bytes  offset 180  (0=off 1=checker 2=grid 3=localcheck 4=localrad)
1086/// - uv_vis_scale:               f32      =  4 bytes  offset 184
1087/// - backface_policy:            u32      =  4 bytes  offset 188  (0=Cull 1=Identical 2=DifferentColour)
1088/// - backface_colour:            [f32;4]   = 16 bytes  offset 192
1089/// - has_warp:                   u32      =  4 bytes  offset 208
1090/// - warp_scale:                 f32      =  4 bytes  offset 212
1091/// - has_position_override:      u32      =  4 bytes  offset 216
1092/// - has_normal_override:        u32      =  4 bytes  offset 220
1093/// - emissive:                   [f32;3]  = 12 bytes  offset 224
1094/// - use_flat:                   u32      =  4 bytes  offset 236  (1=flat shading, recover N from world_pos derivatives)
1095/// - alpha_mode:                 u32      =  4 bytes  offset 240  (0=Opaque, 1=Mask, 2=Blend)
1096/// - alpha_cutoff:               f32      =  4 bytes  offset 244
1097/// - has_metallic_roughness_tex: u32      =  4 bytes  offset 248
1098/// - has_emissive_tex:           u32      =  4 bytes  offset 252
1099/// - uv_transform:               [f32;4]  = 16 bytes  offset 256  (offset.xy, scale.xy)
1100/// - deform_flags:               u32      =  4 bytes  offset 272  (bit i = deformer slot i active)
1101/// - _pad_after_deform:          u32      =  4 bytes  offset 276  (align next vec2 to 8)
1102/// - ao_range:                   [f32;2]  =  8 bytes  offset 280
1103/// - metallic_range:             [f32;2]  =  8 bytes  offset 288
1104/// - roughness_range:            [f32;2]  =  8 bytes  offset 296
1105/// Total: 304 bytes
1106#[repr(C)]
1107#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
1108pub(crate) struct ObjectUniform {
1109    pub(crate) model: [[f32; 4]; 4], //  64 bytes, offset   0
1110    pub(crate) colour: [f32; 4],     //  16 bytes, offset  64
1111    pub(crate) selected: u32,        //   4 bytes, offset  80
1112    pub(crate) wireframe: u32,       //   4 bytes, offset  84
1113    pub(crate) ambient: f32,         //   4 bytes, offset  88
1114    pub(crate) diffuse: f32,         //   4 bytes, offset  92
1115    pub(crate) specular: f32,        //   4 bytes, offset  96
1116    pub(crate) shininess: f32,       //   4 bytes, offset 100
1117    pub(crate) has_texture: u32,     //   4 bytes, offset 104
1118    pub(crate) use_pbr: u32,         //   4 bytes, offset 108
1119    pub(crate) metallic: f32,        //   4 bytes, offset 112
1120    pub(crate) roughness: f32,       //   4 bytes, offset 116
1121    pub(crate) has_normal_map: u32,  //   4 bytes, offset 120
1122    pub(crate) has_ao_map: u32,      //   4 bytes, offset 124
1123    pub(crate) has_attribute: u32,   //   4 bytes, offset 128
1124    pub(crate) scalar_min: f32,      //   4 bytes, offset 132
1125    pub(crate) scalar_max: f32,      //   4 bytes, offset 136
1126    /// 1 = sample the shadow atlas, 0 = treat the fragment as unshadowed.
1127    /// Wired from `ItemSettings.receive_shadows`.
1128    pub(crate) receive_shadows: u32, //   4 bytes, offset 140
1129    pub(crate) nan_colour: [f32; 4], //  16 bytes, offset 144
1130    pub(crate) use_nan_colour: u32,  //   4 bytes, offset 160
1131    pub(crate) use_matcap: u32,      //   4 bytes, offset 164
1132    pub(crate) matcap_blendable: u32, //   4 bytes, offset 168
1133    pub(crate) unlit: u32,           //   4 bytes, offset 172
1134    pub(crate) use_face_colour: u32, //   4 bytes, offset 176
1135    pub(crate) uv_vis_mode: u32,     //   4 bytes, offset 180
1136    pub(crate) uv_vis_scale: f32,    //   4 bytes, offset 184
1137    pub(crate) backface_policy: u32, //   4 bytes, offset 188  (0=Cull 1=Identical 2=DifferentColour)
1138    pub(crate) backface_colour: [f32; 4], //  16 bytes, offset 192
1139    pub(crate) has_warp: u32,        //   4 bytes, offset 208
1140    pub(crate) warp_scale: f32,      //   4 bytes, offset 212
1141    /// 1 when a per-vertex position storage buffer is bound at group 1 binding 13.
1142    /// Wired from `GpuMesh::position_override_buffer.is_some()`.
1143    pub(crate) has_position_override: u32, //   4 bytes, offset 216
1144    /// 1 when a per-vertex normal storage buffer is bound at group 1 binding 14.
1145    pub(crate) has_normal_override: u32, //   4 bytes, offset 220
1146    pub(crate) emissive: [f32; 3],   //  12 bytes, offset 224
1147    /// 1 = recover the shading normal from screen-space derivatives of
1148    /// `world_pos` (`ShadingModel::Flat`); 0 = use the interpolated vertex
1149    /// normal (or TBN normal map when bound).
1150    pub(crate) use_flat: u32, //   4 bytes, offset 236
1151    pub(crate) alpha_mode: u32,      //   4 bytes, offset 240  (0=Opaque, 1=Mask, 2=Blend)
1152    pub(crate) alpha_cutoff: f32,    //   4 bytes, offset 244
1153    pub(crate) has_metallic_roughness_tex: u32, //   4 bytes, offset 248
1154    pub(crate) has_emissive_tex: u32, //   4 bytes, offset 252
1155    /// Per-material UV transform applied to every texture sample.
1156    /// `[offset_x, offset_y, scale_x, scale_y]`. Defaults to `(0, 0, 1, 1)`
1157    /// (identity). Lets atlas-packed materials share one mesh instance.
1158    pub(crate) uv_transform: [f32; 4], //  16 bytes, offset 256
1159    /// Bit `i` set when deformer slot `i` is active for this draw. Zero when
1160    /// no deformer registry has attached data for this mesh.
1161    pub(crate) deform_flags: u32, //   4 bytes, offset 272
1162    pub(crate) _pad_after_deform: u32, //   4 bytes, offset 276 (align next vec2 to 8)
1163    /// Min/max remap applied to the AO map's R sample (identity `[0, 1]`).
1164    /// Mirrors `Material::ao_range`.
1165    pub(crate) ao_range: [f32; 2], //   8 bytes, offset 280
1166    /// Min/max remap applied to the metallic sample (B channel of the MR
1167    /// texture). Identity `[0, 1]`. Mirrors `Material::metallic_range`.
1168    pub(crate) metallic_range: [f32; 2], //   8 bytes, offset 288
1169    /// Min/max remap applied to the roughness sample (G channel of the MR
1170    /// texture). Identity `[0, 1]`. Mirrors `Material::roughness_range`.
1171    pub(crate) roughness_range: [f32; 2], //   8 bytes, offset 296
1172}
1173
1174const _: () = assert!(std::mem::size_of::<ObjectUniform>() == 304);
1175/// Per-instance GPU data for instanced rendering. Matches the WGSL `InstanceData` struct.
1176///
1177/// Layout: 176 bytes.
1178#[repr(C)]
1179#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
1180pub(crate) struct InstanceData {
1181    pub(crate) model: [[f32; 4]; 4], //  64 bytes, offset   0
1182    pub(crate) colour: [f32; 4],     //  16 bytes, offset  64
1183    pub(crate) selected: u32,        //   4 bytes, offset  80
1184    pub(crate) wireframe: u32,       //   4 bytes, offset  84
1185    pub(crate) ambient: f32,         //   4 bytes, offset  88
1186    pub(crate) diffuse: f32,         //   4 bytes, offset  92
1187    pub(crate) specular: f32,        //   4 bytes, offset  96
1188    pub(crate) shininess: f32,       //   4 bytes, offset 100
1189    pub(crate) has_texture: u32,     //   4 bytes, offset 104
1190    pub(crate) use_pbr: u32,         //   4 bytes, offset 108
1191    pub(crate) metallic: f32,        //   4 bytes, offset 112
1192    pub(crate) roughness: f32,       //   4 bytes, offset 116
1193    pub(crate) has_normal_map: u32,  //   4 bytes, offset 120
1194    pub(crate) has_ao_map: u32,      //   4 bytes, offset 124
1195    pub(crate) unlit: u32,           //   4 bytes, offset 128
1196    /// 1 = sample the shadow atlas, 0 = treat the fragment as unshadowed.
1197    pub(crate) receive_shadows: u32, //   4 bytes, offset 132
1198    /// 1 = recover the shading normal from screen-space derivatives of
1199    /// `world_pos` (`ShadingModel::Flat`).
1200    pub(crate) use_flat: u32, //   4 bytes, offset 136
1201    pub(crate) _pad_inst: u32,       //   4 bytes, offset 140
1202    /// Per-material UV transform; mirrors `ObjectUniform::uv_transform`.
1203    /// `[offset_x, offset_y, scale_x, scale_y]`.
1204    pub(crate) uv_transform: [f32; 4], //  16 bytes, offset 144
1205    /// Min/max remap applied to the AO map's R sample (identity `[0, 1]`).
1206    /// Mirrors `Material::ao_range`. The instanced mesh shaders do not sample
1207    /// the MR texture today, so `metallic_range` / `roughness_range` are
1208    /// intentionally absent from `InstanceData`.
1209    pub(crate) ao_range: [f32; 2], //   8 bytes, offset 160
1210    pub(crate) _pad_ao_range: [f32; 2], //  8 bytes, offset 168 (struct stride to 16B)
1211}
1212
1213const _: () = assert!(std::mem::size_of::<InstanceData>() == 176);
1214/// Per-instance GPU data for the object-ID pick pass.
1215///
1216/// Stores only the model matrix and a sentinel object ID : none of the material
1217/// fields needed by the full [`InstanceData`] struct.
1218///
1219/// Layout (80 bytes):
1220/// - model_c0..model_c3: vec4<f32> x 4 = 64 bytes (model matrix, column-major)
1221/// - object_id: u32                     =  4 bytes  (sentinel: scene_items_index + 1)
1222/// - _pad: [u32; 3]                     = 12 bytes  (align to 16)
1223/// Total: 80 bytes
1224#[repr(C)]
1225#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
1226pub(crate) struct PickInstance {
1227    pub(crate) model_c0: [f32; 4],
1228    pub(crate) model_c1: [f32; 4],
1229    pub(crate) model_c2: [f32; 4],
1230    pub(crate) model_c3: [f32; 4],
1231    pub(crate) object_id: u32,
1232    pub(crate) _pad: [u32; 3],
1233}
1234
1235const _: () = assert!(std::mem::size_of::<PickInstance>() == 80);
1236/// Per-instance world-space AABB, uploaded to GPU for the compute cull pass.
1237///
1238/// Layout (32 bytes):
1239/// - min:         [f32; 3] = 12 bytes, offset  0
1240/// - batch_index: u32      =  4 bytes, offset 12 (index into batch_meta_buf)
1241/// - max:         [f32; 3] = 12 bytes, offset 16
1242/// - _pad:        u32      =  4 bytes, offset 28
1243#[repr(C)]
1244#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
1245pub(crate) struct InstanceAabb {
1246    pub(crate) min: [f32; 3],
1247    pub(crate) batch_index: u32,
1248    pub(crate) max: [f32; 3],
1249    /// 1 = item participates in shadow casting, 0 = skipped during shadow cull.
1250    pub(crate) cast_shadows: u32,
1251}
1252
1253const _: () = assert!(std::mem::size_of::<InstanceAabb>() == 32);
1254/// Per-batch metadata read by the GPU cull pass.
1255///
1256/// One entry per batch in the `batch_meta` storage buffer attached to a
1257/// [`CullSubmission`](crate::plugin_api::CullSubmission). Layout (32 bytes,
1258/// 16-byte aligned):
1259///
1260/// - `index_count`:     `u32` - index range used by this batch's draw
1261/// - `first_index`:     `u32` - index buffer offset (typically 0)
1262/// - `instance_offset`: `u32` - first instance for this batch in the AABB buffer
1263/// - `instance_count`:  `u32` - number of instances belonging to this batch
1264/// - `vis_offset`:      `u32` - first slot in the visibility output buffer
1265/// - `is_transparent`:  `u32` - `1` marks a transparent batch
1266/// - `_pad`:            `[u32; 2]`
1267///
1268/// `vis_offset` is a prefix sum of `instance_count` across batches; for a
1269/// scene where instances are laid out contiguously per batch it equals
1270/// `instance_offset`.
1271#[repr(C)]
1272#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
1273pub struct BatchMeta {
1274    /// Mesh index count for one instance.
1275    pub index_count: u32,
1276    /// First index offset into the bound index buffer.
1277    pub first_index: u32,
1278    /// Offset into the instance AABB buffer where this batch begins.
1279    pub instance_offset: u32,
1280    /// Number of instances in the batch.
1281    pub instance_count: u32,
1282    /// First slot in the visibility output buffer this batch writes to.
1283    pub vis_offset: u32,
1284    /// `1` if the batch is transparent, `0` for opaque.
1285    pub is_transparent: u32,
1286    /// Padding to keep the struct 16-byte aligned.
1287    pub _pad: [u32; 2],
1288}
1289
1290const _: () = assert!(std::mem::size_of::<BatchMeta>() == 32);