Skip to main content

viewport_lib/renderer/
mod.rs

1//! `ViewportRenderer` : the main entry point for the viewport library.
2//!
3//! Wraps [`ViewportGpuResources`] and provides `prepare()` / `paint()` methods
4//! that take raw `wgpu` types. GUI framework adapters (e.g. the egui
5//! `CallbackTrait` impl in the application crate) delegate to these methods.
6
7#[macro_use]
8mod types;
9mod indirect;
10mod paths;
11pub use paths::{OwnedPath, PassPath, PassView};
12mod picking;
13pub use picking::PickRectResult;
14mod prepare;
15mod render;
16pub mod shader_hashes;
17mod shadow_debug_stats;
18mod shadows;
19pub mod stats;
20pub use shadow_debug_stats::ShadowDebugStats;
21
22#[cfg(test)]
23mod hidden_tests;
24
25pub use self::types::{
26    AtlasViewerCorner, BorderMode, CameraFrame, ClipObject, ClipShape, ComputeFilterItem,
27    ComputeFilterKind, CylindricalFacing, DebugOutputMode, DebugQuantity, DebugVis, DecalAnimation,
28    DecalBlendMode, DecalItem, DecalProjection, EffectsFrame, EnvironmentMap, FilterMode,
29    FrameData, GaussianSplatData, GaussianSplatId, GaussianSplatItem, GlyphItem, GlyphType,
30    GroundPlane, GroundPlaneMode, ImageAnchor, ImageSliceItem, InteractionFrame, LabelAnchor,
31    LabelItem, LicOverlay, LightKind, LightSource, LightingSettings, LineCap, LoadingBarAnchor,
32    LoadingBarItem, MeshInstanceItem, OverlayAnimation, OverlayFill, OverlayFrame, OverlayImageItem, OverlayRectItem,
33    OverlayShape, OverlayShapeItem, OverlayTextureId, PickId, PointCloudItem, PointRenderMode,
34    PointCloudRefItem, GlyphSetRefItem, TensorGlyphSetRefItem,
35    PolylineItem, PolylineRefItem, PostProcessSettings, RenderCamera, RibbonItem, RibbonRefItem,
36    RulerItem, ScalarBarAnchor, ScalarBarItem, ScalarBarOrientation, ScatterQuality,
37    ScatterSettings, ScatterVolumeItem, SceneEffects, SceneFrame, SceneRenderItem, ScreenImageItem,
38    ShDegree, ShadowFilter, SliceAxis, SpriteBlend, SpriteInstanceSetRefItem, SpriteItem,
39    SpriteSetRefItem, SpriteSizeMode, StreamtubeItem, StreamtubeRefItem, SurfaceLICConfig,
40    SurfaceSubmission, TensorGlyphItem, ToneMapping,
41    TransparentVolumeMeshItem, TriangleDirection, TubeItem, TubeRefItem,
42    ViewportEffects, ViewportFrame, VolumeItem, VolumeMeshItem, VolumeSurfaceSliceItem,
43    aabb_wireframe_polyline, sphere_wireframe_polyline,
44};
45
46/// An opaque handle to a per-viewport GPU state slot.
47///
48/// Obtained from [`ViewportRenderer::create_viewport`] and passed to
49/// [`ViewportRenderer::prepare_viewport`], [`ViewportRenderer::paint_viewport`],
50/// and [`ViewportRenderer::render_viewport`].
51///
52/// The slot index is managed internally. To bind a `ViewportId` to a camera frame,
53/// use [`CameraFrame::with_viewport_id`]. Single-viewport applications that use
54/// the legacy [`ViewportRenderer::prepare`] / [`ViewportRenderer::paint`] API do
55/// not need this type.
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct ViewportId(pub(crate) usize);
58
59use self::shadows::{compute_cascade_matrix, compute_cascade_splits};
60use self::types::{INSTANCING_THRESHOLD, InstancedBatch};
61use crate::resources::{
62    BatchMeta, CLIP_VOLUME_MAX, CameraUniform, ClipPlanesUniform, ClipVolumeEntry,
63    ClipVolumesUniform, GridUniform, InstanceAabb, InstanceData, LightsUniform, ObjectUniform,
64    OutlineEdgeUniform, OutlineObjectBuffers, OutlineUniform, PickInstance, ShadowAtlasUniform,
65    SingleLightUniform, SplatOutlineMaskUniform, ViewportGpuResources,
66};
67
68/// Per-viewport GPU state: uniform buffers and bind groups that differ per viewport.
69///
70/// Each viewport slot owns its own camera, clip planes, clip volume, shadow info,
71/// and grid buffers, plus the bind groups that reference them. Scene-global
72/// resources (lights, shadow atlas texture, IBL) are shared via the bind group
73/// pointing to buffers on `ViewportGpuResources`.
74pub(crate) struct ViewportSlot {
75    pub camera_buf: wgpu::Buffer,
76    pub clip_planes_buf: wgpu::Buffer,
77    pub clip_volume_buf: wgpu::Buffer,
78    pub shadow_info_buf: wgpu::Buffer,
79    pub grid_buf: wgpu::Buffer,
80    /// Camera bind group (group 0) referencing this slot's per-viewport buffers
81    /// plus shared scene-global resources.
82    pub camera_bind_group: wgpu::BindGroup,
83    /// Grid bind group (group 0 for grid pipeline) referencing this slot's grid buffer.
84    pub grid_bind_group: wgpu::BindGroup,
85    /// Per-viewport HDR post-process render targets.
86    ///
87    /// Created lazily on first HDR render call and resized when viewport dimensions change.
88    pub hdr: Option<crate::resources::ViewportHdrState>,
89    /// Per-fragment debug storage buffer (group 0 binding 12). Allocated at
90    /// `width * height * 16` bytes when debug_vis is active; None otherwise.
91    pub debug_frag_buf: Option<wgpu::Buffer>,
92    /// Viewport dimensions for which `debug_frag_buf` was allocated.
93    pub debug_frag_dims: (u32, u32),
94
95    // --- Per-viewport interaction state ---
96    /// Per-frame outline buffers for selected objects, rebuilt in prepare().
97    pub outline_object_buffers: Vec<OutlineObjectBuffers>,
98    /// Per-frame outline buffers for selected Gaussian splat sets, rebuilt in prepare().
99    pub splat_outline_buffers: Vec<crate::resources::SplatOutlineBuffers>,
100    /// Indices into `volume_gpu_data` for selected volumes, rebuilt in prepare().
101    pub volume_outline_indices: Vec<usize>,
102    /// Indices into `glyph_gpu_data` for selected glyph sets, rebuilt in prepare().
103    /// Each entry is (gpu_data_index, instance_filter): None draws all instances,
104    /// Some(indices) draws only those specific instance indices.
105    pub glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
106    /// Indices into `tensor_glyph_gpu_data` for selected tensor glyph sets, rebuilt in prepare().
107    pub tensor_glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
108    /// Indices into `sprite_gpu_data` for selected sprite sets, rebuilt in prepare().
109    pub sprite_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
110    /// Per-frame inline quad outline buffers for selected image slices, rebuilt in prepare().
111    pub raw_geom_outline_buffers: Vec<crate::resources::RawGeomOutlineBuffers>,
112    /// Per-frame NDC rect outline buffers for selected screen images, rebuilt in prepare().
113    pub screen_rect_outline_buffers: Vec<crate::resources::ScreenRectOutlineBuffers>,
114    /// Indices into `implicit_gpu_data` for selected GPU implicit items, rebuilt in prepare().
115    pub implicit_outline_indices: Vec<usize>,
116    /// Per-frame outline data for selected GPU marching cubes jobs, rebuilt in prepare().
117    pub mc_outline_data: Vec<crate::resources::gpu_marching_cubes::McOutlineItem>,
118    /// Outline items for selected streamtubes (index into streamtube_gpu_data + mask bind group).
119    pub streamtube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
120    /// Outline items for selected tubes.
121    pub tube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
122    /// Outline items for selected ribbons.
123    pub ribbon_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
124    /// Indices into polyline_gpu_data for selected user polylines.
125    pub polyline_outline_indices: Vec<usize>,
126    /// Per-frame x-ray buffers for selected objects, rebuilt in prepare().
127    pub xray_object_buffers: Vec<(
128        crate::resources::mesh_store::MeshId,
129        wgpu::Buffer,
130        wgpu::BindGroup,
131    )>,
132    /// Per-frame constraint guide line buffers, rebuilt in prepare().
133    pub constraint_line_buffers: Vec<(
134        wgpu::Buffer,
135        wgpu::Buffer,
136        u32,
137        wgpu::Buffer,
138        wgpu::BindGroup,
139    )>,
140    /// Per-frame cap geometry buffers (section view cross-section fill), rebuilt in prepare().
141    pub cap_buffers: Vec<(
142        wgpu::Buffer,
143        wgpu::Buffer,
144        u32,
145        wgpu::Buffer,
146        wgpu::BindGroup,
147    )>,
148    /// Per-frame clip plane fill overlay buffers, rebuilt in prepare().
149    pub clip_plane_fill_buffers: Vec<(
150        wgpu::Buffer,
151        wgpu::Buffer,
152        u32,
153        wgpu::Buffer,
154        wgpu::BindGroup,
155    )>,
156    /// Per-frame clip plane line overlay buffers, rebuilt in prepare().
157    pub clip_plane_line_buffers: Vec<(
158        wgpu::Buffer,
159        wgpu::Buffer,
160        u32,
161        wgpu::Buffer,
162        wgpu::BindGroup,
163    )>,
164    /// Vertex buffer for axes indicator geometry (rebuilt each frame).
165    pub axes_vertex_buffer: wgpu::Buffer,
166    /// Number of vertices in the axes indicator buffer.
167    pub axes_vertex_count: u32,
168    /// Gizmo model-matrix uniform buffer.
169    pub gizmo_uniform_buf: wgpu::Buffer,
170    /// Gizmo bind group (group 1: model matrix uniform).
171    pub gizmo_bind_group: wgpu::BindGroup,
172    /// Gizmo vertex buffer.
173    pub gizmo_vertex_buffer: wgpu::Buffer,
174    /// Gizmo index buffer.
175    pub gizmo_index_buffer: wgpu::Buffer,
176    /// Number of indices in the current gizmo mesh.
177    pub gizmo_index_count: u32,
178
179    // --- Sub-object highlight (per-viewport, generation-cached) ---
180    /// Per-viewport dynamic resolution intermediate render target.
181    /// `None` when render_scale == 1.0 or not yet initialised.
182    pub dyn_res: Option<crate::resources::dyn_res::DynResTarget>,
183    /// Per-viewport intermediate render target for the HDR eframe callback path.
184    /// `None` until the first `prepare_hdr_callback` call for this viewport.
185    pub hdr_callback: Option<crate::resources::dyn_res::HdrCallbackTarget>,
186    /// Cached GPU data for sub-object highlight rendering.
187    /// `None` when no sub-object selection is active and no volumes are selected.
188    pub sub_highlight: Option<crate::resources::SubHighlightGpuData>,
189    /// Version of the last sub-selection snapshot that was uploaded.
190    /// `u64::MAX` forces a rebuild on the first frame.
191    pub sub_highlight_generation: u64,
192}
193
194/// Retained pick state for one GPU implicit surface, built during `prepare()`.
195struct GpuImplicitPickItem {
196    id: u64,
197    primitives: Vec<crate::resources::ImplicitPrimitive>,
198    blend_mode: crate::resources::ImplicitBlendMode,
199    max_steps: u32,
200    step_scale: f32,
201    hit_threshold: f32,
202    max_distance: f32,
203}
204
205/// Retained pick state for one GPU marching cubes job, built during `prepare()`.
206struct GpuMcPickItem {
207    id: u64,
208    isovalue: f32,
209    volume_data: std::sync::Arc<crate::geometry::marching_cubes::VolumeData>,
210}
211
212/// Renderer wrapping all GPU resources and providing `prepare()` and `paint()` methods.
213pub struct ViewportRenderer {
214    resources: ViewportGpuResources,
215    /// Instanced batches prepared for the current frame. Empty when using per-object path.
216    instanced_batches: Vec<InstancedBatch>,
217    /// Whether the current frame uses the instanced draw path.
218    use_instancing: bool,
219    /// True when the device supports `INDIRECT_FIRST_INSTANCE`.
220    gpu_culling_supported: bool,
221    /// True when GPU-driven culling is active (supported and not disabled by the caller).
222    gpu_culling_enabled: bool,
223    /// GPU culling compute pipelines and frustum buffer. Created lazily on the first
224    /// frame where `gpu_culling_enabled` is true and instance buffers are present.
225    cull_resources: Option<indirect::CullResources>,
226    /// Registered item-type plugins keyed by
227    /// [`ItemTypePlugin::type_name`](crate::plugin_api::ItemTypePlugin::type_name).
228    /// `init_gpu` is invoked once on registration; per-frame `prepare` and
229    /// `paint` fire when a matching collection is on `SceneFrame`.
230    item_type_plugins:
231        std::collections::HashMap<&'static str, Box<dyn crate::plugin_api::ItemTypePlugin>>,
232    /// Monotonic frame counter passed to plugin contexts.
233    plugin_frame_index: u64,
234    /// Performance counters from the last frame.
235    last_stats: crate::renderer::stats::FrameStats,
236    /// Last scene generation seen during prepare(). u64::MAX forces rebuild on first frame.
237    last_scene_generation: u64,
238    /// Last selection generation seen during prepare(). u64::MAX forces rebuild on first frame.
239    last_selection_generation: u64,
240    /// Last scene_items count seen during prepare(). usize::MAX forces rebuild on first frame.
241    /// Included in cache key so that frustum-culling changes (different visible set, different
242    /// count) correctly invalidate the instance buffer even when scene_generation is stable.
243    last_scene_items_count: usize,
244    /// Count of items that passed the instanced-path filter on the last rebuild.
245    /// Used in place of has_per_frame_mutations so scenes that mix instanced and
246    /// non-instanced items (e.g. one two-sided mesh + 10k static boxes) still hit
247    /// the instanced batch cache on frames where the filtered set is unchanged.
248    last_instancable_count: usize,
249    /// Total instance count from the last rebuild. Used as a fast length check
250    /// in `structure_preserved` and as `instance_count` for GPU cull dispatches.
251    cached_instance_count: usize,
252    /// Per-batch content hash from the last rebuild, indexed by batch position.
253    /// A hash mismatch triggers a `write_buffer` for that batch; a match skips it.
254    cached_instance_hashes: Vec<u64>,
255    /// Cached instanced batch descriptors from last rebuild.
256    cached_instanced_batches: Vec<InstancedBatch>,
257    /// When true, the next cache-miss forces a full buffer upload instead of the
258    /// per-batch partial-upload path. Set by `force_dirty()` and consumed once.
259    force_full_upload: bool,
260    /// Per-frame point cloud GPU data, rebuilt in prepare(), consumed in paint().
261    point_cloud_gpu_data: Vec<crate::resources::PointCloudGpuData>,
262    /// Per-frame glyph GPU data, rebuilt in prepare(), consumed in paint().
263    glyph_gpu_data: Vec<crate::resources::GlyphGpuData>,
264    /// Per-frame tensor glyph GPU data, rebuilt in prepare(), consumed in paint().
265    tensor_glyph_gpu_data: Vec<crate::resources::TensorGlyphGpuData>,
266    /// Per-frame polyline GPU data, rebuilt in prepare(), consumed in paint().
267    polyline_gpu_data: Vec<crate::resources::PolylineGpuData>,
268    /// Per-frame volume GPU data, rebuilt in prepare(), consumed in paint().
269    volume_gpu_data: Vec<crate::resources::VolumeGpuData>,
270    /// Per-frame streamtube GPU data, rebuilt in prepare(), consumed in paint().
271    streamtube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
272    /// Per-frame general tube GPU data, rebuilt in prepare(), consumed in paint().
273    tube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
274    /// Per-frame ribbon GPU data, rebuilt in prepare(), consumed in paint().
275    ribbon_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
276    /// Indices into streamtube_gpu_data for selected streamtubes (set in prepare_scene, consumed in prepare_viewport).
277    streamtube_selected_gpu_indices: Vec<usize>,
278    /// Indices into tube_gpu_data for selected tubes (set in prepare_scene, consumed in prepare_viewport).
279    tube_selected_gpu_indices: Vec<usize>,
280    /// Indices into ribbon_gpu_data for selected ribbons (set in prepare_scene, consumed in prepare_viewport).
281    ribbon_selected_gpu_indices: Vec<usize>,
282    /// Indices into polyline_gpu_data for selected user polylines (set in prepare_scene, consumed in prepare_viewport).
283    polyline_selected_gpu_indices: Vec<usize>,
284    /// Per-frame image slice GPU data, rebuilt in prepare(), consumed in paint().
285    image_slice_gpu_data: Vec<crate::resources::ImageSliceGpuData>,
286    /// Per-frame volume surface slice GPU data, rebuilt in prepare(), consumed in paint().
287    volume_surface_slice_gpu_data: Vec<crate::resources::VolumeSurfaceSliceGpuData>,
288    /// Per-frame Surface LIC GPU data, rebuilt in prepare(), consumed in paint().
289    lic_gpu_data: Vec<crate::resources::LicSurfaceGpuData>,
290    /// Per-frame GPU implicit surface data, rebuilt in prepare(), consumed in paint().
291    implicit_gpu_data: Vec<crate::resources::implicit::ImplicitGpuItem>,
292    /// Per-frame decal GPU data, rebuilt in prepare(), consumed in paint() (D1).
293    decal_gpu_data: Vec<crate::resources::decal::DecalGpuItem>,
294    /// Per-frame decal exclude GPU data, rebuilt in prepare(), consumed in paint() (D5).
295    decal_exclude_items: Vec<crate::resources::decal::DecalExcludeGpuItem>,
296    /// Per-frame GPU marching cubes render data, rebuilt in prepare(), consumed in paint().
297    mc_gpu_data: Vec<crate::resources::gpu_marching_cubes::McFrameData>,
298    /// Per-frame sprite GPU data, rebuilt in prepare(), consumed in paint().
299    sprite_gpu_data: Vec<crate::resources::SpriteGpuData>,
300    /// Per-frame mesh-instance batches, rebuilt in prepare(), consumed in paint().
301    mesh_instance_gpu_data: Vec<crate::resources::MeshInstanceGpuData>,
302    /// Per-frame Gaussian splat draw data, rebuilt in prepare_viewport_internal(), consumed in paint().
303    gaussian_splat_draw_data: Vec<crate::resources::GaussianSplatDrawData>,
304    /// Per-frame screen-image GPU data, rebuilt in prepare(), consumed in paint().
305    screen_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
306    /// Per-frame overlay image GPU data, rebuilt in prepare(), consumed in paint().
307    overlay_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
308    /// Per-frame overlay label GPU data, rebuilt in prepare(), consumed in paint().
309    label_gpu_data: Option<crate::resources::LabelGpuData>,
310    /// Per-frame scalar bar GPU data, rebuilt in prepare(), consumed in paint().
311    scalar_bar_gpu_data: Option<crate::resources::LabelGpuData>,
312    /// Per-frame ruler GPU data, rebuilt in prepare(), consumed in paint().
313    ruler_gpu_data: Option<crate::resources::LabelGpuData>,
314    /// Per-frame loading bar GPU data, rebuilt in prepare(), consumed in paint().
315    loading_bar_gpu_data: Option<crate::resources::LabelGpuData>,
316    /// Per-frame overlay rect GPU data, rebuilt in prepare(), consumed in paint().
317    overlay_rect_gpu_data: Option<crate::resources::LabelGpuData>,
318    /// Per-frame SDF overlay shape GPU data, rebuilt in prepare(), consumed in paint().
319    overlay_shape_gpu_data: Option<crate::resources::OverlayShapeGpuData>,
320    /// Cached GPU textures for the backdrop blur effect (frosted glass).
321    /// Recreated when the viewport size changes.
322    backdrop_blur_state: Option<crate::resources::BackdropBlurState>,
323    /// Per-viewport GPU state slots.
324    ///
325    /// Indexed by `FrameData::camera.viewport_index`. Each slot owns independent
326    /// uniform buffers and bind groups for camera, clip planes, clip volume,
327    /// shadow info, and grid. Slots are grown lazily in `prepare` via
328    /// `ensure_viewport_slot`. There are at most 4 in the current UI.
329    viewport_slots: Vec<ViewportSlot>,
330    /// GPU compute filter results from the last `prepare()` call.
331    ///
332    /// Each entry contains a compacted index buffer + count for one filtered mesh.
333    /// Consumed during `paint()` to override the mesh's default index buffer.
334    /// Cleared and rebuilt each frame.
335    compute_filter_results: Vec<crate::resources::ComputeFilterResult>,
336    /// Per-item uniform buffers for wireframe mode. In wireframe mode multiple scene
337    /// items can share the same MeshId, but each needs its own object uniform (model
338    /// matrix, colour, etc.). The mesh's single `object_uniform_buf` gets overwritten
339    /// by the last item prepared, so we maintain a separate pool here. Indexed in the
340    /// same order as the visible scene items. Grown lazily, never shrunk.
341    wireframe_uniform_bufs: Vec<wgpu::Buffer>,
342    /// Bind groups corresponding to `wireframe_uniform_bufs`. Each bind group pairs
343    /// the per-item uniform buffer with the mesh's fallback textures so it is
344    /// compatible with the object bind group layout.
345    wireframe_bind_groups: Vec<wgpu::BindGroup>,
346    /// Per-scene-item uniform buffers for the per-object draw path. Multiple scene
347    /// items can share the same MeshId, but each needs its own object uniform
348    /// (model matrix, colour, etc.). The mesh's single `object_uniform_buf` is
349    /// stomped by the last item prepared, so we maintain a parallel pool indexed
350    /// by position in `scene_items`. Grown lazily, never shrunk.
351    per_item_object_uniform_bufs: Vec<wgpu::Buffer>,
352    /// Bind groups corresponding to `per_item_object_uniform_bufs`. Each pairs the
353    /// per-item uniform buffer with the mesh's real textures, LUT, matcap, scalar
354    /// buffer, etc. -- the same resources that `mesh.object_bind_group` references,
355    /// just with binding 0 swapped for the per-item uniform.
356    per_item_object_bind_groups: Vec<Option<wgpu::BindGroup>>,
357    /// Cache keys for `per_item_object_bind_groups`. When the key matches we only
358    /// write the uniform; otherwise we rebuild the bind group.
359    per_item_object_cache_keys: Vec<u64>,
360    /// Per-frame list of boundary mesh IDs to draw in wireframe for
361    /// TransparentVolumeMeshItems with `appearance.wireframe = true`.
362    /// Cleared and rebuilt each frame in prepare_scene_internal.
363    tvm_wireframe_draws: Vec<crate::resources::mesh_store::MeshId>,
364    /// Shared uniform buffer for TVM boundary wireframe draws (wireframe=1, model=identity).
365    /// Created once on first use, reused every frame.
366    tvm_wireframe_buf: Option<wgpu::Buffer>,
367    /// Bind group for TVM boundary wireframe draws. Pairs `tvm_wireframe_buf` with
368    /// fallback textures matching the object bind group layout.
369    tvm_wireframe_bg: Option<wgpu::BindGroup>,
370    /// Cascade-0 light-space view-projection matrix from the last shadow prepare.
371    /// Cached here so `prepare_viewport_internal` can copy it into the ground plane uniform.
372    last_cascade0_shadow_mat: glam::Mat4,
373    /// Current runtime mode controlling internal default behavior.
374    runtime_mode: crate::renderer::stats::RuntimeMode,
375    /// Optional cap on how much main-thread time `prepare` is allowed to
376    /// spend running apply closures for completed upload jobs.
377    ///
378    /// `None` means unbounded (apply work runs to completion in one
379    /// frame). `Some(d)` spreads the cost across frames so heavy
380    /// completions do not produce one fat frame; the deferred applies
381    /// run on the next call to `prepare`.
382    upload_budget: Option<std::time::Duration>,
383    /// Active performance policy: target FPS, render scale bounds, and permitted reductions.
384    performance_policy: crate::renderer::stats::PerformancePolicy,
385    /// Current render scale tracked by the adaptation controller (or set manually).
386    ///
387    /// Clamped to `[policy.min_render_scale, policy.max_render_scale]`.
388    /// Reported in `FrameStats::render_scale` each frame.
389    current_render_scale: f32,
390    /// Instant the renderer was constructed. Used as the t=0 reference for
391    /// per-frame animated effects (e.g. `ScatterVolume::noise` time scrolling).
392    start_instant: std::time::Instant,
393    /// Instant recorded at the start of the most recent `prepare()` call.
394    /// Used to compute `total_frame_ms` on the following frame.
395    last_prepare_instant: Option<std::time::Instant>,
396    /// Frame counter incremented each `prepare()` call. Used for picking throttle in Playback mode.
397    frame_counter: u64,
398    /// Surface items from the last `prepare()` call, retained for `pick()` dispatch.
399    pick_scene_items: Vec<SceneRenderItem>,
400    /// Point cloud items from the last `prepare()` call, retained for `pick()` dispatch.
401    pick_point_cloud_items: Vec<PointCloudItem>,
402    /// Gaussian splat items from the last `prepare()` call, retained for `pick()` dispatch.
403    pick_splat_items: Vec<GaussianSplatItem>,
404    /// Volume items from the last `prepare()` call, retained for `pick()` dispatch.
405    pick_volume_items: Vec<VolumeItem>,
406    /// Transparent volume mesh items from the last `prepare()` call, retained for `pick()` dispatch.
407    pick_tvm_items: Vec<TransparentVolumeMeshItem>,
408    /// Scatter volume items from the last `prepare()` call, retained for `pick()` dispatch.
409    pick_scatter_volume_items: Vec<crate::renderer::types::ScatterVolumeItem>,
410    /// Volumes packed into the GPU storage buffer this frame
411    /// (volume, density_multiplier, flag bits). Stored so `render_viewport`
412    /// can re-upload as needed without re-walking the scene frame.
413    pub(crate) prepared_scatter_volumes:
414        Vec<(crate::scene::scatter_volume::ScatterVolume, f32, u32)>,
415    /// Subset of the prepared scatter volumes that carry `RefractionParams`.
416    /// Cleared and refilled each frame by `prepare_viewport`. The refraction
417    /// pass walks this list; an empty list skips the pass entirely.
418    pub(crate) prepared_refraction_volumes:
419        Vec<(crate::scene::scatter_volume::ScatterVolume, f32)>,
420    /// Per-viewport scatter intermediates and temporal history. Indexed by
421    /// `vp_idx`. Grown lazily inside the scatter pass; each entry is
422    /// reallocated when the requested scatter target size or downsample mode
423    /// changes.
424    pub(crate) scatter_viewport_states: Vec<Option<crate::resources::ScatterViewportState>>,
425    /// Opaque volume mesh items from the last `prepare()` call, retained for cell-level `pick()` dispatch.
426    pick_volume_mesh_items: Vec<VolumeMeshItem>,
427    /// Polyline items from the last `prepare()` call, retained for `pick()` dispatch.
428    pick_polyline_items: Vec<PolylineItem>,
429    /// Glyph items from the last `prepare()` call, retained for `pick()` dispatch.
430    pick_glyph_items: Vec<GlyphItem>,
431    /// Tensor glyph items from the last `prepare()` call, retained for `pick()` dispatch.
432    pick_tensor_glyph_items: Vec<TensorGlyphItem>,
433    /// Sprite items from the last `prepare()` call, retained for `pick()` dispatch.
434    pick_sprite_items: Vec<SpriteItem>,
435    /// Streamtube items from the last `prepare()` call, retained for `pick()` dispatch.
436    pick_streamtube_items: Vec<StreamtubeItem>,
437    /// Tube items from the last `prepare()` call, retained for `pick()` dispatch.
438    pick_tube_items: Vec<TubeItem>,
439    /// Ribbon items from the last `prepare()` call, retained for `pick()` dispatch.
440    pick_ribbon_items: Vec<RibbonItem>,
441    /// Image slice items from the last `prepare()` call, retained for `pick()` dispatch.
442    pick_image_slice_items: Vec<ImageSliceItem>,
443    /// Volume surface slice items from the last `prepare()` call, retained for `pick()` dispatch.
444    pick_volume_surface_slice_items: Vec<VolumeSurfaceSliceItem>,
445    /// Screen image items from the last `prepare()` call, retained for `pick()` dispatch.
446    pick_screen_image_items: Vec<ScreenImageItem>,
447    /// GPU implicit surface items from the last `prepare()` call, retained for `pick()` dispatch.
448    pick_implicit_items: Vec<GpuImplicitPickItem>,
449    /// GPU marching cubes jobs from the last `prepare()` call, retained for `pick()` dispatch.
450    pick_mc_items: Vec<GpuMcPickItem>,
451
452    // --- GPU timestamp queries ---
453    /// Timestamp query set with 2 entries (scene-pass begin + end).
454    /// `None` when `TIMESTAMP_QUERY` is unavailable or not yet initialized.
455    ts_query_set: Option<wgpu::QuerySet>,
456    /// Resolve buffer: 2 × u64, GPU-only (`QUERY_RESOLVE | COPY_SRC`).
457    ts_resolve_buf: Option<wgpu::Buffer>,
458    /// Staging buffer: 2 × u64, CPU-readable (`COPY_DST | MAP_READ`).
459    ts_staging_buf: Option<wgpu::Buffer>,
460    /// Nanoseconds per GPU timestamp tick, from `queue.get_timestamp_period()`.
461    ts_period: f32,
462    /// Whether the staging buffer holds unread timestamp data from the previous frame.
463    ts_needs_readback: bool,
464
465    // --- Indirect-args readback (GPU-driven culling visible instance count) ---
466    /// CPU-readable staging buffer for `indirect_args_buf` (batch_count × 20 bytes).
467    /// Grown lazily; never shrunk.
468    indirect_readback_buf: Option<wgpu::Buffer>,
469    /// Number of batches whose data was copied into `indirect_readback_buf` last frame.
470    indirect_readback_batch_count: u32,
471    /// True when `indirect_readback_buf` holds unread data from the previous cull pass.
472    indirect_readback_pending: bool,
473
474    // --- Per-pass degradation state ---
475    /// Tiered degradation ladder position (0 = none, 1 = shadows, 2 = volumes, 3 = effects).
476    /// Advanced one step per over-budget frame once render scale hits minimum;
477    /// reversed one step per comfortably-under-budget frame.
478    degradation_tier: u8,
479    /// Whether the shadow pass was skipped this frame due to budget pressure.
480    /// Computed once per frame at the top of prepare() and used by both
481    /// prepare_scene_internal and reported in FrameStats.
482    degradation_shadows_skipped: bool,
483    /// Whether volume raymarch step size was doubled this frame due to budget pressure.
484    degradation_volume_quality_reduced: bool,
485    /// Whether SSAO, contact shadows, and bloom were skipped this frame.
486    /// Set in prepare(); read by the render path.
487    degradation_effects_throttled: bool,
488
489    // --- D8: shadow debug stats cache ---
490    /// Cascade count from the last prepare_scene_internal call.
491    last_cascade_count: u32,
492    /// Cascade split distances from the last prepare_scene_internal call.
493    last_cascade_splits: [f32; 4],
494    /// Shadow frustum half-extent from the last prepare_scene_internal call.
495    last_shadow_extent: f32,
496    /// Shadow atlas resolution from the last prepare_scene_internal call.
497    last_shadow_atlas_resolution: u32,
498    /// Contact shadow enabled state from the last prepare_scene_internal call.
499    last_contact_shadow_active: bool,
500    /// Cascade splits from the last tracing log emission. Sentinel [f32::MAX; 4] forces
501    /// a log on the first frame.
502    last_logged_cascade_splits: [f32; 4],
503    /// Lights dropped by the CPU frustum cull on the most recent frame.
504    /// Surfaced through the cluster debug overlay when enabled.
505    pub(crate) last_frustum_culled_lights: u32,
506    /// Most recent cluster build readback. Populated when a frame's
507    /// `ViewportFrame::cluster_stats_request` was true.
508    pub(crate) last_cluster_stats: Option<crate::resources::clustered::ClusterStats>,
509}
510
511impl ViewportRenderer {
512    /// Create a new renderer with default settings (no MSAA).
513    /// Call once at application startup.
514    pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
515        Self::with_sample_count(device, target_format, 1)
516    }
517
518    /// Create a new renderer with the specified MSAA sample count (1, 2, or 4).
519    ///
520    /// When using MSAA (sample_count > 1), the caller must create multisampled
521    /// colour and depth textures and use them as render pass attachments with the
522    /// final surface texture as the resolve target.
523    pub fn with_sample_count(
524        device: &wgpu::Device,
525        target_format: wgpu::TextureFormat,
526        sample_count: u32,
527    ) -> Self {
528        let gpu_culling_supported = device
529            .features()
530            .contains(wgpu::Features::INDIRECT_FIRST_INSTANCE);
531        Self {
532            resources: ViewportGpuResources::new(device, target_format, sample_count),
533            instanced_batches: Vec::new(),
534            use_instancing: false,
535            gpu_culling_supported,
536            gpu_culling_enabled: gpu_culling_supported,
537            cull_resources: None,
538            item_type_plugins: std::collections::HashMap::new(),
539            plugin_frame_index: 0,
540            last_stats: crate::renderer::stats::FrameStats::default(),
541            last_scene_generation: u64::MAX,
542            last_selection_generation: u64::MAX,
543            last_scene_items_count: usize::MAX,
544            last_instancable_count: usize::MAX,
545            cached_instance_count: 0,
546            cached_instance_hashes: Vec::new(),
547            cached_instanced_batches: Vec::new(),
548            force_full_upload: false,
549            point_cloud_gpu_data: Vec::new(),
550            glyph_gpu_data: Vec::new(),
551            tensor_glyph_gpu_data: Vec::new(),
552            polyline_gpu_data: Vec::new(),
553            volume_gpu_data: Vec::new(),
554            streamtube_gpu_data: Vec::new(),
555            tube_gpu_data: Vec::new(),
556            ribbon_gpu_data: Vec::new(),
557            streamtube_selected_gpu_indices: Vec::new(),
558            tube_selected_gpu_indices: Vec::new(),
559            ribbon_selected_gpu_indices: Vec::new(),
560            polyline_selected_gpu_indices: Vec::new(),
561            image_slice_gpu_data: Vec::new(),
562            volume_surface_slice_gpu_data: Vec::new(),
563            sprite_gpu_data: Vec::new(),
564            mesh_instance_gpu_data: Vec::new(),
565            gaussian_splat_draw_data: Vec::new(),
566            lic_gpu_data: Vec::new(),
567            implicit_gpu_data: Vec::new(),
568            decal_gpu_data: Vec::new(),
569            decal_exclude_items: Vec::new(),
570            mc_gpu_data: Vec::new(),
571            screen_image_gpu_data: Vec::new(),
572            overlay_image_gpu_data: Vec::new(),
573            label_gpu_data: None,
574            scalar_bar_gpu_data: None,
575            ruler_gpu_data: None,
576            loading_bar_gpu_data: None,
577            overlay_rect_gpu_data: None,
578            overlay_shape_gpu_data: None,
579            backdrop_blur_state: None,
580            viewport_slots: Vec::new(),
581            compute_filter_results: Vec::new(),
582            wireframe_uniform_bufs: Vec::new(),
583            wireframe_bind_groups: Vec::new(),
584            per_item_object_uniform_bufs: Vec::new(),
585            per_item_object_bind_groups: Vec::new(),
586            per_item_object_cache_keys: Vec::new(),
587            tvm_wireframe_draws: Vec::new(),
588            tvm_wireframe_buf: None,
589            tvm_wireframe_bg: None,
590            last_cascade0_shadow_mat: glam::Mat4::IDENTITY,
591            runtime_mode: crate::renderer::stats::RuntimeMode::Interactive,
592            performance_policy: crate::renderer::stats::PerformancePolicy::default(),
593            upload_budget: None,
594            current_render_scale: 1.0,
595            start_instant: std::time::Instant::now(),
596            last_prepare_instant: None,
597            frame_counter: 0,
598            pick_scene_items: Vec::new(),
599            pick_point_cloud_items: Vec::new(),
600            pick_splat_items: Vec::new(),
601            pick_volume_items: Vec::new(),
602            pick_tvm_items: Vec::new(),
603            pick_scatter_volume_items: Vec::new(),
604            prepared_scatter_volumes: Vec::new(),
605            prepared_refraction_volumes: Vec::new(),
606            scatter_viewport_states: Vec::new(),
607            pick_volume_mesh_items: Vec::new(),
608            pick_polyline_items: Vec::new(),
609            pick_glyph_items: Vec::new(),
610            pick_tensor_glyph_items: Vec::new(),
611            pick_sprite_items: Vec::new(),
612            pick_streamtube_items: Vec::new(),
613            pick_tube_items: Vec::new(),
614            pick_ribbon_items: Vec::new(),
615            pick_image_slice_items: Vec::new(),
616            pick_volume_surface_slice_items: Vec::new(),
617            pick_screen_image_items: Vec::new(),
618            pick_implicit_items: Vec::new(),
619            pick_mc_items: Vec::new(),
620            ts_query_set: None,
621            ts_resolve_buf: None,
622            ts_staging_buf: None,
623            ts_period: 1.0,
624            ts_needs_readback: false,
625            indirect_readback_buf: None,
626            indirect_readback_batch_count: 0,
627            indirect_readback_pending: false,
628            degradation_tier: 0,
629            degradation_shadows_skipped: false,
630            degradation_volume_quality_reduced: false,
631            degradation_effects_throttled: false,
632            last_cascade_count: 0,
633            last_cascade_splits: [0.0; 4],
634            last_shadow_extent: 20.0,
635            last_shadow_atlas_resolution: 4096,
636            last_contact_shadow_active: false,
637            last_logged_cascade_splits: [f32::MAX; 4],
638            last_frustum_culled_lights: 0,
639            last_cluster_stats: None,
640        }
641    }
642
643    /// Access the underlying GPU resources (e.g. for mesh uploads).
644    pub fn resources(&self) -> &ViewportGpuResources {
645        &self.resources
646    }
647
648    /// Performance counters from the last completed frame.
649    pub fn last_frame_stats(&self) -> crate::renderer::stats::FrameStats {
650        self.last_stats
651    }
652
653    /// Diagnostics from the cluster build pass on the most recent frame that
654    /// requested them (`ViewportFrame::cluster_stats_request`). Returns
655    /// `None` until a request has been served.
656    pub fn cluster_stats(&self) -> Option<crate::resources::clustered::ClusterStats> {
657        self.last_cluster_stats
658    }
659
660    /// Disable GPU-driven culling, reverting to the direct draw path.
661    ///
662    /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`
663    /// (culling is already disabled on those devices).
664    pub fn disable_gpu_driven_culling(&mut self) {
665        self.gpu_culling_enabled = false;
666    }
667
668    /// Force a full instance buffer upload on the next frame.
669    ///
670    /// Normally the renderer skips GPU writes for instanced batches whose data
671    /// has not changed since the last upload. Call this when you have mutated
672    /// batch-relevant state through a path the renderer cannot observe (for
673    /// example, directly modifying GPU buffer contents or scene items after
674    /// `collect_render_items` runs). The flag is consumed once and resets
675    /// automatically after the next `prepare` call.
676    pub fn force_dirty(&mut self) {
677        self.force_full_upload = true;
678        // Also invalidate the generation cache so the next prepare is guaranteed
679        // to enter the rebuild path even if the scene generation is unchanged.
680        self.last_scene_generation = u64::MAX;
681    }
682
683    /// Re-enable GPU-driven culling after a call to `disable_gpu_driven_culling`.
684    ///
685    /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`.
686    pub fn enable_gpu_driven_culling(&mut self) {
687        if self.gpu_culling_supported {
688            self.gpu_culling_enabled = true;
689        }
690    }
691
692    /// Cap the per-frame cost of running upload-job apply closures.
693    ///
694    /// `None` is the default and matches the historical behaviour:
695    /// `prepare` drains every completed upload's apply step in one
696    /// shot. `Some(d)` switches `prepare` over to
697    /// `process_uploads_with_budget` so applies that overflow the
698    /// budget spill to the next frame. Useful when a stress load lands
699    /// many heavy completions on the same frame and the bunched apply
700    /// work shows up as one fat frame at the end of the load.
701    pub fn set_upload_budget(&mut self, budget: Option<std::time::Duration>) {
702        self.upload_budget = budget;
703    }
704
705    /// Currently configured upload budget. See `set_upload_budget`.
706    pub fn upload_budget(&self) -> Option<std::time::Duration> {
707        self.upload_budget
708    }
709
710    /// Set the runtime mode controlling internal default behavior.
711    ///
712    /// - [`RuntimeMode::Interactive`]: full picking rate, full quality (default).
713    /// - [`RuntimeMode::Playback`]: picking throttled to reduce CPU overhead during animation.
714    /// - [`RuntimeMode::Paused`]: full picking rate, full quality.
715    /// - [`RuntimeMode::Capture`]: full quality, intended for screenshot/export workflows.
716    pub fn set_runtime_mode(&mut self, mode: crate::renderer::stats::RuntimeMode) {
717        self.runtime_mode = mode;
718    }
719
720    /// Return the current runtime mode.
721    pub fn runtime_mode(&self) -> crate::renderer::stats::RuntimeMode {
722        self.runtime_mode
723    }
724
725    /// Set the performance policy controlling target FPS, render scale bounds,
726    /// and permitted quality reductions.
727    ///
728    /// The internal adaptation controller activates when
729    /// `policy.allow_dynamic_resolution` is `true` and `policy.target_fps` is
730    /// `Some`. It adjusts `render_scale` within `[min_render_scale,
731    /// max_render_scale]` each frame based on `total_frame_ms`.
732    pub fn set_performance_policy(&mut self, policy: crate::renderer::stats::PerformancePolicy) {
733        self.performance_policy = policy;
734        // Clamp current scale into the new bounds immediately.
735        self.current_render_scale = self
736            .current_render_scale
737            .clamp(policy.min_render_scale, policy.max_render_scale);
738    }
739
740    /// Return the active performance policy.
741    pub fn performance_policy(&self) -> crate::renderer::stats::PerformancePolicy {
742        self.performance_policy
743    }
744
745    /// Manually set the render scale.
746    ///
747    /// Effective when `performance_policy.allow_dynamic_resolution` is `false`.
748    /// When dynamic resolution is enabled the adaptation controller overrides
749    /// this value each frame.
750    ///
751    /// The value is clamped to `[policy.min_render_scale, policy.max_render_scale]`.
752    ///
753    /// Works on both the LDR and HDR render paths. On the HDR path, the scene,
754    /// bloom, SSAO, tone-map, and FXAA all run at the scaled resolution; the
755    /// result is upscale-blitted to native resolution before overlays and grid.
756    pub fn set_render_scale(&mut self, scale: f32) {
757        self.current_render_scale = scale.clamp(
758            self.performance_policy.min_render_scale,
759            self.performance_policy.max_render_scale,
760        );
761    }
762
763    /// Set the target frame rate used to compute [`FrameStats::missed_budget`].
764    ///
765    /// Convenience wrapper that updates `performance_policy.target_fps`.
766    pub fn set_target_fps(&mut self, fps: Option<f32>) {
767        self.performance_policy.target_fps = fps;
768    }
769
770    /// Mutable access to the underlying GPU resources (e.g. for mesh uploads).
771    pub fn resources_mut(&mut self) -> &mut ViewportGpuResources {
772        &mut self.resources
773    }
774
775    /// Returns true when the current frame is rendered via the instanced draw path.
776    ///
777    /// When true, edits to mesh.wgsl shadow sampling code have no effect - the active
778    /// shader is mesh_instanced.wgsl. Check this before testing shader changes.
779    pub fn is_using_instanced_path(&self) -> bool {
780        self.use_instancing
781    }
782
783    /// Returns the number of instanced batches prepared for the current frame.
784    ///
785    /// Zero when using the non-instanced path. Each batch corresponds to a distinct
786    /// (MeshId, material) combination in the scene.
787    pub fn instanced_batch_count(&self) -> usize {
788        self.instanced_batches.len()
789    }
790
791    /// Run the GPU-driven cull compute against a plugin's
792    /// [`CullSubmission`](crate::plugin_api::CullSubmission).
793    ///
794    /// Encodes two compute passes into `encoder`:
795    /// 1. one thread per instance, tests AABB against `frustum`, claims a
796    ///    visibility slot via atomic add;
797    /// 2. one thread per batch, writes a `DrawIndexedIndirect` entry into
798    ///    `sub.indirect_out` with the final visible count and zeroes the
799    ///    counter for the next call.
800    ///
801    /// After the encoder runs, draw each batch with
802    /// `pass.draw_indexed_indirect(sub.indirect_out, batch_idx * 20)` using
803    /// `sub.visible_out` as the per-instance lookup buffer.
804    ///
805    /// The cull pipeline is created lazily on the first call. Returns
806    /// without dispatching if the device does not support
807    /// `INDIRECT_FIRST_INSTANCE` (call
808    /// [`is_gpu_culling_supported`](Self::is_gpu_culling_supported) first).
809    pub fn submit_cull(
810        &mut self,
811        device: &wgpu::Device,
812        queue: &wgpu::Queue,
813        encoder: &mut wgpu::CommandEncoder,
814        frustum: &crate::camera::frustum::Frustum,
815        sub: &crate::plugin_api::CullSubmission<'_>,
816    ) {
817        if !self.gpu_culling_supported {
818            return;
819        }
820        if self.cull_resources.is_none() {
821            self.cull_resources = Some(crate::renderer::indirect::CullResources::new(device));
822        }
823        let cull = self.cull_resources.as_ref().unwrap();
824        cull.dispatch(encoder, device, queue, frustum, None, sub);
825    }
826
827    /// Same as [`submit_cull`](Self::submit_cull) for one shadow cascade.
828    ///
829    /// Uploads the frustum to the cascade slot (so a single frame can submit
830    /// the main pass plus every cascade without overwriting an in-flight
831    /// upload) and forces the cull shader's shadow flag so
832    /// `InstanceAabb::cast_shadows = 0` entries are skipped.
833    ///
834    /// `cascade_idx` must be in `0..4`; values outside that range panic in
835    /// debug builds and clamp to 3 in release.
836    pub fn submit_cull_shadow(
837        &mut self,
838        device: &wgpu::Device,
839        queue: &wgpu::Queue,
840        encoder: &mut wgpu::CommandEncoder,
841        cascade_idx: usize,
842        cascade_frustum: &crate::camera::frustum::Frustum,
843        sub: &crate::plugin_api::CullSubmission<'_>,
844    ) {
845        if !self.gpu_culling_supported {
846            return;
847        }
848        debug_assert!(cascade_idx < 4, "cascade_idx must be in 0..4");
849        let cascade_idx = cascade_idx.min(3);
850        if self.cull_resources.is_none() {
851            self.cull_resources = Some(crate::renderer::indirect::CullResources::new(device));
852        }
853        let cull = self.cull_resources.as_ref().unwrap();
854        cull.dispatch(encoder, device, queue, cascade_frustum, Some(cascade_idx), sub);
855    }
856
857    /// Convenience wrapper around [`submit_cull`](Self::submit_cull) for the
858    /// common case of one mesh with N instances.
859    ///
860    /// The renderer fills its scratch [`BatchMeta`] slot from `draw`, zeroes
861    /// its scratch counter, seeds the indirect entry, and runs a one-batch
862    /// cull. Plugins that only have a single mesh per submission don't have
863    /// to allocate either buffer themselves.
864    ///
865    /// `indirect_out` must hold one `DrawIndexedIndirect` entry (20 bytes).
866    pub fn submit_cull_single_mesh(
867        &mut self,
868        device: &wgpu::Device,
869        queue: &wgpu::Queue,
870        encoder: &mut wgpu::CommandEncoder,
871        frustum: &crate::camera::frustum::Frustum,
872        instance_aabbs: &wgpu::Buffer,
873        instance_count: u32,
874        visible_out: &wgpu::Buffer,
875        indirect_out: &wgpu::Buffer,
876        draw: crate::plugin_api::SingleMeshDraw,
877        shadow_pass: bool,
878    ) {
879        self.dispatch_cull_single_mesh(
880            device,
881            queue,
882            encoder,
883            frustum,
884            None,
885            instance_aabbs,
886            instance_count,
887            visible_out,
888            indirect_out,
889            draw,
890            shadow_pass,
891        );
892    }
893
894    /// Single-mesh shadow variant of
895    /// [`submit_cull_single_mesh`](Self::submit_cull_single_mesh).
896    pub fn submit_cull_shadow_single_mesh(
897        &mut self,
898        device: &wgpu::Device,
899        queue: &wgpu::Queue,
900        encoder: &mut wgpu::CommandEncoder,
901        cascade_idx: usize,
902        cascade_frustum: &crate::camera::frustum::Frustum,
903        instance_aabbs: &wgpu::Buffer,
904        instance_count: u32,
905        visible_out: &wgpu::Buffer,
906        indirect_out: &wgpu::Buffer,
907        draw: crate::plugin_api::SingleMeshDraw,
908    ) {
909        debug_assert!(cascade_idx < 4, "cascade_idx must be in 0..4");
910        let cascade_idx = cascade_idx.min(3);
911        self.dispatch_cull_single_mesh(
912            device,
913            queue,
914            encoder,
915            cascade_frustum,
916            Some(cascade_idx),
917            instance_aabbs,
918            instance_count,
919            visible_out,
920            indirect_out,
921            draw,
922            true,
923        );
924    }
925
926    #[allow(clippy::too_many_arguments)]
927    fn dispatch_cull_single_mesh(
928        &mut self,
929        device: &wgpu::Device,
930        queue: &wgpu::Queue,
931        encoder: &mut wgpu::CommandEncoder,
932        frustum: &crate::camera::frustum::Frustum,
933        cascade: Option<usize>,
934        instance_aabbs: &wgpu::Buffer,
935        instance_count: u32,
936        visible_out: &wgpu::Buffer,
937        indirect_out: &wgpu::Buffer,
938        draw: crate::plugin_api::SingleMeshDraw,
939        shadow_pass: bool,
940    ) {
941        if !self.gpu_culling_supported {
942            return;
943        }
944        if self.cull_resources.is_none() {
945            self.cull_resources = Some(crate::renderer::indirect::CullResources::new(device));
946        }
947        let cull = self.cull_resources.as_ref().unwrap();
948        let (meta_buf, counter_buf) = cull.scratch_single_mesh_buffers();
949        let meta = crate::plugin_api::BatchMeta {
950            index_count: draw.index_count,
951            first_index: draw.first_index,
952            instance_offset: 0,
953            instance_count,
954            vis_offset: 0,
955            is_transparent: 0,
956            _pad: [0, 0],
957        };
958        queue.write_buffer(meta_buf, 0, bytemuck::bytes_of(&meta));
959        queue.write_buffer(counter_buf, 0, &[0u8; 4]);
960        // Seed the static fields of the indirect entry; the compute pass
961        // overwrites `instance_count` with the final visible count.
962        let seed: [u32; 5] = [
963            draw.index_count,
964            0,
965            draw.first_index,
966            draw.base_vertex as u32,
967            draw.first_instance,
968        ];
969        queue.write_buffer(indirect_out, 0, bytemuck::cast_slice(&seed));
970
971        let sub = crate::plugin_api::CullSubmission {
972            instance_aabbs,
973            instance_count,
974            batch_meta: meta_buf,
975            batch_count: 1,
976            counter: counter_buf,
977            visible_out,
978            indirect_out,
979            shadow_pass,
980        };
981        cull.dispatch(encoder, device, queue, frustum, cascade, &sub);
982    }
983
984    /// Register an [`ItemTypePlugin`](crate::plugin_api::ItemTypePlugin).
985    ///
986    /// Invokes the plugin's `init_gpu` against the current device and
987    /// shared bind layout, then stores it keyed by `type_name()` for the
988    /// remainder of the renderer's lifetime. Registering a second plugin
989    /// with the same `type_name` replaces the first.
990    ///
991    /// The renderer will dispatch `prepare` and `paint` to the plugin on
992    /// every frame where
993    /// [`SceneFrame::submit_plugin_items`](crate::renderer::SceneFrame::submit_plugin_items)
994    /// has populated a collection under the same name.
995    pub fn with_item_type_plugin(
996        &mut self,
997        device: &wgpu::Device,
998        mut plugin: Box<dyn crate::plugin_api::ItemTypePlugin>,
999    ) {
1000        let shared = self.resources.shared_bindings();
1001        plugin.init_gpu(device, &shared);
1002        let name = plugin.type_name();
1003        self.item_type_plugins.insert(name, plugin);
1004    }
1005
1006    /// Returns true when an item-type plugin with `type_name` is
1007    /// registered.
1008    pub fn has_item_type_plugin(&self, type_name: &str) -> bool {
1009        self.item_type_plugins.contains_key(type_name)
1010    }
1011
1012    /// Walk registered item-type plugins, invoke `prepare` for each one
1013    /// that has a matching collection submitted on `frame.scene`, and
1014    /// return the concatenated command buffers.
1015    ///
1016    /// Called internally from the lib's prepare paths; not part of the
1017    /// consumer-facing API.
1018    pub(crate) fn dispatch_plugin_prepare(
1019        &mut self,
1020        device: &wgpu::Device,
1021        queue: &wgpu::Queue,
1022        frame: &FrameData,
1023    ) -> Vec<wgpu::CommandBuffer> {
1024        if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1025            return Vec::new();
1026        }
1027        self.plugin_frame_index = self.plugin_frame_index.wrapping_add(1);
1028        let mut bufs: Vec<wgpu::CommandBuffer> = Vec::new();
1029        for (name, plugin) in self.item_type_plugins.iter_mut() {
1030            if let Some(items) = frame.scene.plugin_items.get(*name) {
1031                // Constructed per plugin because `Jobs` borrows `&resources`
1032                // and the borrow only needs to live for this iteration.
1033                let ctx = crate::plugin_api::ItemFrameContext {
1034                    camera: &frame.camera.render_camera,
1035                    viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1036                    viewport_index: frame.camera.viewport_index,
1037                    frame_index: self.plugin_frame_index,
1038                    jobs: crate::resources::Jobs::new(&self.resources),
1039                };
1040                bufs.extend(plugin.prepare(device, queue, &ctx, items.as_ref()));
1041            }
1042        }
1043        bufs
1044    }
1045
1046    /// Walk registered item-type plugins and invoke `paint` for each one
1047    /// that has a matching collection submitted on `frame.scene`.
1048    ///
1049    /// Called from inside the lib's HDR scene pass between built-in
1050    /// opaques and the skybox.
1051    pub(crate) fn dispatch_plugin_paint<'rp>(
1052        &'rp self,
1053        pass: &mut wgpu::RenderPass<'rp>,
1054        frame: &'rp FrameData,
1055    ) {
1056        if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1057            return;
1058        }
1059        let ctx = crate::plugin_api::PaintContext {
1060            camera: &frame.camera.render_camera,
1061            viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1062            viewport_index: frame.camera.viewport_index,
1063            frame_index: self.plugin_frame_index,
1064        };
1065        for (name, plugin) in self.item_type_plugins.iter() {
1066            if let Some(items) = frame.scene.plugin_items.get(*name) {
1067                plugin.paint(pass, &ctx, items.as_ref());
1068            }
1069        }
1070    }
1071
1072    /// Walk registered plugins and invoke `paint_transparent` for each
1073    /// one whose collection is on `frame.scene`.
1074    ///
1075    /// Called from inside the lib's OIT render pass, after built-in
1076    /// transparent draws.
1077    pub(crate) fn dispatch_plugin_paint_transparent<'rp>(
1078        &'rp self,
1079        pass: &mut wgpu::RenderPass<'rp>,
1080        frame: &'rp FrameData,
1081    ) {
1082        if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1083            return;
1084        }
1085        let ctx = crate::plugin_api::PaintContext {
1086            camera: &frame.camera.render_camera,
1087            viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1088            viewport_index: frame.camera.viewport_index,
1089            frame_index: self.plugin_frame_index,
1090        };
1091        for (name, plugin) in self.item_type_plugins.iter() {
1092            if let Some(items) = frame.scene.plugin_items.get(*name) {
1093                plugin.paint_transparent(pass, &ctx, items.as_ref());
1094            }
1095        }
1096    }
1097
1098    /// Walk registered plugins and invoke `cast_shadow_pass` for the
1099    /// given cascade.
1100    ///
1101    /// Currently unused: the shadow-pass call site inlines the plugin
1102    /// dispatch because the surrounding scope holds a mutable borrow of
1103    /// `self.resources` that blocks a normal `&self` method call. Kept
1104    /// alongside the other dispatchers as the natural shape; a future
1105    /// refactor that splits the resources borrow can switch back.
1106    #[allow(dead_code)]
1107    pub(crate) fn dispatch_plugin_shadow<'rp>(
1108        &'rp self,
1109        pass: &mut wgpu::RenderPass<'rp>,
1110        frame: &'rp FrameData,
1111        cascade_idx: u32,
1112        light_view_proj: glam::Mat4,
1113    ) {
1114        if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1115            return;
1116        }
1117        let ctx = crate::plugin_api::ShadowCastContext {
1118            cascade_idx,
1119            light_view_proj,
1120            camera: &frame.camera.render_camera,
1121            viewport_index: frame.camera.viewport_index,
1122            frame_index: self.plugin_frame_index,
1123        };
1124        for (name, plugin) in self.item_type_plugins.iter() {
1125            if let Some(items) = frame.scene.plugin_items.get(*name) {
1126                plugin.cast_shadow_pass(pass, &ctx, items.as_ref());
1127            }
1128        }
1129    }
1130
1131    /// Walk registered plugins and invoke `cull` for each one whose
1132    /// collection is on `frame.scene`.
1133    ///
1134    /// Called from the lib's prepare path once the camera frustum for
1135    /// the frame is known.
1136    pub(crate) fn dispatch_plugin_cull(
1137        &mut self,
1138        frustum: &crate::camera::frustum::Frustum,
1139        frame: &FrameData,
1140    ) {
1141        if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1142            return;
1143        }
1144        for (name, plugin) in self.item_type_plugins.iter_mut() {
1145            if let Some(items) = frame.scene.plugin_items.get(*name) {
1146                let ctx = crate::plugin_api::ItemFrameContext {
1147                    camera: &frame.camera.render_camera,
1148                    viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1149                    viewport_index: frame.camera.viewport_index,
1150                    frame_index: self.plugin_frame_index,
1151                    jobs: crate::resources::Jobs::new(&self.resources),
1152                };
1153                plugin.cull(frustum, &ctx, items.as_ref());
1154            }
1155        }
1156    }
1157
1158    /// Walk registered item-type plugins and invoke `outline_mask` for
1159    /// each one whose collection is on `frame.scene`.
1160    ///
1161    /// Called from inside the lib's outline-mask render pass.
1162    pub(crate) fn dispatch_plugin_outline_mask<'rp>(
1163        &'rp self,
1164        pass: &mut wgpu::RenderPass<'rp>,
1165        frame: &'rp FrameData,
1166    ) {
1167        if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1168            return;
1169        }
1170        let ctx = crate::plugin_api::OutlineMaskContext {
1171            camera: &frame.camera.render_camera,
1172            viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1173            viewport_index: frame.camera.viewport_index,
1174            frame_index: self.plugin_frame_index,
1175        };
1176        for (name, plugin) in self.item_type_plugins.iter() {
1177            if let Some(items) = frame.scene.plugin_items.get(*name) {
1178                plugin.outline_mask(pass, &ctx, items.as_ref());
1179            }
1180        }
1181    }
1182
1183    /// True when the device supports the features GPU-driven culling needs.
1184    ///
1185    /// Plugins should gate `submit_cull` calls on this. If false, the lib
1186    /// silently no-ops the submission and the plugin must fall back to
1187    /// direct draws.
1188    pub fn is_gpu_culling_supported(&self) -> bool {
1189        self.gpu_culling_supported
1190    }
1191
1192    /// Returns per-frame shadow and lighting pipeline statistics for debug inspection.
1193    ///
1194    /// All fields reflect the most recently completed `prepare` call (one frame
1195    /// behind the display). Returns default values before the first `prepare` call.
1196    pub fn shadow_debug_stats(&self) -> ShadowDebugStats {
1197        ShadowDebugStats {
1198            using_instanced_path: self.use_instancing,
1199            instanced_batch_count: self.instanced_batches.len(),
1200            cascade_count: self.last_cascade_count,
1201            cascade_splits: self.last_cascade_splits,
1202            shadow_atlas_resolution: self.last_shadow_atlas_resolution,
1203            shadow_extent_world: self.last_shadow_extent,
1204            contact_shadow_active: self.last_contact_shadow_active,
1205        }
1206    }
1207
1208    /// Read the debug values at a specific pixel from the per-fragment storage buffer.
1209    ///
1210    /// Returns `None` when debug_vis is inactive (no buffer allocated) or when `(x, y)`
1211    /// is outside the viewport. The four channels correspond to the current R/G/B channel
1212    /// selectors plus 1.0 for alpha.
1213    ///
1214    /// This submits a GPU-to-CPU copy and waits synchronously. Only call from outside
1215    /// a render pass (e.g., in the next frame's prepare step), not inside paint callbacks.
1216    ///
1217    /// The returned values are from the previous rendered frame.
1218    pub fn read_debug_pixel(
1219        &self,
1220        device: &wgpu::Device,
1221        queue: &wgpu::Queue,
1222        x: u32,
1223        y: u32,
1224    ) -> Option<[f32; 4]> {
1225        // Use the primary viewport slot (index 0).
1226        let slot = self.viewport_slots.first()?;
1227        let buf = slot.debug_frag_buf.as_ref()?;
1228        let (vw, vh) = slot.debug_frag_dims;
1229        if x >= vw || y >= vh {
1230            return None;
1231        }
1232        let byte_offset = ((y as u64) * (vw as u64) + (x as u64)) * 16;
1233        let staging = device.create_buffer(&wgpu::BufferDescriptor {
1234            label: None,
1235            size: 16,
1236            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1237            mapped_at_creation: false,
1238        });
1239        let mut encoder =
1240            device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
1241        encoder.copy_buffer_to_buffer(buf, byte_offset, &staging, 0, 16);
1242        queue.submit(Some(encoder.finish()));
1243        let slice = staging.slice(..);
1244        let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
1245        slice.map_async(wgpu::MapMode::Read, move |r| {
1246            let _ = tx.send(r);
1247        });
1248        let _ = device.poll(wgpu::PollType::Wait {
1249            submission_index: None,
1250            timeout: Some(std::time::Duration::from_secs(5)),
1251        });
1252        rx.recv().ok()?.ok()?;
1253        let data = slice.get_mapped_range();
1254        Some(bytemuck::pod_read_unaligned::<[f32; 4]>(&data))
1255    }
1256
1257    /// Upload a Gaussian splat set to the GPU.
1258    ///
1259    /// Call once per splat set at startup or when it changes. The returned
1260    /// [`GaussianSplatId`] is valid until [`remove_gaussian_splats`](Self::remove_gaussian_splats) is called.
1261    ///
1262    /// # Errors
1263    ///
1264    /// Returns [`ViewportError::InvalidGaussianSplatData`](crate::error::ViewportError::InvalidGaussianSplatData)
1265    /// if `data.positions` is empty or if `positions`, `scales`, `rotations`, and `opacities`
1266    /// differ in length.
1267    ///
1268    /// # Examples
1269    ///
1270    /// ```no_run
1271    /// # use viewport_lib::error::ViewportError;
1272    /// # use viewport_lib::renderer::{GaussianSplatData, ViewportRenderer};
1273    /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
1274    /// let result = renderer.upload_gaussian_splats(device, queue, &GaussianSplatData::default());
1275    /// assert!(matches!(result, Err(ViewportError::InvalidGaussianSplatData { .. })));
1276    /// # }
1277    /// ```
1278    pub fn upload_gaussian_splats(
1279        &mut self,
1280        device: &wgpu::Device,
1281        queue: &wgpu::Queue,
1282        data: &GaussianSplatData,
1283    ) -> crate::error::ViewportResult<GaussianSplatId> {
1284        self.resources.upload_gaussian_splats(device, queue, data)
1285    }
1286
1287    /// Remove an uploaded Gaussian splat set by handle.
1288    ///
1289    /// After this call the `id` is invalid and must not be submitted in `SceneFrame`.
1290    pub fn remove_gaussian_splats(&mut self, id: GaussianSplatId) {
1291        self.resources.remove_gaussian_splats(id);
1292    }
1293
1294    /// Upload an equirectangular HDR environment map and precompute IBL textures.
1295    ///
1296    /// `pixels` is row-major RGBA f32 data (4 floats per texel), `width`x`height`.
1297    /// This rebuilds camera bind groups so shaders immediately see the new textures.
1298    ///
1299    /// # Errors
1300    ///
1301    /// Returns [`ViewportError::InvalidTextureData`](crate::error::ViewportError::InvalidTextureData)
1302    /// if `pixels.len()` does not equal `width * height * 4`.
1303    ///
1304    /// # Examples
1305    ///
1306    /// ```no_run
1307    /// # use viewport_lib::error::ViewportError;
1308    /// # use viewport_lib::renderer::ViewportRenderer;
1309    /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
1310    /// // 2x2 RGBA image requires exactly 16 floats.
1311    /// let result = renderer.upload_environment_map(device, queue, &[0.0f32; 12], 2, 2);
1312    /// assert!(matches!(result, Err(ViewportError::InvalidTextureData { expected: 16, actual: 12 })));
1313    /// # }
1314    /// ```
1315    pub fn upload_environment_map(
1316        &mut self,
1317        device: &wgpu::Device,
1318        queue: &wgpu::Queue,
1319        pixels: &[f32],
1320        width: u32,
1321        height: u32,
1322    ) -> crate::error::ViewportResult<()> {
1323        crate::resources::environment::upload_environment_map(
1324            &mut self.resources,
1325            device,
1326            queue,
1327            pixels,
1328            width,
1329            height,
1330        )?;
1331        self.rebuild_camera_bind_groups(device);
1332        Ok(())
1333    }
1334
1335    /// Current state of an in-flight upload job.
1336    pub fn upload_status(
1337        &self,
1338        id: crate::resources::JobId,
1339    ) -> crate::resources::UploadStatus {
1340        self.resources.upload_status(id)
1341    }
1342
1343    /// Count of upload jobs still in flight.
1344    pub fn uploads_pending(&self) -> usize {
1345        self.resources.uploads_pending()
1346    }
1347
1348    /// Wall-clock work duration recorded for an async upload job. See
1349    /// [`ViewportGpuResources::job_duration`].
1350    pub fn job_duration(
1351        &self,
1352        id: crate::resources::JobId,
1353    ) -> Option<std::time::Duration> {
1354        self.resources.job_duration(id)
1355    }
1356
1357    /// Drop the recorded duration for `id` after reading it. See
1358    /// [`ViewportGpuResources::drop_job_duration`].
1359    pub fn drop_job_duration(&mut self, id: crate::resources::JobId) {
1360        self.resources.drop_job_duration(id);
1361    }
1362
1363    /// Start an asynchronous 3D volume texture upload. See
1364    /// [`ViewportGpuResources::begin_upload_volume`].
1365    pub fn begin_upload_volume(
1366        &mut self,
1367        device: &wgpu::Device,
1368        queue: &wgpu::Queue,
1369        data: Vec<f32>,
1370        dims: [u32; 3],
1371    ) -> crate::error::ViewportResult<crate::resources::JobId> {
1372        self.resources.begin_upload_volume(device, queue, data, dims)
1373    }
1374
1375    /// Take the volume id produced by a completed
1376    /// [`begin_upload_volume`](Self::begin_upload_volume) job.
1377    pub fn upload_result_volume(
1378        &mut self,
1379        id: crate::resources::JobId,
1380    ) -> crate::error::ViewportResult<crate::resources::VolumeId> {
1381        self.resources.upload_result_volume(id)
1382    }
1383
1384    /// Start an asynchronous marching-cubes-ready volume upload. See
1385    /// [`ViewportGpuResources::begin_upload_volume_for_mc`].
1386    pub fn begin_upload_volume_for_mc(
1387        &mut self,
1388        device: &wgpu::Device,
1389        queue: &wgpu::Queue,
1390        vol: crate::geometry::marching_cubes::VolumeData,
1391    ) -> crate::resources::JobId {
1392        self.resources.begin_upload_volume_for_mc(device, queue, vol)
1393    }
1394
1395    /// Take the [`VolumeGpuId`](crate::resources::VolumeGpuId) produced by a
1396    /// completed [`begin_upload_volume_for_mc`](Self::begin_upload_volume_for_mc) job.
1397    pub fn upload_result_volume_mc(
1398        &mut self,
1399        id: crate::resources::JobId,
1400    ) -> crate::error::ViewportResult<crate::resources::VolumeGpuId> {
1401        self.resources.upload_result_volume_mc(id)
1402    }
1403
1404    /// Start an asynchronous volume mesh upload. See
1405    /// [`ViewportGpuResources::begin_upload_volume_mesh_data`].
1406    pub fn begin_upload_volume_mesh_data(
1407        &mut self,
1408        device: &wgpu::Device,
1409        data: crate::resources::volume_mesh::VolumeMeshData,
1410    ) -> crate::resources::JobId {
1411        self.resources.begin_upload_volume_mesh_data(device, data)
1412    }
1413
1414    /// Take the `(MeshId, face_to_cell)` pair produced by a completed
1415    /// [`begin_upload_volume_mesh_data`](Self::begin_upload_volume_mesh_data) job.
1416    pub fn upload_result_volume_mesh(
1417        &mut self,
1418        id: crate::resources::JobId,
1419    ) -> crate::error::ViewportResult<(crate::resources::mesh_store::MeshId, Vec<u32>)> {
1420        self.resources.upload_result_volume_mesh(id)
1421    }
1422
1423    /// Start an asynchronous clipped volume mesh upload. See
1424    /// [`ViewportGpuResources::begin_upload_clipped_volume_mesh_data`].
1425    pub fn begin_upload_clipped_volume_mesh_data(
1426        &mut self,
1427        device: &wgpu::Device,
1428        data: crate::resources::volume_mesh::VolumeMeshData,
1429        clip_planes: Vec<[f32; 4]>,
1430    ) -> crate::resources::JobId {
1431        self.resources
1432            .begin_upload_clipped_volume_mesh_data(device, data, clip_planes)
1433    }
1434
1435    /// Take the `(MeshId, face_to_cell)` pair produced by a completed
1436    /// [`begin_upload_clipped_volume_mesh_data`](Self::begin_upload_clipped_volume_mesh_data)
1437    /// job.
1438    pub fn upload_result_clipped_volume_mesh(
1439        &mut self,
1440        id: crate::resources::JobId,
1441    ) -> crate::error::ViewportResult<(crate::resources::mesh_store::MeshId, Vec<u32>)> {
1442        self.resources.upload_result_clipped_volume_mesh(id)
1443    }
1444
1445    /// Start an asynchronous sparse voxel grid upload. See
1446    /// [`ViewportGpuResources::begin_upload_sparse_volume_grid_data`].
1447    pub fn begin_upload_sparse_volume_grid_data(
1448        &mut self,
1449        device: &wgpu::Device,
1450        data: crate::resources::SparseVolumeGridData,
1451    ) -> crate::resources::JobId {
1452        self.resources
1453            .begin_upload_sparse_volume_grid_data(device, data)
1454    }
1455
1456    /// Take the [`MeshId`](crate::resources::mesh_store::MeshId) produced by a completed
1457    /// [`begin_upload_sparse_volume_grid_data`](Self::begin_upload_sparse_volume_grid_data)
1458    /// job.
1459    pub fn upload_result_sparse_volume_grid(
1460        &mut self,
1461        id: crate::resources::JobId,
1462    ) -> crate::error::ViewportResult<crate::resources::mesh_store::MeshId> {
1463        self.resources.upload_result_sparse_volume_grid(id)
1464    }
1465
1466    /// Start an asynchronous projected-tet mesh upload. See
1467    /// [`ViewportGpuResources::begin_upload_projected_tet_mesh`].
1468    pub fn begin_upload_projected_tet_mesh(
1469        &mut self,
1470        device: &wgpu::Device,
1471        data: crate::resources::volume_mesh::VolumeMeshData,
1472        scalar_attribute: String,
1473        colourmap_id: crate::resources::ColourmapId,
1474    ) -> crate::resources::JobId {
1475        self.resources
1476            .begin_upload_projected_tet_mesh(device, data, scalar_attribute, colourmap_id)
1477    }
1478
1479    /// Take the `(ProjectedTetId, scalar_min, scalar_max)` triple produced by a
1480    /// completed [`begin_upload_projected_tet_mesh`](Self::begin_upload_projected_tet_mesh) job.
1481    pub fn upload_result_projected_tet_mesh(
1482        &mut self,
1483        id: crate::resources::JobId,
1484    ) -> crate::error::ViewportResult<(crate::resources::ProjectedTetId, f32, f32)> {
1485        self.resources.upload_result_projected_tet_mesh(id)
1486    }
1487
1488    /// Start an asynchronous Gaussian splat upload. See
1489    /// [`ViewportGpuResources::begin_upload_gaussian_splats`].
1490    pub fn begin_upload_gaussian_splats(
1491        &mut self,
1492        device: &wgpu::Device,
1493        queue: &wgpu::Queue,
1494        data: crate::renderer::GaussianSplatData,
1495    ) -> crate::error::ViewportResult<crate::resources::JobId> {
1496        self.resources
1497            .begin_upload_gaussian_splats(device, queue, data)
1498    }
1499
1500    /// Take the [`GaussianSplatId`](crate::renderer::GaussianSplatId) produced by a
1501    /// completed [`begin_upload_gaussian_splats`](Self::begin_upload_gaussian_splats) job.
1502    pub fn upload_result_gaussian_splats(
1503        &mut self,
1504        id: crate::resources::JobId,
1505    ) -> crate::error::ViewportResult<crate::renderer::GaussianSplatId> {
1506        self.resources.upload_result_gaussian_splats(id)
1507    }
1508
1509    /// Start an asynchronous overlay texture upload. See
1510    /// [`ViewportGpuResources::begin_upload_overlay_texture`].
1511    pub fn begin_upload_overlay_texture(
1512        &mut self,
1513        device: &wgpu::Device,
1514        queue: &wgpu::Queue,
1515        width: u32,
1516        height: u32,
1517        rgba_data: Vec<u8>,
1518    ) -> crate::error::ViewportResult<crate::resources::JobId> {
1519        self.resources
1520            .begin_upload_overlay_texture(device, queue, width, height, rgba_data)
1521    }
1522
1523    /// Take the [`OverlayTextureId`](crate::renderer::OverlayTextureId) produced by a
1524    /// completed [`begin_upload_overlay_texture`](Self::begin_upload_overlay_texture) job.
1525    pub fn upload_result_overlay_texture(
1526        &mut self,
1527        id: crate::resources::JobId,
1528    ) -> crate::error::ViewportResult<crate::renderer::OverlayTextureId> {
1529        self.resources.upload_result_overlay_texture(id)
1530    }
1531
1532    /// True when no upload jobs are in flight.
1533    pub fn all_uploads_complete(&self) -> bool {
1534        self.resources.all_uploads_complete()
1535    }
1536
1537    /// Register a callback to fire when an upload job finishes. See
1538    /// [`ViewportGpuResources::on_upload_complete`] for the semantics.
1539    pub fn on_upload_complete<F>(&mut self, id: crate::resources::JobId, cb: F)
1540    where
1541        F: FnOnce(&crate::resources::UploadStatus) + Send + 'static,
1542    {
1543        self.resources.on_upload_complete(id, cb);
1544    }
1545
1546    /// Start an asynchronous albedo texture upload. See
1547    /// [`ViewportGpuResources::begin_upload_texture`] for the semantics.
1548    pub fn begin_upload_texture(
1549        &mut self,
1550        device: &wgpu::Device,
1551        queue: &wgpu::Queue,
1552        width: u32,
1553        height: u32,
1554        rgba: Vec<u8>,
1555    ) -> crate::error::ViewportResult<crate::resources::JobId> {
1556        self.resources
1557            .begin_upload_texture(device, queue, width, height, rgba)
1558    }
1559
1560    /// Start an asynchronous normal-map upload. See
1561    /// [`ViewportGpuResources::begin_upload_normal_map`] for the semantics.
1562    pub fn begin_upload_normal_map(
1563        &mut self,
1564        device: &wgpu::Device,
1565        queue: &wgpu::Queue,
1566        width: u32,
1567        height: u32,
1568        rgba: Vec<u8>,
1569    ) -> crate::error::ViewportResult<crate::resources::JobId> {
1570        self.resources
1571            .begin_upload_normal_map(device, queue, width, height, rgba)
1572    }
1573
1574    /// Take the texture id from a completed async texture upload. See
1575    /// [`ViewportGpuResources::upload_result_texture`] for the error
1576    /// semantics.
1577    pub fn upload_result_texture(
1578        &mut self,
1579        id: crate::resources::JobId,
1580    ) -> crate::error::ViewportResult<u64> {
1581        self.resources.upload_result_texture(id)
1582    }
1583
1584    /// Start an asynchronous mesh upload.
1585    ///
1586    /// Returns a `JobId` immediately. The CPU prep (tangent computation,
1587    /// vertex repack, normal-line build) runs on a worker thread; GPU
1588    /// buffer creation and store insertion run on the main thread during
1589    /// the next `process_uploads` call after the worker finishes. Once the
1590    /// status is `Ready`, take the produced `MeshId` with
1591    /// `upload_result_mesh`.
1592    ///
1593    /// Ownership of `data` transfers into the worker; clone at the call
1594    /// site if you need to retain it.
1595    ///
1596    /// # Errors
1597    ///
1598    /// Same validation errors as `upload_mesh_data` (empty mesh, length
1599    /// mismatch, invalid vertex index), all reported before the job is
1600    /// submitted.
1601    pub fn begin_upload_mesh_data(
1602        &mut self,
1603        device: &wgpu::Device,
1604        data: crate::resources::MeshData,
1605    ) -> crate::error::ViewportResult<crate::resources::JobId> {
1606        self.resources.begin_upload_mesh_data(device, data)
1607    }
1608
1609    /// Take the `MeshId` produced by a completed `begin_upload_mesh_data`
1610    /// job. See [`ViewportGpuResources::upload_result_mesh`] for the error
1611    /// semantics.
1612    pub fn upload_result_mesh(
1613        &mut self,
1614        id: crate::resources::JobId,
1615    ) -> crate::error::ViewportResult<crate::resources::mesh_store::MeshId> {
1616        self.resources.upload_result_mesh(id)
1617    }
1618
1619    /// Start an asynchronous environment-map upload.
1620    ///
1621    /// Returns immediately with a `JobId`. The caller drives the upload-job
1622    /// runner from the renderer's prepare path each frame; once the job
1623    /// reports `Ready`, the IBL textures are live on the renderer and a
1624    /// subsequent call to `rebuild_camera_bind_groups` makes them visible
1625    /// to shaders.
1626    ///
1627    /// Ownership of `pixels` transfers into the background worker.
1628    ///
1629    /// # Errors
1630    ///
1631    /// Returns [`ViewportError::InvalidTextureData`](crate::error::ViewportError::InvalidTextureData)
1632    /// if `pixels.len() != width * height * 4`.
1633    pub fn begin_upload_environment_map(
1634        &mut self,
1635        device: &wgpu::Device,
1636        queue: &wgpu::Queue,
1637        pixels: Vec<f32>,
1638        width: u32,
1639        height: u32,
1640    ) -> crate::error::ViewportResult<crate::resources::JobId> {
1641        crate::resources::environment::begin_upload_environment_map(
1642            &mut self.resources,
1643            device,
1644            queue,
1645            pixels,
1646            width,
1647            height,
1648        )
1649    }
1650
1651    /// Rebuild the primary and per-viewport camera bind groups.
1652    ///
1653    /// Call after IBL textures are uploaded so the shaders see the new
1654    /// environment. The synchronous `upload_environment_map` does this
1655    /// internally; consumers driving the async path through
1656    /// `begin_upload_environment_map` should call this themselves once the
1657    /// matching job reports `Ready`.
1658    pub fn rebuild_camera_bind_groups(&mut self, device: &wgpu::Device) {
1659        self.resources.camera_bind_group = self.resources.create_camera_bind_group(
1660            device,
1661            &self.resources.camera_uniform_buf,
1662            &self.resources.clip_planes_uniform_buf,
1663            &self.resources.shadow_info_buf,
1664            &self.resources.clip_volume_uniform_buf,
1665            &self.resources.debug_frag_sentinel_buf,
1666            "camera_bind_group",
1667        );
1668
1669        for slot in &mut self.viewport_slots {
1670            let dbg_buf = slot
1671                .debug_frag_buf
1672                .as_ref()
1673                .unwrap_or(&self.resources.debug_frag_sentinel_buf);
1674            slot.camera_bind_group = self.resources.create_camera_bind_group(
1675                device,
1676                &slot.camera_buf,
1677                &slot.clip_planes_buf,
1678                &slot.shadow_info_buf,
1679                &slot.clip_volume_buf,
1680                dbg_buf,
1681                "per_viewport_camera_bg",
1682            );
1683        }
1684    }
1685
1686    /// Ensure a per-viewport slot exists for `viewport_index`.
1687    ///
1688    /// Creates a full `ViewportSlot` with independent uniform buffers for camera,
1689    /// clip planes, clip volume, shadow info, and grid. The camera bind group
1690    /// references this slot's per-viewport buffers plus shared scene-global
1691    /// resources. Slots are created lazily and never destroyed.
1692    fn ensure_viewport_slot(&mut self, device: &wgpu::Device, viewport_index: usize) {
1693        while self.viewport_slots.len() <= viewport_index {
1694            let camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
1695                label: Some("vp_camera_buf"),
1696                size: std::mem::size_of::<CameraUniform>() as u64,
1697                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1698                mapped_at_creation: false,
1699            });
1700            let clip_planes_buf = device.create_buffer(&wgpu::BufferDescriptor {
1701                label: Some("vp_clip_planes_buf"),
1702                size: std::mem::size_of::<ClipPlanesUniform>() as u64,
1703                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1704                mapped_at_creation: false,
1705            });
1706            let clip_volume_buf = device.create_buffer(&wgpu::BufferDescriptor {
1707                label: Some("vp_clip_volume_buf"),
1708                size: std::mem::size_of::<ClipVolumesUniform>() as u64,
1709                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1710                mapped_at_creation: false,
1711            });
1712            let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
1713                label: Some("vp_shadow_info_buf"),
1714                size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
1715                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1716                mapped_at_creation: false,
1717            });
1718            let grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
1719                label: Some("vp_grid_buf"),
1720                size: std::mem::size_of::<GridUniform>() as u64,
1721                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1722                mapped_at_creation: false,
1723            });
1724
1725            let camera_bind_group = self.resources.create_camera_bind_group(
1726                device,
1727                &camera_buf,
1728                &clip_planes_buf,
1729                &shadow_info_buf,
1730                &clip_volume_buf,
1731                &self.resources.debug_frag_sentinel_buf,
1732                "per_viewport_camera_bg",
1733            );
1734
1735            let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1736                label: Some("vp_grid_bind_group"),
1737                layout: &self.resources.grid_bind_group_layout,
1738                entries: &[wgpu::BindGroupEntry {
1739                    binding: 0,
1740                    resource: grid_buf.as_entire_binding(),
1741                }],
1742            });
1743
1744            // Per-viewport gizmo buffers (initial mesh: Translate, no hover, identity orientation).
1745            let (gizmo_verts, gizmo_indices) = crate::interaction::gizmo::build_gizmo_mesh(
1746                crate::interaction::gizmo::GizmoMode::Translate,
1747                crate::interaction::gizmo::GizmoAxis::None,
1748                glam::Quat::IDENTITY,
1749            );
1750            let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1751                label: Some("vp_gizmo_vertex_buf"),
1752                size: (std::mem::size_of::<crate::resources::Vertex>() * gizmo_verts.len().max(1))
1753                    as u64,
1754                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1755                mapped_at_creation: true,
1756            });
1757            gizmo_vertex_buffer
1758                .slice(..)
1759                .get_mapped_range_mut()
1760                .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
1761            gizmo_vertex_buffer.unmap();
1762            let gizmo_index_count = gizmo_indices.len() as u32;
1763            let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1764                label: Some("vp_gizmo_index_buf"),
1765                size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
1766                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1767                mapped_at_creation: true,
1768            });
1769            gizmo_index_buffer
1770                .slice(..)
1771                .get_mapped_range_mut()
1772                .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
1773            gizmo_index_buffer.unmap();
1774            let gizmo_uniform = crate::interaction::gizmo::GizmoUniform {
1775                model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1776            };
1777            let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
1778                label: Some("vp_gizmo_uniform_buf"),
1779                size: std::mem::size_of::<crate::interaction::gizmo::GizmoUniform>() as u64,
1780                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1781                mapped_at_creation: true,
1782            });
1783            gizmo_uniform_buf
1784                .slice(..)
1785                .get_mapped_range_mut()
1786                .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
1787            gizmo_uniform_buf.unmap();
1788            let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1789                label: Some("vp_gizmo_bind_group"),
1790                layout: &self.resources.gizmo_bind_group_layout,
1791                entries: &[wgpu::BindGroupEntry {
1792                    binding: 0,
1793                    resource: gizmo_uniform_buf.as_entire_binding(),
1794                }],
1795            });
1796
1797            // Per-viewport axes vertex buffer (2048 vertices = enough for all axes geometry).
1798            let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1799                label: Some("vp_axes_vertex_buf"),
1800                size: (std::mem::size_of::<crate::widgets::axes_indicator::AxesVertex>() * 2048)
1801                    as u64,
1802                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1803                mapped_at_creation: false,
1804            });
1805
1806            self.viewport_slots.push(ViewportSlot {
1807                camera_buf,
1808                clip_planes_buf,
1809                clip_volume_buf,
1810                shadow_info_buf,
1811                grid_buf,
1812                camera_bind_group,
1813                grid_bind_group,
1814                hdr: None,
1815                debug_frag_buf: None,
1816                debug_frag_dims: (0, 0),
1817                outline_object_buffers: Vec::new(),
1818                splat_outline_buffers: Vec::new(),
1819                volume_outline_indices: Vec::new(),
1820                glyph_outline_indices: Vec::new(),
1821                tensor_glyph_outline_indices: Vec::new(),
1822                sprite_outline_indices: Vec::new(),
1823                raw_geom_outline_buffers: Vec::new(),
1824                screen_rect_outline_buffers: Vec::new(),
1825                implicit_outline_indices: Vec::new(),
1826                mc_outline_data: Vec::new(),
1827                streamtube_outline_items: Vec::new(),
1828                tube_outline_items: Vec::new(),
1829                ribbon_outline_items: Vec::new(),
1830                polyline_outline_indices: Vec::new(),
1831                xray_object_buffers: Vec::new(),
1832                constraint_line_buffers: Vec::new(),
1833                cap_buffers: Vec::new(),
1834                clip_plane_fill_buffers: Vec::new(),
1835                clip_plane_line_buffers: Vec::new(),
1836                axes_vertex_buffer,
1837                axes_vertex_count: 0,
1838                gizmo_uniform_buf,
1839                gizmo_bind_group,
1840                gizmo_vertex_buffer,
1841                gizmo_index_buffer,
1842                gizmo_index_count,
1843                sub_highlight: None,
1844                sub_highlight_generation: u64::MAX,
1845                dyn_res: None,
1846                hdr_callback: None,
1847            });
1848        }
1849    }
1850
1851    // -----------------------------------------------------------------------
1852    // Multi-viewport public API
1853    // -----------------------------------------------------------------------
1854
1855    /// Create a new viewport slot and return its handle.
1856    ///
1857    /// The returned [`ViewportId`] is stable for the lifetime of the renderer.
1858    /// Pass it to [`prepare_viewport`](Self::prepare_viewport),
1859    /// [`paint_viewport`](Self::paint_viewport), and
1860    /// [`render_viewport`](Self::render_viewport) each frame.
1861    ///
1862    /// Also set the viewport slot on the camera frame when building the
1863    /// [`FrameData`] for this viewport:
1864    /// ```rust,ignore
1865    /// let id = renderer.create_viewport(&device);
1866    /// let frame = FrameData {
1867    ///     camera: CameraFrame::from_camera(&cam, size).with_viewport_id(id),
1868    ///     ..Default::default()
1869    /// };
1870    /// ```
1871    pub fn create_viewport(&mut self, device: &wgpu::Device) -> ViewportId {
1872        let idx = self.viewport_slots.len();
1873        self.ensure_viewport_slot(device, idx);
1874        ViewportId(idx)
1875    }
1876
1877    /// Release the heavy GPU texture memory (HDR targets, OIT, bloom, SSAO) held
1878    /// by `id`.
1879    ///
1880    /// The slot index is not reclaimed : future calls with this `ViewportId` will
1881    /// lazily recreate the texture resources as needed.  This is useful when a
1882    /// viewport is hidden or minimised and you want to reduce VRAM pressure without
1883    /// invalidating the handle.
1884    pub fn destroy_viewport(&mut self, id: ViewportId) {
1885        if let Some(slot) = self.viewport_slots.get_mut(id.0) {
1886            slot.hdr = None;
1887        }
1888    }
1889
1890    /// Returns the owned-encoder rendering path.
1891    ///
1892    /// Use when you own the window loop and wgpu encoder (winit, raw wgpu).
1893    /// See [`OwnedPath`] for available methods.
1894    pub fn owned(&mut self) -> OwnedPath<'_> {
1895        OwnedPath { renderer: self }
1896    }
1897
1898    /// Returns the pass-based rendering path.
1899    ///
1900    /// Use when a framework provides you with a render pass (eframe, iced).
1901    /// See [`PassPath`] for available methods.
1902    pub fn pass(&mut self) -> PassPath<'_> {
1903        PassPath { renderer: self }
1904    }
1905
1906    /// Returns a read-only paint view for framework paint callbacks.
1907    ///
1908    /// Use this in callbacks where only a shared reference to the renderer is
1909    /// available (e.g. eframe's `CallbackTrait::paint` where `callback_resources`
1910    /// is `&CallbackResources`). Exposes only the paint methods, not prepare.
1911    pub fn pass_view(&self) -> PassView<'_> {
1912        PassView { renderer: self }
1913    }
1914
1915    /// Prepare shared scene data.  Call **once per frame**, before any
1916    /// [`prepare_viewport`](Self::prepare_viewport) calls.
1917    ///
1918    /// `frame` provides the scene content (`frame.scene`) and the primary camera
1919    /// used for shadow cascade framing (`frame.camera`).  In a multi-viewport
1920    /// setup use any one viewport's `FrameData` here : typically the perspective
1921    /// view : as the shadow framing reference.
1922    ///
1923    /// `scene_effects` carries the scene-global effects: lighting, environment
1924    /// map, and compute filters.  Obtain it by constructing [`SceneEffects`]
1925    /// directly or via [`EffectsFrame::split`].
1926    pub(crate) fn prepare_scene(
1927        &mut self,
1928        device: &wgpu::Device,
1929        queue: &wgpu::Queue,
1930        frame: &FrameData,
1931        scene_effects: &SceneEffects<'_>,
1932    ) {
1933        self.prepare_scene_internal(device, queue, frame, scene_effects);
1934    }
1935
1936    /// Prepare per-viewport GPU state (camera, clip planes, overlays, axes).
1937    ///
1938    /// Call once per viewport per frame, **after** [`prepare_scene`](Self::prepare_scene).
1939    ///
1940    /// `id` must have been obtained from [`create_viewport`](Self::create_viewport).
1941    /// `frame.camera.viewport_index` must equal the slot for `id`; use
1942    /// [`CameraFrame::with_viewport_id`] when building the frame.
1943    pub(crate) fn prepare_viewport(
1944        &mut self,
1945        device: &wgpu::Device,
1946        queue: &wgpu::Queue,
1947        id: ViewportId,
1948        frame: &FrameData,
1949    ) {
1950        debug_assert_eq!(
1951            frame.camera.viewport_index, id.0,
1952            "frame.camera.viewport_index ({}) must equal the ViewportId ({}); \
1953             use CameraFrame::with_viewport_id(id)",
1954            frame.camera.viewport_index, id.0,
1955        );
1956        let (_, viewport_fx) = frame.effects.split();
1957        self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1958    }
1959
1960    /// Issue draw calls for `id` into a render pass with any lifetime.
1961    ///
1962    /// Identical to [`paint_viewport`](Self::paint_viewport) but accepts a render pass with a
1963    /// non-`'static` lifetime, making it usable from winit, iced, or raw wgpu where the encoder
1964    /// creates its own render pass.
1965    pub(crate) fn paint_viewport_to<'rp>(
1966        &self,
1967        render_pass: &mut wgpu::RenderPass<'rp>,
1968        id: ViewportId,
1969        frame: &FrameData,
1970    ) {
1971        let vp_idx = id.0;
1972        let camera_bg = self.viewport_camera_bind_group(vp_idx);
1973        let grid_bg = self.viewport_grid_bind_group(vp_idx);
1974        let vp_slot = self.viewport_slots.get(vp_idx);
1975        emit_draw_calls!(
1976            &self.resources,
1977            &mut *render_pass,
1978            frame,
1979            self.use_instancing,
1980            &self.instanced_batches,
1981            camera_bg,
1982            grid_bg,
1983            &self.compute_filter_results,
1984            vp_slot,
1985            &self.wireframe_bind_groups,
1986            &self.per_item_object_bind_groups
1987        );
1988        emit_scivis_draw_calls!(
1989            &self.resources,
1990            &mut *render_pass,
1991            &self.point_cloud_gpu_data,
1992            &self.glyph_gpu_data,
1993            &self.polyline_gpu_data,
1994            &self.volume_gpu_data,
1995            &self.streamtube_gpu_data,
1996            camera_bg,
1997            &self.tube_gpu_data,
1998            &self.image_slice_gpu_data,
1999            &self.tensor_glyph_gpu_data,
2000            &self.ribbon_gpu_data,
2001            &self.volume_surface_slice_gpu_data,
2002            &self.sprite_gpu_data,
2003            &self.mesh_instance_gpu_data,
2004            false
2005        );
2006        // Gaussian splats (alpha-blended, back-to-front sorted, no depth write).
2007        if !self.gaussian_splat_draw_data.is_empty() {
2008            if let Some(ref dual) = self.resources.gaussian_splat_pipeline {
2009                render_pass.set_pipeline(dual.for_format(false));
2010                render_pass.set_bind_group(0, camera_bg, &[]);
2011                for dd in &self.gaussian_splat_draw_data {
2012                    if dd.wireframe {
2013                        continue;
2014                    }
2015                    if let Some(set) = self.resources.gaussian_splat_store.get(dd.store_index) {
2016                        if let Some(Some(vp_sort)) = set.viewport_sort.get(dd.viewport_index) {
2017                            render_pass.set_bind_group(1, &vp_sort.render_bg, &[]);
2018                            render_pass.draw(0..6, 0..dd.count);
2019                        }
2020                    }
2021                }
2022            }
2023        }
2024        // TransparentVolumeMesh boundary wireframe overlay.
2025        if !self.tvm_wireframe_draws.is_empty() {
2026            if let Some(ref tvm_bg) = self.tvm_wireframe_bg {
2027                render_pass.set_bind_group(0, camera_bg, &[]);
2028                for mesh_id in &self.tvm_wireframe_draws {
2029                    if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
2030                        render_pass.set_pipeline(&self.resources.wireframe_pipeline);
2031                        render_pass.set_bind_group(1, tvm_bg, &[]);
2032                        render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2033                        render_pass.set_index_buffer(
2034                            mesh.edge_index_buffer.slice(..),
2035                            wgpu::IndexFormat::Uint32,
2036                        );
2037                        render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
2038                    }
2039                }
2040            }
2041        }
2042        // Shadow atlas viewer overlay.
2043        if frame.effects.show_shadow_atlas {
2044            render_pass.set_pipeline(&self.resources.shadow_atlas_viewer_pipeline);
2045            render_pass.set_bind_group(0, &self.resources.shadow_atlas_viewer_bg, &[]);
2046            render_pass.draw(0..6, 0..1);
2047        }
2048    }
2049
2050    /// Return a reference to the camera bind group for the given viewport slot.
2051    ///
2052    /// Falls back to `resources.camera_bind_group` if no per-viewport slot
2053    /// exists (e.g. in single-viewport mode before the first prepare call).
2054    fn viewport_camera_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
2055        self.viewport_slots
2056            .get(viewport_index)
2057            .map(|slot| &slot.camera_bind_group)
2058            .unwrap_or(&self.resources.camera_bind_group)
2059    }
2060
2061    /// Return a reference to the grid bind group for the given viewport slot.
2062    ///
2063    /// Falls back to `resources.grid_bind_group` if no per-viewport slot exists.
2064    fn viewport_grid_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
2065        self.viewport_slots
2066            .get(viewport_index)
2067            .map(|slot| &slot.grid_bind_group)
2068            .unwrap_or(&self.resources.grid_bind_group)
2069    }
2070
2071    /// Ensure the dyn-res intermediate render target exists for `vp_idx` at the
2072    /// given `scaled_size`, creating or recreating it when size changes.
2073    ///
2074    /// `surface_size` is the native output dimensions (used to size the upscale
2075    /// blit correctly). `ensure_dyn_res_pipeline` is called automatically.
2076    pub(crate) fn ensure_dyn_res_target(
2077        &mut self,
2078        device: &wgpu::Device,
2079        vp_idx: usize,
2080        scaled_size: [u32; 2],
2081        surface_size: [u32; 2],
2082    ) {
2083        self.resources.ensure_dyn_res_pipeline(device);
2084        let needs_create = match &self.viewport_slots[vp_idx].dyn_res {
2085            None => true,
2086            Some(dr) => dr.scaled_size != scaled_size || dr.surface_size != surface_size,
2087        };
2088        if needs_create {
2089            let target = self
2090                .resources
2091                .create_dyn_res_target(device, scaled_size, surface_size);
2092            self.viewport_slots[vp_idx].dyn_res = Some(target);
2093        }
2094    }
2095
2096    /// Ensure per-viewport HDR state exists for `viewport_index` at dimensions `w`×`h`.
2097    ///
2098    /// Calls `ensure_hdr_shared` once to initialise shared pipelines/BGLs/samplers, then
2099    /// lazily creates or resizes the `ViewportHdrState` inside the slot. Idempotent: if the
2100    /// slot already has HDR state at the correct size nothing is recreated.
2101    pub(crate) fn ensure_viewport_hdr(
2102        &mut self,
2103        device: &wgpu::Device,
2104        queue: &wgpu::Queue,
2105        viewport_index: usize,
2106        w: u32,
2107        h: u32,
2108        ssaa_factor: u32,
2109        render_scale: f32,
2110    ) {
2111        let format = self.resources.target_format;
2112        // Ensure shared infrastructure (pipelines, BGLs, samplers) exists.
2113        self.resources.ensure_hdr_shared(device, queue, format);
2114        // When render_scale < 1.0, the HDR upscale path needs the dyn_res
2115        // pipeline and sampler for the final upscale-blit to output resolution.
2116        if render_scale < 1.0 - 0.001 {
2117            self.resources.ensure_dyn_res_pipeline(device);
2118        }
2119        // Compute the scene-resolution render target size.
2120        let scale = render_scale.clamp(0.1, 1.0);
2121        let scene_w = ((w as f32) * scale).round() as u32;
2122        let scene_h = ((h as f32) * scale).round() as u32;
2123        // Ensure the slot exists.
2124        self.ensure_viewport_slot(device, viewport_index);
2125        let slot = &mut self.viewport_slots[viewport_index];
2126        // Create or resize the per-viewport HDR state.
2127        let needs_create = match &slot.hdr {
2128            None => true,
2129            Some(s) => {
2130                s.output_size != [w, h]
2131                    || s.scene_size != [scene_w.max(1), scene_h.max(1)]
2132                    || s.ssaa_factor != ssaa_factor
2133            }
2134        };
2135        if needs_create {
2136            slot.hdr = Some(self.resources.create_hdr_viewport_state(
2137                device,
2138                queue,
2139                format,
2140                w,
2141                h,
2142                scene_w.max(1),
2143                scene_h.max(1),
2144                ssaa_factor,
2145            ));
2146        }
2147    }
2148}