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