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 indirect;
10mod picking;
11mod prepare;
12mod render;
13pub mod shader_hashes;
14mod shadows;
15pub mod stats;
16
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, LoadingBarAnchor, LoadingBarItem, OverlayFrame, OverlayImageItem, PickId,
22 PointCloudItem, PointRenderMode,
23 aabb_wireframe_polyline, PolylineItem, PostProcessSettings, RenderCamera, RulerItem, ScalarBarAnchor, ScalarBarItem,
24 ScalarBarOrientation, SceneEffects,
25 RibbonItem, SceneFrame, SceneRenderItem, ScreenImageItem,
26 ShadowFilter, SliceAxis, StreamtubeItem, SurfaceLICConfig, SurfaceLICItem, SurfaceSubmission,
27 ImageSliceItem, TensorGlyphItem, ToneMapping, TubeItem,
28 TransparentVolumeMeshItem,
29 ViewportEffects, ViewportFrame, VolumeItem,
30};
31
32/// An opaque handle to a per-viewport GPU state slot.
33///
34/// Obtained from [`ViewportRenderer::create_viewport`] and passed to
35/// [`ViewportRenderer::prepare_viewport`], [`ViewportRenderer::paint_viewport`],
36/// and [`ViewportRenderer::render_viewport`].
37///
38/// The inner `usize` is the slot index and doubles as the value for
39/// [`CameraFrame::with_viewport_index`]. Single-viewport applications that use
40/// the legacy [`ViewportRenderer::prepare`] / [`ViewportRenderer::paint`] API do
41/// not need this type.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43pub struct ViewportId(pub usize);
44
45use self::shadows::{compute_cascade_matrix, compute_cascade_splits};
46use self::types::{INSTANCING_THRESHOLD, InstancedBatch};
47use crate::resources::{
48 BatchMeta, CameraUniform, ClipPlanesUniform, ClipVolumeUniform, GridUniform, InstanceAabb,
49 InstanceData, LightsUniform, ObjectUniform, OutlineEdgeUniform, OutlineObjectBuffers,
50 OutlineUniform, PickInstance, ShadowAtlasUniform, SingleLightUniform, ViewportGpuResources,
51};
52
53/// Per-viewport GPU state: uniform buffers and bind groups that differ per viewport.
54///
55/// Each viewport slot owns its own camera, clip planes, clip volume, shadow info,
56/// and grid buffers, plus the bind groups that reference them. Scene-global
57/// resources (lights, shadow atlas texture, IBL) are shared via the bind group
58/// pointing to buffers on `ViewportGpuResources`.
59pub(crate) struct ViewportSlot {
60 pub camera_buf: wgpu::Buffer,
61 pub clip_planes_buf: wgpu::Buffer,
62 pub clip_volume_buf: wgpu::Buffer,
63 pub shadow_info_buf: wgpu::Buffer,
64 pub grid_buf: wgpu::Buffer,
65 /// Camera bind group (group 0) referencing this slot's per-viewport buffers
66 /// plus shared scene-global resources.
67 pub camera_bind_group: wgpu::BindGroup,
68 /// Grid bind group (group 0 for grid pipeline) referencing this slot's grid buffer.
69 pub grid_bind_group: wgpu::BindGroup,
70 /// Per-viewport HDR post-process render targets.
71 ///
72 /// Created lazily on first HDR render call and resized when viewport dimensions change.
73 pub hdr: Option<crate::resources::ViewportHdrState>,
74
75 // --- Per-viewport interaction state (Phase 4) ---
76 /// Per-frame outline buffers for selected objects, rebuilt in prepare().
77 pub outline_object_buffers: Vec<OutlineObjectBuffers>,
78 /// Per-frame x-ray buffers for selected objects, rebuilt in prepare().
79 pub xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)>,
80 /// Per-frame constraint guide line buffers, rebuilt in prepare().
81 pub constraint_line_buffers: Vec<(
82 wgpu::Buffer,
83 wgpu::Buffer,
84 u32,
85 wgpu::Buffer,
86 wgpu::BindGroup,
87 )>,
88 /// Per-frame cap geometry buffers (section view cross-section fill), rebuilt in prepare().
89 pub cap_buffers: Vec<(
90 wgpu::Buffer,
91 wgpu::Buffer,
92 u32,
93 wgpu::Buffer,
94 wgpu::BindGroup,
95 )>,
96 /// Per-frame clip plane fill overlay buffers, rebuilt in prepare().
97 pub clip_plane_fill_buffers: Vec<(
98 wgpu::Buffer,
99 wgpu::Buffer,
100 u32,
101 wgpu::Buffer,
102 wgpu::BindGroup,
103 )>,
104 /// Per-frame clip plane line overlay buffers, rebuilt in prepare().
105 pub clip_plane_line_buffers: Vec<(
106 wgpu::Buffer,
107 wgpu::Buffer,
108 u32,
109 wgpu::Buffer,
110 wgpu::BindGroup,
111 )>,
112 /// Vertex buffer for axes indicator geometry (rebuilt each frame).
113 pub axes_vertex_buffer: wgpu::Buffer,
114 /// Number of vertices in the axes indicator buffer.
115 pub axes_vertex_count: u32,
116 /// Gizmo model-matrix uniform buffer.
117 pub gizmo_uniform_buf: wgpu::Buffer,
118 /// Gizmo bind group (group 1: model matrix uniform).
119 pub gizmo_bind_group: wgpu::BindGroup,
120 /// Gizmo vertex buffer.
121 pub gizmo_vertex_buffer: wgpu::Buffer,
122 /// Gizmo index buffer.
123 pub gizmo_index_buffer: wgpu::Buffer,
124 /// Number of indices in the current gizmo mesh.
125 pub gizmo_index_count: u32,
126
127 // --- Sub-object highlight (per-viewport, generation-cached) ---
128 /// Per-viewport dynamic resolution intermediate render target.
129 /// `None` when render_scale == 1.0 or not yet initialised.
130 pub dyn_res: Option<crate::resources::dyn_res::DynResTarget>,
131 /// Cached GPU data for sub-object highlight rendering.
132 /// `None` when no sub-object selection is active.
133 pub sub_highlight: Option<crate::resources::SubHighlightGpuData>,
134 /// Version of the last sub-selection snapshot that was uploaded.
135 /// `u64::MAX` forces a rebuild on the first frame.
136 pub sub_highlight_generation: u64,
137}
138
139/// Renderer wrapping all GPU resources and providing `prepare()` and `paint()` methods.
140pub struct ViewportRenderer {
141 resources: ViewportGpuResources,
142 /// Instanced batches prepared for the current frame. Empty when using per-object path.
143 instanced_batches: Vec<InstancedBatch>,
144 /// Whether the current frame uses the instanced draw path.
145 use_instancing: bool,
146 /// True when the device supports `INDIRECT_FIRST_INSTANCE`.
147 gpu_culling_supported: bool,
148 /// True when GPU-driven culling is active (supported and not disabled by the caller).
149 gpu_culling_enabled: bool,
150 /// GPU culling compute pipelines and frustum buffer. Created lazily on the first
151 /// frame where `gpu_culling_enabled` is true and instance buffers are present.
152 cull_resources: Option<indirect::CullResources>,
153 /// Performance counters from the last frame.
154 last_stats: crate::renderer::stats::FrameStats,
155 /// Last scene generation seen during prepare(). u64::MAX forces rebuild on first frame.
156 last_scene_generation: u64,
157 /// Last selection generation seen during prepare(). u64::MAX forces rebuild on first frame.
158 last_selection_generation: u64,
159 /// Last scene_items count seen during prepare(). usize::MAX forces rebuild on first frame.
160 /// Included in cache key so that frustum-culling changes (different visible set, different
161 /// count) correctly invalidate the instance buffer even when scene_generation is stable.
162 last_scene_items_count: usize,
163 /// Count of items that passed the instanced-path filter on the last rebuild.
164 /// Used in place of has_per_frame_mutations so scenes that mix instanced and
165 /// non-instanced items (e.g. one two-sided mesh + 10k static boxes) still hit
166 /// the instanced batch cache on frames where the filtered set is unchanged.
167 last_instancable_count: usize,
168 /// Cached instance data from last rebuild (mirrors the GPU buffer contents).
169 cached_instance_data: Vec<InstanceData>,
170 /// Cached instanced batch descriptors from last rebuild.
171 cached_instanced_batches: Vec<InstancedBatch>,
172 /// Per-frame point cloud GPU data, rebuilt in prepare(), consumed in paint().
173 point_cloud_gpu_data: Vec<crate::resources::PointCloudGpuData>,
174 /// Per-frame glyph GPU data, rebuilt in prepare(), consumed in paint().
175 glyph_gpu_data: Vec<crate::resources::GlyphGpuData>,
176 /// Per-frame tensor glyph GPU data, rebuilt in prepare(), consumed in paint() (Phase 5).
177 tensor_glyph_gpu_data: Vec<crate::resources::TensorGlyphGpuData>,
178 /// Per-frame polyline GPU data, rebuilt in prepare(), consumed in paint().
179 polyline_gpu_data: Vec<crate::resources::PolylineGpuData>,
180 /// Per-frame volume GPU data, rebuilt in prepare(), consumed in paint().
181 volume_gpu_data: Vec<crate::resources::VolumeGpuData>,
182 /// Per-frame streamtube GPU data, rebuilt in prepare(), consumed in paint().
183 streamtube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
184 /// Per-frame general tube GPU data, rebuilt in prepare(), consumed in paint() (Phase 3).
185 tube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
186 /// Per-frame ribbon GPU data, rebuilt in prepare(), consumed in paint() (Phase 8.1).
187 ribbon_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
188 /// Per-frame image slice GPU data, rebuilt in prepare(), consumed in paint() (Phase 3).
189 image_slice_gpu_data: Vec<crate::resources::ImageSliceGpuData>,
190 /// Per-frame Surface LIC GPU data, rebuilt in prepare(), consumed in paint() (Phase 4).
191 lic_gpu_data: Vec<crate::resources::LicSurfaceGpuData>,
192 /// Per-frame GPU implicit surface data, rebuilt in prepare(), consumed in paint() (Phase 16).
193 implicit_gpu_data: Vec<crate::resources::implicit::ImplicitGpuItem>,
194 /// Per-frame GPU marching cubes render data, rebuilt in prepare(), consumed in paint() (Phase 17).
195 mc_gpu_data: Vec<crate::resources::gpu_marching_cubes::McFrameData>,
196 /// Per-frame screen-image GPU data, rebuilt in prepare(), consumed in paint() (Phase 10B).
197 screen_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
198 /// Per-frame overlay image GPU data, rebuilt in prepare(), consumed in paint() (Phase 7).
199 overlay_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
200 /// Per-frame overlay label GPU data, rebuilt in prepare(), consumed in paint().
201 label_gpu_data: Option<crate::resources::LabelGpuData>,
202 /// Per-frame scalar bar GPU data, rebuilt in prepare(), consumed in paint().
203 scalar_bar_gpu_data: Option<crate::resources::LabelGpuData>,
204 /// Per-frame ruler GPU data, rebuilt in prepare(), consumed in paint().
205 ruler_gpu_data: Option<crate::resources::LabelGpuData>,
206 /// Per-frame loading bar GPU data, rebuilt in prepare(), consumed in paint().
207 loading_bar_gpu_data: Option<crate::resources::LabelGpuData>,
208 /// Per-viewport GPU state slots.
209 ///
210 /// Indexed by `FrameData::camera.viewport_index`. Each slot owns independent
211 /// uniform buffers and bind groups for camera, clip planes, clip volume,
212 /// shadow info, and grid. Slots are grown lazily in `prepare` via
213 /// `ensure_viewport_slot`. There are at most 4 in the current UI.
214 viewport_slots: Vec<ViewportSlot>,
215 /// Phase G : GPU compute filter results from the last `prepare()` call.
216 ///
217 /// Each entry contains a compacted index buffer + count for one filtered mesh.
218 /// Consumed during `paint()` to override the mesh's default index buffer.
219 /// Cleared and rebuilt each frame.
220 compute_filter_results: Vec<crate::resources::ComputeFilterResult>,
221 /// Cascade-0 light-space view-projection matrix from the last shadow prepare.
222 /// Cached here so `prepare_viewport_internal` can copy it into the ground plane uniform.
223 last_cascade0_shadow_mat: glam::Mat4,
224 /// Current runtime mode controlling internal default behavior.
225 runtime_mode: crate::renderer::stats::RuntimeMode,
226 /// Active performance policy: target FPS, render scale bounds, and permitted reductions.
227 performance_policy: crate::renderer::stats::PerformancePolicy,
228 /// Current render scale tracked by the adaptation controller (or set manually).
229 ///
230 /// Clamped to `[policy.min_render_scale, policy.max_render_scale]`.
231 /// Reported in `FrameStats::render_scale` each frame.
232 current_render_scale: f32,
233 /// Instant recorded at the start of the most recent `prepare()` call.
234 /// Used to compute `total_frame_ms` on the following frame.
235 last_prepare_instant: Option<std::time::Instant>,
236 /// Frame counter incremented each `prepare()` call. Used for picking throttle in Playback mode.
237 frame_counter: u64,
238
239 // --- Phase 4 : GPU timestamp queries ---
240 /// Timestamp query set with 2 entries (scene-pass begin + end).
241 /// `None` when `TIMESTAMP_QUERY` is unavailable or not yet initialized.
242 ts_query_set: Option<wgpu::QuerySet>,
243 /// Resolve buffer: 2 × u64, GPU-only (`QUERY_RESOLVE | COPY_SRC`).
244 ts_resolve_buf: Option<wgpu::Buffer>,
245 /// Staging buffer: 2 × u64, CPU-readable (`COPY_DST | MAP_READ`).
246 ts_staging_buf: Option<wgpu::Buffer>,
247 /// Nanoseconds per GPU timestamp tick, from `queue.get_timestamp_period()`.
248 ts_period: f32,
249 /// Whether the staging buffer holds unread timestamp data from the previous frame.
250 ts_needs_readback: bool,
251
252 // --- Indirect-args readback (GPU-driven culling visible instance count) ---
253 /// CPU-readable staging buffer for `indirect_args_buf` (batch_count × 20 bytes).
254 /// Grown lazily; never shrunk.
255 indirect_readback_buf: Option<wgpu::Buffer>,
256 /// Number of batches whose data was copied into `indirect_readback_buf` last frame.
257 indirect_readback_batch_count: u32,
258 /// True when `indirect_readback_buf` holds unread data from the previous cull pass.
259 indirect_readback_pending: bool,
260
261 // --- Per-pass degradation state (Phases 6 + 11) ---
262 /// Tiered degradation ladder position (0 = none, 1 = shadows, 2 = volumes, 3 = effects).
263 /// Advanced one step per over-budget frame once render scale hits minimum;
264 /// reversed one step per comfortably-under-budget frame.
265 degradation_tier: u8,
266 /// Whether the shadow pass was skipped this frame due to budget pressure.
267 /// Computed once per frame at the top of prepare() and used by both
268 /// prepare_scene_internal and reported in FrameStats.
269 degradation_shadows_skipped: bool,
270 /// Whether volume raymarch step size was doubled this frame due to budget pressure.
271 degradation_volume_quality_reduced: bool,
272 /// Whether SSAO, contact shadows, and bloom were skipped this frame.
273 /// Set in prepare(); read by the render path.
274 degradation_effects_throttled: bool,
275}
276
277impl ViewportRenderer {
278 /// Create a new renderer with default settings (no MSAA).
279 /// Call once at application startup.
280 pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
281 Self::with_sample_count(device, target_format, 1)
282 }
283
284 /// Create a new renderer with the specified MSAA sample count (1, 2, or 4).
285 ///
286 /// When using MSAA (sample_count > 1), the caller must create multisampled
287 /// color and depth textures and use them as render pass attachments with the
288 /// final surface texture as the resolve target.
289 pub fn with_sample_count(
290 device: &wgpu::Device,
291 target_format: wgpu::TextureFormat,
292 sample_count: u32,
293 ) -> Self {
294 let gpu_culling_supported = device
295 .features()
296 .contains(wgpu::Features::INDIRECT_FIRST_INSTANCE);
297 Self {
298 resources: ViewportGpuResources::new(device, target_format, sample_count),
299 instanced_batches: Vec::new(),
300 use_instancing: false,
301 gpu_culling_supported,
302 gpu_culling_enabled: gpu_culling_supported,
303 cull_resources: None,
304 last_stats: crate::renderer::stats::FrameStats::default(),
305 last_scene_generation: u64::MAX,
306 last_selection_generation: u64::MAX,
307 last_scene_items_count: usize::MAX,
308 last_instancable_count: usize::MAX,
309 cached_instance_data: Vec::new(),
310 cached_instanced_batches: Vec::new(),
311 point_cloud_gpu_data: Vec::new(),
312 glyph_gpu_data: Vec::new(),
313 tensor_glyph_gpu_data: Vec::new(),
314 polyline_gpu_data: Vec::new(),
315 volume_gpu_data: Vec::new(),
316 streamtube_gpu_data: Vec::new(),
317 tube_gpu_data: Vec::new(),
318 ribbon_gpu_data: Vec::new(),
319 image_slice_gpu_data: Vec::new(),
320 lic_gpu_data: Vec::new(),
321 implicit_gpu_data: Vec::new(),
322 mc_gpu_data: Vec::new(),
323 screen_image_gpu_data: Vec::new(),
324 overlay_image_gpu_data: Vec::new(),
325 label_gpu_data: None,
326 scalar_bar_gpu_data: None,
327 ruler_gpu_data: None,
328 loading_bar_gpu_data: None,
329 viewport_slots: Vec::new(),
330 compute_filter_results: Vec::new(),
331 last_cascade0_shadow_mat: glam::Mat4::IDENTITY,
332 runtime_mode: crate::renderer::stats::RuntimeMode::Interactive,
333 performance_policy: crate::renderer::stats::PerformancePolicy::default(),
334 current_render_scale: 1.0,
335 last_prepare_instant: None,
336 frame_counter: 0,
337 ts_query_set: None,
338 ts_resolve_buf: None,
339 ts_staging_buf: None,
340 ts_period: 1.0,
341 ts_needs_readback: false,
342 indirect_readback_buf: None,
343 indirect_readback_batch_count: 0,
344 indirect_readback_pending: false,
345 degradation_tier: 0,
346 degradation_shadows_skipped: false,
347 degradation_volume_quality_reduced: false,
348 degradation_effects_throttled: false,
349 }
350 }
351
352 /// Access the underlying GPU resources (e.g. for mesh uploads).
353 pub fn resources(&self) -> &ViewportGpuResources {
354 &self.resources
355 }
356
357 /// Performance counters from the last completed frame.
358 pub fn last_frame_stats(&self) -> crate::renderer::stats::FrameStats {
359 self.last_stats
360 }
361
362 /// Disable GPU-driven culling, reverting to the direct draw path.
363 ///
364 /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`
365 /// (culling is already disabled on those devices).
366 pub fn disable_gpu_driven_culling(&mut self) {
367 self.gpu_culling_enabled = false;
368 }
369
370 /// Re-enable GPU-driven culling after a call to `disable_gpu_driven_culling`.
371 ///
372 /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`.
373 pub fn enable_gpu_driven_culling(&mut self) {
374 if self.gpu_culling_supported {
375 self.gpu_culling_enabled = true;
376 }
377 }
378
379 /// Set the runtime mode controlling internal default behavior.
380 ///
381 /// - [`RuntimeMode::Interactive`]: full picking rate, full quality (default).
382 /// - [`RuntimeMode::Playback`]: picking throttled to reduce CPU overhead during animation.
383 /// - [`RuntimeMode::Paused`]: full picking rate, full quality.
384 /// - [`RuntimeMode::Capture`]: full quality, intended for screenshot/export workflows.
385 pub fn set_runtime_mode(&mut self, mode: crate::renderer::stats::RuntimeMode) {
386 self.runtime_mode = mode;
387 }
388
389 /// Return the current runtime mode.
390 pub fn runtime_mode(&self) -> crate::renderer::stats::RuntimeMode {
391 self.runtime_mode
392 }
393
394 /// Set the performance policy controlling target FPS, render scale bounds,
395 /// and permitted quality reductions.
396 ///
397 /// The internal adaptation controller activates when
398 /// `policy.allow_dynamic_resolution` is `true` and `policy.target_fps` is
399 /// `Some`. It adjusts `render_scale` within `[min_render_scale,
400 /// max_render_scale]` each frame based on `total_frame_ms`.
401 pub fn set_performance_policy(
402 &mut self,
403 policy: crate::renderer::stats::PerformancePolicy,
404 ) {
405 self.performance_policy = policy;
406 // Clamp current scale into the new bounds immediately.
407 self.current_render_scale = self.current_render_scale.clamp(
408 policy.min_render_scale,
409 policy.max_render_scale,
410 );
411 }
412
413 /// Return the active performance policy.
414 pub fn performance_policy(&self) -> crate::renderer::stats::PerformancePolicy {
415 self.performance_policy
416 }
417
418 /// Manually set the render scale.
419 ///
420 /// Effective when `performance_policy.allow_dynamic_resolution` is `false`.
421 /// When dynamic resolution is enabled the adaptation controller overrides
422 /// this value each frame.
423 ///
424 /// The value is clamped to `[policy.min_render_scale, policy.max_render_scale]`.
425 ///
426 /// Has no effect on the HDR render path (`render` / `render_viewport` with
427 /// `PostProcessSettings::enabled = true`). See `allow_dynamic_resolution`.
428 pub fn set_render_scale(&mut self, scale: f32) {
429 self.current_render_scale = scale.clamp(
430 self.performance_policy.min_render_scale,
431 self.performance_policy.max_render_scale,
432 );
433 }
434
435 /// Set the target frame rate used to compute [`FrameStats::missed_budget`].
436 ///
437 /// Convenience wrapper that updates `performance_policy.target_fps`.
438 pub fn set_target_fps(&mut self, fps: Option<f32>) {
439 self.performance_policy.target_fps = fps;
440 }
441
442 /// Mutable access to the underlying GPU resources (e.g. for mesh uploads).
443 pub fn resources_mut(&mut self) -> &mut ViewportGpuResources {
444 &mut self.resources
445 }
446
447 /// Upload an equirectangular HDR environment map and precompute IBL textures.
448 ///
449 /// `pixels` is row-major RGBA f32 data (4 floats per texel), `width`×`height`.
450 /// This rebuilds camera bind groups so shaders immediately see the new textures.
451 pub fn upload_environment_map(
452 &mut self,
453 device: &wgpu::Device,
454 queue: &wgpu::Queue,
455 pixels: &[f32],
456 width: u32,
457 height: u32,
458 ) {
459 crate::resources::environment::upload_environment_map(
460 &mut self.resources,
461 device,
462 queue,
463 pixels,
464 width,
465 height,
466 );
467 self.rebuild_camera_bind_groups(device);
468 }
469
470 /// Rebuild the primary + per-viewport camera bind groups.
471 ///
472 /// Call after IBL textures are uploaded so shaders see the new environment.
473 fn rebuild_camera_bind_groups(&mut self, device: &wgpu::Device) {
474 self.resources.camera_bind_group = self.resources.create_camera_bind_group(
475 device,
476 &self.resources.camera_uniform_buf,
477 &self.resources.clip_planes_uniform_buf,
478 &self.resources.shadow_info_buf,
479 &self.resources.clip_volume_uniform_buf,
480 "camera_bind_group",
481 );
482
483 for slot in &mut self.viewport_slots {
484 slot.camera_bind_group = self.resources.create_camera_bind_group(
485 device,
486 &slot.camera_buf,
487 &slot.clip_planes_buf,
488 &slot.shadow_info_buf,
489 &slot.clip_volume_buf,
490 "per_viewport_camera_bg",
491 );
492 }
493 }
494
495 /// Ensure a per-viewport slot exists for `viewport_index`.
496 ///
497 /// Creates a full `ViewportSlot` with independent uniform buffers for camera,
498 /// clip planes, clip volume, shadow info, and grid. The camera bind group
499 /// references this slot's per-viewport buffers plus shared scene-global
500 /// resources. Slots are created lazily and never destroyed.
501 fn ensure_viewport_slot(&mut self, device: &wgpu::Device, viewport_index: usize) {
502 while self.viewport_slots.len() <= viewport_index {
503 let camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
504 label: Some("vp_camera_buf"),
505 size: std::mem::size_of::<CameraUniform>() as u64,
506 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
507 mapped_at_creation: false,
508 });
509 let clip_planes_buf = device.create_buffer(&wgpu::BufferDescriptor {
510 label: Some("vp_clip_planes_buf"),
511 size: std::mem::size_of::<ClipPlanesUniform>() as u64,
512 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
513 mapped_at_creation: false,
514 });
515 let clip_volume_buf = device.create_buffer(&wgpu::BufferDescriptor {
516 label: Some("vp_clip_volume_buf"),
517 size: std::mem::size_of::<ClipVolumeUniform>() as u64,
518 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
519 mapped_at_creation: false,
520 });
521 let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
522 label: Some("vp_shadow_info_buf"),
523 size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
524 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
525 mapped_at_creation: false,
526 });
527 let grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
528 label: Some("vp_grid_buf"),
529 size: std::mem::size_of::<GridUniform>() as u64,
530 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
531 mapped_at_creation: false,
532 });
533
534 let camera_bind_group = self.resources.create_camera_bind_group(
535 device,
536 &camera_buf,
537 &clip_planes_buf,
538 &shadow_info_buf,
539 &clip_volume_buf,
540 "per_viewport_camera_bg",
541 );
542
543 let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
544 label: Some("vp_grid_bind_group"),
545 layout: &self.resources.grid_bind_group_layout,
546 entries: &[wgpu::BindGroupEntry {
547 binding: 0,
548 resource: grid_buf.as_entire_binding(),
549 }],
550 });
551
552 // Per-viewport gizmo buffers (initial mesh: Translate, no hover, identity orientation).
553 let (gizmo_verts, gizmo_indices) = crate::interaction::gizmo::build_gizmo_mesh(
554 crate::interaction::gizmo::GizmoMode::Translate,
555 crate::interaction::gizmo::GizmoAxis::None,
556 glam::Quat::IDENTITY,
557 );
558 let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
559 label: Some("vp_gizmo_vertex_buf"),
560 size: (std::mem::size_of::<crate::resources::Vertex>() * gizmo_verts.len().max(1))
561 as u64,
562 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
563 mapped_at_creation: true,
564 });
565 gizmo_vertex_buffer
566 .slice(..)
567 .get_mapped_range_mut()
568 .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
569 gizmo_vertex_buffer.unmap();
570 let gizmo_index_count = gizmo_indices.len() as u32;
571 let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
572 label: Some("vp_gizmo_index_buf"),
573 size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
574 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
575 mapped_at_creation: true,
576 });
577 gizmo_index_buffer
578 .slice(..)
579 .get_mapped_range_mut()
580 .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
581 gizmo_index_buffer.unmap();
582 let gizmo_uniform = crate::interaction::gizmo::GizmoUniform {
583 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
584 };
585 let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
586 label: Some("vp_gizmo_uniform_buf"),
587 size: std::mem::size_of::<crate::interaction::gizmo::GizmoUniform>() as u64,
588 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
589 mapped_at_creation: true,
590 });
591 gizmo_uniform_buf
592 .slice(..)
593 .get_mapped_range_mut()
594 .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
595 gizmo_uniform_buf.unmap();
596 let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
597 label: Some("vp_gizmo_bind_group"),
598 layout: &self.resources.gizmo_bind_group_layout,
599 entries: &[wgpu::BindGroupEntry {
600 binding: 0,
601 resource: gizmo_uniform_buf.as_entire_binding(),
602 }],
603 });
604
605 // Per-viewport axes vertex buffer (2048 vertices = enough for all axes geometry).
606 let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
607 label: Some("vp_axes_vertex_buf"),
608 size: (std::mem::size_of::<crate::widgets::axes_indicator::AxesVertex>() * 2048)
609 as u64,
610 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
611 mapped_at_creation: false,
612 });
613
614 self.viewport_slots.push(ViewportSlot {
615 camera_buf,
616 clip_planes_buf,
617 clip_volume_buf,
618 shadow_info_buf,
619 grid_buf,
620 camera_bind_group,
621 grid_bind_group,
622 hdr: None,
623 outline_object_buffers: Vec::new(),
624 xray_object_buffers: Vec::new(),
625 constraint_line_buffers: Vec::new(),
626 cap_buffers: Vec::new(),
627 clip_plane_fill_buffers: Vec::new(),
628 clip_plane_line_buffers: Vec::new(),
629 axes_vertex_buffer,
630 axes_vertex_count: 0,
631 gizmo_uniform_buf,
632 gizmo_bind_group,
633 gizmo_vertex_buffer,
634 gizmo_index_buffer,
635 gizmo_index_count,
636 sub_highlight: None,
637 sub_highlight_generation: u64::MAX,
638 dyn_res: None,
639 });
640 }
641 }
642
643 // -----------------------------------------------------------------------
644 // Multi-viewport public API (Phase 5)
645 // -----------------------------------------------------------------------
646
647 /// Create a new viewport slot and return its handle.
648 ///
649 /// The returned [`ViewportId`] is stable for the lifetime of the renderer.
650 /// Pass it to [`prepare_viewport`](Self::prepare_viewport),
651 /// [`paint_viewport`](Self::paint_viewport), and
652 /// [`render_viewport`](Self::render_viewport) each frame.
653 ///
654 /// Also set `CameraFrame::viewport_index` to `id.0` when building the
655 /// [`FrameData`] for this viewport:
656 /// ```rust,ignore
657 /// let id = renderer.create_viewport(&device);
658 /// let frame = FrameData {
659 /// camera: CameraFrame::from_camera(&cam, size).with_viewport_index(id.0),
660 /// ..Default::default()
661 /// };
662 /// ```
663 pub fn create_viewport(&mut self, device: &wgpu::Device) -> ViewportId {
664 let idx = self.viewport_slots.len();
665 self.ensure_viewport_slot(device, idx);
666 ViewportId(idx)
667 }
668
669 /// Release the heavy GPU texture memory (HDR targets, OIT, bloom, SSAO) held
670 /// by `id`.
671 ///
672 /// The slot index is not reclaimed : future calls with this `ViewportId` will
673 /// lazily recreate the texture resources as needed. This is useful when a
674 /// viewport is hidden or minimised and you want to reduce VRAM pressure without
675 /// invalidating the handle.
676 pub fn destroy_viewport(&mut self, id: ViewportId) {
677 if let Some(slot) = self.viewport_slots.get_mut(id.0) {
678 slot.hdr = None;
679 }
680 }
681
682 /// Prepare shared scene data. Call **once per frame**, before any
683 /// [`prepare_viewport`](Self::prepare_viewport) calls.
684 ///
685 /// `frame` provides the scene content (`frame.scene`) and the primary camera
686 /// used for shadow cascade framing (`frame.camera`). In a multi-viewport
687 /// setup use any one viewport's `FrameData` here : typically the perspective
688 /// view : as the shadow framing reference.
689 ///
690 /// `scene_effects` carries the scene-global effects: lighting, environment
691 /// map, and compute filters. Obtain it by constructing [`SceneEffects`]
692 /// directly or via [`EffectsFrame::split`].
693 pub fn prepare_scene(
694 &mut self,
695 device: &wgpu::Device,
696 queue: &wgpu::Queue,
697 frame: &FrameData,
698 scene_effects: &SceneEffects<'_>,
699 ) {
700 self.prepare_scene_internal(device, queue, frame, scene_effects);
701 }
702
703 /// Prepare per-viewport GPU state (camera, clip planes, overlays, axes).
704 ///
705 /// Call once per viewport per frame, **after** [`prepare_scene`](Self::prepare_scene).
706 ///
707 /// `id` must have been obtained from [`create_viewport`](Self::create_viewport).
708 /// `frame.camera.viewport_index` must equal `id.0`; use
709 /// [`CameraFrame::with_viewport_index`] when building the frame.
710 pub fn prepare_viewport(
711 &mut self,
712 device: &wgpu::Device,
713 queue: &wgpu::Queue,
714 id: ViewportId,
715 frame: &FrameData,
716 ) {
717 debug_assert_eq!(
718 frame.camera.viewport_index, id.0,
719 "frame.camera.viewport_index ({}) must equal the ViewportId ({}); \
720 use CameraFrame::with_viewport_index(id.0)",
721 frame.camera.viewport_index, id.0,
722 );
723 let (_, viewport_fx) = frame.effects.split();
724 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
725 }
726
727 /// Issue draw calls for `id` into a `'static` render pass (as provided by egui callbacks).
728 ///
729 /// This is the method to use from an egui/eframe `CallbackTrait::paint` implementation.
730 /// Call [`prepare_scene`](Self::prepare_scene) and [`prepare_viewport`](Self::prepare_viewport)
731 /// first (in `CallbackTrait::prepare`), then set the render pass viewport/scissor to confine
732 /// drawing to the correct quadrant, and call this method.
733 ///
734 /// For non-`'static` render passes (winit, iced, manual wgpu), use
735 /// [`paint_viewport_to`](Self::paint_viewport_to).
736 pub fn paint_viewport(
737 &self,
738 render_pass: &mut wgpu::RenderPass<'static>,
739 id: ViewportId,
740 frame: &FrameData,
741 ) {
742 let vp_idx = id.0;
743 let camera_bg = self.viewport_camera_bind_group(vp_idx);
744 let grid_bg = self.viewport_grid_bind_group(vp_idx);
745 let vp_slot = self.viewport_slots.get(vp_idx);
746 emit_draw_calls!(
747 &self.resources,
748 &mut *render_pass,
749 frame,
750 self.use_instancing,
751 &self.instanced_batches,
752 camera_bg,
753 grid_bg,
754 &self.compute_filter_results,
755 vp_slot
756 );
757 emit_scivis_draw_calls!(
758 &self.resources,
759 render_pass,
760 &self.point_cloud_gpu_data,
761 &self.glyph_gpu_data,
762 &self.polyline_gpu_data,
763 &self.volume_gpu_data,
764 &self.streamtube_gpu_data,
765 camera_bg,
766 &self.tube_gpu_data,
767 &self.image_slice_gpu_data,
768 &self.tensor_glyph_gpu_data,
769 &self.ribbon_gpu_data
770 );
771 }
772
773 /// Issue draw calls for `id` into a render pass with any lifetime.
774 ///
775 /// Identical to [`paint_viewport`](Self::paint_viewport) but accepts a render pass with a
776 /// non-`'static` lifetime, making it usable from winit, iced, or raw wgpu where the encoder
777 /// creates its own render pass.
778 pub fn paint_viewport_to<'rp>(
779 &'rp self,
780 render_pass: &mut wgpu::RenderPass<'rp>,
781 id: ViewportId,
782 frame: &FrameData,
783 ) {
784 let vp_idx = id.0;
785 let camera_bg = self.viewport_camera_bind_group(vp_idx);
786 let grid_bg = self.viewport_grid_bind_group(vp_idx);
787 let vp_slot = self.viewport_slots.get(vp_idx);
788 emit_draw_calls!(
789 &self.resources,
790 &mut *render_pass,
791 frame,
792 self.use_instancing,
793 &self.instanced_batches,
794 camera_bg,
795 grid_bg,
796 &self.compute_filter_results,
797 vp_slot
798 );
799 emit_scivis_draw_calls!(
800 &self.resources,
801 render_pass,
802 &self.point_cloud_gpu_data,
803 &self.glyph_gpu_data,
804 &self.polyline_gpu_data,
805 &self.volume_gpu_data,
806 &self.streamtube_gpu_data,
807 camera_bg,
808 &self.tube_gpu_data,
809 &self.image_slice_gpu_data,
810 &self.tensor_glyph_gpu_data,
811 &self.ribbon_gpu_data
812 );
813 }
814
815 /// Return a reference to the camera bind group for the given viewport slot.
816 ///
817 /// Falls back to `resources.camera_bind_group` if no per-viewport slot
818 /// exists (e.g. in single-viewport mode before the first prepare call).
819 fn viewport_camera_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
820 self.viewport_slots
821 .get(viewport_index)
822 .map(|slot| &slot.camera_bind_group)
823 .unwrap_or(&self.resources.camera_bind_group)
824 }
825
826 /// Return a reference to the grid bind group for the given viewport slot.
827 ///
828 /// Falls back to `resources.grid_bind_group` if no per-viewport slot exists.
829 fn viewport_grid_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
830 self.viewport_slots
831 .get(viewport_index)
832 .map(|slot| &slot.grid_bind_group)
833 .unwrap_or(&self.resources.grid_bind_group)
834 }
835
836 /// Ensure the dyn-res intermediate render target exists for `vp_idx` at the
837 /// given `scaled_size`, creating or recreating it when size changes.
838 ///
839 /// `surface_size` is the native output dimensions (used to size the upscale
840 /// blit correctly). `ensure_dyn_res_pipeline` is called automatically.
841 pub(crate) fn ensure_dyn_res_target(
842 &mut self,
843 device: &wgpu::Device,
844 vp_idx: usize,
845 scaled_size: [u32; 2],
846 surface_size: [u32; 2],
847 ) {
848 self.resources.ensure_dyn_res_pipeline(device);
849 let needs_create = match &self.viewport_slots[vp_idx].dyn_res {
850 None => true,
851 Some(dr) => dr.scaled_size != scaled_size || dr.surface_size != surface_size,
852 };
853 if needs_create {
854 let target =
855 self.resources.create_dyn_res_target(device, scaled_size, surface_size);
856 self.viewport_slots[vp_idx].dyn_res = Some(target);
857 }
858 }
859
860 /// Ensure per-viewport HDR state exists for `viewport_index` at dimensions `w`×`h`.
861 ///
862 /// Calls `ensure_hdr_shared` once to initialise shared pipelines/BGLs/samplers, then
863 /// lazily creates or resizes the `ViewportHdrState` inside the slot. Idempotent: if the
864 /// slot already has HDR state at the correct size nothing is recreated.
865 pub(crate) fn ensure_viewport_hdr(
866 &mut self,
867 device: &wgpu::Device,
868 queue: &wgpu::Queue,
869 viewport_index: usize,
870 w: u32,
871 h: u32,
872 ssaa_factor: u32,
873 ) {
874 let format = self.resources.target_format;
875 // Ensure shared infrastructure (pipelines, BGLs, samplers) exists.
876 self.resources.ensure_hdr_shared(device, queue, format);
877 // Ensure the slot exists.
878 self.ensure_viewport_slot(device, viewport_index);
879 let slot = &mut self.viewport_slots[viewport_index];
880 // Create or resize the per-viewport HDR state.
881 let needs_create = match &slot.hdr {
882 None => true,
883 Some(h_state) => h_state.size != [w, h] || h_state.ssaa_factor != ssaa_factor,
884 };
885 if needs_create {
886 slot.hdr = Some(self.resources.create_hdr_viewport_state(
887 device,
888 queue,
889 format,
890 w,
891 h,
892 ssaa_factor,
893 ));
894 }
895 }
896}