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 shadows;
18pub mod stats;
19mod shadow_debug_stats;
20pub use shadow_debug_stats::ShadowDebugStats;
21
22#[cfg(test)]
23mod hidden_tests;
24
25pub use self::types::{
26    AtlasViewerCorner,
27    CameraFrame, ClipObject, ClipShape, ComputeFilterItem, ComputeFilterKind,
28    CylindricalFacing, DecalAnimation, DecalBlendMode, DecalItem, DecalProjection,
29    DebugOutputMode, DebugQuantity, DebugVis,
30    EffectsFrame, EnvironmentMap, FilterMode, FrameData, GaussianSplatData, GaussianSplatId,
31    GaussianSplatItem, GlyphItem, GlyphType, GroundPlane, GroundPlaneMode, ImageAnchor,
32    ImageSliceItem, InteractionFrame, LabelAnchor, LabelItem, LightKind, LightSource,
33    LicOverlay, LightingSettings, LoadingBarAnchor, LoadingBarItem, OverlayFrame, OverlayImageItem,
34    OverlayAnimation, OverlayFill, OverlayRectItem, OverlayShape, OverlayShapeItem,
35    OverlayTextureId, BorderMode, LineCap, PickId, TriangleDirection,
36    PointCloudItem, PointRenderMode, PolylineItem, PostProcessSettings, RenderCamera, RibbonItem,
37    RulerItem, ScalarBarAnchor, ScalarBarItem, ScalarBarOrientation, SceneEffects, SceneFrame,
38    SceneRenderItem, ScreenImageItem, ShDegree, ShadowFilter, SliceAxis, SpriteItem,
39    SpriteSizeMode, StreamtubeItem, SurfaceLICConfig, SurfaceSubmission,
40    ScatterQuality, ScatterSettings, ScatterVolumeItem,
41    TensorGlyphItem, ToneMapping, TransparentVolumeMeshItem, TubeItem, ViewportEffects,
42    ViewportFrame, VolumeItem, VolumeMeshItem, VolumeSurfaceSliceItem, aabb_wireframe_polyline,
43    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    /// Performance counters from the last frame.
227    last_stats: crate::renderer::stats::FrameStats,
228    /// Last scene generation seen during prepare(). u64::MAX forces rebuild on first frame.
229    last_scene_generation: u64,
230    /// Last selection generation seen during prepare(). u64::MAX forces rebuild on first frame.
231    last_selection_generation: u64,
232    /// Last scene_items count seen during prepare(). usize::MAX forces rebuild on first frame.
233    /// Included in cache key so that frustum-culling changes (different visible set, different
234    /// count) correctly invalidate the instance buffer even when scene_generation is stable.
235    last_scene_items_count: usize,
236    /// Count of items that passed the instanced-path filter on the last rebuild.
237    /// Used in place of has_per_frame_mutations so scenes that mix instanced and
238    /// non-instanced items (e.g. one two-sided mesh + 10k static boxes) still hit
239    /// the instanced batch cache on frames where the filtered set is unchanged.
240    last_instancable_count: usize,
241    /// Total instance count from the last rebuild. Used as a fast length check
242    /// in `structure_preserved` and as `instance_count` for GPU cull dispatches.
243    cached_instance_count: usize,
244    /// Per-batch content hash from the last rebuild, indexed by batch position.
245    /// A hash mismatch triggers a `write_buffer` for that batch; a match skips it.
246    cached_instance_hashes: Vec<u64>,
247    /// Cached instanced batch descriptors from last rebuild.
248    cached_instanced_batches: Vec<InstancedBatch>,
249    /// When true, the next cache-miss forces a full buffer upload instead of the
250    /// per-batch partial-upload path. Set by `force_dirty()` and consumed once.
251    force_full_upload: bool,
252    /// Per-frame point cloud GPU data, rebuilt in prepare(), consumed in paint().
253    point_cloud_gpu_data: Vec<crate::resources::PointCloudGpuData>,
254    /// Per-frame glyph GPU data, rebuilt in prepare(), consumed in paint().
255    glyph_gpu_data: Vec<crate::resources::GlyphGpuData>,
256    /// Per-frame tensor glyph GPU data, rebuilt in prepare(), consumed in paint().
257    tensor_glyph_gpu_data: Vec<crate::resources::TensorGlyphGpuData>,
258    /// Per-frame polyline GPU data, rebuilt in prepare(), consumed in paint().
259    polyline_gpu_data: Vec<crate::resources::PolylineGpuData>,
260    /// Per-frame volume GPU data, rebuilt in prepare(), consumed in paint().
261    volume_gpu_data: Vec<crate::resources::VolumeGpuData>,
262    /// Per-frame streamtube GPU data, rebuilt in prepare(), consumed in paint().
263    streamtube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
264    /// Per-frame general tube GPU data, rebuilt in prepare(), consumed in paint().
265    tube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
266    /// Per-frame ribbon GPU data, rebuilt in prepare(), consumed in paint().
267    ribbon_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
268    /// Indices into streamtube_gpu_data for selected streamtubes (set in prepare_scene, consumed in prepare_viewport).
269    streamtube_selected_gpu_indices: Vec<usize>,
270    /// Indices into tube_gpu_data for selected tubes (set in prepare_scene, consumed in prepare_viewport).
271    tube_selected_gpu_indices: Vec<usize>,
272    /// Indices into ribbon_gpu_data for selected ribbons (set in prepare_scene, consumed in prepare_viewport).
273    ribbon_selected_gpu_indices: Vec<usize>,
274    /// Indices into polyline_gpu_data for selected user polylines (set in prepare_scene, consumed in prepare_viewport).
275    polyline_selected_gpu_indices: Vec<usize>,
276    /// Per-frame image slice GPU data, rebuilt in prepare(), consumed in paint().
277    image_slice_gpu_data: Vec<crate::resources::ImageSliceGpuData>,
278    /// Per-frame volume surface slice GPU data, rebuilt in prepare(), consumed in paint().
279    volume_surface_slice_gpu_data: Vec<crate::resources::VolumeSurfaceSliceGpuData>,
280    /// Per-frame Surface LIC GPU data, rebuilt in prepare(), consumed in paint().
281    lic_gpu_data: Vec<crate::resources::LicSurfaceGpuData>,
282    /// Per-frame GPU implicit surface data, rebuilt in prepare(), consumed in paint().
283    implicit_gpu_data: Vec<crate::resources::implicit::ImplicitGpuItem>,
284    /// Per-frame decal GPU data, rebuilt in prepare(), consumed in paint() (D1).
285    decal_gpu_data: Vec<crate::resources::decal::DecalGpuItem>,
286    /// Per-frame decal exclude GPU data, rebuilt in prepare(), consumed in paint() (D5).
287    decal_exclude_items: Vec<crate::resources::decal::DecalExcludeGpuItem>,
288    /// Per-frame GPU marching cubes render data, rebuilt in prepare(), consumed in paint().
289    mc_gpu_data: Vec<crate::resources::gpu_marching_cubes::McFrameData>,
290    /// Per-frame sprite GPU data, rebuilt in prepare(), consumed in paint().
291    sprite_gpu_data: Vec<crate::resources::SpriteGpuData>,
292    /// Per-frame Gaussian splat draw data, rebuilt in prepare_viewport_internal(), consumed in paint().
293    gaussian_splat_draw_data: Vec<crate::resources::GaussianSplatDrawData>,
294    /// Per-frame screen-image GPU data, rebuilt in prepare(), consumed in paint().
295    screen_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
296    /// Per-frame overlay image GPU data, rebuilt in prepare(), consumed in paint().
297    overlay_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
298    /// Per-frame overlay label GPU data, rebuilt in prepare(), consumed in paint().
299    label_gpu_data: Option<crate::resources::LabelGpuData>,
300    /// Per-frame scalar bar GPU data, rebuilt in prepare(), consumed in paint().
301    scalar_bar_gpu_data: Option<crate::resources::LabelGpuData>,
302    /// Per-frame ruler GPU data, rebuilt in prepare(), consumed in paint().
303    ruler_gpu_data: Option<crate::resources::LabelGpuData>,
304    /// Per-frame loading bar GPU data, rebuilt in prepare(), consumed in paint().
305    loading_bar_gpu_data: Option<crate::resources::LabelGpuData>,
306    /// Per-frame overlay rect GPU data, rebuilt in prepare(), consumed in paint().
307    overlay_rect_gpu_data: Option<crate::resources::LabelGpuData>,
308    /// Per-frame SDF overlay shape GPU data, rebuilt in prepare(), consumed in paint().
309    overlay_shape_gpu_data: Option<crate::resources::OverlayShapeGpuData>,
310    /// Cached GPU textures for the backdrop blur effect (frosted glass).
311    /// Recreated when the viewport size changes.
312    backdrop_blur_state: Option<crate::resources::BackdropBlurState>,
313    /// Per-viewport GPU state slots.
314    ///
315    /// Indexed by `FrameData::camera.viewport_index`. Each slot owns independent
316    /// uniform buffers and bind groups for camera, clip planes, clip volume,
317    /// shadow info, and grid. Slots are grown lazily in `prepare` via
318    /// `ensure_viewport_slot`. There are at most 4 in the current UI.
319    viewport_slots: Vec<ViewportSlot>,
320    /// GPU compute filter results from the last `prepare()` call.
321    ///
322    /// Each entry contains a compacted index buffer + count for one filtered mesh.
323    /// Consumed during `paint()` to override the mesh's default index buffer.
324    /// Cleared and rebuilt each frame.
325    compute_filter_results: Vec<crate::resources::ComputeFilterResult>,
326    /// Per-item uniform buffers for wireframe mode. In wireframe mode multiple scene
327    /// items can share the same MeshId, but each needs its own object uniform (model
328    /// matrix, colour, etc.). The mesh's single `object_uniform_buf` gets overwritten
329    /// by the last item prepared, so we maintain a separate pool here. Indexed in the
330    /// same order as the visible scene items. Grown lazily, never shrunk.
331    wireframe_uniform_bufs: Vec<wgpu::Buffer>,
332    /// Bind groups corresponding to `wireframe_uniform_bufs`. Each bind group pairs
333    /// the per-item uniform buffer with the mesh's fallback textures so it is
334    /// compatible with the object bind group layout.
335    wireframe_bind_groups: Vec<wgpu::BindGroup>,
336    /// Per-frame list of boundary mesh IDs to draw in wireframe for
337    /// TransparentVolumeMeshItems with `appearance.wireframe = true`.
338    /// Cleared and rebuilt each frame in prepare_scene_internal.
339    tvm_wireframe_draws: Vec<crate::resources::mesh_store::MeshId>,
340    /// Shared uniform buffer for TVM boundary wireframe draws (wireframe=1, model=identity).
341    /// Created once on first use, reused every frame.
342    tvm_wireframe_buf: Option<wgpu::Buffer>,
343    /// Bind group for TVM boundary wireframe draws. Pairs `tvm_wireframe_buf` with
344    /// fallback textures matching the object bind group layout.
345    tvm_wireframe_bg: Option<wgpu::BindGroup>,
346    /// Cascade-0 light-space view-projection matrix from the last shadow prepare.
347    /// Cached here so `prepare_viewport_internal` can copy it into the ground plane uniform.
348    last_cascade0_shadow_mat: glam::Mat4,
349    /// Current runtime mode controlling internal default behavior.
350    runtime_mode: crate::renderer::stats::RuntimeMode,
351    /// Active performance policy: target FPS, render scale bounds, and permitted reductions.
352    performance_policy: crate::renderer::stats::PerformancePolicy,
353    /// Current render scale tracked by the adaptation controller (or set manually).
354    ///
355    /// Clamped to `[policy.min_render_scale, policy.max_render_scale]`.
356    /// Reported in `FrameStats::render_scale` each frame.
357    current_render_scale: f32,
358    /// Instant the renderer was constructed. Used as the t=0 reference for
359    /// per-frame animated effects (e.g. `ScatterVolume::noise` time scrolling).
360    start_instant: std::time::Instant,
361    /// Instant recorded at the start of the most recent `prepare()` call.
362    /// Used to compute `total_frame_ms` on the following frame.
363    last_prepare_instant: Option<std::time::Instant>,
364    /// Frame counter incremented each `prepare()` call. Used for picking throttle in Playback mode.
365    frame_counter: u64,
366    /// Surface items from the last `prepare()` call, retained for `pick()` dispatch.
367    pick_scene_items: Vec<SceneRenderItem>,
368    /// Point cloud items from the last `prepare()` call, retained for `pick()` dispatch.
369    pick_point_cloud_items: Vec<PointCloudItem>,
370    /// Gaussian splat items from the last `prepare()` call, retained for `pick()` dispatch.
371    pick_splat_items: Vec<GaussianSplatItem>,
372    /// Volume items from the last `prepare()` call, retained for `pick()` dispatch.
373    pick_volume_items: Vec<VolumeItem>,
374    /// Transparent volume mesh items from the last `prepare()` call, retained for `pick()` dispatch.
375    pick_tvm_items: Vec<TransparentVolumeMeshItem>,
376    /// Scatter volume items from the last `prepare()` call, retained for `pick()` dispatch.
377    pick_scatter_volume_items: Vec<crate::renderer::types::ScatterVolumeItem>,
378    /// Volumes packed into the GPU storage buffer this frame
379    /// (volume, density_multiplier, flag bits). Stored so `render_viewport`
380    /// can re-upload as needed without re-walking the scene frame.
381    pub(crate) prepared_scatter_volumes:
382        Vec<(crate::scene::scatter_volume::ScatterVolume, f32, u32)>,
383    /// Per-viewport scatter intermediates and temporal history. Indexed by
384    /// `vp_idx`. Grown lazily inside the scatter pass; each entry is
385    /// reallocated when the requested scatter target size or downsample mode
386    /// changes.
387    pub(crate) scatter_viewport_states: Vec<Option<crate::resources::ScatterViewportState>>,
388    /// Opaque volume mesh items from the last `prepare()` call, retained for cell-level `pick()` dispatch.
389    pick_volume_mesh_items: Vec<VolumeMeshItem>,
390    /// Polyline items from the last `prepare()` call, retained for `pick()` dispatch.
391    pick_polyline_items: Vec<PolylineItem>,
392    /// Glyph items from the last `prepare()` call, retained for `pick()` dispatch.
393    pick_glyph_items: Vec<GlyphItem>,
394    /// Tensor glyph items from the last `prepare()` call, retained for `pick()` dispatch.
395    pick_tensor_glyph_items: Vec<TensorGlyphItem>,
396    /// Sprite items from the last `prepare()` call, retained for `pick()` dispatch.
397    pick_sprite_items: Vec<SpriteItem>,
398    /// Streamtube items from the last `prepare()` call, retained for `pick()` dispatch.
399    pick_streamtube_items: Vec<StreamtubeItem>,
400    /// Tube items from the last `prepare()` call, retained for `pick()` dispatch.
401    pick_tube_items: Vec<TubeItem>,
402    /// Ribbon items from the last `prepare()` call, retained for `pick()` dispatch.
403    pick_ribbon_items: Vec<RibbonItem>,
404    /// Image slice items from the last `prepare()` call, retained for `pick()` dispatch.
405    pick_image_slice_items: Vec<ImageSliceItem>,
406    /// Volume surface slice items from the last `prepare()` call, retained for `pick()` dispatch.
407    pick_volume_surface_slice_items: Vec<VolumeSurfaceSliceItem>,
408    /// Screen image items from the last `prepare()` call, retained for `pick()` dispatch.
409    pick_screen_image_items: Vec<ScreenImageItem>,
410    /// GPU implicit surface items from the last `prepare()` call, retained for `pick()` dispatch.
411    pick_implicit_items: Vec<GpuImplicitPickItem>,
412    /// GPU marching cubes jobs from the last `prepare()` call, retained for `pick()` dispatch.
413    pick_mc_items: Vec<GpuMcPickItem>,
414
415    // --- GPU timestamp queries ---
416    /// Timestamp query set with 2 entries (scene-pass begin + end).
417    /// `None` when `TIMESTAMP_QUERY` is unavailable or not yet initialized.
418    ts_query_set: Option<wgpu::QuerySet>,
419    /// Resolve buffer: 2 × u64, GPU-only (`QUERY_RESOLVE | COPY_SRC`).
420    ts_resolve_buf: Option<wgpu::Buffer>,
421    /// Staging buffer: 2 × u64, CPU-readable (`COPY_DST | MAP_READ`).
422    ts_staging_buf: Option<wgpu::Buffer>,
423    /// Nanoseconds per GPU timestamp tick, from `queue.get_timestamp_period()`.
424    ts_period: f32,
425    /// Whether the staging buffer holds unread timestamp data from the previous frame.
426    ts_needs_readback: bool,
427
428    // --- Indirect-args readback (GPU-driven culling visible instance count) ---
429    /// CPU-readable staging buffer for `indirect_args_buf` (batch_count × 20 bytes).
430    /// Grown lazily; never shrunk.
431    indirect_readback_buf: Option<wgpu::Buffer>,
432    /// Number of batches whose data was copied into `indirect_readback_buf` last frame.
433    indirect_readback_batch_count: u32,
434    /// True when `indirect_readback_buf` holds unread data from the previous cull pass.
435    indirect_readback_pending: bool,
436
437    // --- Per-pass degradation state ---
438    /// Tiered degradation ladder position (0 = none, 1 = shadows, 2 = volumes, 3 = effects).
439    /// Advanced one step per over-budget frame once render scale hits minimum;
440    /// reversed one step per comfortably-under-budget frame.
441    degradation_tier: u8,
442    /// Whether the shadow pass was skipped this frame due to budget pressure.
443    /// Computed once per frame at the top of prepare() and used by both
444    /// prepare_scene_internal and reported in FrameStats.
445    degradation_shadows_skipped: bool,
446    /// Whether volume raymarch step size was doubled this frame due to budget pressure.
447    degradation_volume_quality_reduced: bool,
448    /// Whether SSAO, contact shadows, and bloom were skipped this frame.
449    /// Set in prepare(); read by the render path.
450    degradation_effects_throttled: bool,
451
452    // --- D8: shadow debug stats cache ---
453    /// Cascade count from the last prepare_scene_internal call.
454    last_cascade_count: u32,
455    /// Cascade split distances from the last prepare_scene_internal call.
456    last_cascade_splits: [f32; 4],
457    /// Shadow frustum half-extent from the last prepare_scene_internal call.
458    last_shadow_extent: f32,
459    /// Shadow atlas resolution from the last prepare_scene_internal call.
460    last_shadow_atlas_resolution: u32,
461    /// Contact shadow enabled state from the last prepare_scene_internal call.
462    last_contact_shadow_active: bool,
463    /// Cascade splits from the last tracing log emission. Sentinel [f32::MAX; 4] forces
464    /// a log on the first frame.
465    last_logged_cascade_splits: [f32; 4],
466}
467
468impl ViewportRenderer {
469    /// Create a new renderer with default settings (no MSAA).
470    /// Call once at application startup.
471    pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
472        Self::with_sample_count(device, target_format, 1)
473    }
474
475    /// Create a new renderer with the specified MSAA sample count (1, 2, or 4).
476    ///
477    /// When using MSAA (sample_count > 1), the caller must create multisampled
478    /// colour and depth textures and use them as render pass attachments with the
479    /// final surface texture as the resolve target.
480    pub fn with_sample_count(
481        device: &wgpu::Device,
482        target_format: wgpu::TextureFormat,
483        sample_count: u32,
484    ) -> Self {
485        let gpu_culling_supported = device
486            .features()
487            .contains(wgpu::Features::INDIRECT_FIRST_INSTANCE);
488        Self {
489            resources: ViewportGpuResources::new(device, target_format, sample_count),
490            instanced_batches: Vec::new(),
491            use_instancing: false,
492            gpu_culling_supported,
493            gpu_culling_enabled: gpu_culling_supported,
494            cull_resources: None,
495            last_stats: crate::renderer::stats::FrameStats::default(),
496            last_scene_generation: u64::MAX,
497            last_selection_generation: u64::MAX,
498            last_scene_items_count: usize::MAX,
499            last_instancable_count: usize::MAX,
500            cached_instance_count: 0,
501            cached_instance_hashes: Vec::new(),
502            cached_instanced_batches: Vec::new(),
503            force_full_upload: false,
504            point_cloud_gpu_data: Vec::new(),
505            glyph_gpu_data: Vec::new(),
506            tensor_glyph_gpu_data: Vec::new(),
507            polyline_gpu_data: Vec::new(),
508            volume_gpu_data: Vec::new(),
509            streamtube_gpu_data: Vec::new(),
510            tube_gpu_data: Vec::new(),
511            ribbon_gpu_data: Vec::new(),
512            streamtube_selected_gpu_indices: Vec::new(),
513            tube_selected_gpu_indices: Vec::new(),
514            ribbon_selected_gpu_indices: Vec::new(),
515            polyline_selected_gpu_indices: Vec::new(),
516            image_slice_gpu_data: Vec::new(),
517            volume_surface_slice_gpu_data: Vec::new(),
518            sprite_gpu_data: Vec::new(),
519            gaussian_splat_draw_data: Vec::new(),
520            lic_gpu_data: Vec::new(),
521            implicit_gpu_data: Vec::new(),
522            decal_gpu_data: Vec::new(),
523            decal_exclude_items: Vec::new(),
524            mc_gpu_data: Vec::new(),
525            screen_image_gpu_data: Vec::new(),
526            overlay_image_gpu_data: Vec::new(),
527            label_gpu_data: None,
528            scalar_bar_gpu_data: None,
529            ruler_gpu_data: None,
530            loading_bar_gpu_data: None,
531            overlay_rect_gpu_data: None,
532            overlay_shape_gpu_data: None,
533            backdrop_blur_state: None,
534            viewport_slots: Vec::new(),
535            compute_filter_results: Vec::new(),
536            wireframe_uniform_bufs: Vec::new(),
537            wireframe_bind_groups: Vec::new(),
538            tvm_wireframe_draws: Vec::new(),
539            tvm_wireframe_buf: None,
540            tvm_wireframe_bg: None,
541            last_cascade0_shadow_mat: glam::Mat4::IDENTITY,
542            runtime_mode: crate::renderer::stats::RuntimeMode::Interactive,
543            performance_policy: crate::renderer::stats::PerformancePolicy::default(),
544            current_render_scale: 1.0,
545            start_instant: std::time::Instant::now(),
546            last_prepare_instant: None,
547            frame_counter: 0,
548            pick_scene_items: Vec::new(),
549            pick_point_cloud_items: Vec::new(),
550            pick_splat_items: Vec::new(),
551            pick_volume_items: Vec::new(),
552            pick_tvm_items: Vec::new(),
553            pick_scatter_volume_items: Vec::new(),
554            prepared_scatter_volumes: Vec::new(),
555            scatter_viewport_states: Vec::new(),
556            pick_volume_mesh_items: Vec::new(),
557            pick_polyline_items: Vec::new(),
558            pick_glyph_items: Vec::new(),
559            pick_tensor_glyph_items: Vec::new(),
560            pick_sprite_items: Vec::new(),
561            pick_streamtube_items: Vec::new(),
562            pick_tube_items: Vec::new(),
563            pick_ribbon_items: Vec::new(),
564            pick_image_slice_items: Vec::new(),
565            pick_volume_surface_slice_items: Vec::new(),
566            pick_screen_image_items: Vec::new(),
567            pick_implicit_items: Vec::new(),
568            pick_mc_items: Vec::new(),
569            ts_query_set: None,
570            ts_resolve_buf: None,
571            ts_staging_buf: None,
572            ts_period: 1.0,
573            ts_needs_readback: false,
574            indirect_readback_buf: None,
575            indirect_readback_batch_count: 0,
576            indirect_readback_pending: false,
577            degradation_tier: 0,
578            degradation_shadows_skipped: false,
579            degradation_volume_quality_reduced: false,
580            degradation_effects_throttled: false,
581            last_cascade_count: 0,
582            last_cascade_splits: [0.0; 4],
583            last_shadow_extent: 20.0,
584            last_shadow_atlas_resolution: 4096,
585            last_contact_shadow_active: false,
586            last_logged_cascade_splits: [f32::MAX; 4],
587        }
588    }
589
590    /// Access the underlying GPU resources (e.g. for mesh uploads).
591    pub fn resources(&self) -> &ViewportGpuResources {
592        &self.resources
593    }
594
595    /// Performance counters from the last completed frame.
596    pub fn last_frame_stats(&self) -> crate::renderer::stats::FrameStats {
597        self.last_stats
598    }
599
600    /// Disable GPU-driven culling, reverting to the direct draw path.
601    ///
602    /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`
603    /// (culling is already disabled on those devices).
604    pub fn disable_gpu_driven_culling(&mut self) {
605        self.gpu_culling_enabled = false;
606    }
607
608    /// Force a full instance buffer upload on the next frame.
609    ///
610    /// Normally the renderer skips GPU writes for instanced batches whose data
611    /// has not changed since the last upload. Call this when you have mutated
612    /// batch-relevant state through a path the renderer cannot observe (for
613    /// example, directly modifying GPU buffer contents or scene items after
614    /// `collect_render_items` runs). The flag is consumed once and resets
615    /// automatically after the next `prepare` call.
616    pub fn force_dirty(&mut self) {
617        self.force_full_upload = true;
618        // Also invalidate the generation cache so the next prepare is guaranteed
619        // to enter the rebuild path even if the scene generation is unchanged.
620        self.last_scene_generation = u64::MAX;
621    }
622
623    /// Re-enable GPU-driven culling after a call to `disable_gpu_driven_culling`.
624    ///
625    /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`.
626    pub fn enable_gpu_driven_culling(&mut self) {
627        if self.gpu_culling_supported {
628            self.gpu_culling_enabled = true;
629        }
630    }
631
632    /// Set the runtime mode controlling internal default behavior.
633    ///
634    /// - [`RuntimeMode::Interactive`]: full picking rate, full quality (default).
635    /// - [`RuntimeMode::Playback`]: picking throttled to reduce CPU overhead during animation.
636    /// - [`RuntimeMode::Paused`]: full picking rate, full quality.
637    /// - [`RuntimeMode::Capture`]: full quality, intended for screenshot/export workflows.
638    pub fn set_runtime_mode(&mut self, mode: crate::renderer::stats::RuntimeMode) {
639        self.runtime_mode = mode;
640    }
641
642    /// Return the current runtime mode.
643    pub fn runtime_mode(&self) -> crate::renderer::stats::RuntimeMode {
644        self.runtime_mode
645    }
646
647    /// Set the performance policy controlling target FPS, render scale bounds,
648    /// and permitted quality reductions.
649    ///
650    /// The internal adaptation controller activates when
651    /// `policy.allow_dynamic_resolution` is `true` and `policy.target_fps` is
652    /// `Some`. It adjusts `render_scale` within `[min_render_scale,
653    /// max_render_scale]` each frame based on `total_frame_ms`.
654    pub fn set_performance_policy(&mut self, policy: crate::renderer::stats::PerformancePolicy) {
655        self.performance_policy = policy;
656        // Clamp current scale into the new bounds immediately.
657        self.current_render_scale = self
658            .current_render_scale
659            .clamp(policy.min_render_scale, policy.max_render_scale);
660    }
661
662    /// Return the active performance policy.
663    pub fn performance_policy(&self) -> crate::renderer::stats::PerformancePolicy {
664        self.performance_policy
665    }
666
667    /// Manually set the render scale.
668    ///
669    /// Effective when `performance_policy.allow_dynamic_resolution` is `false`.
670    /// When dynamic resolution is enabled the adaptation controller overrides
671    /// this value each frame.
672    ///
673    /// The value is clamped to `[policy.min_render_scale, policy.max_render_scale]`.
674    ///
675    /// Works on both the LDR and HDR render paths. On the HDR path, the scene,
676    /// bloom, SSAO, tone-map, and FXAA all run at the scaled resolution; the
677    /// result is upscale-blitted to native resolution before overlays and grid.
678    pub fn set_render_scale(&mut self, scale: f32) {
679        self.current_render_scale = scale.clamp(
680            self.performance_policy.min_render_scale,
681            self.performance_policy.max_render_scale,
682        );
683    }
684
685    /// Set the target frame rate used to compute [`FrameStats::missed_budget`].
686    ///
687    /// Convenience wrapper that updates `performance_policy.target_fps`.
688    pub fn set_target_fps(&mut self, fps: Option<f32>) {
689        self.performance_policy.target_fps = fps;
690    }
691
692    /// Mutable access to the underlying GPU resources (e.g. for mesh uploads).
693    pub fn resources_mut(&mut self) -> &mut ViewportGpuResources {
694        &mut self.resources
695    }
696
697    /// Returns true when the current frame is rendered via the instanced draw path.
698    ///
699    /// When true, edits to mesh.wgsl shadow sampling code have no effect - the active
700    /// shader is mesh_instanced.wgsl. Check this before testing shader changes.
701    pub fn is_using_instanced_path(&self) -> bool {
702        self.use_instancing
703    }
704
705    /// Returns the number of instanced batches prepared for the current frame.
706    ///
707    /// Zero when using the non-instanced path. Each batch corresponds to a distinct
708    /// (MeshId, material) combination in the scene.
709    pub fn instanced_batch_count(&self) -> usize {
710        self.instanced_batches.len()
711    }
712
713    /// Returns per-frame shadow and lighting pipeline statistics for debug inspection.
714    ///
715    /// All fields reflect the most recently completed `prepare` call (one frame
716    /// behind the display). Returns default values before the first `prepare` call.
717    pub fn shadow_debug_stats(&self) -> ShadowDebugStats {
718        ShadowDebugStats {
719            using_instanced_path: self.use_instancing,
720            instanced_batch_count: self.instanced_batches.len(),
721            cascade_count: self.last_cascade_count,
722            cascade_splits: self.last_cascade_splits,
723            shadow_atlas_resolution: self.last_shadow_atlas_resolution,
724            shadow_extent_world: self.last_shadow_extent,
725            contact_shadow_active: self.last_contact_shadow_active,
726        }
727    }
728
729    /// Read the debug values at a specific pixel from the per-fragment storage buffer.
730    ///
731    /// Returns `None` when debug_vis is inactive (no buffer allocated) or when `(x, y)`
732    /// is outside the viewport. The four channels correspond to the current R/G/B channel
733    /// selectors plus 1.0 for alpha.
734    ///
735    /// This submits a GPU-to-CPU copy and waits synchronously. Only call from outside
736    /// a render pass (e.g., in the next frame's prepare step), not inside paint callbacks.
737    ///
738    /// The returned values are from the previous rendered frame.
739    pub fn read_debug_pixel(
740        &self,
741        device: &wgpu::Device,
742        queue: &wgpu::Queue,
743        x: u32,
744        y: u32,
745    ) -> Option<[f32; 4]> {
746        // Use the primary viewport slot (index 0).
747        let slot = self.viewport_slots.first()?;
748        let buf = slot.debug_frag_buf.as_ref()?;
749        let (vw, vh) = slot.debug_frag_dims;
750        if x >= vw || y >= vh {
751            return None;
752        }
753        let byte_offset = ((y as u64) * (vw as u64) + (x as u64)) * 16;
754        let staging = device.create_buffer(&wgpu::BufferDescriptor {
755            label: None,
756            size: 16,
757            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
758            mapped_at_creation: false,
759        });
760        let mut encoder = device
761            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
762        encoder.copy_buffer_to_buffer(buf, byte_offset, &staging, 0, 16);
763        queue.submit(Some(encoder.finish()));
764        let slice = staging.slice(..);
765        let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
766        slice.map_async(wgpu::MapMode::Read, move |r| {
767            let _ = tx.send(r);
768        });
769        let _ = device.poll(wgpu::PollType::Wait {
770            submission_index: None,
771            timeout: Some(std::time::Duration::from_secs(5)),
772        });
773        rx.recv().ok()?.ok()?;
774        let data = slice.get_mapped_range();
775        Some(bytemuck::pod_read_unaligned::<[f32; 4]>(&data))
776    }
777
778    /// Upload a Gaussian splat set to the GPU.
779    ///
780    /// Call once per splat set at startup or when it changes. The returned
781    /// [`GaussianSplatId`] is valid until [`remove_gaussian_splats`](Self::remove_gaussian_splats) is called.
782    ///
783    /// # Errors
784    ///
785    /// Returns [`ViewportError::InvalidGaussianSplatData`](crate::error::ViewportError::InvalidGaussianSplatData)
786    /// if `data.positions` is empty or if `positions`, `scales`, `rotations`, and `opacities`
787    /// differ in length.
788    ///
789    /// # Examples
790    ///
791    /// ```no_run
792    /// # use viewport_lib::error::ViewportError;
793    /// # use viewport_lib::renderer::{GaussianSplatData, ViewportRenderer};
794    /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
795    /// let result = renderer.upload_gaussian_splats(device, queue, &GaussianSplatData::default());
796    /// assert!(matches!(result, Err(ViewportError::InvalidGaussianSplatData { .. })));
797    /// # }
798    /// ```
799    pub fn upload_gaussian_splats(
800        &mut self,
801        device: &wgpu::Device,
802        queue: &wgpu::Queue,
803        data: &GaussianSplatData,
804    ) -> crate::error::ViewportResult<GaussianSplatId> {
805        self.resources.upload_gaussian_splats(device, queue, data)
806    }
807
808    /// Remove an uploaded Gaussian splat set by handle.
809    ///
810    /// After this call the `id` is invalid and must not be submitted in `SceneFrame`.
811    pub fn remove_gaussian_splats(&mut self, id: GaussianSplatId) {
812        self.resources.remove_gaussian_splats(id);
813    }
814
815    /// Upload an equirectangular HDR environment map and precompute IBL textures.
816    ///
817    /// `pixels` is row-major RGBA f32 data (4 floats per texel), `width`x`height`.
818    /// This rebuilds camera bind groups so shaders immediately see the new textures.
819    ///
820    /// # Errors
821    ///
822    /// Returns [`ViewportError::InvalidTextureData`](crate::error::ViewportError::InvalidTextureData)
823    /// if `pixels.len()` does not equal `width * height * 4`.
824    ///
825    /// # Examples
826    ///
827    /// ```no_run
828    /// # use viewport_lib::error::ViewportError;
829    /// # use viewport_lib::renderer::ViewportRenderer;
830    /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
831    /// // 2x2 RGBA image requires exactly 16 floats.
832    /// let result = renderer.upload_environment_map(device, queue, &[0.0f32; 12], 2, 2);
833    /// assert!(matches!(result, Err(ViewportError::InvalidTextureData { expected: 16, actual: 12 })));
834    /// # }
835    /// ```
836    pub fn upload_environment_map(
837        &mut self,
838        device: &wgpu::Device,
839        queue: &wgpu::Queue,
840        pixels: &[f32],
841        width: u32,
842        height: u32,
843    ) -> crate::error::ViewportResult<()> {
844        crate::resources::environment::upload_environment_map(
845            &mut self.resources,
846            device,
847            queue,
848            pixels,
849            width,
850            height,
851        )?;
852        self.rebuild_camera_bind_groups(device);
853        Ok(())
854    }
855
856    /// Rebuild the primary + per-viewport camera bind groups.
857    ///
858    /// Call after IBL textures are uploaded so shaders see the new environment.
859    fn rebuild_camera_bind_groups(&mut self, device: &wgpu::Device) {
860        self.resources.camera_bind_group = self.resources.create_camera_bind_group(
861            device,
862            &self.resources.camera_uniform_buf,
863            &self.resources.clip_planes_uniform_buf,
864            &self.resources.shadow_info_buf,
865            &self.resources.clip_volume_uniform_buf,
866            &self.resources.debug_frag_sentinel_buf,
867            "camera_bind_group",
868        );
869
870        for slot in &mut self.viewport_slots {
871            let dbg_buf = slot
872                .debug_frag_buf
873                .as_ref()
874                .unwrap_or(&self.resources.debug_frag_sentinel_buf);
875            slot.camera_bind_group = self.resources.create_camera_bind_group(
876                device,
877                &slot.camera_buf,
878                &slot.clip_planes_buf,
879                &slot.shadow_info_buf,
880                &slot.clip_volume_buf,
881                dbg_buf,
882                "per_viewport_camera_bg",
883            );
884        }
885    }
886
887    /// Ensure a per-viewport slot exists for `viewport_index`.
888    ///
889    /// Creates a full `ViewportSlot` with independent uniform buffers for camera,
890    /// clip planes, clip volume, shadow info, and grid. The camera bind group
891    /// references this slot's per-viewport buffers plus shared scene-global
892    /// resources. Slots are created lazily and never destroyed.
893    fn ensure_viewport_slot(&mut self, device: &wgpu::Device, viewport_index: usize) {
894        while self.viewport_slots.len() <= viewport_index {
895            let camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
896                label: Some("vp_camera_buf"),
897                size: std::mem::size_of::<CameraUniform>() as u64,
898                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
899                mapped_at_creation: false,
900            });
901            let clip_planes_buf = device.create_buffer(&wgpu::BufferDescriptor {
902                label: Some("vp_clip_planes_buf"),
903                size: std::mem::size_of::<ClipPlanesUniform>() as u64,
904                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
905                mapped_at_creation: false,
906            });
907            let clip_volume_buf = device.create_buffer(&wgpu::BufferDescriptor {
908                label: Some("vp_clip_volume_buf"),
909                size: std::mem::size_of::<ClipVolumesUniform>() as u64,
910                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
911                mapped_at_creation: false,
912            });
913            let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
914                label: Some("vp_shadow_info_buf"),
915                size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
916                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
917                mapped_at_creation: false,
918            });
919            let grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
920                label: Some("vp_grid_buf"),
921                size: std::mem::size_of::<GridUniform>() as u64,
922                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
923                mapped_at_creation: false,
924            });
925
926            let camera_bind_group = self.resources.create_camera_bind_group(
927                device,
928                &camera_buf,
929                &clip_planes_buf,
930                &shadow_info_buf,
931                &clip_volume_buf,
932                &self.resources.debug_frag_sentinel_buf,
933                "per_viewport_camera_bg",
934            );
935
936            let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
937                label: Some("vp_grid_bind_group"),
938                layout: &self.resources.grid_bind_group_layout,
939                entries: &[wgpu::BindGroupEntry {
940                    binding: 0,
941                    resource: grid_buf.as_entire_binding(),
942                }],
943            });
944
945            // Per-viewport gizmo buffers (initial mesh: Translate, no hover, identity orientation).
946            let (gizmo_verts, gizmo_indices) = crate::interaction::gizmo::build_gizmo_mesh(
947                crate::interaction::gizmo::GizmoMode::Translate,
948                crate::interaction::gizmo::GizmoAxis::None,
949                glam::Quat::IDENTITY,
950            );
951            let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
952                label: Some("vp_gizmo_vertex_buf"),
953                size: (std::mem::size_of::<crate::resources::Vertex>() * gizmo_verts.len().max(1))
954                    as u64,
955                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
956                mapped_at_creation: true,
957            });
958            gizmo_vertex_buffer
959                .slice(..)
960                .get_mapped_range_mut()
961                .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
962            gizmo_vertex_buffer.unmap();
963            let gizmo_index_count = gizmo_indices.len() as u32;
964            let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
965                label: Some("vp_gizmo_index_buf"),
966                size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
967                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
968                mapped_at_creation: true,
969            });
970            gizmo_index_buffer
971                .slice(..)
972                .get_mapped_range_mut()
973                .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
974            gizmo_index_buffer.unmap();
975            let gizmo_uniform = crate::interaction::gizmo::GizmoUniform {
976                model: glam::Mat4::IDENTITY.to_cols_array_2d(),
977            };
978            let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
979                label: Some("vp_gizmo_uniform_buf"),
980                size: std::mem::size_of::<crate::interaction::gizmo::GizmoUniform>() as u64,
981                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
982                mapped_at_creation: true,
983            });
984            gizmo_uniform_buf
985                .slice(..)
986                .get_mapped_range_mut()
987                .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
988            gizmo_uniform_buf.unmap();
989            let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
990                label: Some("vp_gizmo_bind_group"),
991                layout: &self.resources.gizmo_bind_group_layout,
992                entries: &[wgpu::BindGroupEntry {
993                    binding: 0,
994                    resource: gizmo_uniform_buf.as_entire_binding(),
995                }],
996            });
997
998            // Per-viewport axes vertex buffer (2048 vertices = enough for all axes geometry).
999            let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1000                label: Some("vp_axes_vertex_buf"),
1001                size: (std::mem::size_of::<crate::widgets::axes_indicator::AxesVertex>() * 2048)
1002                    as u64,
1003                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1004                mapped_at_creation: false,
1005            });
1006
1007            self.viewport_slots.push(ViewportSlot {
1008                camera_buf,
1009                clip_planes_buf,
1010                clip_volume_buf,
1011                shadow_info_buf,
1012                grid_buf,
1013                camera_bind_group,
1014                grid_bind_group,
1015                hdr: None,
1016                debug_frag_buf: None,
1017                debug_frag_dims: (0, 0),
1018                outline_object_buffers: Vec::new(),
1019                splat_outline_buffers: Vec::new(),
1020                volume_outline_indices: Vec::new(),
1021                glyph_outline_indices: Vec::new(),
1022                tensor_glyph_outline_indices: Vec::new(),
1023                sprite_outline_indices: Vec::new(),
1024                raw_geom_outline_buffers: Vec::new(),
1025                screen_rect_outline_buffers: Vec::new(),
1026                implicit_outline_indices: Vec::new(),
1027                mc_outline_data: Vec::new(),
1028                streamtube_outline_items: Vec::new(),
1029                tube_outline_items: Vec::new(),
1030                ribbon_outline_items: Vec::new(),
1031                polyline_outline_indices: Vec::new(),
1032                xray_object_buffers: Vec::new(),
1033                constraint_line_buffers: Vec::new(),
1034                cap_buffers: Vec::new(),
1035                clip_plane_fill_buffers: Vec::new(),
1036                clip_plane_line_buffers: Vec::new(),
1037                axes_vertex_buffer,
1038                axes_vertex_count: 0,
1039                gizmo_uniform_buf,
1040                gizmo_bind_group,
1041                gizmo_vertex_buffer,
1042                gizmo_index_buffer,
1043                gizmo_index_count,
1044                sub_highlight: None,
1045                sub_highlight_generation: u64::MAX,
1046                dyn_res: None,
1047                hdr_callback: None,
1048            });
1049        }
1050    }
1051
1052    // -----------------------------------------------------------------------
1053    // Multi-viewport public API
1054    // -----------------------------------------------------------------------
1055
1056    /// Create a new viewport slot and return its handle.
1057    ///
1058    /// The returned [`ViewportId`] is stable for the lifetime of the renderer.
1059    /// Pass it to [`prepare_viewport`](Self::prepare_viewport),
1060    /// [`paint_viewport`](Self::paint_viewport), and
1061    /// [`render_viewport`](Self::render_viewport) each frame.
1062    ///
1063    /// Also set the viewport slot on the camera frame when building the
1064    /// [`FrameData`] for this viewport:
1065    /// ```rust,ignore
1066    /// let id = renderer.create_viewport(&device);
1067    /// let frame = FrameData {
1068    ///     camera: CameraFrame::from_camera(&cam, size).with_viewport_id(id),
1069    ///     ..Default::default()
1070    /// };
1071    /// ```
1072    pub fn create_viewport(&mut self, device: &wgpu::Device) -> ViewportId {
1073        let idx = self.viewport_slots.len();
1074        self.ensure_viewport_slot(device, idx);
1075        ViewportId(idx)
1076    }
1077
1078    /// Release the heavy GPU texture memory (HDR targets, OIT, bloom, SSAO) held
1079    /// by `id`.
1080    ///
1081    /// The slot index is not reclaimed : future calls with this `ViewportId` will
1082    /// lazily recreate the texture resources as needed.  This is useful when a
1083    /// viewport is hidden or minimised and you want to reduce VRAM pressure without
1084    /// invalidating the handle.
1085    pub fn destroy_viewport(&mut self, id: ViewportId) {
1086        if let Some(slot) = self.viewport_slots.get_mut(id.0) {
1087            slot.hdr = None;
1088        }
1089    }
1090
1091    /// Returns the owned-encoder rendering path.
1092    ///
1093    /// Use when you own the window loop and wgpu encoder (winit, raw wgpu).
1094    /// See [`OwnedPath`] for available methods.
1095    pub fn owned(&mut self) -> OwnedPath<'_> {
1096        OwnedPath { renderer: self }
1097    }
1098
1099    /// Returns the pass-based rendering path.
1100    ///
1101    /// Use when a framework provides you with a render pass (eframe, iced).
1102    /// See [`PassPath`] for available methods.
1103    pub fn pass(&mut self) -> PassPath<'_> {
1104        PassPath { renderer: self }
1105    }
1106
1107    /// Returns a read-only paint view for framework paint callbacks.
1108    ///
1109    /// Use this in callbacks where only a shared reference to the renderer is
1110    /// available (e.g. eframe's `CallbackTrait::paint` where `callback_resources`
1111    /// is `&CallbackResources`). Exposes only the paint methods, not prepare.
1112    pub fn pass_view(&self) -> PassView<'_> {
1113        PassView { renderer: self }
1114    }
1115
1116    /// Prepare shared scene data.  Call **once per frame**, before any
1117    /// [`prepare_viewport`](Self::prepare_viewport) calls.
1118    ///
1119    /// `frame` provides the scene content (`frame.scene`) and the primary camera
1120    /// used for shadow cascade framing (`frame.camera`).  In a multi-viewport
1121    /// setup use any one viewport's `FrameData` here : typically the perspective
1122    /// view : as the shadow framing reference.
1123    ///
1124    /// `scene_effects` carries the scene-global effects: lighting, environment
1125    /// map, and compute filters.  Obtain it by constructing [`SceneEffects`]
1126    /// directly or via [`EffectsFrame::split`].
1127    pub(crate) fn prepare_scene(
1128        &mut self,
1129        device: &wgpu::Device,
1130        queue: &wgpu::Queue,
1131        frame: &FrameData,
1132        scene_effects: &SceneEffects<'_>,
1133    ) {
1134        self.prepare_scene_internal(device, queue, frame, scene_effects);
1135    }
1136
1137    /// Prepare per-viewport GPU state (camera, clip planes, overlays, axes).
1138    ///
1139    /// Call once per viewport per frame, **after** [`prepare_scene`](Self::prepare_scene).
1140    ///
1141    /// `id` must have been obtained from [`create_viewport`](Self::create_viewport).
1142    /// `frame.camera.viewport_index` must equal the slot for `id`; use
1143    /// [`CameraFrame::with_viewport_id`] when building the frame.
1144    pub(crate) fn prepare_viewport(
1145        &mut self,
1146        device: &wgpu::Device,
1147        queue: &wgpu::Queue,
1148        id: ViewportId,
1149        frame: &FrameData,
1150    ) {
1151        debug_assert_eq!(
1152            frame.camera.viewport_index, id.0,
1153            "frame.camera.viewport_index ({}) must equal the ViewportId ({}); \
1154             use CameraFrame::with_viewport_id(id)",
1155            frame.camera.viewport_index, id.0,
1156        );
1157        let (_, viewport_fx) = frame.effects.split();
1158        self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1159    }
1160
1161    /// Issue draw calls for `id` into a render pass with any lifetime.
1162    ///
1163    /// Identical to [`paint_viewport`](Self::paint_viewport) but accepts a render pass with a
1164    /// non-`'static` lifetime, making it usable from winit, iced, or raw wgpu where the encoder
1165    /// creates its own render pass.
1166    pub(crate) fn paint_viewport_to<'rp>(
1167        &self,
1168        render_pass: &mut wgpu::RenderPass<'rp>,
1169        id: ViewportId,
1170        frame: &FrameData,
1171    ) {
1172        let vp_idx = id.0;
1173        let camera_bg = self.viewport_camera_bind_group(vp_idx);
1174        let grid_bg = self.viewport_grid_bind_group(vp_idx);
1175        let vp_slot = self.viewport_slots.get(vp_idx);
1176        emit_draw_calls!(
1177            &self.resources,
1178            &mut *render_pass,
1179            frame,
1180            self.use_instancing,
1181            &self.instanced_batches,
1182            camera_bg,
1183            grid_bg,
1184            &self.compute_filter_results,
1185            vp_slot,
1186            &self.wireframe_bind_groups
1187        );
1188        emit_scivis_draw_calls!(
1189            &self.resources,
1190            &mut *render_pass,
1191            &self.point_cloud_gpu_data,
1192            &self.glyph_gpu_data,
1193            &self.polyline_gpu_data,
1194            &self.volume_gpu_data,
1195            &self.streamtube_gpu_data,
1196            camera_bg,
1197            &self.tube_gpu_data,
1198            &self.image_slice_gpu_data,
1199            &self.tensor_glyph_gpu_data,
1200            &self.ribbon_gpu_data,
1201            &self.volume_surface_slice_gpu_data,
1202            &self.sprite_gpu_data,
1203            false
1204        );
1205        // Gaussian splats (alpha-blended, back-to-front sorted, no depth write).
1206        if !self.gaussian_splat_draw_data.is_empty() {
1207            if let Some(ref dual) = self.resources.gaussian_splat_pipeline {
1208                render_pass.set_pipeline(dual.for_format(false));
1209                render_pass.set_bind_group(0, camera_bg, &[]);
1210                for dd in &self.gaussian_splat_draw_data {
1211                    if dd.wireframe {
1212                        continue;
1213                    }
1214                    if let Some(set) = self.resources.gaussian_splat_store.get(dd.store_index) {
1215                        if let Some(Some(vp_sort)) = set.viewport_sort.get(dd.viewport_index) {
1216                            render_pass.set_bind_group(1, &vp_sort.render_bg, &[]);
1217                            render_pass.draw(0..6, 0..dd.count);
1218                        }
1219                    }
1220                }
1221            }
1222        }
1223        // TransparentVolumeMesh boundary wireframe overlay.
1224        if !self.tvm_wireframe_draws.is_empty() {
1225            if let Some(ref tvm_bg) = self.tvm_wireframe_bg {
1226                render_pass.set_bind_group(0, camera_bg, &[]);
1227                for mesh_id in &self.tvm_wireframe_draws {
1228                    if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
1229                        render_pass.set_pipeline(&self.resources.wireframe_pipeline);
1230                        render_pass.set_bind_group(1, tvm_bg, &[]);
1231                        render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1232                        render_pass.set_index_buffer(
1233                            mesh.edge_index_buffer.slice(..),
1234                            wgpu::IndexFormat::Uint32,
1235                        );
1236                        render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1237                    }
1238                }
1239            }
1240        }
1241        // Shadow atlas viewer overlay.
1242        if frame.effects.show_shadow_atlas {
1243            render_pass.set_pipeline(&self.resources.shadow_atlas_viewer_pipeline);
1244            render_pass.set_bind_group(0, &self.resources.shadow_atlas_viewer_bg, &[]);
1245            render_pass.draw(0..6, 0..1);
1246        }
1247    }
1248
1249    /// Return a reference to the camera bind group for the given viewport slot.
1250    ///
1251    /// Falls back to `resources.camera_bind_group` if no per-viewport slot
1252    /// exists (e.g. in single-viewport mode before the first prepare call).
1253    fn viewport_camera_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
1254        self.viewport_slots
1255            .get(viewport_index)
1256            .map(|slot| &slot.camera_bind_group)
1257            .unwrap_or(&self.resources.camera_bind_group)
1258    }
1259
1260    /// Return a reference to the grid bind group for the given viewport slot.
1261    ///
1262    /// Falls back to `resources.grid_bind_group` if no per-viewport slot exists.
1263    fn viewport_grid_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
1264        self.viewport_slots
1265            .get(viewport_index)
1266            .map(|slot| &slot.grid_bind_group)
1267            .unwrap_or(&self.resources.grid_bind_group)
1268    }
1269
1270    /// Ensure the dyn-res intermediate render target exists for `vp_idx` at the
1271    /// given `scaled_size`, creating or recreating it when size changes.
1272    ///
1273    /// `surface_size` is the native output dimensions (used to size the upscale
1274    /// blit correctly). `ensure_dyn_res_pipeline` is called automatically.
1275    pub(crate) fn ensure_dyn_res_target(
1276        &mut self,
1277        device: &wgpu::Device,
1278        vp_idx: usize,
1279        scaled_size: [u32; 2],
1280        surface_size: [u32; 2],
1281    ) {
1282        self.resources.ensure_dyn_res_pipeline(device);
1283        let needs_create = match &self.viewport_slots[vp_idx].dyn_res {
1284            None => true,
1285            Some(dr) => dr.scaled_size != scaled_size || dr.surface_size != surface_size,
1286        };
1287        if needs_create {
1288            let target = self
1289                .resources
1290                .create_dyn_res_target(device, scaled_size, surface_size);
1291            self.viewport_slots[vp_idx].dyn_res = Some(target);
1292        }
1293    }
1294
1295    /// Ensure per-viewport HDR state exists for `viewport_index` at dimensions `w`×`h`.
1296    ///
1297    /// Calls `ensure_hdr_shared` once to initialise shared pipelines/BGLs/samplers, then
1298    /// lazily creates or resizes the `ViewportHdrState` inside the slot. Idempotent: if the
1299    /// slot already has HDR state at the correct size nothing is recreated.
1300    pub(crate) fn ensure_viewport_hdr(
1301        &mut self,
1302        device: &wgpu::Device,
1303        queue: &wgpu::Queue,
1304        viewport_index: usize,
1305        w: u32,
1306        h: u32,
1307        ssaa_factor: u32,
1308        render_scale: f32,
1309    ) {
1310        let format = self.resources.target_format;
1311        // Ensure shared infrastructure (pipelines, BGLs, samplers) exists.
1312        self.resources.ensure_hdr_shared(device, queue, format);
1313        // When render_scale < 1.0, the HDR upscale path needs the dyn_res
1314        // pipeline and sampler for the final upscale-blit to output resolution.
1315        if render_scale < 1.0 - 0.001 {
1316            self.resources.ensure_dyn_res_pipeline(device);
1317        }
1318        // Compute the scene-resolution render target size.
1319        let scale = render_scale.clamp(0.1, 1.0);
1320        let scene_w = ((w as f32) * scale).round() as u32;
1321        let scene_h = ((h as f32) * scale).round() as u32;
1322        // Ensure the slot exists.
1323        self.ensure_viewport_slot(device, viewport_index);
1324        let slot = &mut self.viewport_slots[viewport_index];
1325        // Create or resize the per-viewport HDR state.
1326        let needs_create = match &slot.hdr {
1327            None => true,
1328            Some(s) => {
1329                s.output_size != [w, h]
1330                    || s.scene_size != [scene_w.max(1), scene_h.max(1)]
1331                    || s.ssaa_factor != ssaa_factor
1332            }
1333        };
1334        if needs_create {
1335            slot.hdr = Some(self.resources.create_hdr_viewport_state(
1336                device,
1337                queue,
1338                format,
1339                w,
1340                h,
1341                scene_w.max(1),
1342                scene_h.max(1),
1343                ssaa_factor,
1344            ));
1345        }
1346    }
1347}