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;
19
20pub use self::types::{
21    CameraFrame, CameraFrustumItem, ClipObject, ClipShape, ComputeFilterItem, ComputeFilterKind,
22    EffectsFrame, EnvironmentMap, FilterMode, FrameData, GaussianSplatData, GaussianSplatId,
23    GaussianSplatItem, GlyphItem, GlyphType, GroundPlane, GroundPlaneMode, ImageAnchor,
24    ImageSliceItem, InteractionFrame, LabelAnchor, LabelItem, LightKind, LightSource,
25    LightingSettings, LoadingBarAnchor, LoadingBarItem, OverlayFrame, OverlayImageItem,
26    OverlayAnimation, OverlayFill, OverlayRectItem, OverlayShape, OverlayShapeItem,
27    OverlayTextureId, BorderMode, LineCap, PickId, TriangleDirection,
28    PointCloudItem, PointRenderMode, PolylineItem, PostProcessSettings, RenderCamera, RibbonItem,
29    RulerItem, ScalarBarAnchor, ScalarBarItem, ScalarBarOrientation, SceneEffects, SceneFrame,
30    SceneRenderItem, ScreenImageItem, ShDegree, ShadowFilter, SliceAxis, SpriteItem,
31    SpriteSizeMode, StreamtubeItem, SurfaceLICConfig, SurfaceLICItem, SurfaceSubmission,
32    TensorGlyphItem, ToneMapping, TransparentVolumeMeshItem, TubeItem, ViewportEffects,
33    ViewportFrame, VolumeItem, VolumeMeshItem, VolumeSurfaceSliceItem, aabb_wireframe_polyline,
34};
35
36/// An opaque handle to a per-viewport GPU state slot.
37///
38/// Obtained from [`ViewportRenderer::create_viewport`] and passed to
39/// [`ViewportRenderer::prepare_viewport`], [`ViewportRenderer::paint_viewport`],
40/// and [`ViewportRenderer::render_viewport`].
41///
42/// The inner `usize` is the slot index and doubles as the value for
43/// [`CameraFrame::with_viewport_index`].  Single-viewport applications that use
44/// the legacy [`ViewportRenderer::prepare`] / [`ViewportRenderer::paint`] API do
45/// not need this type.
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub struct ViewportId(pub usize);
48
49use self::shadows::{compute_cascade_matrix, compute_cascade_splits};
50use self::types::{INSTANCING_THRESHOLD, InstancedBatch};
51use crate::resources::{
52    BatchMeta, CLIP_VOLUME_MAX, CameraUniform, ClipPlanesUniform, ClipVolumeEntry,
53    ClipVolumesUniform, GridUniform, InstanceAabb, InstanceData, LightsUniform, ObjectUniform,
54    OutlineEdgeUniform, OutlineObjectBuffers, OutlineUniform, PickInstance, ShadowAtlasUniform,
55    SingleLightUniform, SplatOutlineMaskUniform, ViewportGpuResources,
56};
57
58/// Per-viewport GPU state: uniform buffers and bind groups that differ per viewport.
59///
60/// Each viewport slot owns its own camera, clip planes, clip volume, shadow info,
61/// and grid buffers, plus the bind groups that reference them. Scene-global
62/// resources (lights, shadow atlas texture, IBL) are shared via the bind group
63/// pointing to buffers on `ViewportGpuResources`.
64pub(crate) struct ViewportSlot {
65    pub camera_buf: wgpu::Buffer,
66    pub clip_planes_buf: wgpu::Buffer,
67    pub clip_volume_buf: wgpu::Buffer,
68    pub shadow_info_buf: wgpu::Buffer,
69    pub grid_buf: wgpu::Buffer,
70    /// Camera bind group (group 0) referencing this slot's per-viewport buffers
71    /// plus shared scene-global resources.
72    pub camera_bind_group: wgpu::BindGroup,
73    /// Grid bind group (group 0 for grid pipeline) referencing this slot's grid buffer.
74    pub grid_bind_group: wgpu::BindGroup,
75    /// Per-viewport HDR post-process render targets.
76    ///
77    /// Created lazily on first HDR render call and resized when viewport dimensions change.
78    pub hdr: Option<crate::resources::ViewportHdrState>,
79
80    // --- Per-viewport interaction state (Phase 4) ---
81    /// Per-frame outline buffers for selected objects, rebuilt in prepare().
82    pub outline_object_buffers: Vec<OutlineObjectBuffers>,
83    /// Per-frame outline buffers for selected Gaussian splat sets, rebuilt in prepare().
84    pub splat_outline_buffers: Vec<crate::resources::SplatOutlineBuffers>,
85    /// Indices into `volume_gpu_data` for selected volumes, rebuilt in prepare().
86    pub volume_outline_indices: Vec<usize>,
87    /// Indices into `glyph_gpu_data` for selected glyph sets, rebuilt in prepare().
88    /// Each entry is (gpu_data_index, instance_filter): None draws all instances,
89    /// Some(indices) draws only those specific instance indices.
90    pub glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
91    /// Indices into `tensor_glyph_gpu_data` for selected tensor glyph sets, rebuilt in prepare().
92    pub tensor_glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
93    /// Indices into `sprite_gpu_data` for selected sprite sets, rebuilt in prepare().
94    pub sprite_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
95    /// Per-frame inline quad outline buffers for selected image slices, rebuilt in prepare().
96    pub raw_geom_outline_buffers: Vec<crate::resources::RawGeomOutlineBuffers>,
97    /// Per-frame NDC rect outline buffers for selected screen images, rebuilt in prepare().
98    pub screen_rect_outline_buffers: Vec<crate::resources::ScreenRectOutlineBuffers>,
99    /// Indices into `implicit_gpu_data` for selected GPU implicit items, rebuilt in prepare().
100    pub implicit_outline_indices: Vec<usize>,
101    /// Per-frame outline data for selected GPU marching cubes jobs, rebuilt in prepare().
102    pub mc_outline_data: Vec<crate::resources::gpu_marching_cubes::McOutlineItem>,
103    /// Outline items for selected streamtubes (index into streamtube_gpu_data + mask bind group).
104    pub streamtube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
105    /// Outline items for selected tubes.
106    pub tube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
107    /// Outline items for selected ribbons.
108    pub ribbon_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
109    /// Indices into polyline_gpu_data for selected user polylines.
110    pub polyline_outline_indices: Vec<usize>,
111    /// Per-frame x-ray buffers for selected objects, rebuilt in prepare().
112    pub xray_object_buffers: Vec<(
113        crate::resources::mesh_store::MeshId,
114        wgpu::Buffer,
115        wgpu::BindGroup,
116    )>,
117    /// Per-frame constraint guide line buffers, rebuilt in prepare().
118    pub constraint_line_buffers: Vec<(
119        wgpu::Buffer,
120        wgpu::Buffer,
121        u32,
122        wgpu::Buffer,
123        wgpu::BindGroup,
124    )>,
125    /// Per-frame cap geometry buffers (section view cross-section fill), rebuilt in prepare().
126    pub cap_buffers: Vec<(
127        wgpu::Buffer,
128        wgpu::Buffer,
129        u32,
130        wgpu::Buffer,
131        wgpu::BindGroup,
132    )>,
133    /// Per-frame clip plane fill overlay buffers, rebuilt in prepare().
134    pub clip_plane_fill_buffers: Vec<(
135        wgpu::Buffer,
136        wgpu::Buffer,
137        u32,
138        wgpu::Buffer,
139        wgpu::BindGroup,
140    )>,
141    /// Per-frame clip plane line overlay buffers, rebuilt in prepare().
142    pub clip_plane_line_buffers: Vec<(
143        wgpu::Buffer,
144        wgpu::Buffer,
145        u32,
146        wgpu::Buffer,
147        wgpu::BindGroup,
148    )>,
149    /// Vertex buffer for axes indicator geometry (rebuilt each frame).
150    pub axes_vertex_buffer: wgpu::Buffer,
151    /// Number of vertices in the axes indicator buffer.
152    pub axes_vertex_count: u32,
153    /// Gizmo model-matrix uniform buffer.
154    pub gizmo_uniform_buf: wgpu::Buffer,
155    /// Gizmo bind group (group 1: model matrix uniform).
156    pub gizmo_bind_group: wgpu::BindGroup,
157    /// Gizmo vertex buffer.
158    pub gizmo_vertex_buffer: wgpu::Buffer,
159    /// Gizmo index buffer.
160    pub gizmo_index_buffer: wgpu::Buffer,
161    /// Number of indices in the current gizmo mesh.
162    pub gizmo_index_count: u32,
163
164    // --- Sub-object highlight (per-viewport, generation-cached) ---
165    /// Per-viewport dynamic resolution intermediate render target.
166    /// `None` when render_scale == 1.0 or not yet initialised.
167    pub dyn_res: Option<crate::resources::dyn_res::DynResTarget>,
168    /// Per-viewport intermediate render target for the HDR eframe callback path.
169    /// `None` until the first `prepare_hdr_callback` call for this viewport.
170    pub hdr_callback: Option<crate::resources::dyn_res::HdrCallbackTarget>,
171    /// Cached GPU data for sub-object highlight rendering.
172    /// `None` when no sub-object selection is active and no volumes are selected.
173    pub sub_highlight: Option<crate::resources::SubHighlightGpuData>,
174    /// Version of the last sub-selection snapshot that was uploaded.
175    /// `u64::MAX` forces a rebuild on the first frame.
176    pub sub_highlight_generation: u64,
177}
178
179/// Retained pick state for one GPU implicit surface, built during `prepare()`.
180struct GpuImplicitPickItem {
181    id: u64,
182    primitives: Vec<crate::resources::ImplicitPrimitive>,
183    blend_mode: crate::resources::ImplicitBlendMode,
184    max_steps: u32,
185    step_scale: f32,
186    hit_threshold: f32,
187    max_distance: f32,
188}
189
190/// Retained pick state for one GPU marching cubes job, built during `prepare()`.
191struct GpuMcPickItem {
192    id: u64,
193    isovalue: f32,
194    volume_data: std::sync::Arc<crate::geometry::marching_cubes::VolumeData>,
195}
196
197/// Renderer wrapping all GPU resources and providing `prepare()` and `paint()` methods.
198pub struct ViewportRenderer {
199    resources: ViewportGpuResources,
200    /// Instanced batches prepared for the current frame. Empty when using per-object path.
201    instanced_batches: Vec<InstancedBatch>,
202    /// Whether the current frame uses the instanced draw path.
203    use_instancing: bool,
204    /// True when the device supports `INDIRECT_FIRST_INSTANCE`.
205    gpu_culling_supported: bool,
206    /// True when GPU-driven culling is active (supported and not disabled by the caller).
207    gpu_culling_enabled: bool,
208    /// GPU culling compute pipelines and frustum buffer. Created lazily on the first
209    /// frame where `gpu_culling_enabled` is true and instance buffers are present.
210    cull_resources: Option<indirect::CullResources>,
211    /// Performance counters from the last frame.
212    last_stats: crate::renderer::stats::FrameStats,
213    /// Last scene generation seen during prepare(). u64::MAX forces rebuild on first frame.
214    last_scene_generation: u64,
215    /// Last selection generation seen during prepare(). u64::MAX forces rebuild on first frame.
216    last_selection_generation: u64,
217    /// Last scene_items count seen during prepare(). usize::MAX forces rebuild on first frame.
218    /// Included in cache key so that frustum-culling changes (different visible set, different
219    /// count) correctly invalidate the instance buffer even when scene_generation is stable.
220    last_scene_items_count: usize,
221    /// Count of items that passed the instanced-path filter on the last rebuild.
222    /// Used in place of has_per_frame_mutations so scenes that mix instanced and
223    /// non-instanced items (e.g. one two-sided mesh + 10k static boxes) still hit
224    /// the instanced batch cache on frames where the filtered set is unchanged.
225    last_instancable_count: usize,
226    /// Total instance count from the last rebuild. Used as a fast length check
227    /// in `structure_preserved` and as `instance_count` for GPU cull dispatches.
228    cached_instance_count: usize,
229    /// Per-batch content hash from the last rebuild, indexed by batch position.
230    /// A hash mismatch triggers a `write_buffer` for that batch; a match skips it.
231    cached_instance_hashes: Vec<u64>,
232    /// Cached instanced batch descriptors from last rebuild.
233    cached_instanced_batches: Vec<InstancedBatch>,
234    /// When true, the next cache-miss forces a full buffer upload instead of the
235    /// per-batch partial-upload path. Set by `force_dirty()` and consumed once.
236    force_full_upload: bool,
237    /// Per-frame point cloud GPU data, rebuilt in prepare(), consumed in paint().
238    point_cloud_gpu_data: Vec<crate::resources::PointCloudGpuData>,
239    /// Per-frame glyph GPU data, rebuilt in prepare(), consumed in paint().
240    glyph_gpu_data: Vec<crate::resources::GlyphGpuData>,
241    /// Per-frame tensor glyph GPU data, rebuilt in prepare(), consumed in paint() (Phase 5).
242    tensor_glyph_gpu_data: Vec<crate::resources::TensorGlyphGpuData>,
243    /// Per-frame polyline GPU data, rebuilt in prepare(), consumed in paint().
244    polyline_gpu_data: Vec<crate::resources::PolylineGpuData>,
245    /// Per-frame volume GPU data, rebuilt in prepare(), consumed in paint().
246    volume_gpu_data: Vec<crate::resources::VolumeGpuData>,
247    /// Per-frame streamtube GPU data, rebuilt in prepare(), consumed in paint().
248    streamtube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
249    /// Per-frame general tube GPU data, rebuilt in prepare(), consumed in paint() (Phase 3).
250    tube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
251    /// Per-frame ribbon GPU data, rebuilt in prepare(), consumed in paint() (Phase 8.1).
252    ribbon_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
253    /// Indices into streamtube_gpu_data for selected streamtubes (set in prepare_scene, consumed in prepare_viewport).
254    streamtube_selected_gpu_indices: Vec<usize>,
255    /// Indices into tube_gpu_data for selected tubes (set in prepare_scene, consumed in prepare_viewport).
256    tube_selected_gpu_indices: Vec<usize>,
257    /// Indices into ribbon_gpu_data for selected ribbons (set in prepare_scene, consumed in prepare_viewport).
258    ribbon_selected_gpu_indices: Vec<usize>,
259    /// Indices into polyline_gpu_data for selected user polylines (set in prepare_scene, consumed in prepare_viewport).
260    polyline_selected_gpu_indices: Vec<usize>,
261    /// Per-frame image slice GPU data, rebuilt in prepare(), consumed in paint() (Phase 3).
262    image_slice_gpu_data: Vec<crate::resources::ImageSliceGpuData>,
263    /// Per-frame volume surface slice GPU data, rebuilt in prepare(), consumed in paint() (Phase 10).
264    volume_surface_slice_gpu_data: Vec<crate::resources::VolumeSurfaceSliceGpuData>,
265    /// Per-frame Surface LIC GPU data, rebuilt in prepare(), consumed in paint() (Phase 4).
266    lic_gpu_data: Vec<crate::resources::LicSurfaceGpuData>,
267    /// Per-frame GPU implicit surface data, rebuilt in prepare(), consumed in paint() (Phase 16).
268    implicit_gpu_data: Vec<crate::resources::implicit::ImplicitGpuItem>,
269    /// Per-frame GPU marching cubes render data, rebuilt in prepare(), consumed in paint() (Phase 17).
270    mc_gpu_data: Vec<crate::resources::gpu_marching_cubes::McFrameData>,
271    /// Per-frame sprite GPU data, rebuilt in prepare(), consumed in paint().
272    sprite_gpu_data: Vec<crate::resources::SpriteGpuData>,
273    /// Per-frame Gaussian splat draw data, rebuilt in prepare_viewport_internal(), consumed in paint().
274    gaussian_splat_draw_data: Vec<crate::resources::GaussianSplatDrawData>,
275    /// Per-frame screen-image GPU data, rebuilt in prepare(), consumed in paint() (Phase 10B).
276    screen_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
277    /// Per-frame overlay image GPU data, rebuilt in prepare(), consumed in paint() (Phase 7).
278    overlay_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
279    /// Per-frame overlay label GPU data, rebuilt in prepare(), consumed in paint().
280    label_gpu_data: Option<crate::resources::LabelGpuData>,
281    /// Per-frame scalar bar GPU data, rebuilt in prepare(), consumed in paint().
282    scalar_bar_gpu_data: Option<crate::resources::LabelGpuData>,
283    /// Per-frame ruler GPU data, rebuilt in prepare(), consumed in paint().
284    ruler_gpu_data: Option<crate::resources::LabelGpuData>,
285    /// Per-frame loading bar GPU data, rebuilt in prepare(), consumed in paint().
286    loading_bar_gpu_data: Option<crate::resources::LabelGpuData>,
287    /// Per-frame overlay rect GPU data, rebuilt in prepare(), consumed in paint().
288    overlay_rect_gpu_data: Option<crate::resources::LabelGpuData>,
289    /// Per-frame SDF overlay shape GPU data, rebuilt in prepare(), consumed in paint().
290    overlay_shape_gpu_data: Option<crate::resources::OverlayShapeGpuData>,
291    /// Cached GPU textures for the backdrop blur effect (frosted glass).
292    /// Recreated when the viewport size changes.
293    backdrop_blur_state: Option<crate::resources::BackdropBlurState>,
294    /// Per-viewport GPU state slots.
295    ///
296    /// Indexed by `FrameData::camera.viewport_index`. Each slot owns independent
297    /// uniform buffers and bind groups for camera, clip planes, clip volume,
298    /// shadow info, and grid. Slots are grown lazily in `prepare` via
299    /// `ensure_viewport_slot`. There are at most 4 in the current UI.
300    viewport_slots: Vec<ViewportSlot>,
301    /// Phase G : GPU compute filter results from the last `prepare()` call.
302    ///
303    /// Each entry contains a compacted index buffer + count for one filtered mesh.
304    /// Consumed during `paint()` to override the mesh's default index buffer.
305    /// Cleared and rebuilt each frame.
306    compute_filter_results: Vec<crate::resources::ComputeFilterResult>,
307    /// Per-item uniform buffers for wireframe mode. In wireframe mode multiple scene
308    /// items can share the same MeshId, but each needs its own object uniform (model
309    /// matrix, colour, etc.). The mesh's single `object_uniform_buf` gets overwritten
310    /// by the last item prepared, so we maintain a separate pool here. Indexed in the
311    /// same order as the visible scene items. Grown lazily, never shrunk.
312    wireframe_uniform_bufs: Vec<wgpu::Buffer>,
313    /// Bind groups corresponding to `wireframe_uniform_bufs`. Each bind group pairs
314    /// the per-item uniform buffer with the mesh's fallback textures so it is
315    /// compatible with the object bind group layout.
316    wireframe_bind_groups: Vec<wgpu::BindGroup>,
317    /// Per-frame list of boundary mesh IDs to draw in wireframe for
318    /// TransparentVolumeMeshItems with `appearance.wireframe = true`.
319    /// Cleared and rebuilt each frame in prepare_scene_internal.
320    tvm_wireframe_draws: Vec<crate::resources::mesh_store::MeshId>,
321    /// Shared uniform buffer for TVM boundary wireframe draws (wireframe=1, model=identity).
322    /// Created once on first use, reused every frame.
323    tvm_wireframe_buf: Option<wgpu::Buffer>,
324    /// Bind group for TVM boundary wireframe draws. Pairs `tvm_wireframe_buf` with
325    /// fallback textures matching the object bind group layout.
326    tvm_wireframe_bg: Option<wgpu::BindGroup>,
327    /// Cascade-0 light-space view-projection matrix from the last shadow prepare.
328    /// Cached here so `prepare_viewport_internal` can copy it into the ground plane uniform.
329    last_cascade0_shadow_mat: glam::Mat4,
330    /// Current runtime mode controlling internal default behavior.
331    runtime_mode: crate::renderer::stats::RuntimeMode,
332    /// Active performance policy: target FPS, render scale bounds, and permitted reductions.
333    performance_policy: crate::renderer::stats::PerformancePolicy,
334    /// Current render scale tracked by the adaptation controller (or set manually).
335    ///
336    /// Clamped to `[policy.min_render_scale, policy.max_render_scale]`.
337    /// Reported in `FrameStats::render_scale` each frame.
338    current_render_scale: f32,
339    /// Instant recorded at the start of the most recent `prepare()` call.
340    /// Used to compute `total_frame_ms` on the following frame.
341    last_prepare_instant: Option<std::time::Instant>,
342    /// Frame counter incremented each `prepare()` call. Used for picking throttle in Playback mode.
343    frame_counter: u64,
344    /// Surface items from the last `prepare()` call, retained for `pick()` dispatch.
345    pick_scene_items: Vec<SceneRenderItem>,
346    /// Point cloud items from the last `prepare()` call, retained for `pick()` dispatch.
347    pick_point_cloud_items: Vec<PointCloudItem>,
348    /// Gaussian splat items from the last `prepare()` call, retained for `pick()` dispatch.
349    pick_splat_items: Vec<GaussianSplatItem>,
350    /// Volume items from the last `prepare()` call, retained for `pick()` dispatch.
351    pick_volume_items: Vec<VolumeItem>,
352    /// Transparent volume mesh items from the last `prepare()` call, retained for `pick()` dispatch.
353    pick_tvm_items: Vec<TransparentVolumeMeshItem>,
354    /// Opaque volume mesh items from the last `prepare()` call, retained for cell-level `pick()` dispatch.
355    pick_volume_mesh_items: Vec<VolumeMeshItem>,
356    /// Polyline items from the last `prepare()` call, retained for `pick()` dispatch.
357    pick_polyline_items: Vec<PolylineItem>,
358    /// Glyph items from the last `prepare()` call, retained for `pick()` dispatch.
359    pick_glyph_items: Vec<GlyphItem>,
360    /// Tensor glyph items from the last `prepare()` call, retained for `pick()` dispatch.
361    pick_tensor_glyph_items: Vec<TensorGlyphItem>,
362    /// Sprite items from the last `prepare()` call, retained for `pick()` dispatch.
363    pick_sprite_items: Vec<SpriteItem>,
364    /// Streamtube items from the last `prepare()` call, retained for `pick()` dispatch.
365    pick_streamtube_items: Vec<StreamtubeItem>,
366    /// Tube items from the last `prepare()` call, retained for `pick()` dispatch.
367    pick_tube_items: Vec<TubeItem>,
368    /// Ribbon items from the last `prepare()` call, retained for `pick()` dispatch.
369    pick_ribbon_items: Vec<RibbonItem>,
370    /// Image slice items from the last `prepare()` call, retained for `pick()` dispatch.
371    pick_image_slice_items: Vec<ImageSliceItem>,
372    /// Volume surface slice items from the last `prepare()` call, retained for `pick()` dispatch.
373    pick_volume_surface_slice_items: Vec<VolumeSurfaceSliceItem>,
374    /// Screen image items from the last `prepare()` call, retained for `pick()` dispatch.
375    pick_screen_image_items: Vec<ScreenImageItem>,
376    /// GPU implicit surface items from the last `prepare()` call, retained for `pick()` dispatch.
377    pick_implicit_items: Vec<GpuImplicitPickItem>,
378    /// GPU marching cubes jobs from the last `prepare()` call, retained for `pick()` dispatch.
379    pick_mc_items: Vec<GpuMcPickItem>,
380
381    // --- Phase 4 : GPU timestamp queries ---
382    /// Timestamp query set with 2 entries (scene-pass begin + end).
383    /// `None` when `TIMESTAMP_QUERY` is unavailable or not yet initialized.
384    ts_query_set: Option<wgpu::QuerySet>,
385    /// Resolve buffer: 2 × u64, GPU-only (`QUERY_RESOLVE | COPY_SRC`).
386    ts_resolve_buf: Option<wgpu::Buffer>,
387    /// Staging buffer: 2 × u64, CPU-readable (`COPY_DST | MAP_READ`).
388    ts_staging_buf: Option<wgpu::Buffer>,
389    /// Nanoseconds per GPU timestamp tick, from `queue.get_timestamp_period()`.
390    ts_period: f32,
391    /// Whether the staging buffer holds unread timestamp data from the previous frame.
392    ts_needs_readback: bool,
393
394    // --- Indirect-args readback (GPU-driven culling visible instance count) ---
395    /// CPU-readable staging buffer for `indirect_args_buf` (batch_count × 20 bytes).
396    /// Grown lazily; never shrunk.
397    indirect_readback_buf: Option<wgpu::Buffer>,
398    /// Number of batches whose data was copied into `indirect_readback_buf` last frame.
399    indirect_readback_batch_count: u32,
400    /// True when `indirect_readback_buf` holds unread data from the previous cull pass.
401    indirect_readback_pending: bool,
402
403    // --- Per-pass degradation state (Phases 6 + 11) ---
404    /// Tiered degradation ladder position (0 = none, 1 = shadows, 2 = volumes, 3 = effects).
405    /// Advanced one step per over-budget frame once render scale hits minimum;
406    /// reversed one step per comfortably-under-budget frame.
407    degradation_tier: u8,
408    /// Whether the shadow pass was skipped this frame due to budget pressure.
409    /// Computed once per frame at the top of prepare() and used by both
410    /// prepare_scene_internal and reported in FrameStats.
411    degradation_shadows_skipped: bool,
412    /// Whether volume raymarch step size was doubled this frame due to budget pressure.
413    degradation_volume_quality_reduced: bool,
414    /// Whether SSAO, contact shadows, and bloom were skipped this frame.
415    /// Set in prepare(); read by the render path.
416    degradation_effects_throttled: bool,
417}
418
419impl ViewportRenderer {
420    /// Create a new renderer with default settings (no MSAA).
421    /// Call once at application startup.
422    pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
423        Self::with_sample_count(device, target_format, 1)
424    }
425
426    /// Create a new renderer with the specified MSAA sample count (1, 2, or 4).
427    ///
428    /// When using MSAA (sample_count > 1), the caller must create multisampled
429    /// colour and depth textures and use them as render pass attachments with the
430    /// final surface texture as the resolve target.
431    pub fn with_sample_count(
432        device: &wgpu::Device,
433        target_format: wgpu::TextureFormat,
434        sample_count: u32,
435    ) -> Self {
436        let gpu_culling_supported = device
437            .features()
438            .contains(wgpu::Features::INDIRECT_FIRST_INSTANCE);
439        Self {
440            resources: ViewportGpuResources::new(device, target_format, sample_count),
441            instanced_batches: Vec::new(),
442            use_instancing: false,
443            gpu_culling_supported,
444            gpu_culling_enabled: gpu_culling_supported,
445            cull_resources: None,
446            last_stats: crate::renderer::stats::FrameStats::default(),
447            last_scene_generation: u64::MAX,
448            last_selection_generation: u64::MAX,
449            last_scene_items_count: usize::MAX,
450            last_instancable_count: usize::MAX,
451            cached_instance_count: 0,
452            cached_instance_hashes: Vec::new(),
453            cached_instanced_batches: Vec::new(),
454            force_full_upload: false,
455            point_cloud_gpu_data: Vec::new(),
456            glyph_gpu_data: Vec::new(),
457            tensor_glyph_gpu_data: Vec::new(),
458            polyline_gpu_data: Vec::new(),
459            volume_gpu_data: Vec::new(),
460            streamtube_gpu_data: Vec::new(),
461            tube_gpu_data: Vec::new(),
462            ribbon_gpu_data: Vec::new(),
463            streamtube_selected_gpu_indices: Vec::new(),
464            tube_selected_gpu_indices: Vec::new(),
465            ribbon_selected_gpu_indices: Vec::new(),
466            polyline_selected_gpu_indices: Vec::new(),
467            image_slice_gpu_data: Vec::new(),
468            volume_surface_slice_gpu_data: Vec::new(),
469            sprite_gpu_data: Vec::new(),
470            gaussian_splat_draw_data: Vec::new(),
471            lic_gpu_data: Vec::new(),
472            implicit_gpu_data: Vec::new(),
473            mc_gpu_data: Vec::new(),
474            screen_image_gpu_data: Vec::new(),
475            overlay_image_gpu_data: Vec::new(),
476            label_gpu_data: None,
477            scalar_bar_gpu_data: None,
478            ruler_gpu_data: None,
479            loading_bar_gpu_data: None,
480            overlay_rect_gpu_data: None,
481            overlay_shape_gpu_data: None,
482            backdrop_blur_state: None,
483            viewport_slots: Vec::new(),
484            compute_filter_results: Vec::new(),
485            wireframe_uniform_bufs: Vec::new(),
486            wireframe_bind_groups: Vec::new(),
487            tvm_wireframe_draws: Vec::new(),
488            tvm_wireframe_buf: None,
489            tvm_wireframe_bg: None,
490            last_cascade0_shadow_mat: glam::Mat4::IDENTITY,
491            runtime_mode: crate::renderer::stats::RuntimeMode::Interactive,
492            performance_policy: crate::renderer::stats::PerformancePolicy::default(),
493            current_render_scale: 1.0,
494            last_prepare_instant: None,
495            frame_counter: 0,
496            pick_scene_items: Vec::new(),
497            pick_point_cloud_items: Vec::new(),
498            pick_splat_items: Vec::new(),
499            pick_volume_items: Vec::new(),
500            pick_tvm_items: Vec::new(),
501            pick_volume_mesh_items: Vec::new(),
502            pick_polyline_items: Vec::new(),
503            pick_glyph_items: Vec::new(),
504            pick_tensor_glyph_items: Vec::new(),
505            pick_sprite_items: Vec::new(),
506            pick_streamtube_items: Vec::new(),
507            pick_tube_items: Vec::new(),
508            pick_ribbon_items: Vec::new(),
509            pick_image_slice_items: Vec::new(),
510            pick_volume_surface_slice_items: Vec::new(),
511            pick_screen_image_items: Vec::new(),
512            pick_implicit_items: Vec::new(),
513            pick_mc_items: Vec::new(),
514            ts_query_set: None,
515            ts_resolve_buf: None,
516            ts_staging_buf: None,
517            ts_period: 1.0,
518            ts_needs_readback: false,
519            indirect_readback_buf: None,
520            indirect_readback_batch_count: 0,
521            indirect_readback_pending: false,
522            degradation_tier: 0,
523            degradation_shadows_skipped: false,
524            degradation_volume_quality_reduced: false,
525            degradation_effects_throttled: false,
526        }
527    }
528
529    /// Access the underlying GPU resources (e.g. for mesh uploads).
530    pub fn resources(&self) -> &ViewportGpuResources {
531        &self.resources
532    }
533
534    /// Performance counters from the last completed frame.
535    pub fn last_frame_stats(&self) -> crate::renderer::stats::FrameStats {
536        self.last_stats
537    }
538
539    /// Disable GPU-driven culling, reverting to the direct draw path.
540    ///
541    /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`
542    /// (culling is already disabled on those devices).
543    pub fn disable_gpu_driven_culling(&mut self) {
544        self.gpu_culling_enabled = false;
545    }
546
547    /// Force a full instance buffer upload on the next frame.
548    ///
549    /// Normally the renderer skips GPU writes for instanced batches whose data
550    /// has not changed since the last upload. Call this when you have mutated
551    /// batch-relevant state through a path the renderer cannot observe (for
552    /// example, directly modifying GPU buffer contents or scene items after
553    /// `collect_render_items` runs). The flag is consumed once and resets
554    /// automatically after the next `prepare` call.
555    pub fn force_dirty(&mut self) {
556        self.force_full_upload = true;
557        // Also invalidate the generation cache so the next prepare is guaranteed
558        // to enter the rebuild path even if the scene generation is unchanged.
559        self.last_scene_generation = u64::MAX;
560    }
561
562    /// Re-enable GPU-driven culling after a call to `disable_gpu_driven_culling`.
563    ///
564    /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`.
565    pub fn enable_gpu_driven_culling(&mut self) {
566        if self.gpu_culling_supported {
567            self.gpu_culling_enabled = true;
568        }
569    }
570
571    /// Set the runtime mode controlling internal default behavior.
572    ///
573    /// - [`RuntimeMode::Interactive`]: full picking rate, full quality (default).
574    /// - [`RuntimeMode::Playback`]: picking throttled to reduce CPU overhead during animation.
575    /// - [`RuntimeMode::Paused`]: full picking rate, full quality.
576    /// - [`RuntimeMode::Capture`]: full quality, intended for screenshot/export workflows.
577    pub fn set_runtime_mode(&mut self, mode: crate::renderer::stats::RuntimeMode) {
578        self.runtime_mode = mode;
579    }
580
581    /// Return the current runtime mode.
582    pub fn runtime_mode(&self) -> crate::renderer::stats::RuntimeMode {
583        self.runtime_mode
584    }
585
586    /// Set the performance policy controlling target FPS, render scale bounds,
587    /// and permitted quality reductions.
588    ///
589    /// The internal adaptation controller activates when
590    /// `policy.allow_dynamic_resolution` is `true` and `policy.target_fps` is
591    /// `Some`. It adjusts `render_scale` within `[min_render_scale,
592    /// max_render_scale]` each frame based on `total_frame_ms`.
593    pub fn set_performance_policy(&mut self, policy: crate::renderer::stats::PerformancePolicy) {
594        self.performance_policy = policy;
595        // Clamp current scale into the new bounds immediately.
596        self.current_render_scale = self
597            .current_render_scale
598            .clamp(policy.min_render_scale, policy.max_render_scale);
599    }
600
601    /// Return the active performance policy.
602    pub fn performance_policy(&self) -> crate::renderer::stats::PerformancePolicy {
603        self.performance_policy
604    }
605
606    /// Manually set the render scale.
607    ///
608    /// Effective when `performance_policy.allow_dynamic_resolution` is `false`.
609    /// When dynamic resolution is enabled the adaptation controller overrides
610    /// this value each frame.
611    ///
612    /// The value is clamped to `[policy.min_render_scale, policy.max_render_scale]`.
613    ///
614    /// Works on both the LDR and HDR render paths. On the HDR path, the scene,
615    /// bloom, SSAO, tone-map, and FXAA all run at the scaled resolution; the
616    /// result is upscale-blitted to native resolution before overlays and grid.
617    pub fn set_render_scale(&mut self, scale: f32) {
618        self.current_render_scale = scale.clamp(
619            self.performance_policy.min_render_scale,
620            self.performance_policy.max_render_scale,
621        );
622    }
623
624    /// Set the target frame rate used to compute [`FrameStats::missed_budget`].
625    ///
626    /// Convenience wrapper that updates `performance_policy.target_fps`.
627    pub fn set_target_fps(&mut self, fps: Option<f32>) {
628        self.performance_policy.target_fps = fps;
629    }
630
631    /// Mutable access to the underlying GPU resources (e.g. for mesh uploads).
632    pub fn resources_mut(&mut self) -> &mut ViewportGpuResources {
633        &mut self.resources
634    }
635
636    /// Upload a Gaussian splat set to the GPU.
637    ///
638    /// Call once per splat set at startup or when it changes. The returned
639    /// [`GaussianSplatId`] is valid until [`remove_gaussian_splats`](Self::remove_gaussian_splats) is called.
640    pub fn upload_gaussian_splats(
641        &mut self,
642        device: &wgpu::Device,
643        queue: &wgpu::Queue,
644        data: &GaussianSplatData,
645    ) -> GaussianSplatId {
646        self.resources.upload_gaussian_splats(device, queue, data)
647    }
648
649    /// Remove an uploaded Gaussian splat set by handle.
650    ///
651    /// After this call the `id` is invalid and must not be submitted in `SceneFrame`.
652    pub fn remove_gaussian_splats(&mut self, id: GaussianSplatId) {
653        self.resources.remove_gaussian_splats(id);
654    }
655
656    /// Upload an equirectangular HDR environment map and precompute IBL textures.
657    ///
658    /// `pixels` is row-major RGBA f32 data (4 floats per texel), `width`×`height`.
659    /// This rebuilds camera bind groups so shaders immediately see the new textures.
660    pub fn upload_environment_map(
661        &mut self,
662        device: &wgpu::Device,
663        queue: &wgpu::Queue,
664        pixels: &[f32],
665        width: u32,
666        height: u32,
667    ) {
668        crate::resources::environment::upload_environment_map(
669            &mut self.resources,
670            device,
671            queue,
672            pixels,
673            width,
674            height,
675        );
676        self.rebuild_camera_bind_groups(device);
677    }
678
679    /// Rebuild the primary + per-viewport camera bind groups.
680    ///
681    /// Call after IBL textures are uploaded so shaders see the new environment.
682    fn rebuild_camera_bind_groups(&mut self, device: &wgpu::Device) {
683        self.resources.camera_bind_group = self.resources.create_camera_bind_group(
684            device,
685            &self.resources.camera_uniform_buf,
686            &self.resources.clip_planes_uniform_buf,
687            &self.resources.shadow_info_buf,
688            &self.resources.clip_volume_uniform_buf,
689            "camera_bind_group",
690        );
691
692        for slot in &mut self.viewport_slots {
693            slot.camera_bind_group = self.resources.create_camera_bind_group(
694                device,
695                &slot.camera_buf,
696                &slot.clip_planes_buf,
697                &slot.shadow_info_buf,
698                &slot.clip_volume_buf,
699                "per_viewport_camera_bg",
700            );
701        }
702    }
703
704    /// Ensure a per-viewport slot exists for `viewport_index`.
705    ///
706    /// Creates a full `ViewportSlot` with independent uniform buffers for camera,
707    /// clip planes, clip volume, shadow info, and grid. The camera bind group
708    /// references this slot's per-viewport buffers plus shared scene-global
709    /// resources. Slots are created lazily and never destroyed.
710    fn ensure_viewport_slot(&mut self, device: &wgpu::Device, viewport_index: usize) {
711        while self.viewport_slots.len() <= viewport_index {
712            let camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
713                label: Some("vp_camera_buf"),
714                size: std::mem::size_of::<CameraUniform>() as u64,
715                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
716                mapped_at_creation: false,
717            });
718            let clip_planes_buf = device.create_buffer(&wgpu::BufferDescriptor {
719                label: Some("vp_clip_planes_buf"),
720                size: std::mem::size_of::<ClipPlanesUniform>() as u64,
721                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
722                mapped_at_creation: false,
723            });
724            let clip_volume_buf = device.create_buffer(&wgpu::BufferDescriptor {
725                label: Some("vp_clip_volume_buf"),
726                size: std::mem::size_of::<ClipVolumesUniform>() as u64,
727                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
728                mapped_at_creation: false,
729            });
730            let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
731                label: Some("vp_shadow_info_buf"),
732                size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
733                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
734                mapped_at_creation: false,
735            });
736            let grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
737                label: Some("vp_grid_buf"),
738                size: std::mem::size_of::<GridUniform>() as u64,
739                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
740                mapped_at_creation: false,
741            });
742
743            let camera_bind_group = self.resources.create_camera_bind_group(
744                device,
745                &camera_buf,
746                &clip_planes_buf,
747                &shadow_info_buf,
748                &clip_volume_buf,
749                "per_viewport_camera_bg",
750            );
751
752            let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
753                label: Some("vp_grid_bind_group"),
754                layout: &self.resources.grid_bind_group_layout,
755                entries: &[wgpu::BindGroupEntry {
756                    binding: 0,
757                    resource: grid_buf.as_entire_binding(),
758                }],
759            });
760
761            // Per-viewport gizmo buffers (initial mesh: Translate, no hover, identity orientation).
762            let (gizmo_verts, gizmo_indices) = crate::interaction::gizmo::build_gizmo_mesh(
763                crate::interaction::gizmo::GizmoMode::Translate,
764                crate::interaction::gizmo::GizmoAxis::None,
765                glam::Quat::IDENTITY,
766            );
767            let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
768                label: Some("vp_gizmo_vertex_buf"),
769                size: (std::mem::size_of::<crate::resources::Vertex>() * gizmo_verts.len().max(1))
770                    as u64,
771                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
772                mapped_at_creation: true,
773            });
774            gizmo_vertex_buffer
775                .slice(..)
776                .get_mapped_range_mut()
777                .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
778            gizmo_vertex_buffer.unmap();
779            let gizmo_index_count = gizmo_indices.len() as u32;
780            let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
781                label: Some("vp_gizmo_index_buf"),
782                size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
783                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
784                mapped_at_creation: true,
785            });
786            gizmo_index_buffer
787                .slice(..)
788                .get_mapped_range_mut()
789                .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
790            gizmo_index_buffer.unmap();
791            let gizmo_uniform = crate::interaction::gizmo::GizmoUniform {
792                model: glam::Mat4::IDENTITY.to_cols_array_2d(),
793            };
794            let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
795                label: Some("vp_gizmo_uniform_buf"),
796                size: std::mem::size_of::<crate::interaction::gizmo::GizmoUniform>() as u64,
797                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
798                mapped_at_creation: true,
799            });
800            gizmo_uniform_buf
801                .slice(..)
802                .get_mapped_range_mut()
803                .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
804            gizmo_uniform_buf.unmap();
805            let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
806                label: Some("vp_gizmo_bind_group"),
807                layout: &self.resources.gizmo_bind_group_layout,
808                entries: &[wgpu::BindGroupEntry {
809                    binding: 0,
810                    resource: gizmo_uniform_buf.as_entire_binding(),
811                }],
812            });
813
814            // Per-viewport axes vertex buffer (2048 vertices = enough for all axes geometry).
815            let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
816                label: Some("vp_axes_vertex_buf"),
817                size: (std::mem::size_of::<crate::widgets::axes_indicator::AxesVertex>() * 2048)
818                    as u64,
819                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
820                mapped_at_creation: false,
821            });
822
823            self.viewport_slots.push(ViewportSlot {
824                camera_buf,
825                clip_planes_buf,
826                clip_volume_buf,
827                shadow_info_buf,
828                grid_buf,
829                camera_bind_group,
830                grid_bind_group,
831                hdr: None,
832                outline_object_buffers: Vec::new(),
833                splat_outline_buffers: Vec::new(),
834                volume_outline_indices: Vec::new(),
835                glyph_outline_indices: Vec::new(),
836                tensor_glyph_outline_indices: Vec::new(),
837                sprite_outline_indices: Vec::new(),
838                raw_geom_outline_buffers: Vec::new(),
839                screen_rect_outline_buffers: Vec::new(),
840                implicit_outline_indices: Vec::new(),
841                mc_outline_data: Vec::new(),
842                streamtube_outline_items: Vec::new(),
843                tube_outline_items: Vec::new(),
844                ribbon_outline_items: Vec::new(),
845                polyline_outline_indices: Vec::new(),
846                xray_object_buffers: Vec::new(),
847                constraint_line_buffers: Vec::new(),
848                cap_buffers: Vec::new(),
849                clip_plane_fill_buffers: Vec::new(),
850                clip_plane_line_buffers: Vec::new(),
851                axes_vertex_buffer,
852                axes_vertex_count: 0,
853                gizmo_uniform_buf,
854                gizmo_bind_group,
855                gizmo_vertex_buffer,
856                gizmo_index_buffer,
857                gizmo_index_count,
858                sub_highlight: None,
859                sub_highlight_generation: u64::MAX,
860                dyn_res: None,
861                hdr_callback: None,
862            });
863        }
864    }
865
866    // -----------------------------------------------------------------------
867    // Multi-viewport public API (Phase 5)
868    // -----------------------------------------------------------------------
869
870    /// Create a new viewport slot and return its handle.
871    ///
872    /// The returned [`ViewportId`] is stable for the lifetime of the renderer.
873    /// Pass it to [`prepare_viewport`](Self::prepare_viewport),
874    /// [`paint_viewport`](Self::paint_viewport), and
875    /// [`render_viewport`](Self::render_viewport) each frame.
876    ///
877    /// Also set `CameraFrame::viewport_index` to `id.0` when building the
878    /// [`FrameData`] for this viewport:
879    /// ```rust,ignore
880    /// let id = renderer.create_viewport(&device);
881    /// let frame = FrameData {
882    ///     camera: CameraFrame::from_camera(&cam, size).with_viewport_index(id.0),
883    ///     ..Default::default()
884    /// };
885    /// ```
886    pub fn create_viewport(&mut self, device: &wgpu::Device) -> ViewportId {
887        let idx = self.viewport_slots.len();
888        self.ensure_viewport_slot(device, idx);
889        ViewportId(idx)
890    }
891
892    /// Release the heavy GPU texture memory (HDR targets, OIT, bloom, SSAO) held
893    /// by `id`.
894    ///
895    /// The slot index is not reclaimed : future calls with this `ViewportId` will
896    /// lazily recreate the texture resources as needed.  This is useful when a
897    /// viewport is hidden or minimised and you want to reduce VRAM pressure without
898    /// invalidating the handle.
899    pub fn destroy_viewport(&mut self, id: ViewportId) {
900        if let Some(slot) = self.viewport_slots.get_mut(id.0) {
901            slot.hdr = None;
902        }
903    }
904
905    /// Returns the owned-encoder rendering path.
906    ///
907    /// Use when you own the window loop and wgpu encoder (winit, raw wgpu).
908    /// See [`OwnedPath`] for available methods.
909    pub fn owned(&mut self) -> OwnedPath<'_> {
910        OwnedPath { renderer: self }
911    }
912
913    /// Returns the pass-based rendering path.
914    ///
915    /// Use when a framework provides you with a render pass (eframe, iced).
916    /// See [`PassPath`] for available methods.
917    pub fn pass(&mut self) -> PassPath<'_> {
918        PassPath { renderer: self }
919    }
920
921    /// Returns a read-only paint view for framework paint callbacks.
922    ///
923    /// Use this in callbacks where only a shared reference to the renderer is
924    /// available (e.g. eframe's `CallbackTrait::paint` where `callback_resources`
925    /// is `&CallbackResources`). Exposes only the paint methods, not prepare.
926    pub fn pass_view(&self) -> PassView<'_> {
927        PassView { renderer: self }
928    }
929
930    /// Prepare shared scene data.  Call **once per frame**, before any
931    /// [`prepare_viewport`](Self::prepare_viewport) calls.
932    ///
933    /// `frame` provides the scene content (`frame.scene`) and the primary camera
934    /// used for shadow cascade framing (`frame.camera`).  In a multi-viewport
935    /// setup use any one viewport's `FrameData` here : typically the perspective
936    /// view : as the shadow framing reference.
937    ///
938    /// `scene_effects` carries the scene-global effects: lighting, environment
939    /// map, and compute filters.  Obtain it by constructing [`SceneEffects`]
940    /// directly or via [`EffectsFrame::split`].
941    pub(crate) fn prepare_scene(
942        &mut self,
943        device: &wgpu::Device,
944        queue: &wgpu::Queue,
945        frame: &FrameData,
946        scene_effects: &SceneEffects<'_>,
947    ) {
948        self.prepare_scene_internal(device, queue, frame, scene_effects);
949    }
950
951    /// Prepare per-viewport GPU state (camera, clip planes, overlays, axes).
952    ///
953    /// Call once per viewport per frame, **after** [`prepare_scene`](Self::prepare_scene).
954    ///
955    /// `id` must have been obtained from [`create_viewport`](Self::create_viewport).
956    /// `frame.camera.viewport_index` must equal `id.0`; use
957    /// [`CameraFrame::with_viewport_index`] when building the frame.
958    pub(crate) fn prepare_viewport(
959        &mut self,
960        device: &wgpu::Device,
961        queue: &wgpu::Queue,
962        id: ViewportId,
963        frame: &FrameData,
964    ) {
965        debug_assert_eq!(
966            frame.camera.viewport_index, id.0,
967            "frame.camera.viewport_index ({}) must equal the ViewportId ({}); \
968             use CameraFrame::with_viewport_index(id.0)",
969            frame.camera.viewport_index, id.0,
970        );
971        let (_, viewport_fx) = frame.effects.split();
972        self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
973    }
974
975    /// Issue draw calls for `id` into a render pass with any lifetime.
976    ///
977    /// Identical to [`paint_viewport`](Self::paint_viewport) but accepts a render pass with a
978    /// non-`'static` lifetime, making it usable from winit, iced, or raw wgpu where the encoder
979    /// creates its own render pass.
980    pub(crate) fn paint_viewport_to<'rp>(
981        &self,
982        render_pass: &mut wgpu::RenderPass<'rp>,
983        id: ViewportId,
984        frame: &FrameData,
985    ) {
986        let vp_idx = id.0;
987        let camera_bg = self.viewport_camera_bind_group(vp_idx);
988        let grid_bg = self.viewport_grid_bind_group(vp_idx);
989        let vp_slot = self.viewport_slots.get(vp_idx);
990        emit_draw_calls!(
991            &self.resources,
992            &mut *render_pass,
993            frame,
994            self.use_instancing,
995            &self.instanced_batches,
996            camera_bg,
997            grid_bg,
998            &self.compute_filter_results,
999            vp_slot,
1000            &self.wireframe_bind_groups
1001        );
1002        emit_scivis_draw_calls!(
1003            &self.resources,
1004            &mut *render_pass,
1005            &self.point_cloud_gpu_data,
1006            &self.glyph_gpu_data,
1007            &self.polyline_gpu_data,
1008            &self.volume_gpu_data,
1009            &self.streamtube_gpu_data,
1010            camera_bg,
1011            &self.tube_gpu_data,
1012            &self.image_slice_gpu_data,
1013            &self.tensor_glyph_gpu_data,
1014            &self.ribbon_gpu_data,
1015            &self.volume_surface_slice_gpu_data,
1016            &self.sprite_gpu_data,
1017            false
1018        );
1019        // Gaussian splats (alpha-blended, back-to-front sorted, no depth write).
1020        if !self.gaussian_splat_draw_data.is_empty() {
1021            if let Some(ref dual) = self.resources.gaussian_splat_pipeline {
1022                render_pass.set_pipeline(dual.for_format(false));
1023                render_pass.set_bind_group(0, camera_bg, &[]);
1024                for dd in &self.gaussian_splat_draw_data {
1025                    if dd.wireframe {
1026                        continue;
1027                    }
1028                    if let Some(set) = self.resources.gaussian_splat_store.get(dd.store_index) {
1029                        if let Some(Some(vp_sort)) = set.viewport_sort.get(dd.viewport_index) {
1030                            render_pass.set_bind_group(1, &vp_sort.render_bg, &[]);
1031                            render_pass.draw(0..6, 0..dd.count);
1032                        }
1033                    }
1034                }
1035            }
1036        }
1037        // TransparentVolumeMesh boundary wireframe overlay.
1038        if !self.tvm_wireframe_draws.is_empty() {
1039            if let Some(ref tvm_bg) = self.tvm_wireframe_bg {
1040                render_pass.set_bind_group(0, camera_bg, &[]);
1041                for mesh_id in &self.tvm_wireframe_draws {
1042                    if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
1043                        render_pass.set_pipeline(&self.resources.wireframe_pipeline);
1044                        render_pass.set_bind_group(1, tvm_bg, &[]);
1045                        render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1046                        render_pass.set_index_buffer(
1047                            mesh.edge_index_buffer.slice(..),
1048                            wgpu::IndexFormat::Uint32,
1049                        );
1050                        render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1051                    }
1052                }
1053            }
1054        }
1055    }
1056
1057    /// Return a reference to the camera bind group for the given viewport slot.
1058    ///
1059    /// Falls back to `resources.camera_bind_group` if no per-viewport slot
1060    /// exists (e.g. in single-viewport mode before the first prepare call).
1061    fn viewport_camera_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
1062        self.viewport_slots
1063            .get(viewport_index)
1064            .map(|slot| &slot.camera_bind_group)
1065            .unwrap_or(&self.resources.camera_bind_group)
1066    }
1067
1068    /// Return a reference to the grid bind group for the given viewport slot.
1069    ///
1070    /// Falls back to `resources.grid_bind_group` if no per-viewport slot exists.
1071    fn viewport_grid_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
1072        self.viewport_slots
1073            .get(viewport_index)
1074            .map(|slot| &slot.grid_bind_group)
1075            .unwrap_or(&self.resources.grid_bind_group)
1076    }
1077
1078    /// Ensure the dyn-res intermediate render target exists for `vp_idx` at the
1079    /// given `scaled_size`, creating or recreating it when size changes.
1080    ///
1081    /// `surface_size` is the native output dimensions (used to size the upscale
1082    /// blit correctly). `ensure_dyn_res_pipeline` is called automatically.
1083    pub(crate) fn ensure_dyn_res_target(
1084        &mut self,
1085        device: &wgpu::Device,
1086        vp_idx: usize,
1087        scaled_size: [u32; 2],
1088        surface_size: [u32; 2],
1089    ) {
1090        self.resources.ensure_dyn_res_pipeline(device);
1091        let needs_create = match &self.viewport_slots[vp_idx].dyn_res {
1092            None => true,
1093            Some(dr) => dr.scaled_size != scaled_size || dr.surface_size != surface_size,
1094        };
1095        if needs_create {
1096            let target = self
1097                .resources
1098                .create_dyn_res_target(device, scaled_size, surface_size);
1099            self.viewport_slots[vp_idx].dyn_res = Some(target);
1100        }
1101    }
1102
1103    /// Ensure per-viewport HDR state exists for `viewport_index` at dimensions `w`×`h`.
1104    ///
1105    /// Calls `ensure_hdr_shared` once to initialise shared pipelines/BGLs/samplers, then
1106    /// lazily creates or resizes the `ViewportHdrState` inside the slot. Idempotent: if the
1107    /// slot already has HDR state at the correct size nothing is recreated.
1108    pub(crate) fn ensure_viewport_hdr(
1109        &mut self,
1110        device: &wgpu::Device,
1111        queue: &wgpu::Queue,
1112        viewport_index: usize,
1113        w: u32,
1114        h: u32,
1115        ssaa_factor: u32,
1116        render_scale: f32,
1117    ) {
1118        let format = self.resources.target_format;
1119        // Ensure shared infrastructure (pipelines, BGLs, samplers) exists.
1120        self.resources.ensure_hdr_shared(device, queue, format);
1121        // When render_scale < 1.0, the HDR upscale path needs the dyn_res
1122        // pipeline and sampler for the final upscale-blit to output resolution.
1123        if render_scale < 1.0 - 0.001 {
1124            self.resources.ensure_dyn_res_pipeline(device);
1125        }
1126        // Compute the scene-resolution render target size.
1127        let scale = render_scale.clamp(0.1, 1.0);
1128        let scene_w = ((w as f32) * scale).round() as u32;
1129        let scene_h = ((h as f32) * scale).round() as u32;
1130        // Ensure the slot exists.
1131        self.ensure_viewport_slot(device, viewport_index);
1132        let slot = &mut self.viewport_slots[viewport_index];
1133        // Create or resize the per-viewport HDR state.
1134        let needs_create = match &slot.hdr {
1135            None => true,
1136            Some(s) => {
1137                s.output_size != [w, h]
1138                    || s.scene_size != [scene_w.max(1), scene_h.max(1)]
1139                    || s.ssaa_factor != ssaa_factor
1140            }
1141        };
1142        if needs_create {
1143            slot.hdr = Some(self.resources.create_hdr_viewport_state(
1144                device,
1145                queue,
1146                format,
1147                w,
1148                h,
1149                scene_w.max(1),
1150                scene_h.max(1),
1151                ssaa_factor,
1152            ));
1153        }
1154    }
1155}