Skip to main content

dreamwell_engine/
material.rs

1//! Material descriptor types shared between engine and GPU crates.
2//!
3//! These are CPU-side descriptions — the GPU crate translates them
4//! into pipeline-specific bind group data.
5
6use serde::{Deserialize, Serialize};
7
8/// Alpha blending mode for materials.
9#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
10pub enum AlphaMode {
11    /// Fully opaque. No blending.
12    #[default]
13    Opaque,
14    /// Alpha-tested with cutoff threshold. Fragments below cutoff are discarded.
15    Mask { cutoff: f32 },
16    /// Alpha-blended. Requires sorted draw order.
17    Blend,
18}
19
20/// Classification of material pipeline variants.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub enum MaterialClass {
23    PbrLit,
24    PbrLitUntextured,
25    PbrEmissive,
26    PbrMasked,
27    PbrTransparent,
28    Unlit,
29    SpriteLit2D,
30    SpriteUnlit2D,
31}
32
33/// Ray tracing quality preset for SceneDreamMode::PbrRayTraced.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub enum RtQuality {
36    /// Full RT GI + RT shadows (highest quality, ~2x GPU cost).
37    Full,
38    /// RT shadows only (moderate cost, significant visual improvement).
39    ShadowsOnly,
40    /// RT GI only (high quality ambient, keep CSM shadows).
41    GiOnly,
42}
43
44impl Default for RtQuality {
45    fn default() -> Self {
46        Self::Full
47    }
48}
49
50/// Scene-wide rendering mode that controls which pipeline features are active.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub enum SceneDreamMode {
53    /// Full PBR with all texture slots, HDR post-processing, shadows.
54    PbrDefault,
55    /// PBR with reduced texture slots (no occlusion, no emissive). Lower VRAM.
56    PbrLightweight,
57    /// No lighting. Flat color / texture only. Fastest.
58    Unlit,
59    /// 2D sprites with normal-mapped PBR lighting.
60    SpriteLit2D,
61    /// 2D sprites, no lighting. Standard 2D game mode.
62    SpriteUnlit2D,
63    /// PBR with native ray-traced global illumination and/or shadows.
64    /// Requires GPU with EXPERIMENTAL_RAY_QUERY support. Falls back to PbrDefault without RT hardware.
65    PbrRayTraced(RtQuality),
66    /// User-provided shader pipeline. Engine provides frame lifecycle only.
67    Custom,
68}
69
70impl Default for SceneDreamMode {
71    fn default() -> Self {
72        Self::PbrDefault
73    }
74}
75
76/// HDR tonemapping operator applied during post-processing.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum TonemapOperator {
79    /// ACES fitted (BakingLab). Default. Best color fidelity.
80    AcesFilmic,
81    /// Uncharted 2 filmic (Hable). Warmer, cinematic look.
82    Uncharted2,
83    /// Reinhard. Simple x/(x+1). Useful for debugging.
84    Reinhard,
85    /// No tonemapping. Linear HDR passthrough.
86    None,
87}
88
89impl Default for TonemapOperator {
90    fn default() -> Self {
91        Self::AcesFilmic
92    }
93}
94
95/// PBR lit material descriptor.
96/// Texture references are optional `u32` handles resolved by the GPU store.
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub struct PbrMaterial {
99    /// Base color (RGBA linear).
100    pub base_color: [f32; 4],
101    /// Emissive color (RGB linear).
102    pub emissive: [f32; 3],
103    /// Roughness [0..1]. 0 = mirror, 1 = diffuse.
104    pub roughness: f32,
105    /// Metallic [0..1]. 0 = dielectric, 1 = metal.
106    pub metallic: f32,
107    /// Optional base color texture handle.
108    pub base_color_tex: Option<u32>,
109    /// Optional normal map texture handle.
110    pub normal_tex: Option<u32>,
111    /// Optional emissive texture handle.
112    pub emissive_tex: Option<u32>,
113    /// Optional roughness-metalness texture handle (R=metallic, G=roughness, glTF convention).
114    pub roughness_metalness_tex: Option<u32>,
115    /// Optional occlusion texture handle.
116    pub occlusion_tex: Option<u32>,
117    /// Occlusion strength [0..1]. 1.0 = full effect.
118    pub occlusion_strength: f32,
119    /// Normal map scale factor. 1.0 = unmodified normals.
120    pub normal_scale: f32,
121    /// Emissive strength multiplier. 1.0 = use emissive color as-is.
122    pub emissive_strength: f32,
123    /// Alpha blending mode.
124    pub alpha_mode: AlphaMode,
125    /// Render both front and back faces.
126    pub double_sided: bool,
127}
128
129/// Backward compatibility alias.
130pub type LitMaterialDesc = PbrMaterial;
131
132// ═══════════════════════════════════════════════════════════════════════
133// Dream Lighting — unified GI + reflection + shadow quality presets
134// ═══════════════════════════════════════════════════════════════════════
135
136/// Dream Lighting quality preset — controls the entire lighting pipeline.
137/// Maps to a fixed parameter matrix for all GI, shadow, reflection, and
138/// post-processing subsystems.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
140pub enum DreamLightingQuality {
141    /// Screen-space GI only, no RT. SSAO + TAA. Minimal VRAM.
142    Low,
143    /// Screen-space GI, CSM shadows, SSAO + SSR + TAA. Moderate VRAM.
144    Medium,
145    /// SHaRC RT GI + RT shadows, full screen-space stack, screen traces. ~150 MB.
146    High,
147    /// SHaRC RT GI (high density) + RT shadows, all effects, Hi-Z. ~250 MB.
148    Ultra,
149    /// Maximum quality: Dream TSR, 4-bounce GI, all effects at max settings.
150    Cinematic,
151}
152
153impl Default for DreamLightingQuality {
154    fn default() -> Self {
155        Self::Medium
156    }
157}
158
159impl DreamLightingQuality {
160    /// Auto-detect quality from GPU capabilities (spec §4.3).
161    pub fn auto_detect(ray_tracing: bool, max_storage_bytes: u64, max_tlas_instances: u32) -> Self {
162        if !ray_tracing {
163            if max_storage_bytes >= 64 * 1024 * 1024 {
164                return Self::Medium;
165            }
166            return Self::Low;
167        }
168        // RT-capable GPU — select based on TLAS instance capacity
169        if max_tlas_instances >= 16_000_000 {
170            Self::Ultra
171        } else {
172            Self::High
173        }
174    }
175}
176
177/// Full Dream Lighting configuration — all 21 parameters from the lighting spec.
178/// Created via `for_quality()` or manually tuned.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct DreamLightingConfig {
181    // ── RT features ──
182    pub rt_gi_enabled: bool,
183    pub rt_shadows_enabled: bool,
184    // ── Screen-space effects ──
185    pub ssgi_enabled: bool,
186    pub ssao_enabled: bool,
187    pub ssr_enabled: bool,
188    pub taa_enabled: bool,
189    pub screen_traces_enabled: bool,
190    // ── SHaRC radiance cache ──
191    pub sharc_hash_capacity: u32,
192    pub sharc_sparse_rate: f32,
193    pub sharc_max_bounces: u32,
194    pub sharc_grid_log_base: f32,
195    // ── Denoiser ──
196    pub denoiser_enabled: bool,
197    pub denoiser_temporal_alpha: f32,
198    // ── Occlusion & motion ──
199    pub hiz_enabled: bool,
200    pub motion_vectors_enabled: bool,
201    // ── Shadows ──
202    pub csm_cascades: u32,
203    // ── Reflections ──
204    pub reflection_max_roughness: f32,
205    // ── IBL ──
206    pub ibl_resolution: u32,
207    // ── Atmospheric ──
208    pub volumetric_fog_enabled: bool,
209    // ── DOF ──
210    pub dof_enabled: bool,
211    // ── TSR ──
212    pub dream_tsr_enabled: bool,
213}
214
215impl Default for DreamLightingConfig {
216    fn default() -> Self {
217        Self::for_quality(DreamLightingQuality::Medium)
218    }
219}
220
221impl DreamLightingConfig {
222    /// Create a config for a specific quality preset.
223    pub fn for_quality(quality: DreamLightingQuality) -> Self {
224        match quality {
225            // Spec §4.2: IBL ambient only. No RT, no screen traces, no SSGI.
226            DreamLightingQuality::Low => Self {
227                rt_gi_enabled: false,
228                rt_shadows_enabled: false,
229                ssgi_enabled: false,
230                ssao_enabled: false,
231                ssr_enabled: false,
232                taa_enabled: false,
233                screen_traces_enabled: false,
234                sharc_hash_capacity: 0,
235                sharc_sparse_rate: 0.0,
236                sharc_max_bounces: 0,
237                sharc_grid_log_base: 2.0,
238                denoiser_enabled: false,
239                denoiser_temporal_alpha: 0.0,
240                hiz_enabled: false,
241                motion_vectors_enabled: false,
242                csm_cascades: 2,
243                reflection_max_roughness: 0.0,
244                ibl_resolution: 32,
245                volumetric_fog_enabled: false,
246                dof_enabled: false,
247                dream_tsr_enabled: false,
248            },
249            // Spec §4.2: SSGI + SSAO + SSR + TAA. No RT.
250            DreamLightingQuality::Medium => Self {
251                rt_gi_enabled: false,
252                rt_shadows_enabled: false,
253                ssgi_enabled: true,
254                ssao_enabled: true,
255                ssr_enabled: true,
256                taa_enabled: true,
257                screen_traces_enabled: false,
258                sharc_hash_capacity: 0,
259                sharc_sparse_rate: 0.0,
260                sharc_max_bounces: 0,
261                sharc_grid_log_base: 2.0,
262                denoiser_enabled: false,
263                denoiser_temporal_alpha: 0.0,
264                hiz_enabled: false,
265                motion_vectors_enabled: true,
266                csm_cascades: 4,
267                reflection_max_roughness: 0.3,
268                ibl_resolution: 64,
269                volumetric_fog_enabled: false,
270                dof_enabled: false,
271                dream_tsr_enabled: false,
272            },
273            // Spec §4.2: Screen traces + SHaRC + RT shadows.
274            DreamLightingQuality::High => Self {
275                rt_gi_enabled: true,
276                rt_shadows_enabled: true,
277                ssgi_enabled: false,
278                ssao_enabled: true,
279                ssr_enabled: true,
280                taa_enabled: true,
281                screen_traces_enabled: true,
282                sharc_hash_capacity: 1 << 18, // 262K
283                sharc_sparse_rate: 0.02,
284                sharc_max_bounces: 2,
285                sharc_grid_log_base: 2.5,
286                denoiser_enabled: true,
287                denoiser_temporal_alpha: 0.80,
288                hiz_enabled: true,
289                motion_vectors_enabled: true,
290                csm_cascades: 0, // RT shadows replace CSM
291                reflection_max_roughness: 0.4,
292                ibl_resolution: 128,
293                volumetric_fog_enabled: false,
294                dof_enabled: false,
295                dream_tsr_enabled: false,
296            },
297            // Spec §4.2: Full Dream Lighting with multi-bounce + denoiser.
298            DreamLightingQuality::Ultra => Self {
299                rt_gi_enabled: true,
300                rt_shadows_enabled: true,
301                ssgi_enabled: false,
302                ssao_enabled: true,
303                ssr_enabled: true,
304                taa_enabled: true,
305                screen_traces_enabled: true,
306                sharc_hash_capacity: 1 << 20, // 1M
307                sharc_sparse_rate: 0.04,
308                sharc_max_bounces: 3,
309                sharc_grid_log_base: 2.0,
310                denoiser_enabled: true,
311                denoiser_temporal_alpha: 0.85,
312                hiz_enabled: true,
313                motion_vectors_enabled: true,
314                csm_cascades: 0, // RT shadows replace CSM
315                reflection_max_roughness: 0.5,
316                ibl_resolution: 128,
317                volumetric_fog_enabled: true,
318                dof_enabled: false,
319                dream_tsr_enabled: false,
320            },
321            // Spec §4.2: Maximum quality with Dream TSR + 5 bounces.
322            DreamLightingQuality::Cinematic => Self {
323                rt_gi_enabled: true,
324                rt_shadows_enabled: true,
325                ssgi_enabled: false,
326                ssao_enabled: true,
327                ssr_enabled: true,
328                taa_enabled: false, // replaced by Dream TSR
329                screen_traces_enabled: true,
330                sharc_hash_capacity: 1 << 22, // 4M
331                sharc_sparse_rate: 0.08,
332                sharc_max_bounces: 5,
333                sharc_grid_log_base: 1.5,
334                denoiser_enabled: true,
335                denoiser_temporal_alpha: 0.90,
336                hiz_enabled: true,
337                motion_vectors_enabled: true,
338                csm_cascades: 0, // RT shadows replace CSM
339                reflection_max_roughness: 0.6,
340                ibl_resolution: 256,
341                volumetric_fog_enabled: true,
342                dof_enabled: true,
343                dream_tsr_enabled: true,
344            },
345        }
346    }
347
348    /// The quality preset this config most closely matches (if any).
349    pub fn infer_quality(&self) -> DreamLightingQuality {
350        if self.dream_tsr_enabled {
351            DreamLightingQuality::Cinematic
352        } else if self.rt_gi_enabled && self.sharc_hash_capacity >= (1 << 20) {
353            DreamLightingQuality::Ultra
354        } else if self.rt_gi_enabled {
355            DreamLightingQuality::High
356        } else if self.ssgi_enabled {
357            DreamLightingQuality::Medium
358        } else {
359            DreamLightingQuality::Low
360        }
361    }
362}
363
364impl Default for PbrMaterial {
365    fn default() -> Self {
366        Self {
367            base_color: [1.0, 1.0, 1.0, 1.0],
368            emissive: [0.0, 0.0, 0.0],
369            roughness: 0.5,
370            metallic: 0.0,
371            base_color_tex: None,
372            normal_tex: None,
373            emissive_tex: None,
374            roughness_metalness_tex: None,
375            occlusion_tex: None,
376            occlusion_strength: 1.0,
377            normal_scale: 1.0,
378            emissive_strength: 1.0,
379            alpha_mode: AlphaMode::Opaque,
380            double_sided: false,
381        }
382    }
383}
384
385impl PbrMaterial {
386    pub fn painted_metal() -> Self {
387        Self {
388            roughness: 0.4,
389            metallic: 1.0,
390            base_color: [0.8, 0.1, 0.1, 1.0],
391            ..Default::default()
392        }
393    }
394
395    pub fn brushed_steel() -> Self {
396        Self {
397            roughness: 0.3,
398            metallic: 1.0,
399            base_color: [0.7, 0.7, 0.7, 1.0],
400            ..Default::default()
401        }
402    }
403
404    pub fn matte_plastic() -> Self {
405        Self {
406            roughness: 0.9,
407            metallic: 0.0,
408            base_color: [0.8, 0.8, 0.8, 1.0],
409            ..Default::default()
410        }
411    }
412
413    pub fn glossy_ceramic() -> Self {
414        Self {
415            roughness: 0.15,
416            metallic: 0.0,
417            base_color: [0.95, 0.95, 0.9, 1.0],
418            ..Default::default()
419        }
420    }
421
422    pub fn rough_wood() -> Self {
423        Self {
424            roughness: 0.85,
425            metallic: 0.0,
426            base_color: [0.55, 0.35, 0.2, 1.0],
427            ..Default::default()
428        }
429    }
430
431    pub fn polished_marble() -> Self {
432        Self {
433            roughness: 0.2,
434            metallic: 0.0,
435            base_color: [0.95, 0.93, 0.88, 1.0],
436            ..Default::default()
437        }
438    }
439
440    pub fn wet_stone() -> Self {
441        Self {
442            roughness: 0.3,
443            metallic: 0.0,
444            base_color: [0.4, 0.4, 0.4, 1.0],
445            ..Default::default()
446        }
447    }
448
449    pub fn gold() -> Self {
450        Self {
451            roughness: 0.25,
452            metallic: 1.0,
453            base_color: [1.0, 0.76, 0.33, 1.0],
454            ..Default::default()
455        }
456    }
457
458    pub fn copper() -> Self {
459        Self {
460            roughness: 0.3,
461            metallic: 1.0,
462            base_color: [0.95, 0.64, 0.54, 1.0],
463            ..Default::default()
464        }
465    }
466
467    pub fn rubber() -> Self {
468        Self {
469            roughness: 0.95,
470            metallic: 0.0,
471            base_color: [0.15, 0.15, 0.15, 1.0],
472            ..Default::default()
473        }
474    }
475
476    pub fn glass() -> Self {
477        Self {
478            roughness: 0.05,
479            metallic: 0.0,
480            alpha_mode: AlphaMode::Blend,
481            base_color: [1.0, 1.0, 1.0, 0.3],
482            ..Default::default()
483        }
484    }
485
486    pub fn fabric() -> Self {
487        Self {
488            roughness: 0.8,
489            metallic: 0.0,
490            base_color: [0.6, 0.5, 0.4, 1.0],
491            ..Default::default()
492        }
493    }
494
495    pub fn skin() -> Self {
496        Self {
497            roughness: 0.5,
498            metallic: 0.0,
499            base_color: [0.9, 0.7, 0.6, 1.0],
500            ..Default::default()
501        }
502    }
503
504    pub fn emissive_panel() -> Self {
505        Self {
506            emissive: [5.0, 5.0, 5.0],
507            emissive_strength: 2.0,
508            base_color: [0.1, 0.1, 0.1, 1.0],
509            ..Default::default()
510        }
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn alpha_mode_default_opaque() {
520        assert_eq!(AlphaMode::default(), AlphaMode::Opaque);
521    }
522
523    #[test]
524    fn pbr_material_default() {
525        let m = PbrMaterial::default();
526        assert_eq!(m.base_color, [1.0, 1.0, 1.0, 1.0]);
527        assert_eq!(m.roughness, 0.5);
528        assert_eq!(m.metallic, 0.0);
529        assert!(!m.double_sided);
530    }
531
532    #[test]
533    fn alpha_mask_cutoff() {
534        let mode = AlphaMode::Mask { cutoff: 0.5 };
535        if let AlphaMode::Mask { cutoff } = mode {
536            assert_eq!(cutoff, 0.5);
537        } else {
538            panic!("expected Mask");
539        }
540    }
541
542    #[test]
543    fn serde_roundtrip() {
544        let m = PbrMaterial {
545            roughness: 0.8,
546            metallic: 1.0,
547            alpha_mode: AlphaMode::Blend,
548            ..Default::default()
549        };
550        let json = serde_json::to_string(&m).unwrap();
551        let back: PbrMaterial = serde_json::from_str(&json).unwrap();
552        assert_eq!(m, back);
553    }
554
555    #[test]
556    fn pbr_material_new_fields_default() {
557        let m = PbrMaterial::default();
558        assert_eq!(m.roughness_metalness_tex, None);
559        assert_eq!(m.occlusion_tex, None);
560        assert_eq!(m.occlusion_strength, 1.0);
561        assert_eq!(m.normal_scale, 1.0);
562        assert_eq!(m.emissive_strength, 1.0);
563    }
564
565    #[test]
566    fn scene_dream_mode_default() {
567        assert_eq!(SceneDreamMode::default(), SceneDreamMode::PbrDefault);
568    }
569
570    #[test]
571    fn rt_quality_default() {
572        assert_eq!(RtQuality::default(), RtQuality::Full);
573    }
574
575    #[test]
576    fn scene_dream_mode_ray_traced() {
577        let mode = SceneDreamMode::PbrRayTraced(RtQuality::Full);
578        assert_ne!(mode, SceneDreamMode::PbrDefault);
579        let mode2 = SceneDreamMode::PbrRayTraced(RtQuality::ShadowsOnly);
580        assert_ne!(mode, mode2);
581    }
582
583    #[test]
584    fn serde_roundtrip_rt_quality() {
585        let q = RtQuality::GiOnly;
586        let json = serde_json::to_string(&q).unwrap();
587        let back: RtQuality = serde_json::from_str(&json).unwrap();
588        assert_eq!(q, back);
589    }
590
591    #[test]
592    fn serde_roundtrip_pbr_ray_traced() {
593        let mode = SceneDreamMode::PbrRayTraced(RtQuality::Full);
594        let json = serde_json::to_string(&mode).unwrap();
595        let back: SceneDreamMode = serde_json::from_str(&json).unwrap();
596        assert_eq!(mode, back);
597    }
598
599    #[test]
600    fn tonemap_operator_default() {
601        assert_eq!(TonemapOperator::default(), TonemapOperator::AcesFilmic);
602    }
603
604    #[test]
605    fn material_class_variants() {
606        let variants = [
607            MaterialClass::PbrLit,
608            MaterialClass::PbrLitUntextured,
609            MaterialClass::PbrEmissive,
610            MaterialClass::PbrMasked,
611            MaterialClass::PbrTransparent,
612            MaterialClass::Unlit,
613            MaterialClass::SpriteLit2D,
614            MaterialClass::SpriteUnlit2D,
615        ];
616        assert_eq!(variants.len(), 8);
617    }
618
619    #[test]
620    fn preset_painted_metal() {
621        let m = PbrMaterial::painted_metal();
622        assert_eq!(m.roughness, 0.4);
623        assert_eq!(m.metallic, 1.0);
624        assert_eq!(m.base_color, [0.8, 0.1, 0.1, 1.0]);
625    }
626
627    #[test]
628    fn preset_glass_is_blend() {
629        let m = PbrMaterial::glass();
630        assert_eq!(m.alpha_mode, AlphaMode::Blend);
631        assert_eq!(m.base_color[3], 0.3);
632    }
633
634    #[test]
635    fn serde_roundtrip_scene_dream_mode() {
636        let mode = SceneDreamMode::SpriteLit2D;
637        let json = serde_json::to_string(&mode).unwrap();
638        let back: SceneDreamMode = serde_json::from_str(&json).unwrap();
639        assert_eq!(mode, back);
640    }
641
642    #[test]
643    fn serde_roundtrip_tonemap() {
644        let op = TonemapOperator::Uncharted2;
645        let json = serde_json::to_string(&op).unwrap();
646        let back: TonemapOperator = serde_json::from_str(&json).unwrap();
647        assert_eq!(op, back);
648    }
649
650    #[test]
651    fn serde_roundtrip_material_class() {
652        let cls = MaterialClass::PbrEmissive;
653        let json = serde_json::to_string(&cls).unwrap();
654        let back: MaterialClass = serde_json::from_str(&json).unwrap();
655        assert_eq!(cls, back);
656    }
657
658    // ── Dream Lighting tests ──
659
660    #[test]
661    fn dream_lighting_quality_default() {
662        assert_eq!(DreamLightingQuality::default(), DreamLightingQuality::Medium);
663    }
664
665    #[test]
666    fn dream_lighting_config_default_matches_medium() {
667        let config = DreamLightingConfig::default();
668        let medium = DreamLightingConfig::for_quality(DreamLightingQuality::Medium);
669        assert_eq!(config.rt_gi_enabled, medium.rt_gi_enabled);
670        assert_eq!(config.ssao_enabled, medium.ssao_enabled);
671        assert_eq!(config.csm_cascades, medium.csm_cascades);
672        assert!(!config.hiz_enabled); // spec §4.2: Medium has no Hi-Z
673        assert_eq!(config.ibl_resolution, 64); // spec §4.2
674        assert!((config.reflection_max_roughness - 0.3).abs() < 0.01);
675    }
676
677    #[test]
678    fn quality_preset_low_no_rt() {
679        let config = DreamLightingConfig::for_quality(DreamLightingQuality::Low);
680        assert!(!config.rt_gi_enabled);
681        assert!(!config.rt_shadows_enabled);
682        assert!(!config.ssgi_enabled);
683        assert!(!config.ssao_enabled); // spec §4.2: Low has no SSAO
684        assert!(!config.taa_enabled); // spec §4.2: Low has no TAA
685        assert_eq!(config.ibl_resolution, 32);
686        assert_eq!(config.csm_cascades, 2);
687    }
688
689    #[test]
690    fn quality_preset_high_has_rt() {
691        let config = DreamLightingConfig::for_quality(DreamLightingQuality::High);
692        assert!(config.rt_gi_enabled);
693        assert!(config.rt_shadows_enabled);
694        assert!(config.screen_traces_enabled);
695        assert_eq!(config.sharc_hash_capacity, 1 << 18); // spec: 262K
696        assert_eq!(config.sharc_max_bounces, 2); // spec: 2
697        assert_eq!(config.csm_cascades, 0); // spec: RT replaces CSM
698        assert!((config.sharc_sparse_rate - 0.02).abs() < 0.001);
699        assert!((config.sharc_grid_log_base - 2.5).abs() < 0.01);
700        assert!((config.denoiser_temporal_alpha - 0.80).abs() < 0.01);
701        assert_eq!(config.ibl_resolution, 128);
702    }
703
704    #[test]
705    fn quality_preset_cinematic_has_tsr() {
706        let config = DreamLightingConfig::for_quality(DreamLightingQuality::Cinematic);
707        assert!(config.dream_tsr_enabled);
708        assert!(!config.taa_enabled); // TSR replaces TAA
709        assert_eq!(config.sharc_max_bounces, 5); // spec: 5
710        assert_eq!(config.sharc_hash_capacity, 1 << 22);
711        assert_eq!(config.csm_cascades, 0); // spec: RT replaces CSM
712        assert_eq!(config.ibl_resolution, 256);
713        assert!((config.sharc_grid_log_base - 1.5).abs() < 0.01);
714        assert!((config.denoiser_temporal_alpha - 0.90).abs() < 0.01);
715        assert!((config.reflection_max_roughness - 0.6).abs() < 0.01);
716    }
717
718    #[test]
719    fn auto_detect_no_rt_64mb_returns_medium() {
720        let q = DreamLightingQuality::auto_detect(false, 64 * 1024 * 1024, 0);
721        assert_eq!(q, DreamLightingQuality::Medium);
722    }
723
724    #[test]
725    fn auto_detect_no_rt_low_storage_returns_low() {
726        let q = DreamLightingQuality::auto_detect(false, 32 * 1024 * 1024, 0);
727        assert_eq!(q, DreamLightingQuality::Low);
728    }
729
730    #[test]
731    fn auto_detect_rt_16m_tlas_returns_ultra() {
732        let q = DreamLightingQuality::auto_detect(true, 512 * 1024 * 1024, 16_000_000);
733        assert_eq!(q, DreamLightingQuality::Ultra);
734    }
735
736    #[test]
737    fn auto_detect_rt_below_16m_returns_high() {
738        let q = DreamLightingQuality::auto_detect(true, 256 * 1024 * 1024, 100_000);
739        assert_eq!(q, DreamLightingQuality::High);
740    }
741
742    #[test]
743    fn infer_quality_roundtrip() {
744        for quality in [
745            DreamLightingQuality::Low,
746            DreamLightingQuality::Medium,
747            DreamLightingQuality::High,
748            DreamLightingQuality::Ultra,
749            DreamLightingQuality::Cinematic,
750        ] {
751            let config = DreamLightingConfig::for_quality(quality);
752            assert_eq!(
753                config.infer_quality(),
754                quality,
755                "infer_quality failed for {:?}",
756                quality
757            );
758        }
759    }
760
761    #[test]
762    fn serde_roundtrip_dream_lighting_quality() {
763        let q = DreamLightingQuality::Cinematic;
764        let json = serde_json::to_string(&q).unwrap();
765        let back: DreamLightingQuality = serde_json::from_str(&json).unwrap();
766        assert_eq!(q, back);
767    }
768
769    #[test]
770    fn serde_roundtrip_dream_lighting_config() {
771        let config = DreamLightingConfig::for_quality(DreamLightingQuality::Ultra);
772        let json = serde_json::to_string(&config).unwrap();
773        let back: DreamLightingConfig = serde_json::from_str(&json).unwrap();
774        assert_eq!(config.rt_gi_enabled, back.rt_gi_enabled);
775        assert_eq!(config.sharc_hash_capacity, back.sharc_hash_capacity);
776        assert_eq!(config.dream_tsr_enabled, back.dream_tsr_enabled);
777    }
778}