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