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