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