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