concinnity_core/render/backend_init.rs
1//! Grouped construction inputs for the render backends, plus the requirements
2//! derivation that trims scene-scoped features when a world has no 3D content.
3//! GraphicsSystem init assembles a `BackendInit` from the drained world assets,
4//! calls `resolve_requirements()`, and hands it to the backend constructor
5//! selected at compile time (Metal / DirectX / Vulkan). Every backend receives
6//! the same struct; each reads the fields its feature set consumes.
7
8use crate::components::{
9 GlassPanel, SdfVolume, ShaderPrograms, ShadowUpdate, UpscalerBackend, WaterSurface, Window,
10};
11use crate::gfx::auto_exposure::AutoExposureSettings;
12use crate::gfx::mesh_payload::Vertex;
13use crate::gfx::render_types::{
14 AreaLightData, DrawObject, GpuLight, InstancedCluster, LightUniforms, PostProcessTunables,
15 SpotShadowData,
16};
17use crate::gfx::rt_reflections::RtReflectionSettings;
18use crate::gfx::ssao::SsaoSettings;
19use crate::gfx::ssgi::SsgiSettings;
20use crate::gfx::ssr::SsrSettings;
21use crate::render::decal::DecalRecord;
22use crate::render::particles::ParticleEmitterRecord;
23use crate::render::rt_geom::RtDynamicMode;
24use crate::render::volumetric_fog::FogSettings;
25use alloc::string::String;
26use alloc::vec;
27use alloc::vec::Vec;
28
29/// Static scene geometry and the draw lists built over it.
30pub struct SceneData<'a> {
31 /// The world's shared static vertex buffer.
32 pub vertices: &'a [Vertex],
33 /// The world's shared static index buffer.
34 pub indices: &'a [u32],
35 /// One draw record per static placement.
36 pub draw_objects: Vec<DrawObject>,
37 /// One record per instanced-prop cluster.
38 pub instanced_clusters: Vec<InstancedCluster>,
39 /// Skinned draw-object count (the world's `SkinnedMesh` count). Sizes each
40 /// backend's shared GPU-cull buffers for the merged total (static +
41 /// instances + skinned) at init; the skinned geometry itself is uploaded
42 /// later via `upload_skinned`.
43 pub n_skinned: usize,
44 /// Worst-case resident chunk count for a streaming VoxelWorld (0
45 /// otherwise). Reserves a chunk record region in the shared GPU-cull
46 /// buffers at init; resident chunks fold into the indirect path each
47 /// frame. Honoured by DirectX + Vulkan; Metal's per-frame rebuild already
48 /// covers chunks, so it needs no reserve.
49 pub n_chunk_max: usize,
50}
51
52/// One world Shader as the backend receives it: the cook's compiled programs,
53/// which the backend resolves per entry against the source it assembles (see
54/// each backend's surface-source lookup), or nothing for the engine's own
55/// program.
56#[derive(Clone, Copy)]
57pub struct WorldShader<'a> {
58 /// The decoded payload; `None` for the engine's own main-pass program,
59 /// which every backend compiles from its embedded source.
60 pub programs: Option<&'a ShaderPrograms>,
61 /// This entry's payload was not decoded because a scene other than the start
62 /// scene owns it: the backend leaves the bucket's pipeline unbuilt and the
63 /// streaming pump installs it when that scene pins.
64 pub deferred: bool,
65}
66
67/// Decoded image payloads: texture pools, glyph atlases, and the serialised
68/// IBL / grading payloads (None = the backend binds identity fallbacks).
69pub struct MediaPayloads<'a> {
70 /// Decoded textures for the shared handle-indexed pool: one `TextureImage`
71 /// per slot carrying its GPU format and mip chain. Every texture -- albedo,
72 /// normal map, emissive/ORM, terrain secondary -- lives here once at its
73 /// handle; the backend appends a flat-normal fallback past the last entry for
74 /// normal-less draws. RGBA8 images regenerate mips on upload; block-
75 /// compressed images upload their chain verbatim.
76 pub textures: &'a [crate::bake::texture::TextureImage],
77 /// Glyph atlas textures for text rendering; empty = no text support.
78 pub text_atlases: Vec<(u32, u32, Vec<u8>)>,
79 /// Serialised EnvironmentMap payload (irradiance + prefilter cubemaps).
80 /// None disables IBL; the runtime binds 1x1 grey fallback cubes.
81 pub env_map_bytes: Option<&'a [u8]>,
82 /// Serialised ColorLut payload (3D grading LUT). None = identity LUT.
83 pub color_lut_bytes: Option<&'a [u8]>,
84}
85
86/// Shadow-mapping knobs from GraphicsConfig. `map_size == 0` disables the
87/// shadow pipeline and cascade array entirely.
88#[derive(Copy, Clone, Debug)]
89pub struct ShadowParams {
90 /// Shadow map edge in texels; 0 disables shadows entirely.
91 pub map_size: u32,
92 /// Cascade re-render policy: hybrid amortizes far cascades across frames.
93 pub update: ShadowUpdate,
94 /// Shadow distance in world units, capped at the camera far plane by the
95 /// per-frame cascade split.
96 pub distance: u32,
97 /// Cascade count (1..=4) the per-frame split + schedule render.
98 pub cascades: u32,
99}
100
101/// Post-process and display settings resolved from PostProcessConfig (plus
102/// the user's persisted overrides, the quality-preset ceiling, and the
103/// launch's render requests). Every Option here is an init-time gate: None
104/// allocates nothing.
105pub struct PostSettings {
106 /// Composite tunables pushed to the post pass. The backend pairs them with
107 /// the display-output flags it negotiates below (`hdr_display` / `hdr_pq`)
108 /// to build the uniform the shaders read.
109 pub post_process: PostProcessTunables,
110 /// Whether the temporal anti-aliasing pass runs.
111 pub taa_enabled: bool,
112 /// Screen-space ambient occlusion, or `None` when off.
113 pub ssao: Option<SsaoSettings>,
114 /// Screen-space reflections, or `None` when off.
115 pub ssr: Option<SsrSettings>,
116 /// Screen-space global illumination, or `None` when off.
117 pub ssgi: Option<SsgiSettings>,
118 /// Requires an RT-capable GPU; backends fall back to SSR without one.
119 pub rt_reflections: Option<RtReflectionSettings>,
120 /// How the ray-tracing acceleration structure tracks moving props. Inert
121 /// when `rt_reflections` is None.
122 pub rt_dynamic: RtDynamicMode,
123 /// Whether skinned meshes join the ray-tracing acceleration structure.
124 /// False leaves the BVH over static + instanced geometry only, so nothing
125 /// animated appears in a ray-traced reflection.
126 pub rt_skinned_geometry: bool,
127 /// Per-axis divisor for the roughness-aware reflection blur target.
128 pub reflection_blur_scale: u32,
129 /// Auto-exposure, or `None` when off.
130 pub auto_exposure: Option<AutoExposureSettings>,
131 /// Authored exposure_ev carried as a bias on the adapted EV when
132 /// auto-exposure is on; otherwise baked into post_process.exposure.
133 pub auto_exposure_bias_ev: f32,
134 /// HDR display request; each backend gates it on its own EDR / colour-
135 /// space capability probe and falls back to SDR with a warning.
136 pub hdr_display: bool,
137 /// PQ-encoded HDR output; honoured by Metal today, accepted elsewhere.
138 pub hdr_pq: bool,
139 /// Whether temporal upscaling runs.
140 pub temporal_upscaling: bool,
141 /// Per-axis input-to-output ratio; ignored when upscaling is off.
142 pub upscale_scale: f32,
143 /// Upscaler selector for DirectX / Vulkan (FSR3 / DLSS / XeSS); Metal
144 /// always uses MetalFX and ignores it.
145 pub upscale_backend: UpscalerBackend,
146 /// Two-pass Hi-Z occlusion request; gated on the bindless cull path.
147 pub occlusion_two_pass: bool,
148}
149
150/// World-authored effect content drained from components. Empty / None means
151/// the backend builds no pipelines or pools for that feature.
152pub struct WorldFx {
153 /// Projected decals declared by the world.
154 pub decals: Vec<DecalRecord>,
155 /// Particle emitters declared by the world.
156 pub particles: Vec<ParticleEmitterRecord>,
157 /// Volumetric fog settings, or `None` when the world declares none.
158 pub fog: Option<FogSettings>,
159 /// Transparent water surfaces; rendered by Metal today, accepted by the
160 /// other backends for parity until their water ports land.
161 pub water_surfaces: Vec<WaterSurface>,
162 /// Refractive glass panels declared by the world.
163 pub glass_panels: Vec<GlassPanel>,
164 /// Raymarched SDF volumes as (volume, compiled fragment source bytes,
165 /// asset label for error messages).
166 pub sdf_volumes: Vec<(SdfVolume, Vec<u8>, String)>,
167}
168
169/// Everything a backend constructor needs, assembled once by GraphicsSystem
170/// init after the world's assets have been drained and settings resolved.
171pub struct BackendInit<'a> {
172 /// The window the backend opens.
173 pub window: &'a Window,
174 /// Debug-layer toggle for the DirectX / Vulkan validation layers.
175 pub validation: bool,
176 /// Frames the backend keeps in flight.
177 pub frames_in_flight: usize,
178 /// Whether presentation waits for vertical blank.
179 pub vsync: bool,
180 /// Linear RGBA the target is cleared to.
181 pub clear_color: [f32; 4],
182 /// True only under `cn debug`: disk-first shader resolution + watcher.
183 pub hot_reload: bool,
184 /// Keep the presented frame blit-readable so `screenshot` can capture it.
185 /// On under the dev loop, and armed by `cn run --screenshot`; production
186 /// otherwise pays nothing for it (Metal leaves the drawable
187 /// framebuffer-only and retains nothing).
188 pub capture: bool,
189 /// The world's static geometry and draw lists.
190 pub scene: SceneData<'a>,
191 /// One entry per world Shader, indexed by the dense ShaderHandle value a
192 /// DrawObject's `shader_bucket` carries; entry 0 is the world default
193 /// program. Never empty for a rendering world.
194 pub shaders: Vec<WorldShader<'a>>,
195 /// Compiled media payloads (textures, fonts, environment maps).
196 pub media: MediaPayloads<'a>,
197 /// The fixed directional / point light arrays.
198 pub light_uniforms: LightUniforms,
199 /// Every local light (point + spot + area) for the clustered forward pass,
200 /// uploaded to a per-scene GpuLight storage buffer. The first MAX_POINT_LIGHTS
201 /// point lights are also mirrored into `light_uniforms.point` for the
202 /// raymarch / fog / probe paths that still read the fixed array.
203 pub local_lights: Vec<GpuLight>,
204 /// One entry per spot shadow map slice, indexed by `GpuLight.shadow_index`.
205 /// Empty when no spot light casts shadows, in which case the backend skips
206 /// allocating the shadow array entirely.
207 pub spot_shadows: Vec<SpotShadowData>,
208 /// One entry per rectangular area light, indexed by `GpuLight.data_index`.
209 /// Empty when the world declares none.
210 pub area_lights: Vec<AreaLightData>,
211 /// Shadow-mapping settings.
212 pub shadows: ShadowParams,
213 /// Scene-sampler max anisotropy, clamped to the GPU's range at init.
214 pub anisotropy: u32,
215 /// Distinct planar-reflection plane budget from the quality preset / GPU
216 /// tier ceiling; reflectors past it fall back to the probe cube.
217 pub planar_planes: usize,
218 /// Post-process and display settings.
219 pub post: PostSettings,
220 /// World-authored effect content.
221 pub fx: WorldFx,
222 /// Derived by `resolve_requirements()`; the conservative default assumes a
223 /// full scene so a caller that skips resolution never under-allocates.
224 pub requirements: RenderRequirements,
225}
226
227/// The swapchain-level configuration a backend bakes into its window / surface
228/// at construction: the ring depth and the HDR-output request that together fix
229/// the drawable pixel format and frames-in-flight sizing. A live world swap
230/// (`RenderBackend::reload_world`) can only reuse the existing window when these
231/// are unchanged; a difference forces a full backend rebuild (a new window).
232/// Kept small + `Eq` so the swap decision is one comparison.
233#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
234pub struct SwapchainConfig {
235 /// Frames the backend keeps in flight.
236 pub frames_in_flight: usize,
237 /// Whether the swapchain requests an HDR pixel format.
238 pub hdr_display: bool,
239 /// Whether HDR output is PQ-encoded.
240 pub hdr_pq: bool,
241}
242
243/// What the world's content requires of the renderer. Derived from the
244/// assembled scene + fx data, backend-agnostic, so all three backends make
245/// identical trimming decisions.
246#[derive(Copy, Clone, Debug)]
247pub struct RenderRequirements {
248 /// True when any 3D scene content exists (meshes, instances, skinned
249 /// meshes, streamed chunks, water, glass, SDF volumes, particles, or
250 /// decals). False = the world renders UI / text only: the backend skips
251 /// the scene pipelines and the frame collapses to a clear + composite.
252 pub scene: bool,
253}
254
255impl Default for RenderRequirements {
256 fn default() -> Self {
257 RenderRequirements { scene: true }
258 }
259}
260
261impl RenderRequirements {
262 /// Derive the requirements a scene plus its effect content imposes.
263 pub fn derive(scene: &SceneData, fx: &WorldFx) -> Self {
264 let scene_present = !scene.vertices.is_empty()
265 || !scene.draw_objects.is_empty()
266 || !scene.instanced_clusters.is_empty()
267 || scene.n_skinned > 0
268 || scene.n_chunk_max > 0
269 || !fx.water_surfaces.is_empty()
270 || !fx.glass_panels.is_empty()
271 || !fx.sdf_volumes.is_empty()
272 || !fx.particles.is_empty()
273 || !fx.decals.is_empty();
274 RenderRequirements {
275 scene: scene_present,
276 }
277 }
278}
279
280impl<'a> BackendInit<'a> {
281 /// A backend carrying nothing but a window and glyph atlases: no geometry,
282 /// no textures, no lights, no effects. The single shader entry has empty
283 /// stage bytes, which every backend resolves to its built-in default
284 /// program. `resolve_requirements` then trims every scene-scoped feature.
285 ///
286 /// This is the startup error screen's path, which has to stand up a window
287 /// with no compiled world data at all. Keeping it here means the field
288 /// defaulting is maintained beside the struct it fills.
289 pub fn minimal(window: &'a Window, text_atlases: Vec<(u32, u32, Vec<u8>)>) -> Self {
290 let mut init = Self {
291 window,
292 validation: false,
293 frames_in_flight: 2,
294 vsync: true,
295 clear_color: [0.0, 0.0, 0.0, 1.0],
296 hot_reload: false,
297 capture: false,
298 scene: SceneData {
299 vertices: &[],
300 indices: &[],
301 draw_objects: Vec::new(),
302 instanced_clusters: Vec::new(),
303 n_skinned: 0,
304 n_chunk_max: 0,
305 },
306 shaders: vec![WorldShader {
307 programs: None,
308 deferred: false,
309 }],
310 media: MediaPayloads {
311 textures: &[],
312 text_atlases,
313 env_map_bytes: None,
314 color_lut_bytes: None,
315 },
316 light_uniforms: LightUniforms::DEFAULT,
317 local_lights: Vec::new(),
318 spot_shadows: Vec::new(),
319 area_lights: Vec::new(),
320 shadows: ShadowParams {
321 map_size: 0,
322 update: ShadowUpdate::default(),
323 distance: 0,
324 cascades: 1,
325 },
326 anisotropy: 1,
327 planar_planes: 0,
328 post: PostSettings {
329 post_process: PostProcessTunables::DEFAULT,
330 taa_enabled: false,
331 ssao: None,
332 ssr: None,
333 ssgi: None,
334 rt_reflections: None,
335 rt_dynamic: RtDynamicMode::Auto,
336 rt_skinned_geometry: true,
337 reflection_blur_scale: 1,
338 auto_exposure: None,
339 auto_exposure_bias_ev: 0.0,
340 hdr_display: false,
341 hdr_pq: false,
342 temporal_upscaling: false,
343 upscale_scale: 1.0,
344 upscale_backend: UpscalerBackend::Auto,
345 occlusion_two_pass: false,
346 },
347 fx: WorldFx {
348 decals: Vec::new(),
349 particles: Vec::new(),
350 fog: None,
351 water_surfaces: Vec::new(),
352 glass_panels: Vec::new(),
353 sdf_volumes: Vec::new(),
354 },
355 requirements: Default::default(),
356 };
357 init.resolve_requirements();
358 init
359 }
360
361 /// The swapchain-level configuration this world needs. Compared against a
362 /// transplanted backend's `RenderBackend::hot_swap_config` to decide whether
363 /// a live SAVE can reuse the existing window (`reload_world`) or must rebuild.
364 pub fn swapchain_config(&self) -> SwapchainConfig {
365 SwapchainConfig {
366 // Normalise to at least 1 to match how the backends size their ring
367 // buffers (e.g. Metal stores `frames_in_flight.max(1)`), so an
368 // out-of-range authored 0 does not read as a swapchain change vs a
369 // backend that already clamped it, spuriously forcing a full rebuild.
370 frames_in_flight: self.frames_in_flight.max(1),
371 hdr_display: self.post.hdr_display,
372 hdr_pq: self.post.hdr_pq,
373 }
374 }
375
376 /// Derive the requirements from the assembled content and trim
377 /// scene-scoped features accordingly. Runtime spawning can only clone
378 /// assets already declared in the world, so the derivation here is
379 /// complete: a world with no scene content at init can never grow one.
380 pub fn resolve_requirements(&mut self) {
381 let req = RenderRequirements::derive(&self.scene, &self.fx);
382 if !req.scene {
383 trim_scene_features(
384 &mut self.shadows,
385 &mut self.post,
386 &mut self.fx,
387 &mut self.planar_planes,
388 );
389 }
390 self.requirements = req;
391 }
392}
393
394// Force off every feature that only decorates a 3D scene. All of these are
395// existing init-time gates in the backends, so zeroing them here means every
396// backend skips the matching resources with no backend-side changes.
397fn trim_scene_features(
398 shadows: &mut ShadowParams,
399 post: &mut PostSettings,
400 fx: &mut WorldFx,
401 planar_planes: &mut usize,
402) {
403 shadows.map_size = 0;
404 post.taa_enabled = false;
405 post.ssao = None;
406 post.ssr = None;
407 post.ssgi = None;
408 post.rt_reflections = None;
409 post.auto_exposure = None;
410 post.temporal_upscaling = false;
411 post.occlusion_two_pass = false;
412 fx.fog = None;
413 *planar_planes = 0;
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 fn empty_scene() -> SceneData<'static> {
421 SceneData {
422 vertices: &[],
423 indices: &[],
424 draw_objects: Vec::new(),
425 instanced_clusters: Vec::new(),
426 n_skinned: 0,
427 n_chunk_max: 0,
428 }
429 }
430
431 fn empty_fx() -> WorldFx {
432 WorldFx {
433 decals: Vec::new(),
434 particles: Vec::new(),
435 fog: None,
436 water_surfaces: Vec::new(),
437 glass_panels: Vec::new(),
438 sdf_volumes: Vec::new(),
439 }
440 }
441
442 fn full_post() -> PostSettings {
443 PostSettings {
444 post_process: PostProcessTunables::DEFAULT,
445 taa_enabled: true,
446 ssao: Some(SsaoSettings::resolve(0.5, 1.0)),
447 ssr: None,
448 ssgi: None,
449 rt_reflections: None,
450 rt_dynamic: RtDynamicMode::Auto,
451 rt_skinned_geometry: true,
452 reflection_blur_scale: 2,
453 auto_exposure: None,
454 auto_exposure_bias_ev: 0.0,
455 hdr_display: false,
456 hdr_pq: false,
457 temporal_upscaling: true,
458 upscale_scale: 0.5,
459 upscale_backend: UpscalerBackend::Auto,
460 occlusion_two_pass: true,
461 }
462 }
463
464 #[test]
465 fn text_only_world_derives_no_scene() {
466 let req = RenderRequirements::derive(&empty_scene(), &empty_fx());
467 assert!(!req.scene);
468 }
469
470 #[test]
471 fn minimal_carries_only_a_window_and_its_atlases() {
472 let window = Window::default();
473 let atlas = vec![(2u32, 2u32, vec![255u8; 2 * 2 * 4])];
474 let init = BackendInit::minimal(&window, atlas);
475
476 // The text pipeline is the one thing it keeps: backends gate that pass
477 // on a non-empty atlas list.
478 assert_eq!(init.media.text_atlases.len(), 1);
479 // One shader entry carrying no payload, so every backend resolves it
480 // to its built-in default program rather than leaving bucket 0 unbuilt.
481 assert_eq!(init.shaders.len(), 1);
482 assert!(init.shaders[0].programs.is_none());
483 assert!(!init.shaders[0].deferred);
484 // No scene content, so `resolve_requirements` ran and trimmed the
485 // scene-scoped features.
486 assert!(!init.requirements.scene);
487 assert_eq!(init.shadows.map_size, 0);
488 assert!(!init.post.taa_enabled);
489 assert!(init.post.ssao.is_none());
490 assert_eq!(init.planar_planes, 0);
491 }
492
493 #[test]
494 fn any_scene_content_derives_scene() {
495 let mut scene = empty_scene();
496 scene.n_skinned = 1;
497 assert!(RenderRequirements::derive(&scene, &empty_fx()).scene);
498
499 let mut scene = empty_scene();
500 scene.n_chunk_max = 8;
501 assert!(RenderRequirements::derive(&scene, &empty_fx()).scene);
502
503 // FX content alone is scene content too (a water-only world still
504 // renders into the HDR scene chain).
505 let scene = empty_scene();
506 let mut fx = empty_fx();
507 fx.water_surfaces.push(WaterSurface::default());
508 assert!(RenderRequirements::derive(&scene, &fx).scene);
509 }
510
511 #[test]
512 fn sceneless_world_trims_scene_features() {
513 let mut shadows = ShadowParams {
514 map_size: 2048,
515 update: ShadowUpdate::default(),
516 distance: 120,
517 cascades: 4,
518 };
519 let mut post = full_post();
520 let mut fx = empty_fx();
521 let mut planar = 3usize;
522 trim_scene_features(&mut shadows, &mut post, &mut fx, &mut planar);
523 assert_eq!(shadows.map_size, 0);
524 assert!(!post.taa_enabled);
525 assert!(post.ssao.is_none());
526 assert!(!post.temporal_upscaling);
527 assert!(!post.occlusion_two_pass);
528 assert!(fx.fog.is_none());
529 assert_eq!(planar, 0);
530 }
531
532 #[test]
533 fn scene_world_keeps_settings() {
534 // A world with content must pass its resolved settings through
535 // untouched: derivation flags the scene, and nothing is trimmed.
536 let mut scene = empty_scene();
537 scene.n_skinned = 2;
538 let fx = empty_fx();
539 let req = RenderRequirements::derive(&scene, &fx);
540 assert!(req.scene);
541 }
542}