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