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