viewport_lib/renderer/mod.rs
1//! `ViewportRenderer` : the main entry point for the viewport library.
2//!
3//! Wraps [`DeviceResources`] 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 instancing_state;
11use instancing_state::InstancingState;
12mod per_object_state;
13use per_object_state::PerObjectState;
14mod shadow_state;
15use shadow_state::ShadowState;
16mod paths;
17pub use paths::{OwnedPath, PassPath, PassView};
18mod picking;
19pub use picking::PickRectResult;
20mod point_shadow_pool;
21mod prepare;
22mod render;
23pub mod shader_hashes;
24mod shadow_debug_stats;
25mod shadows;
26pub mod stats;
27pub use shadow_debug_stats::ShadowDebugStats;
28
29#[cfg(test)]
30mod hidden_tests;
31#[cfg(test)]
32mod lod_instance_tests;
33
34pub use self::types::{
35 AnimTrack, AtlasViewerCorner, BorderMode, CameraFrame, ClipObject, ClipShape,
36 ComputeFilterItem, ComputeFilterKind, CylindricalFacing, DebugOutputMode, DebugQuantity,
37 DebugVis, DecalAnimation, DecalBlendMode, DecalItem, DecalProjection, EffectsFrame,
38 EmitterConfig, EnvironmentMap, FilterMode, ForceField, FrameData, GaussianSplatData,
39 GaussianSplatId, GaussianSplatItem, GlyphItem, GlyphSetRefItem, GlyphType, GpuImplicitItem,
40 GpuMarchingCubesJob, GpuParticleSystemItem, GradientStop, GroundPlane, GroundPlaneMode,
41 ImageAnchor, ImageSliceItem, InteractionFrame, LabelAnchor, LabelItem, LerpAnim, LicOverlay,
42 LightKind, LightSource, LightingSettings, LineCap, LineJoin, LoadingBarAnchor, LoadingBarItem,
43 MAX_POINT_SHADOW_LIGHTS, MeshInstanceItem, NineSlice, OVERLAY_MAX_GRADIENT_STOPS,
44 OverlayAnimation, OverlayAnimations, OverlayEasing, OverlayFill, OverlayFrame,
45 OverlayImageItem, OverlayPolylineItem, OverlayRectItem, OverlayShape, OverlayShapeItem,
46 OverlayTextureId, POINT_SHADOW_FACE_SIZE, ParticleMeshAlign, PathTrack, PickId, PointCloudItem,
47 PointCloudRefItem, PointRenderMode, PointShadowMode, PolylineItem, PolylineRefItem,
48 PostProcessSettings, RenderCamera, RepeatMode, RibbonItem, RibbonRefItem, RulerItem,
49 ScalarBarAnchor, ScalarBarItem, ScalarBarOrientation, ScatterQuality, ScatterSettings,
50 ScatterVolumeItem, SceneEffects, SceneFrame, SceneRenderItem, ScreenImageItem, ShDegree,
51 ShadowFilter, SliceAxis, SpawnShape, SpriteBlend, SpriteInstanceSetRefItem, SpriteItem,
52 SpriteLitParams, SpriteNormalMode, SpriteOrientation, SpriteSetRefItem, SpriteSizeMode,
53 StreamtubeItem, StreamtubeRefItem, SurfaceLICConfig, SurfaceSubmission, TensorGlyphItem,
54 TensorGlyphSetRefItem, TextureTransform, TileMode, ToneMapping, TriangleDirection, TubeItem,
55 TubeRefItem, VelocityDist, ViewportEffects, ViewportFrame, VolumeItem, VolumeMeshItem,
56 VolumeSurfaceSliceItem, VolumeTransparency, aabb_wireframe_polyline, sphere_wireframe_polyline,
57};
58
59/// An opaque handle to a per-viewport GPU state slot.
60///
61/// Obtained from [`ViewportRenderer::create_viewport`] and passed to
62/// [`ViewportRenderer::prepare_viewport`], [`ViewportRenderer::paint_viewport`],
63/// and [`ViewportRenderer::render_viewport`].
64///
65/// The slot index is managed internally. To bind a `ViewportId` to a camera frame,
66/// use [`CameraFrame::with_viewport_id`]. Single-viewport applications that use
67/// the legacy [`ViewportRenderer::prepare`] / [`ViewportRenderer::paint`] API do
68/// not need this type.
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70pub struct ViewportId(pub(crate) usize);
71
72use self::shadows::{compute_cascade_matrix, compute_cascade_splits};
73use self::types::{INSTANCING_THRESHOLD, InstancedBatch};
74use crate::resources::{
75 BatchMeta, CLIP_VOLUME_MAX, CameraUniform, ClipPlanesUniform, ClipVolumeEntry,
76 ClipVolumesUniform, DeviceResources, GridUniform, InstanceAabb, InstanceData, LightsUniform,
77 ObjectUniform, OutlineEdgeUniform, OutlineObjectBuffers, OutlineUniform, PickInstance,
78 ShadowAtlasUniform, SingleLightUniform, SplatOutlineMaskUniform,
79};
80
81/// Per-viewport GPU state: uniform buffers and bind groups that differ per viewport.
82///
83/// Each viewport slot owns its own camera, clip planes, clip volume, shadow info,
84/// and grid buffers, plus the bind groups that reference them. Scene-global
85/// resources (lights, shadow atlas texture, IBL) are shared via the bind group
86/// pointing to buffers on `DeviceResources`.
87pub(crate) struct ViewportSlot {
88 pub camera_buf: wgpu::Buffer,
89 pub clip_planes_buf: wgpu::Buffer,
90 pub clip_volume_buf: wgpu::Buffer,
91 pub shadow_info_buf: wgpu::Buffer,
92 pub grid_buf: wgpu::Buffer,
93 /// Camera bind group (group 0) referencing this slot's per-viewport buffers
94 /// plus shared scene-global resources.
95 pub camera_bind_group: wgpu::BindGroup,
96 /// Grid bind group (group 0 for grid pipeline) referencing this slot's grid buffer.
97 pub grid_bind_group: wgpu::BindGroup,
98 /// Per-viewport HDR post-process render targets.
99 ///
100 /// Created lazily on first HDR render call and resized when viewport dimensions change.
101 pub hdr: Option<crate::resources::ViewportHdrState>,
102 /// Per-viewport GPU culling outputs (visibility indices, indirect args,
103 /// batch counters, and their bind groups). The cull dispatch for this
104 /// viewport's camera writes here; the draw path reads from here.
105 pub cull: crate::resources::ViewportCullState,
106 /// Per-fragment debug storage buffer (group 0 binding 12). Allocated at
107 /// `width * height * 16` bytes when debug_vis is active; None otherwise.
108 pub debug_frag_buf: Option<wgpu::Buffer>,
109 /// Viewport dimensions for which `debug_frag_buf` was allocated.
110 pub debug_frag_dims: (u32, u32),
111
112 // --- Per-viewport interaction state ---
113 /// Per-frame outline buffers for selected objects, rebuilt in prepare().
114 pub outline_object_buffers: Vec<OutlineObjectBuffers>,
115 /// Per-frame outline buffers for selected Gaussian splat sets, rebuilt in prepare().
116 pub splat_outline_buffers: Vec<crate::resources::SplatOutlineBuffers>,
117 /// Indices into `volume_gpu_data` for selected volumes, rebuilt in prepare().
118 pub volume_outline_indices: Vec<usize>,
119 /// Indices into `glyph_gpu_data` for selected glyph sets, rebuilt in prepare().
120 /// Each entry is (gpu_data_index, instance_filter): None draws all instances,
121 /// Some(indices) draws only those specific instance indices.
122 pub glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
123 /// Indices into `tensor_glyph_gpu_data` for selected tensor glyph sets, rebuilt in prepare().
124 pub tensor_glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
125 /// Indices into `sprite_gpu_data` for selected sprite sets, rebuilt in prepare().
126 pub sprite_outline_indices: Vec<(usize, Option<Vec<u32>>)>,
127 /// Per-frame inline quad outline buffers for selected image slices, rebuilt in prepare().
128 pub raw_geom_outline_buffers: Vec<crate::resources::RawGeomOutlineBuffers>,
129 /// Per-frame NDC rect outline buffers for selected screen images, rebuilt in prepare().
130 pub screen_rect_outline_buffers: Vec<crate::resources::ScreenRectOutlineBuffers>,
131 /// Indices into `implicit_gpu_data` for selected GPU implicit items, rebuilt in prepare().
132 pub implicit_outline_indices: Vec<usize>,
133 /// Per-frame outline data for selected GPU marching cubes jobs, rebuilt in prepare().
134 pub mc_outline_data: Vec<crate::resources::volume::gpu_marching_cubes::McOutlineItem>,
135 /// Outline items for selected streamtubes (index into streamtube_gpu_data + mask bind group).
136 pub streamtube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
137 /// Outline items for selected tubes.
138 pub tube_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
139 /// Outline items for selected ribbons.
140 pub ribbon_outline_items: Vec<crate::resources::CurveMeshOutlineItem>,
141 /// Indices into polyline_gpu_data for selected user polylines.
142 pub polyline_outline_indices: Vec<usize>,
143 /// Per-frame x-ray buffers for selected objects, rebuilt in prepare().
144 pub xray_object_buffers: Vec<(
145 crate::resources::mesh::mesh_store::MeshId,
146 wgpu::Buffer,
147 wgpu::BindGroup,
148 )>,
149 /// Per-frame constraint guide line buffers, rebuilt in prepare().
150 pub constraint_line_buffers: Vec<(
151 wgpu::Buffer,
152 wgpu::Buffer,
153 u32,
154 wgpu::Buffer,
155 wgpu::BindGroup,
156 )>,
157 /// Per-frame cap geometry buffers (section view cross-section fill), rebuilt in prepare().
158 pub cap_buffers: Vec<(
159 wgpu::Buffer,
160 wgpu::Buffer,
161 u32,
162 wgpu::Buffer,
163 wgpu::BindGroup,
164 )>,
165 /// Per-frame clip plane fill overlay buffers, rebuilt in prepare().
166 pub clip_plane_fill_buffers: Vec<(
167 wgpu::Buffer,
168 wgpu::Buffer,
169 u32,
170 wgpu::Buffer,
171 wgpu::BindGroup,
172 )>,
173 /// Per-frame clip plane line overlay buffers, rebuilt in prepare().
174 pub clip_plane_line_buffers: Vec<(
175 wgpu::Buffer,
176 wgpu::Buffer,
177 u32,
178 wgpu::Buffer,
179 wgpu::BindGroup,
180 )>,
181 /// Vertex buffer for axes indicator geometry (rebuilt each frame).
182 pub axes_vertex_buffer: wgpu::Buffer,
183 /// Number of vertices in the axes indicator buffer.
184 pub axes_vertex_count: u32,
185 /// Gizmo model-matrix uniform buffer.
186 pub gizmo_uniform_buf: wgpu::Buffer,
187 /// Gizmo bind group (group 1: model matrix uniform).
188 pub gizmo_bind_group: wgpu::BindGroup,
189 /// Gizmo vertex buffer.
190 pub gizmo_vertex_buffer: wgpu::Buffer,
191 /// Gizmo index buffer.
192 pub gizmo_index_buffer: wgpu::Buffer,
193 /// Number of indices in the current gizmo mesh.
194 pub gizmo_index_count: u32,
195
196 // --- Sub-object highlight (per-viewport, generation-cached) ---
197 /// Per-viewport dynamic resolution intermediate render target.
198 /// `None` when render_scale == 1.0 or not yet initialised.
199 pub dyn_res: Option<crate::resources::gpu::dyn_res::DynResTarget>,
200 /// Per-viewport intermediate render target for the HDR eframe callback path.
201 /// `None` until the first `prepare_hdr_callback` call for this viewport.
202 pub hdr_callback: Option<crate::resources::gpu::dyn_res::HdrCallbackTarget>,
203 /// Cached GPU data for sub-object highlight rendering.
204 /// `None` when no sub-object selection is active and no volumes are selected.
205 pub sub_highlight: Option<crate::resources::SubHighlightGpuData>,
206 /// Version of the last sub-selection snapshot that was uploaded.
207 /// `u64::MAX` forces a rebuild on the first frame.
208 pub sub_highlight_generation: u64,
209}
210
211impl ViewportSlot {
212 /// Draw the axes orientation indicator into an already-begun render pass.
213 ///
214 /// Shared by the LDR and HDR paths; both draw it last in screen space so it
215 /// sits on top. Does nothing when the indicator is disabled or empty.
216 pub(crate) fn draw_axes_indicator(
217 &self,
218 render_pass: &mut wgpu::RenderPass<'_>,
219 resources: &DeviceResources,
220 show_axes_indicator: bool,
221 ) {
222 if show_axes_indicator && self.axes_vertex_count > 0 {
223 render_pass.set_pipeline(&resources.axes_pipeline);
224 render_pass.set_vertex_buffer(0, self.axes_vertex_buffer.slice(..));
225 render_pass.draw(0..self.axes_vertex_count, 0..1);
226 }
227 }
228}
229
230/// Retained pick state for one GPU implicit surface, built during `prepare()`.
231struct GpuImplicitPickItem {
232 id: u64,
233 primitives: Vec<crate::resources::ImplicitPrimitive>,
234 blend_mode: crate::resources::ImplicitBlendMode,
235 max_steps: u32,
236 step_scale: f32,
237 hit_threshold: f32,
238 max_distance: f32,
239}
240
241/// Retained pick state for one GPU marching cubes job, built during `prepare()`.
242struct GpuMcPickItem {
243 id: u64,
244 isovalue: f32,
245 volume_data: std::sync::Arc<crate::geometry::marching_cubes::VolumeData>,
246}
247
248/// Renderer wrapping all GPU resources and providing `prepare()` and `paint()` methods.
249/// Per-viewport scene-colour resolve sampled by the refractive sprite pass.
250///
251/// Lazily allocated on the first frame containing a refractive sprite; resized
252/// whenever the HDR target dimensions change. The bind group is rebuilt with
253/// the resolve when either changes.
254struct SpriteRefractionResolve {
255 texture: wgpu::Texture,
256 view: wgpu::TextureView,
257 size: [u32; 2],
258}
259
260/// GPU timestamp slot for the main opaque HDR scene pass.
261pub(crate) const GPU_TS_SCENE: u32 = 0;
262/// GPU timestamp slot for the directional shadow depth pass.
263pub(crate) const GPU_TS_SHADOW: u32 = 1;
264/// GPU timestamp slot for the OIT accumulation pass.
265pub(crate) const GPU_TS_OIT: u32 = 2;
266/// GPU timestamp slot for the tone-map / resolve pass.
267pub(crate) const GPU_TS_POST: u32 = 3;
268/// GPU timestamp slot for the main-camera GPU cull dispatch (the
269/// `cull_instances` + `write_indirect_args` compute passes). Only the main
270/// camera cull is timed; shadow-cascade and single-mesh culls are not.
271pub(crate) const GPU_TS_CULL: u32 = 4;
272/// Number of measured GPU passes; the query set holds `2 * GPU_TS_SLOTS` entries
273/// (a begin/end pair per slot).
274pub(crate) const GPU_TS_SLOTS: u32 = 5;
275
276/// Owns the GPU pipelines and per-frame state for rendering a scene. Call
277/// `prepare` once per frame to upload data, then `paint_to` (or `render`) to
278/// issue draw calls.
279pub struct ViewportRenderer {
280 resources: DeviceResources,
281 /// State for the instanced (GPU-driven) mesh draw path.
282 instancing: InstancingState,
283 /// Registered item-type plugins keyed by
284 /// [`ItemTypePlugin::type_name`](crate::plugin_api::ItemTypePlugin::type_name).
285 /// `init_gpu` is invoked once on registration; per-frame `prepare` and
286 /// `paint` fire when a matching collection is on `SceneFrame`.
287 item_type_plugins:
288 std::collections::HashMap<&'static str, Box<dyn crate::plugin_api::ItemTypePlugin>>,
289 /// Monotonic frame counter passed to plugin contexts.
290 plugin_frame_index: u64,
291 /// Performance counters from the last frame.
292 last_stats: crate::renderer::stats::FrameStats,
293 /// Per-frame point cloud GPU data, rebuilt in prepare(), consumed in paint().
294 point_cloud_gpu_data: Vec<crate::resources::PointCloudGpuData>,
295 /// Per-frame glyph GPU data, rebuilt in prepare(), consumed in paint().
296 glyph_gpu_data: Vec<crate::resources::GlyphGpuData>,
297 /// Per-frame tensor glyph GPU data, rebuilt in prepare(), consumed in paint().
298 tensor_glyph_gpu_data: Vec<crate::resources::TensorGlyphGpuData>,
299 /// Per-frame polyline GPU data, rebuilt in prepare(), consumed in paint().
300 polyline_gpu_data: Vec<crate::resources::PolylineGpuData>,
301 /// Per-frame volume GPU data, rebuilt in prepare(), consumed in paint().
302 volume_gpu_data: Vec<crate::resources::VolumeGpuData>,
303 /// Per-frame streamtube GPU data, rebuilt in prepare(), consumed in paint().
304 streamtube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
305 /// Per-frame general tube GPU data, rebuilt in prepare(), consumed in paint().
306 tube_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
307 /// Per-frame ribbon GPU data, rebuilt in prepare(), consumed in paint().
308 ribbon_gpu_data: Vec<crate::resources::StreamtubeGpuData>,
309 /// Indices into streamtube_gpu_data for selected streamtubes (set in prepare_scene, consumed in prepare_viewport).
310 streamtube_selected_gpu_indices: Vec<usize>,
311 /// Indices into tube_gpu_data for selected tubes (set in prepare_scene, consumed in prepare_viewport).
312 tube_selected_gpu_indices: Vec<usize>,
313 /// Indices into ribbon_gpu_data for selected ribbons (set in prepare_scene, consumed in prepare_viewport).
314 ribbon_selected_gpu_indices: Vec<usize>,
315 /// Indices into polyline_gpu_data for selected user polylines (set in prepare_scene, consumed in prepare_viewport).
316 polyline_selected_gpu_indices: Vec<usize>,
317 /// Per-frame image slice GPU data, rebuilt in prepare(), consumed in paint().
318 image_slice_gpu_data: Vec<crate::resources::ImageSliceGpuData>,
319 /// Per-frame volume surface slice GPU data, rebuilt in prepare(), consumed in paint().
320 volume_surface_slice_gpu_data: Vec<crate::resources::VolumeSurfaceSliceGpuData>,
321 /// Per-frame Surface LIC GPU data, rebuilt in prepare(), consumed in paint().
322 lic_gpu_data: Vec<crate::resources::LicSurfaceGpuData>,
323 /// Per-frame GPU implicit surface data, rebuilt in prepare(), consumed in paint().
324 implicit_gpu_data: Vec<crate::resources::volume::implicit::ImplicitGpuItem>,
325 /// Per-frame decal draw list, rebuilt in prepare(), consumed in paint() (D1).
326 /// Entries are cheap clones of cached GPU handles from `decal_cache`.
327 decal_gpu_data: Vec<crate::resources::decal::DecalGpuItem>,
328 /// Decal GPU resources cached across frames, keyed by decal content hash.
329 /// Decals are static per submission, so this skips rebuilding a uniform
330 /// buffer and bind group for each decal every frame. Entries not seen in a
331 /// frame are evicted so removed decals do not leak.
332 decal_cache: std::collections::HashMap<u64, crate::resources::decal::DecalGpuItem>,
333 /// Per-frame decal exclude GPU data, rebuilt in prepare(), consumed in paint() (D5).
334 decal_exclude_items: Vec<crate::resources::decal::DecalExcludeGpuItem>,
335 /// Per-frame GPU marching cubes render data, rebuilt in prepare(), consumed in paint().
336 mc_gpu_data: Vec<crate::resources::volume::gpu_marching_cubes::McFrameData>,
337 /// Per-frame sprite GPU data, rebuilt in prepare(), consumed in paint().
338 sprite_gpu_data: Vec<crate::resources::SpriteGpuData>,
339 /// Per-frame mesh-instance batches, rebuilt in prepare(), consumed in paint().
340 mesh_instance_gpu_data: Vec<crate::resources::MeshInstanceGpuData>,
341 /// Per-frame GPU particle systems, dispatched in prepare(), consumed in paint().
342 particle_gpu_data: Vec<crate::resources::gpu::gpu_particles::ParticleFrameData>,
343 /// Scene-colour resolve textures for the refractive sprite pass, indexed
344 /// alongside `viewport_slots`. Lazily allocated when the first refractive
345 /// sprite appears for a viewport.
346 sprite_refraction_resolves: Vec<Option<SpriteRefractionResolve>>,
347 /// Per-frame Gaussian splat draw data, rebuilt in prepare_viewport_internal(), consumed in paint().
348 gaussian_splat_draw_data: Vec<crate::resources::GaussianSplatDrawData>,
349 /// Per-frame screen-image GPU data, rebuilt in prepare(), consumed in paint().
350 screen_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
351 /// Per-frame overlay image GPU data, rebuilt in prepare(), consumed in paint().
352 overlay_image_gpu_data: Vec<crate::resources::ScreenImageGpuData>,
353 /// Per-frame overlay label GPU data, rebuilt in prepare(), consumed in paint().
354 label_gpu_data: Option<crate::resources::LabelGpuData>,
355 /// Per-frame scalar bar GPU data, rebuilt in prepare(), consumed in paint().
356 scalar_bar_gpu_data: Option<crate::resources::LabelGpuData>,
357 /// Per-frame ruler GPU data, rebuilt in prepare(), consumed in paint().
358 ruler_gpu_data: Option<crate::resources::LabelGpuData>,
359 /// Per-frame loading bar GPU data, rebuilt in prepare(), consumed in paint().
360 loading_bar_gpu_data: Option<crate::resources::LabelGpuData>,
361 /// Per-frame overlay rect GPU data, rebuilt in prepare(), consumed in paint().
362 overlay_rect_gpu_data: Option<crate::resources::LabelGpuData>,
363 /// Per-frame SDF overlay shape GPU data, rebuilt in prepare(), consumed in paint().
364 overlay_shape_gpu_data: Option<crate::resources::OverlayShapeGpuData>,
365 /// Cached GPU textures for the backdrop blur effect (frosted glass).
366 /// Recreated when the viewport size changes.
367 backdrop_blur_state: Option<crate::resources::BackdropBlurState>,
368 /// Per-viewport GPU state slots.
369 ///
370 /// Indexed by `FrameData::camera.viewport_index`. Each slot owns independent
371 /// uniform buffers and bind groups for camera, clip planes, clip volume,
372 /// shadow info, and grid. Slots are grown lazily in `prepare` via
373 /// `ensure_viewport_slot`. There are at most 4 in the current UI.
374 viewport_slots: Vec<ViewportSlot>,
375 /// GPU compute filter results from the last `prepare()` call.
376 ///
377 /// Each entry contains a compacted index buffer + count for one filtered mesh.
378 /// Consumed during `paint()` to override the mesh's default index buffer.
379 /// Cleared and rebuilt each frame.
380 compute_filter_results: Vec<crate::resources::ComputeFilterResult>,
381 /// State for the non-instanced (per-object) mesh draw path.
382 mesh_uniforms: PerObjectState,
383 /// Scene surface items after the per-frame LOD resolve, in submission order
384 /// and length. Filled at the end of `prepare_scene_internal` so the paint
385 /// pass draws the resolved level meshes and skips culled items: without
386 /// this the draw path re-read the raw `frame.scene.surfaces`, discarding
387 /// the LOD level swap and cull for every non-instanced item.
388 prepared_surfaces: Vec<SceneRenderItem>,
389 /// Cached shadow state carried across frames.
390 shadow: ShadowState,
391 /// Current runtime mode controlling internal default behavior.
392 runtime_mode: crate::renderer::stats::RuntimeMode,
393 /// Optional cap on how much main-thread time `prepare` is allowed to
394 /// spend running apply closures for completed upload jobs.
395 ///
396 /// `None` means unbounded (apply work runs to completion in one
397 /// frame). `Some(d)` spreads the cost across frames so heavy
398 /// completions do not produce one fat frame; the deferred applies
399 /// run on the next call to `prepare`.
400 upload_budget: Option<std::time::Duration>,
401 /// Active performance policy: target FPS, render scale bounds, and permitted reductions.
402 performance_policy: crate::renderer::stats::PerformancePolicy,
403 /// Current render scale tracked by the adaptation controller (or set manually).
404 ///
405 /// Clamped to `[policy.min_render_scale, policy.max_render_scale]`.
406 /// Reported in `FrameStats::render_scale` each frame.
407 current_render_scale: f32,
408 /// Instant the renderer was constructed. Used as the t=0 reference for
409 /// per-frame animated effects (e.g. `ScatterVolume::noise` time scrolling).
410 start_instant: std::time::Instant,
411 /// Instant recorded at the start of the most recent `prepare()` call.
412 /// Used to compute `total_frame_ms` on the following frame.
413 last_prepare_instant: Option<std::time::Instant>,
414 /// Frame counter incremented each `prepare()` call. Used for picking throttle in Playback mode.
415 frame_counter: u64,
416 /// Current LOD level per item, keyed by pick id, carried across frames so
417 /// level switches use hysteresis. Items without a pick id are not tracked
418 /// here and resolve fresh each frame. Pruned to the items seen each frame.
419 lod_levels: std::collections::HashMap<u64, usize>,
420 /// Current LOD level per mesh instance, keyed by `(item pick id, instance
421 /// index)`. Same role as `lod_levels` but for `MeshInstanceItem`, where each
422 /// instance picks its own level. Instances in items without a pick id are
423 /// not tracked. Pruned to the instances seen each frame.
424 mesh_instance_lod_levels: std::collections::HashMap<(u64, u32), usize>,
425 /// Surface items from the last `prepare()` call, retained for `pick()` dispatch.
426 pick_scene_items: Vec<SceneRenderItem>,
427 /// Point cloud items from the last `prepare()` call, retained for `pick()` dispatch.
428 pick_point_cloud_items: Vec<PointCloudItem>,
429 /// Gaussian splat items from the last `prepare()` call, retained for `pick()` dispatch.
430 pick_splat_items: Vec<GaussianSplatItem>,
431 /// Volume items from the last `prepare()` call, retained for `pick()` dispatch.
432 pick_volume_items: Vec<VolumeItem>,
433 /// Scatter volume items from the last `prepare()` call, retained for `pick()` dispatch.
434 pick_scatter_volume_items: Vec<crate::renderer::types::ScatterVolumeItem>,
435 /// Volumes packed into the GPU storage buffer this frame
436 /// (volume, density_multiplier, flag bits). Stored so `render_viewport`
437 /// can re-upload as needed without re-walking the scene frame.
438 pub(crate) prepared_scatter_volumes:
439 Vec<(crate::scene::scatter_volume::ScatterVolume, f32, u32)>,
440 /// Subset of the prepared scatter volumes that carry `RefractionParams`.
441 /// Cleared and refilled each frame by `prepare_viewport`. The refraction
442 /// pass walks this list; an empty list skips the pass entirely.
443 pub(crate) prepared_refraction_volumes: Vec<(crate::scene::scatter_volume::ScatterVolume, f32)>,
444 /// Per-viewport scatter intermediates and temporal history. Indexed by
445 /// `vp_idx`. Grown lazily inside the scatter pass; each entry is
446 /// reallocated when the requested scatter target size or downsample mode
447 /// changes.
448 pub(crate) scatter_viewport_states: Vec<Option<crate::resources::ScatterViewportState>>,
449 /// Opaque volume mesh items from the last `prepare()` call, retained for cell-level `pick()` dispatch.
450 pick_volume_mesh_items: Vec<VolumeMeshItem>,
451 /// Polyline items from the last `prepare()` call, retained for `pick()` dispatch.
452 pick_polyline_items: Vec<PolylineItem>,
453 /// Glyph items from the last `prepare()` call, retained for `pick()` dispatch.
454 pick_glyph_items: Vec<GlyphItem>,
455 /// Tensor glyph items from the last `prepare()` call, retained for `pick()` dispatch.
456 pick_tensor_glyph_items: Vec<TensorGlyphItem>,
457 /// Sprite items from the last `prepare()` call, retained for `pick()` dispatch.
458 pick_sprite_items: Vec<SpriteItem>,
459 /// Streamtube items from the last `prepare()` call, retained for `pick()` dispatch.
460 pick_streamtube_items: Vec<StreamtubeItem>,
461 /// Tube items from the last `prepare()` call, retained for `pick()` dispatch.
462 pick_tube_items: Vec<TubeItem>,
463 /// Ribbon items from the last `prepare()` call, retained for `pick()` dispatch.
464 pick_ribbon_items: Vec<RibbonItem>,
465 /// Image slice items from the last `prepare()` call, retained for `pick()` dispatch.
466 pick_image_slice_items: Vec<ImageSliceItem>,
467 /// Volume surface slice items from the last `prepare()` call, retained for `pick()` dispatch.
468 pick_volume_surface_slice_items: Vec<VolumeSurfaceSliceItem>,
469 /// Screen image items from the last `prepare()` call, retained for `pick()` dispatch.
470 pick_screen_image_items: Vec<ScreenImageItem>,
471 /// Decal items from the last `prepare()` call, retained for `pick()` dispatch.
472 pick_decal_items: Vec<DecalItem>,
473 /// GPU implicit surface items from the last `prepare()` call, retained for `pick()` dispatch.
474 pick_implicit_items: Vec<GpuImplicitPickItem>,
475 /// GPU marching cubes jobs from the last `prepare()` call, retained for `pick()` dispatch.
476 pick_mc_items: Vec<GpuMcPickItem>,
477 /// When `false`, `prepare()` skips populating the CPU pick caches above, so
478 /// scenes that never call `pick()`/`pick_rect()` avoid a per-frame deep copy
479 /// of all inline geometry. Enable with `set_cpu_pick_cache(true)`.
480 cpu_pick_cache_enabled: bool,
481
482 // --- GPU timestamp queries ---
483 /// Timestamp query set with `2 * GPU_TS_SLOTS` entries: a begin/end pair per
484 /// measured pass (see the `GPU_TS_*` slot constants). `None` when
485 /// `TIMESTAMP_QUERY` is unavailable or not yet initialized.
486 ts_query_set: Option<wgpu::QuerySet>,
487 /// Resolve buffer: `2 * GPU_TS_SLOTS` x u64, GPU-only (`QUERY_RESOLVE | COPY_SRC`).
488 ts_resolve_buf: Option<wgpu::Buffer>,
489 /// Staging buffer: `2 * GPU_TS_SLOTS` x u64, CPU-readable (`COPY_DST | MAP_READ`).
490 ts_staging_buf: Option<wgpu::Buffer>,
491 /// Bitmask of `GPU_TS_*` slots whose timestamps were written this frame.
492 /// Passes are conditional, so unwritten slots hold stale/undefined query
493 /// data; only slots set here are read back. Reset at the start of each frame.
494 ///
495 /// Atomic so a pass method can set its bit through `&self` without colliding
496 /// with the immutable viewport-slot borrows live during pass encoding (and
497 /// to keep `ViewportRenderer: Sync`).
498 ts_written_mask: std::sync::atomic::AtomicU32,
499 /// Snapshot of `ts_written_mask` taken when the queries are resolved, carried
500 /// alongside the one-frame-delayed readback so the reader knows which slots
501 /// are valid.
502 ts_pending_mask: u32,
503 /// Nanoseconds per GPU timestamp tick, from `queue.get_timestamp_period()`.
504 ts_period: f32,
505 /// True when `TIMESTAMP_QUERY_INSIDE_ENCODERS` is available, allowing skipped
506 /// timestamp slots to be filled before resolve (avoids resolving undefined,
507 /// never-written queries, which corrupts the command stream on some drivers).
508 ts_can_fill_gaps: bool,
509 /// True when the staging buffer holds resolved timestamps that have not yet
510 /// been mapped for readback.
511 ts_data_ready: bool,
512 /// True when a map of the timestamp staging buffer is in flight. The render
513 /// path skips the resolve/copy while this is set so the single staging
514 /// buffer is not overwritten before `prepare()` has read it.
515 ts_map_inflight: bool,
516 /// In-flight timestamp map status, set from the map callback: 0 = pending,
517 /// 1 = mapped, 2 = failed. An `Arc<AtomicU8>` rather than an mpsc channel so
518 /// `ViewportRenderer` stays `Sync` (mpsc receivers are not).
519 ts_map_status: std::sync::Arc<std::sync::atomic::AtomicU8>,
520
521 /// Per-phase CPU timings accumulated during the current `prepare()` call,
522 /// copied into `FrameStats::prepare_breakdown` at the end of the frame.
523 prepare_breakdown: crate::renderer::stats::PrepareBreakdown,
524
525 // --- Per-pass degradation state ---
526 /// Tiered degradation ladder position (0 = none, 1 = shadows, 2 = volumes, 3 = effects).
527 /// Advanced one step per over-budget frame once render scale hits minimum;
528 /// reversed one step per comfortably-under-budget frame.
529 degradation_tier: u8,
530 /// Whether the shadow pass was skipped this frame due to budget pressure.
531 /// Computed once per frame at the top of prepare() and used by both
532 /// prepare_scene_internal and reported in FrameStats.
533 degradation_shadows_skipped: bool,
534 /// Whether volume raymarch step size was doubled this frame due to budget pressure.
535 degradation_volume_quality_reduced: bool,
536 /// Whether SSAO, contact shadows, and bloom were skipped this frame.
537 /// Set in prepare(); read by the render path.
538 degradation_effects_throttled: bool,
539
540 /// Lights dropped by the CPU frustum cull on the most recent frame.
541 /// Surfaced through the cluster debug overlay when enabled.
542 pub(crate) last_frustum_culled_lights: u32,
543 /// Most recent cluster build readback. Populated when a frame's
544 /// `ViewportFrame::cluster_stats_request` was true.
545 pub(crate) last_cluster_stats: Option<crate::resources::gpu::clustered::ClusterStats>,
546}
547
548impl ViewportRenderer {
549 /// Create a new renderer with default settings (no MSAA).
550 /// Call once at application startup.
551 pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
552 Self::with_sample_count(device, target_format, 1)
553 }
554
555 /// Create a new renderer with the specified MSAA sample count (1, 2, or 4).
556 ///
557 /// When using MSAA (sample_count > 1), the caller must create multisampled
558 /// colour and depth textures and use them as render pass attachments with the
559 /// final surface texture as the resolve target.
560 pub fn with_sample_count(
561 device: &wgpu::Device,
562 target_format: wgpu::TextureFormat,
563 sample_count: u32,
564 ) -> Self {
565 Self::with_sample_count_and_cache(device, target_format, sample_count, None)
566 }
567
568 /// Create a renderer, seeding the GPU pipeline cache from previously saved
569 /// data so shader compilation can be skipped on later launches.
570 ///
571 /// Pass the bytes returned by an earlier [`pipeline_cache_data`](Self::pipeline_cache_data)
572 /// call, or `None` on first run. The cache only takes effect when the device
573 /// was created with `Features::PIPELINE_CACHE`; otherwise the data is ignored
574 /// and this matches [`new`](Self::new).
575 pub fn new_with_pipeline_cache(
576 device: &wgpu::Device,
577 target_format: wgpu::TextureFormat,
578 pipeline_cache_data: Option<&[u8]>,
579 ) -> Self {
580 Self::with_sample_count_and_cache(device, target_format, 1, pipeline_cache_data)
581 }
582
583 /// Returns the current contents of the GPU pipeline cache, suitable for
584 /// persisting and feeding back into [`new_with_pipeline_cache`](Self::new_with_pipeline_cache)
585 /// on the next launch. `None` when the device lacks `Features::PIPELINE_CACHE`.
586 pub fn pipeline_cache_data(&self) -> Option<Vec<u8>> {
587 self.resources.pipeline_cache.as_ref()?.get_data()
588 }
589
590 /// Like [`with_sample_count`](Self::with_sample_count) with an MSAA count and
591 /// an optional saved pipeline cache.
592 pub fn with_sample_count_and_cache(
593 device: &wgpu::Device,
594 target_format: wgpu::TextureFormat,
595 sample_count: u32,
596 pipeline_cache_data: Option<&[u8]>,
597 ) -> Self {
598 let gpu_culling_supported = device
599 .features()
600 .contains(wgpu::Features::INDIRECT_FIRST_INSTANCE);
601 Self {
602 resources: DeviceResources::new_with_cache(
603 device,
604 target_format,
605 sample_count,
606 pipeline_cache_data,
607 ),
608 instancing: InstancingState::new(gpu_culling_supported),
609 item_type_plugins: std::collections::HashMap::new(),
610 plugin_frame_index: 0,
611 last_stats: crate::renderer::stats::FrameStats::default(),
612 prepare_breakdown: crate::renderer::stats::PrepareBreakdown::default(),
613 point_cloud_gpu_data: Vec::new(),
614 glyph_gpu_data: Vec::new(),
615 tensor_glyph_gpu_data: Vec::new(),
616 polyline_gpu_data: Vec::new(),
617 volume_gpu_data: Vec::new(),
618 streamtube_gpu_data: Vec::new(),
619 tube_gpu_data: Vec::new(),
620 ribbon_gpu_data: Vec::new(),
621 streamtube_selected_gpu_indices: Vec::new(),
622 tube_selected_gpu_indices: Vec::new(),
623 ribbon_selected_gpu_indices: Vec::new(),
624 polyline_selected_gpu_indices: Vec::new(),
625 image_slice_gpu_data: Vec::new(),
626 volume_surface_slice_gpu_data: Vec::new(),
627 sprite_gpu_data: Vec::new(),
628 mesh_instance_gpu_data: Vec::new(),
629 particle_gpu_data: Vec::new(),
630 sprite_refraction_resolves: Vec::new(),
631 gaussian_splat_draw_data: Vec::new(),
632 lic_gpu_data: Vec::new(),
633 implicit_gpu_data: Vec::new(),
634 decal_gpu_data: Vec::new(),
635 decal_cache: std::collections::HashMap::new(),
636 decal_exclude_items: Vec::new(),
637 mc_gpu_data: Vec::new(),
638 screen_image_gpu_data: Vec::new(),
639 overlay_image_gpu_data: Vec::new(),
640 label_gpu_data: None,
641 scalar_bar_gpu_data: None,
642 ruler_gpu_data: None,
643 loading_bar_gpu_data: None,
644 overlay_rect_gpu_data: None,
645 overlay_shape_gpu_data: None,
646 backdrop_blur_state: None,
647 viewport_slots: Vec::new(),
648 compute_filter_results: Vec::new(),
649 mesh_uniforms: PerObjectState::new(),
650 prepared_surfaces: Vec::new(),
651 shadow: ShadowState::new(),
652 runtime_mode: crate::renderer::stats::RuntimeMode::Interactive,
653 performance_policy: crate::renderer::stats::PerformancePolicy::default(),
654 upload_budget: None,
655 current_render_scale: 1.0,
656 start_instant: std::time::Instant::now(),
657 last_prepare_instant: None,
658 frame_counter: 0,
659 lod_levels: std::collections::HashMap::new(),
660 mesh_instance_lod_levels: std::collections::HashMap::new(),
661 pick_scene_items: Vec::new(),
662 pick_point_cloud_items: Vec::new(),
663 pick_splat_items: Vec::new(),
664 pick_volume_items: Vec::new(),
665 pick_scatter_volume_items: Vec::new(),
666 prepared_scatter_volumes: Vec::new(),
667 prepared_refraction_volumes: Vec::new(),
668 scatter_viewport_states: Vec::new(),
669 pick_volume_mesh_items: Vec::new(),
670 pick_polyline_items: Vec::new(),
671 pick_glyph_items: Vec::new(),
672 pick_tensor_glyph_items: Vec::new(),
673 pick_sprite_items: Vec::new(),
674 pick_streamtube_items: Vec::new(),
675 pick_tube_items: Vec::new(),
676 pick_ribbon_items: Vec::new(),
677 pick_image_slice_items: Vec::new(),
678 pick_volume_surface_slice_items: Vec::new(),
679 pick_screen_image_items: Vec::new(),
680 pick_decal_items: Vec::new(),
681 pick_implicit_items: Vec::new(),
682 pick_mc_items: Vec::new(),
683 cpu_pick_cache_enabled: false,
684 ts_query_set: None,
685 ts_resolve_buf: None,
686 ts_staging_buf: None,
687 ts_period: 1.0,
688 ts_data_ready: false,
689 ts_map_inflight: false,
690 ts_map_status: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(0)),
691 ts_written_mask: std::sync::atomic::AtomicU32::new(0),
692 ts_pending_mask: 0,
693 ts_can_fill_gaps: false,
694 degradation_tier: 0,
695 degradation_shadows_skipped: false,
696 degradation_volume_quality_reduced: false,
697 degradation_effects_throttled: false,
698 last_frustum_culled_lights: 0,
699 last_cluster_stats: None,
700 }
701 }
702
703 /// Access the underlying GPU resources (e.g. for mesh uploads).
704 pub fn resources(&self) -> &DeviceResources {
705 &self.resources
706 }
707
708 /// Performance counters from the last completed frame.
709 pub fn last_frame_stats(&self) -> crate::renderer::stats::FrameStats {
710 self.last_stats
711 }
712
713 /// Diagnostics from the cluster build pass on the most recent frame that
714 /// requested them (`ViewportFrame::cluster_stats_request`). Returns
715 /// `None` until a request has been served.
716 pub fn cluster_stats(&self) -> Option<crate::resources::gpu::clustered::ClusterStats> {
717 self.last_cluster_stats
718 }
719
720 /// Disable GPU-driven culling, reverting to the direct draw path.
721 ///
722 /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`
723 /// (culling is already disabled on those devices).
724 pub fn disable_gpu_driven_culling(&mut self) {
725 self.instancing.gpu_culling_enabled = false;
726 }
727
728 /// Force a full instance buffer upload on the next frame.
729 ///
730 /// Normally the renderer skips GPU writes for instanced batches whose data
731 /// has not changed since the last upload. Call this when you have mutated
732 /// batch-relevant state through a path the renderer cannot observe (for
733 /// example, directly modifying GPU buffer contents or scene items after
734 /// `collect_render_items` runs). The flag is consumed once and resets
735 /// automatically after the next `prepare` call.
736 pub fn force_dirty(&mut self) {
737 self.instancing.force_full_upload = true;
738 // Also invalidate the generation cache so the next prepare is guaranteed
739 // to enter the rebuild path even if the scene generation is unchanged.
740 self.instancing.last_scene_generation = u64::MAX;
741 }
742
743 /// Re-enable GPU-driven culling after a call to `disable_gpu_driven_culling`.
744 ///
745 /// Has no effect when the device does not support `INDIRECT_FIRST_INSTANCE`.
746 pub fn enable_gpu_driven_culling(&mut self) {
747 if self.instancing.gpu_culling_supported {
748 self.instancing.gpu_culling_enabled = true;
749 }
750 }
751
752 /// Enable or disable HiZ occlusion culling on the main-camera cull.
753 ///
754 /// When on, the GPU cull builds a hierarchical-Z depth pyramid from the
755 /// previous frame's scene depth and drops instances whose screen-space box
756 /// is entirely behind nearer geometry, on top of the frustum test. Off by
757 /// default. Has no effect unless GPU-driven culling is also active.
758 ///
759 /// The depth source is the previous frame's scene depth reprojected into the
760 /// current camera, so the test is one frame stale and assumes a mostly
761 /// static world. For a static scene (or static occluders) nothing visible is
762 /// culled. With moving or animated occluders, reprojection places last
763 /// frame's occluder depth at its old position and can briefly cull an
764 /// instance that is actually visible this frame; it self-corrects the next
765 /// frame. Treat the "never cull a visible instance" guarantee as holding for
766 /// static occluders only, and leave this off for highly dynamic scenes where
767 /// that pop is unacceptable.
768 ///
769 /// Runs on the HDR path and the owned LDR render path (`render` /
770 /// `render_viewport`), both of which capture scene depth for the
771 /// reprojection. The immediate-mode `paint_to` / `paint_viewport` path does
772 /// not capture depth, so occlusion is a no-op there. Single-viewport only:
773 /// the cull result and HiZ state are shared, not per-view, so with multiple
774 /// viewports on different cameras occlusion can drop geometry that is visible
775 /// in another viewport.
776 ///
777 /// The breakdown is reported in [`FrameStats`]: `gpu_culled_total`,
778 /// `gpu_frustum_visible`, and `gpu_visible_instances` give the per-stage
779 /// survivor counts.
780 pub fn set_occlusion_culling(&mut self, enabled: bool) {
781 self.resources.set_occlusion_culling(enabled);
782 }
783
784 /// Whether HiZ occlusion culling is currently enabled.
785 pub fn occlusion_culling_enabled(&self) -> bool {
786 self.resources.occlusion_culling_enabled()
787 }
788
789 /// Cap the per-frame cost of running upload-job apply closures.
790 ///
791 /// `None` is the default and matches the historical behaviour:
792 /// `prepare` drains every completed upload's apply step in one
793 /// shot. `Some(d)` switches `prepare` over to
794 /// `process_uploads_with_budget` so applies that overflow the
795 /// budget spill to the next frame. Useful when a stress load lands
796 /// many heavy completions on the same frame and the bunched apply
797 /// work shows up as one fat frame at the end of the load.
798 pub fn set_upload_budget(&mut self, budget: Option<std::time::Duration>) {
799 self.upload_budget = budget;
800 }
801
802 /// Currently configured upload budget. See `set_upload_budget`.
803 pub fn upload_budget(&self) -> Option<std::time::Duration> {
804 self.upload_budget
805 }
806
807 /// Set the runtime mode controlling internal default behavior.
808 ///
809 /// - [`RuntimeMode::Interactive`]: full picking rate, full quality (default).
810 /// - [`RuntimeMode::Playback`]: picking throttled to reduce CPU overhead during animation.
811 /// - [`RuntimeMode::Paused`]: full picking rate, full quality.
812 /// - [`RuntimeMode::Capture`]: full quality, intended for screenshot/export workflows.
813 pub fn set_runtime_mode(&mut self, mode: crate::renderer::stats::RuntimeMode) {
814 self.runtime_mode = mode;
815 }
816
817 /// Return the current runtime mode.
818 pub fn runtime_mode(&self) -> crate::renderer::stats::RuntimeMode {
819 self.runtime_mode
820 }
821
822 /// Enable or disable the CPU pick cache.
823 ///
824 /// When enabled, `prepare()` retains a copy of the frame's pickable items so
825 /// `pick()` and `pick_rect()` can run later (e.g. on a mouse click) without the
826 /// scene data. This copies all inline point/glyph/curve geometry each frame, so it
827 /// is disabled by default: turn it on only when using the CPU `pick()`/`pick_rect()`
828 /// path. The GPU path (`pick_scene_gpu`) and the renderer-free
829 /// `interaction::picking` functions do not need it.
830 pub fn set_cpu_pick_cache(&mut self, enabled: bool) {
831 if !enabled && self.cpu_pick_cache_enabled {
832 self.clear_pick_cache();
833 }
834 self.cpu_pick_cache_enabled = enabled;
835 }
836
837 /// Whether the CPU pick cache is enabled. See `set_cpu_pick_cache`.
838 pub fn cpu_pick_cache(&self) -> bool {
839 self.cpu_pick_cache_enabled
840 }
841
842 /// Set the performance policy controlling target FPS, render scale bounds,
843 /// and permitted quality reductions.
844 ///
845 /// The internal adaptation controller activates when
846 /// `policy.allow_dynamic_resolution` is `true` and `policy.target_fps` is
847 /// `Some`. It adjusts `render_scale` within `[min_render_scale,
848 /// max_render_scale]` each frame based on `total_frame_ms`.
849 pub fn set_performance_policy(&mut self, policy: crate::renderer::stats::PerformancePolicy) {
850 self.performance_policy = policy;
851 // Clamp current scale into the new bounds immediately.
852 self.current_render_scale = self
853 .current_render_scale
854 .clamp(policy.min_render_scale, policy.max_render_scale);
855 }
856
857 /// Return the active performance policy.
858 pub fn performance_policy(&self) -> crate::renderer::stats::PerformancePolicy {
859 self.performance_policy
860 }
861
862 /// Manually set the render scale.
863 ///
864 /// Effective when `performance_policy.allow_dynamic_resolution` is `false`.
865 /// When dynamic resolution is enabled the adaptation controller overrides
866 /// this value each frame.
867 ///
868 /// The value is clamped to `[policy.min_render_scale, policy.max_render_scale]`.
869 ///
870 /// Works on both the LDR and HDR render paths. On the HDR path, the scene,
871 /// bloom, SSAO, tone-map, and FXAA all run at the scaled resolution; the
872 /// result is upscale-blitted to native resolution before overlays and grid.
873 pub fn set_render_scale(&mut self, scale: f32) {
874 self.current_render_scale = scale.clamp(
875 self.performance_policy.min_render_scale,
876 self.performance_policy.max_render_scale,
877 );
878 }
879
880 /// Set the target frame rate used to compute [`FrameStats::missed_budget`].
881 ///
882 /// Convenience wrapper that updates `performance_policy.target_fps`.
883 pub fn set_target_fps(&mut self, fps: Option<f32>) {
884 self.performance_policy.target_fps = fps;
885 }
886
887 /// Mutable access to the underlying GPU resources (e.g. for mesh uploads).
888 pub fn resources_mut(&mut self) -> &mut DeviceResources {
889 &mut self.resources
890 }
891
892 /// Returns true when the current frame is rendered via the instanced draw path.
893 ///
894 /// When true, edits to mesh.wgsl shadow sampling code have no effect - the active
895 /// shader is mesh_instanced.wgsl. Check this before testing shader changes.
896 pub fn is_using_instanced_path(&self) -> bool {
897 self.instancing.use_instancing
898 }
899
900 /// Returns the number of instanced batches prepared for the current frame.
901 ///
902 /// Zero when using the non-instanced path. Each batch corresponds to a distinct
903 /// (MeshId, material) combination in the scene.
904 pub fn instanced_batch_count(&self) -> usize {
905 self.instancing.batches.len()
906 }
907
908 /// Run the GPU-driven cull compute against a plugin's
909 /// [`CullSubmission`](crate::plugin_api::CullSubmission).
910 ///
911 /// Encodes two compute passes into `encoder`:
912 /// 1. one thread per instance, tests AABB against `frustum`, claims a
913 /// visibility slot via atomic add;
914 /// 2. one thread per batch, writes a `DrawIndexedIndirect` entry into
915 /// `sub.indirect_out` with the final visible count and zeroes the
916 /// counter for the next call.
917 ///
918 /// After the encoder runs, draw each batch with
919 /// `pass.draw_indexed_indirect(sub.indirect_out, batch_idx * 20)` using
920 /// `sub.visible_out` as the per-instance lookup buffer.
921 ///
922 /// The cull pipeline is created lazily on the first call. Returns
923 /// without dispatching if the device does not support
924 /// `INDIRECT_FIRST_INSTANCE` (call
925 /// [`is_gpu_culling_supported`](Self::is_gpu_culling_supported) first).
926 pub fn submit_cull(
927 &mut self,
928 device: &wgpu::Device,
929 queue: &wgpu::Queue,
930 encoder: &mut wgpu::CommandEncoder,
931 frustum: &crate::camera::frustum::Frustum,
932 sub: &crate::plugin_api::CullSubmission<'_>,
933 ) {
934 if !self.instancing.gpu_culling_supported {
935 return;
936 }
937 if self.instancing.cull_resources.is_none() {
938 self.instancing.cull_resources =
939 Some(crate::renderer::indirect::CullResources::new(device));
940 }
941 let cull = self.instancing.cull_resources.as_ref().unwrap();
942 cull.dispatch(encoder, device, queue, frustum, None, sub, None, None);
943 }
944
945 /// Same as [`submit_cull`](Self::submit_cull) for one shadow cascade.
946 ///
947 /// Uploads the frustum to the cascade slot (so a single frame can submit
948 /// the main pass plus every cascade without overwriting an in-flight
949 /// upload) and forces the cull shader's shadow flag so
950 /// `InstanceAabb::cast_shadows = 0` entries are skipped.
951 ///
952 /// `cascade_idx` must be in `0..4`; values outside that range panic in
953 /// debug builds and clamp to 3 in release.
954 pub fn submit_cull_shadow(
955 &mut self,
956 device: &wgpu::Device,
957 queue: &wgpu::Queue,
958 encoder: &mut wgpu::CommandEncoder,
959 cascade_idx: usize,
960 cascade_frustum: &crate::camera::frustum::Frustum,
961 sub: &crate::plugin_api::CullSubmission<'_>,
962 ) {
963 if !self.instancing.gpu_culling_supported {
964 return;
965 }
966 debug_assert!(cascade_idx < 4, "cascade_idx must be in 0..4");
967 let cascade_idx = cascade_idx.min(3);
968 if self.instancing.cull_resources.is_none() {
969 self.instancing.cull_resources =
970 Some(crate::renderer::indirect::CullResources::new(device));
971 }
972 let cull = self.instancing.cull_resources.as_ref().unwrap();
973 cull.dispatch(
974 encoder,
975 device,
976 queue,
977 cascade_frustum,
978 Some(cascade_idx),
979 sub,
980 None,
981 None,
982 );
983 }
984
985 /// Convenience wrapper around [`submit_cull`](Self::submit_cull) for the
986 /// common case of one mesh with N instances.
987 ///
988 /// The renderer fills its scratch [`BatchMeta`] slot from `draw`, zeroes
989 /// its scratch counter, seeds the indirect entry, and runs a one-batch
990 /// cull. Plugins that only have a single mesh per submission don't have
991 /// to allocate either buffer themselves.
992 ///
993 /// `indirect_out` must hold one `DrawIndexedIndirect` entry (20 bytes).
994 pub fn submit_cull_single_mesh(
995 &mut self,
996 device: &wgpu::Device,
997 queue: &wgpu::Queue,
998 encoder: &mut wgpu::CommandEncoder,
999 frustum: &crate::camera::frustum::Frustum,
1000 instance_aabbs: &wgpu::Buffer,
1001 instance_count: u32,
1002 visible_out: &wgpu::Buffer,
1003 indirect_out: &wgpu::Buffer,
1004 draw: crate::plugin_api::SingleMeshDraw,
1005 shadow_pass: bool,
1006 ) {
1007 self.dispatch_cull_single_mesh(
1008 device,
1009 queue,
1010 encoder,
1011 frustum,
1012 None,
1013 instance_aabbs,
1014 instance_count,
1015 visible_out,
1016 indirect_out,
1017 draw,
1018 shadow_pass,
1019 );
1020 }
1021
1022 /// Single-mesh shadow variant of
1023 /// [`submit_cull_single_mesh`](Self::submit_cull_single_mesh).
1024 pub fn submit_cull_shadow_single_mesh(
1025 &mut self,
1026 device: &wgpu::Device,
1027 queue: &wgpu::Queue,
1028 encoder: &mut wgpu::CommandEncoder,
1029 cascade_idx: usize,
1030 cascade_frustum: &crate::camera::frustum::Frustum,
1031 instance_aabbs: &wgpu::Buffer,
1032 instance_count: u32,
1033 visible_out: &wgpu::Buffer,
1034 indirect_out: &wgpu::Buffer,
1035 draw: crate::plugin_api::SingleMeshDraw,
1036 ) {
1037 debug_assert!(cascade_idx < 4, "cascade_idx must be in 0..4");
1038 let cascade_idx = cascade_idx.min(3);
1039 self.dispatch_cull_single_mesh(
1040 device,
1041 queue,
1042 encoder,
1043 cascade_frustum,
1044 Some(cascade_idx),
1045 instance_aabbs,
1046 instance_count,
1047 visible_out,
1048 indirect_out,
1049 draw,
1050 true,
1051 );
1052 }
1053
1054 #[allow(clippy::too_many_arguments)]
1055 fn dispatch_cull_single_mesh(
1056 &mut self,
1057 device: &wgpu::Device,
1058 queue: &wgpu::Queue,
1059 encoder: &mut wgpu::CommandEncoder,
1060 frustum: &crate::camera::frustum::Frustum,
1061 cascade: Option<usize>,
1062 instance_aabbs: &wgpu::Buffer,
1063 instance_count: u32,
1064 visible_out: &wgpu::Buffer,
1065 indirect_out: &wgpu::Buffer,
1066 draw: crate::plugin_api::SingleMeshDraw,
1067 shadow_pass: bool,
1068 ) {
1069 if !self.instancing.gpu_culling_supported {
1070 return;
1071 }
1072 if self.instancing.cull_resources.is_none() {
1073 self.instancing.cull_resources =
1074 Some(crate::renderer::indirect::CullResources::new(device));
1075 }
1076 let cull = self.instancing.cull_resources.as_ref().unwrap();
1077 let (meta_buf, counter_buf) = cull.scratch_single_mesh_buffers();
1078 let meta = crate::plugin_api::BatchMeta {
1079 index_count: draw.index_count,
1080 first_index: draw.first_index,
1081 instance_offset: 0,
1082 instance_count,
1083 vis_offset: 0,
1084 is_transparent: 0,
1085 _pad: [0, 0],
1086 };
1087 queue.write_buffer(meta_buf, 0, bytemuck::bytes_of(&meta));
1088 queue.write_buffer(counter_buf, 0, &[0u8; 4]);
1089 // Seed the static fields of the indirect entry; the compute pass
1090 // overwrites `instance_count` with the final visible count.
1091 let seed: [u32; 5] = [
1092 draw.index_count,
1093 0,
1094 draw.first_index,
1095 draw.base_vertex as u32,
1096 draw.first_instance,
1097 ];
1098 queue.write_buffer(indirect_out, 0, bytemuck::cast_slice(&seed));
1099
1100 let sub = crate::plugin_api::CullSubmission {
1101 instance_aabbs,
1102 instance_count,
1103 batch_meta: meta_buf,
1104 batch_count: 1,
1105 counter: counter_buf,
1106 visible_out,
1107 indirect_out,
1108 shadow_pass,
1109 };
1110 cull.dispatch(encoder, device, queue, frustum, cascade, &sub, None, None);
1111 }
1112
1113 /// Register an [`ItemTypePlugin`](crate::plugin_api::ItemTypePlugin).
1114 ///
1115 /// Invokes the plugin's `init_gpu` against the current device and
1116 /// shared bind layout, then stores it keyed by `type_name()` for the
1117 /// remainder of the renderer's lifetime. Registering a second plugin
1118 /// with the same `type_name` replaces the first.
1119 ///
1120 /// The renderer will dispatch `prepare` and `paint` to the plugin on
1121 /// every frame where
1122 /// [`SceneFrame::submit_plugin_items`](crate::renderer::SceneFrame::submit_plugin_items)
1123 /// has populated a collection under the same name.
1124 pub fn with_item_type_plugin(
1125 &mut self,
1126 device: &wgpu::Device,
1127 mut plugin: Box<dyn crate::plugin_api::ItemTypePlugin>,
1128 ) {
1129 let shared = self.resources.shared_bindings();
1130 plugin.init_gpu(device, &shared);
1131 let name = plugin.type_name();
1132 self.item_type_plugins.insert(name, plugin);
1133 }
1134
1135 /// Returns true when an item-type plugin with `type_name` is
1136 /// registered.
1137 pub fn has_item_type_plugin(&self, type_name: &str) -> bool {
1138 self.item_type_plugins.contains_key(type_name)
1139 }
1140
1141 /// Notify every registered item-type plugin that the wgpu device has been
1142 /// recreated (device loss, surface re-init, host-driven reset).
1143 ///
1144 /// Calls [`ItemTypePlugin::on_device_recreated`](crate::plugin_api::ItemTypePlugin::on_device_recreated)
1145 /// on each plugin, then re-runs its `init_gpu` against the new device and
1146 /// the current shared bind layout, mirroring registration. The renderer
1147 /// does not detect device loss on its own; the host invokes this after it
1148 /// recreates the device. Matches
1149 /// [`ViewportRuntime::notify_device_recreated`](crate::runtime::ViewportRuntime::notify_device_recreated)
1150 /// on the GPU-plugin side.
1151 pub fn notify_device_recreated(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1152 let shared = self.resources.shared_bindings();
1153 for plugin in self.item_type_plugins.values_mut() {
1154 plugin.on_device_recreated(device, queue);
1155 plugin.init_gpu(device, &shared);
1156 }
1157 }
1158
1159 /// Walk registered item-type plugins, invoke `prepare` for each one
1160 /// that has a matching collection submitted on `frame.scene`, and
1161 /// return the concatenated command buffers.
1162 ///
1163 /// Called internally from the lib's prepare paths; not part of the
1164 /// consumer-facing API.
1165 pub(crate) fn dispatch_plugin_prepare(
1166 &mut self,
1167 device: &wgpu::Device,
1168 queue: &wgpu::Queue,
1169 frame: &FrameData,
1170 ) -> Vec<wgpu::CommandBuffer> {
1171 if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1172 return Vec::new();
1173 }
1174 self.plugin_frame_index = self.plugin_frame_index.wrapping_add(1);
1175 let mut bufs: Vec<wgpu::CommandBuffer> = Vec::new();
1176 for (name, plugin) in self.item_type_plugins.iter_mut() {
1177 if let Some(items) = frame.scene.plugin_items.get(*name) {
1178 // Constructed per plugin because `Jobs` borrows `&resources`
1179 // and the borrow only needs to live for this iteration.
1180 let ctx = crate::plugin_api::ItemFrameContext {
1181 camera: &frame.camera.render_camera,
1182 viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1183 viewport_index: frame.camera.viewport_index,
1184 frame_index: self.plugin_frame_index,
1185 jobs: crate::resources::Jobs::new(&self.resources),
1186 };
1187 bufs.extend(plugin.prepare(device, queue, &ctx, items.as_ref()));
1188 }
1189 }
1190 bufs
1191 }
1192
1193 /// Walk registered item-type plugins and invoke `paint` for each one
1194 /// that has a matching collection submitted on `frame.scene`.
1195 ///
1196 /// Called from inside the lib's HDR scene pass between built-in
1197 /// opaques and the skybox.
1198 pub(crate) fn dispatch_plugin_paint<'rp>(
1199 &'rp self,
1200 pass: &mut wgpu::RenderPass<'rp>,
1201 frame: &'rp FrameData,
1202 ) {
1203 if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1204 return;
1205 }
1206 let ctx = crate::plugin_api::PaintContext {
1207 camera: &frame.camera.render_camera,
1208 viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1209 viewport_index: frame.camera.viewport_index,
1210 frame_index: self.plugin_frame_index,
1211 };
1212 for (name, plugin) in self.item_type_plugins.iter() {
1213 if let Some(items) = frame.scene.plugin_items.get(*name) {
1214 plugin.paint(pass, &ctx, items.as_ref());
1215 }
1216 }
1217 }
1218
1219 /// Walk registered plugins and invoke `paint_transparent` for each
1220 /// one whose collection is on `frame.scene`.
1221 ///
1222 /// Called from inside the lib's OIT render pass, after built-in
1223 /// transparent draws.
1224 pub(crate) fn dispatch_plugin_paint_transparent<'rp>(
1225 &'rp self,
1226 pass: &mut wgpu::RenderPass<'rp>,
1227 frame: &'rp FrameData,
1228 ) {
1229 if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1230 return;
1231 }
1232 let ctx = crate::plugin_api::PaintContext {
1233 camera: &frame.camera.render_camera,
1234 viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1235 viewport_index: frame.camera.viewport_index,
1236 frame_index: self.plugin_frame_index,
1237 };
1238 for (name, plugin) in self.item_type_plugins.iter() {
1239 if let Some(items) = frame.scene.plugin_items.get(*name) {
1240 plugin.paint_transparent(pass, &ctx, items.as_ref());
1241 }
1242 }
1243 }
1244
1245 /// Walk registered plugins and invoke `cast_shadow_pass` for the
1246 /// given cascade.
1247 ///
1248 /// Currently unused: the shadow-pass call site inlines the plugin
1249 /// dispatch because the surrounding scope holds a mutable borrow of
1250 /// `self.resources` that blocks a normal `&self` method call. Kept
1251 /// alongside the other dispatchers as the natural shape; a future
1252 /// refactor that splits the resources borrow can switch back.
1253 #[allow(dead_code)]
1254 pub(crate) fn dispatch_plugin_shadow<'rp>(
1255 &'rp self,
1256 pass: &mut wgpu::RenderPass<'rp>,
1257 frame: &'rp FrameData,
1258 cascade_idx: u32,
1259 light_view_proj: glam::Mat4,
1260 ) {
1261 if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1262 return;
1263 }
1264 let ctx = crate::plugin_api::ShadowCastContext {
1265 cascade_idx,
1266 light_view_proj,
1267 camera: &frame.camera.render_camera,
1268 viewport_index: frame.camera.viewport_index,
1269 frame_index: self.plugin_frame_index,
1270 };
1271 for (name, plugin) in self.item_type_plugins.iter() {
1272 if let Some(items) = frame.scene.plugin_items.get(*name) {
1273 plugin.cast_shadow_pass(pass, &ctx, items.as_ref());
1274 }
1275 }
1276 }
1277
1278 /// Walk registered plugins and invoke `cull` for each one whose
1279 /// collection is on `frame.scene`.
1280 ///
1281 /// Called from the lib's prepare path once the camera frustum for
1282 /// the frame is known.
1283 pub(crate) fn dispatch_plugin_cull(
1284 &mut self,
1285 frustum: &crate::camera::frustum::Frustum,
1286 frame: &FrameData,
1287 ) {
1288 if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1289 return;
1290 }
1291 for (name, plugin) in self.item_type_plugins.iter_mut() {
1292 if let Some(items) = frame.scene.plugin_items.get(*name) {
1293 let ctx = crate::plugin_api::ItemFrameContext {
1294 camera: &frame.camera.render_camera,
1295 viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1296 viewport_index: frame.camera.viewport_index,
1297 frame_index: self.plugin_frame_index,
1298 jobs: crate::resources::Jobs::new(&self.resources),
1299 };
1300 plugin.cull(frustum, &ctx, items.as_ref());
1301 }
1302 }
1303 }
1304
1305 /// Walk registered item-type plugins and invoke `outline_mask` for
1306 /// each one whose collection is on `frame.scene`.
1307 ///
1308 /// Called from inside the lib's outline-mask render pass.
1309 pub(crate) fn dispatch_plugin_outline_mask<'rp>(
1310 &'rp self,
1311 pass: &mut wgpu::RenderPass<'rp>,
1312 frame: &'rp FrameData,
1313 ) {
1314 if self.item_type_plugins.is_empty() || frame.scene.plugin_items.is_empty() {
1315 return;
1316 }
1317 let ctx = crate::plugin_api::OutlineMaskContext {
1318 camera: &frame.camera.render_camera,
1319 viewport_size: glam::Vec2::from(frame.camera.viewport_size),
1320 viewport_index: frame.camera.viewport_index,
1321 frame_index: self.plugin_frame_index,
1322 };
1323 for (name, plugin) in self.item_type_plugins.iter() {
1324 if let Some(items) = frame.scene.plugin_items.get(*name) {
1325 plugin.outline_mask(pass, &ctx, items.as_ref());
1326 }
1327 }
1328 }
1329
1330 /// True when the device supports the features GPU-driven culling needs.
1331 ///
1332 /// Plugins should gate `submit_cull` calls on this. If false, the lib
1333 /// silently no-ops the submission and the plugin must fall back to
1334 /// direct draws.
1335 pub fn is_gpu_culling_supported(&self) -> bool {
1336 self.instancing.gpu_culling_supported
1337 }
1338
1339 /// Returns per-frame shadow and lighting pipeline statistics for debug inspection.
1340 ///
1341 /// All fields reflect the most recently completed `prepare` call (one frame
1342 /// behind the display). Returns default values before the first `prepare` call.
1343 pub fn shadow_debug_stats(&self) -> ShadowDebugStats {
1344 ShadowDebugStats {
1345 using_instanced_path: self.instancing.use_instancing,
1346 instanced_batch_count: self.instancing.batches.len(),
1347 cascade_count: self.shadow.last_cascade_count,
1348 cascade_splits: self.shadow.last_cascade_splits,
1349 shadow_atlas_resolution: self.shadow.last_shadow_atlas_resolution,
1350 shadow_extent_world: self.shadow.last_shadow_extent,
1351 contact_shadow_active: self.shadow.last_contact_shadow_active,
1352 }
1353 }
1354
1355 /// Read the debug values at a specific pixel from the per-fragment storage buffer.
1356 ///
1357 /// Returns `None` when debug_vis is inactive (no buffer allocated) or when `(x, y)`
1358 /// is outside the viewport. The four channels correspond to the current R/G/B channel
1359 /// selectors plus 1.0 for alpha.
1360 ///
1361 /// This submits a GPU-to-CPU copy and waits synchronously. Only call from outside
1362 /// a render pass (e.g., in the next frame's prepare step), not inside paint callbacks.
1363 ///
1364 /// The returned values are from the previous rendered frame.
1365 pub fn read_debug_pixel(
1366 &self,
1367 device: &wgpu::Device,
1368 queue: &wgpu::Queue,
1369 x: u32,
1370 y: u32,
1371 ) -> Option<[f32; 4]> {
1372 // Use the primary viewport slot (index 0).
1373 let slot = self.viewport_slots.first()?;
1374 let buf = slot.debug_frag_buf.as_ref()?;
1375 let (vw, vh) = slot.debug_frag_dims;
1376 if x >= vw || y >= vh {
1377 return None;
1378 }
1379 let byte_offset = ((y as u64) * (vw as u64) + (x as u64)) * 16;
1380 let staging = device.create_buffer(&wgpu::BufferDescriptor {
1381 label: None,
1382 size: 16,
1383 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1384 mapped_at_creation: false,
1385 });
1386 let mut encoder =
1387 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
1388 encoder.copy_buffer_to_buffer(buf, byte_offset, &staging, 0, 16);
1389 queue.submit(Some(encoder.finish()));
1390 let slice = staging.slice(..);
1391 let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
1392 slice.map_async(wgpu::MapMode::Read, move |r| {
1393 let _ = tx.send(r);
1394 });
1395 let _ = device.poll(wgpu::PollType::Wait {
1396 submission_index: None,
1397 timeout: Some(std::time::Duration::from_secs(5)),
1398 });
1399 rx.recv().ok()?.ok()?;
1400 let data = slice.get_mapped_range();
1401 Some(bytemuck::pod_read_unaligned::<[f32; 4]>(&data))
1402 }
1403
1404 /// Upload a Gaussian splat set to the GPU.
1405 ///
1406 /// Call once per splat set at startup or when it changes. The returned
1407 /// [`GaussianSplatId`] is valid until [`free_gaussian_splat`](Self::free_gaussian_splat) is called.
1408 ///
1409 /// # Errors
1410 ///
1411 /// Returns [`ViewportError::InvalidGaussianSplatData`](crate::error::ViewportError::InvalidGaussianSplatData)
1412 /// if `data.positions` is empty or if `positions`, `scales`, `rotations`, and `opacities`
1413 /// differ in length.
1414 ///
1415 /// # Examples
1416 ///
1417 /// ```no_run
1418 /// # use viewport_lib::error::ViewportError;
1419 /// # use viewport_lib::renderer::{GaussianSplatData, ViewportRenderer};
1420 /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
1421 /// let result = renderer.upload_gaussian_splat(device, queue, &GaussianSplatData::default());
1422 /// assert!(matches!(result, Err(ViewportError::InvalidGaussianSplatData { .. })));
1423 /// # }
1424 /// ```
1425 pub fn upload_gaussian_splat(
1426 &mut self,
1427 device: &wgpu::Device,
1428 queue: &wgpu::Queue,
1429 data: &GaussianSplatData,
1430 ) -> crate::error::ViewportResult<GaussianSplatId> {
1431 self.resources.upload_gaussian_splat(device, queue, data)
1432 }
1433
1434 /// Remove an uploaded Gaussian splat set by handle.
1435 ///
1436 /// After this call the `id` is invalid and must not be submitted in `SceneFrame`.
1437 pub fn free_gaussian_splat(&mut self, id: GaussianSplatId) {
1438 self.resources.free_gaussian_splat(id);
1439 }
1440
1441 /// Upload an equirectangular HDR environment map and precompute IBL textures.
1442 ///
1443 /// `pixels` is row-major RGBA f32 data (4 floats per texel), `width`x`height`.
1444 /// This rebuilds camera bind groups so shaders immediately see the new textures.
1445 ///
1446 /// # Errors
1447 ///
1448 /// Returns [`ViewportError::InvalidTextureData`](crate::error::ViewportError::InvalidTextureData)
1449 /// if `pixels.len()` does not equal `width * height * 4`.
1450 ///
1451 /// # Examples
1452 ///
1453 /// ```no_run
1454 /// # use viewport_lib::error::ViewportError;
1455 /// # use viewport_lib::renderer::ViewportRenderer;
1456 /// # fn demo(renderer: &mut ViewportRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
1457 /// // 2x2 RGBA image requires exactly 16 floats.
1458 /// let result = renderer.upload_environment_map(device, queue, &[0.0f32; 12], 2, 2);
1459 /// assert!(matches!(result, Err(ViewportError::InvalidTextureData { expected: 16, actual: 12 })));
1460 /// # }
1461 /// ```
1462 pub fn upload_environment_map(
1463 &mut self,
1464 device: &wgpu::Device,
1465 queue: &wgpu::Queue,
1466 pixels: &[f32],
1467 width: u32,
1468 height: u32,
1469 ) -> crate::error::ViewportResult<()> {
1470 crate::resources::material::environment::upload_environment_map(
1471 &mut self.resources,
1472 device,
1473 queue,
1474 pixels,
1475 width,
1476 height,
1477 )?;
1478 self.rebuild_camera_bind_groups(device);
1479 Ok(())
1480 }
1481
1482 /// Current state of an in-flight upload job.
1483 pub fn upload_status(&self, id: crate::resources::JobId) -> crate::resources::UploadStatus {
1484 self.resources.upload_status(id)
1485 }
1486
1487 /// Count of upload jobs still in flight.
1488 pub fn uploads_pending(&self) -> usize {
1489 self.resources.uploads_pending()
1490 }
1491
1492 /// Wall-clock work duration recorded for an async upload job. See
1493 /// [`DeviceResources::job_duration`].
1494 pub fn job_duration(&self, id: crate::resources::JobId) -> Option<std::time::Duration> {
1495 self.resources.job_duration(id)
1496 }
1497
1498 /// Drop the recorded duration for `id` after reading it. See
1499 /// [`DeviceResources::drop_job_duration`].
1500 pub fn drop_job_duration(&mut self, id: crate::resources::JobId) {
1501 self.resources.drop_job_duration(id);
1502 }
1503
1504 /// Start an asynchronous 3D volume texture upload. See
1505 /// [`DeviceResources::begin_upload_volume`].
1506 pub fn begin_upload_volume(
1507 &mut self,
1508 device: &wgpu::Device,
1509 queue: &wgpu::Queue,
1510 data: Vec<f32>,
1511 dims: [u32; 3],
1512 ) -> crate::error::ViewportResult<crate::resources::JobId> {
1513 self.resources
1514 .begin_upload_volume(device, queue, data, dims)
1515 }
1516
1517 /// Take the volume id produced by a completed
1518 /// [`begin_upload_volume`](Self::begin_upload_volume) job.
1519 pub fn upload_result_volume(
1520 &mut self,
1521 id: crate::resources::JobId,
1522 ) -> crate::error::ViewportResult<crate::resources::VolumeId> {
1523 self.resources.upload_result_volume(id)
1524 }
1525
1526 /// Start an asynchronous marching-cubes-ready volume upload. See
1527 /// [`DeviceResources::begin_upload_volume_for_mc`].
1528 pub fn begin_upload_volume_for_mc(
1529 &mut self,
1530 device: &wgpu::Device,
1531 queue: &wgpu::Queue,
1532 vol: crate::geometry::marching_cubes::VolumeData,
1533 ) -> crate::resources::JobId {
1534 self.resources
1535 .begin_upload_volume_for_mc(device, queue, vol)
1536 }
1537
1538 /// Take the [`McVolumeId`](crate::resources::McVolumeId) produced by a
1539 /// completed [`begin_upload_volume_for_mc`](Self::begin_upload_volume_for_mc) job.
1540 pub fn upload_result_volume_mc(
1541 &mut self,
1542 id: crate::resources::JobId,
1543 ) -> crate::error::ViewportResult<crate::resources::McVolumeId> {
1544 self.resources.upload_result_volume_mc(id)
1545 }
1546
1547 /// Start an asynchronous boundary-only volume mesh upload. See
1548 /// [`DeviceResources::begin_upload_volume_mesh`].
1549 pub fn begin_upload_volume_mesh(
1550 &mut self,
1551 device: &wgpu::Device,
1552 data: crate::resources::volume::volume_mesh::VolumeMeshData,
1553 ) -> crate::resources::JobId {
1554 self.resources.begin_upload_volume_mesh(device, data)
1555 }
1556
1557 /// Take the [`VolumeMeshItem`](crate::VolumeMeshItem)
1558 /// produced by a completed
1559 /// [`begin_upload_volume_mesh`](Self::begin_upload_volume_mesh) job.
1560 pub fn upload_result_volume_mesh(
1561 &mut self,
1562 id: crate::resources::JobId,
1563 ) -> crate::error::ViewportResult<crate::VolumeMeshItem> {
1564 self.resources.upload_result_volume_mesh(id)
1565 }
1566
1567 /// Start an asynchronous clipped volume mesh upload. See
1568 /// [`DeviceResources::begin_upload_clipped_volume_mesh`].
1569 pub fn begin_upload_clipped_volume_mesh(
1570 &mut self,
1571 device: &wgpu::Device,
1572 data: crate::resources::volume::volume_mesh::VolumeMeshData,
1573 clip_planes: Vec<[f32; 4]>,
1574 ) -> crate::resources::JobId {
1575 self.resources
1576 .begin_upload_clipped_volume_mesh(device, data, clip_planes)
1577 }
1578
1579 /// Take the [`VolumeMeshItem`](crate::VolumeMeshItem)
1580 /// produced by a completed
1581 /// [`begin_upload_clipped_volume_mesh`](Self::begin_upload_clipped_volume_mesh) job.
1582 pub fn upload_result_clipped_volume_mesh(
1583 &mut self,
1584 id: crate::resources::JobId,
1585 ) -> crate::error::ViewportResult<crate::VolumeMeshItem> {
1586 self.resources.upload_result_clipped_volume_mesh(id)
1587 }
1588
1589 /// Start an asynchronous sparse voxel grid upload. See
1590 /// [`DeviceResources::begin_upload_sparse_volume_grid_data`].
1591 pub fn begin_upload_sparse_volume_grid_data(
1592 &mut self,
1593 device: &wgpu::Device,
1594 data: crate::resources::SparseVolumeGridData,
1595 ) -> crate::resources::JobId {
1596 self.resources
1597 .begin_upload_sparse_volume_grid_data(device, data)
1598 }
1599
1600 /// Take the [`MeshId`](crate::resources::mesh::mesh_store::MeshId) produced by a completed
1601 /// [`begin_upload_sparse_volume_grid_data`](Self::begin_upload_sparse_volume_grid_data)
1602 /// job.
1603 pub fn upload_result_sparse_volume_grid(
1604 &mut self,
1605 id: crate::resources::JobId,
1606 ) -> crate::error::ViewportResult<crate::resources::mesh::mesh_store::MeshId> {
1607 self.resources.upload_result_sparse_volume_grid(id)
1608 }
1609
1610 /// Start an asynchronous Gaussian splat upload. See
1611 /// [`DeviceResources::begin_upload_gaussian_splat`].
1612 pub fn begin_upload_gaussian_splat(
1613 &mut self,
1614 device: &wgpu::Device,
1615 queue: &wgpu::Queue,
1616 data: crate::renderer::GaussianSplatData,
1617 ) -> crate::error::ViewportResult<crate::resources::JobId> {
1618 self.resources
1619 .begin_upload_gaussian_splat(device, queue, data)
1620 }
1621
1622 /// Take the [`GaussianSplatId`](crate::renderer::GaussianSplatId) produced by a
1623 /// completed [`begin_upload_gaussian_splat`](Self::begin_upload_gaussian_splat) job.
1624 pub fn upload_result_gaussian_splat(
1625 &mut self,
1626 id: crate::resources::JobId,
1627 ) -> crate::error::ViewportResult<crate::renderer::GaussianSplatId> {
1628 self.resources.upload_result_gaussian_splat(id)
1629 }
1630
1631 /// Start an asynchronous overlay texture upload. See
1632 /// [`DeviceResources::begin_upload_overlay_texture`].
1633 pub fn begin_upload_overlay_texture(
1634 &mut self,
1635 device: &wgpu::Device,
1636 queue: &wgpu::Queue,
1637 width: u32,
1638 height: u32,
1639 rgba_data: Vec<u8>,
1640 ) -> crate::error::ViewportResult<crate::resources::JobId> {
1641 self.resources
1642 .begin_upload_overlay_texture(device, queue, width, height, rgba_data)
1643 }
1644
1645 /// Take the [`OverlayTextureId`](crate::renderer::OverlayTextureId) produced by a
1646 /// completed [`begin_upload_overlay_texture`](Self::begin_upload_overlay_texture) job.
1647 pub fn upload_result_overlay_texture(
1648 &mut self,
1649 id: crate::resources::JobId,
1650 ) -> crate::error::ViewportResult<crate::renderer::OverlayTextureId> {
1651 self.resources.upload_result_overlay_texture(id)
1652 }
1653
1654 /// True when no upload jobs are in flight.
1655 pub fn all_uploads_complete(&self) -> bool {
1656 self.resources.all_uploads_complete()
1657 }
1658
1659 /// Register a callback to fire when an upload job finishes. See
1660 /// [`DeviceResources::on_upload_complete`] for the semantics.
1661 pub fn on_upload_complete<F>(&mut self, id: crate::resources::JobId, cb: F)
1662 where
1663 F: FnOnce(&crate::resources::UploadStatus) + Send + 'static,
1664 {
1665 self.resources.on_upload_complete(id, cb);
1666 }
1667
1668 /// Start an asynchronous albedo texture upload. See
1669 /// [`DeviceResources::begin_upload_texture`] for the semantics.
1670 pub fn begin_upload_texture(
1671 &mut self,
1672 device: &wgpu::Device,
1673 queue: &wgpu::Queue,
1674 width: u32,
1675 height: u32,
1676 rgba: Vec<u8>,
1677 ) -> crate::error::ViewportResult<crate::resources::JobId> {
1678 self.resources
1679 .begin_upload_texture(device, queue, width, height, rgba)
1680 }
1681
1682 /// Start an asynchronous normal-map upload. See
1683 /// [`DeviceResources::begin_upload_normal_map`] for the semantics.
1684 pub fn begin_upload_normal_map(
1685 &mut self,
1686 device: &wgpu::Device,
1687 queue: &wgpu::Queue,
1688 width: u32,
1689 height: u32,
1690 rgba: Vec<u8>,
1691 ) -> crate::error::ViewportResult<crate::resources::JobId> {
1692 self.resources
1693 .begin_upload_normal_map(device, queue, width, height, rgba)
1694 }
1695
1696 /// Take the texture id from a completed async texture upload. See
1697 /// [`DeviceResources::upload_result_texture`] for the error
1698 /// semantics.
1699 pub fn upload_result_texture(
1700 &mut self,
1701 id: crate::resources::JobId,
1702 ) -> crate::error::ViewportResult<crate::resources::TextureId> {
1703 self.resources.upload_result_texture(id)
1704 }
1705
1706 /// Start an asynchronous mesh upload.
1707 ///
1708 /// Returns a `JobId` immediately. The CPU prep (tangent computation,
1709 /// vertex repack, normal-line build) runs on a worker thread; GPU
1710 /// buffer creation and store insertion run on the main thread during
1711 /// the next `process_uploads` call after the worker finishes. Once the
1712 /// status is `Ready`, take the produced `MeshId` with
1713 /// `upload_result_mesh`.
1714 ///
1715 /// Ownership of `data` transfers into the worker; clone at the call
1716 /// site if you need to retain it.
1717 ///
1718 /// # Errors
1719 ///
1720 /// Same validation errors as `upload_mesh_data` (empty mesh, length
1721 /// mismatch, invalid vertex index), all reported before the job is
1722 /// submitted.
1723 pub fn begin_upload_mesh_data(
1724 &mut self,
1725 device: &wgpu::Device,
1726 data: crate::resources::MeshData,
1727 ) -> crate::error::ViewportResult<crate::resources::JobId> {
1728 self.resources.begin_upload_mesh_data(device, data)
1729 }
1730
1731 /// Take the `MeshId` produced by a completed `begin_upload_mesh_data`
1732 /// job. See [`DeviceResources::upload_result_mesh`] for the error
1733 /// semantics.
1734 pub fn upload_result_mesh(
1735 &mut self,
1736 id: crate::resources::JobId,
1737 ) -> crate::error::ViewportResult<crate::resources::mesh::mesh_store::MeshId> {
1738 self.resources.upload_result_mesh(id)
1739 }
1740
1741 /// Start an asynchronous environment-map upload.
1742 ///
1743 /// Returns immediately with a `JobId`. The caller drives the upload-job
1744 /// runner from the renderer's prepare path each frame; once the job
1745 /// reports `Ready`, the IBL textures are live on the renderer and a
1746 /// subsequent call to `rebuild_camera_bind_groups` makes them visible
1747 /// to shaders.
1748 ///
1749 /// Ownership of `pixels` transfers into the background worker.
1750 ///
1751 /// # Errors
1752 ///
1753 /// Returns [`ViewportError::InvalidTextureData`](crate::error::ViewportError::InvalidTextureData)
1754 /// if `pixels.len() != width * height * 4`.
1755 pub fn begin_upload_environment_map(
1756 &mut self,
1757 device: &wgpu::Device,
1758 queue: &wgpu::Queue,
1759 pixels: Vec<f32>,
1760 width: u32,
1761 height: u32,
1762 ) -> crate::error::ViewportResult<crate::resources::JobId> {
1763 crate::resources::material::environment::begin_upload_environment_map(
1764 &mut self.resources,
1765 device,
1766 queue,
1767 pixels,
1768 width,
1769 height,
1770 )
1771 }
1772
1773 /// Rebuild the primary and per-viewport camera bind groups.
1774 ///
1775 /// Call after IBL textures are uploaded so the shaders see the new
1776 /// environment. The synchronous `upload_environment_map` does this
1777 /// internally; consumers driving the async path through
1778 /// `begin_upload_environment_map` should call this themselves once the
1779 /// matching job reports `Ready`.
1780 pub fn rebuild_camera_bind_groups(&mut self, device: &wgpu::Device) {
1781 self.resources.camera_bind_group = self.resources.create_camera_bind_group(
1782 device,
1783 &self.resources.camera_uniform_buf,
1784 &self.resources.clip_planes_uniform_buf,
1785 &self.resources.shadow_info_buf,
1786 &self.resources.clip_volume_uniform_buf,
1787 &self.resources.debug_frag_sentinel_buf,
1788 "camera_bind_group",
1789 );
1790
1791 for slot in &mut self.viewport_slots {
1792 let dbg_buf = slot
1793 .debug_frag_buf
1794 .as_ref()
1795 .unwrap_or(&self.resources.debug_frag_sentinel_buf);
1796 slot.camera_bind_group = self.resources.create_camera_bind_group(
1797 device,
1798 &slot.camera_buf,
1799 &slot.clip_planes_buf,
1800 &slot.shadow_info_buf,
1801 &slot.clip_volume_buf,
1802 dbg_buf,
1803 "per_viewport_camera_bg",
1804 );
1805 }
1806 }
1807
1808 /// Ensure a per-viewport slot exists for `viewport_index`.
1809 ///
1810 /// Creates a full `ViewportSlot` with independent uniform buffers for camera,
1811 /// clip planes, clip volume, shadow info, and grid. The camera bind group
1812 /// references this slot's per-viewport buffers plus shared scene-global
1813 /// resources. Slots are created lazily and never destroyed.
1814 fn ensure_viewport_slot(&mut self, device: &wgpu::Device, viewport_index: usize) {
1815 while self.viewport_slots.len() <= viewport_index {
1816 let camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
1817 label: Some("vp_camera_buf"),
1818 size: std::mem::size_of::<CameraUniform>() as u64,
1819 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1820 mapped_at_creation: false,
1821 });
1822 let clip_planes_buf = device.create_buffer(&wgpu::BufferDescriptor {
1823 label: Some("vp_clip_planes_buf"),
1824 size: std::mem::size_of::<ClipPlanesUniform>() as u64,
1825 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1826 mapped_at_creation: false,
1827 });
1828 let clip_volume_buf = device.create_buffer(&wgpu::BufferDescriptor {
1829 label: Some("vp_clip_volume_buf"),
1830 size: std::mem::size_of::<ClipVolumesUniform>() as u64,
1831 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1832 mapped_at_creation: false,
1833 });
1834 // Seeded with the latest shadow atlas uniform rather than zeros:
1835 // prepare_scene_internal writes shadow info only to slots that
1836 // exist at that point, so a slot created later in the same frame
1837 // would otherwise render its first frame with zeroed cascade
1838 // matrices (NaN shadow UVs, everything shadowed).
1839 let shadow_info_buf = device.create_buffer(&wgpu::BufferDescriptor {
1840 label: Some("vp_shadow_info_buf"),
1841 size: std::mem::size_of::<ShadowAtlasUniform>() as u64,
1842 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1843 mapped_at_creation: true,
1844 });
1845 shadow_info_buf
1846 .slice(..)
1847 .get_mapped_range_mut()
1848 .copy_from_slice(bytemuck::cast_slice(&[self
1849 .shadow
1850 .last_shadow_atlas_uniform]));
1851 shadow_info_buf.unmap();
1852 let grid_buf = device.create_buffer(&wgpu::BufferDescriptor {
1853 label: Some("vp_grid_buf"),
1854 size: std::mem::size_of::<GridUniform>() as u64,
1855 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1856 mapped_at_creation: false,
1857 });
1858
1859 let camera_bind_group = self.resources.create_camera_bind_group(
1860 device,
1861 &camera_buf,
1862 &clip_planes_buf,
1863 &shadow_info_buf,
1864 &clip_volume_buf,
1865 &self.resources.debug_frag_sentinel_buf,
1866 "per_viewport_camera_bg",
1867 );
1868
1869 let grid_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1870 label: Some("vp_grid_bind_group"),
1871 layout: &self.resources.grid_bind_group_layout,
1872 entries: &[wgpu::BindGroupEntry {
1873 binding: 0,
1874 resource: grid_buf.as_entire_binding(),
1875 }],
1876 });
1877
1878 // Per-viewport gizmo buffers (initial mesh: Translate, no hover, identity orientation).
1879 let (gizmo_verts, gizmo_indices) =
1880 crate::interaction::manipulation::gizmo::build_gizmo_mesh(
1881 crate::interaction::manipulation::gizmo::GizmoMode::Translate,
1882 crate::interaction::manipulation::gizmo::GizmoAxis::None,
1883 glam::Quat::IDENTITY,
1884 );
1885 let gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1886 label: Some("vp_gizmo_vertex_buf"),
1887 size: (std::mem::size_of::<crate::resources::Vertex>() * gizmo_verts.len().max(1))
1888 as u64,
1889 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1890 mapped_at_creation: true,
1891 });
1892 gizmo_vertex_buffer
1893 .slice(..)
1894 .get_mapped_range_mut()
1895 .copy_from_slice(bytemuck::cast_slice(&gizmo_verts));
1896 gizmo_vertex_buffer.unmap();
1897 let gizmo_index_count = gizmo_indices.len() as u32;
1898 let gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1899 label: Some("vp_gizmo_index_buf"),
1900 size: (std::mem::size_of::<u32>() * gizmo_indices.len().max(1)) as u64,
1901 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1902 mapped_at_creation: true,
1903 });
1904 gizmo_index_buffer
1905 .slice(..)
1906 .get_mapped_range_mut()
1907 .copy_from_slice(bytemuck::cast_slice(&gizmo_indices));
1908 gizmo_index_buffer.unmap();
1909 let gizmo_uniform = crate::interaction::manipulation::gizmo::GizmoUniform {
1910 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1911 };
1912 let gizmo_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
1913 label: Some("vp_gizmo_uniform_buf"),
1914 size: std::mem::size_of::<crate::interaction::manipulation::gizmo::GizmoUniform>()
1915 as u64,
1916 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1917 mapped_at_creation: true,
1918 });
1919 gizmo_uniform_buf
1920 .slice(..)
1921 .get_mapped_range_mut()
1922 .copy_from_slice(bytemuck::cast_slice(&[gizmo_uniform]));
1923 gizmo_uniform_buf.unmap();
1924 let gizmo_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1925 label: Some("vp_gizmo_bind_group"),
1926 layout: &self.resources.gizmo_bind_group_layout,
1927 entries: &[wgpu::BindGroupEntry {
1928 binding: 0,
1929 resource: gizmo_uniform_buf.as_entire_binding(),
1930 }],
1931 });
1932
1933 // Per-viewport axes vertex buffer (2048 vertices = enough for all axes geometry).
1934 let axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1935 label: Some("vp_axes_vertex_buf"),
1936 size: (std::mem::size_of::<crate::interaction::widgets::axes_indicator::AxesVertex>(
1937 ) * 2048) as u64,
1938 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1939 mapped_at_creation: false,
1940 });
1941
1942 self.viewport_slots.push(ViewportSlot {
1943 camera_buf,
1944 clip_planes_buf,
1945 clip_volume_buf,
1946 shadow_info_buf,
1947 grid_buf,
1948 camera_bind_group,
1949 grid_bind_group,
1950 hdr: None,
1951 cull: crate::resources::ViewportCullState::new(),
1952 debug_frag_buf: None,
1953 debug_frag_dims: (0, 0),
1954 outline_object_buffers: Vec::new(),
1955 splat_outline_buffers: Vec::new(),
1956 volume_outline_indices: Vec::new(),
1957 glyph_outline_indices: Vec::new(),
1958 tensor_glyph_outline_indices: Vec::new(),
1959 sprite_outline_indices: Vec::new(),
1960 raw_geom_outline_buffers: Vec::new(),
1961 screen_rect_outline_buffers: Vec::new(),
1962 implicit_outline_indices: Vec::new(),
1963 mc_outline_data: Vec::new(),
1964 streamtube_outline_items: Vec::new(),
1965 tube_outline_items: Vec::new(),
1966 ribbon_outline_items: Vec::new(),
1967 polyline_outline_indices: Vec::new(),
1968 xray_object_buffers: Vec::new(),
1969 constraint_line_buffers: Vec::new(),
1970 cap_buffers: Vec::new(),
1971 clip_plane_fill_buffers: Vec::new(),
1972 clip_plane_line_buffers: Vec::new(),
1973 axes_vertex_buffer,
1974 axes_vertex_count: 0,
1975 gizmo_uniform_buf,
1976 gizmo_bind_group,
1977 gizmo_vertex_buffer,
1978 gizmo_index_buffer,
1979 gizmo_index_count,
1980 sub_highlight: None,
1981 sub_highlight_generation: u64::MAX,
1982 dyn_res: None,
1983 hdr_callback: None,
1984 });
1985 }
1986 }
1987
1988 // -----------------------------------------------------------------------
1989 // Multi-viewport public API
1990 // -----------------------------------------------------------------------
1991
1992 /// Create a new viewport slot and return its handle.
1993 ///
1994 /// The returned [`ViewportId`] is stable for the lifetime of the renderer.
1995 /// Pass it to [`prepare_viewport`](Self::prepare_viewport),
1996 /// [`paint_viewport`](Self::paint_viewport), and
1997 /// [`render_viewport`](Self::render_viewport) each frame.
1998 ///
1999 /// Also set the viewport slot on the camera frame when building the
2000 /// [`FrameData`] for this viewport:
2001 /// ```rust,ignore
2002 /// let id = renderer.create_viewport(&device);
2003 /// let frame = FrameData {
2004 /// camera: CameraFrame::from_camera(&cam, size).with_viewport_id(id),
2005 /// ..Default::default()
2006 /// };
2007 /// ```
2008 pub fn create_viewport(&mut self, device: &wgpu::Device) -> ViewportId {
2009 let idx = self.viewport_slots.len();
2010 self.ensure_viewport_slot(device, idx);
2011 ViewportId(idx)
2012 }
2013
2014 /// Release the heavy GPU texture memory (HDR targets, OIT, bloom, SSAO) held
2015 /// by `id`.
2016 ///
2017 /// The slot index is not reclaimed : future calls with this `ViewportId` will
2018 /// lazily recreate the texture resources as needed. This is useful when a
2019 /// viewport is hidden or minimised and you want to reduce VRAM pressure without
2020 /// invalidating the handle.
2021 pub fn destroy_viewport(&mut self, id: ViewportId) {
2022 if let Some(slot) = self.viewport_slots.get_mut(id.0) {
2023 slot.hdr = None;
2024 }
2025 }
2026
2027 /// Returns the owned-encoder rendering path.
2028 ///
2029 /// Use when you own the window loop and wgpu encoder (winit, raw wgpu).
2030 /// See [`OwnedPath`] for available methods.
2031 pub fn owned(&mut self) -> OwnedPath<'_> {
2032 OwnedPath { renderer: self }
2033 }
2034
2035 /// Returns the pass-based rendering path.
2036 ///
2037 /// Use when a framework provides you with a render pass (eframe, iced).
2038 /// See [`PassPath`] for available methods.
2039 pub fn pass(&mut self) -> PassPath<'_> {
2040 PassPath { renderer: self }
2041 }
2042
2043 /// Returns a read-only paint view for framework paint callbacks.
2044 ///
2045 /// Use this in callbacks where only a shared reference to the renderer is
2046 /// available (e.g. eframe's `CallbackTrait::paint` where `callback_resources`
2047 /// is `&CallbackResources`). Exposes only the paint methods, not prepare.
2048 pub fn pass_view(&self) -> PassView<'_> {
2049 PassView { renderer: self }
2050 }
2051
2052 /// Prepare shared scene data. Call **once per frame**, before any
2053 /// [`prepare_viewport`](Self::prepare_viewport) calls.
2054 ///
2055 /// `frame` provides the scene content (`frame.scene`) and the primary camera
2056 /// used for shadow cascade framing (`frame.camera`). In a multi-viewport
2057 /// setup use any one viewport's `FrameData` here : typically the perspective
2058 /// view : as the shadow framing reference.
2059 ///
2060 /// `scene_effects` carries the scene-global effects: lighting, environment
2061 /// map, and compute filters. Obtain it by constructing [`SceneEffects`]
2062 /// directly or via [`EffectsFrame::split`].
2063 pub(crate) fn prepare_scene(
2064 &mut self,
2065 device: &wgpu::Device,
2066 queue: &wgpu::Queue,
2067 frame: &FrameData,
2068 scene_effects: &SceneEffects<'_>,
2069 ) {
2070 self.prepare_scene_internal(device, queue, frame, scene_effects);
2071 }
2072
2073 /// Prepare per-viewport GPU state (camera, clip planes, overlays, axes).
2074 ///
2075 /// Call once per viewport per frame, **after** [`prepare_scene`](Self::prepare_scene).
2076 ///
2077 /// `id` must have been obtained from [`create_viewport`](Self::create_viewport).
2078 /// `frame.camera.viewport_index` must equal the slot for `id`; use
2079 /// [`CameraFrame::with_viewport_id`] when building the frame.
2080 pub(crate) fn prepare_viewport(
2081 &mut self,
2082 device: &wgpu::Device,
2083 queue: &wgpu::Queue,
2084 id: ViewportId,
2085 frame: &FrameData,
2086 ) {
2087 debug_assert_eq!(
2088 frame.camera.viewport_index, id.0,
2089 "frame.camera.viewport_index ({}) must equal the ViewportId ({}); \
2090 use CameraFrame::with_viewport_id(id)",
2091 frame.camera.viewport_index, id.0,
2092 );
2093 let (_, viewport_fx) = frame.effects.split();
2094 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
2095 }
2096
2097 /// Issue draw calls for `id` into a render pass with any lifetime.
2098 ///
2099 /// Identical to [`paint_viewport`](Self::paint_viewport) but accepts a render pass with a
2100 /// non-`'static` lifetime, making it usable from winit, iced, or raw wgpu where the encoder
2101 /// creates its own render pass.
2102 pub(crate) fn paint_viewport_to<'rp>(
2103 &self,
2104 render_pass: &mut wgpu::RenderPass<'rp>,
2105 id: ViewportId,
2106 frame: &FrameData,
2107 ) {
2108 let vp_idx = id.0;
2109 let camera_bg = self.viewport_camera_bind_group(vp_idx);
2110 let grid_bg = self.viewport_grid_bind_group(vp_idx);
2111 let vp_slot = self.viewport_slots.get(vp_idx);
2112 emit_draw_calls!(
2113 &self.resources,
2114 &mut *render_pass,
2115 frame,
2116 self.instancing.use_instancing,
2117 &self.instancing.batches,
2118 camera_bg,
2119 grid_bg,
2120 &self.compute_filter_results,
2121 vp_slot,
2122 &self.mesh_uniforms.wireframe_bind_groups,
2123 &self.mesh_uniforms.bind_groups,
2124 &self.prepared_surfaces
2125 );
2126 emit_scivis_draw_calls!(
2127 &self.resources,
2128 &mut *render_pass,
2129 &self.point_cloud_gpu_data,
2130 &self.glyph_gpu_data,
2131 &self.polyline_gpu_data,
2132 &self.volume_gpu_data,
2133 &self.streamtube_gpu_data,
2134 camera_bg,
2135 &self.tube_gpu_data,
2136 &self.image_slice_gpu_data,
2137 &self.tensor_glyph_gpu_data,
2138 &self.ribbon_gpu_data,
2139 &self.volume_surface_slice_gpu_data,
2140 &self.sprite_gpu_data,
2141 &self.mesh_instance_gpu_data,
2142 false
2143 );
2144 // Gaussian splats (alpha-blended, back-to-front sorted, no depth write).
2145 if !self.gaussian_splat_draw_data.is_empty() {
2146 if let Some(ref dual) = self.resources.gaussian_splat.pipeline {
2147 render_pass.set_pipeline(dual.for_format(false));
2148 render_pass.set_bind_group(0, camera_bg, &[]);
2149 for dd in &self.gaussian_splat_draw_data {
2150 if dd.wireframe {
2151 continue;
2152 }
2153 if let Some(set) = self
2154 .resources
2155 .content
2156 .gaussian_splat_store
2157 .get_by_index(dd.store_index)
2158 {
2159 if let Some(Some(vp_sort)) = set.viewport_sort.get(dd.viewport_index) {
2160 render_pass.set_bind_group(1, &vp_sort.render_bg, &[]);
2161 render_pass.draw(0..6, 0..dd.count);
2162 }
2163 }
2164 }
2165 }
2166 }
2167 // TransparentVolumeMesh boundary wireframe overlay.
2168 if !self.mesh_uniforms.tvm_wireframe_draws.is_empty() {
2169 if let Some(ref tvm_bg) = self.mesh_uniforms.tvm_wireframe_bg {
2170 render_pass.set_bind_group(0, camera_bg, &[]);
2171 for mesh_id in &self.mesh_uniforms.tvm_wireframe_draws {
2172 if let Some(mesh) = self.resources.mesh_store.get(*mesh_id) {
2173 render_pass.set_pipeline(&self.resources.wireframe_pipeline);
2174 render_pass.set_bind_group(2, &self.resources.deform.dummy_bind_group, &[]);
2175 render_pass.set_bind_group(1, tvm_bg, &[]);
2176 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2177 render_pass.set_index_buffer(
2178 mesh.edge_index_buffer.slice(..),
2179 wgpu::IndexFormat::Uint32,
2180 );
2181 render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
2182 }
2183 }
2184 }
2185 }
2186 // Shadow atlas viewer overlay.
2187 if frame.effects.show_shadow_atlas {
2188 render_pass.set_pipeline(&self.resources.shadow_atlas_viewer_pipeline);
2189 render_pass.set_bind_group(0, &self.resources.shadow_atlas_viewer_bg, &[]);
2190 render_pass.draw(0..6, 0..1);
2191 }
2192 }
2193
2194 /// Return a reference to the camera bind group for the given viewport slot.
2195 ///
2196 /// Falls back to `resources.camera_bind_group` if no per-viewport slot
2197 /// exists (e.g. in single-viewport mode before the first prepare call).
2198 fn viewport_camera_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
2199 self.viewport_slots
2200 .get(viewport_index)
2201 .map(|slot| &slot.camera_bind_group)
2202 .unwrap_or(&self.resources.camera_bind_group)
2203 }
2204
2205 /// Return a reference to the grid bind group for the given viewport slot.
2206 ///
2207 /// Falls back to `resources.grid_bind_group` if no per-viewport slot exists.
2208 fn viewport_grid_bind_group(&self, viewport_index: usize) -> &wgpu::BindGroup {
2209 self.viewport_slots
2210 .get(viewport_index)
2211 .map(|slot| &slot.grid_bind_group)
2212 .unwrap_or(&self.resources.grid_bind_group)
2213 }
2214
2215 /// Ensure the dyn-res intermediate render target exists for `vp_idx` at the
2216 /// given `scaled_size`, creating or recreating it when size changes.
2217 ///
2218 /// `surface_size` is the native output dimensions (used to size the upscale
2219 /// blit correctly). `ensure_dyn_res_pipeline` is called automatically.
2220 pub(crate) fn ensure_dyn_res_target(
2221 &mut self,
2222 device: &wgpu::Device,
2223 vp_idx: usize,
2224 scaled_size: [u32; 2],
2225 surface_size: [u32; 2],
2226 ) {
2227 self.resources.ensure_dyn_res_pipeline(device);
2228 let needs_create = match &self.viewport_slots[vp_idx].dyn_res {
2229 None => true,
2230 Some(dr) => dr.scaled_size != scaled_size || dr.surface_size != surface_size,
2231 };
2232 if needs_create {
2233 let target = self
2234 .resources
2235 .create_dyn_res_target(device, scaled_size, surface_size);
2236 self.viewport_slots[vp_idx].dyn_res = Some(target);
2237 }
2238 }
2239
2240 /// Ensure per-viewport HDR state exists for `viewport_index` at dimensions `w`x`h`.
2241 ///
2242 /// Calls `ensure_hdr_shared` once to initialise shared pipelines/BGLs/samplers, then
2243 /// lazily creates or resizes the `ViewportHdrState` inside the slot. Idempotent: if the
2244 /// slot already has HDR state at the correct size nothing is recreated.
2245 pub(crate) fn ensure_viewport_hdr(
2246 &mut self,
2247 device: &wgpu::Device,
2248 queue: &wgpu::Queue,
2249 viewport_index: usize,
2250 w: u32,
2251 h: u32,
2252 ssaa_factor: u32,
2253 render_scale: f32,
2254 ) {
2255 let format = self.resources.target_format;
2256 // Ensure shared infrastructure (pipelines, BGLs, samplers) exists.
2257 self.resources.ensure_hdr_shared(device, queue, format);
2258 // When render_scale < 1.0, the HDR upscale path needs the dyn_res
2259 // pipeline and sampler for the final upscale-blit to output resolution.
2260 if render_scale < 1.0 - 0.001 {
2261 self.resources.ensure_dyn_res_pipeline(device);
2262 }
2263 // Compute the scene-resolution render target size.
2264 let scale = render_scale.clamp(0.1, 1.0);
2265 let scene_w = ((w as f32) * scale).round() as u32;
2266 let scene_h = ((h as f32) * scale).round() as u32;
2267 // Ensure the slot exists.
2268 self.ensure_viewport_slot(device, viewport_index);
2269 let slot = &mut self.viewport_slots[viewport_index];
2270 // Create or resize the per-viewport HDR state.
2271 let needs_create = match &slot.hdr {
2272 None => true,
2273 Some(s) => {
2274 s.output_size != [w, h]
2275 || s.scene_size != [scene_w.max(1), scene_h.max(1)]
2276 || s.ssaa_factor != ssaa_factor
2277 }
2278 };
2279 if needs_create {
2280 slot.hdr = Some(self.resources.create_hdr_viewport_state(
2281 device,
2282 queue,
2283 format,
2284 w,
2285 h,
2286 scene_w.max(1),
2287 scene_h.max(1),
2288 ssaa_factor,
2289 ));
2290 }
2291 }
2292}