Skip to main content

viewport_lib/resources/
device_resources.rs

1//! `DeviceResources`: the device-shared GPU resource container, its content
2//! and per-viewport scope structs, and the small feature-resource clusters.
3
4use crate::resources::types::*;
5
6/// Per-viewport HDR/post-process GPU state.
7///
8/// Holds all viewport-size-dependent render targets, their associated bind
9/// groups, and the per-viewport uniform buffers used by the post-process
10/// pipeline.  Created lazily in `ViewportRenderer::ensure_viewport_hdr` and
11/// resized automatically when the viewport dimensions change.
12///
13/// Shared infrastructure (pipelines, BGLs, samplers, placeholder textures,
14/// SSAO noise/kernel) lives on [`DeviceResources`] and is created once
15/// by `ensure_hdr_shared`.
16#[allow(dead_code)]
17pub(crate) struct ViewportHdrState {
18    // --- HDR scene target ---
19    pub hdr_texture: wgpu::Texture,
20    pub hdr_view: wgpu::TextureView,
21    pub hdr_depth_texture: wgpu::Texture,
22    pub hdr_depth_view: wgpu::TextureView,
23    pub hdr_depth_only_view: wgpu::TextureView,
24    pub hdr_stencil_only_view: wgpu::TextureView,
25
26    // --- Bloom ---
27    pub bloom_threshold_texture: wgpu::Texture,
28    pub bloom_threshold_view: wgpu::TextureView,
29    pub bloom_ping_texture: wgpu::Texture,
30    pub bloom_ping_view: wgpu::TextureView,
31    pub bloom_pong_texture: wgpu::Texture,
32    pub bloom_pong_view: wgpu::TextureView,
33
34    // --- SSAO ---
35    pub ssao_texture: wgpu::Texture,
36    pub ssao_view: wgpu::TextureView,
37    pub ssao_blur_texture: wgpu::Texture,
38    pub ssao_blur_view: wgpu::TextureView,
39
40    // --- Depth of field ---
41    pub dof_texture: wgpu::Texture,
42    pub dof_view: wgpu::TextureView,
43    pub dof_bind_group: wgpu::BindGroup,
44    pub dof_uniform_buf: wgpu::Buffer,
45
46    // --- Contact shadow ---
47    pub contact_shadow_texture: wgpu::Texture,
48    pub contact_shadow_view: wgpu::TextureView,
49
50    // --- Surface LIC ---
51    /// Encodes screen-space flow vector per surface pixel (Rgba8Unorm, viewport-sized).
52    pub lic_vector_texture: wgpu::Texture,
53    pub lic_vector_view: wgpu::TextureView,
54    /// LIC intensity after advection (R8Unorm, viewport-sized). Read by tone_map.wgsl binding 7.
55    pub lic_output_texture: wgpu::Texture,
56    pub lic_output_view: wgpu::TextureView,
57    /// Per-pixel white noise (R8Unorm, viewport-sized). One independent random value per pixel.
58    /// Sampled with textureLoad (nearest) in lic_advect.wgsl to produce directional LIC contrast.
59    pub lic_noise_texture: wgpu::Texture,
60    pub lic_noise_view: wgpu::TextureView,
61    /// Bind group for the LIC advect render pass (reads lic_vector_texture + lic_noise_texture).
62    pub lic_advect_bind_group: wgpu::BindGroup,
63    /// Uniform buffer for LicAdvectUniform (steps, step_size, viewport dims).
64    pub lic_uniform_buf: wgpu::Buffer,
65
66    // --- FXAA ---
67    pub fxaa_texture: wgpu::Texture,
68    pub fxaa_view: wgpu::TextureView,
69
70    // --- SSAA (allocated when ssaa_factor > 1) ---
71    /// Supersampled colour render target. `None` when ssaa_factor == 1.
72    pub ssaa_colour_texture: Option<wgpu::Texture>,
73    pub ssaa_colour_view: Option<wgpu::TextureView>,
74    /// Supersampled depth render target. `None` when ssaa_factor == 1.
75    pub ssaa_depth_texture: Option<wgpu::Texture>,
76    pub ssaa_depth_view: Option<wgpu::TextureView>,
77    /// Depth-aspect-only view of `ssaa_depth_texture`, used as the soft-particle
78    /// sample source during the SSAA sprite post-pass. `None` when SSAA is off.
79    pub ssaa_depth_only_view: Option<wgpu::TextureView>,
80    /// Bind group for the SSAA resolve pass (reads ssaa_colour_texture). `None` when ssaa_factor == 1.
81    pub ssaa_resolve_bind_group: Option<wgpu::BindGroup>,
82    /// Uniform buffer holding the ssaa_factor value for the resolve shader.
83    pub ssaa_uniform_buf: Option<wgpu::Buffer>,
84    /// The ssaa_factor this state was created with (1 = no SSAA).
85    pub ssaa_factor: u32,
86
87    // --- OIT (lazily allocated when transparent geometry is present) ---
88    pub oit_accum_texture: Option<wgpu::Texture>,
89    pub oit_accum_view: Option<wgpu::TextureView>,
90    pub oit_reveal_texture: Option<wgpu::Texture>,
91    pub oit_reveal_view: Option<wgpu::TextureView>,
92    pub oit_composite_bind_group: Option<wgpu::BindGroup>,
93    pub oit_size: [u32; 2],
94
95    // --- Outline offscreen (used by the outline prepare pass) ---
96    /// R8Unorm mask: selected objects rendered as white on black.
97    pub outline_mask_texture: wgpu::Texture,
98    pub outline_mask_view: wgpu::TextureView,
99    /// RGBA output of the edge-detection pass (composited onto the main target).
100    pub outline_colour_texture: wgpu::Texture,
101    pub outline_colour_view: wgpu::TextureView,
102    pub outline_depth_texture: wgpu::Texture,
103    pub outline_depth_view: wgpu::TextureView,
104    /// Depth-aspect view of `outline_depth_texture` for sampling (the HiZ
105    /// occlusion prev-depth copy on the LDR path).
106    pub outline_depth_only_view: wgpu::TextureView,
107    /// Bind group for the edge-detection pass (reads mask, writes to colour).
108    pub outline_edge_bind_group: wgpu::BindGroup,
109    /// Uniform buffer for the edge-detection pass parameters.
110    pub outline_edge_uniform_buf: wgpu::Buffer,
111    pub outline_composite_bind_group: wgpu::BindGroup,
112
113    // --- Bind groups (rebuilt when viewport dimensions change) ---
114    pub tone_map_bind_group: wgpu::BindGroup,
115    pub bloom_threshold_bg: wgpu::BindGroup,
116    /// H-blur bind group that reads from bloom_threshold (pass 0 only).
117    pub bloom_blur_h_bg: wgpu::BindGroup,
118    /// V-blur bind group that reads from bloom_ping.
119    pub bloom_blur_v_bg: wgpu::BindGroup,
120    /// H-blur bind group that reads from bloom_pong (passes 1+).
121    pub bloom_blur_h_pong_bg: wgpu::BindGroup,
122    pub ssao_bg: wgpu::BindGroup,
123    pub ssao_blur_bg: wgpu::BindGroup,
124    pub dof_bg: wgpu::BindGroup,
125    pub contact_shadow_bg: wgpu::BindGroup,
126    pub fxaa_bind_group: wgpu::BindGroup,
127
128    // --- Per-viewport uniform buffers ---
129    pub tone_map_uniform_buf: wgpu::Buffer,
130    pub bloom_uniform_buf: wgpu::Buffer,
131    /// Constant H-blur uniform buffer (horizontal=1, written once at creation).
132    pub bloom_h_uniform_buf: wgpu::Buffer,
133    /// Constant V-blur uniform buffer (horizontal=0, written once at creation).
134    pub bloom_v_uniform_buf: wgpu::Buffer,
135    pub ssao_uniform_buf: wgpu::Buffer,
136    pub contact_shadow_uniform_buf: wgpu::Buffer,
137
138    // --- Post-tone-map depth buffer (native resolution) ---
139    // When scene_size == output_size (render_scale = 1.0) this is None and
140    // hdr_depth_view is used directly for post-tone-map passes.
141    // When scene_size != output_size the scene depth is blitted into this
142    // native-resolution texture so that post-tone-map passes (grid, gizmos,
143    // axes, etc.) can use it as a depth attachment alongside output_view.
144    pub output_depth_texture: Option<wgpu::Texture>,
145    pub output_depth_view: wgpu::TextureView,
146    /// Bind group for the depth blit pass (reads hdr_depth_only_view).
147    /// None when scene_size == output_size (no blit needed).
148    pub depth_blit_bind_group: Option<wgpu::BindGroup>,
149
150    // --- HDR upscale (allocated when scene_size != output_size) ---
151    // When render_scale < 1.0, tone-map and FXAA run at scene resolution.
152    // The result is written to upscale_texture, then upscale-blitted to output_view.
153    pub upscale_texture: Option<wgpu::Texture>,
154    pub upscale_view: Option<wgpu::TextureView>,
155    pub upscale_bind_group: Option<wgpu::BindGroup>,
156
157    /// Native output resolution [width, height].
158    pub output_size: [u32; 2],
159    /// Effective scene resolution after render scale: [output_size * render_scale].
160    /// Equals output_size when render_scale = 1.0.
161    pub scene_size: [u32; 2],
162
163    // --- Decal pass depth binding (D1) ---
164    /// Bind group for group 1 of the decal pass: reads hdr_depth_only_view as a depth texture.
165    /// Rebuilt on viewport resize alongside the other viewport-sized bind groups.
166    pub decal_depth_bg: wgpu::BindGroup,
167}
168/// Per-viewport scatter-pass intermediates: two RGBA16F ping-pong targets
169/// driven by the temporal-accumulation logic, plus the composite bind groups
170/// and previous-frame view-projection used for reprojection.
171///
172/// Lives on `ViewportRenderer` (not `ViewportHdrState`) so that the scatter
173/// pass can allocate and mutate it without conflicting with the immutable
174/// `slot_hdr` borrow held across the larger paint phase.
175pub(crate) struct ScatterViewportState {
176    // Textures keep the GPU allocation alive; views are sampled or rendered
177    // into.
178    /// Per-volume scatter draws accumulate into this target each frame.
179    /// Cleared at the start of the scatter pass.
180    #[allow(dead_code)]
181    pub raw_current_texture: wgpu::Texture,
182    pub raw_current_view: wgpu::TextureView,
183    /// History ping-pong. The temporal-resolve pass reads one slot
184    /// (history_prev) and writes the other (history_new). `parity` selects.
185    #[allow(dead_code)]
186    pub history_a_texture: wgpu::Texture,
187    pub history_a_view: wgpu::TextureView,
188    #[allow(dead_code)]
189    pub history_b_texture: wgpu::Texture,
190    pub history_b_view: wgpu::TextureView,
191    /// Composite bind group reading the raw-current texture.
192    /// Used when temporal accumulation is disabled.
193    pub composite_bg_raw: wgpu::BindGroup,
194    /// Composite bind groups reading either history slot, used as the source
195    /// after the temporal-resolve pass has written history_new.
196    pub composite_bg_history_a: wgpu::BindGroup,
197    pub composite_bg_history_b: wgpu::BindGroup,
198    /// Temporal-resolve bind groups, keyed by which history slot is being
199    /// read as the previous-frame input. Each binds raw_current + the chosen
200    /// history slot.
201    pub temporal_resolve_bg_read_a: wgpu::BindGroup,
202    pub temporal_resolve_bg_read_b: wgpu::BindGroup,
203    /// Current allocated intermediate size, [width, height].
204    pub size: [u32; 2],
205    /// Whether `size` reflects the downsampled (half-res) allocation.
206    pub downsampled: bool,
207    /// Index of the history slot the next frame writes to (0 = A, 1 = B).
208    /// The other slot is read as the previous-frame history.
209    pub parity: u32,
210    /// True when the history slot opposite `parity` holds a usable
211    /// previous-frame composite result.
212    pub history_valid: bool,
213    /// Previous frame's view-projection (row-major mat4).
214    pub prev_view_proj: [[f32; 4]; 4],
215    /// Scene colour copy sampled by the refraction pass. Allocated on demand
216    /// when at least one volume has refraction enabled. Matches the HDR
217    /// target's size and format.
218    #[allow(dead_code)]
219    pub refraction_source_texture: Option<wgpu::Texture>,
220    /// View paired with `refraction_source_texture`. Bound as the source
221    /// during the refraction pass and as the render target during the
222    /// preceding blit-copy of the HDR scene.
223    pub refraction_source_view: Option<wgpu::TextureView>,
224    /// Per-viewport bind group binding `(refraction_source_view, depth)` to
225    /// the refraction pass.
226    pub refraction_source_bg: Option<wgpu::BindGroup>,
227    /// Per-viewport bind group binding the HDR view as the source for the
228    /// blit-copy that fills `refraction_source_view`.
229    pub refraction_blit_bg: Option<wgpu::BindGroup>,
230    /// Allocated size of the refraction source, matched to the HDR target.
231    pub refraction_source_size: [u32; 2],
232}
233/// A render pipeline compiled for both the LDR swapchain format and the HDR
234/// intermediate format (`Rgba16Float`). Used for pipelines that draw into the
235/// primary scene colour attachment, which may be either format depending on
236/// whether post-processing is active.
237pub(crate) struct DualPipeline {
238    pub ldr: wgpu::RenderPipeline,
239    pub hdr: wgpu::RenderPipeline,
240}
241
242impl DualPipeline {
243    /// Select the pipeline matching the current render target format.
244    /// Pass `true` when drawing into the HDR scene pass (`Rgba16Float`),
245    /// `false` when drawing into the LDR swapchain pass.
246    pub fn for_format(&self, hdr: bool) -> &wgpu::RenderPipeline {
247        if hdr { &self.hdr } else { &self.ldr }
248    }
249}
250
251/// GPU object-ID picking pipeline and its bind group layouts. Lazily built.
252#[derive(Default)]
253pub(crate) struct PickResources {
254    /// Render pipeline that outputs flat u32 object IDs to R32Uint + R32Float targets.
255    pub(crate) pipeline: Option<wgpu::RenderPipeline>,
256    /// Group 1 layout (PickInstance storage buffer).
257    pub(crate) bind_group_layout_1: Option<wgpu::BindGroupLayout>,
258    /// Minimal camera-only bind group layout (group 0).
259    pub(crate) camera_bgl: Option<wgpu::BindGroupLayout>,
260}
261
262/// GPU implicit-surface ray-march pipeline and layout. Lazily built.
263#[derive(Default)]
264pub(crate) struct ImplicitResources {
265    /// Render pipeline for GPU-side implicit surface ray-marching.
266    pub(crate) pipeline: Option<DualPipeline>,
267    /// Group 1 layout (ImplicitUniformRaw).
268    pub(crate) bgl: Option<wgpu::BindGroupLayout>,
269    /// Outline mask pipeline for implicit surfaces. None until first selected item.
270    pub(crate) outline_mask_pipeline: Option<wgpu::RenderPipeline>,
271}
272
273/// Screen-space image quad pipelines (plain + depth-composite) and the rect
274/// outline mask pipeline. Lazily built.
275#[derive(Default)]
276pub(crate) struct ScreenImageResources {
277    /// Render pipeline for screen-space image quads.
278    pub(crate) pipeline: Option<wgpu::RenderPipeline>,
279    /// Group 0 layout (uniform + texture + sampler).
280    pub(crate) bgl: Option<wgpu::BindGroupLayout>,
281    /// Depth-composite pipeline (LessEqual depth, per-pixel image depth).
282    pub(crate) dc_pipeline: Option<wgpu::RenderPipeline>,
283    /// Group 0 layout for the dc pipeline (uniform + colour + sampler + depth).
284    pub(crate) dc_bgl: Option<wgpu::BindGroupLayout>,
285    /// Outline mask pipeline for screen-space rect images. None until first selected.
286    pub(crate) rect_outline_mask_pipeline: Option<wgpu::RenderPipeline>,
287    /// Layout for the rect outline mask pipeline (NdcRectUniform).
288    pub(crate) rect_outline_bgl: Option<wgpu::BindGroupLayout>,
289}
290
291/// Sub-object highlight pipelines (fill / edge / sprite, HDR + LDR) and layout.
292/// Lazily built the first frame a sub-selection is present.
293#[derive(Default)]
294pub(crate) struct SubHighlightResources {
295    /// Translucent face fill pipeline (HDR).
296    pub(crate) fill_pipeline: Option<wgpu::RenderPipeline>,
297    /// Depth-nudged billboard edge-line pipeline (HDR).
298    pub(crate) edge_pipeline: Option<wgpu::RenderPipeline>,
299    /// Billboard sprite pipeline for vertex/point highlights (HDR).
300    pub(crate) sprite_pipeline: Option<wgpu::RenderPipeline>,
301    /// Translucent face fill pipeline (LDR).
302    pub(crate) fill_ldr_pipeline: Option<wgpu::RenderPipeline>,
303    /// Depth-nudged billboard edge-line pipeline (LDR).
304    pub(crate) edge_ldr_pipeline: Option<wgpu::RenderPipeline>,
305    /// Billboard sprite pipeline for vertex/point highlights (LDR).
306    pub(crate) sprite_ldr_pipeline: Option<wgpu::RenderPipeline>,
307    /// Shared group 1 layout (SubHighlightUniform).
308    pub(crate) bgl: Option<wgpu::BindGroupLayout>,
309}
310
311/// Projected-tetrahedra transparent volume pipeline, layouts, and LUT bind
312/// group cache. Lazily built.
313#[derive(Default)]
314pub(crate) struct ProjectedTetResources {
315    /// Render pipeline for the projected tetrahedra pass.
316    pub(crate) pipeline: Option<wgpu::RenderPipeline>,
317    /// Group 1 layout (per-volume uniform + tet storage buffer).
318    pub(crate) bind_group_layout: Option<wgpu::BindGroupLayout>,
319    /// Group 2 layout (per-frame colourmap LUT + sampler).
320    pub(crate) lut_bind_group_layout: Option<wgpu::BindGroupLayout>,
321    /// Cache of LUT bind groups keyed by colourmap slot index.
322    pub(crate) lut_bind_groups: std::collections::HashMap<usize, wgpu::BindGroup>,
323    /// LUT bind group for the fallback colourmap.
324    pub(crate) fallback_lut_bind_group: Option<wgpu::BindGroup>,
325}
326
327/// Selection-outline and x-ray pipelines, the offscreen mask/composite targets,
328/// and their layouts. The mask/edge/xray/splat pipelines are built eagerly at
329/// init; the offscreen textures and composite pipelines are lazily created.
330pub(crate) struct OutlineResources {
331    /// Group 1 layout for OutlineUniform (mask/xray pipelines).
332    pub(crate) bind_group_layout: wgpu::BindGroupLayout,
333    /// Mask-write pipeline: selected objects as r=1.0 to an R8 mask.
334    pub(crate) mask_pipeline: wgpu::RenderPipeline,
335    /// Two-sided mask-write pipeline (no face culling).
336    pub(crate) mask_two_sided_pipeline: wgpu::RenderPipeline,
337    /// Fullscreen edge-detection pipeline: reads mask, outputs the outline ring.
338    pub(crate) edge_pipeline: wgpu::RenderPipeline,
339    /// Layout for the edge-detection pass (mask texture + sampler + uniform).
340    pub(crate) edge_bgl: wgpu::BindGroupLayout,
341    /// X-ray pipeline: draws selected objects through occluders (depth Always).
342    pub(crate) xray_pipeline: wgpu::RenderPipeline,
343    /// Billboard disc pipeline for the Gaussian splat outline mask pass.
344    pub(crate) splat_mask_pipeline: wgpu::RenderPipeline,
345    /// Offscreen RGBA texture the outline stencil pass renders into.
346    pub(crate) colour_texture: Option<wgpu::Texture>,
347    pub(crate) colour_view: Option<wgpu::TextureView>,
348    /// Depth+stencil texture for the offscreen outline pass.
349    pub(crate) depth_texture: Option<wgpu::Texture>,
350    pub(crate) depth_view: Option<wgpu::TextureView>,
351    /// Size of the current outline offscreen textures.
352    pub(crate) target_size: [u32; 2],
353    /// Fullscreen composite pipelines: single-sample LDR, MSAA, HDR.
354    pub(crate) composite_pipeline_single: Option<wgpu::RenderPipeline>,
355    pub(crate) composite_pipeline_msaa: Option<wgpu::RenderPipeline>,
356    pub(crate) composite_pipeline_hdr: Option<wgpu::RenderPipeline>,
357    pub(crate) composite_bgl: Option<wgpu::BindGroupLayout>,
358    pub(crate) composite_bind_group: Option<wgpu::BindGroup>,
359    pub(crate) composite_sampler: Option<wgpu::Sampler>,
360}
361
362/// Image slice render pipeline and layout. Lazily built.
363#[derive(Default)]
364pub(crate) struct ImageSliceResources {
365    /// Image slice render pipeline. None until first slice item is submitted.
366    pub(crate) pipeline: Option<DualPipeline>,
367    /// Group 1 layout for image slice uniforms.
368    pub(crate) bgl: Option<wgpu::BindGroupLayout>,
369}
370
371/// Former name of [`DeviceResources`]. Renamed to reflect that this holds the
372/// device-shared resources, not per-viewport state. Kept as an alias so existing
373/// code keeps compiling; prefer `DeviceResources` in new code.
374#[deprecated(note = "renamed to DeviceResources")]
375pub type ViewportGpuResources = DeviceResources;
376
377/// Uploaded GPU assets and their handle registries: user textures, geometry /
378/// scivis stores, colourmap and matcap tables, plus the fallback LUT and the
379/// zero-fill attribute buffers bound when an optional attribute is absent.
380///
381/// This is the content-residency set: everything addressed by a handle
382/// (`TextureId`, `PolylineId`, `ColourmapId`, ...) or provided as a default when
383/// a handle is missing. It carries no pipelines; the upload / registry methods
384/// stay on `DeviceResources` and reach these through `self.content`. The shared
385/// material and LUT samplers and the fallback material textures stay on the core
386/// because the lit pass samples them on every draw.
387pub struct ContentResources {
388    /// Cache of material bind groups keyed by (albedo_id, normal_map_id, ao_map_id).
389    /// u64::MAX sentinel = use fallback texture for that slot.
390    #[allow(dead_code)]
391    pub(crate) material_bind_groups: std::collections::HashMap<(u64, u64, u64), wgpu::BindGroup>,
392    /// User-uploaded textures, keyed by the `texture_id` in Material. Slotted
393    /// with generational ids so a freed slot cannot alias a later upload.
394    pub(crate) textures: crate::resources::material::texture_store::TextureStore,
395    /// Pre-uploaded polyline storage; entries are referenced from per-frame
396    /// `PolylineRefItem`s.
397    pub(crate) polyline_store: super::PolylineStore,
398    /// Pre-uploaded streamtube storage.
399    pub(crate) streamtube_store: super::StreamtubeStore,
400    /// Pre-uploaded tube storage.
401    pub(crate) tube_store: super::TubeStore,
402    /// Pre-uploaded ribbon storage.
403    pub(crate) ribbon_store: super::RibbonStore,
404    /// Pre-uploaded point cloud storage.
405    pub(crate) point_cloud_store: super::PointCloudStore,
406    /// Pre-uploaded glyph set storage.
407    pub(crate) glyph_set_store: super::GlyphSetStore,
408    /// Pre-uploaded tensor glyph set storage.
409    pub(crate) tensor_glyph_set_store: super::TensorGlyphSetStore,
410    /// Pre-uploaded sprite set storage.
411    pub(crate) sprite_set_store: super::SpriteSetStore,
412    /// Pre-uploaded sprite instance set storage.
413    pub(crate) sprite_instance_set_store: super::SpriteInstanceSetStore,
414    /// Slotted store of all uploaded Gaussian splat sets.
415    pub(crate) gaussian_splat_store: GaussianSplatStore,
416    /// Uploaded 3D volume textures. Index = VolumeId value.
417    pub(crate) volume_textures:
418        crate::resources::handle::Registry<(wgpu::Texture, wgpu::TextureView)>,
419    /// Uploaded projected-tet meshes. Index = ProjectedTetId value.
420    pub(crate) projected_tet_store: crate::resources::handle::Registry<GpuProjectedTetMesh>,
421    /// Glyph atlas for overlay text rendering (labels, scalar bars, rulers).
422    pub(crate) glyph_atlas: crate::resources::overlay::font::GlyphAtlas,
423    /// Persistent textures uploaded via `upload_overlay_texture`.
424    pub(crate) overlay_textures: crate::resources::handle::Registry<OverlayShapeTextureEntry>,
425    /// Matcap textures (256x256 RGBA), indexed by `MatcapId::index`.
426    pub(crate) matcap_textures: Vec<wgpu::Texture>,
427    /// Texture views for each uploaded matcap.
428    pub(crate) matcap_views: Vec<wgpu::TextureView>,
429    /// Linear-clamp sampler shared by all matcap texture lookups.
430    pub(crate) matcap_sampler: Option<wgpu::Sampler>,
431    /// Fallback 1x1 white view bound to binding 7 when no matcap is active.
432    pub(crate) fallback_matcap_view: Option<wgpu::TextureView>,
433    /// Whether built-in matcaps have been uploaded to the GPU.
434    pub(crate) matcaps_initialized: bool,
435    /// `MatcapId` for each built-in preset, populated by `ensure_matcaps_initialized`.
436    pub(crate) builtin_matcap_ids: Option<[MatcapId; 8]>,
437    /// Uploaded colourmap GPU textures. Index = ColourmapId value.
438    pub(crate) colourmap_textures: Vec<wgpu::Texture>,
439    /// Views into colourmap_textures. Index = ColourmapId value.
440    pub(crate) colourmap_views: Vec<wgpu::TextureView>,
441    /// CPU-side copy of each colourmap for egui scalar bar rendering. Index = ColourmapId value.
442    pub(crate) colourmaps_cpu: Vec<[[u8; 4]; 256]>,
443    /// Fallback 1x1 LUT texture (bound when has_attribute=0; content irrelevant to the shader).
444    #[allow(dead_code)]
445    pub(crate) fallback_lut_texture: wgpu::Texture,
446    /// View of fallback_lut_texture.
447    pub(crate) fallback_lut_view: wgpu::TextureView,
448    /// Fallback 4-byte zero storage buffer (bound when no scalar attribute is active).
449    pub(crate) fallback_scalar_buf: wgpu::Buffer,
450    /// Fallback 16-byte zero storage buffer (bound to binding 8 when no face colour attribute is active).
451    pub(crate) fallback_face_colour_buf: wgpu::Buffer,
452    /// Fallback 12-byte zero storage buffer (bound to binding 9 when no warp attribute is active).
453    pub(crate) fallback_warp_buf: wgpu::Buffer,
454    /// Fallback 12-byte zero storage buffer (bound to binding 13 when no
455    /// position override is active). Single `vec3<f32>(0,0,0)` entry; the
456    /// shader bounds-checks `arrayLength` before reading.
457    pub(crate) fallback_position_override_buf: wgpu::Buffer,
458    /// Fallback 12-byte zero storage buffer (bound to binding 14 when no
459    /// normal override is active).
460    pub(crate) fallback_normal_override_buf: wgpu::Buffer,
461    /// IDs of built-in preset colourmaps, in BuiltinColourmap discriminant order.
462    /// `None` until `ensure_colourmaps_initialized()` has been called.
463    pub(crate) builtin_colourmap_ids: Option<[ColourmapId; 10]>,
464    /// Whether built-in colourmaps have been uploaded to the GPU.
465    pub(crate) colourmaps_initialized: bool,
466}
467
468/// Device-shared GPU resources: pipelines, layouts, samplers, fallbacks, LUTs,
469/// and the per-feature pipeline clusters (`decal`, `scatter`, `volume`, ...).
470/// Created once at init and shared across every viewport.
471///
472/// Typically stored in the host framework's resource container and accessed
473/// by `ViewportRenderer` during prepare() and paint().
474#[allow(dead_code)]
475pub struct DeviceResources {
476    /// Swapchain texture format; all pipelines are compiled for this format.
477    pub target_format: wgpu::TextureFormat,
478    /// MSAA sample count used by all render pipelines.
479    pub sample_count: u32,
480    /// Optional pipeline cache shared by every pipeline built here. `Some` only
481    /// when the device enables `Features::PIPELINE_CACHE`. Persist its contents
482    /// across runs with `ViewportRenderer::pipeline_cache_data` to skip shader
483    /// recompilation on later launches.
484    pub pipeline_cache: Option<wgpu::PipelineCache>,
485    /// Solid-shaded render pipeline (TriangleList topology, no blending).
486    pub solid_pipeline: wgpu::RenderPipeline,
487    /// Solid-shaded render pipeline with back-face culling disabled (two-sided surfaces).
488    pub solid_two_sided_pipeline: wgpu::RenderPipeline,
489    /// Transparent render pipeline (TriangleList topology, alpha blending).
490    pub transparent_pipeline: wgpu::RenderPipeline,
491    /// Wireframe render pipeline (LineList topology, same shader).
492    pub wireframe_pipeline: wgpu::RenderPipeline,
493    /// Uniform buffer holding the per-frame `CameraUniform` (view-proj + eye position).
494    pub camera_uniform_buf: wgpu::Buffer,
495    /// Uniform buffer holding the per-frame `LightsUniform` header (count +
496    /// hemisphere + IBL + debug params). The per-light array lives in
497    /// `light_storage_buf` (binding 13).
498    pub light_uniform_buf: wgpu::Buffer,
499    /// Storage buffer of per-light `SingleLightUniform` entries (binding 13).
500    ///
501    /// Sized for `MAX_SCENE_LIGHTS`. The renderer truncates the consumer's
502    /// light list to this cap each frame, ranking surplus lights by
503    /// `LightSource::importance * proximity_weight`.
504    pub light_storage_buf: wgpu::Buffer,
505    /// Clustered-shading state: cluster grid, global light index list, and the
506    /// per-frame cluster build pipeline. Bindings 14/15/16 of the camera bind
507    /// group expose this state to every lit pipeline.
508    pub clustered: crate::resources::gpu::clustered::ClusteredResources,
509    /// Bind group (group 0) binding camera, light, clip-plane, and shadow uniforms.
510    pub camera_bind_group: wgpu::BindGroup,
511    /// Bind group layout for group 0 (shared by all scene pipelines).
512    pub camera_bind_group_layout: wgpu::BindGroupLayout,
513    /// Bind group layout for group 1 (per-object uniform: model, material, selection).
514    pub object_bind_group_layout: wgpu::BindGroupLayout,
515    /// Scene meshes (slotted storage with free-list removal).
516    pub(crate) mesh_store: crate::resources::mesh::mesh_store::MeshStore,
517    /// Registered LOD groups. Each groups several meshes that are detail
518    /// variants of one object; the renderer picks a level per frame.
519    pub(crate) lod_groups: crate::resources::mesh::lod::LodGroupStore,
520    /// Per-vertex deformation sidecar storage: header uniform, dummy fallback
521    /// buffers, and per-mesh slot bind groups. Every mesh-family pipeline
522    /// binds `@group(2)` from this state; meshes without attached deformer
523    /// data fall back to the renderer-owned dummy bind group.
524    pub(crate) deform: crate::resources::mesh_sidecar::deform::DeformationState,
525    // --- Shadow map resources ---
526    /// Shadow atlas depth texture (Depth32Float, atlas_size x atlas_size, 2x2 tile grid).
527    pub shadow_map_texture: wgpu::Texture,
528    /// Depth texture view for binding as a shader resource (sampling).
529    pub shadow_map_view: wgpu::TextureView,
530    /// Comparison sampler for PCF shadow filtering.
531    pub shadow_sampler: wgpu::Sampler,
532    /// Cubemap-array depth texture for point-light shadows. Layered as
533    /// `MAX_POINT_SHADOW_LIGHTS * 6` faces of `POINT_SHADOW_FACE_SIZE` px.
534    pub point_shadow_cube_texture: wgpu::Texture,
535    /// `texture_depth_cube_array` view bound to the lit-pass bind group.
536    pub point_shadow_cube_view: wgpu::TextureView,
537    /// One 2D-array view per face, used as the depth attachment during the
538    /// shadow render pass. `len() == MAX_POINT_SHADOW_LIGHTS * 6`, indexed
539    /// as `slot * 6 + face`.
540    pub point_shadow_face_views: Vec<wgpu::TextureView>,
541    /// Render pipeline for the point-shadow depth pass. Same vertex layout
542    /// as the cascade shadow pipeline; writes linear distance-to-light.
543    pub shadow_point_pipeline: wgpu::RenderPipeline,
544    /// Bind group layout for the point-shadow per-face uniform (group 0
545    /// of the point shadow pass). Kept for pipeline rebuilds.
546    pub(crate) shadow_point_face_bind_group_layout: wgpu::BindGroupLayout,
547    /// Per-face uniform buffer holding `view_proj`, `light_pos`, `range`
548    /// for every (slot, face) of the point shadow array. Sized as
549    /// `MAX_POINT_SHADOW_LIGHTS * 6 * 256` bytes (256-byte dynamic-offset
550    /// stride).
551    pub shadow_point_face_buf: wgpu::Buffer,
552    /// Bind group for the point-shadow per-face uniform. Stride is 256;
553    /// the per-face render pass sets a dynamic offset.
554    pub shadow_point_face_bind_group: wgpu::BindGroup,
555    /// Render pipeline for the shadow depth pass (depth-only, no fragment output).
556    ///
557    /// Culls front faces, so closed solids cast shadow from their back face
558    /// and a solid's own front face is never compared against itself in the
559    /// shadow map. Two-sided materials (`BackfacePolicy::Identical` and
560    /// friends) are routed to `shadow_pipeline_two_sided` instead so both
561    /// sides of cloth, foliage, and planar surfaces cast shadows.
562    pub shadow_pipeline: wgpu::RenderPipeline,
563    /// Shadow caster pipeline for two-sided materials. Same layout and shader
564    /// as `shadow_pipeline` but with `cull_mode: None` and a larger caster-side
565    /// depth bias (`CSM_SHADOW_BIAS_TWO_SIDED`) so both sides of a two-sided
566    /// mesh rasterise into the shadow atlas without the surface self-shadowing
567    /// where it is its own receiver.
568    pub shadow_pipeline_two_sided: wgpu::RenderPipeline,
569    /// Bind group layout for the shadow camera uniform (group 0 of the
570    /// shadow pass). Kept on the renderer so `register_deformer` can rebuild
571    /// the shadow pipeline from a freshly composed shader module.
572    pub(crate) shadow_camera_bind_group_layout: wgpu::BindGroupLayout,
573    /// Uniform buffer holding the per-cascade light-space view-projection matrix (64 bytes).
574    pub shadow_uniform_buf: wgpu::Buffer,
575    /// Bind group for the shadow pass (group 0: light uniform).
576    pub shadow_bind_group: wgpu::BindGroup,
577    /// Uniform buffer for the ShadowAtlasUniform (binding 5 of camera_bgl, 416 bytes).
578    pub shadow_info_buf: wgpu::Buffer,
579    /// Current shadow atlas texture size. Used to detect when atlas needs recreation.
580    #[allow(dead_code)]
581    pub(crate) shadow_atlas_size: u32,
582    /// Non-comparison sampler for reading depth values as float (atlas viewer).
583    pub shadow_atlas_depth_sampler: wgpu::Sampler,
584    /// Pipeline for the shadow atlas corner overlay.
585    pub shadow_atlas_viewer_pipeline: wgpu::RenderPipeline,
586    /// Bind group for the atlas viewer (uniform + depth texture + sampler).
587    pub shadow_atlas_viewer_bg: wgpu::BindGroup,
588    /// Uniform buffer: NDC rect of the atlas viewer quad.
589    pub shadow_atlas_viewer_buf: wgpu::Buffer,
590    /// 16-byte sentinel bound at group 0 binding 12 when the debug fragment buffer is inactive.
591    pub debug_frag_sentinel_buf: wgpu::Buffer,
592
593    // --- Gizmo resources ---
594    /// Gizmo render pipeline (TriangleList, depth_compare Always : always on top).
595    pub gizmo_pipeline: wgpu::RenderPipeline,
596    /// Gizmo vertex buffer (3 axis arrows, regenerated when hovered axis changes).
597    pub gizmo_vertex_buffer: wgpu::Buffer,
598    /// Gizmo index buffer.
599    pub gizmo_index_buffer: wgpu::Buffer,
600    /// Number of indices in the gizmo index buffer.
601    pub gizmo_index_count: u32,
602    /// Gizmo uniform buffer (model matrix: positions gizmo at selected object, scaled to screen size).
603    pub gizmo_uniform_buf: wgpu::Buffer,
604    /// Bind group for gizmo uniform (group 1).
605    pub gizmo_bind_group: wgpu::BindGroup,
606    /// Bind group layout for gizmo uniforms : stored so per-viewport gizmo bind groups can be created.
607    pub(crate) gizmo_bind_group_layout: wgpu::BindGroupLayout,
608
609    // --- Overlay resources ---
610    /// Overlay render pipeline (TriangleList with alpha blending : for semi-transparent BC quads).
611    pub overlay_pipeline: wgpu::RenderPipeline,
612    /// Overlay wireframe pipeline (LineList, no alpha blending needed).
613    pub overlay_line_pipeline: wgpu::RenderPipeline,
614    /// Full-screen analytical grid pipeline (no vertex buffer : positions hardcoded in shader).
615    pub grid_pipeline: wgpu::RenderPipeline,
616    /// Uniform buffer for the grid shader (GridUniform : written every frame in prepare()).
617    pub grid_uniform_buf: wgpu::Buffer,
618    /// Bind group for the grid uniform (group 0, single binding).
619    pub grid_bind_group: wgpu::BindGroup,
620    /// Bind group layout for the grid uniform (stored so per-viewport grid bind groups can be created).
621    pub(crate) grid_bind_group_layout: wgpu::BindGroupLayout,
622    /// Bind group layout for overlay uniforms (group 1: model + colour uniform).
623    pub overlay_bind_group_layout: wgpu::BindGroupLayout,
624
625    // --- Constraint guide lines ---
626    /// Transient constraint guide lines, rebuilt each frame in prepare().
627    /// Each entry: (vertex_buffer, index_buffer, index_count, uniform_buffer, bind_group).
628    pub constraint_line_buffers: Vec<(
629        wgpu::Buffer,
630        wgpu::Buffer,
631        u32,
632        wgpu::Buffer,
633        wgpu::BindGroup,
634    )>,
635
636    // --- Axes indicator ---
637    /// Screen-space axes indicator pipeline (TriangleList, no depth, alpha blending).
638    pub axes_pipeline: wgpu::RenderPipeline,
639    /// Vertex buffer for axes indicator geometry (rebuilt each frame).
640    pub axes_vertex_buffer: wgpu::Buffer,
641    /// Number of vertices in the axes indicator buffer.
642    pub axes_vertex_count: u32,
643
644    // --- Texture system ---
645    /// Bind group layout for texture group (group 2: albedo + sampler + normal_map + ao_map).
646    pub texture_bind_group_layout: wgpu::BindGroupLayout,
647    /// Fallback 1x1 white texture used when material.texture_id is None.
648    pub fallback_texture: GpuTexture,
649    /// Fallback 1x1 flat normal map [128,128,255,255] (tangent-space neutral).
650    pub(crate) fallback_normal_map: wgpu::Texture,
651    pub(crate) fallback_normal_map_view: wgpu::TextureView,
652    /// Fallback 1x1 AO map [255,255,255,255] (no occlusion).
653    pub(crate) fallback_ao_map: wgpu::Texture,
654    pub(crate) fallback_ao_map_view: wgpu::TextureView,
655    /// Fallback 1x1 metallic-roughness texture [0, 255, 255, 255].
656    /// G=1.0 and B=1.0 so scalar factors pass through unchanged when no ORM texture is set.
657    pub(crate) fallback_metallic_roughness_texture: wgpu::Texture,
658    pub(crate) fallback_metallic_roughness_texture_view: wgpu::TextureView,
659    /// Fallback 1x1 emissive texture [0, 0, 0, 255] (no emission).
660    pub(crate) fallback_emissive_texture: wgpu::Texture,
661    pub(crate) fallback_emissive_texture_view: wgpu::TextureView,
662    /// Shared linear-repeat sampler for material textures.
663    pub(crate) material_sampler: wgpu::Sampler,
664    /// Shared linear-clamp sampler for colourmap LUT lookups.
665    pub(crate) lut_sampler: wgpu::Sampler,
666    /// Uploaded GPU assets and their handle registries (textures, geometry /
667    /// scivis stores, colourmap and matcap tables, fallback LUT and attribute buffers).
668    pub(crate) content: ContentResources,
669    /// Background runner used by async upload entry points. Drained once per
670    /// frame during `prepare_scene` so completion is visible to the caller.
671    /// Wrapped in a mutex because mpsc receivers and boxed `FnOnce`
672    /// callbacks are `Send` but not `Sync`, and several host frameworks
673    /// require this struct to be `Sync`.
674    pub(crate) jobs: std::sync::Mutex<super::upload_jobs::JobRunner>,
675    /// Typed result slots for every async upload path, keyed by job id.
676    /// Grouped into one struct so the async bookkeeping is a single field
677    /// rather than a score of flat ones; see `upload_jobs::JobResults`.
678    pub(crate) job_results: super::upload_jobs::JobResults,
679
680    /// Whether fallback normal map / AO map pixels have been uploaded.
681    pub(crate) fallback_textures_uploaded: bool,
682
683    // --- Shared post-processing pipelines / layouts / samplers ---
684    /// FXAA/SSAA, bloom, SSAO, tone-map, DoF, contact shadows, placeholders,
685    /// PP samplers, depth blit, and dyn-res upscale. Viewport-sized targets and
686    /// per-frame uniforms live on `ViewportHdrState`.
687    pub(crate) post: crate::resources::postprocess::PostProcessResources,
688
689    // --- Clip planes ---
690    /// Uniform buffer for clip planes (binding 4 of camera bind group).
691    pub(crate) clip_planes_uniform_buf: wgpu::Buffer,
692    /// Uniform buffer for the extended clip volume (binding 6 of camera bind group, 128 bytes).
693    pub(crate) clip_volume_uniform_buf: wgpu::Buffer,
694
695    // --- Outline & x-ray resources ---
696    // The volume outline mask pipeline lives on `volume.outline_mask_pipeline`;
697    // the glyph / tensor-glyph ones on `glyph.outline_mask_pipeline` and
698    // `tensor_glyph.outline_mask_pipeline`.
699    /// Outline / x-ray pipelines, offscreen mask/composite targets, and layouts.
700    pub(crate) outline: OutlineResources,
701
702    // --- Instancing and GPU-culling clusters ---
703    /// Instanced-draw pipelines, shared instance storage buffer, and bind group cache.
704    pub(crate) instancing: crate::resources::mesh::instancing::InstancingResources,
705    /// GPU-cull inputs (per-instance AABBs, per-batch meta) and cull-variant pipelines.
706    /// The cull OUTPUTS (visibility indices, indirect args, batch counters) are
707    /// per-viewport and live in `ViewportCullState` on each `ViewportSlot`, not here.
708    pub(crate) cull: crate::resources::mesh::instancing::CullResources,
709
710    // --- Surface LIC shared resources ---
711    /// Surface LIC pipelines and layouts (surface + advect passes).
712    pub(crate) lic: crate::resources::postprocess::LicResources,
713
714    /// HDR-format variants of core scene pipelines.
715    pub(crate) hdr_solid_pipeline: Option<wgpu::RenderPipeline>,
716    /// HDR two-sided variant (cull_mode: None) for analytical surfaces.
717    pub(crate) hdr_solid_two_sided_pipeline: Option<wgpu::RenderPipeline>,
718    pub(crate) hdr_transparent_pipeline: Option<wgpu::RenderPipeline>,
719    pub(crate) hdr_wireframe_pipeline: Option<wgpu::RenderPipeline>,
720    /// HDR overlay pipeline (TriangleList, Rgba16Float, alpha blending) for cap fill in HDR path.
721    pub(crate) hdr_overlay_pipeline: Option<wgpu::RenderPipeline>,
722
723    // --- Gaussian splat pipelines (lazily created) ---
724    /// Gaussian splat render/sort pipelines and their bind group layouts.
725    pub(crate) gaussian_splat: crate::resources::scivis::gaussian_splat::GaussianSplatResources,
726
727    // --- Sprite billboard pipelines (lazily created) ---
728    /// Sprite (emissive + lit) pipelines, layouts, refraction, and soft-particle fallbacks.
729    pub(crate) sprite: crate::resources::scivis::sprite::SpriteResources,
730    // The polyline outline mask pipeline lives on `polyline.outline_mask_pipeline`.
731
732    // --- point cloud pipelines (lazily created) ---
733    /// Point cloud render pipeline. None until first point cloud is submitted.
734    pub(crate) point_cloud_pipeline: Option<DualPipeline>,
735    /// Bind group layout for point cloud uniforms (group 1).
736    pub(crate) point_cloud_bgl: Option<wgpu::BindGroupLayout>,
737
738    // --- glyph rendering (lazily created) ---
739    /// Arrow/sphere/cube glyph pipelines, layouts, and cached base meshes.
740    pub(crate) glyph: crate::resources::scivis::glyph::GlyphResources,
741    /// Tensor glyph pipelines and layouts.
742    pub(crate) tensor_glyph: crate::resources::scivis::glyph::TensorGlyphResources,
743
744    // --- polyline / streamtube / ribbon rendering (lazily created) ---
745    /// Polyline pipelines and layouts.
746    pub(crate) polyline: crate::resources::scivis::polyline::PolylineResources,
747    /// Streamtube pipelines and layout.
748    pub(crate) streamtube: crate::resources::scivis::tube::StreamtubeResources,
749    /// Ribbon pipelines (one per blend) and layout.
750    pub(crate) ribbon: crate::resources::scivis::tube::RibbonResources,
751
752    // --- Image slice rendering (lazily created) ---
753    /// Image slice render pipeline and layout.
754    pub(crate) image_slice: ImageSliceResources,
755
756    // --- volume rendering (lazily created) ---
757    /// Volume render/surface-slice/outline pipelines, layouts, cube geometry, and default LUT.
758    pub(crate) volume: crate::resources::volume::volumes::VolumeResources,
759
760    // --- GPU compute filtering (lazily created) ---
761    /// Compute pipeline for Clip / Threshold index compaction. None until first use.
762    pub(crate) compute_filter_pipeline: Option<wgpu::ComputePipeline>,
763    /// Bind group layout for the compute filter shader (group 0). None until first use.
764    pub(crate) compute_filter_bgl: Option<wgpu::BindGroupLayout>,
765
766    // --- Order-independent transparency (OIT) : lazily created ---
767    // The viewport-sized accum/reveal textures, composite bind group, and target
768    // size live on ViewportHdrState; only the shared pipelines and layout sit here.
769    /// Weighted-blended OIT pipelines and composite layout.
770    pub(crate) oit: crate::resources::postprocess::OitResources,
771
772    // --- Projected tetrahedra transparent volume rendering (lazily created) ---
773    /// Projected-tetrahedra pipeline, layouts, and LUT bind group cache.
774    pub(crate) pt: ProjectedTetResources,
775
776    // --- Scatter-volume (participating media) rendering (lazily created) ---
777    /// Scatter-volume pipelines, layouts, and per-frame upload buffers.
778    pub(crate) scatter: crate::resources::volume::scatter_volume::ScatterResources,
779
780    // --- IBL / environment map resources ---
781    /// IBL irradiance equirect texture view (binding 7). None until environment uploaded.
782    pub ibl_irradiance_view: Option<wgpu::TextureView>,
783    /// IBL prefiltered specular equirect texture view (binding 8). None until environment uploaded.
784    pub ibl_prefiltered_view: Option<wgpu::TextureView>,
785    /// BRDF integration LUT texture view (binding 9). None until the first
786    /// `upload_environment_map`; cached across subsequent uploads (the LUT is
787    /// scene-independent: function of roughness x N.V only).
788    pub ibl_brdf_lut_view: Option<wgpu::TextureView>,
789    /// IBL linear-clamp sampler (binding 10).
790    pub(crate) ibl_sampler: wgpu::Sampler,
791    /// Skybox / full-res environment equirect texture view (binding 11). None until uploaded.
792    pub ibl_skybox_view: Option<wgpu::TextureView>,
793    /// Fallback 1x1 black Rgba16Float texture for IBL slots when no environment is loaded.
794    #[allow(dead_code)]
795    pub(crate) ibl_fallback_texture: wgpu::Texture,
796    /// View of ibl_fallback_texture.
797    pub(crate) ibl_fallback_view: wgpu::TextureView,
798    /// Fallback 1x1 BRDF LUT placeholder; swapped for the real 128x128 LUT
799    /// on the first `upload_environment_map` call. Bound to satisfy the bind
800    /// group layout when no environment map has been uploaded yet.
801    #[allow(dead_code)]
802    pub(crate) ibl_fallback_brdf_texture: wgpu::Texture,
803    pub(crate) ibl_fallback_brdf_view: wgpu::TextureView,
804    /// Uploaded irradiance texture (owned, kept alive for view).
805    #[allow(dead_code)]
806    pub(crate) ibl_irradiance_texture: Option<wgpu::Texture>,
807    /// Uploaded prefiltered specular texture (owned).
808    #[allow(dead_code)]
809    pub(crate) ibl_prefiltered_texture: Option<wgpu::Texture>,
810    /// Uploaded BRDF LUT texture (owned).
811    #[allow(dead_code)]
812    pub(crate) ibl_brdf_lut_texture: Option<wgpu::Texture>,
813    /// Uploaded skybox equirect texture (owned).
814    #[allow(dead_code)]
815    pub(crate) ibl_skybox_texture: Option<wgpu::Texture>,
816    /// Skybox fullscreen render pipeline (renders equirect environment as background).
817    pub(crate) skybox_pipeline: wgpu::RenderPipeline,
818
819    // --- Ground plane ---
820    /// Full-screen ground plane render pipeline (alpha blending, LessEqual depth).
821    pub(crate) ground_plane_pipeline: wgpu::RenderPipeline,
822    /// Bind group layout for the ground plane (binding 0: uniform, 1: shadow depth, 2: comparison sampler).
823    pub(crate) _ground_plane_bgl: wgpu::BindGroupLayout,
824    /// Uniform buffer for GroundPlaneUniform (256 bytes, written each frame in prepare()).
825    pub(crate) ground_plane_uniform_buf: wgpu::Buffer,
826    /// Bind group for the ground plane pass (rebuilt when shadow atlas changes).
827    pub(crate) ground_plane_bind_group: wgpu::BindGroup,
828
829    // --- GPU implicit surface (lazily created) ---
830    /// Implicit-surface ray-march pipeline, layout, and outline mask.
831    pub(crate) implicit: ImplicitResources,
832
833    // --- GPU marching cubes (lazily created) ---
834    /// Marching-cubes compute/render pipelines, layouts, case tables, and per-item volumes.
835    pub(crate) mc: crate::resources::volume::gpu_marching_cubes::McResources,
836
837    // --- GPU particle systems ---
838    /// Particle compute/draw pipelines, their layouts, and the live systems.
839    pub(crate) particle: crate::resources::gpu::gpu_particles::ParticleResources,
840
841    // --- Screen-space image overlays (lazily created) ---
842    /// Screen-space image pipelines (plain + depth-composite) and rect outline mask.
843    pub(crate) screen_image: ScreenImageResources,
844
845    // --- GPU object-ID picking (lazily created) ---
846    /// Object-ID pick pipeline and its bind group layouts.
847    pub(crate) pick: PickResources,
848
849    // --- Sub-object highlight (lazily created) ---
850    /// Sub-object highlight pipelines (fill / edge / sprite, HDR + LDR) and layout.
851    pub(crate) sub_highlight: SubHighlightResources,
852
853    // --- Overlay text / SDF shape / backdrop-blur pipelines (lazily created) ---
854    /// Overlay text pipeline, layout, and sampler.
855    pub(crate) overlay_text: crate::resources::overlay::overlay_text::OverlayTextResources,
856    /// SDF overlay shape pipelines (solid + textured) and sampler.
857    pub(crate) overlay_shape: crate::resources::overlay::overlay_shape::OverlayShapeResources,
858    /// Backdrop blur pipeline, layout, and sampler.
859    pub(crate) backdrop_blur: crate::resources::overlay::overlay_shape::BackdropBlurResources,
860
861    // --- Depth blit pipeline (lazily created, shared across all viewports) ---
862    // Copies a scene-resolution depth texture to a native-resolution depth-only target.
863    // Used by the HDR path when render_scale < 1.0.
864    // The depth-blit and dynamic-resolution upscale pipelines live on `post`.
865
866    // --- Runtime performance tracking ---
867    /// Cumulative bytes of geometry data uploaded since the last `prepare()` reset.
868    ///
869    /// Incremented by `upload_mesh`, `upload_mesh_data`, and `replace_mesh_data`.
870    /// Read and reset at the start of each `prepare()` call to populate
871    /// `FrameStats::upload_bytes`.
872    pub frame_upload_bytes: u64,
873
874    // --- Screen-space decal pipelines (D1 + D5, lazily created) ---
875    /// Decal render/exclude pipelines and their bind group layouts.
876    pub(crate) decal: crate::resources::decal::DecalResources,
877
878    // --- HiZ occlusion culling ---
879    /// When true, the main-camera GPU cull runs the HiZ occlusion test on top
880    /// of the frustum test. Off by default (the test is scene-dependent and a
881    /// safety valve against the one-frame-stale depth source). The HiZ pyramid
882    /// itself is per-viewport and lives on `ViewportCullState::hiz`.
883    pub(crate) occlusion_culling_enabled: bool,
884}
885
886/// Per-viewport GPU culling outputs.
887///
888/// The frustum/occlusion cull runs against one camera and writes a compact
889/// visibility list plus the indirect draw args for that camera. Those results
890/// are viewport-specific: two viewports on different cameras must not share
891/// them, or the last one to run would clobber the other. The cull INPUTS
892/// (per-instance AABBs, per-batch meta) and all cull PIPELINES are
893/// camera-independent and stay on `DeviceResources`.
894///
895/// Owned by each `ViewportSlot`. The bind-group caches here reference this
896/// state's own buffers, so they are invalidated when those buffers resize.
897pub(crate) struct ViewportCullState {
898    /// Per-batch atomic counter buffer. Zeroed at the start of each cull dispatch.
899    pub(crate) batch_counter_buf: Option<wgpu::Buffer>,
900    /// Compact list of visible instance indices. Written by the compute cull pass.
901    pub(crate) visibility_index_buf: Option<wgpu::Buffer>,
902    pub(crate) visibility_index_capacity: usize,
903    /// Indirect draw args buffer for the main pass (one DrawIndexedIndirect per batch).
904    pub(crate) indirect_args_buf: Option<wgpu::Buffer>,
905    /// Capacity (in batches) of the counter and indirect-args buffers.
906    pub(crate) batch_output_capacity: usize,
907    /// Per-texture-key bind groups for the main cull pipelines.
908    /// Keyed by (albedo_id, normal_map_id, ao_map_id); invalidated when
909    /// `visibility_index_buf` is resized.
910    pub(crate) instance_cull_bind_groups:
911        std::collections::HashMap<(u64, u64, u64), wgpu::BindGroup>,
912    /// Generation of the shared instance buffers the main cull bind groups were
913    /// built against. When it falls behind `InstancingState::instance_gen` the
914    /// shared instance storage buffer was rebuilt, so those bind groups (which
915    /// bind it at binding 0) are stale and get cleared.
916    pub(crate) built_gen: u64,
917    /// Hierarchical-Z max-depth pyramid for this viewport's occlusion test.
918    /// Lazily created the first frame occlusion culling stores depth here, and
919    /// rebuilt when the depth target changes size. Per-viewport so two viewports
920    /// on different cameras reproject their own depth instead of clobbering a
921    /// shared pyramid.
922    pub(crate) hiz: Option<crate::resources::gpu::hiz::HizState>,
923}
924
925impl ViewportCullState {
926    pub(crate) fn new() -> Self {
927        Self {
928            batch_counter_buf: None,
929            visibility_index_buf: None,
930            visibility_index_capacity: 0,
931            indirect_args_buf: None,
932            batch_output_capacity: 0,
933            instance_cull_bind_groups: std::collections::HashMap::new(),
934            built_gen: u64::MAX,
935            hiz: None,
936        }
937    }
938
939    /// Allocate or grow this viewport's cull output buffers to fit the current
940    /// instance and batch counts. The visibility buffer grows with
941    /// `instance_count`; the counter and indirect-args buffers grow with
942    /// `batch_count`. Uses the same 2x growth as the shared input buffers. Bind
943    /// groups referencing a reallocated buffer are cleared.
944    pub(crate) fn ensure_outputs(
945        &mut self,
946        device: &wgpu::Device,
947        instance_count: u32,
948        batch_count: u32,
949    ) {
950        // Visibility buffer, sized like the shared AABB buffer.
951        let max_instances = (device.limits().max_storage_buffer_binding_size as usize)
952            / std::mem::size_of::<InstanceAabb>();
953        let instance_count = (instance_count as usize).min(max_instances);
954        if instance_count > self.visibility_index_capacity {
955            let new_cap = (instance_count * 2).max(64).min(max_instances);
956            let vis_size = (new_cap * std::mem::size_of::<u32>()) as u64;
957            self.visibility_index_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
958                label: Some("visibility_index_buf"),
959                size: vis_size,
960                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
961                mapped_at_creation: false,
962            }));
963            self.visibility_index_capacity = new_cap;
964            // The cull bind groups bind the vis buffer at binding 5.
965            self.instance_cull_bind_groups.clear();
966        }
967
968        // Counter and indirect-args buffers, sized like the shared batch-meta buffer.
969        let max_batches = (device.limits().max_storage_buffer_binding_size as usize)
970            / std::mem::size_of::<BatchMeta>();
971        let batch_count = (batch_count as usize).min(max_batches);
972        if batch_count > self.batch_output_capacity {
973            let new_cap = (batch_count * 2).max(16).min(max_batches);
974            let counter_size = (new_cap * std::mem::size_of::<u32>()) as u64;
975            // wgpu::util::DrawIndexedIndirect is 5 x u32 = 20 bytes.
976            let indirect_size = (new_cap * 20) as u64;
977            self.batch_counter_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
978                label: Some("batch_counter_buf"),
979                size: counter_size,
980                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
981                mapped_at_creation: false,
982            }));
983            // iOS Metal and Android (emulator and older devices) do not
984            // reliably support INDIRECT_EXECUTION. Leave these as None so the
985            // renderer falls back to direct draw calls.
986            if cfg!(not(any(target_os = "ios", target_os = "android"))) {
987                self.indirect_args_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
988                    label: Some("indirect_args_buf"),
989                    size: indirect_size,
990                    usage: wgpu::BufferUsages::STORAGE
991                        | wgpu::BufferUsages::INDIRECT
992                        | wgpu::BufferUsages::COPY_DST
993                        | wgpu::BufferUsages::COPY_SRC,
994                    mapped_at_creation: false,
995                }));
996            }
997            self.batch_output_capacity = new_cap;
998        }
999    }
1000}
1001
1002/// Scene-scoped GPU culling outputs for the directional shadow cascades.
1003///
1004/// Shadows are fit to the primary camera and rendered once into a shared atlas,
1005/// so the shadow cull is not per-viewport: it runs once per frame and its
1006/// outputs live here rather than on any `ViewportSlot`. Owned by
1007/// `InstancingState`.
1008pub(crate) struct ShadowCullState {
1009    /// Per-batch atomic counter buffer. Zeroed at the start of each cascade's
1010    /// cull dispatch.
1011    pub(crate) batch_counter_buf: Option<wgpu::Buffer>,
1012    /// Per-cascade visibility index buffers (grow with the instance count).
1013    pub(crate) shadow_vis_bufs: [Option<wgpu::Buffer>; 4],
1014    /// Per-cascade indirect draw args buffers (grow with the batch count).
1015    pub(crate) shadow_indirect_bufs: [Option<wgpu::Buffer>; 4],
1016    /// Per-cascade instance+visibility bind groups. Invalidated when
1017    /// `shadow_vis_bufs` are reallocated.
1018    pub(crate) shadow_cull_instance_bgs: [Option<wgpu::BindGroup>; 4],
1019    /// Capacity (in instances) of `shadow_vis_bufs`.
1020    pub(crate) vis_capacity: usize,
1021    /// Capacity (in batches) of the counter and indirect-args buffers.
1022    pub(crate) batch_output_capacity: usize,
1023    /// Generation of the shared instance buffers the shadow cull bind groups were
1024    /// built against. Mirrors `ViewportCullState::built_gen`: when it falls behind
1025    /// `InstancingState::instance_gen` the instance storage buffer was rebuilt, so
1026    /// the bind groups (which bind it at binding 0) are stale.
1027    pub(crate) built_gen: u64,
1028}
1029
1030impl ShadowCullState {
1031    pub(crate) fn new() -> Self {
1032        Self {
1033            batch_counter_buf: None,
1034            shadow_vis_bufs: [None, None, None, None],
1035            shadow_indirect_bufs: [None, None, None, None],
1036            shadow_cull_instance_bgs: [None, None, None, None],
1037            vis_capacity: 0,
1038            batch_output_capacity: 0,
1039            built_gen: u64::MAX,
1040        }
1041    }
1042
1043    /// Allocate or grow the shadow cull output buffers to fit the current
1044    /// instance and batch counts. Mirrors `ViewportCullState::ensure_outputs`
1045    /// for the shadow cascades.
1046    pub(crate) fn ensure_outputs(
1047        &mut self,
1048        device: &wgpu::Device,
1049        instance_count: u32,
1050        batch_count: u32,
1051    ) {
1052        let max_instances = (device.limits().max_storage_buffer_binding_size as usize)
1053            / std::mem::size_of::<InstanceAabb>();
1054        let instance_count = (instance_count as usize).min(max_instances);
1055        if instance_count > self.vis_capacity {
1056            let new_cap = (instance_count * 2).max(64).min(max_instances);
1057            let vis_size = (new_cap * std::mem::size_of::<u32>()) as u64;
1058            for i in 0..4 {
1059                self.shadow_vis_bufs[i] = Some(device.create_buffer(&wgpu::BufferDescriptor {
1060                    label: Some(&format!("shadow_vis_buf_{i}")),
1061                    size: vis_size,
1062                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1063                    mapped_at_creation: false,
1064                }));
1065            }
1066            self.vis_capacity = new_cap;
1067            // The shadow cull bind groups bind these vis buffers at binding 5.
1068            self.shadow_cull_instance_bgs = [None, None, None, None];
1069        }
1070
1071        let max_batches = (device.limits().max_storage_buffer_binding_size as usize)
1072            / std::mem::size_of::<BatchMeta>();
1073        let batch_count = (batch_count as usize).min(max_batches);
1074        if batch_count > self.batch_output_capacity {
1075            let new_cap = (batch_count * 2).max(16).min(max_batches);
1076            let counter_size = (new_cap * std::mem::size_of::<u32>()) as u64;
1077            let indirect_size = (new_cap * 20) as u64;
1078            self.batch_counter_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
1079                label: Some("shadow_batch_counter_buf"),
1080                size: counter_size,
1081                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1082                mapped_at_creation: false,
1083            }));
1084            if cfg!(not(any(target_os = "ios", target_os = "android"))) {
1085                for i in 0..4 {
1086                    self.shadow_indirect_bufs[i] =
1087                        Some(device.create_buffer(&wgpu::BufferDescriptor {
1088                            label: Some(&format!("shadow_indirect_buf_{i}")),
1089                            size: indirect_size,
1090                            usage: wgpu::BufferUsages::STORAGE
1091                                | wgpu::BufferUsages::INDIRECT
1092                                | wgpu::BufferUsages::COPY_DST,
1093                            mapped_at_creation: false,
1094                        }));
1095                }
1096            }
1097            self.batch_output_capacity = new_cap;
1098        }
1099    }
1100}
1101
1102impl DeviceResources {
1103    /// Create a camera bind group (group 0) for the given per-viewport buffers.
1104    ///
1105    /// Per-viewport buffers (camera, clip planes, shadow info, clip volume) are
1106    /// passed explicitly. Scene-global resources (lights, shadow atlas, IBL) come
1107    /// from shared resources on `self`.
1108    ///
1109    /// NOTE: The initial bind group in `init.rs` is constructed inline (before
1110    /// `Self` exists). Keep the binding layout in sync when modifying either site.
1111    pub(crate) fn create_camera_bind_group(
1112        &self,
1113        device: &wgpu::Device,
1114        camera_buf: &wgpu::Buffer,
1115        clip_planes_buf: &wgpu::Buffer,
1116        shadow_info_buf: &wgpu::Buffer,
1117        clip_volume_buf: &wgpu::Buffer,
1118        debug_frag_buf: &wgpu::Buffer,
1119        label: &str,
1120    ) -> wgpu::BindGroup {
1121        let irr = self
1122            .ibl_irradiance_view
1123            .as_ref()
1124            .unwrap_or(&self.ibl_fallback_view);
1125        let spec = self
1126            .ibl_prefiltered_view
1127            .as_ref()
1128            .unwrap_or(&self.ibl_fallback_view);
1129        let brdf = self
1130            .ibl_brdf_lut_view
1131            .as_ref()
1132            .unwrap_or(&self.ibl_fallback_brdf_view);
1133        let skybox = self
1134            .ibl_skybox_view
1135            .as_ref()
1136            .unwrap_or(&self.ibl_fallback_view);
1137
1138        device.create_bind_group(&wgpu::BindGroupDescriptor {
1139            label: Some(label),
1140            layout: &self.camera_bind_group_layout,
1141            entries: &[
1142                wgpu::BindGroupEntry {
1143                    binding: 0,
1144                    resource: camera_buf.as_entire_binding(),
1145                },
1146                wgpu::BindGroupEntry {
1147                    binding: 1,
1148                    resource: wgpu::BindingResource::TextureView(&self.shadow_map_view),
1149                },
1150                wgpu::BindGroupEntry {
1151                    binding: 2,
1152                    resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
1153                },
1154                wgpu::BindGroupEntry {
1155                    binding: 3,
1156                    resource: self.light_uniform_buf.as_entire_binding(),
1157                },
1158                wgpu::BindGroupEntry {
1159                    binding: 4,
1160                    resource: clip_planes_buf.as_entire_binding(),
1161                },
1162                wgpu::BindGroupEntry {
1163                    binding: 5,
1164                    resource: shadow_info_buf.as_entire_binding(),
1165                },
1166                wgpu::BindGroupEntry {
1167                    binding: 6,
1168                    resource: clip_volume_buf.as_entire_binding(),
1169                },
1170                wgpu::BindGroupEntry {
1171                    binding: 7,
1172                    resource: wgpu::BindingResource::TextureView(irr),
1173                },
1174                wgpu::BindGroupEntry {
1175                    binding: 8,
1176                    resource: wgpu::BindingResource::TextureView(spec),
1177                },
1178                wgpu::BindGroupEntry {
1179                    binding: 9,
1180                    resource: wgpu::BindingResource::TextureView(brdf),
1181                },
1182                wgpu::BindGroupEntry {
1183                    binding: 10,
1184                    resource: wgpu::BindingResource::Sampler(&self.ibl_sampler),
1185                },
1186                wgpu::BindGroupEntry {
1187                    binding: 11,
1188                    resource: wgpu::BindingResource::TextureView(skybox),
1189                },
1190                wgpu::BindGroupEntry {
1191                    binding: 12,
1192                    resource: debug_frag_buf.as_entire_binding(),
1193                },
1194                wgpu::BindGroupEntry {
1195                    binding: 13,
1196                    resource: self.light_storage_buf.as_entire_binding(),
1197                },
1198                wgpu::BindGroupEntry {
1199                    binding: 14,
1200                    resource: self.clustered.grid_uniform_buf.as_entire_binding(),
1201                },
1202                wgpu::BindGroupEntry {
1203                    binding: 15,
1204                    resource: self.clustered.cluster_grid_buf.as_entire_binding(),
1205                },
1206                wgpu::BindGroupEntry {
1207                    binding: 16,
1208                    resource: self.clustered.light_index_buf.as_entire_binding(),
1209                },
1210                wgpu::BindGroupEntry {
1211                    binding: 17,
1212                    resource: wgpu::BindingResource::TextureView(&self.point_shadow_cube_view),
1213                },
1214            ],
1215        })
1216    }
1217}
1218
1219impl DeviceResources {
1220    /// Lazily create the GPU pick pipeline and associated bind group layouts.
1221    ///
1222    /// No-op if already created. Called from `ViewportRenderer::pick_scene_gpu`
1223    /// on first invocation : zero overhead when GPU picking is never used.
1224    pub(crate) fn ensure_pick_pipeline(&mut self, device: &wgpu::Device) {
1225        if self.pick.pipeline.is_some() {
1226            return;
1227        }
1228
1229        // --- group 0: pick camera bind group layout ---
1230        // Includes binding 0 (CameraUniform) and binding 6 (ClipVolumesUniform).
1231        // The full camera_bind_group_layout has many more bindings; a separate
1232        // minimal layout is cleaner and avoids binding unused resources.
1233        let pick_camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1234            label: Some("pick_camera_bgl"),
1235            entries: &[
1236                wgpu::BindGroupLayoutEntry {
1237                    binding: 0,
1238                    visibility: wgpu::ShaderStages::VERTEX,
1239                    ty: wgpu::BindingType::Buffer {
1240                        ty: wgpu::BufferBindingType::Uniform,
1241                        has_dynamic_offset: false,
1242                        min_binding_size: None,
1243                    },
1244                    count: None,
1245                },
1246                wgpu::BindGroupLayoutEntry {
1247                    binding: 6,
1248                    visibility: wgpu::ShaderStages::FRAGMENT,
1249                    ty: wgpu::BindingType::Buffer {
1250                        ty: wgpu::BufferBindingType::Uniform,
1251                        has_dynamic_offset: false,
1252                        min_binding_size: None,
1253                    },
1254                    count: None,
1255                },
1256            ],
1257        });
1258
1259        // --- group 1: PickInstance storage buffer ---
1260        let pick_instance_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1261            label: Some("pick_instance_bgl"),
1262            entries: &[wgpu::BindGroupLayoutEntry {
1263                binding: 0,
1264                visibility: wgpu::ShaderStages::VERTEX,
1265                ty: wgpu::BindingType::Buffer {
1266                    ty: wgpu::BufferBindingType::Storage { read_only: true },
1267                    has_dynamic_offset: false,
1268                    min_binding_size: None,
1269                },
1270                count: None,
1271            }],
1272        });
1273
1274        let shader = crate::resources::builders::wgsl_module(
1275            device,
1276            "pick_id_shader",
1277            crate::resources::builders::wgsl_source!("pick_id"),
1278        );
1279
1280        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1281            label: Some("pick_pipeline_layout"),
1282            bind_group_layouts: &[&pick_camera_bgl, &pick_instance_bgl],
1283            push_constant_ranges: &[],
1284        });
1285
1286        // Vertex layout: reuse the 64-byte Vertex stride but only declare position (location 0).
1287        let pick_vertex_layout = wgpu::VertexBufferLayout {
1288            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress, // 64 bytes
1289            step_mode: wgpu::VertexStepMode::Vertex,
1290            attributes: &[wgpu::VertexAttribute {
1291                offset: 0,
1292                shader_location: 0,
1293                format: wgpu::VertexFormat::Float32x3,
1294            }],
1295        };
1296
1297        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1298            label: Some("pick_pipeline"),
1299            layout: Some(&layout),
1300            vertex: wgpu::VertexState {
1301                module: &shader,
1302                entry_point: Some("vs_main"),
1303                buffers: &[pick_vertex_layout],
1304                compilation_options: wgpu::PipelineCompilationOptions::default(),
1305            },
1306            fragment: Some(wgpu::FragmentState {
1307                module: &shader,
1308                entry_point: Some("fs_main"),
1309                targets: &[
1310                    // location 0: R32Uint object ID
1311                    Some(wgpu::ColorTargetState {
1312                        format: wgpu::TextureFormat::R32Uint,
1313                        blend: None, // replace : no blending for integer targets
1314                        write_mask: wgpu::ColorWrites::ALL,
1315                    }),
1316                    // location 1: R32Float depth
1317                    Some(wgpu::ColorTargetState {
1318                        format: wgpu::TextureFormat::R32Float,
1319                        blend: None,
1320                        write_mask: wgpu::ColorWrites::ALL,
1321                    }),
1322                ],
1323                compilation_options: wgpu::PipelineCompilationOptions::default(),
1324            }),
1325            primitive: wgpu::PrimitiveState {
1326                topology: wgpu::PrimitiveTopology::TriangleList,
1327                front_face: wgpu::FrontFace::Ccw,
1328                cull_mode: None, // No culling: 3D meshes are often rendered two-sided; pick both faces.
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::Less,
1335                stencil: wgpu::StencilState::default(),
1336                bias: wgpu::DepthBiasState::default(),
1337            }),
1338            multisample: wgpu::MultisampleState {
1339                count: 1, // pick pass is always 1x (no MSAA)
1340                ..Default::default()
1341            },
1342            multiview: None,
1343            cache: None,
1344        });
1345
1346        self.pick.camera_bgl = Some(pick_camera_bgl);
1347        self.pick.bind_group_layout_1 = Some(pick_instance_bgl);
1348        self.pick.pipeline = Some(pipeline);
1349    }
1350}