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 picking;
10mod prepare;
11mod render;
12pub mod shader_hashes;
13mod shadows;
14pub mod stats;
15
16pub(crate) use self::types::ClipPlane;
17pub use self::types::{
18    CameraFrame, CameraFrustumItem, ClipObject, ClipShape, ComputeFilterItem, ComputeFilterKind,
19    EffectsFrame, EnvironmentMap, FilterMode, FrameData, GlyphItem, GlyphType, GroundPlane,
20    GroundPlaneMode, ImageAnchor, InteractionFrame, LightKind, LightSource, LightingSettings,
21    PickId, PointCloudItem, PointRenderMode, PolylineItem, PostProcessSettings, RenderCamera,
22    ScalarBar, ScalarBarAnchor, ScalarBarOrientation, SceneEffects, SceneFrame, SceneRenderItem,
23    ScreenImageItem, ShadowFilter, StreamtubeItem, SurfaceSubmission, ToneMapping, ViewportEffects,
24    ViewportFrame, VolumeItem,
25};
26
27/// An opaque handle to a per-viewport GPU state slot.
28///
29/// Obtained from [`ViewportRenderer::create_viewport`] and passed to
30/// [`ViewportRenderer::prepare_viewport`], [`ViewportRenderer::paint_viewport`],
31/// and [`ViewportRenderer::render_viewport`].
32///
33/// The inner `usize` is the slot index and doubles as the value for
34/// [`CameraFrame::with_viewport_index`].  Single-viewport applications that use
35/// the legacy [`ViewportRenderer::prepare`] / [`ViewportRenderer::paint`] API do
36/// not need this type.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub struct ViewportId(pub usize);
39
40use self::shadows::{compute_cascade_matrix, compute_cascade_splits};
41use self::types::{INSTANCING_THRESHOLD, InstancedBatch};
42use crate::resources::{
43    CameraUniform, ClipPlanesUniform, ClipVolumeUniform, GridUniform, InstanceData, LightsUniform,
44    ObjectUniform, OutlineEdgeUniform, OutlineObjectBuffers, OutlineUniform, PickInstance,
45    ShadowAtlasUniform,
46    SingleLightUniform, ViewportGpuResources,
47};
48
49/// Per-viewport GPU state: uniform buffers and bind groups that differ per viewport.
50///
51/// Each viewport slot owns its own camera, clip planes, clip volume, shadow info,
52/// and grid buffers, plus the bind groups that reference them. Scene-global
53/// resources (lights, shadow atlas texture, IBL) are shared via the bind group
54/// pointing to buffers on `ViewportGpuResources`.
55pub(crate) struct ViewportSlot {
56    pub camera_buf: wgpu::Buffer,
57    pub clip_planes_buf: wgpu::Buffer,
58    pub clip_volume_buf: wgpu::Buffer,
59    pub shadow_info_buf: wgpu::Buffer,
60    pub grid_buf: wgpu::Buffer,
61    /// Camera bind group (group 0) referencing this slot's per-viewport buffers
62    /// plus shared scene-global resources.
63    pub camera_bind_group: wgpu::BindGroup,
64    /// Grid bind group (group 0 for grid pipeline) referencing this slot's grid buffer.
65    pub grid_bind_group: wgpu::BindGroup,
66    /// Per-viewport HDR post-process render targets.
67    ///
68    /// Created lazily on first HDR render call and resized when viewport dimensions change.
69    pub hdr: Option<crate::resources::ViewportHdrState>,
70
71    // --- Per-viewport interaction state (Phase 4) ---
72    /// Per-frame outline buffers for selected objects, rebuilt in prepare().
73    pub outline_object_buffers: Vec<OutlineObjectBuffers>,
74    /// Per-frame x-ray buffers for selected objects, rebuilt in prepare().
75    pub xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)>,
76    /// Per-frame constraint guide line buffers, rebuilt in prepare().
77    pub constraint_line_buffers: Vec<(
78        wgpu::Buffer,
79        wgpu::Buffer,
80        u32,
81        wgpu::Buffer,
82        wgpu::BindGroup,
83    )>,
84    /// Per-frame cap geometry buffers (section view cross-section fill), rebuilt in prepare().
85    pub cap_buffers: Vec<(
86        wgpu::Buffer,
87        wgpu::Buffer,
88        u32,
89        wgpu::Buffer,
90        wgpu::BindGroup,
91    )>,
92    /// Per-frame clip plane fill overlay buffers, rebuilt in prepare().
93    pub clip_plane_fill_buffers: Vec<(
94        wgpu::Buffer,
95        wgpu::Buffer,
96        u32,
97        wgpu::Buffer,
98        wgpu::BindGroup,
99    )>,
100    /// Per-frame clip plane line overlay buffers, rebuilt in prepare().
101    pub clip_plane_line_buffers: Vec<(
102        wgpu::Buffer,
103        wgpu::Buffer,
104        u32,
105        wgpu::Buffer,
106        wgpu::BindGroup,
107    )>,
108    /// Vertex buffer for axes indicator geometry (rebuilt each frame).
109    pub axes_vertex_buffer: wgpu::Buffer,
110    /// Number of vertices in the axes indicator buffer.
111    pub axes_vertex_count: u32,
112    /// Gizmo model-matrix uniform buffer.
113    pub gizmo_uniform_buf: wgpu::Buffer,
114    /// Gizmo bind group (group 1: model matrix uniform).
115    pub gizmo_bind_group: wgpu::BindGroup,
116    /// Gizmo vertex buffer.
117    pub gizmo_vertex_buffer: wgpu::Buffer,
118    /// Gizmo index buffer.
119    pub gizmo_index_buffer: wgpu::Buffer,
120    /// Number of indices in the current gizmo mesh.
121    pub gizmo_index_count: u32,
122}
123
124/// High-level renderer wrapping all GPU resources and providing framework-agnostic
125/// `prepare()` and `paint()` methods.
126pub struct ViewportRenderer {
127    resources: ViewportGpuResources,
128    /// Instanced batches prepared for the current frame. Empty when using per-object path.
129    instanced_batches: Vec<InstancedBatch>,
130    /// Whether the current frame uses the instanced draw path.
131    use_instancing: bool,
132    /// Performance counters from the last frame.
133    last_stats: crate::renderer::stats::FrameStats,
134    /// Last scene generation seen during prepare(). u64::MAX forces rebuild on first frame.
135    last_scene_generation: u64,
136    /// Last selection generation seen during prepare(). u64::MAX forces rebuild on first frame.
137    last_selection_generation: u64,
138    /// Last scene_items count seen during prepare(). usize::MAX forces rebuild on first frame.
139    /// Included in cache key so that frustum-culling changes (different visible set, different
140    /// count) correctly invalidate the instance buffer even when scene_generation is stable.
141    last_scene_items_count: usize,
142    /// Cached instance data from last rebuild (mirrors the GPU buffer contents).
143    cached_instance_data: Vec<InstanceData>,
144    /// Cached instanced batch descriptors from last rebuild.
145    cached_instanced_batches: Vec<InstancedBatch>,
146    /// Per-frame point cloud GPU data, rebuilt in prepare(), consumed in paint().
147    point_cloud_gpu_data: Vec<crate::resources::PointCloudGpuData>,
148    /// Per-frame glyph GPU data, rebuilt in prepare(), consumed in paint().
149    glyph_gpu_data: Vec<crate::resources::GlyphGpuData>,
150    /// Per-frame polyline GPU data, rebuilt in prepare(), consumed in paint().
151    polyline_gpu_data: Vec<crate::resources::PolylineGpuData>,
152    /// Per-frame volume GPU data, rebuilt in prepare(), consumed in paint().
153    volume_gpu_data: Vec<crate::resources::VolumeGpuData>,
154    /// Per-frame streamtube GPU data, rebuilt in prepare(), consumed in paint().
155    streamtube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
156    /// Per-frame GPU implicit surface data, rebuilt in prepare(), consumed in paint() (Phase 16).
157    implicit_gpu_data: Vec<crate::resources::implicit::ImplicitGpuItem>,
158    /// Per-frame GPU marching cubes render data, rebuilt in prepare(), consumed in paint() (Phase 17).
159    mc_gpu_data: Vec<crate::resources::gpu_marching_cubes::McFrameData>,
160    /// Per-frame screen-image GPU data, rebuilt in prepare(), consumed in paint() (Phase 10B).
161    screen_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
162    /// Per-viewport GPU state slots.
163    ///
164    /// Indexed by `FrameData::camera.viewport_index`. Each slot owns independent
165    /// uniform buffers and bind groups for camera, clip planes, clip volume,
166    /// shadow info, and grid. Slots are grown lazily in `prepare` via
167    /// `ensure_viewport_slot`. There are at most 4 in the current UI.
168    viewport_slots: Vec<ViewportSlot>,
169    /// Phase G : GPU compute filter results from the last `prepare()` call.
170    ///
171    /// Each entry contains a compacted index buffer + count for one filtered mesh.
172    /// Consumed during `paint()` to override the mesh's default index buffer.
173    /// Cleared and rebuilt each frame.
174    compute_filter_results: Vec<crate::resources::ComputeFilterResult>,
175    /// Cascade-0 light-space view-projection matrix from the last shadow prepare.
176    /// Cached here so `prepare_viewport_internal` can copy it into the ground plane uniform.
177    last_cascade0_shadow_mat: glam::Mat4,
178}
179
180impl ViewportRenderer {
181    /// Create a new renderer with default settings (no MSAA).
182    /// Call once at application startup.
183    pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
184        Self::with_sample_count(device, target_format, 1)
185    }
186
187    /// Create a new renderer with the specified MSAA sample count (1, 2, or 4).
188    ///
189    /// When using MSAA (sample_count > 1), the caller must create multisampled
190    /// color and depth textures and use them as render pass attachments with the
191    /// final surface texture as the resolve target.
192    pub fn with_sample_count(
193        device: &wgpu::Device,
194        target_format: wgpu::TextureFormat,
195        sample_count: u32,
196    ) -> Self {
197        Self {
198            resources: ViewportGpuResources::new(device, target_format, sample_count),
199            instanced_batches: Vec::new(),
200            use_instancing: false,
201            last_stats: crate::renderer::stats::FrameStats::default(),
202            last_scene_generation: u64::MAX,
203            last_selection_generation: u64::MAX,
204            last_scene_items_count: usize::MAX,
205            cached_instance_data: Vec::new(),
206            cached_instanced_batches: Vec::new(),
207            point_cloud_gpu_data: Vec::new(),
208            glyph_gpu_data: Vec::new(),
209            polyline_gpu_data: Vec::new(),
210            volume_gpu_data: Vec::new(),
211            streamtube_gpu_data: Vec::new(),
212            implicit_gpu_data: Vec::new(),
213            mc_gpu_data: Vec::new(),
214            screen_image_gpu_data: Vec::new(),
215            viewport_slots: Vec::new(),
216            compute_filter_results: Vec::new(),
217            last_cascade0_shadow_mat: glam::Mat4::IDENTITY,
218        }
219    }
220
221    /// Access the underlying GPU resources (e.g. for mesh uploads).
222    pub fn resources(&self) -> &ViewportGpuResources {
223        &self.resources
224    }
225
226    /// Performance counters from the last completed frame.
227    pub fn last_frame_stats(&self) -> crate::renderer::stats::FrameStats {
228        self.last_stats
229    }
230
231    /// Mutable access to the underlying GPU resources (e.g. for mesh uploads).
232    pub fn resources_mut(&mut self) -> &mut ViewportGpuResources {
233        &mut self.resources
234    }
235
236    /// Upload an equirectangular HDR environment map and precompute IBL textures.
237    ///
238    /// `pixels` is row-major RGBA f32 data (4 floats per texel), `width`×`height`.
239    /// This rebuilds camera bind groups so shaders immediately see the new textures.
240    pub fn upload_environment_map(
241        &mut self,
242        device: &wgpu::Device,
243        queue: &wgpu::Queue,
244        pixels: &[f32],
245        width: u32,
246        height: u32,
247    ) {
248        crate::resources::environment::upload_environment_map(
249            &mut self.resources,
250            device,
251            queue,
252            pixels,
253            width,
254            height,
255        );
256        self.rebuild_camera_bind_groups(device);
257    }
258
259    /// Rebuild the primary + per-viewport camera bind groups.
260    ///
261    /// Call after IBL textures are uploaded so shaders see the new environment.
262    fn rebuild_camera_bind_groups(&mut self, device: &wgpu::Device) {
263        self.resources.camera_bind_group = self.resources.create_camera_bind_group(
264            device,
265            &self.resources.camera_uniform_buf,
266            &self.resources.clip_planes_uniform_buf,
267            &self.resources.shadow_info_buf,
268            &self.resources.clip_volume_uniform_buf,
269            "camera_bind_group",
270        );
271
272        for slot in &mut self.viewport_slots {
273            slot.camera_bind_group = self.resources.create_camera_bind_group(
274                device,
275                &slot.camera_buf,
276                &slot.clip_planes_buf,
277                &slot.shadow_info_buf,
278                &slot.clip_volume_buf,
279                "per_viewport_camera_bg",
280            );
281        }
282    }
283
284    /// Ensure a per-viewport slot exists for `viewport_index`.
285    ///
286    /// Creates a full `ViewportSlot` with independent uniform buffers for camera,
287    /// clip planes, clip volume, shadow info, and grid. The camera bind group
288    /// references this slot's per-viewport buffers plus shared scene-global
289    /// resources. Slots are created lazily and never destroyed.
290    fn ensure_viewport_slot(&mut self, device: &wgpu::Device, viewport_index: usize) {
291        while self.viewport_slots.len() <= viewport_index {
292            let camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
293                label: Some("vp_camera_buf"),
294                size: std::mem::size_of::<CameraUniform>() as u64,
295                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
296                mapped_at_creation: false,
297            });
298            let clip_planes_buf = device.create_buffer(&wgpu::BufferDescriptor {
299                label: Some("vp_clip_planes_buf"),
300                size: std::mem::size_of::<ClipPlanesUniform>() as u64,
301                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
302                mapped_at_creation: false,
303            });
304            let clip_volume_buf = device.create_buffer(&wgpu::BufferDescriptor {
305                label: Some("vp_clip_volume_buf"),
306                size: std::mem::size_of::<ClipVolumeUniform>() as u64,
307                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
308                mapped_at_creation: false,
309            });
310            let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
311                label: Some("vp_shadow_info_buf"),
312                size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
313                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
314                mapped_at_creation: false,
315            });
316            let grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
317                label: Some("vp_grid_buf"),
318                size: std::mem::size_of::<GridUniform>() as u64,
319                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
320                mapped_at_creation: false,
321            });
322
323            let camera_bind_group = self.resources.create_camera_bind_group(
324                device,
325                &camera_buf,
326                &clip_planes_buf,
327                &shadow_info_buf,
328                &clip_volume_buf,
329                "per_viewport_camera_bg",
330            );
331
332            let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
333                label: Some("vp_grid_bind_group"),
334                layout: &self.resources.grid_bind_group_layout,
335                entries: &[wgpu::BindGroupEntry {
336                    binding: 0,
337                    resource: grid_buf.as_entire_binding(),
338                }],
339            });
340
341            // Per-viewport gizmo buffers (initial mesh: Translate, no hover, identity orientation).
342            let (gizmo_verts, gizmo_indices) = crate::interaction::gizmo::build_gizmo_mesh(
343                crate::interaction::gizmo::GizmoMode::Translate,
344                crate::interaction::gizmo::GizmoAxis::None,
345                glam::Quat::IDENTITY,
346            );
347            let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
348                label: Some("vp_gizmo_vertex_buf"),
349                size: (std::mem::size_of::<crate::resources::Vertex>() * gizmo_verts.len().max(1))
350                    as u64,
351                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
352                mapped_at_creation: true,
353            });
354            gizmo_vertex_buffer
355                .slice(..)
356                .get_mapped_range_mut()
357                .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
358            gizmo_vertex_buffer.unmap();
359            let gizmo_index_count = gizmo_indices.len() as u32;
360            let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
361                label: Some("vp_gizmo_index_buf"),
362                size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
363                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
364                mapped_at_creation: true,
365            });
366            gizmo_index_buffer
367                .slice(..)
368                .get_mapped_range_mut()
369                .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
370            gizmo_index_buffer.unmap();
371            let gizmo_uniform = crate::interaction::gizmo::GizmoUniform {
372                model: glam::Mat4::IDENTITY.to_cols_array_2d(),
373            };
374            let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
375                label: Some("vp_gizmo_uniform_buf"),
376                size: std::mem::size_of::<crate::interaction::gizmo::GizmoUniform>() as u64,
377                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
378                mapped_at_creation: true,
379            });
380            gizmo_uniform_buf
381                .slice(..)
382                .get_mapped_range_mut()
383                .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
384            gizmo_uniform_buf.unmap();
385            let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
386                label: Some("vp_gizmo_bind_group"),
387                layout: &self.resources.gizmo_bind_group_layout,
388                entries: &[wgpu::BindGroupEntry {
389                    binding: 0,
390                    resource: gizmo_uniform_buf.as_entire_binding(),
391                }],
392            });
393
394            // Per-viewport axes vertex buffer (2048 vertices = enough for all axes geometry).
395            let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
396                label: Some("vp_axes_vertex_buf"),
397                size: (std::mem::size_of::<crate::widgets::axes_indicator::AxesVertex>() * 2048)
398                    as u64,
399                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
400                mapped_at_creation: false,
401            });
402
403            self.viewport_slots.push(ViewportSlot {
404                camera_buf,
405                clip_planes_buf,
406                clip_volume_buf,
407                shadow_info_buf,
408                grid_buf,
409                camera_bind_group,
410                grid_bind_group,
411                hdr: None,
412                outline_object_buffers: Vec::new(),
413                xray_object_buffers: Vec::new(),
414                constraint_line_buffers: Vec::new(),
415                cap_buffers: Vec::new(),
416                clip_plane_fill_buffers: Vec::new(),
417                clip_plane_line_buffers: Vec::new(),
418                axes_vertex_buffer,
419                axes_vertex_count: 0,
420                gizmo_uniform_buf,
421                gizmo_bind_group,
422                gizmo_vertex_buffer,
423                gizmo_index_buffer,
424                gizmo_index_count,
425            });
426        }
427    }
428
429    // -----------------------------------------------------------------------
430    // Multi-viewport public API (Phase 5)
431    // -----------------------------------------------------------------------
432
433    /// Create a new viewport slot and return its handle.
434    ///
435    /// The returned [`ViewportId`] is stable for the lifetime of the renderer.
436    /// Pass it to [`prepare_viewport`](Self::prepare_viewport),
437    /// [`paint_viewport`](Self::paint_viewport), and
438    /// [`render_viewport`](Self::render_viewport) each frame.
439    ///
440    /// Also set `CameraFrame::viewport_index` to `id.0` when building the
441    /// [`FrameData`] for this viewport:
442    /// ```rust,ignore
443    /// let id = renderer.create_viewport(&device);
444    /// let frame = FrameData {
445    ///     camera: CameraFrame::from_camera(&cam, size).with_viewport_index(id.0),
446    ///     ..Default::default()
447    /// };
448    /// ```
449    pub fn create_viewport(&mut self, device: &wgpu::Device) -> ViewportId {
450        let idx = self.viewport_slots.len();
451        self.ensure_viewport_slot(device, idx);
452        ViewportId(idx)
453    }
454
455    /// Release the heavy GPU texture memory (HDR targets, OIT, bloom, SSAO) held
456    /// by `id`.
457    ///
458    /// The slot index is not reclaimed : future calls with this `ViewportId` will
459    /// lazily recreate the texture resources as needed.  This is useful when a
460    /// viewport is hidden or minimised and you want to reduce VRAM pressure without
461    /// invalidating the handle.
462    pub fn destroy_viewport(&mut self, id: ViewportId) {
463        if let Some(slot) = self.viewport_slots.get_mut(id.0) {
464            slot.hdr = None;
465        }
466    }
467
468    /// Prepare shared scene data.  Call **once per frame**, before any
469    /// [`prepare_viewport`](Self::prepare_viewport) calls.
470    ///
471    /// `frame` provides the scene content (`frame.scene`) and the primary camera
472    /// used for shadow cascade framing (`frame.camera`).  In a multi-viewport
473    /// setup use any one viewport's `FrameData` here : typically the perspective
474    /// view : as the shadow framing reference.
475    ///
476    /// `scene_effects` carries the scene-global effects: lighting, environment
477    /// map, and compute filters.  Obtain it by constructing [`SceneEffects`]
478    /// directly or via [`EffectsFrame::split`].
479    pub fn prepare_scene(
480        &mut self,
481        device: &wgpu::Device,
482        queue: &wgpu::Queue,
483        frame: &FrameData,
484        scene_effects: &SceneEffects<'_>,
485    ) {
486        self.prepare_scene_internal(device, queue, frame, scene_effects);
487    }
488
489    /// Prepare per-viewport GPU state (camera, clip planes, overlays, axes).
490    ///
491    /// Call once per viewport per frame, **after** [`prepare_scene`](Self::prepare_scene).
492    ///
493    /// `id` must have been obtained from [`create_viewport`](Self::create_viewport).
494    /// `frame.camera.viewport_index` must equal `id.0`; use
495    /// [`CameraFrame::with_viewport_index`] when building the frame.
496    pub fn prepare_viewport(
497        &mut self,
498        device: &wgpu::Device,
499        queue: &wgpu::Queue,
500        id: ViewportId,
501        frame: &FrameData,
502    ) {
503        debug_assert_eq!(
504            frame.camera.viewport_index, id.0,
505            "frame.camera.viewport_index ({}) must equal the ViewportId ({}); \
506             use CameraFrame::with_viewport_index(id.0)",
507            frame.camera.viewport_index, id.0,
508        );
509        let (_, viewport_fx) = frame.effects.split();
510        self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
511    }
512
513    /// Issue draw calls for `id` into a `'static` render pass (as provided by egui callbacks).
514    ///
515    /// This is the method to use from an egui/eframe `CallbackTrait::paint` implementation.
516    /// Call [`prepare_scene`](Self::prepare_scene) and [`prepare_viewport`](Self::prepare_viewport)
517    /// first (in `CallbackTrait::prepare`), then set the render pass viewport/scissor to confine
518    /// drawing to the correct quadrant, and call this method.
519    ///
520    /// For non-`'static` render passes (winit, iced, manual wgpu), use
521    /// [`paint_viewport_to`](Self::paint_viewport_to).
522    pub fn paint_viewport(
523        &self,
524        render_pass: &mut wgpu::RenderPass<'static>,
525        id: ViewportId,
526        frame: &FrameData,
527    ) {
528        let vp_idx = id.0;
529        let camera_bg = self.viewport_camera_bind_group(vp_idx);
530        let grid_bg = self.viewport_grid_bind_group(vp_idx);
531        let vp_slot = self.viewport_slots.get(vp_idx);
532        emit_draw_calls!(
533            &self.resources,
534            &mut *render_pass,
535            frame,
536            self.use_instancing,
537            &self.instanced_batches,
538            camera_bg,
539            grid_bg,
540            &self.compute_filter_results,
541            vp_slot
542        );
543        emit_scivis_draw_calls!(
544            &self.resources,
545            render_pass,
546            &self.point_cloud_gpu_data,
547            &self.glyph_gpu_data,
548            &self.polyline_gpu_data,
549            &self.volume_gpu_data,
550            &self.streamtube_gpu_data,
551            camera_bg
552        );
553    }
554
555    /// Issue draw calls for `id` into a render pass with any lifetime.
556    ///
557    /// Identical to [`paint_viewport`](Self::paint_viewport) but accepts a render pass with a
558    /// non-`'static` lifetime, making it usable from winit, iced, or raw wgpu where the encoder
559    /// creates its own render pass.
560    pub fn paint_viewport_to<'rp>(
561        &'rp self,
562        render_pass: &mut wgpu::RenderPass<'rp>,
563        id: ViewportId,
564        frame: &FrameData,
565    ) {
566        let vp_idx = id.0;
567        let camera_bg = self.viewport_camera_bind_group(vp_idx);
568        let grid_bg = self.viewport_grid_bind_group(vp_idx);
569        let vp_slot = self.viewport_slots.get(vp_idx);
570        emit_draw_calls!(
571            &self.resources,
572            &mut *render_pass,
573            frame,
574            self.use_instancing,
575            &self.instanced_batches,
576            camera_bg,
577            grid_bg,
578            &self.compute_filter_results,
579            vp_slot
580        );
581        emit_scivis_draw_calls!(
582            &self.resources,
583            render_pass,
584            &self.point_cloud_gpu_data,
585            &self.glyph_gpu_data,
586            &self.polyline_gpu_data,
587            &self.volume_gpu_data,
588            &self.streamtube_gpu_data,
589            camera_bg
590        );
591    }
592
593    /// Return a reference to the camera bind group for the given viewport slot.
594    ///
595    /// Falls back to `resources.camera_bind_group` if no per-viewport slot
596    /// exists (e.g. in single-viewport mode before the first prepare call).
597    fn viewport_camera_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
598        self.viewport_slots
599            .get(viewport_index)
600            .map(|slot| &slot.camera_bind_group)
601            .unwrap_or(&self.resources.camera_bind_group)
602    }
603
604    /// Return a reference to the grid bind group for the given viewport slot.
605    ///
606    /// Falls back to `resources.grid_bind_group` if no per-viewport slot exists.
607    fn viewport_grid_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
608        self.viewport_slots
609            .get(viewport_index)
610            .map(|slot| &slot.grid_bind_group)
611            .unwrap_or(&self.resources.grid_bind_group)
612    }
613
614    /// Ensure per-viewport HDR state exists for `viewport_index` at dimensions `w`×`h`.
615    ///
616    /// Calls `ensure_hdr_shared` once to initialise shared pipelines/BGLs/samplers, then
617    /// lazily creates or resizes the `ViewportHdrState` inside the slot. Idempotent: if the
618    /// slot already has HDR state at the correct size nothing is recreated.
619    pub(crate) fn ensure_viewport_hdr(
620        &mut self,
621        device: &wgpu::Device,
622        queue: &wgpu::Queue,
623        viewport_index: usize,
624        w: u32,
625        h: u32,
626        ssaa_factor: u32,
627    ) {
628        let format = self.resources.target_format;
629        // Ensure shared infrastructure (pipelines, BGLs, samplers) exists.
630        self.resources.ensure_hdr_shared(device, queue, format);
631        // Ensure the slot exists.
632        self.ensure_viewport_slot(device, viewport_index);
633        let slot = &mut self.viewport_slots[viewport_index];
634        // Create or resize the per-viewport HDR state.
635        let needs_create = match &slot.hdr {
636            None => true,
637            Some(h_state) => h_state.size != [w, h] || h_state.ssaa_factor != ssaa_factor,
638        };
639        if needs_create {
640            slot.hdr = Some(self.resources.create_hdr_viewport_state(
641                device,
642                queue,
643                format,
644                w,
645                h,
646                ssaa_factor,
647            ));
648        }
649    }
650}