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