Skip to main content

viewport_lib/resources/
init.rs

1use super::*;
2
3impl DeviceResources {
4    /// Create all GPU resources for the viewport.
5    ///
6    /// Call once at application startup. `target_format` must match the swap-chain surface
7    /// format. Use `sample_count = 1` unless the caller is providing MSAA resolve targets.
8    pub fn new(
9        device: &wgpu::Device,
10        target_format: wgpu::TextureFormat,
11        sample_count: u32,
12    ) -> Self {
13        Self::new_with_cache(device, target_format, sample_count, None)
14    }
15
16    /// Like [`new`](Self::new), but seeds a `wgpu::PipelineCache` from previously
17    /// saved data so shader compilation can be skipped on later launches.
18    ///
19    /// `pipeline_cache_data` should come from a prior
20    /// [`ViewportRenderer::pipeline_cache_data`](crate::renderer::ViewportRenderer::pipeline_cache_data)
21    /// call, or `None` on first run. The cache is only created when the device
22    /// enables `Features::PIPELINE_CACHE`; otherwise this behaves exactly like
23    /// `new` and the data is ignored.
24    pub fn new_with_cache(
25        device: &wgpu::Device,
26        target_format: wgpu::TextureFormat,
27        sample_count: u32,
28        pipeline_cache_data: Option<&[u8]>,
29    ) -> Self {
30        use wgpu;
31
32        // A pipeline cache records compiled pipelines so a later run (seeded with
33        // saved data) skips recompilation. Only available when the device enables
34        // `Features::PIPELINE_CACHE`; `fallback: true` discards stale/invalid data
35        // rather than failing. Safety: the data, if any, came from a prior
36        // `PipelineCache::get_data` call as the API requires.
37        let pipeline_cache = if device.features().contains(wgpu::Features::PIPELINE_CACHE) {
38            Some(unsafe {
39                device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
40                    label: Some("viewport_pipeline_cache"),
41                    data: pipeline_cache_data,
42                    fallback: true,
43                })
44            })
45        } else {
46            None
47        };
48
49        // Cold-start instrumentation. Pipeline compilation and large depth-texture
50        // allocation can dominate construction on some backends (notably Adreno
51        // Vulkan, where pipeline creation compiles shaders synchronously). These
52        // marks attribute the per-phase cost. Filter with
53        // `RUST_LOG=viewport_lib::init=info`.
54        let init_start = std::time::Instant::now();
55        let mut init_ckpt = init_start;
56        let mut mark = |section: &str| {
57            let now = std::time::Instant::now();
58            tracing::info!(
59                target: "viewport_lib::init",
60                section,
61                ms = now.duration_since(init_ckpt).as_secs_f32() * 1000.0,
62                "gpu resources init phase"
63            );
64            init_ckpt = now;
65        };
66
67        // ------------------------------------------------------------------
68        // Shader module
69        // ------------------------------------------------------------------
70        // iced_wgpu and other WebGL2-targeting backends cap max_bind_groups at
71        // 2. viewport-lib's mesh shader family uses 3 groups (camera, object,
72        // deform). On limited devices we compile the _noop variants which omit
73        // the deform group, and build 2-group pipeline layouts instead.
74        let deform_enabled = device.limits().max_bind_groups >= 3;
75
76        let mesh_src = if deform_enabled {
77            include_str!(concat!(env!("OUT_DIR"), "/mesh.wgsl"))
78        } else {
79            include_str!(concat!(env!("OUT_DIR"), "/mesh_noop.wgsl"))
80        };
81        let shader = crate::resources::builders::wgsl_module(device, "mesh_shader", mesh_src);
82
83        // ------------------------------------------------------------------
84        // Bind group layouts
85        // ------------------------------------------------------------------
86        let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
87            label: Some("camera_bgl"),
88            entries: &[
89                wgpu::BindGroupLayoutEntry {
90                    binding: 0,
91                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
92                    ty: wgpu::BindingType::Buffer {
93                        ty: wgpu::BufferBindingType::Uniform,
94                        has_dynamic_offset: false,
95                        min_binding_size: None,
96                    },
97                    count: None,
98                },
99                wgpu::BindGroupLayoutEntry {
100                    binding: 1,
101                    visibility: wgpu::ShaderStages::FRAGMENT,
102                    ty: wgpu::BindingType::Texture {
103                        sample_type: wgpu::TextureSampleType::Depth,
104                        view_dimension: wgpu::TextureViewDimension::D2,
105                        multisampled: false,
106                    },
107                    count: None,
108                },
109                wgpu::BindGroupLayoutEntry {
110                    binding: 2,
111                    visibility: wgpu::ShaderStages::FRAGMENT,
112                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
113                    count: None,
114                },
115                wgpu::BindGroupLayoutEntry {
116                    binding: 3,
117                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
118                    ty: wgpu::BindingType::Buffer {
119                        ty: wgpu::BufferBindingType::Uniform,
120                        has_dynamic_offset: false,
121                        min_binding_size: None,
122                    },
123                    count: None,
124                },
125                // Binding 4: clip planes uniform (section view clipping).
126                wgpu::BindGroupLayoutEntry {
127                    binding: 4,
128                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
129                    ty: wgpu::BindingType::Buffer {
130                        ty: wgpu::BufferBindingType::Uniform,
131                        has_dynamic_offset: false,
132                        min_binding_size: None,
133                    },
134                    count: None,
135                },
136                // Binding 5: shadow atlas uniform (CSM matrices, splits, PCSS params).
137                wgpu::BindGroupLayoutEntry {
138                    binding: 5,
139                    visibility: wgpu::ShaderStages::FRAGMENT,
140                    ty: wgpu::BindingType::Buffer {
141                        ty: wgpu::BufferBindingType::Uniform,
142                        has_dynamic_offset: false,
143                        min_binding_size: None,
144                    },
145                    count: None,
146                },
147                // Binding 6: clip volume uniform (box/sphere/plane extended clip region).
148                wgpu::BindGroupLayoutEntry {
149                    binding: 6,
150                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
151                    ty: wgpu::BindingType::Buffer {
152                        ty: wgpu::BufferBindingType::Uniform,
153                        has_dynamic_offset: false,
154                        min_binding_size: None,
155                    },
156                    count: None,
157                },
158                // Binding 7: IBL irradiance equirect texture.
159                wgpu::BindGroupLayoutEntry {
160                    binding: 7,
161                    visibility: wgpu::ShaderStages::FRAGMENT,
162                    ty: wgpu::BindingType::Texture {
163                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
164                        view_dimension: wgpu::TextureViewDimension::D2,
165                        multisampled: false,
166                    },
167                    count: None,
168                },
169                // Binding 8: IBL prefiltered specular equirect texture.
170                wgpu::BindGroupLayoutEntry {
171                    binding: 8,
172                    visibility: wgpu::ShaderStages::FRAGMENT,
173                    ty: wgpu::BindingType::Texture {
174                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
175                        view_dimension: wgpu::TextureViewDimension::D2,
176                        multisampled: false,
177                    },
178                    count: None,
179                },
180                // Binding 9: BRDF integration LUT texture.
181                wgpu::BindGroupLayoutEntry {
182                    binding: 9,
183                    visibility: wgpu::ShaderStages::FRAGMENT,
184                    ty: wgpu::BindingType::Texture {
185                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
186                        view_dimension: wgpu::TextureViewDimension::D2,
187                        multisampled: false,
188                    },
189                    count: None,
190                },
191                // Binding 10: IBL sampler (linear, clamp-to-edge).
192                wgpu::BindGroupLayoutEntry {
193                    binding: 10,
194                    visibility: wgpu::ShaderStages::FRAGMENT,
195                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
196                    count: None,
197                },
198                // Binding 11: Skybox/environment equirect texture (full-res for skybox).
199                wgpu::BindGroupLayoutEntry {
200                    binding: 11,
201                    visibility: wgpu::ShaderStages::FRAGMENT,
202                    ty: wgpu::BindingType::Texture {
203                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
204                        view_dimension: wgpu::TextureViewDimension::D2,
205                        multisampled: false,
206                    },
207                    count: None,
208                },
209                // Binding 12: per-fragment debug storage buffer (written in debug_vis.wgsl).
210                // Sized to viewport_width * viewport_height * 16 bytes when debug is active;
211                // a 16-byte sentinel buffer is used otherwise.
212                wgpu::BindGroupLayoutEntry {
213                    binding: 12,
214                    visibility: wgpu::ShaderStages::FRAGMENT,
215                    ty: wgpu::BindingType::Buffer {
216                        ty: wgpu::BufferBindingType::Storage { read_only: false },
217                        has_dynamic_offset: false,
218                        min_binding_size: None,
219                    },
220                    count: None,
221                },
222                // Binding 13: read-only storage buffer of `SingleLightUniform`
223                // entries. Indexed against the `count` field of the lights
224                // header uniform (binding 3). Capacity = `MAX_SCENE_LIGHTS`.
225                wgpu::BindGroupLayoutEntry {
226                    binding: 13,
227                    visibility: wgpu::ShaderStages::FRAGMENT,
228                    ty: wgpu::BindingType::Buffer {
229                        ty: wgpu::BufferBindingType::Storage { read_only: true },
230                        has_dynamic_offset: false,
231                        min_binding_size: None,
232                    },
233                    count: None,
234                },
235                // Binding 14: clustered-shading grid uniform (dimensions,
236                // near/far, screen size, fallback flag).
237                wgpu::BindGroupLayoutEntry {
238                    binding: 14,
239                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
240                    ty: wgpu::BindingType::Buffer {
241                        ty: wgpu::BufferBindingType::Uniform,
242                        has_dynamic_offset: false,
243                        min_binding_size: None,
244                    },
245                    count: None,
246                },
247                // Binding 15: cluster cell storage (offset + count per cell),
248                // read-only in the fragment stage.
249                wgpu::BindGroupLayoutEntry {
250                    binding: 15,
251                    visibility: wgpu::ShaderStages::FRAGMENT,
252                    ty: wgpu::BindingType::Buffer {
253                        ty: wgpu::BufferBindingType::Storage { read_only: true },
254                        has_dynamic_offset: false,
255                        min_binding_size: None,
256                    },
257                    count: None,
258                },
259                // Binding 16: global cluster light index list, read-only in
260                // the fragment stage.
261                wgpu::BindGroupLayoutEntry {
262                    binding: 16,
263                    visibility: wgpu::ShaderStages::FRAGMENT,
264                    ty: wgpu::BindingType::Buffer {
265                        ty: wgpu::BufferBindingType::Storage { read_only: true },
266                        has_dynamic_offset: false,
267                        min_binding_size: None,
268                    },
269                    count: None,
270                },
271                // Binding 17: point-light shadow depth array.
272                // iOS Metal does not support cube array textures, so on that
273                // target the shader uses texture_depth_2d_array and we bind a
274                // D2Array view instead.
275                wgpu::BindGroupLayoutEntry {
276                    binding: 17,
277                    visibility: wgpu::ShaderStages::FRAGMENT,
278                    ty: wgpu::BindingType::Texture {
279                        sample_type: wgpu::TextureSampleType::Depth,
280                        view_dimension: if cfg!(target_os = "ios") {
281                            wgpu::TextureViewDimension::D2Array
282                        } else {
283                            wgpu::TextureViewDimension::CubeArray
284                        },
285                        multisampled: false,
286                    },
287                    count: None,
288                },
289            ],
290        });
291
292        // Object bind group layout (group 1 for non-instanced pipelines).
293        // binding 0: per-object uniform (model matrix, material, selection state)
294        // binding 1: albedo texture (filterable)
295        // binding 2: shared filtering sampler
296        // binding 3: normal map texture (filterable)
297        // binding 4: AO map texture (filterable)
298        //
299        // Textures are co-located in group 1 (rather than a separate group 2) so that
300        // the total bind group count stays at 2, compatible with iced's wgpu device
301        // which hardcodes max_bind_groups = 2 in its DeviceDescriptor.
302        let object_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
303            label: Some("object_bgl"),
304            entries: &[
305                // binding 0: per-object uniform buffer
306                wgpu::BindGroupLayoutEntry {
307                    binding: 0,
308                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
309                    ty: wgpu::BindingType::Buffer {
310                        ty: wgpu::BufferBindingType::Uniform,
311                        has_dynamic_offset: false,
312                        min_binding_size: None,
313                    },
314                    count: None,
315                },
316                // binding 1: albedo texture (filterable)
317                wgpu::BindGroupLayoutEntry {
318                    binding: 1,
319                    visibility: wgpu::ShaderStages::FRAGMENT,
320                    ty: wgpu::BindingType::Texture {
321                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
322                        view_dimension: wgpu::TextureViewDimension::D2,
323                        multisampled: false,
324                    },
325                    count: None,
326                },
327                // binding 2: shared filtering sampler
328                wgpu::BindGroupLayoutEntry {
329                    binding: 2,
330                    visibility: wgpu::ShaderStages::FRAGMENT,
331                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
332                    count: None,
333                },
334                // binding 3: normal map texture (filterable)
335                wgpu::BindGroupLayoutEntry {
336                    binding: 3,
337                    visibility: wgpu::ShaderStages::FRAGMENT,
338                    ty: wgpu::BindingType::Texture {
339                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
340                        view_dimension: wgpu::TextureViewDimension::D2,
341                        multisampled: false,
342                    },
343                    count: None,
344                },
345                // binding 4: AO map texture (filterable)
346                wgpu::BindGroupLayoutEntry {
347                    binding: 4,
348                    visibility: wgpu::ShaderStages::FRAGMENT,
349                    ty: wgpu::BindingType::Texture {
350                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
351                        view_dimension: wgpu::TextureViewDimension::D2,
352                        multisampled: false,
353                    },
354                    count: None,
355                },
356                // binding 5: LUT (colourmap) texture (256x1 Rgba8Unorm, FRAGMENT, filterable)
357                wgpu::BindGroupLayoutEntry {
358                    binding: 5,
359                    visibility: wgpu::ShaderStages::FRAGMENT,
360                    ty: wgpu::BindingType::Texture {
361                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
362                        view_dimension: wgpu::TextureViewDimension::D2,
363                        multisampled: false,
364                    },
365                    count: None,
366                },
367                // binding 6: scalar attribute storage buffer (VERTEX | FRAGMENT, read-only)
368                wgpu::BindGroupLayoutEntry {
369                    binding: 6,
370                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
371                    ty: wgpu::BindingType::Buffer {
372                        ty: wgpu::BufferBindingType::Storage { read_only: true },
373                        has_dynamic_offset: false,
374                        min_binding_size: None,
375                    },
376                    count: None,
377                },
378                // binding 7: matcap texture (FRAGMENT, filterable 2D)
379                wgpu::BindGroupLayoutEntry {
380                    binding: 7,
381                    visibility: wgpu::ShaderStages::FRAGMENT,
382                    ty: wgpu::BindingType::Texture {
383                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
384                        view_dimension: wgpu::TextureViewDimension::D2,
385                        multisampled: false,
386                    },
387                    count: None,
388                },
389                // binding 8: per-face colour storage buffer (VERTEX | FRAGMENT, read-only)
390                wgpu::BindGroupLayoutEntry {
391                    binding: 8,
392                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
393                    ty: wgpu::BindingType::Buffer {
394                        ty: wgpu::BufferBindingType::Storage { read_only: true },
395                        has_dynamic_offset: false,
396                        min_binding_size: None,
397                    },
398                    count: None,
399                },
400                // binding 9: warp vector storage buffer (VERTEX, read-only)
401                wgpu::BindGroupLayoutEntry {
402                    binding: 9,
403                    visibility: wgpu::ShaderStages::VERTEX,
404                    ty: wgpu::BindingType::Buffer {
405                        ty: wgpu::BufferBindingType::Storage { read_only: true },
406                        has_dynamic_offset: false,
407                        min_binding_size: None,
408                    },
409                    count: None,
410                },
411                // binding 10: LUT clamp sampler (FRAGMENT, filtering)
412                wgpu::BindGroupLayoutEntry {
413                    binding: 10,
414                    visibility: wgpu::ShaderStages::FRAGMENT,
415                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
416                    count: None,
417                },
418                // binding 11: metallic-roughness ORM texture (FRAGMENT, filterable)
419                wgpu::BindGroupLayoutEntry {
420                    binding: 11,
421                    visibility: wgpu::ShaderStages::FRAGMENT,
422                    ty: wgpu::BindingType::Texture {
423                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
424                        view_dimension: wgpu::TextureViewDimension::D2,
425                        multisampled: false,
426                    },
427                    count: None,
428                },
429                // binding 12: emissive texture (FRAGMENT, filterable)
430                wgpu::BindGroupLayoutEntry {
431                    binding: 12,
432                    visibility: wgpu::ShaderStages::FRAGMENT,
433                    ty: wgpu::BindingType::Texture {
434                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
435                        view_dimension: wgpu::TextureViewDimension::D2,
436                        multisampled: false,
437                    },
438                    count: None,
439                },
440                // binding 13: position override storage buffer (VERTEX, read-only).
441                // Fallback sentinel (1x vec3<f32>) is bound when no override is set.
442                wgpu::BindGroupLayoutEntry {
443                    binding: 13,
444                    visibility: wgpu::ShaderStages::VERTEX,
445                    ty: wgpu::BindingType::Buffer {
446                        ty: wgpu::BufferBindingType::Storage { read_only: true },
447                        has_dynamic_offset: false,
448                        min_binding_size: None,
449                    },
450                    count: None,
451                },
452                // binding 14: normal override storage buffer (VERTEX, read-only).
453                wgpu::BindGroupLayoutEntry {
454                    binding: 14,
455                    visibility: wgpu::ShaderStages::VERTEX,
456                    ty: wgpu::BindingType::Buffer {
457                        ty: wgpu::BufferBindingType::Storage { read_only: true },
458                        has_dynamic_offset: false,
459                        min_binding_size: None,
460                    },
461                    count: None,
462                },
463            ],
464        });
465
466        // Texture-only bind group layout : kept for the instanced pipeline (group 1 bindings
467        // 1-4 are added alongside the storage buffer binding 0 in init_instanced_pipeline).
468        // Also used as the standalone layout when creating material bind groups keyed by
469        // texture combination for the instanced path.
470        let texture_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
471            label: Some("texture_bgl"),
472            entries: &[
473                // binding 0: albedo texture (filterable)
474                wgpu::BindGroupLayoutEntry {
475                    binding: 0,
476                    visibility: wgpu::ShaderStages::FRAGMENT,
477                    ty: wgpu::BindingType::Texture {
478                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
479                        view_dimension: wgpu::TextureViewDimension::D2,
480                        multisampled: false,
481                    },
482                    count: None,
483                },
484                // binding 1: shared filtering sampler
485                wgpu::BindGroupLayoutEntry {
486                    binding: 1,
487                    visibility: wgpu::ShaderStages::FRAGMENT,
488                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
489                    count: None,
490                },
491                // binding 2: normal map texture (filterable)
492                wgpu::BindGroupLayoutEntry {
493                    binding: 2,
494                    visibility: wgpu::ShaderStages::FRAGMENT,
495                    ty: wgpu::BindingType::Texture {
496                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
497                        view_dimension: wgpu::TextureViewDimension::D2,
498                        multisampled: false,
499                    },
500                    count: None,
501                },
502                // binding 3: AO map texture (filterable)
503                wgpu::BindGroupLayoutEntry {
504                    binding: 3,
505                    visibility: wgpu::ShaderStages::FRAGMENT,
506                    ty: wgpu::BindingType::Texture {
507                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
508                        view_dimension: wgpu::TextureViewDimension::D2,
509                        multisampled: false,
510                    },
511                    count: None,
512                },
513            ],
514        });
515
516        mark("shaders_and_bind_group_layouts");
517
518        // ------------------------------------------------------------------
519        // Per-vertex deformation sidecar. Constructed early because every
520        // mesh-family pipeline layout binds its group(2) BGL.
521        // ------------------------------------------------------------------
522        let deform = if deform_enabled {
523            crate::resources::mesh_sidecar::deform::DeformationState::new(device)
524        } else {
525            crate::resources::mesh_sidecar::deform::DeformationState::new_disabled(device)
526        };
527        let deform_bgl = deform.enabled.then_some(&deform.bind_group_layout);
528
529        // ------------------------------------------------------------------
530        // Pipeline layout (shared between solid and transparent pipelines)
531        // Groups: 0=camera, 1=object+texture, and optionally 2=deform sidecar
532        // ------------------------------------------------------------------
533        let pipeline_layout = crate::resources::mesh::mesh_pipelines::mesh_pipeline_layout(
534            device,
535            "mesh_pipeline_layout",
536            &camera_bgl,
537            &object_bgl,
538            deform_bgl,
539        );
540
541        // ------------------------------------------------------------------
542        // LDR mesh.wgsl pipelines: solid + two-sided + transparent + wireframe.
543        // Built through the shared factory so `register_deformer` can rebuild
544        // them with a freshly composed shader module.
545        // ------------------------------------------------------------------
546        let ldr = crate::resources::mesh::mesh_pipelines::build_ldr_mesh_pipelines(
547            device,
548            &pipeline_layout,
549            &shader,
550            target_format,
551            sample_count,
552            pipeline_cache.as_ref(),
553        );
554        let solid_pipeline = ldr.solid;
555        let solid_two_sided_pipeline = ldr.solid_two_sided;
556        let transparent_pipeline = ldr.transparent;
557        let wireframe_pipeline = ldr.wireframe;
558
559        mark("mesh_pipelines");
560
561        // ------------------------------------------------------------------
562        // Camera uniform buffer and bind group
563        // ------------------------------------------------------------------
564        let camera_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
565            label: Some("camera_uniform_buf"),
566            size: std::mem::size_of::<CameraUniform>() as u64,
567            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
568            mapped_at_creation: false,
569        });
570
571        let light_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
572            label: Some("light_uniform_buf"),
573            size: std::mem::size_of::<LightUniform>() as u64,
574            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
575            mapped_at_creation: false,
576        });
577
578        let light_storage_buf = device.create_buffer(&wgpu::BufferDescriptor {
579            label: Some("light_storage_buf"),
580            size: (std::mem::size_of::<crate::resources::SingleLightUniform>()
581                * crate::resources::MAX_SCENE_LIGHTS) as u64,
582            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
583            mapped_at_creation: false,
584        });
585
586        // Clip planes uniform buffer (binding 4 of camera bind group).
587        // Initialized to count=0 (no active clip planes).
588        let clip_planes_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
589            label: Some("clip_planes_uniform_buf"),
590            size: std::mem::size_of::<ClipPlanesUniform>() as u64,
591            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
592            mapped_at_creation: false,
593        });
594
595        // Clip volume uniform buffer (binding 6 of camera bind group).
596        // Holds up to CLIP_VOLUME_MAX box/sphere entries; initialized to count=0 (no volumes).
597        let clip_volume_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
598            label: Some("clip_volume_uniform_buf"),
599            size: std::mem::size_of::<ClipVolumesUniform>() as u64,
600            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
601            mapped_at_creation: false,
602        });
603
604        // ------------------------------------------------------------------
605        // Shadow map texture, sampler, and bind group
606        // ------------------------------------------------------------------
607        let shadow_map_texture = device.create_texture(&wgpu::TextureDescriptor {
608            label: Some("shadow_atlas"),
609            size: wgpu::Extent3d {
610                width: SHADOW_ATLAS_SIZE,
611                height: SHADOW_ATLAS_SIZE,
612                depth_or_array_layers: 1,
613            },
614            mip_level_count: 1,
615            sample_count: 1,
616            dimension: wgpu::TextureDimension::D2,
617            format: wgpu::TextureFormat::Depth32Float,
618            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
619            view_formats: &[],
620        });
621        let shadow_map_view =
622            shadow_map_texture.create_view(&wgpu::TextureViewDescriptor::default());
623
624        let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
625            label: Some("shadow_sampler"),
626            compare: Some(wgpu::CompareFunction::LessEqual),
627            mag_filter: wgpu::FilterMode::Linear,
628            min_filter: wgpu::FilterMode::Linear,
629            ..Default::default()
630        });
631
632        // ------------------------------------------------------------------
633        // Point-light cubemap shadow texture array.
634        //
635        // Stored as a 2D texture array of `MAX_POINT_SHADOW_LIGHTS * 6`
636        // layers and viewed two ways:
637        //  - Per-face 2D-array views (one per face) for shadow render passes.
638        //  - A `CubeArray` view bound to the lit pass for sampling.
639        // ------------------------------------------------------------------
640        let point_shadow_face_size = crate::renderer::POINT_SHADOW_FACE_SIZE;
641        let point_shadow_max_lights = crate::renderer::MAX_POINT_SHADOW_LIGHTS;
642        let point_shadow_layers = point_shadow_max_lights * 6;
643        let point_shadow_cube_texture = device.create_texture(&wgpu::TextureDescriptor {
644            label: Some("point_shadow_cube_array"),
645            size: wgpu::Extent3d {
646                width: point_shadow_face_size,
647                height: point_shadow_face_size,
648                depth_or_array_layers: point_shadow_layers,
649            },
650            mip_level_count: 1,
651            sample_count: 1,
652            dimension: wgpu::TextureDimension::D2,
653            format: wgpu::TextureFormat::Depth32Float,
654            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
655            view_formats: &[],
656        });
657        let point_shadow_cube_view =
658            point_shadow_cube_texture.create_view(&wgpu::TextureViewDescriptor {
659                label: Some("point_shadow_cube_view"),
660                // iOS Metal does not support CubeArray views. Use D2Array instead;
661                // the shader is patched at build time to match.
662                dimension: Some(if cfg!(target_os = "ios") {
663                    wgpu::TextureViewDimension::D2Array
664                } else {
665                    wgpu::TextureViewDimension::CubeArray
666                }),
667                aspect: wgpu::TextureAspect::DepthOnly,
668                base_array_layer: 0,
669                array_layer_count: Some(point_shadow_layers),
670                base_mip_level: 0,
671                mip_level_count: Some(1),
672                format: Some(wgpu::TextureFormat::Depth32Float),
673                usage: None,
674            });
675        let point_shadow_face_views: Vec<wgpu::TextureView> = (0..point_shadow_layers)
676            .map(|layer| {
677                point_shadow_cube_texture.create_view(&wgpu::TextureViewDescriptor {
678                    label: Some("point_shadow_face_view"),
679                    dimension: Some(wgpu::TextureViewDimension::D2),
680                    aspect: wgpu::TextureAspect::DepthOnly,
681                    base_array_layer: layer,
682                    array_layer_count: Some(1),
683                    base_mip_level: 0,
684                    mip_level_count: Some(1),
685                    format: Some(wgpu::TextureFormat::Depth32Float),
686                    usage: None,
687                })
688            })
689            .collect();
690
691        // Includes the 4096^2 directional atlas and the point-shadow cube array
692        // (POINT_SHADOW_FACE_SIZE^2 * MAX_POINT_SHADOW_LIGHTS * 6 layers), both
693        // allocated unconditionally here. Watch this number on mobile.
694        mark("buffers_and_shadow_textures");
695
696        // Non-comparison sampler (no compare field) for plain float depth reads.
697        let shadow_atlas_depth_sampler =
698            crate::resources::builders::clamp_nearest_sampler(device, "shadow_atlas_depth_sampler");
699
700        // Shadow atlas uniform buffer (binding 5).
701        let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
702            label: Some("shadow_info_buf"),
703            size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
704            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
705            mapped_at_creation: false,
706        });
707
708        // ------------------------------------------------------------------
709        // IBL fallback textures: 1x1 black (Rgba16Float) placeholder for all IBL slots,
710        // and a linear/repeat sampler. Never sampled : the `ibl_enabled` uniform guard
711        // prevents IBL calculations when no environment map is uploaded.
712        // ------------------------------------------------------------------
713        let ibl_fallback_texture = device.create_texture(&wgpu::TextureDescriptor {
714            label: Some("ibl_fallback_black"),
715            size: wgpu::Extent3d {
716                width: 1,
717                height: 1,
718                depth_or_array_layers: 1,
719            },
720            mip_level_count: 1,
721            sample_count: 1,
722            dimension: wgpu::TextureDimension::D2,
723            format: wgpu::TextureFormat::Rgba16Float,
724            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
725            view_formats: &[],
726        });
727        let ibl_fallback_view =
728            ibl_fallback_texture.create_view(&wgpu::TextureViewDescriptor::default());
729
730        // BRDF integration LUT placeholder: a 1x1 black fallback that's swapped for the real
731        // 128x128 LUT on the first call to `upload_environment_map`. The LUT is scene-independent
732        // (function of roughness x N.V only); idempotent caching inside `upload_environment_map`
733        // means subsequent uploads skip its ~16.7M Hammersley samples.
734        let ibl_fallback_brdf_texture = device.create_texture(&wgpu::TextureDescriptor {
735            label: Some("ibl_fallback_brdf"),
736            size: wgpu::Extent3d {
737                width: 1,
738                height: 1,
739                depth_or_array_layers: 1,
740            },
741            mip_level_count: 1,
742            sample_count: 1,
743            dimension: wgpu::TextureDimension::D2,
744            format: wgpu::TextureFormat::Rgba16Float,
745            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
746            view_formats: &[],
747        });
748        let ibl_fallback_brdf_view =
749            ibl_fallback_brdf_texture.create_view(&wgpu::TextureViewDescriptor::default());
750
751        let ibl_sampler = crate::resources::builders::env_sampler(device, "ibl_sampler");
752
753        // 16-byte sentinel bound at group 0 binding 12 when the debug fragment buffer is inactive.
754        let debug_frag_sentinel_buf = device.create_buffer(&wgpu::BufferDescriptor {
755            label: Some("debug_frag_sentinel_buf"),
756            size: 16,
757            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
758            mapped_at_creation: false,
759        });
760
761        let clustered = crate::resources::gpu::clustered::ClusteredResources::new(device);
762
763        mark("clustered");
764
765        let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
766            label: Some("camera_bind_group"),
767            layout: &camera_bgl,
768            entries: &[
769                wgpu::BindGroupEntry {
770                    binding: 0,
771                    resource: camera_uniform_buf.as_entire_binding(),
772                },
773                wgpu::BindGroupEntry {
774                    binding: 1,
775                    resource: wgpu::BindingResource::TextureView(&shadow_map_view),
776                },
777                wgpu::BindGroupEntry {
778                    binding: 2,
779                    resource: wgpu::BindingResource::Sampler(&shadow_sampler),
780                },
781                wgpu::BindGroupEntry {
782                    binding: 3,
783                    resource: light_uniform_buf.as_entire_binding(),
784                },
785                wgpu::BindGroupEntry {
786                    binding: 4,
787                    resource: clip_planes_uniform_buf.as_entire_binding(),
788                },
789                wgpu::BindGroupEntry {
790                    binding: 5,
791                    resource: shadow_info_buf.as_entire_binding(),
792                },
793                wgpu::BindGroupEntry {
794                    binding: 6,
795                    resource: clip_volume_uniform_buf.as_entire_binding(),
796                },
797                // IBL textures (bindings 7-11) : fallback until environment is uploaded.
798                wgpu::BindGroupEntry {
799                    binding: 7,
800                    resource: wgpu::BindingResource::TextureView(&ibl_fallback_view),
801                },
802                wgpu::BindGroupEntry {
803                    binding: 8,
804                    resource: wgpu::BindingResource::TextureView(&ibl_fallback_view),
805                },
806                wgpu::BindGroupEntry {
807                    binding: 9,
808                    resource: wgpu::BindingResource::TextureView(&ibl_fallback_brdf_view),
809                },
810                wgpu::BindGroupEntry {
811                    binding: 10,
812                    resource: wgpu::BindingResource::Sampler(&ibl_sampler),
813                },
814                wgpu::BindGroupEntry {
815                    binding: 11,
816                    resource: wgpu::BindingResource::TextureView(&ibl_fallback_view),
817                },
818                wgpu::BindGroupEntry {
819                    binding: 12,
820                    resource: debug_frag_sentinel_buf.as_entire_binding(),
821                },
822                wgpu::BindGroupEntry {
823                    binding: 13,
824                    resource: light_storage_buf.as_entire_binding(),
825                },
826                wgpu::BindGroupEntry {
827                    binding: 14,
828                    resource: clustered.grid_uniform_buf.as_entire_binding(),
829                },
830                wgpu::BindGroupEntry {
831                    binding: 15,
832                    resource: clustered.cluster_grid_buf.as_entire_binding(),
833                },
834                wgpu::BindGroupEntry {
835                    binding: 16,
836                    resource: clustered.light_index_buf.as_entire_binding(),
837                },
838                wgpu::BindGroupEntry {
839                    binding: 17,
840                    resource: wgpu::BindingResource::TextureView(&point_shadow_cube_view),
841                },
842            ],
843        });
844
845        // ------------------------------------------------------------------
846        // Shadow pass pipeline (depth-only, renders from light's POV)
847        // ------------------------------------------------------------------
848        let shadow_src = if deform_enabled {
849            include_str!(concat!(env!("OUT_DIR"), "/shadow.wgsl"))
850        } else {
851            include_str!(concat!(env!("OUT_DIR"), "/shadow_noop.wgsl"))
852        };
853        let shadow_shader =
854            crate::resources::builders::wgsl_module(device, "shadow_shader", shadow_src);
855
856        // Shadow pass uses a simple bind group layout: just the light uniform.
857        let shadow_camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
858            label: Some("shadow_camera_bgl"),
859            entries: &[wgpu::BindGroupLayoutEntry {
860                binding: 0,
861                visibility: wgpu::ShaderStages::VERTEX,
862                ty: wgpu::BindingType::Buffer {
863                    ty: wgpu::BufferBindingType::Uniform,
864                    // Dynamic offset lets the cascade loop select per-cascade matrix slot
865                    // without calling write_buffer inside the render pass (which would be
866                    // a no-op per-cascade since wgpu batches all writes before execution).
867                    has_dynamic_offset: true,
868                    min_binding_size: None,
869                },
870                count: None,
871            }],
872        });
873
874        let shadow_pl_bgls: Vec<&wgpu::BindGroupLayout> = if let Some(d) = deform_bgl {
875            vec![&shadow_camera_bgl, &object_bgl, d]
876        } else {
877            vec![&shadow_camera_bgl, &object_bgl]
878        };
879        let shadow_pipeline_layout =
880            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
881                label: Some("shadow_pipeline_layout"),
882                bind_group_layouts: &shadow_pl_bgls,
883                push_constant_ranges: &[],
884            });
885
886        // Depth-only pass through the shared factory so register_deformer
887        // can rebuild it from composed source. Two variants:
888        // - cull-front (default) for closed solids: back faces are the
889        //   casters, so a solid's own front face is never compared against
890        //   itself in the shadow map.
891        // - cull-none for two-sided materials (`BackfacePolicy::Identical`):
892        //   both sides of cloth and planar surfaces rasterise; a larger
893        //   caster-side bias keeps the receiver from self-shadowing.
894        let shadow_pipeline = crate::resources::mesh::mesh_pipelines::build_shadow_pipeline(
895            device,
896            &shadow_pipeline_layout,
897            &shadow_shader,
898            Some(wgpu::Face::Front),
899            pipeline_cache.as_ref(),
900        );
901        let shadow_pipeline_two_sided =
902            crate::resources::mesh::mesh_pipelines::build_shadow_pipeline(
903                device,
904                &shadow_pipeline_layout,
905                &shadow_shader,
906                None,
907                pipeline_cache.as_ref(),
908            );
909
910        // Shadow pass uniform buffer : 4 cascade slots x 256 bytes (wgpu dynamic-offset alignment).
911        // Each slot holds one 4x4 matrix (64 bytes); the remaining 192 bytes per slot are padding.
912        const SHADOW_SLOT_STRIDE: u64 = 256;
913        let shadow_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
914            label: Some("shadow_uniform_buf"),
915            size: 4 * SHADOW_SLOT_STRIDE,
916            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
917            mapped_at_creation: false,
918        });
919
920        let shadow_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
921            label: Some("shadow_bind_group"),
922            layout: &shadow_camera_bgl,
923            entries: &[wgpu::BindGroupEntry {
924                binding: 0,
925                // Bind only the first 64-byte matrix slot; dynamic offset selects cascade.
926                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
927                    buffer: &shadow_uniform_buf,
928                    offset: 0,
929                    size: Some(
930                        wgpu::BufferSize::new(std::mem::size_of::<[[f32; 4]; 4]>() as u64).unwrap(),
931                    ),
932                }),
933            }],
934        });
935
936        // ------------------------------------------------------------------
937        // Point-light cubemap shadow pipeline.
938        //
939        // One render pass per (light slot, face) writes linear distance to
940        // the light into a per-face depth attachment. The bind group layout
941        // mirrors the cascade pipeline (same object + deform bind groups)
942        // and carries a per-face uniform with view_proj + light_pos + range.
943        // ------------------------------------------------------------------
944        let shadow_point_src = if deform_enabled {
945            include_str!(concat!(env!("OUT_DIR"), "/shadow_point.wgsl"))
946        } else {
947            include_str!(concat!(env!("OUT_DIR"), "/shadow_point_noop.wgsl"))
948        };
949        let shadow_point_shader = crate::resources::builders::wgsl_module(
950            device,
951            "shadow_point_shader",
952            shadow_point_src,
953        );
954        let shadow_point_face_bgl =
955            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
956                label: Some("shadow_point_face_bgl"),
957                entries: &[wgpu::BindGroupLayoutEntry {
958                    binding: 0,
959                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
960                    ty: wgpu::BindingType::Buffer {
961                        ty: wgpu::BufferBindingType::Uniform,
962                        has_dynamic_offset: true,
963                        min_binding_size: None,
964                    },
965                    count: None,
966                }],
967            });
968        let shadow_point_pl_bgls: Vec<&wgpu::BindGroupLayout> = if let Some(d) = deform_bgl {
969            vec![&shadow_point_face_bgl, &object_bgl, d]
970        } else {
971            vec![&shadow_point_face_bgl, &object_bgl]
972        };
973        let shadow_point_pipeline_layout =
974            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
975                label: Some("shadow_point_pipeline_layout"),
976                bind_group_layouts: &shadow_point_pl_bgls,
977                push_constant_ranges: &[],
978            });
979        let shadow_point_pipeline =
980            crate::resources::mesh::mesh_pipelines::build_shadow_point_pipeline(
981                device,
982                &shadow_point_pipeline_layout,
983                &shadow_point_shader,
984                pipeline_cache.as_ref(),
985            );
986
987        // Per-face uniform buffer. Stride 256 satisfies wgpu's dynamic-offset
988        // alignment requirement. Total slots = MAX_POINT_SHADOW_LIGHTS * 6.
989        const SHADOW_POINT_FACE_STRIDE: u64 = 256;
990        let shadow_point_face_count = (point_shadow_max_lights * 6) as u64;
991        let shadow_point_face_buf = device.create_buffer(&wgpu::BufferDescriptor {
992            label: Some("shadow_point_face_buf"),
993            size: shadow_point_face_count * SHADOW_POINT_FACE_STRIDE,
994            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
995            mapped_at_creation: false,
996        });
997        let shadow_point_face_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
998            label: Some("shadow_point_face_bind_group"),
999            layout: &shadow_point_face_bgl,
1000            entries: &[wgpu::BindGroupEntry {
1001                binding: 0,
1002                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1003                    buffer: &shadow_point_face_buf,
1004                    offset: 0,
1005                    // Bind one PointFace slot worth (96 bytes rounded up
1006                    // to 16-byte alignment is fine here).
1007                    size: Some(wgpu::BufferSize::new(96).unwrap()),
1008                }),
1009            }],
1010        });
1011
1012        mark("shadow_pipelines");
1013
1014        // ------------------------------------------------------------------
1015        // Gizmo shader module
1016        // ------------------------------------------------------------------
1017        let gizmo_shader = crate::resources::builders::wgsl_module(
1018            device,
1019            "gizmo_shader",
1020            crate::resources::builders::wgsl_source!("gizmo"),
1021        );
1022
1023        // ------------------------------------------------------------------
1024        // Gizmo bind group layout (group 1: model matrix uniform)
1025        // ------------------------------------------------------------------
1026        let gizmo_bgl = crate::resources::builders::uniform_bgl(
1027            device,
1028            "gizmo_bgl",
1029            wgpu::ShaderStages::VERTEX,
1030        );
1031
1032        // ------------------------------------------------------------------
1033        // Gizmo pipeline layout
1034        // ------------------------------------------------------------------
1035        let gizmo_pipeline_layout =
1036            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1037                label: Some("gizmo_pipeline_layout"),
1038                bind_group_layouts: &[&camera_bgl, &gizmo_bgl],
1039                push_constant_ranges: &[],
1040            });
1041
1042        // ------------------------------------------------------------------
1043        // Gizmo render pipeline
1044        // depth_compare: Always : gizmo always renders on top of scene (Pitfall 8).
1045        // depth_write_enabled: false : do not corrupt depth buffer.
1046        // ------------------------------------------------------------------
1047        let gizmo_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1048            label: Some("gizmo_pipeline"),
1049            layout: Some(&gizmo_pipeline_layout),
1050            vertex: wgpu::VertexState {
1051                module: &gizmo_shader,
1052                entry_point: Some("vs_main"),
1053                buffers: &[Vertex::buffer_layout()],
1054                compilation_options: wgpu::PipelineCompilationOptions::default(),
1055            },
1056            fragment: Some(wgpu::FragmentState {
1057                module: &gizmo_shader,
1058                entry_point: Some("fs_main"),
1059                targets: &[Some(wgpu::ColorTargetState {
1060                    format: target_format,
1061                    blend: None,
1062                    write_mask: wgpu::ColorWrites::ALL,
1063                })],
1064                compilation_options: wgpu::PipelineCompilationOptions::default(),
1065            }),
1066            primitive: wgpu::PrimitiveState {
1067                topology: wgpu::PrimitiveTopology::TriangleList,
1068                strip_index_format: None,
1069                front_face: wgpu::FrontFace::Ccw,
1070                cull_mode: None, // No culling: gizmo geometry is viewed from all angles.
1071                unclipped_depth: false,
1072                polygon_mode: wgpu::PolygonMode::Fill,
1073                conservative: false,
1074            },
1075            depth_stencil: Some(wgpu::DepthStencilState {
1076                format: wgpu::TextureFormat::Depth24PlusStencil8,
1077                depth_write_enabled: false,
1078                depth_compare: wgpu::CompareFunction::Always, // Always on top.
1079                stencil: wgpu::StencilState::default(),
1080                bias: wgpu::DepthBiasState::default(),
1081            }),
1082            multisample: wgpu::MultisampleState {
1083                count: sample_count,
1084                mask: !0,
1085                alpha_to_coverage_enabled: false,
1086            },
1087            multiview: None,
1088            cache: pipeline_cache.as_ref(),
1089        });
1090
1091        // ------------------------------------------------------------------
1092        // Gizmo vertex/index buffers (initial mesh: no hover highlight)
1093        // ------------------------------------------------------------------
1094        let (gizmo_verts, gizmo_indices) =
1095            crate::interaction::manipulation::gizmo::build_gizmo_mesh(
1096                crate::interaction::manipulation::gizmo::GizmoMode::Translate,
1097                crate::interaction::manipulation::gizmo::GizmoAxis::None,
1098                glam::Quat::IDENTITY,
1099            );
1100
1101        let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1102            label: Some("gizmo_vertex_buf"),
1103            size: (std::mem::size_of::<Vertex>() * gizmo_verts.len().max(1)) as u64,
1104            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1105            mapped_at_creation: true,
1106        });
1107        gizmo_vertex_buffer
1108            .slice(..)
1109            .get_mapped_range_mut()
1110            .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
1111        gizmo_vertex_buffer.unmap();
1112
1113        let gizmo_index_count = gizmo_indices.len() as u32;
1114        let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1115            label: Some("gizmo_index_buf"),
1116            size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
1117            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1118            mapped_at_creation: true,
1119        });
1120        gizmo_index_buffer
1121            .slice(..)
1122            .get_mapped_range_mut()
1123            .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
1124        gizmo_index_buffer.unmap();
1125
1126        // ------------------------------------------------------------------
1127        // Gizmo uniform buffer (model matrix : identity until first update)
1128        // ------------------------------------------------------------------
1129        let gizmo_uniform = crate::interaction::manipulation::gizmo::GizmoUniform {
1130            model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1131        };
1132        let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
1133            label: Some("gizmo_uniform_buf"),
1134            size: std::mem::size_of::<crate::interaction::manipulation::gizmo::GizmoUniform>()
1135                as u64,
1136            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1137            mapped_at_creation: true,
1138        });
1139        gizmo_uniform_buf
1140            .slice(..)
1141            .get_mapped_range_mut()
1142            .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
1143        gizmo_uniform_buf.unmap();
1144
1145        let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1146            label: Some("gizmo_bind_group"),
1147            layout: &gizmo_bgl,
1148            entries: &[wgpu::BindGroupEntry {
1149                binding: 0,
1150                resource: gizmo_uniform_buf.as_entire_binding(),
1151            }],
1152        });
1153
1154        // ------------------------------------------------------------------
1155        // Overlay shader module
1156        // ------------------------------------------------------------------
1157        let overlay_shader = crate::resources::builders::wgsl_module(
1158            device,
1159            "overlay_shader",
1160            crate::resources::builders::wgsl_source!("overlay"),
1161        );
1162
1163        // ------------------------------------------------------------------
1164        // Overlay bind group layout (group 1: model + colour uniform)
1165        // ------------------------------------------------------------------
1166        let overlay_bgl = crate::resources::builders::uniform_bgl(
1167            device,
1168            "overlay_bgl",
1169            wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
1170        );
1171
1172        // ------------------------------------------------------------------
1173        // Overlay pipeline layout (group 0: camera, group 1: overlay uniform)
1174        // ------------------------------------------------------------------
1175        let overlay_pipeline_layout =
1176            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1177                label: Some("overlay_pipeline_layout"),
1178                bind_group_layouts: &[&camera_bgl, &overlay_bgl],
1179                push_constant_ranges: &[],
1180            });
1181
1182        // ------------------------------------------------------------------
1183        // Overlay render pipeline
1184        // TriangleList topology with alpha blending for semi-transparent quads.
1185        // depth_write_enabled: false : do not corrupt depth buffer with overlays.
1186        // depth_compare: Less : overlays respect depth (hidden by geometry in front).
1187        // cull_mode: None : quads viewed from both sides.
1188        // ------------------------------------------------------------------
1189        let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1190            label: Some("overlay_pipeline"),
1191            layout: Some(&overlay_pipeline_layout),
1192            vertex: wgpu::VertexState {
1193                module: &overlay_shader,
1194                entry_point: Some("vs_main"),
1195                buffers: &[OverlayVertex::buffer_layout()],
1196                compilation_options: wgpu::PipelineCompilationOptions::default(),
1197            },
1198            fragment: Some(wgpu::FragmentState {
1199                module: &overlay_shader,
1200                entry_point: Some("fs_main"),
1201                targets: &[Some(wgpu::ColorTargetState {
1202                    format: target_format,
1203                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1204                    write_mask: wgpu::ColorWrites::ALL,
1205                })],
1206                compilation_options: wgpu::PipelineCompilationOptions::default(),
1207            }),
1208            primitive: wgpu::PrimitiveState {
1209                topology: wgpu::PrimitiveTopology::TriangleList,
1210                strip_index_format: None,
1211                front_face: wgpu::FrontFace::Ccw,
1212                cull_mode: None, // BC quads are visible from both sides.
1213                unclipped_depth: false,
1214                polygon_mode: wgpu::PolygonMode::Fill,
1215                conservative: false,
1216            },
1217            depth_stencil: Some(wgpu::DepthStencilState {
1218                format: wgpu::TextureFormat::Depth24PlusStencil8,
1219                depth_write_enabled: false, // Do not write to depth buffer.
1220                depth_compare: wgpu::CompareFunction::Less,
1221                stencil: wgpu::StencilState::default(),
1222                bias: wgpu::DepthBiasState::default(),
1223            }),
1224            multisample: wgpu::MultisampleState {
1225                count: sample_count,
1226                mask: !0,
1227                alpha_to_coverage_enabled: false,
1228            },
1229            multiview: None,
1230            cache: pipeline_cache.as_ref(),
1231        });
1232
1233        // ------------------------------------------------------------------
1234        // Overlay line pipeline (LineList)
1235        // Uses the same overlay shader + bind group layout as the triangle overlay.
1236        // No alpha blending needed for line overlays.
1237        // depth_write_enabled: false : overlay lines don't corrupt depth buffer.
1238        // ------------------------------------------------------------------
1239        let overlay_line_pipeline =
1240            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1241                label: Some("overlay_line_pipeline"),
1242                layout: Some(&overlay_pipeline_layout),
1243                vertex: wgpu::VertexState {
1244                    module: &overlay_shader,
1245                    entry_point: Some("vs_main"),
1246                    buffers: &[OverlayVertex::buffer_layout()],
1247                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1248                },
1249                fragment: Some(wgpu::FragmentState {
1250                    module: &overlay_shader,
1251                    entry_point: Some("fs_main"),
1252                    targets: &[Some(wgpu::ColorTargetState {
1253                        format: target_format,
1254                        blend: None,
1255                        write_mask: wgpu::ColorWrites::ALL,
1256                    })],
1257                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1258                }),
1259                primitive: wgpu::PrimitiveState {
1260                    topology: wgpu::PrimitiveTopology::LineList,
1261                    strip_index_format: None,
1262                    front_face: wgpu::FrontFace::Ccw,
1263                    cull_mode: None,
1264                    unclipped_depth: false,
1265                    polygon_mode: wgpu::PolygonMode::Fill,
1266                    conservative: false,
1267                },
1268                depth_stencil: Some(wgpu::DepthStencilState {
1269                    format: wgpu::TextureFormat::Depth24PlusStencil8,
1270                    depth_write_enabled: false,
1271                    depth_compare: wgpu::CompareFunction::Less,
1272                    stencil: wgpu::StencilState::default(),
1273                    bias: wgpu::DepthBiasState::default(),
1274                }),
1275                multisample: wgpu::MultisampleState {
1276                    count: sample_count,
1277                    mask: !0,
1278                    alpha_to_coverage_enabled: false,
1279                },
1280                multiview: None,
1281                cache: pipeline_cache.as_ref(),
1282            });
1283
1284        // ------------------------------------------------------------------
1285        // Full-screen analytical grid pipeline
1286        //
1287        // No vertex buffer. A hardcoded triangle in the vertex shader covers
1288        // the entire screen. The fragment shader ray-marches to the grid plane,
1289        // computes analytical anti-aliased lines with fwidth(), and writes
1290        // clip-space depth via @builtin(frag_depth) for correct occlusion.
1291        // Horizon fade eliminates clipping artefacts at shallow viewing angles.
1292        // ------------------------------------------------------------------
1293        let grid_shader = crate::resources::builders::wgsl_module(
1294            device,
1295            "grid_shader",
1296            crate::resources::builders::wgsl_source!("grid"),
1297        );
1298        let grid_bgl = crate::resources::builders::uniform_bgl(
1299            device,
1300            "grid_bgl",
1301            wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
1302        );
1303        let grid_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1304            label: Some("grid_pipeline_layout"),
1305            bind_group_layouts: &[&grid_bgl],
1306            push_constant_ranges: &[],
1307        });
1308        let grid_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1309            label: Some("grid_pipeline"),
1310            layout: Some(&grid_pipeline_layout),
1311            vertex: wgpu::VertexState {
1312                module: &grid_shader,
1313                entry_point: Some("vs_main"),
1314                buffers: &[], // no vertex buffer : positions hardcoded in shader
1315                compilation_options: wgpu::PipelineCompilationOptions::default(),
1316            },
1317            fragment: Some(wgpu::FragmentState {
1318                module: &grid_shader,
1319                entry_point: Some("fs_main"),
1320                targets: &[Some(wgpu::ColorTargetState {
1321                    format: target_format,
1322                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1323                    write_mask: wgpu::ColorWrites::ALL,
1324                })],
1325                compilation_options: wgpu::PipelineCompilationOptions::default(),
1326            }),
1327            primitive: wgpu::PrimitiveState {
1328                topology: wgpu::PrimitiveTopology::TriangleList,
1329                ..Default::default()
1330            },
1331            depth_stencil: Some(wgpu::DepthStencilState {
1332                format: wgpu::TextureFormat::Depth24PlusStencil8,
1333                depth_write_enabled: true,
1334                depth_compare: wgpu::CompareFunction::LessEqual,
1335                stencil: wgpu::StencilState::default(),
1336                bias: wgpu::DepthBiasState {
1337                    // Push grid depth slightly behind coplanar geometry to prevent
1338                    // z-fighting when object faces coincide with the grid plane.
1339                    // 4 x the minimum representable Depth24 unit ~ 2.4e-7 : invisible
1340                    // at any distance but reliably loses the depth test to geometry.
1341                    constant: 4,
1342                    slope_scale: 0.0,
1343                    clamp: 0.0,
1344                },
1345            }),
1346            multisample: wgpu::MultisampleState {
1347                count: sample_count,
1348                mask: !0,
1349                alpha_to_coverage_enabled: false,
1350            },
1351            multiview: None,
1352            cache: pipeline_cache.as_ref(),
1353        });
1354        // Default-zero uniform : overwritten every frame in prepare().
1355        let grid_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
1356            label: Some("grid_uniform_buf"),
1357            size: std::mem::size_of::<GridUniform>() as u64,
1358            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1359            mapped_at_creation: false,
1360        });
1361        let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1362            label: Some("grid_bind_group"),
1363            layout: &grid_bgl,
1364            entries: &[wgpu::BindGroupEntry {
1365                binding: 0,
1366                resource: grid_uniform_buf.as_entire_binding(),
1367            }],
1368        });
1369
1370        // ------------------------------------------------------------------
1371        // Ground plane pipeline
1372        //
1373        // Full-screen ray-march approach (same as grid).  The fragment shader
1374        // intersects the camera ray with a horizontal plane at a configurable
1375        // Z height, then renders one of four modes: None (skipped), ShadowOnly,
1376        // Tile, SolidColour.  Uses @builtin(frag_depth) for depth occlusion.
1377        // ------------------------------------------------------------------
1378        let ground_plane_shader = crate::resources::builders::wgsl_module(
1379            device,
1380            "ground_plane_shader",
1381            crate::resources::builders::wgsl_source!("ground_plane"),
1382        );
1383        let ground_plane_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1384            label: Some("ground_plane_bgl"),
1385            entries: &[
1386                // binding 0: GroundPlaneUniform
1387                wgpu::BindGroupLayoutEntry {
1388                    binding: 0,
1389                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
1390                    ty: wgpu::BindingType::Buffer {
1391                        ty: wgpu::BufferBindingType::Uniform,
1392                        has_dynamic_offset: false,
1393                        min_binding_size: None,
1394                    },
1395                    count: None,
1396                },
1397                // binding 1: shadow atlas (depth texture)
1398                wgpu::BindGroupLayoutEntry {
1399                    binding: 1,
1400                    visibility: wgpu::ShaderStages::FRAGMENT,
1401                    ty: wgpu::BindingType::Texture {
1402                        sample_type: wgpu::TextureSampleType::Depth,
1403                        view_dimension: wgpu::TextureViewDimension::D2,
1404                        multisampled: false,
1405                    },
1406                    count: None,
1407                },
1408                // binding 2: shadow comparison sampler
1409                wgpu::BindGroupLayoutEntry {
1410                    binding: 2,
1411                    visibility: wgpu::ShaderStages::FRAGMENT,
1412                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
1413                    count: None,
1414                },
1415                // binding 3: shadow atlas info (cascade matrices, splits, atlas rects)
1416                wgpu::BindGroupLayoutEntry {
1417                    binding: 3,
1418                    visibility: wgpu::ShaderStages::FRAGMENT,
1419                    ty: wgpu::BindingType::Buffer {
1420                        ty: wgpu::BufferBindingType::Uniform,
1421                        has_dynamic_offset: false,
1422                        min_binding_size: None,
1423                    },
1424                    count: None,
1425                },
1426            ],
1427        });
1428        let ground_plane_pipeline_layout =
1429            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1430                label: Some("ground_plane_pipeline_layout"),
1431                bind_group_layouts: &[&ground_plane_bgl],
1432                push_constant_ranges: &[],
1433            });
1434        let ground_plane_pipeline =
1435            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1436                label: Some("ground_plane_pipeline"),
1437                layout: Some(&ground_plane_pipeline_layout),
1438                vertex: wgpu::VertexState {
1439                    module: &ground_plane_shader,
1440                    entry_point: Some("vs_main"),
1441                    buffers: &[],
1442                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1443                },
1444                fragment: Some(wgpu::FragmentState {
1445                    module: &ground_plane_shader,
1446                    entry_point: Some("fs_main"),
1447                    targets: &[Some(wgpu::ColorTargetState {
1448                        format: target_format,
1449                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1450                        write_mask: wgpu::ColorWrites::ALL,
1451                    })],
1452                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1453                }),
1454                primitive: wgpu::PrimitiveState {
1455                    topology: wgpu::PrimitiveTopology::TriangleList,
1456                    cull_mode: None,
1457                    ..Default::default()
1458                },
1459                depth_stencil: Some(wgpu::DepthStencilState {
1460                    format: wgpu::TextureFormat::Depth24PlusStencil8,
1461                    depth_write_enabled: true,
1462                    depth_compare: wgpu::CompareFunction::LessEqual,
1463                    stencil: wgpu::StencilState::default(),
1464                    bias: wgpu::DepthBiasState::default(),
1465                }),
1466                multisample: wgpu::MultisampleState {
1467                    count: sample_count,
1468                    mask: !0,
1469                    alpha_to_coverage_enabled: false,
1470                },
1471                multiview: None,
1472                cache: pipeline_cache.as_ref(),
1473            });
1474        let ground_plane_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
1475            label: Some("ground_plane_uniform_buf"),
1476            size: std::mem::size_of::<GroundPlaneUniform>() as u64,
1477            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1478            mapped_at_creation: false,
1479        });
1480        let ground_plane_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1481            label: Some("ground_plane_bind_group"),
1482            layout: &ground_plane_bgl,
1483            entries: &[
1484                wgpu::BindGroupEntry {
1485                    binding: 0,
1486                    resource: ground_plane_uniform_buf.as_entire_binding(),
1487                },
1488                wgpu::BindGroupEntry {
1489                    binding: 1,
1490                    resource: wgpu::BindingResource::TextureView(&shadow_map_view),
1491                },
1492                wgpu::BindGroupEntry {
1493                    binding: 2,
1494                    resource: wgpu::BindingResource::Sampler(&shadow_sampler),
1495                },
1496                wgpu::BindGroupEntry {
1497                    binding: 3,
1498                    resource: shadow_info_buf.as_entire_binding(),
1499                },
1500            ],
1501        });
1502
1503        // ------------------------------------------------------------------
1504        // Shadow atlas viewer pipeline (corner overlay, no vertex buffers)
1505        // ------------------------------------------------------------------
1506        let atlas_blit_shader = crate::resources::builders::wgsl_module(
1507            device,
1508            "shadow_atlas_blit",
1509            crate::resources::builders::wgsl_source!("shadow_atlas_blit"),
1510        );
1511        let atlas_blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1512            label: Some("atlas_blit_bgl"),
1513            entries: &[
1514                wgpu::BindGroupLayoutEntry {
1515                    binding: 0,
1516                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
1517                    ty: wgpu::BindingType::Buffer {
1518                        ty: wgpu::BufferBindingType::Uniform,
1519                        has_dynamic_offset: false,
1520                        min_binding_size: None,
1521                    },
1522                    count: None,
1523                },
1524                wgpu::BindGroupLayoutEntry {
1525                    binding: 1,
1526                    visibility: wgpu::ShaderStages::FRAGMENT,
1527                    ty: wgpu::BindingType::Texture {
1528                        sample_type: wgpu::TextureSampleType::Depth,
1529                        view_dimension: wgpu::TextureViewDimension::D2,
1530                        multisampled: false,
1531                    },
1532                    count: None,
1533                },
1534                wgpu::BindGroupLayoutEntry {
1535                    binding: 2,
1536                    visibility: wgpu::ShaderStages::FRAGMENT,
1537                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
1538                    count: None,
1539                },
1540            ],
1541        });
1542        let atlas_blit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1543            label: Some("atlas_blit_layout"),
1544            bind_group_layouts: &[&atlas_blit_bgl],
1545            push_constant_ranges: &[],
1546        });
1547        let shadow_atlas_viewer_buf = device.create_buffer(&wgpu::BufferDescriptor {
1548            label: Some("shadow_atlas_viewer_buf"),
1549            size: std::mem::size_of::<AtlasBlitUniform>() as u64,
1550            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1551            mapped_at_creation: false,
1552        });
1553        let shadow_atlas_viewer_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1554            label: Some("shadow_atlas_viewer_bg"),
1555            layout: &atlas_blit_bgl,
1556            entries: &[
1557                wgpu::BindGroupEntry {
1558                    binding: 0,
1559                    resource: shadow_atlas_viewer_buf.as_entire_binding(),
1560                },
1561                wgpu::BindGroupEntry {
1562                    binding: 1,
1563                    resource: wgpu::BindingResource::TextureView(&shadow_map_view),
1564                },
1565                wgpu::BindGroupEntry {
1566                    binding: 2,
1567                    resource: wgpu::BindingResource::Sampler(&shadow_atlas_depth_sampler),
1568                },
1569            ],
1570        });
1571        let shadow_atlas_viewer_pipeline =
1572            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1573                label: Some("shadow_atlas_viewer_pipeline"),
1574                layout: Some(&atlas_blit_layout),
1575                vertex: wgpu::VertexState {
1576                    module: &atlas_blit_shader,
1577                    entry_point: Some("vs_main"),
1578                    buffers: &[],
1579                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1580                },
1581                fragment: Some(wgpu::FragmentState {
1582                    module: &atlas_blit_shader,
1583                    entry_point: Some("fs_main"),
1584                    targets: &[Some(wgpu::ColorTargetState {
1585                        format: target_format,
1586                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1587                        write_mask: wgpu::ColorWrites::ALL,
1588                    })],
1589                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1590                }),
1591                primitive: wgpu::PrimitiveState {
1592                    topology: wgpu::PrimitiveTopology::TriangleList,
1593                    cull_mode: None,
1594                    ..Default::default()
1595                },
1596                depth_stencil: Some(wgpu::DepthStencilState {
1597                    format: wgpu::TextureFormat::Depth24PlusStencil8,
1598                    depth_write_enabled: false,
1599                    depth_compare: wgpu::CompareFunction::Always,
1600                    stencil: wgpu::StencilState::default(),
1601                    bias: wgpu::DepthBiasState::default(),
1602                }),
1603                multisample: wgpu::MultisampleState {
1604                    count: sample_count,
1605                    mask: !0,
1606                    alpha_to_coverage_enabled: false,
1607                },
1608                multiview: None,
1609                cache: pipeline_cache.as_ref(),
1610            });
1611
1612        // ------------------------------------------------------------------
1613        // Axes indicator pipeline (screen-space, no camera, no depth)
1614        // ------------------------------------------------------------------
1615        let axes_shader = crate::resources::builders::wgsl_module(
1616            device,
1617            "axes_overlay_shader",
1618            crate::resources::builders::wgsl_source!("axes_overlay"),
1619        );
1620
1621        let axes_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1622            label: Some("axes_pipeline_layout"),
1623            bind_group_layouts: &[],
1624            push_constant_ranges: &[],
1625        });
1626
1627        let axes_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1628            label: Some("axes_pipeline"),
1629            layout: Some(&axes_pipeline_layout),
1630            vertex: wgpu::VertexState {
1631                module: &axes_shader,
1632                entry_point: Some("vs_main"),
1633                buffers: &[
1634                    crate::interaction::widgets::axes_indicator::AxesVertex::buffer_layout(),
1635                ],
1636                compilation_options: wgpu::PipelineCompilationOptions::default(),
1637            },
1638            fragment: Some(wgpu::FragmentState {
1639                module: &axes_shader,
1640                entry_point: Some("fs_main"),
1641                targets: &[Some(wgpu::ColorTargetState {
1642                    format: target_format,
1643                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1644                    write_mask: wgpu::ColorWrites::ALL,
1645                })],
1646                compilation_options: wgpu::PipelineCompilationOptions::default(),
1647            }),
1648            primitive: wgpu::PrimitiveState {
1649                topology: wgpu::PrimitiveTopology::TriangleList,
1650                strip_index_format: None,
1651                front_face: wgpu::FrontFace::Ccw,
1652                cull_mode: None,
1653                unclipped_depth: false,
1654                polygon_mode: wgpu::PolygonMode::Fill,
1655                conservative: false,
1656            },
1657            depth_stencil: Some(wgpu::DepthStencilState {
1658                format: wgpu::TextureFormat::Depth24PlusStencil8,
1659                depth_write_enabled: false,
1660                depth_compare: wgpu::CompareFunction::Always,
1661                stencil: wgpu::StencilState::default(),
1662                bias: wgpu::DepthBiasState::default(),
1663            }),
1664            multisample: wgpu::MultisampleState {
1665                count: sample_count,
1666                mask: !0,
1667                alpha_to_coverage_enabled: false,
1668            },
1669            multiview: None,
1670            cache: pipeline_cache.as_ref(),
1671        });
1672
1673        // Pre-allocate vertex buffer (resized in prepare if needed).
1674        let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1675            label: Some("axes_vertex_buf"),
1676            size: (std::mem::size_of::<crate::interaction::widgets::axes_indicator::AxesVertex>()
1677                * 2048) as u64,
1678            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1679            mapped_at_creation: false,
1680        });
1681
1682        mark("ui_pipelines");
1683
1684        // ------------------------------------------------------------------
1685        // Shared material sampler (linear + repeat : reused for all material textures)
1686        // ------------------------------------------------------------------
1687        let material_sampler = crate::resources::builders::repeat_linear_sampler(
1688            device,
1689            "material_sampler",
1690            wgpu::FilterMode::Nearest,
1691        );
1692
1693        // Clamp-to-edge sampler for colourmap LUT lookups (prevents wrap artifact at scalar extremes).
1694        let lut_sampler = crate::resources::builders::clamp_linear_sampler(device, "lut_sampler");
1695
1696        // ------------------------------------------------------------------
1697        // Fallback normal map: 1x1 [128, 128, 255, 255] : flat tangent-space normal
1698        // ------------------------------------------------------------------
1699        let fallback_normal_map = device.create_texture(&wgpu::TextureDescriptor {
1700            label: Some("fallback_normal_map"),
1701            size: wgpu::Extent3d {
1702                width: 1,
1703                height: 1,
1704                depth_or_array_layers: 1,
1705            },
1706            mip_level_count: 1,
1707            sample_count: 1,
1708            dimension: wgpu::TextureDimension::D2,
1709            format: wgpu::TextureFormat::Rgba8Unorm,
1710            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1711            view_formats: &[],
1712        });
1713        let fallback_normal_map_view =
1714            fallback_normal_map.create_view(&wgpu::TextureViewDescriptor::default());
1715
1716        // ------------------------------------------------------------------
1717        // Fallback AO map: 1x1 [255, 255, 255, 255] : no occlusion
1718        // ------------------------------------------------------------------
1719        let fallback_ao_map = device.create_texture(&wgpu::TextureDescriptor {
1720            label: Some("fallback_ao_map"),
1721            size: wgpu::Extent3d {
1722                width: 1,
1723                height: 1,
1724                depth_or_array_layers: 1,
1725            },
1726            mip_level_count: 1,
1727            sample_count: 1,
1728            dimension: wgpu::TextureDimension::D2,
1729            format: wgpu::TextureFormat::Rgba8Unorm,
1730            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1731            view_formats: &[],
1732        });
1733        let fallback_ao_map_view =
1734            fallback_ao_map.create_view(&wgpu::TextureViewDescriptor::default());
1735
1736        // ------------------------------------------------------------------
1737        // Fallback metallic-roughness texture: 1x1 Rgba8Unorm.
1738        // Content is uninitialized: shader only samples when has_metallic_roughness_tex != 0.
1739        // ------------------------------------------------------------------
1740        let fallback_metallic_roughness_texture = device.create_texture(&wgpu::TextureDescriptor {
1741            label: Some("fallback_metallic_roughness_texture"),
1742            size: wgpu::Extent3d {
1743                width: 1,
1744                height: 1,
1745                depth_or_array_layers: 1,
1746            },
1747            mip_level_count: 1,
1748            sample_count: 1,
1749            dimension: wgpu::TextureDimension::D2,
1750            format: wgpu::TextureFormat::Rgba8Unorm,
1751            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1752            view_formats: &[],
1753        });
1754        let fallback_metallic_roughness_texture_view = fallback_metallic_roughness_texture
1755            .create_view(&wgpu::TextureViewDescriptor::default());
1756
1757        // ------------------------------------------------------------------
1758        // Fallback emissive texture: 1x1 Rgba8Unorm.
1759        // Content is uninitialized: shader only samples when has_emissive_tex != 0.
1760        // ------------------------------------------------------------------
1761        let fallback_emissive_texture = device.create_texture(&wgpu::TextureDescriptor {
1762            label: Some("fallback_emissive_texture"),
1763            size: wgpu::Extent3d {
1764                width: 1,
1765                height: 1,
1766                depth_or_array_layers: 1,
1767            },
1768            mip_level_count: 1,
1769            sample_count: 1,
1770            dimension: wgpu::TextureDimension::D2,
1771            format: wgpu::TextureFormat::Rgba8Unorm,
1772            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1773            view_formats: &[],
1774        });
1775        let fallback_emissive_texture_view =
1776            fallback_emissive_texture.create_view(&wgpu::TextureViewDescriptor::default());
1777
1778        // ------------------------------------------------------------------
1779        // Fallback texture: 1x1 white RGBA (used when no albedo texture is assigned)
1780        // ------------------------------------------------------------------
1781        let fallback_texture = {
1782            let tex = device.create_texture(&wgpu::TextureDescriptor {
1783                label: Some("fallback_texture"),
1784                size: wgpu::Extent3d {
1785                    width: 1,
1786                    height: 1,
1787                    depth_or_array_layers: 1,
1788                },
1789                mip_level_count: 1,
1790                sample_count: 1,
1791                dimension: wgpu::TextureDimension::D2,
1792                format: wgpu::TextureFormat::Rgba8UnormSrgb,
1793                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1794                view_formats: &[],
1795            });
1796            // Texture pixels are uploaded lazily on first prepare() via queue.write_texture.
1797            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1798            // Same config as material_sampler (repeat, linear), so share it.
1799            let sampler = material_sampler.clone();
1800            let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1801                label: Some("fallback_texture_bg"),
1802                layout: &texture_bgl,
1803                entries: &[
1804                    wgpu::BindGroupEntry {
1805                        binding: 0,
1806                        resource: wgpu::BindingResource::TextureView(&view),
1807                    },
1808                    wgpu::BindGroupEntry {
1809                        binding: 1,
1810                        resource: wgpu::BindingResource::Sampler(&sampler),
1811                    },
1812                    wgpu::BindGroupEntry {
1813                        binding: 2,
1814                        resource: wgpu::BindingResource::TextureView(&fallback_normal_map_view),
1815                    },
1816                    wgpu::BindGroupEntry {
1817                        binding: 3,
1818                        resource: wgpu::BindingResource::TextureView(&fallback_ao_map_view),
1819                    },
1820                ],
1821            });
1822            GpuTexture {
1823                texture: tex,
1824                view,
1825                sampler,
1826                bind_group,
1827            }
1828        };
1829
1830        // ------------------------------------------------------------------
1831        // Colourmap / LUT fallback resources
1832        // ------------------------------------------------------------------
1833        let fallback_lut_texture = device.create_texture(&wgpu::TextureDescriptor {
1834            label: Some("fallback_lut_texture"),
1835            size: wgpu::Extent3d {
1836                width: 1,
1837                height: 1,
1838                depth_or_array_layers: 1,
1839            },
1840            mip_level_count: 1,
1841            sample_count: 1,
1842            dimension: wgpu::TextureDimension::D2,
1843            format: wgpu::TextureFormat::Rgba8Unorm,
1844            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1845            view_formats: &[],
1846        });
1847        // Content of fallback_lut_view is never sampled by the shader when has_attribute=0.
1848        // Data is intentionally left uninitialised here; it will be a zeroed 1-pixel texture
1849        // after the GPU zeros it on allocation (implementation-defined but harmless).
1850        let fallback_lut_view =
1851            fallback_lut_texture.create_view(&wgpu::TextureViewDescriptor::default());
1852
1853        let fallback_scalar_buf = device.create_buffer(&wgpu::BufferDescriptor {
1854            label: Some("fallback_scalar_buf"),
1855            size: 4,
1856            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1857            mapped_at_creation: true,
1858        });
1859        {
1860            let mut view = fallback_scalar_buf.slice(..).get_mapped_range_mut();
1861            view.copy_from_slice(&[0u8; 4]);
1862        }
1863        fallback_scalar_buf.unmap();
1864
1865        let fallback_face_colour_buf = device.create_buffer(&wgpu::BufferDescriptor {
1866            label: Some("fallback_face_colour_buf"),
1867            size: 16, // one vec4<f32>
1868            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1869            mapped_at_creation: true,
1870        });
1871        {
1872            let mut view = fallback_face_colour_buf.slice(..).get_mapped_range_mut();
1873            view.copy_from_slice(&[0u8; 16]);
1874        }
1875        fallback_face_colour_buf.unmap();
1876
1877        let fallback_warp_buf = device.create_buffer(&wgpu::BufferDescriptor {
1878            label: Some("fallback_warp_buf"),
1879            size: 12, // one vec3<f32>
1880            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1881            mapped_at_creation: true,
1882        });
1883        {
1884            let mut view = fallback_warp_buf.slice(..).get_mapped_range_mut();
1885            view.copy_from_slice(&[0u8; 12]);
1886        }
1887        fallback_warp_buf.unmap();
1888
1889        let fallback_position_override_buf = device.create_buffer(&wgpu::BufferDescriptor {
1890            label: Some("fallback_position_override_buf"),
1891            size: 12, // one vec3<f32>
1892            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1893            mapped_at_creation: true,
1894        });
1895        {
1896            let mut view = fallback_position_override_buf
1897                .slice(..)
1898                .get_mapped_range_mut();
1899            view.copy_from_slice(&[0u8; 12]);
1900        }
1901        fallback_position_override_buf.unmap();
1902
1903        let fallback_normal_override_buf = device.create_buffer(&wgpu::BufferDescriptor {
1904            label: Some("fallback_normal_override_buf"),
1905            size: 12,
1906            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1907            mapped_at_creation: true,
1908        });
1909        {
1910            let mut view = fallback_normal_override_buf
1911                .slice(..)
1912                .get_mapped_range_mut();
1913            view.copy_from_slice(&[0u8; 12]);
1914        }
1915        fallback_normal_override_buf.unmap();
1916
1917        // ------------------------------------------------------------------
1918        // Hardcoded unit cube mesh (test scene object)
1919        // Created here : after fallback textures : so the combined bind group
1920        // can reference the fallback texture views at creation time.
1921        // ------------------------------------------------------------------
1922        let (cube_verts, cube_indices) = build_unit_cube();
1923        let cube_mesh = Self::create_mesh(
1924            device,
1925            &object_bgl,
1926            &fallback_texture.view,
1927            &fallback_normal_map_view,
1928            &fallback_ao_map_view,
1929            &fallback_texture.sampler,
1930            &lut_sampler,
1931            &fallback_lut_view,
1932            &fallback_scalar_buf,
1933            &fallback_texture.view,
1934            &fallback_face_colour_buf,
1935            &fallback_warp_buf,
1936            &fallback_position_override_buf,
1937            &fallback_normal_override_buf,
1938            &fallback_metallic_roughness_texture_view,
1939            &fallback_emissive_texture_view,
1940            &cube_verts,
1941            &cube_indices,
1942        );
1943
1944        // ------------------------------------------------------------------
1945        // Outline & x-ray pipelines
1946        // ------------------------------------------------------------------
1947
1948        // Bind group layout for OutlineUniform (group 1).
1949        let outline_bgl = crate::resources::builders::uniform_bgl(
1950            device,
1951            "outline_bgl",
1952            wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
1953        );
1954
1955        let xray_shader = crate::resources::builders::wgsl_module(
1956            device,
1957            "xray_shader",
1958            crate::resources::builders::wgsl_source!("xray"),
1959        );
1960
1961        let outline_pl_bgls: Vec<&wgpu::BindGroupLayout> = if let Some(d) = deform_bgl {
1962            vec![&camera_bgl, &outline_bgl, d]
1963        } else {
1964            vec![&camera_bgl, &outline_bgl]
1965        };
1966        let outline_pipeline_layout =
1967            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1968                label: Some("outline_pipeline_layout"),
1969                bind_group_layouts: &outline_pl_bgls,
1970                push_constant_ranges: &[],
1971            });
1972
1973        // Mask-write pipeline: renders selected objects as r=1.0 to an R8 mask
1974        // texture with depth testing, replacing the old stencil-based approach.
1975        let outline_mask_src = if deform_enabled {
1976            include_str!(concat!(env!("OUT_DIR"), "/outline_mask.wgsl"))
1977        } else {
1978            include_str!(concat!(env!("OUT_DIR"), "/outline_mask_noop.wgsl"))
1979        };
1980        let outline_mask_shader = crate::resources::builders::wgsl_module(
1981            device,
1982            "outline_mask_shader",
1983            outline_mask_src,
1984        );
1985        let outline_masks = crate::resources::mesh::mesh_pipelines::build_outline_mask_pipelines(
1986            device,
1987            &outline_pipeline_layout,
1988            &outline_mask_shader,
1989            wgpu::TextureFormat::R8Unorm,
1990            pipeline_cache.as_ref(),
1991        );
1992        let outline_mask_pipeline = outline_masks.mask;
1993        let outline_mask_two_sided_pipeline = outline_masks.mask_two_sided;
1994
1995        // Billboard disc pipeline for the Gaussian splat outline mask pass.
1996        // Reuses the same pipeline layout as the mesh mask pipelines (camera_bgl + outline_bgl).
1997        // Positions are instance-stepped vec3; each instance expands to a 6-vertex quad.
1998        let splat_outline_mask_shader = crate::resources::builders::wgsl_module(
1999            device,
2000            "splat_outline_mask_shader",
2001            crate::resources::builders::wgsl_source!("splat_outline_mask"),
2002        );
2003        let splat_outline_pos_attrs = [wgpu::VertexAttribute {
2004            offset: 0,
2005            shader_location: 0,
2006            format: wgpu::VertexFormat::Float32x3,
2007        }];
2008        let splat_outline_pos_layout = wgpu::VertexBufferLayout {
2009            array_stride: 12, // vec3<f32>
2010            step_mode: wgpu::VertexStepMode::Instance,
2011            attributes: &splat_outline_pos_attrs,
2012        };
2013        let splat_outline_size_attrs = [wgpu::VertexAttribute {
2014            offset: 0,
2015            shader_location: 1,
2016            format: wgpu::VertexFormat::Float32,
2017        }];
2018        let splat_outline_size_layout = wgpu::VertexBufferLayout {
2019            array_stride: 4, // f32
2020            step_mode: wgpu::VertexStepMode::Instance,
2021            attributes: &splat_outline_size_attrs,
2022        };
2023        let splat_outline_mask_pipeline =
2024            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2025                label: Some("splat_outline_mask_pipeline"),
2026                layout: Some(&outline_pipeline_layout),
2027                vertex: wgpu::VertexState {
2028                    module: &splat_outline_mask_shader,
2029                    entry_point: Some("vs_main"),
2030                    buffers: &[splat_outline_pos_layout, splat_outline_size_layout],
2031                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2032                },
2033                fragment: Some(wgpu::FragmentState {
2034                    module: &splat_outline_mask_shader,
2035                    entry_point: Some("fs_main"),
2036                    targets: &[Some(wgpu::ColorTargetState {
2037                        format: wgpu::TextureFormat::R8Unorm,
2038                        blend: None,
2039                        write_mask: wgpu::ColorWrites::ALL,
2040                    })],
2041                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2042                }),
2043                primitive: wgpu::PrimitiveState {
2044                    topology: wgpu::PrimitiveTopology::TriangleList,
2045                    cull_mode: None,
2046                    ..Default::default()
2047                },
2048                depth_stencil: Some(wgpu::DepthStencilState {
2049                    format: wgpu::TextureFormat::Depth24PlusStencil8,
2050                    depth_write_enabled: false,
2051                    depth_compare: wgpu::CompareFunction::Less,
2052                    stencil: wgpu::StencilState::default(),
2053                    bias: wgpu::DepthBiasState::default(),
2054                }),
2055                multisample: wgpu::MultisampleState {
2056                    count: 1,
2057                    mask: !0,
2058                    alpha_to_coverage_enabled: false,
2059                },
2060                multiview: None,
2061                cache: pipeline_cache.as_ref(),
2062            });
2063
2064        // Edge-detection pipeline: fullscreen pass that reads the R8 mask and
2065        // outputs an anti-aliased outline ring to the outline colour texture.
2066        let outline_edge_shader = crate::resources::builders::wgsl_module(
2067            device,
2068            "outline_edge_shader",
2069            crate::resources::builders::wgsl_source!("outline_edge"),
2070        );
2071        let outline_edge_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2072            label: Some("outline_edge_bgl"),
2073            entries: &[
2074                wgpu::BindGroupLayoutEntry {
2075                    binding: 0,
2076                    visibility: wgpu::ShaderStages::FRAGMENT,
2077                    ty: wgpu::BindingType::Texture {
2078                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
2079                        view_dimension: wgpu::TextureViewDimension::D2,
2080                        multisampled: false,
2081                    },
2082                    count: None,
2083                },
2084                wgpu::BindGroupLayoutEntry {
2085                    binding: 1,
2086                    visibility: wgpu::ShaderStages::FRAGMENT,
2087                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2088                    count: None,
2089                },
2090                wgpu::BindGroupLayoutEntry {
2091                    binding: 2,
2092                    visibility: wgpu::ShaderStages::FRAGMENT,
2093                    ty: wgpu::BindingType::Buffer {
2094                        ty: wgpu::BufferBindingType::Uniform,
2095                        has_dynamic_offset: false,
2096                        min_binding_size: None,
2097                    },
2098                    count: None,
2099                },
2100            ],
2101        });
2102        let outline_edge_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2103            label: Some("outline_edge_layout"),
2104            bind_group_layouts: &[&outline_edge_bgl],
2105            push_constant_ranges: &[],
2106        });
2107        let outline_edge_pipeline =
2108            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2109                label: Some("outline_edge_pipeline"),
2110                layout: Some(&outline_edge_layout),
2111                vertex: wgpu::VertexState {
2112                    module: &outline_edge_shader,
2113                    entry_point: Some("vs_main"),
2114                    buffers: &[],
2115                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2116                },
2117                fragment: Some(wgpu::FragmentState {
2118                    module: &outline_edge_shader,
2119                    entry_point: Some("fs_main"),
2120                    targets: &[Some(wgpu::ColorTargetState {
2121                        format: target_format,
2122                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2123                        write_mask: wgpu::ColorWrites::ALL,
2124                    })],
2125                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2126                }),
2127                primitive: wgpu::PrimitiveState {
2128                    topology: wgpu::PrimitiveTopology::TriangleList,
2129                    cull_mode: None,
2130                    ..Default::default()
2131                },
2132                depth_stencil: None,
2133                multisample: wgpu::MultisampleState::default(),
2134                multiview: None,
2135                cache: pipeline_cache.as_ref(),
2136            });
2137
2138        // X-ray pipeline: render selected objects through all geometry as a semi-transparent tint.
2139        let xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2140            label: Some("xray_pipeline"),
2141            layout: Some(&outline_pipeline_layout),
2142            vertex: wgpu::VertexState {
2143                module: &xray_shader,
2144                entry_point: Some("vs_main"),
2145                buffers: &[Vertex::buffer_layout()],
2146                compilation_options: wgpu::PipelineCompilationOptions::default(),
2147            },
2148            fragment: Some(wgpu::FragmentState {
2149                module: &xray_shader,
2150                entry_point: Some("fs_main"),
2151                targets: &[Some(wgpu::ColorTargetState {
2152                    format: target_format,
2153                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2154                    write_mask: wgpu::ColorWrites::ALL,
2155                })],
2156                compilation_options: wgpu::PipelineCompilationOptions::default(),
2157            }),
2158            primitive: wgpu::PrimitiveState {
2159                topology: wgpu::PrimitiveTopology::TriangleList,
2160                cull_mode: None,
2161                ..Default::default()
2162            },
2163            depth_stencil: Some(wgpu::DepthStencilState {
2164                format: wgpu::TextureFormat::Depth24PlusStencil8,
2165                depth_write_enabled: false,
2166                depth_compare: wgpu::CompareFunction::Always,
2167                stencil: wgpu::StencilState::default(),
2168                bias: wgpu::DepthBiasState::default(),
2169            }),
2170            multisample: wgpu::MultisampleState {
2171                count: sample_count,
2172                mask: !0,
2173                alpha_to_coverage_enabled: false,
2174            },
2175            multiview: None,
2176            cache: pipeline_cache.as_ref(),
2177        });
2178
2179        // Skybox pipeline: fullscreen triangle that samples the equirect environment map.
2180        let skybox_shader = crate::resources::builders::wgsl_module(
2181            device,
2182            "skybox_shader",
2183            crate::resources::builders::wgsl_source!("skybox"),
2184        );
2185        let skybox_pipeline_layout =
2186            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2187                label: Some("skybox_pipeline_layout"),
2188                bind_group_layouts: &[&camera_bgl],
2189                push_constant_ranges: &[],
2190            });
2191        let skybox_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2192            label: Some("skybox_pipeline"),
2193            layout: Some(&skybox_pipeline_layout),
2194            vertex: wgpu::VertexState {
2195                module: &skybox_shader,
2196                entry_point: Some("vs_main"),
2197                buffers: &[],
2198                compilation_options: wgpu::PipelineCompilationOptions::default(),
2199            },
2200            fragment: Some(wgpu::FragmentState {
2201                module: &skybox_shader,
2202                entry_point: Some("fs_main"),
2203                targets: &[Some(wgpu::ColorTargetState {
2204                    format: wgpu::TextureFormat::Rgba16Float,
2205                    blend: None,
2206                    write_mask: wgpu::ColorWrites::ALL,
2207                })],
2208                compilation_options: wgpu::PipelineCompilationOptions::default(),
2209            }),
2210            primitive: wgpu::PrimitiveState {
2211                topology: wgpu::PrimitiveTopology::TriangleList,
2212                cull_mode: None,
2213                ..Default::default()
2214            },
2215            depth_stencil: Some(wgpu::DepthStencilState {
2216                format: wgpu::TextureFormat::Depth24PlusStencil8,
2217                // Drawn after opaques: only sky pixels (depth == 1.0) pass.
2218                depth_write_enabled: false,
2219                depth_compare: wgpu::CompareFunction::Equal,
2220                stencil: wgpu::StencilState::default(),
2221                bias: wgpu::DepthBiasState::default(),
2222            }),
2223            multisample: wgpu::MultisampleState::default(),
2224            multiview: None,
2225            cache: pipeline_cache.as_ref(),
2226        });
2227
2228        mark("misc_pipelines");
2229
2230        // `deform` is constructed earlier (before the mesh pipeline layout).
2231
2232        let resources = Self {
2233            target_format,
2234            sample_count,
2235            pipeline_cache,
2236            solid_pipeline,
2237            deform,
2238            solid_two_sided_pipeline,
2239            transparent_pipeline,
2240            wireframe_pipeline,
2241            camera_uniform_buf,
2242            light_uniform_buf,
2243            light_storage_buf,
2244            clustered,
2245            camera_bind_group,
2246            camera_bind_group_layout: camera_bgl,
2247            object_bind_group_layout: object_bgl,
2248            mesh_store: {
2249                let mut store = crate::resources::mesh::mesh_store::MeshStore::new();
2250                store.insert(cube_mesh);
2251                store
2252            },
2253            lod_groups: crate::resources::mesh::lod::LodGroupStore::new(),
2254            shadow_map_texture,
2255            shadow_map_view,
2256            shadow_sampler,
2257            point_shadow_cube_texture,
2258            point_shadow_cube_view,
2259            point_shadow_face_views,
2260            shadow_point_pipeline,
2261            shadow_point_face_bind_group_layout: shadow_point_face_bgl,
2262            shadow_point_face_buf,
2263            shadow_point_face_bind_group,
2264            shadow_pipeline,
2265            shadow_pipeline_two_sided,
2266            shadow_camera_bind_group_layout: shadow_camera_bgl,
2267            shadow_uniform_buf,
2268            shadow_bind_group,
2269            shadow_info_buf,
2270            shadow_atlas_size: SHADOW_ATLAS_SIZE,
2271            shadow_atlas_depth_sampler,
2272            shadow_atlas_viewer_pipeline,
2273            shadow_atlas_viewer_bg,
2274            shadow_atlas_viewer_buf,
2275            debug_frag_sentinel_buf,
2276            gizmo_pipeline,
2277            gizmo_vertex_buffer,
2278            gizmo_index_buffer,
2279            gizmo_index_count,
2280            gizmo_uniform_buf,
2281            gizmo_bind_group,
2282            gizmo_bind_group_layout: gizmo_bgl,
2283            overlay_pipeline,
2284            overlay_line_pipeline,
2285            grid_pipeline,
2286            grid_uniform_buf,
2287            grid_bind_group,
2288            grid_bind_group_layout: grid_bgl,
2289            ground_plane_pipeline,
2290            _ground_plane_bgl: ground_plane_bgl,
2291            ground_plane_uniform_buf,
2292            ground_plane_bind_group,
2293            overlay_bind_group_layout: overlay_bgl,
2294            constraint_line_buffers: Vec::new(),
2295            axes_pipeline,
2296            axes_vertex_buffer,
2297            axes_vertex_count: 0,
2298            texture_bind_group_layout: texture_bgl,
2299            fallback_texture,
2300            fallback_normal_map,
2301            fallback_normal_map_view,
2302            fallback_ao_map,
2303            fallback_ao_map_view,
2304            fallback_metallic_roughness_texture,
2305            fallback_metallic_roughness_texture_view,
2306            fallback_emissive_texture,
2307            fallback_emissive_texture_view,
2308            material_sampler,
2309            lut_sampler,
2310            content: crate::resources::types::ContentResources {
2311                material_bind_groups: std::collections::HashMap::new(),
2312                textures: crate::resources::material::texture_store::TextureStore::new(),
2313                polyline_store: crate::resources::PolylineStore::new(),
2314                streamtube_store: crate::resources::StreamtubeStore::new(),
2315                tube_store: crate::resources::TubeStore::new(),
2316                ribbon_store: crate::resources::RibbonStore::new(),
2317                point_cloud_store: crate::resources::PointCloudStore::new(),
2318                glyph_set_store: crate::resources::GlyphSetStore::new(),
2319                tensor_glyph_set_store: crate::resources::TensorGlyphSetStore::new(),
2320                sprite_set_store: crate::resources::SpriteSetStore::new(),
2321                sprite_instance_set_store: crate::resources::SpriteInstanceSetStore::new(),
2322                gaussian_splat_store: crate::resources::types::GaussianSplatStore::new(),
2323                volume_textures: crate::resources::handle::Registry::default(),
2324                projected_tet_store: crate::resources::handle::Registry::default(),
2325                glyph_atlas: crate::resources::overlay::font::GlyphAtlas::new(device),
2326                overlay_textures: crate::resources::handle::Registry::default(),
2327                matcap_textures: Vec::new(),
2328                matcap_views: Vec::new(),
2329                matcap_sampler: None,
2330                fallback_matcap_view: None,
2331                matcaps_initialized: false,
2332                builtin_matcap_ids: None,
2333                colourmap_textures: Vec::new(),
2334                colourmap_views: Vec::new(),
2335                colourmaps_cpu: Vec::new(),
2336                fallback_lut_texture,
2337                fallback_lut_view,
2338                fallback_scalar_buf,
2339                fallback_face_colour_buf,
2340                fallback_warp_buf,
2341                fallback_position_override_buf,
2342                fallback_normal_override_buf,
2343                builtin_colourmap_ids: None,
2344                colourmaps_initialized: false,
2345            },
2346            jobs: std::sync::Mutex::new(crate::resources::upload_jobs::JobRunner::new()),
2347            job_results: crate::resources::upload_jobs::JobResults::default(),
2348            fallback_textures_uploaded: false,
2349            post: crate::resources::postprocess::PostProcessResources::default(),
2350            clip_planes_uniform_buf,
2351            clip_volume_uniform_buf,
2352            outline: crate::resources::types::OutlineResources {
2353                bind_group_layout: outline_bgl,
2354                mask_pipeline: outline_mask_pipeline,
2355                mask_two_sided_pipeline: outline_mask_two_sided_pipeline,
2356                edge_pipeline: outline_edge_pipeline,
2357                edge_bgl: outline_edge_bgl,
2358                xray_pipeline,
2359                splat_mask_pipeline: splat_outline_mask_pipeline,
2360                colour_texture: None,
2361                colour_view: None,
2362                depth_texture: None,
2363                depth_view: None,
2364                target_size: [0, 0],
2365                composite_pipeline_single: None,
2366                composite_pipeline_msaa: None,
2367                composite_pipeline_hdr: None,
2368                composite_bgl: None,
2369                composite_bind_group: None,
2370                composite_sampler: None,
2371            },
2372            instancing: crate::resources::mesh::instancing::InstancingResources::default(),
2373            cull: crate::resources::mesh::instancing::CullResources::default(),
2374            lic: crate::resources::postprocess::LicResources::default(),
2375            hdr_solid_pipeline: None,
2376            hdr_solid_two_sided_pipeline: None,
2377            hdr_transparent_pipeline: None,
2378            hdr_wireframe_pipeline: None,
2379            hdr_overlay_pipeline: None,
2380            gaussian_splat:
2381                crate::resources::scivis::gaussian_splat::GaussianSplatResources::default(),
2382            sprite: crate::resources::scivis::sprite::SpriteResources::default(),
2383            point_cloud_pipeline: None,
2384            point_cloud_bgl: None,
2385            glyph: crate::resources::scivis::glyph::GlyphResources::default(),
2386            tensor_glyph: crate::resources::scivis::glyph::TensorGlyphResources::default(),
2387            volume: crate::resources::volume::volumes::VolumeResources::default(),
2388            polyline: crate::resources::scivis::polyline::PolylineResources::default(),
2389            streamtube: crate::resources::scivis::tube::StreamtubeResources::default(),
2390            ribbon: crate::resources::scivis::tube::RibbonResources::default(),
2391            image_slice: crate::resources::types::ImageSliceResources::default(),
2392            compute_filter_pipeline: None,
2393            compute_filter_bgl: None,
2394            oit: crate::resources::postprocess::OitResources::default(),
2395            pt: crate::resources::types::ProjectedTetResources::default(),
2396            // Scatter-volume (participating media) pipeline (lazily created).
2397            scatter: crate::resources::volume::scatter_volume::ScatterResources::default(),
2398            // IBL / environment map resources.
2399            ibl_irradiance_view: None,
2400            ibl_prefiltered_view: None,
2401            ibl_brdf_lut_view: None,
2402            ibl_sampler,
2403            ibl_skybox_view: None,
2404            ibl_fallback_texture,
2405            ibl_fallback_view,
2406            ibl_fallback_brdf_texture,
2407            ibl_fallback_brdf_view,
2408            ibl_irradiance_texture: None,
2409            ibl_prefiltered_texture: None,
2410            ibl_brdf_lut_texture: None,
2411            ibl_skybox_texture: None,
2412            skybox_pipeline,
2413            pick: crate::resources::types::PickResources::default(),
2414            implicit: crate::resources::types::ImplicitResources::default(),
2415            mc: crate::resources::volume::gpu_marching_cubes::McResources::default(),
2416
2417            particle: crate::resources::gpu::gpu_particles::ParticleResources::default(),
2418            screen_image: crate::resources::types::ScreenImageResources::default(),
2419            sub_highlight: crate::resources::types::SubHighlightResources::default(),
2420            overlay_text: crate::resources::overlay::overlay_text::OverlayTextResources::default(),
2421            overlay_shape: crate::resources::overlay::overlay_shape::OverlayShapeResources::default(
2422            ),
2423            backdrop_blur: crate::resources::overlay::overlay_shape::BackdropBlurResources::default(
2424            ),
2425            frame_upload_bytes: 0,
2426            occlusion_culling_enabled: false,
2427            decal: crate::resources::decal::DecalResources::default(),
2428        };
2429        // GPU skinning is opt-in: hosts call
2430        // `viewport_lib::plugins::skinning::SkinningPlugin::install(&mut resources, &device)`
2431        // before uploading any skin data. The renderer otherwise carries no
2432        // skinning state.
2433        tracing::info!(
2434            target: "viewport_lib::init",
2435            ms = init_start.elapsed().as_secs_f32() * 1000.0,
2436            "gpu resources init total"
2437        );
2438        resources
2439    }
2440}