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