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