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