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