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 paths;
11pub use paths::{OwnedPath, PassPath, PassView};
12mod picking;
13pub use picking::PickRectResult;
14mod prepare;
15mod render;
16pub mod shader_hashes;
17mod shadows;
18pub mod stats;
19mod shadow_debug_stats;
20pub use shadow_debug_stats::ShadowDebugStats;
21
22#[cfg(test)]
23mod hidden_tests;
24
25pub use self::types::{
26 AtlasViewerCorner,
27 CameraFrame, ClipObject, ClipShape, ComputeFilterItem, ComputeFilterKind,
28 CylindricalFacing, DecalAnimation, DecalBlendMode, DecalItem, DecalProjection,
29 DebugOutputMode, DebugQuantity, DebugVis,
30 EffectsFrame, EnvironmentMap, FilterMode, FrameData, GaussianSplatData, GaussianSplatId,
31 GaussianSplatItem, GlyphItem, GlyphType, GroundPlane, GroundPlaneMode, ImageAnchor,
32 ImageSliceItem, InteractionFrame, LabelAnchor, LabelItem, LightKind, LightSource,
33 LicOverlay, LightingSettings, LoadingBarAnchor, LoadingBarItem, OverlayFrame, OverlayImageItem,
34 OverlayAnimation, OverlayFill, OverlayRectItem, OverlayShape, OverlayShapeItem,
35 OverlayTextureId, BorderMode, LineCap, PickId, TriangleDirection,
36 PointCloudItem, PointRenderMode, PolylineItem, PostProcessSettings, RenderCamera, RibbonItem,
37 RulerItem, ScalarBarAnchor, ScalarBarItem, ScalarBarOrientation, SceneEffects, SceneFrame,
38 SceneRenderItem, ScreenImageItem, ShDegree, ShadowFilter, SliceAxis, SpriteItem,
39 SpriteSizeMode, StreamtubeItem, SurfaceLICConfig, SurfaceSubmission,
40 ScatterQuality, ScatterSettings, ScatterVolumeItem,
41 TensorGlyphItem, ToneMapping, TransparentVolumeMeshItem, TubeItem, ViewportEffects,
42 ViewportFrame, VolumeItem, VolumeMeshItem, VolumeSurfaceSliceItem, aabb_wireframe_polyline,
43 sphere_wireframe_polyline,
44};
45
46/// An opaque handle to a per-viewport GPU state slot.
47///
48/// Obtained from [`ViewportRenderer::create_viewport`] and passed to
49/// [`ViewportRenderer::prepare_viewport`], [`ViewportRenderer::paint_viewport`],
50/// and [`ViewportRenderer::render_viewport`].
51///
52/// The slot index is managed internally. To bind a `ViewportId` to a camera frame,
53/// use [`CameraFrame::with_viewport_id`]. Single-viewport applications that use
54/// the legacy [`ViewportRenderer::prepare`] / [`ViewportRenderer::paint`] API do
55/// not need this type.
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct ViewportId(pub(crate) usize);
58
59use self::shadows::{compute_cascade_matrix, compute_cascade_splits};
60use self::types::{INSTANCING_THRESHOLD, InstancedBatch};
61use crate::resources::{
62 BatchMeta, CLIP_VOLUME_MAX, CameraUniform, ClipPlanesUniform, ClipVolumeEntry,
63 ClipVolumesUniform, GridUniform, InstanceAabb, InstanceData, LightsUniform, ObjectUniform,
64 OutlineEdgeUniform, OutlineObjectBuffers, OutlineUniform, PickInstance, ShadowAtlasUniform,
65 SingleLightUniform, SplatOutlineMaskUniform, ViewportGpuResources,
66};
67
68/// Per-viewport GPU state: uniform buffers and bind groups that differ per viewport.
69///
70/// Each viewport slot owns its own camera, clip planes, clip volume, shadow info,
71/// and grid buffers, plus the bind groups that reference them. Scene-global
72/// resources (lights, shadow atlas texture, IBL) are shared via the bind group
73/// pointing to buffers on `ViewportGpuResources`.
74pub(crate) struct ViewportSlot {
75 pub camera_buf: wgpu::Buffer,
76 pub clip_planes_buf: wgpu::Buffer,
77 pub clip_volume_buf: wgpu::Buffer,
78 pub shadow_info_buf: wgpu::Buffer,
79 pub grid_buf: wgpu::Buffer,
80 /// Camera bind group (group 0) referencing this slot's per-viewport buffers
81 /// plus shared scene-global resources.
82 pub camera_bind_group: wgpu::BindGroup,
83 /// Grid bind group (group 0 for grid pipeline) referencing this slot's grid buffer.
84 pub grid_bind_group: wgpu::BindGroup,
85 /// Per-viewport HDR post-process render targets.
86 ///
87 /// Created lazily on first HDR render call and resized when viewport dimensions change.
88 pub hdr: Option<crate::resources::ViewportHdrState>,
89 /// Per-fragment debug storage buffer (group 0 binding 12). Allocated at
90 /// `width * height * 16` bytes when debug_vis is active; None otherwise.
91 pub debug_frag_buf: Option<wgpu::Buffer>,
92 /// Viewport dimensions for which `debug_frag_buf` was allocated.
93 pub debug_frag_dims: (u32, u32),
94
95 // --- Per-viewport interaction state ---
96 /// Per-frame outline buffers for selected objects, rebuilt in prepare().
97 pub outline_object_buffers: Vec<OutlineObjectBuffers>,
98 /// Per-frame outline buffers for selected Gaussian splat sets, rebuilt in prepare().
99 pub splat_outline_buffers: Vec<crate::resources::SplatOutlineBuffers>,
100 /// Indices into `volume_gpu_data` for selected volumes, rebuilt in prepare().
101 pub volume_outline_indices: Vec<usize>,
102 /// Indices into `glyph_gpu_data` for selected glyph sets, rebuilt in prepare().
103 /// Each entry is (gpu_data_index, instance_filter): None draws all instances,
104 /// Some(indices) draws only those specific instance indices.
105 pub glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
106 /// Indices into `tensor_glyph_gpu_data` for selected tensor glyph sets, rebuilt in prepare().
107 pub tensor_glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
108 /// Indices into `sprite_gpu_data` for selected sprite sets, rebuilt in prepare().
109 pub sprite_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
110 /// Per-frame inline quad outline buffers for selected image slices, rebuilt in prepare().
111 pub raw_geom_outline_buffers: Vec<crate::resources::RawGeomOutlineBuffers>,
112 /// Per-frame NDC rect outline buffers for selected screen images, rebuilt in prepare().
113 pub screen_rect_outline_buffers: Vec<crate::resources::ScreenRectOutlineBuffers>,
114 /// Indices into `implicit_gpu_data` for selected GPU implicit items, rebuilt in prepare().
115 pub implicit_outline_indices: Vec<usize>,
116 /// Per-frame outline data for selected GPU marching cubes jobs, rebuilt in prepare().
117 pub mc_outline_data: Vec<crate::resources::gpu_marching_cubes::McOutlineItem>,
118 /// Outline items for selected streamtubes (index into streamtube_gpu_data + mask bind group).
119 pub streamtube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
120 /// Outline items for selected tubes.
121 pub tube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
122 /// Outline items for selected ribbons.
123 pub ribbon_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
124 /// Indices into polyline_gpu_data for selected user polylines.
125 pub polyline_outline_indices: Vec<usize>,
126 /// Per-frame x-ray buffers for selected objects, rebuilt in prepare().
127 pub xray_object_buffers: Vec<(
128 crate::resources::mesh_store::MeshId,
129 wgpu::Buffer,
130 wgpu::BindGroup,
131 )>,
132 /// Per-frame constraint guide line buffers, rebuilt in prepare().
133 pub constraint_line_buffers: Vec<(
134 wgpu::Buffer,
135 wgpu::Buffer,
136 u32,
137 wgpu::Buffer,
138 wgpu::BindGroup,
139 )>,
140 /// Per-frame cap geometry buffers (section view cross-section fill), rebuilt in prepare().
141 pub cap_buffers: Vec<(
142 wgpu::Buffer,
143 wgpu::Buffer,
144 u32,
145 wgpu::Buffer,
146 wgpu::BindGroup,
147 )>,
148 /// Per-frame clip plane fill overlay buffers, rebuilt in prepare().
149 pub clip_plane_fill_buffers: Vec<(
150 wgpu::Buffer,
151 wgpu::Buffer,
152 u32,
153 wgpu::Buffer,
154 wgpu::BindGroup,
155 )>,
156 /// Per-frame clip plane line overlay buffers, rebuilt in prepare().
157 pub clip_plane_line_buffers: Vec<(
158 wgpu::Buffer,
159 wgpu::Buffer,
160 u32,
161 wgpu::Buffer,
162 wgpu::BindGroup,
163 )>,
164 /// Vertex buffer for axes indicator geometry (rebuilt each frame).
165 pub axes_vertex_buffer: wgpu::Buffer,
166 /// Number of vertices in the axes indicator buffer.
167 pub axes_vertex_count: u32,
168 /// Gizmo model-matrix uniform buffer.
169 pub gizmo_uniform_buf: wgpu::Buffer,
170 /// Gizmo bind group (group 1: model matrix uniform).
171 pub gizmo_bind_group: wgpu::BindGroup,
172 /// Gizmo vertex buffer.
173 pub gizmo_vertex_buffer: wgpu::Buffer,
174 /// Gizmo index buffer.
175 pub gizmo_index_buffer: wgpu::Buffer,
176 /// Number of indices in the current gizmo mesh.
177 pub gizmo_index_count: u32,
178
179 // --- Sub-object highlight (per-viewport, generation-cached) ---
180 /// Per-viewport dynamic resolution intermediate render target.
181 /// `None` when render_scale == 1.0 or not yet initialised.
182 pub dyn_res: Option<crate::resources::dyn_res::DynResTarget>,
183 /// Per-viewport intermediate render target for the HDR eframe callback path.
184 /// `None` until the first `prepare_hdr_callback` call for this viewport.
185 pub hdr_callback: Option<crate::resources::dyn_res::HdrCallbackTarget>,
186 /// Cached GPU data for sub-object highlight rendering.
187 /// `None` when no sub-object selection is active and no volumes are selected.
188 pub sub_highlight: Option<crate::resources::SubHighlightGpuData>,
189 /// Version of the last sub-selection snapshot that was uploaded.
190 /// `u64::MAX` forces a rebuild on the first frame.
191 pub sub_highlight_generation: u64,
192}
193
194/// Retained pick state for one GPU implicit surface, built during `prepare()`.
195struct GpuImplicitPickItem {
196 id: u64,
197 primitives: Vec<crate::resources::ImplicitPrimitive>,
198 blend_mode: crate::resources::ImplicitBlendMode,
199 max_steps: u32,
200 step_scale: f32,
201 hit_threshold: f32,
202 max_distance: f32,
203}
204
205/// Retained pick state for one GPU marching cubes job, built during `prepare()`.
206struct GpuMcPickItem {
207 id: u64,
208 isovalue: f32,
209 volume_data: std::sync::Arc<crate::geometry::marching_cubes::VolumeData>,
210}
211
212/// Renderer wrapping all GPU resources and providing `prepare()` and `paint()` methods.
213pub struct ViewportRenderer {
214 resources: ViewportGpuResources,
215 /// Instanced batches prepared for the current frame. Empty when using per-object path.
216 instanced_batches: Vec<InstancedBatch>,
217 /// Whether the current frame uses the instanced draw path.
218 use_instancing: bool,
219 /// True when the device supports `INDIRECT_FIRST_INSTANCE`.
220 gpu_culling_supported: bool,
221 /// True when GPU-driven culling is active (supported and not disabled by the caller).
222 gpu_culling_enabled: bool,
223 /// GPU culling compute pipelines and frustum buffer. Created lazily on the first
224 /// frame where `gpu_culling_enabled` is true and instance buffers are present.
225 cull_resources: Option<indirect::CullResources>,
226 /// Performance counters from the last frame.
227 last_stats: crate::renderer::stats::FrameStats,
228 /// Last scene generation seen during prepare(). u64::MAX forces rebuild on first frame.
229 last_scene_generation: u64,
230 /// Last selection generation seen during prepare(). u64::MAX forces rebuild on first frame.
231 last_selection_generation: u64,
232 /// Last scene_items count seen during prepare(). usize::MAX forces rebuild on first frame.
233 /// Included in cache key so that frustum-culling changes (different visible set, different
234 /// count) correctly invalidate the instance buffer even when scene_generation is stable.
235 last_scene_items_count: usize,
236 /// Count of items that passed the instanced-path filter on the last rebuild.
237 /// Used in place of has_per_frame_mutations so scenes that mix instanced and
238 /// non-instanced items (e.g. one two-sided mesh + 10k static boxes) still hit
239 /// the instanced batch cache on frames where the filtered set is unchanged.
240 last_instancable_count: usize,
241 /// Total instance count from the last rebuild. Used as a fast length check
242 /// in `structure_preserved` and as `instance_count` for GPU cull dispatches.
243 cached_instance_count: usize,
244 /// Per-batch content hash from the last rebuild, indexed by batch position.
245 /// A hash mismatch triggers a `write_buffer` for that batch; a match skips it.
246 cached_instance_hashes: Vec<u64>,
247 /// Cached instanced batch descriptors from last rebuild.
248 cached_instanced_batches: Vec<InstancedBatch>,
249 /// When true, the next cache-miss forces a full buffer upload instead of the
250 /// per-batch partial-upload path. Set by `force_dirty()` and consumed once.
251 force_full_upload: bool,
252 /// Per-frame point cloud GPU data, rebuilt in prepare(), consumed in paint().
253 point_cloud_gpu_data: Vec<crate::resources::PointCloudGpuData>,
254 /// Per-frame glyph GPU data, rebuilt in prepare(), consumed in paint().
255 glyph_gpu_data: Vec<crate::resources::GlyphGpuData>,
256 /// Per-frame tensor glyph GPU data, rebuilt in prepare(), consumed in paint().
257 tensor_glyph_gpu_data: Vec<crate::resources::TensorGlyphGpuData>,
258 /// Per-frame polyline GPU data, rebuilt in prepare(), consumed in paint().
259 polyline_gpu_data: Vec<crate::resources::PolylineGpuData>,
260 /// Per-frame volume GPU data, rebuilt in prepare(), consumed in paint().
261 volume_gpu_data: Vec<crate::resources::VolumeGpuData>,
262 /// Per-frame streamtube GPU data, rebuilt in prepare(), consumed in paint().
263 streamtube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
264 /// Per-frame general tube GPU data, rebuilt in prepare(), consumed in paint().
265 tube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
266 /// Per-frame ribbon GPU data, rebuilt in prepare(), consumed in paint().
267 ribbon_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
268 /// Indices into streamtube_gpu_data for selected streamtubes (set in prepare_scene, consumed in prepare_viewport).
269 streamtube_selected_gpu_indices: Vec<usize>,
270 /// Indices into tube_gpu_data for selected tubes (set in prepare_scene, consumed in prepare_viewport).
271 tube_selected_gpu_indices: Vec<usize>,
272 /// Indices into ribbon_gpu_data for selected ribbons (set in prepare_scene, consumed in prepare_viewport).
273 ribbon_selected_gpu_indices: Vec<usize>,
274 /// Indices into polyline_gpu_data for selected user polylines (set in prepare_scene, consumed in prepare_viewport).
275 polyline_selected_gpu_indices: Vec<usize>,
276 /// Per-frame image slice GPU data, rebuilt in prepare(), consumed in paint().
277 image_slice_gpu_data: Vec<crate::resources::ImageSliceGpuData>,
278 /// Per-frame volume surface slice GPU data, rebuilt in prepare(), consumed in paint().
279 volume_surface_slice_gpu_data: Vec<crate::resources::VolumeSurfaceSliceGpuData>,
280 /// Per-frame Surface LIC GPU data, rebuilt in prepare(), consumed in paint().
281 lic_gpu_data: Vec<crate::resources::LicSurfaceGpuData>,
282 /// Per-frame GPU implicit surface data, rebuilt in prepare(), consumed in paint().
283 implicit_gpu_data: Vec<crate::resources::implicit::ImplicitGpuItem>,
284 /// Per-frame decal GPU data, rebuilt in prepare(), consumed in paint() (D1).
285 decal_gpu_data: Vec<crate::resources::decal::DecalGpuItem>,
286 /// Per-frame decal exclude GPU data, rebuilt in prepare(), consumed in paint() (D5).
287 decal_exclude_items: Vec<crate::resources::decal::DecalExcludeGpuItem>,
288 /// Per-frame GPU marching cubes render data, rebuilt in prepare(), consumed in paint().
289 mc_gpu_data: Vec<crate::resources::gpu_marching_cubes::McFrameData>,
290 /// Per-frame sprite GPU data, rebuilt in prepare(), consumed in paint().
291 sprite_gpu_data: Vec<crate::resources::SpriteGpuData>,
292 /// Per-frame Gaussian splat draw data, rebuilt in prepare_viewport_internal(), consumed in paint().
293 gaussian_splat_draw_data: Vec<crate::resources::GaussianSplatDrawData>,
294 /// Per-frame screen-image GPU data, rebuilt in prepare(), consumed in paint().
295 screen_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
296 /// Per-frame overlay image GPU data, rebuilt in prepare(), consumed in paint().
297 overlay_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
298 /// Per-frame overlay label GPU data, rebuilt in prepare(), consumed in paint().
299 label_gpu_data: Option<crate::resources::LabelGpuData>,
300 /// Per-frame scalar bar GPU data, rebuilt in prepare(), consumed in paint().
301 scalar_bar_gpu_data: Option<crate::resources::LabelGpuData>,
302 /// Per-frame ruler GPU data, rebuilt in prepare(), consumed in paint().
303 ruler_gpu_data: Option<crate::resources::LabelGpuData>,
304 /// Per-frame loading bar GPU data, rebuilt in prepare(), consumed in paint().
305 loading_bar_gpu_data: Option<crate::resources::LabelGpuData>,
306 /// Per-frame overlay rect GPU data, rebuilt in prepare(), consumed in paint().
307 overlay_rect_gpu_data: Option<crate::resources::LabelGpuData>,
308 /// Per-frame SDF overlay shape GPU data, rebuilt in prepare(), consumed in paint().
309 overlay_shape_gpu_data: Option<crate::resources::OverlayShapeGpuData>,
310 /// Cached GPU textures for the backdrop blur effect (frosted glass).
311 /// Recreated when the viewport size changes.
312 backdrop_blur_state: Option<crate::resources::BackdropBlurState>,
313 /// Per-viewport GPU state slots.
314 ///
315 /// Indexed by `FrameData::camera.viewport_index`. Each slot owns independent
316 /// uniform buffers and bind groups for camera, clip planes, clip volume,
317 /// shadow info, and grid. Slots are grown lazily in `prepare` via
318 /// `ensure_viewport_slot`. There are at most 4 in the current UI.
319 viewport_slots: Vec<ViewportSlot>,
320 /// GPU compute filter results from the last `prepare()` call.
321 ///
322 /// Each entry contains a compacted index buffer + count for one filtered mesh.
323 /// Consumed during `paint()` to override the mesh's default index buffer.
324 /// Cleared and rebuilt each frame.
325 compute_filter_results: Vec<crate::resources::ComputeFilterResult>,
326 /// Per-item uniform buffers for wireframe mode. In wireframe mode multiple scene
327 /// items can share the same MeshId, but each needs its own object uniform (model
328 /// matrix, colour, etc.). The mesh's single `object_uniform_buf` gets overwritten
329 /// by the last item prepared, so we maintain a separate pool here. Indexed in the
330 /// same order as the visible scene items. Grown lazily, never shrunk.
331 wireframe_uniform_bufs: Vec<wgpu::Buffer>,
332 /// Bind groups corresponding to `wireframe_uniform_bufs`. Each bind group pairs
333 /// the per-item uniform buffer with the mesh's fallback textures so it is
334 /// compatible with the object bind group layout.
335 wireframe_bind_groups: Vec<wgpu::BindGroup>,
336 /// Per-frame list of boundary mesh IDs to draw in wireframe for
337 /// TransparentVolumeMeshItems with `appearance.wireframe = true`.
338 /// Cleared and rebuilt each frame in prepare_scene_internal.
339 tvm_wireframe_draws: Vec<crate::resources::mesh_store::MeshId>,
340 /// Shared uniform buffer for TVM boundary wireframe draws (wireframe=1, model=identity).
341 /// Created once on first use, reused every frame.
342 tvm_wireframe_buf: Option<wgpu::Buffer>,
343 /// Bind group for TVM boundary wireframe draws. Pairs `tvm_wireframe_buf` with
344 /// fallback textures matching the object bind group layout.
345 tvm_wireframe_bg: Option<wgpu::BindGroup>,
346 /// Cascade-0 light-space view-projection matrix from the last shadow prepare.
347 /// Cached here so `prepare_viewport_internal` can copy it into the ground plane uniform.
348 last_cascade0_shadow_mat: glam::Mat4,
349 /// Current runtime mode controlling internal default behavior.
350 runtime_mode: crate::renderer::stats::RuntimeMode,
351 /// Active performance policy: target FPS, render scale bounds, and permitted reductions.
352 performance_policy: crate::renderer::stats::PerformancePolicy,
353 /// Current render scale tracked by the adaptation controller (or set manually).
354 ///
355 /// Clamped to `[policy.min_render_scale, policy.max_render_scale]`.
356 /// Reported in `FrameStats::render_scale` each frame.
357 current_render_scale: f32,
358 /// Instant the renderer was constructed. Used as the t=0 reference for
359 /// per-frame animated effects (e.g. `ScatterVolume::noise` time scrolling).
360 start_instant: std::time::Instant,
361 /// Instant recorded at the start of the most recent `prepare()` call.
362 /// Used to compute `total_frame_ms` on the following frame.
363 last_prepare_instant: Option<std::time::Instant>,
364 /// Frame counter incremented each `prepare()` call. Used for picking throttle in Playback mode.
365 frame_counter: u64,
366 /// Surface items from the last `prepare()` call, retained for `pick()` dispatch.
367 pick_scene_items: Vec<SceneRenderItem>,
368 /// Point cloud items from the last `prepare()` call, retained for `pick()` dispatch.
369 pick_point_cloud_items: Vec<PointCloudItem>,
370 /// Gaussian splat items from the last `prepare()` call, retained for `pick()` dispatch.
371 pick_splat_items: Vec<GaussianSplatItem>,
372 /// Volume items from the last `prepare()` call, retained for `pick()` dispatch.
373 pick_volume_items: Vec<VolumeItem>,
374 /// Transparent volume mesh items from the last `prepare()` call, retained for `pick()` dispatch.
375 pick_tvm_items: Vec<TransparentVolumeMeshItem>,
376 /// Scatter volume items from the last `prepare()` call, retained for `pick()` dispatch.
377 pick_scatter_volume_items: Vec<crate::renderer::types::ScatterVolumeItem>,
378 /// Volumes packed into the GPU storage buffer this frame
379 /// (volume, density_multiplier, flag bits). Stored so `render_viewport`
380 /// can re-upload as needed without re-walking the scene frame.
381 pub(crate) prepared_scatter_volumes:
382 Vec<(crate::scene::scatter_volume::ScatterVolume, f32, u32)>,
383 /// Per-viewport scatter intermediates and temporal history. Indexed by
384 /// `vp_idx`. Grown lazily inside the scatter pass; each entry is
385 /// reallocated when the requested scatter target size or downsample mode
386 /// changes.
387 pub(crate) scatter_viewport_states: Vec<Option<crate::resources::ScatterViewportState>>,
388 /// Opaque volume mesh items from the last `prepare()` call, retained for cell-level `pick()` dispatch.
389 pick_volume_mesh_items: Vec<VolumeMeshItem>,
390 /// Polyline items from the last `prepare()` call, retained for `pick()` dispatch.
391 pick_polyline_items: Vec<PolylineItem>,
392 /// Glyph items from the last `prepare()` call, retained for `pick()` dispatch.
393 pick_glyph_items: Vec<GlyphItem>,
394 /// Tensor glyph items from the last `prepare()` call, retained for `pick()` dispatch.
395 pick_tensor_glyph_items: Vec<TensorGlyphItem>,
396 /// Sprite items from the last `prepare()` call, retained for `pick()` dispatch.
397 pick_sprite_items: Vec<SpriteItem>,
398 /// Streamtube items from the last `prepare()` call, retained for `pick()` dispatch.
399 pick_streamtube_items: Vec<StreamtubeItem>,
400 /// Tube items from the last `prepare()` call, retained for `pick()` dispatch.
401 pick_tube_items: Vec<TubeItem>,
402 /// Ribbon items from the last `prepare()` call, retained for `pick()` dispatch.
403 pick_ribbon_items: Vec<RibbonItem>,
404 /// Image slice items from the last `prepare()` call, retained for `pick()` dispatch.
405 pick_image_slice_items: Vec<ImageSliceItem>,
406 /// Volume surface slice items from the last `prepare()` call, retained for `pick()` dispatch.
407 pick_volume_surface_slice_items: Vec<VolumeSurfaceSliceItem>,
408 /// Screen image items from the last `prepare()` call, retained for `pick()` dispatch.
409 pick_screen_image_items: Vec<ScreenImageItem>,
410 /// GPU implicit surface items from the last `prepare()` call, retained for `pick()` dispatch.
411 pick_implicit_items: Vec<GpuImplicitPickItem>,
412 /// GPU marching cubes jobs from the last `prepare()` call, retained for `pick()` dispatch.
413 pick_mc_items: Vec<GpuMcPickItem>,
414
415 // --- GPU timestamp queries ---
416 /// Timestamp query set with 2 entries (scene-pass begin + end).
417 /// `None` when `TIMESTAMP_QUERY` is unavailable or not yet initialized.
418 ts_query_set: Option<wgpu::QuerySet>,
419 /// Resolve buffer: 2 × u64, GPU-only (`QUERY_RESOLVE | COPY_SRC`).
420 ts_resolve_buf: Option<wgpu::Buffer>,
421 /// Staging buffer: 2 × u64, CPU-readable (`COPY_DST | MAP_READ`).
422 ts_staging_buf: Option<wgpu::Buffer>,
423 /// Nanoseconds per GPU timestamp tick, from `queue.get_timestamp_period()`.
424 ts_period: f32,
425 /// Whether the staging buffer holds unread timestamp data from the previous frame.
426 ts_needs_readback: bool,
427
428 // --- Indirect-args readback (GPU-driven culling visible instance count) ---
429 /// CPU-readable staging buffer for `indirect_args_buf` (batch_count × 20 bytes).
430 /// Grown lazily; never shrunk.
431 indirect_readback_buf: Option<wgpu::Buffer>,
432 /// Number of batches whose data was copied into `indirect_readback_buf` last frame.
433 indirect_readback_batch_count: u32,
434 /// True when `indirect_readback_buf` holds unread data from the previous cull pass.
435 indirect_readback_pending: bool,
436
437 // --- Per-pass degradation state ---
438 /// Tiered degradation ladder position (0 = none, 1 = shadows, 2 = volumes, 3 = effects).
439 /// Advanced one step per over-budget frame once render scale hits minimum;
440 /// reversed one step per comfortably-under-budget frame.
441 degradation_tier: u8,
442 /// Whether the shadow pass was skipped this frame due to budget pressure.
443 /// Computed once per frame at the top of prepare() and used by both
444 /// prepare_scene_internal and reported in FrameStats.
445 degradation_shadows_skipped: bool,
446 /// Whether volume raymarch step size was doubled this frame due to budget pressure.
447 degradation_volume_quality_reduced: bool,
448 /// Whether SSAO, contact shadows, and bloom were skipped this frame.
449 /// Set in prepare(); read by the render path.
450 degradation_effects_throttled: bool,
451
452 // --- D8: shadow debug stats cache ---
453 /// Cascade count from the last prepare_scene_internal call.
454 last_cascade_count: u32,
455 /// Cascade split distances from the last prepare_scene_internal call.
456 last_cascade_splits: [f32; 4],
457 /// Shadow frustum half-extent from the last prepare_scene_internal call.
458 last_shadow_extent: f32,
459 /// Shadow atlas resolution from the last prepare_scene_internal call.
460 last_shadow_atlas_resolution: u32,
461 /// Contact shadow enabled state from the last prepare_scene_internal call.
462 last_contact_shadow_active: bool,
463 /// Cascade splits from the last tracing log emission. Sentinel [f32::MAX; 4] forces
464 /// a log on the first frame.
465 last_logged_cascade_splits: [f32; 4],
466}
467
468impl ViewportRenderer {
469 /// Create a new renderer with default settings (no MSAA).
470 /// Call once at application startup.
471 pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
472 Self::with_sample_count(device, target_format, 1)
473 }
474
475 /// Create a new renderer with the specified MSAA sample count (1, 2, or 4).
476 ///
477 /// When using MSAA (sample_count > 1), the caller must create multisampled
478 /// colour and depth textures and use them as render pass attachments with the
479 /// final surface texture as the resolve target.
480 pub fn with_sample_count(
481 device: &wgpu::Device,
482 target_format: wgpu::TextureFormat,
483 sample_count: u32,
484 ) -> Self {
485 let gpu_culling_supported = device
486 .features()
487 .contains(wgpu::Features::INDIRECT_FIRST_INSTANCE);
488 Self {
489 resources: ViewportGpuResources::new(device, target_format, sample_count),
490 instanced_batches: Vec::new(),
491 use_instancing: false,
492 gpu_culling_supported,
493 gpu_culling_enabled: gpu_culling_supported,
494 cull_resources: None,
495 last_stats: crate::renderer::stats::FrameStats::default(),
496 last_scene_generation: u64::MAX,
497 last_selection_generation: u64::MAX,
498 last_scene_items_count: usize::MAX,
499 last_instancable_count: usize::MAX,
500 cached_instance_count: 0,
501 cached_instance_hashes: Vec::new(),
502 cached_instanced_batches: Vec::new(),
503 force_full_upload: false,
504 point_cloud_gpu_data: Vec::new(),
505 glyph_gpu_data: Vec::new(),
506 tensor_glyph_gpu_data: Vec::new(),
507 polyline_gpu_data: Vec::new(),
508 volume_gpu_data: Vec::new(),
509 streamtube_gpu_data: Vec::new(),
510 tube_gpu_data: Vec::new(),
511 ribbon_gpu_data: Vec::new(),
512 streamtube_selected_gpu_indices: Vec::new(),
513 tube_selected_gpu_indices: Vec::new(),
514 ribbon_selected_gpu_indices: Vec::new(),
515 polyline_selected_gpu_indices: Vec::new(),
516 image_slice_gpu_data: Vec::new(),
517 volume_surface_slice_gpu_data: Vec::new(),
518 sprite_gpu_data: Vec::new(),
519 gaussian_splat_draw_data: Vec::new(),
520 lic_gpu_data: Vec::new(),
521 implicit_gpu_data: Vec::new(),
522 decal_gpu_data: Vec::new(),
523 decal_exclude_items: Vec::new(),
524 mc_gpu_data: Vec::new(),
525 screen_image_gpu_data: Vec::new(),
526 overlay_image_gpu_data: Vec::new(),
527 label_gpu_data: None,
528 scalar_bar_gpu_data: None,
529 ruler_gpu_data: None,
530 loading_bar_gpu_data: None,
531 overlay_rect_gpu_data: None,
532 overlay_shape_gpu_data: None,
533 backdrop_blur_state: None,
534 viewport_slots: Vec::new(),
535 compute_filter_results: Vec::new(),
536 wireframe_uniform_bufs: Vec::new(),
537 wireframe_bind_groups: Vec::new(),
538 tvm_wireframe_draws: Vec::new(),
539 tvm_wireframe_buf: None,
540 tvm_wireframe_bg: None,
541 last_cascade0_shadow_mat: glam::Mat4::IDENTITY,
542 runtime_mode: crate::renderer::stats::RuntimeMode::Interactive,
543 performance_policy: crate::renderer::stats::PerformancePolicy::default(),
544 current_render_scale: 1.0,
545 start_instant: std::time::Instant::now(),
546 last_prepare_instant: None,
547 frame_counter: 0,
548 pick_scene_items: Vec::new(),
549 pick_point_cloud_items: Vec::new(),
550 pick_splat_items: Vec::new(),
551 pick_volume_items: Vec::new(),
552 pick_tvm_items: Vec::new(),
553 pick_scatter_volume_items: Vec::new(),
554 prepared_scatter_volumes: Vec::new(),
555 scatter_viewport_states: Vec::new(),
556 pick_volume_mesh_items: Vec::new(),
557 pick_polyline_items: Vec::new(),
558 pick_glyph_items: Vec::new(),
559 pick_tensor_glyph_items: Vec::new(),
560 pick_sprite_items: Vec::new(),
561 pick_streamtube_items: Vec::new(),
562 pick_tube_items: Vec::new(),
563 pick_ribbon_items: Vec::new(),
564 pick_image_slice_items: Vec::new(),
565 pick_volume_surface_slice_items: Vec::new(),
566 pick_screen_image_items: Vec::new(),
567 pick_implicit_items: Vec::new(),
568 pick_mc_items: Vec::new(),
569 ts_query_set: None,
570 ts_resolve_buf: None,
571 ts_staging_buf: None,
572 ts_period: 1.0,
573 ts_needs_readback: false,
574 indirect_readback_buf: None,
575 indirect_readback_batch_count: 0,
576 indirect_readback_pending: false,
577 degradation_tier: 0,
578 degradation_shadows_skipped: false,
579 degradation_volume_quality_reduced: false,
580 degradation_effects_throttled: false,
581 last_cascade_count: 0,
582 last_cascade_splits: [0.0; 4],
583 last_shadow_extent: 20.0,
584 last_shadow_atlas_resolution: 4096,
585 last_contact_shadow_active: false,
586 last_logged_cascade_splits: [f32::MAX; 4],
587 }
588 }
589
590 /// Access the underlying GPU resources (e.g. for mesh uploads).
591 pub fn resources(&self) -> &ViewportGpuResources {
592 &self.resources
593 }
594
595 /// Performance counters from the last completed frame.
596 pub fn last_frame_stats(&self) -> crate::renderer::stats::FrameStats {
597 self.last_stats
598 }
599
600 /// Disable GPU-driven culling, reverting to the direct draw path.
601 ///
602 /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`
603 /// (culling is already disabled on those devices).
604 pub fn disable_gpu_driven_culling(&mut self) {
605 self.gpu_culling_enabled = false;
606 }
607
608 /// Force a full instance buffer upload on the next frame.
609 ///
610 /// Normally the renderer skips GPU writes for instanced batches whose data
611 /// has not changed since the last upload. Call this when you have mutated
612 /// batch-relevant state through a path the renderer cannot observe (for
613 /// example, directly modifying GPU buffer contents or scene items after
614 /// `collect_render_items` runs). The flag is consumed once and resets
615 /// automatically after the next `prepare` call.
616 pub fn force_dirty(&mut self) {
617 self.force_full_upload = true;
618 // Also invalidate the generation cache so the next prepare is guaranteed
619 // to enter the rebuild path even if the scene generation is unchanged.
620 self.last_scene_generation = u64::MAX;
621 }
622
623 /// Re-enable GPU-driven culling after a call to `disable_gpu_driven_culling`.
624 ///
625 /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`.
626 pub fn enable_gpu_driven_culling(&mut self) {
627 if self.gpu_culling_supported {
628 self.gpu_culling_enabled = true;
629 }
630 }
631
632 /// Set the runtime mode controlling internal default behavior.
633 ///
634 /// - [`RuntimeMode::Interactive`]: full picking rate, full quality (default).
635 /// - [`RuntimeMode::Playback`]: picking throttled to reduce CPU overhead during animation.
636 /// - [`RuntimeMode::Paused`]: full picking rate, full quality.
637 /// - [`RuntimeMode::Capture`]: full quality, intended for screenshot/export workflows.
638 pub fn set_runtime_mode(&mut self, mode: crate::renderer::stats::RuntimeMode) {
639 self.runtime_mode = mode;
640 }
641
642 /// Return the current runtime mode.
643 pub fn runtime_mode(&self) -> crate::renderer::stats::RuntimeMode {
644 self.runtime_mode
645 }
646
647 /// Set the performance policy controlling target FPS, render scale bounds,
648 /// and permitted quality reductions.
649 ///
650 /// The internal adaptation controller activates when
651 /// `policy.allow_dynamic_resolution` is `true` and `policy.target_fps` is
652 /// `Some`. It adjusts `render_scale` within `[min_render_scale,
653 /// max_render_scale]` each frame based on `total_frame_ms`.
654 pub fn set_performance_policy(&mut self, policy: crate::renderer::stats::PerformancePolicy) {
655 self.performance_policy = policy;
656 // Clamp current scale into the new bounds immediately.
657 self.current_render_scale = self
658 .current_render_scale
659 .clamp(policy.min_render_scale, policy.max_render_scale);
660 }
661
662 /// Return the active performance policy.
663 pub fn performance_policy(&self) -> crate::renderer::stats::PerformancePolicy {
664 self.performance_policy
665 }
666
667 /// Manually set the render scale.
668 ///
669 /// Effective when `performance_policy.allow_dynamic_resolution` is `false`.
670 /// When dynamic resolution is enabled the adaptation controller overrides
671 /// this value each frame.
672 ///
673 /// The value is clamped to `[policy.min_render_scale, policy.max_render_scale]`.
674 ///
675 /// Works on both the LDR and HDR render paths. On the HDR path, the scene,
676 /// bloom, SSAO, tone-map, and FXAA all run at the scaled resolution; the
677 /// result is upscale-blitted to native resolution before overlays and grid.
678 pub fn set_render_scale(&mut self, scale: f32) {
679 self.current_render_scale = scale.clamp(
680 self.performance_policy.min_render_scale,
681 self.performance_policy.max_render_scale,
682 );
683 }
684
685 /// Set the target frame rate used to compute [`FrameStats::missed_budget`].
686 ///
687 /// Convenience wrapper that updates `performance_policy.target_fps`.
688 pub fn set_target_fps(&mut self, fps: Option<f32>) {
689 self.performance_policy.target_fps = fps;
690 }
691
692 /// Mutable access to the underlying GPU resources (e.g. for mesh uploads).
693 pub fn resources_mut(&mut self) -> &mut ViewportGpuResources {
694 &mut self.resources
695 }
696
697 /// Returns true when the current frame is rendered via the instanced draw path.
698 ///
699 /// When true, edits to mesh.wgsl shadow sampling code have no effect - the active
700 /// shader is mesh_instanced.wgsl. Check this before testing shader changes.
701 pub fn is_using_instanced_path(&self) -> bool {
702 self.use_instancing
703 }
704
705 /// Returns the number of instanced batches prepared for the current frame.
706 ///
707 /// Zero when using the non-instanced path. Each batch corresponds to a distinct
708 /// (MeshId, material) combination in the scene.
709 pub fn instanced_batch_count(&self) -> usize {
710 self.instanced_batches.len()
711 }
712
713 /// Returns per-frame shadow and lighting pipeline statistics for debug inspection.
714 ///
715 /// All fields reflect the most recently completed `prepare` call (one frame
716 /// behind the display). Returns default values before the first `prepare` call.
717 pub fn shadow_debug_stats(&self) -> ShadowDebugStats {
718 ShadowDebugStats {
719 using_instanced_path: self.use_instancing,
720 instanced_batch_count: self.instanced_batches.len(),
721 cascade_count: self.last_cascade_count,
722 cascade_splits: self.last_cascade_splits,
723 shadow_atlas_resolution: self.last_shadow_atlas_resolution,
724 shadow_extent_world: self.last_shadow_extent,
725 contact_shadow_active: self.last_contact_shadow_active,
726 }
727 }
728
729 /// Read the debug values at a specific pixel from the per-fragment storage buffer.
730 ///
731 /// Returns `None` when debug_vis is inactive (no buffer allocated) or when `(x, y)`
732 /// is outside the viewport. The four channels correspond to the current R/G/B channel
733 /// selectors plus 1.0 for alpha.
734 ///
735 /// This submits a GPU-to-CPU copy and waits synchronously. Only call from outside
736 /// a render pass (e.g., in the next frame's prepare step), not inside paint callbacks.
737 ///
738 /// The returned values are from the previous rendered frame.
739 pub fn read_debug_pixel(
740 &self,
741 device: &wgpu::Device,
742 queue: &wgpu::Queue,
743 x: u32,
744 y: u32,
745 ) -> Option<[f32; 4]> {
746 // Use the primary viewport slot (index 0).
747 let slot = self.viewport_slots.first()?;
748 let buf = slot.debug_frag_buf.as_ref()?;
749 let (vw, vh) = slot.debug_frag_dims;
750 if x >= vw || y >= vh {
751 return None;
752 }
753 let byte_offset = ((y as u64) * (vw as u64) + (x as u64)) * 16;
754 let staging = device.create_buffer(&wgpu::BufferDescriptor {
755 label: None,
756 size: 16,
757 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
758 mapped_at_creation: false,
759 });
760 let mut encoder = device
761 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
762 encoder.copy_buffer_to_buffer(buf, byte_offset, &staging, 0, 16);
763 queue.submit(Some(encoder.finish()));
764 let slice = staging.slice(..);
765 let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
766 slice.map_async(wgpu::MapMode::Read, move |r| {
767 let _ = tx.send(r);
768 });
769 let _ = device.poll(wgpu::PollType::Wait {
770 submission_index: None,
771 timeout: Some(std::time::Duration::from_secs(5)),
772 });
773 rx.recv().ok()?.ok()?;
774 let data = slice.get_mapped_range();
775 Some(bytemuck::pod_read_unaligned::<[f32; 4]>(&data))
776 }
777
778 /// Upload a Gaussian splat set to the GPU.
779 ///
780 /// Call once per splat set at startup or when it changes. The returned
781 /// [`GaussianSplatId`] is valid until [`remove_gaussian_splats`](Self::remove_gaussian_splats) is called.
782 ///
783 /// # Errors
784 ///
785 /// Returns [`ViewportError::InvalidGaussianSplatData`](crate::error::ViewportError::InvalidGaussianSplatData)
786 /// if `data.positions` is empty or if `positions`, `scales`, `rotations`, and `opacities`
787 /// differ in length.
788 ///
789 /// # Examples
790 ///
791 /// ```no_run
792 /// # use viewport_lib::error::ViewportError;
793 /// # use viewport_lib::renderer::{GaussianSplatData, ViewportRenderer};
794 /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
795 /// let result = renderer.upload_gaussian_splats(device, queue, &GaussianSplatData::default());
796 /// assert!(matches!(result, Err(ViewportError::InvalidGaussianSplatData { .. })));
797 /// # }
798 /// ```
799 pub fn upload_gaussian_splats(
800 &mut self,
801 device: &wgpu::Device,
802 queue: &wgpu::Queue,
803 data: &GaussianSplatData,
804 ) -> crate::error::ViewportResult<GaussianSplatId> {
805 self.resources.upload_gaussian_splats(device, queue, data)
806 }
807
808 /// Remove an uploaded Gaussian splat set by handle.
809 ///
810 /// After this call the `id` is invalid and must not be submitted in `SceneFrame`.
811 pub fn remove_gaussian_splats(&mut self, id: GaussianSplatId) {
812 self.resources.remove_gaussian_splats(id);
813 }
814
815 /// Upload an equirectangular HDR environment map and precompute IBL textures.
816 ///
817 /// `pixels` is row-major RGBA f32 data (4 floats per texel), `width`x`height`.
818 /// This rebuilds camera bind groups so shaders immediately see the new textures.
819 ///
820 /// # Errors
821 ///
822 /// Returns [`ViewportError::InvalidTextureData`](crate::error::ViewportError::InvalidTextureData)
823 /// if `pixels.len()` does not equal `width * height * 4`.
824 ///
825 /// # Examples
826 ///
827 /// ```no_run
828 /// # use viewport_lib::error::ViewportError;
829 /// # use viewport_lib::renderer::ViewportRenderer;
830 /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
831 /// // 2x2 RGBA image requires exactly 16 floats.
832 /// let result = renderer.upload_environment_map(device, queue, &[0.0f32; 12], 2, 2);
833 /// assert!(matches!(result, Err(ViewportError::InvalidTextureData { expected: 16, actual: 12 })));
834 /// # }
835 /// ```
836 pub fn upload_environment_map(
837 &mut self,
838 device: &wgpu::Device,
839 queue: &wgpu::Queue,
840 pixels: &[f32],
841 width: u32,
842 height: u32,
843 ) -> crate::error::ViewportResult<()> {
844 crate::resources::environment::upload_environment_map(
845 &mut self.resources,
846 device,
847 queue,
848 pixels,
849 width,
850 height,
851 )?;
852 self.rebuild_camera_bind_groups(device);
853 Ok(())
854 }
855
856 /// Rebuild the primary + per-viewport camera bind groups.
857 ///
858 /// Call after IBL textures are uploaded so shaders see the new environment.
859 fn rebuild_camera_bind_groups(&mut self, device: &wgpu::Device) {
860 self.resources.camera_bind_group = self.resources.create_camera_bind_group(
861 device,
862 &self.resources.camera_uniform_buf,
863 &self.resources.clip_planes_uniform_buf,
864 &self.resources.shadow_info_buf,
865 &self.resources.clip_volume_uniform_buf,
866 &self.resources.debug_frag_sentinel_buf,
867 "camera_bind_group",
868 );
869
870 for slot in &mut self.viewport_slots {
871 let dbg_buf = slot
872 .debug_frag_buf
873 .as_ref()
874 .unwrap_or(&self.resources.debug_frag_sentinel_buf);
875 slot.camera_bind_group = self.resources.create_camera_bind_group(
876 device,
877 &slot.camera_buf,
878 &slot.clip_planes_buf,
879 &slot.shadow_info_buf,
880 &slot.clip_volume_buf,
881 dbg_buf,
882 "per_viewport_camera_bg",
883 );
884 }
885 }
886
887 /// Ensure a per-viewport slot exists for `viewport_index`.
888 ///
889 /// Creates a full `ViewportSlot` with independent uniform buffers for camera,
890 /// clip planes, clip volume, shadow info, and grid. The camera bind group
891 /// references this slot's per-viewport buffers plus shared scene-global
892 /// resources. Slots are created lazily and never destroyed.
893 fn ensure_viewport_slot(&mut self, device: &wgpu::Device, viewport_index: usize) {
894 while self.viewport_slots.len() <= viewport_index {
895 let camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
896 label: Some("vp_camera_buf"),
897 size: std::mem::size_of::<CameraUniform>() as u64,
898 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
899 mapped_at_creation: false,
900 });
901 let clip_planes_buf = device.create_buffer(&wgpu::BufferDescriptor {
902 label: Some("vp_clip_planes_buf"),
903 size: std::mem::size_of::<ClipPlanesUniform>() as u64,
904 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
905 mapped_at_creation: false,
906 });
907 let clip_volume_buf = device.create_buffer(&wgpu::BufferDescriptor {
908 label: Some("vp_clip_volume_buf"),
909 size: std::mem::size_of::<ClipVolumesUniform>() as u64,
910 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
911 mapped_at_creation: false,
912 });
913 let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
914 label: Some("vp_shadow_info_buf"),
915 size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
916 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
917 mapped_at_creation: false,
918 });
919 let grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
920 label: Some("vp_grid_buf"),
921 size: std::mem::size_of::<GridUniform>() as u64,
922 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
923 mapped_at_creation: false,
924 });
925
926 let camera_bind_group = self.resources.create_camera_bind_group(
927 device,
928 &camera_buf,
929 &clip_planes_buf,
930 &shadow_info_buf,
931 &clip_volume_buf,
932 &self.resources.debug_frag_sentinel_buf,
933 "per_viewport_camera_bg",
934 );
935
936 let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
937 label: Some("vp_grid_bind_group"),
938 layout: &self.resources.grid_bind_group_layout,
939 entries: &[wgpu::BindGroupEntry {
940 binding: 0,
941 resource: grid_buf.as_entire_binding(),
942 }],
943 });
944
945 // Per-viewport gizmo buffers (initial mesh: Translate, no hover, identity orientation).
946 let (gizmo_verts, gizmo_indices) = crate::interaction::gizmo::build_gizmo_mesh(
947 crate::interaction::gizmo::GizmoMode::Translate,
948 crate::interaction::gizmo::GizmoAxis::None,
949 glam::Quat::IDENTITY,
950 );
951 let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
952 label: Some("vp_gizmo_vertex_buf"),
953 size: (std::mem::size_of::<crate::resources::Vertex>() * gizmo_verts.len().max(1))
954 as u64,
955 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
956 mapped_at_creation: true,
957 });
958 gizmo_vertex_buffer
959 .slice(..)
960 .get_mapped_range_mut()
961 .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
962 gizmo_vertex_buffer.unmap();
963 let gizmo_index_count = gizmo_indices.len() as u32;
964 let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
965 label: Some("vp_gizmo_index_buf"),
966 size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
967 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
968 mapped_at_creation: true,
969 });
970 gizmo_index_buffer
971 .slice(..)
972 .get_mapped_range_mut()
973 .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
974 gizmo_index_buffer.unmap();
975 let gizmo_uniform = crate::interaction::gizmo::GizmoUniform {
976 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
977 };
978 let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
979 label: Some("vp_gizmo_uniform_buf"),
980 size: std::mem::size_of::<crate::interaction::gizmo::GizmoUniform>() as u64,
981 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
982 mapped_at_creation: true,
983 });
984 gizmo_uniform_buf
985 .slice(..)
986 .get_mapped_range_mut()
987 .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
988 gizmo_uniform_buf.unmap();
989 let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
990 label: Some("vp_gizmo_bind_group"),
991 layout: &self.resources.gizmo_bind_group_layout,
992 entries: &[wgpu::BindGroupEntry {
993 binding: 0,
994 resource: gizmo_uniform_buf.as_entire_binding(),
995 }],
996 });
997
998 // Per-viewport axes vertex buffer (2048 vertices = enough for all axes geometry).
999 let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1000 label: Some("vp_axes_vertex_buf"),
1001 size: (std::mem::size_of::<crate::widgets::axes_indicator::AxesVertex>() * 2048)
1002 as u64,
1003 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1004 mapped_at_creation: false,
1005 });
1006
1007 self.viewport_slots.push(ViewportSlot {
1008 camera_buf,
1009 clip_planes_buf,
1010 clip_volume_buf,
1011 shadow_info_buf,
1012 grid_buf,
1013 camera_bind_group,
1014 grid_bind_group,
1015 hdr: None,
1016 debug_frag_buf: None,
1017 debug_frag_dims: (0, 0),
1018 outline_object_buffers: Vec::new(),
1019 splat_outline_buffers: Vec::new(),
1020 volume_outline_indices: Vec::new(),
1021 glyph_outline_indices: Vec::new(),
1022 tensor_glyph_outline_indices: Vec::new(),
1023 sprite_outline_indices: Vec::new(),
1024 raw_geom_outline_buffers: Vec::new(),
1025 screen_rect_outline_buffers: Vec::new(),
1026 implicit_outline_indices: Vec::new(),
1027 mc_outline_data: Vec::new(),
1028 streamtube_outline_items: Vec::new(),
1029 tube_outline_items: Vec::new(),
1030 ribbon_outline_items: Vec::new(),
1031 polyline_outline_indices: Vec::new(),
1032 xray_object_buffers: Vec::new(),
1033 constraint_line_buffers: Vec::new(),
1034 cap_buffers: Vec::new(),
1035 clip_plane_fill_buffers: Vec::new(),
1036 clip_plane_line_buffers: Vec::new(),
1037 axes_vertex_buffer,
1038 axes_vertex_count: 0,
1039 gizmo_uniform_buf,
1040 gizmo_bind_group,
1041 gizmo_vertex_buffer,
1042 gizmo_index_buffer,
1043 gizmo_index_count,
1044 sub_highlight: None,
1045 sub_highlight_generation: u64::MAX,
1046 dyn_res: None,
1047 hdr_callback: None,
1048 });
1049 }
1050 }
1051
1052 // -----------------------------------------------------------------------
1053 // Multi-viewport public API
1054 // -----------------------------------------------------------------------
1055
1056 /// Create a new viewport slot and return its handle.
1057 ///
1058 /// The returned [`ViewportId`] is stable for the lifetime of the renderer.
1059 /// Pass it to [`prepare_viewport`](Self::prepare_viewport),
1060 /// [`paint_viewport`](Self::paint_viewport), and
1061 /// [`render_viewport`](Self::render_viewport) each frame.
1062 ///
1063 /// Also set the viewport slot on the camera frame when building the
1064 /// [`FrameData`] for this viewport:
1065 /// ```rust,ignore
1066 /// let id = renderer.create_viewport(&device);
1067 /// let frame = FrameData {
1068 /// camera: CameraFrame::from_camera(&cam, size).with_viewport_id(id),
1069 /// ..Default::default()
1070 /// };
1071 /// ```
1072 pub fn create_viewport(&mut self, device: &wgpu::Device) -> ViewportId {
1073 let idx = self.viewport_slots.len();
1074 self.ensure_viewport_slot(device, idx);
1075 ViewportId(idx)
1076 }
1077
1078 /// Release the heavy GPU texture memory (HDR targets, OIT, bloom, SSAO) held
1079 /// by `id`.
1080 ///
1081 /// The slot index is not reclaimed : future calls with this `ViewportId` will
1082 /// lazily recreate the texture resources as needed. This is useful when a
1083 /// viewport is hidden or minimised and you want to reduce VRAM pressure without
1084 /// invalidating the handle.
1085 pub fn destroy_viewport(&mut self, id: ViewportId) {
1086 if let Some(slot) = self.viewport_slots.get_mut(id.0) {
1087 slot.hdr = None;
1088 }
1089 }
1090
1091 /// Returns the owned-encoder rendering path.
1092 ///
1093 /// Use when you own the window loop and wgpu encoder (winit, raw wgpu).
1094 /// See [`OwnedPath`] for available methods.
1095 pub fn owned(&mut self) -> OwnedPath<'_> {
1096 OwnedPath { renderer: self }
1097 }
1098
1099 /// Returns the pass-based rendering path.
1100 ///
1101 /// Use when a framework provides you with a render pass (eframe, iced).
1102 /// See [`PassPath`] for available methods.
1103 pub fn pass(&mut self) -> PassPath<'_> {
1104 PassPath { renderer: self }
1105 }
1106
1107 /// Returns a read-only paint view for framework paint callbacks.
1108 ///
1109 /// Use this in callbacks where only a shared reference to the renderer is
1110 /// available (e.g. eframe's `CallbackTrait::paint` where `callback_resources`
1111 /// is `&CallbackResources`). Exposes only the paint methods, not prepare.
1112 pub fn pass_view(&self) -> PassView<'_> {
1113 PassView { renderer: self }
1114 }
1115
1116 /// Prepare shared scene data. Call **once per frame**, before any
1117 /// [`prepare_viewport`](Self::prepare_viewport) calls.
1118 ///
1119 /// `frame` provides the scene content (`frame.scene`) and the primary camera
1120 /// used for shadow cascade framing (`frame.camera`). In a multi-viewport
1121 /// setup use any one viewport's `FrameData` here : typically the perspective
1122 /// view : as the shadow framing reference.
1123 ///
1124 /// `scene_effects` carries the scene-global effects: lighting, environment
1125 /// map, and compute filters. Obtain it by constructing [`SceneEffects`]
1126 /// directly or via [`EffectsFrame::split`].
1127 pub(crate) fn prepare_scene(
1128 &mut self,
1129 device: &wgpu::Device,
1130 queue: &wgpu::Queue,
1131 frame: &FrameData,
1132 scene_effects: &SceneEffects<'_>,
1133 ) {
1134 self.prepare_scene_internal(device, queue, frame, scene_effects);
1135 }
1136
1137 /// Prepare per-viewport GPU state (camera, clip planes, overlays, axes).
1138 ///
1139 /// Call once per viewport per frame, **after** [`prepare_scene`](Self::prepare_scene).
1140 ///
1141 /// `id` must have been obtained from [`create_viewport`](Self::create_viewport).
1142 /// `frame.camera.viewport_index` must equal the slot for `id`; use
1143 /// [`CameraFrame::with_viewport_id`] when building the frame.
1144 pub(crate) fn prepare_viewport(
1145 &mut self,
1146 device: &wgpu::Device,
1147 queue: &wgpu::Queue,
1148 id: ViewportId,
1149 frame: &FrameData,
1150 ) {
1151 debug_assert_eq!(
1152 frame.camera.viewport_index, id.0,
1153 "frame.camera.viewport_index ({}) must equal the ViewportId ({}); \
1154 use CameraFrame::with_viewport_id(id)",
1155 frame.camera.viewport_index, id.0,
1156 );
1157 let (_, viewport_fx) = frame.effects.split();
1158 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1159 }
1160
1161 /// Issue draw calls for `id` into a render pass with any lifetime.
1162 ///
1163 /// Identical to [`paint_viewport`](Self::paint_viewport) but accepts a render pass with a
1164 /// non-`'static` lifetime, making it usable from winit, iced, or raw wgpu where the encoder
1165 /// creates its own render pass.
1166 pub(crate) fn paint_viewport_to<'rp>(
1167 &self,
1168 render_pass: &mut wgpu::RenderPass<'rp>,
1169 id: ViewportId,
1170 frame: &FrameData,
1171 ) {
1172 let vp_idx = id.0;
1173 let camera_bg = self.viewport_camera_bind_group(vp_idx);
1174 let grid_bg = self.viewport_grid_bind_group(vp_idx);
1175 let vp_slot = self.viewport_slots.get(vp_idx);
1176 emit_draw_calls!(
1177 &self.resources,
1178 &mut *render_pass,
1179 frame,
1180 self.use_instancing,
1181 &self.instanced_batches,
1182 camera_bg,
1183 grid_bg,
1184 &self.compute_filter_results,
1185 vp_slot,
1186 &self.wireframe_bind_groups
1187 );
1188 emit_scivis_draw_calls!(
1189 &self.resources,
1190 &mut *render_pass,
1191 &self.point_cloud_gpu_data,
1192 &self.glyph_gpu_data,
1193 &self.polyline_gpu_data,
1194 &self.volume_gpu_data,
1195 &self.streamtube_gpu_data,
1196 camera_bg,
1197 &self.tube_gpu_data,
1198 &self.image_slice_gpu_data,
1199 &self.tensor_glyph_gpu_data,
1200 &self.ribbon_gpu_data,
1201 &self.volume_surface_slice_gpu_data,
1202 &self.sprite_gpu_data,
1203 false
1204 );
1205 // Gaussian splats (alpha-blended, back-to-front sorted, no depth write).
1206 if !self.gaussian_splat_draw_data.is_empty() {
1207 if let Some(ref dual) = self.resources.gaussian_splat_pipeline {
1208 render_pass.set_pipeline(dual.for_format(false));
1209 render_pass.set_bind_group(0, camera_bg, &[]);
1210 for dd in &self.gaussian_splat_draw_data {
1211 if dd.wireframe {
1212 continue;
1213 }
1214 if let Some(set) = self.resources.gaussian_splat_store.get(dd.store_index) {
1215 if let Some(Some(vp_sort)) = set.viewport_sort.get(dd.viewport_index) {
1216 render_pass.set_bind_group(1, &vp_sort.render_bg, &[]);
1217 render_pass.draw(0..6, 0..dd.count);
1218 }
1219 }
1220 }
1221 }
1222 }
1223 // TransparentVolumeMesh boundary wireframe overlay.
1224 if !self.tvm_wireframe_draws.is_empty() {
1225 if let Some(ref tvm_bg) = self.tvm_wireframe_bg {
1226 render_pass.set_bind_group(0, camera_bg, &[]);
1227 for mesh_id in &self.tvm_wireframe_draws {
1228 if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
1229 render_pass.set_pipeline(&self.resources.wireframe_pipeline);
1230 render_pass.set_bind_group(1, tvm_bg, &[]);
1231 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1232 render_pass.set_index_buffer(
1233 mesh.edge_index_buffer.slice(..),
1234 wgpu::IndexFormat::Uint32,
1235 );
1236 render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
1237 }
1238 }
1239 }
1240 }
1241 // Shadow atlas viewer overlay.
1242 if frame.effects.show_shadow_atlas {
1243 render_pass.set_pipeline(&self.resources.shadow_atlas_viewer_pipeline);
1244 render_pass.set_bind_group(0, &self.resources.shadow_atlas_viewer_bg, &[]);
1245 render_pass.draw(0..6, 0..1);
1246 }
1247 }
1248
1249 /// Return a reference to the camera bind group for the given viewport slot.
1250 ///
1251 /// Falls back to `resources.camera_bind_group` if no per-viewport slot
1252 /// exists (e.g. in single-viewport mode before the first prepare call).
1253 fn viewport_camera_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
1254 self.viewport_slots
1255 .get(viewport_index)
1256 .map(|slot| &slot.camera_bind_group)
1257 .unwrap_or(&self.resources.camera_bind_group)
1258 }
1259
1260 /// Return a reference to the grid bind group for the given viewport slot.
1261 ///
1262 /// Falls back to `resources.grid_bind_group` if no per-viewport slot exists.
1263 fn viewport_grid_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
1264 self.viewport_slots
1265 .get(viewport_index)
1266 .map(|slot| &slot.grid_bind_group)
1267 .unwrap_or(&self.resources.grid_bind_group)
1268 }
1269
1270 /// Ensure the dyn-res intermediate render target exists for `vp_idx` at the
1271 /// given `scaled_size`, creating or recreating it when size changes.
1272 ///
1273 /// `surface_size` is the native output dimensions (used to size the upscale
1274 /// blit correctly). `ensure_dyn_res_pipeline` is called automatically.
1275 pub(crate) fn ensure_dyn_res_target(
1276 &mut self,
1277 device: &wgpu::Device,
1278 vp_idx: usize,
1279 scaled_size: [u32; 2],
1280 surface_size: [u32; 2],
1281 ) {
1282 self.resources.ensure_dyn_res_pipeline(device);
1283 let needs_create = match &self.viewport_slots[vp_idx].dyn_res {
1284 None => true,
1285 Some(dr) => dr.scaled_size != scaled_size || dr.surface_size != surface_size,
1286 };
1287 if needs_create {
1288 let target = self
1289 .resources
1290 .create_dyn_res_target(device, scaled_size, surface_size);
1291 self.viewport_slots[vp_idx].dyn_res = Some(target);
1292 }
1293 }
1294
1295 /// Ensure per-viewport HDR state exists for `viewport_index` at dimensions `w`×`h`.
1296 ///
1297 /// Calls `ensure_hdr_shared` once to initialise shared pipelines/BGLs/samplers, then
1298 /// lazily creates or resizes the `ViewportHdrState` inside the slot. Idempotent: if the
1299 /// slot already has HDR state at the correct size nothing is recreated.
1300 pub(crate) fn ensure_viewport_hdr(
1301 &mut self,
1302 device: &wgpu::Device,
1303 queue: &wgpu::Queue,
1304 viewport_index: usize,
1305 w: u32,
1306 h: u32,
1307 ssaa_factor: u32,
1308 render_scale: f32,
1309 ) {
1310 let format = self.resources.target_format;
1311 // Ensure shared infrastructure (pipelines, BGLs, samplers) exists.
1312 self.resources.ensure_hdr_shared(device, queue, format);
1313 // When render_scale < 1.0, the HDR upscale path needs the dyn_res
1314 // pipeline and sampler for the final upscale-blit to output resolution.
1315 if render_scale < 1.0 - 0.001 {
1316 self.resources.ensure_dyn_res_pipeline(device);
1317 }
1318 // Compute the scene-resolution render target size.
1319 let scale = render_scale.clamp(0.1, 1.0);
1320 let scene_w = ((w as f32) * scale).round() as u32;
1321 let scene_h = ((h as f32) * scale).round() as u32;
1322 // Ensure the slot exists.
1323 self.ensure_viewport_slot(device, viewport_index);
1324 let slot = &mut self.viewport_slots[viewport_index];
1325 // Create or resize the per-viewport HDR state.
1326 let needs_create = match &slot.hdr {
1327 None => true,
1328 Some(s) => {
1329 s.output_size != [w, h]
1330 || s.scene_size != [scene_w.max(1), scene_h.max(1)]
1331 || s.ssaa_factor != ssaa_factor
1332 }
1333 };
1334 if needs_create {
1335 slot.hdr = Some(self.resources.create_hdr_viewport_state(
1336 device,
1337 queue,
1338 format,
1339 w,
1340 h,
1341 scene_w.max(1),
1342 scene_h.max(1),
1343 ssaa_factor,
1344 ));
1345 }
1346 }
1347}