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