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