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