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}