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