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