Skip to main content

dreamwell_engine/
dream_file.rs

1// Dreamwell .dream File Format v1.0.0
2//
3// Binary scene container for the Dreamwell Simulation Development Kit.
4// Equivalent to Unity .unity or Unreal .umap — packages everything needed
5// to load a PBR scene into any Dreamwell client: editor, runtime, or benchmark.
6//
7// Format: 32-byte header + MessagePack-encoded payload + FNV-1a attestation.
8// Assets are referenced by path (not embedded), resolved from project assets/ at load time.
9//
10// Clean Compute: zero per-frame allocation. Scenes are loaded once, GPU buffers
11// pre-allocated from scene manifest, no hot-path deserialization.
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::io::Write;
16use std::path::Path;
17
18use crate::hash::fnv1a_64;
19use crate::waymark::schema::{GridConfig, SimulationConfig, SpatialConfig};
20
21// =============================================================================
22// §0  CONSTANTS
23// =============================================================================
24
25/// Magic bytes: "DREAMWL\0" (8 bytes)
26pub const DREAM_MAGIC: &[u8; 8] = b"DREAMWL\0";
27
28/// File format version (u32). Increment on breaking schema changes.
29pub const DREAM_VERSION: u32 = 1;
30
31/// Scene schema version string.
32pub const SCENE_SCHEMA_VERSION: &str = "dreamwell_scene_v1.0.0";
33
34/// Tapestry schema version string.
35pub const TAPESTRY_SCHEMA_VERSION: &str = "dreamwell_tapestry_v1.0.0";
36
37// Flag bits
38pub const FLAG_COMPRESSED: u32 = 1 << 0;
39pub const FLAG_SIGNED: u32 = 1 << 1;
40
41// =============================================================================
42// §1  DREAM SCENE — scene.dream payload
43// =============================================================================
44
45/// Complete scene definition serialized into a .dream binary.
46/// This is the compiled output of scene.json + zone/chunk/poi/avatar manifests.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DreamSceneV1 {
49    // ── Identity ──
50    pub scene_id: String,
51    pub name: String,
52    #[serde(default)]
53    pub description: String,
54    #[serde(default = "default_scene_schema")]
55    pub schema_version: String,
56
57    // ── Topology ──
58    #[serde(default = "default_topology_layer")]
59    pub topology_layer: u8,
60    #[serde(default)]
61    pub starting_zone_id: String,
62    #[serde(default)]
63    pub world_id: Option<String>,
64
65    // ── Camera ──
66    #[serde(default = "default_camera")]
67    pub default_camera: String,
68    #[serde(default)]
69    pub allowed_cameras: Vec<String>,
70
71    // ── Scene graph ──
72    #[serde(default)]
73    pub objects: Vec<SceneObject>,
74
75    // ── Lights ──
76    #[serde(default)]
77    pub directional_lights: Vec<DirectionalLightDef>,
78    #[serde(default)]
79    pub point_lights: Vec<PointLightDef>,
80    #[serde(default)]
81    pub spot_lights: Vec<SpotLightDef>,
82
83    // ── Physics ──
84    #[serde(default)]
85    pub colliders: Vec<ColliderDef>,
86    #[serde(default)]
87    pub physics_defaults: PhysicsDefaults,
88
89    // ── POIs ──
90    #[serde(default)]
91    pub pois: Vec<PoiDef>,
92
93    // ── Asset references (paths relative to project root) ──
94    #[serde(default)]
95    pub asset_refs: Vec<AssetRef>,
96
97    // ── GPU pipeline hints ──
98    #[serde(default = "default_render_path")]
99    pub render_path: String,
100    #[serde(default)]
101    pub quality_preset: Option<String>,
102    #[serde(default)]
103    pub feature_flags: FeatureFlags,
104
105    // ── Waymark pack (optional, for server seeding) ──
106    #[serde(default)]
107    pub waymark_pack: Option<serde_json::Value>,
108
109    // ── Spatial config ──
110    #[serde(default)]
111    pub grid: GridConfig,
112    #[serde(default)]
113    pub spatial: SpatialConfig,
114    #[serde(default)]
115    pub simulation: SimulationConfig,
116}
117
118fn default_scene_schema() -> String {
119    SCENE_SCHEMA_VERSION.to_string()
120}
121fn default_topology_layer() -> u8 {
122    6 // Area
123}
124fn default_camera() -> String {
125    "ThirdPerson".to_string()
126}
127fn default_render_path() -> String {
128    "Dreamwell".to_string()
129}
130
131impl Default for DreamSceneV1 {
132    fn default() -> Self {
133        Self {
134            scene_id: String::new(),
135            name: String::new(),
136            description: String::new(),
137            schema_version: default_scene_schema(),
138            topology_layer: default_topology_layer(),
139            starting_zone_id: String::new(),
140            world_id: None,
141            default_camera: default_camera(),
142            allowed_cameras: Vec::new(),
143            objects: Vec::new(),
144            directional_lights: Vec::new(),
145            point_lights: Vec::new(),
146            spot_lights: Vec::new(),
147            colliders: Vec::new(),
148            physics_defaults: PhysicsDefaults::default(),
149            pois: Vec::new(),
150            asset_refs: Vec::new(),
151            render_path: default_render_path(),
152            quality_preset: None,
153            feature_flags: FeatureFlags::default(),
154            waymark_pack: None,
155            grid: GridConfig::default(),
156            spatial: SpatialConfig::default(),
157            simulation: SimulationConfig::default(),
158        }
159    }
160}
161
162// =============================================================================
163// §2  SCENE OBJECT — authored entity in the scene graph
164// =============================================================================
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct SceneObject {
168    pub id: String,
169    #[serde(default)]
170    pub name: String,
171    /// "Mesh" | "Light" | "Camera" | "Particle" | "Trigger" | "Meshlet" | "GltfMesh"
172    #[serde(default = "default_kind")]
173    pub kind: String,
174    #[serde(default)]
175    pub position: [f32; 3],
176    /// Euler rotation in degrees.
177    #[serde(default)]
178    pub rotation: [f32; 3],
179    #[serde(default = "default_scale")]
180    pub scale: [f32; 3],
181    #[serde(default)]
182    pub parent_id: Option<String>,
183    /// Key into DreamSceneV1.asset_refs for external assets.
184    #[serde(default)]
185    pub asset_ref: Option<String>,
186    #[serde(default)]
187    pub material: Option<MaterialDef>,
188    /// Built-in primitive: "Cube" | "Sphere" | "Cylinder" | "Cone" | "Torus" | "Capsule" | "Plane"
189    #[serde(default)]
190    pub primitive: Option<String>,
191    /// Arbitrary key-value properties for extensibility.
192    #[serde(default)]
193    pub properties: HashMap<String, serde_json::Value>,
194}
195
196fn default_kind() -> String {
197    "Mesh".to_string()
198}
199fn default_scale() -> [f32; 3] {
200    [1.0, 1.0, 1.0]
201}
202
203// =============================================================================
204// §3  LIGHTS
205// =============================================================================
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct DirectionalLightDef {
209    #[serde(default)]
210    pub id: String,
211    pub direction: [f32; 3],
212    #[serde(default = "default_light_color")]
213    pub color: [f32; 3],
214    #[serde(default = "default_one")]
215    pub intensity: f32,
216    #[serde(default)]
217    pub cast_shadows: bool,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct PointLightDef {
222    #[serde(default)]
223    pub id: String,
224    pub position: [f32; 3],
225    #[serde(default = "default_light_color")]
226    pub color: [f32; 3],
227    #[serde(default = "default_one")]
228    pub intensity: f32,
229    #[serde(default = "default_light_range")]
230    pub range: f32,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct SpotLightDef {
235    #[serde(default)]
236    pub id: String,
237    pub position: [f32; 3],
238    pub direction: [f32; 3],
239    #[serde(default = "default_light_color")]
240    pub color: [f32; 3],
241    #[serde(default = "default_one")]
242    pub intensity: f32,
243    #[serde(default = "default_light_range")]
244    pub range: f32,
245    /// Inner cone angle in degrees.
246    #[serde(default = "default_inner_cone")]
247    pub inner_cone_degrees: f32,
248    /// Outer cone angle in degrees.
249    #[serde(default = "default_outer_cone")]
250    pub outer_cone_degrees: f32,
251}
252
253fn default_light_color() -> [f32; 3] {
254    [1.0, 1.0, 1.0]
255}
256fn default_one() -> f32 {
257    1.0
258}
259fn default_light_range() -> f32 {
260    20.0
261}
262fn default_inner_cone() -> f32 {
263    30.0
264}
265fn default_outer_cone() -> f32 {
266    45.0
267}
268
269// =============================================================================
270// §4  PHYSICS
271// =============================================================================
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct ColliderDef {
275    #[serde(default)]
276    pub id: String,
277    /// "Sphere" | "Plane" | "Aabb" | "Capsule" | "Cylinder" | "Cone"
278    pub shape: String,
279    #[serde(default)]
280    pub position: [f32; 3],
281    /// Shape-specific dimensions: radius, half_extents, half_height, etc.
282    #[serde(default)]
283    pub dimensions: HashMap<String, f32>,
284    #[serde(default = "default_one")]
285    pub restitution: f32,
286    #[serde(default = "default_friction")]
287    pub friction: f32,
288    #[serde(default)]
289    pub is_static: bool,
290}
291
292fn default_friction() -> f32 {
293    0.5
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct PhysicsDefaults {
298    #[serde(default = "default_gravity")]
299    pub gravity: [f32; 3],
300    #[serde(default = "default_friction")]
301    pub friction: f32,
302    #[serde(default = "default_one")]
303    pub atmosphere_density: f32,
304}
305
306fn default_gravity() -> [f32; 3] {
307    [0.0, -9.81, 0.0]
308}
309
310impl Default for PhysicsDefaults {
311    fn default() -> Self {
312        Self {
313            gravity: default_gravity(),
314            friction: default_friction(),
315            atmosphere_density: 1.0,
316        }
317    }
318}
319
320// =============================================================================
321// §5  MATERIALS
322// =============================================================================
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct MaterialDef {
326    #[serde(default = "default_base_color")]
327    pub base_color: [f32; 4],
328    #[serde(default = "default_roughness")]
329    pub roughness: f32,
330    #[serde(default)]
331    pub metallic: f32,
332    #[serde(default)]
333    pub emissive: [f32; 3],
334    #[serde(default = "default_one")]
335    pub emissive_strength: f32,
336    #[serde(default)]
337    pub double_sided: bool,
338}
339
340fn default_base_color() -> [f32; 4] {
341    [0.8, 0.8, 0.8, 1.0]
342}
343fn default_roughness() -> f32 {
344    0.5
345}
346
347// =============================================================================
348// §6  POI
349// =============================================================================
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct PoiDef {
353    pub id: String,
354    pub position: [f32; 3],
355    #[serde(default = "default_poi_radius")]
356    pub interaction_radius: f32,
357    #[serde(default)]
358    pub property_tag: String,
359    #[serde(default)]
360    pub animation_binding: Option<String>,
361    #[serde(default)]
362    pub transition_target: Option<TransitionTarget>,
363}
364
365fn default_poi_radius() -> f32 {
366    2.0
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct TransitionTarget {
371    pub zone_id: String,
372    #[serde(default)]
373    pub layer: Option<String>,
374    #[serde(default)]
375    pub camera_profile: Option<String>,
376}
377
378// =============================================================================
379// §7  ASSET REFERENCES
380// =============================================================================
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct AssetRef {
384    /// Unique key within the scene.
385    pub key: String,
386    /// Path relative to project root: "assets/dreamwell/models/glTF/model.zip"
387    pub path: String,
388    /// "gltf" | "fbx" | "texture" | "audio" | "pbr_pack"
389    #[serde(default = "default_asset_kind")]
390    pub kind: String,
391}
392
393fn default_asset_kind() -> String {
394    "gltf".to_string()
395}
396
397// =============================================================================
398// §8  FEATURE FLAGS
399// =============================================================================
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct FeatureFlags {
403    #[serde(default)]
404    pub quantum_culling: bool,
405    #[serde(default)]
406    pub rtx_gi: bool,
407    #[serde(default)]
408    pub rtx_shadows: bool,
409    #[serde(default)]
410    pub ssao: bool,
411    #[serde(default)]
412    pub ssr: bool,
413    #[serde(default)]
414    pub ssgi: bool,
415    #[serde(default)]
416    pub taa: bool,
417    #[serde(default)]
418    pub volumetric_fog: bool,
419    #[serde(default)]
420    pub dof: bool,
421    #[serde(default)]
422    pub bloom: bool,
423    #[serde(default)]
424    pub motion_vectors: bool,
425    #[serde(default)]
426    pub hiz_culling: bool,
427    #[serde(default)]
428    pub dream_tsr: bool,
429    #[serde(default)]
430    pub dreamphysics: bool,
431    #[serde(default)]
432    pub dreammatter: bool,
433    #[serde(default)]
434    pub procedural_terrain: bool,
435    #[serde(default)]
436    pub fbx_avatar: bool,
437}
438
439impl Default for FeatureFlags {
440    fn default() -> Self {
441        Self {
442            quantum_culling: true,
443            rtx_gi: false,
444            rtx_shadows: false,
445            ssao: true,
446            ssr: true,
447            ssgi: true,
448            taa: true,
449            volumetric_fog: false,
450            dof: false,
451            bloom: true,
452            motion_vectors: true,
453            hiz_culling: true,
454            dream_tsr: false,
455            dreamphysics: true,
456            dreammatter: true,
457            procedural_terrain: false,
458            fbx_avatar: false,
459        }
460    }
461}
462
463// =============================================================================
464// §9  TAPESTRY — project manifest (tapestry.dream)
465// =============================================================================
466
467/// Project-level manifest — the master entry point for all Dreamwell clients.
468/// Equivalent to a Unity project or Unreal .uproject file.
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct TapestryV1 {
471    #[serde(default = "default_tapestry_schema")]
472    pub schema_version: String,
473    pub project_name: String,
474    pub project_id: String,
475    #[serde(default = "default_engine_version")]
476    pub engine_version: String,
477
478    /// Registered scenes in this project.
479    #[serde(default)]
480    pub scenes: Vec<SceneEntry>,
481
482    /// Scene loaded on startup.
483    #[serde(default)]
484    pub starting_scene: String,
485
486    /// Asset root directories relative to project root.
487    #[serde(default)]
488    pub asset_roots: Vec<String>,
489
490    /// Waymark pack directories to scan.
491    #[serde(default)]
492    pub waymark_packs: Vec<String>,
493
494    /// Build profiles for different benchmark/release configurations.
495    #[serde(default)]
496    pub profiles: Vec<BuildProfile>,
497
498    /// System simulation config override.
499    #[serde(default)]
500    pub simulation: Option<SimulationConfig>,
501}
502
503fn default_tapestry_schema() -> String {
504    TAPESTRY_SCHEMA_VERSION.to_string()
505}
506fn default_engine_version() -> String {
507    "1.0.0".to_string()
508}
509
510/// A scene registered in the tapestry manifest.
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct SceneEntry {
513    pub scene_id: String,
514    pub name: String,
515    /// Path to scene.dream relative to project root.
516    pub path: String,
517    #[serde(default)]
518    pub topology_layer: String,
519    #[serde(default)]
520    pub tags: Vec<String>,
521}
522
523/// A build profile: named configuration for benchmarks or releases.
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct BuildProfile {
526    pub id: String,
527    pub scene_id: String,
528    #[serde(default = "default_render_path")]
529    pub render_path: String,
530    #[serde(default)]
531    pub quality_preset: Option<String>,
532    #[serde(default)]
533    pub cli_args: Vec<String>,
534}
535
536// =============================================================================
537// §10  BINARY READ/WRITE — Clean Compute attestable scene I/O
538// =============================================================================
539
540/// Write a DreamSceneV1 to a .dream binary file.
541///
542/// Format: 32-byte header (magic + version + flags + content_len + hash) + MessagePack payload.
543/// Attestation: FNV-1a hash over content bytes, verified on read.
544pub fn write_dream_file(path: &Path, scene: &DreamSceneV1) -> Result<(), String> {
545    let content = rmp_serde::to_vec(scene).map_err(|e| format!("dream_serialize:{e}"))?;
546    let hash = fnv1a_64(&content);
547    let mut file = std::fs::File::create(path).map_err(|e| format!("dream_create:{e}"))?;
548    file.write_all(DREAM_MAGIC).map_err(|e| format!("dream_write:{e}"))?;
549    file.write_all(&DREAM_VERSION.to_le_bytes())
550        .map_err(|e| format!("dream_write:{e}"))?;
551    file.write_all(&0u32.to_le_bytes())
552        .map_err(|e| format!("dream_write:{e}"))?;
553    file.write_all(&(content.len() as u64).to_le_bytes())
554        .map_err(|e| format!("dream_write:{e}"))?;
555    file.write_all(&hash.to_le_bytes())
556        .map_err(|e| format!("dream_write:{e}"))?;
557    file.write_all(&content).map_err(|e| format!("dream_write:{e}"))?;
558    Ok(())
559}
560
561/// Read a DreamSceneV1 from a .dream binary file.
562///
563/// Validates: magic bytes, version, content length, FNV-1a attestation hash.
564pub fn read_dream_file(path: &Path) -> Result<DreamSceneV1, String> {
565    let data = std::fs::read(path).map_err(|e| format!("dream_read:{e}"))?;
566    deserialize_dream_scene(&data)
567}
568
569/// Deserialize a DreamSceneV1 from raw .dream bytes (header + payload).
570pub fn deserialize_dream_scene(data: &[u8]) -> Result<DreamSceneV1, String> {
571    if data.len() < 32 {
572        return Err("dream_validate:file too small (< 32 bytes)".into());
573    }
574    if &data[0..8] != DREAM_MAGIC {
575        return Err("dream_validate:invalid magic bytes (expected DREAMWL\\0)".into());
576    }
577    let version = u32::from_le_bytes(data[8..12].try_into().map_err(|_| "dream_validate:bad version bytes")?);
578    if version != DREAM_VERSION {
579        return Err(format!(
580            "dream_validate:unsupported version {version} (expected {DREAM_VERSION})"
581        ));
582    }
583    let content_len = u64::from_le_bytes(
584        data[16..24]
585            .try_into()
586            .map_err(|_| "dream_validate:bad content_len bytes")?,
587    ) as usize;
588    let expected_hash = u64::from_le_bytes(data[24..32].try_into().map_err(|_| "dream_validate:bad hash bytes")?);
589    if data.len() < 32 + content_len {
590        return Err(format!(
591            "dream_validate:content truncated (expected {content_len} bytes, got {})",
592            data.len() - 32
593        ));
594    }
595    let content = &data[32..32 + content_len];
596    let actual_hash = fnv1a_64(content);
597    if actual_hash != expected_hash {
598        return Err(format!(
599            "dream_validate:attestation hash mismatch (expected {expected_hash:#x}, got {actual_hash:#x})"
600        ));
601    }
602    rmp_serde::from_slice(content).map_err(|e| format!("dream_deserialize:{e}"))
603}
604
605/// Write a TapestryV1 to a tapestry.dream binary file.
606pub fn write_tapestry(path: &Path, tapestry: &TapestryV1) -> Result<(), String> {
607    let content = rmp_serde::to_vec(tapestry).map_err(|e| format!("tapestry_serialize:{e}"))?;
608    let hash = fnv1a_64(&content);
609    let mut file = std::fs::File::create(path).map_err(|e| format!("tapestry_create:{e}"))?;
610    file.write_all(DREAM_MAGIC).map_err(|e| format!("tapestry_write:{e}"))?;
611    file.write_all(&DREAM_VERSION.to_le_bytes())
612        .map_err(|e| format!("tapestry_write:{e}"))?;
613    file.write_all(&FLAG_SIGNED.to_le_bytes())
614        .map_err(|e| format!("tapestry_write:{e}"))?;
615    file.write_all(&(content.len() as u64).to_le_bytes())
616        .map_err(|e| format!("tapestry_write:{e}"))?;
617    file.write_all(&hash.to_le_bytes())
618        .map_err(|e| format!("tapestry_write:{e}"))?;
619    file.write_all(&content).map_err(|e| format!("tapestry_write:{e}"))?;
620    Ok(())
621}
622
623/// Read a TapestryV1 from a tapestry.dream binary file.
624pub fn read_tapestry(path: &Path) -> Result<TapestryV1, String> {
625    let data = std::fs::read(path).map_err(|e| format!("tapestry_read:{e}"))?;
626    if data.len() < 32 {
627        return Err("tapestry_validate:file too small".into());
628    }
629    if &data[0..8] != DREAM_MAGIC {
630        return Err("tapestry_validate:invalid magic bytes".into());
631    }
632    let version = u32::from_le_bytes(data[8..12].try_into().map_err(|_| "tapestry_validate:bad version")?);
633    if version != DREAM_VERSION {
634        return Err(format!("tapestry_validate:unsupported version {version}"));
635    }
636    let content_len = u64::from_le_bytes(
637        data[16..24]
638            .try_into()
639            .map_err(|_| "tapestry_validate:bad content_len")?,
640    ) as usize;
641    let expected_hash = u64::from_le_bytes(data[24..32].try_into().map_err(|_| "tapestry_validate:bad hash")?);
642    if data.len() < 32 + content_len {
643        return Err("tapestry_validate:content truncated".into());
644    }
645    let content = &data[32..32 + content_len];
646    let actual_hash = fnv1a_64(content);
647    if actual_hash != expected_hash {
648        return Err("tapestry_validate:attestation hash mismatch".into());
649    }
650    rmp_serde::from_slice(content).map_err(|e| format!("tapestry_deserialize:{e}"))
651}
652
653// =============================================================================
654// §11  SCENE JSON COMPILER — scene.json → DreamSceneV1
655// =============================================================================
656
657/// Compile a scene.json (human-authored JSON) into a DreamSceneV1.
658///
659/// The scene.json is a direct JSON serialization of DreamSceneV1.
660/// Additional zone/chunk/poi manifests in the same directory are merged if present.
661pub fn compile_scene_json(scene_json: &str) -> Result<DreamSceneV1, String> {
662    serde_json::from_str(scene_json).map_err(|e| format!("scene_compile:{e}"))
663}
664
665/// Compile a tapestry.json (human-authored JSON) into a TapestryV1.
666pub fn compile_tapestry_json(tapestry_json: &str) -> Result<TapestryV1, String> {
667    serde_json::from_str(tapestry_json).map_err(|e| format!("tapestry_compile:{e}"))
668}
669
670/// Compile a scene.json file and write the .dream binary next to it.
671///
672/// Reads `{scene_dir}/scene.json`, compiles to DreamSceneV1, writes `{scene_dir}/scene.dream`.
673pub fn compile_scene_dir(scene_dir: &Path) -> Result<(), String> {
674    let json_path = scene_dir.join("scene.json");
675    let dream_path = scene_dir.join("scene.dream");
676    let json_str = std::fs::read_to_string(&json_path).map_err(|e| format!("scene_read:{json_path:?}:{e}"))?;
677    let scene = compile_scene_json(&json_str)?;
678    write_dream_file(&dream_path, &scene)?;
679    log::info!("Compiled {} → {}", json_path.display(), dream_path.display());
680    Ok(())
681}
682
683/// Compile an entire project: tapestry.json + all scene.json files.
684pub fn compile_project(project_dir: &Path) -> Result<(), String> {
685    // Compile tapestry
686    let tapestry_json_path = project_dir.join("tapestry.json");
687    if tapestry_json_path.exists() {
688        let json_str = std::fs::read_to_string(&tapestry_json_path).map_err(|e| format!("tapestry_read:{e}"))?;
689        let tapestry = compile_tapestry_json(&json_str)?;
690        write_tapestry(&project_dir.join("tapestry.dream"), &tapestry)?;
691        log::info!("Compiled tapestry.dream");
692    }
693
694    // Compile all scenes
695    let scenes_dir = project_dir.join("scenes");
696    if scenes_dir.is_dir() {
697        let entries = std::fs::read_dir(&scenes_dir).map_err(|e| format!("scenes_scan:{e}"))?;
698        for entry in entries.flatten() {
699            if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
700                let scene_json = entry.path().join("scene.json");
701                if scene_json.exists() {
702                    compile_scene_dir(&entry.path())?;
703                }
704            }
705        }
706    }
707
708    Ok(())
709}
710
711// =============================================================================
712// §12  REALITY CHECK — validate a loaded scene against engine expectations
713// =============================================================================
714
715/// Validate a loaded DreamSceneV1 for correctness.
716///
717/// Returns (warnings, errors). Scene is loadable if errors is empty.
718pub fn reality_check(scene: &DreamSceneV1) -> (Vec<String>, Vec<String>) {
719    let mut warnings = Vec::new();
720    let mut errors = Vec::new();
721
722    // Identity
723    if scene.scene_id.is_empty() {
724        errors.push("reality_check:scene_id is empty".into());
725    }
726    if scene.name.is_empty() {
727        warnings.push("reality_check:scene name is empty".into());
728    }
729    if scene.schema_version != SCENE_SCHEMA_VERSION {
730        warnings.push(format!(
731            "reality_check:schema_version mismatch (got '{}', expected '{SCENE_SCHEMA_VERSION}')",
732            scene.schema_version
733        ));
734    }
735
736    // Topology
737    if scene.topology_layer > 9 {
738        errors.push(format!("reality_check:topology_layer {} > 9", scene.topology_layer));
739    }
740
741    // Objects
742    let mut obj_ids = std::collections::HashSet::new();
743    for obj in &scene.objects {
744        if obj.id.is_empty() {
745            errors.push("reality_check:object with empty id".into());
746        }
747        if !obj_ids.insert(&obj.id) {
748            errors.push(format!("reality_check:duplicate object id '{}'", obj.id));
749        }
750        // Validate position is finite
751        for &v in &obj.position {
752            if !v.is_finite() {
753                errors.push(format!("reality_check:object '{}' has non-finite position", obj.id));
754                break;
755            }
756        }
757        // Validate asset ref exists
758        if let Some(ref key) = obj.asset_ref {
759            if !scene.asset_refs.iter().any(|a| &a.key == key) {
760                errors.push(format!(
761                    "reality_check:object '{}' references unknown asset '{key}'",
762                    obj.id
763                ));
764            }
765        }
766    }
767
768    // Lights
769    for light in &scene.directional_lights {
770        let len_sq: f32 = light.direction.iter().map(|v| v * v).sum();
771        if len_sq < 0.001 {
772            warnings.push(format!(
773                "reality_check:directional light '{}' has near-zero direction",
774                light.id
775            ));
776        }
777    }
778
779    // POIs
780    for poi in &scene.pois {
781        if poi.interaction_radius <= 0.0 {
782            errors.push(format!("reality_check:POI '{}' has non-positive radius", poi.id));
783        }
784    }
785
786    (warnings, errors)
787}
788
789// =============================================================================
790// §13  TESTS
791// =============================================================================
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796
797    fn sample_scene() -> DreamSceneV1 {
798        DreamSceneV1 {
799            scene_id: "test-scene".into(),
800            name: "Test Scene".into(),
801            description: "Unit test scene".into(),
802            objects: vec![SceneObject {
803                id: "ground".into(),
804                name: "Ground Plane".into(),
805                kind: "Mesh".into(),
806                position: [0.0, 0.0, 0.0],
807                rotation: [0.0, 0.0, 0.0],
808                scale: [80.0, 1.0, 80.0],
809                parent_id: None,
810                asset_ref: None,
811                material: Some(MaterialDef {
812                    base_color: [0.3, 0.5, 0.2, 1.0],
813                    roughness: 0.8,
814                    metallic: 0.0,
815                    emissive: [0.0, 0.0, 0.0],
816                    emissive_strength: 0.0,
817                    double_sided: false,
818                }),
819                primitive: Some("Plane".into()),
820                properties: HashMap::new(),
821            }],
822            directional_lights: vec![DirectionalLightDef {
823                id: "sun".into(),
824                direction: [0.4, -0.7, 0.3],
825                color: [1.0, 0.95, 0.85],
826                intensity: 1.2,
827                cast_shadows: true,
828            }],
829            point_lights: vec![PointLightDef {
830                id: "fill".into(),
831                position: [5.0, 3.0, 0.0],
832                color: [0.6, 0.8, 1.0],
833                intensity: 0.5,
834                range: 15.0,
835            }],
836            pois: vec![PoiDef {
837                id: "demo-poi".into(),
838                position: [8.0, 0.0, 0.0],
839                interaction_radius: 2.0,
840                property_tag: "interact.demo.spawn".into(),
841                animation_binding: None,
842                transition_target: None,
843            }],
844            ..Default::default()
845        }
846    }
847
848    fn sample_tapestry() -> TapestryV1 {
849        TapestryV1 {
850            schema_version: TAPESTRY_SCHEMA_VERSION.into(),
851            project_name: "Test Project".into(),
852            project_id: "test-project".into(),
853            engine_version: "1.0.0".into(),
854            scenes: vec![SceneEntry {
855                scene_id: "test-scene".into(),
856                name: "Test Scene".into(),
857                path: "scenes/test/scene.dream".into(),
858                topology_layer: "Area".into(),
859                tags: vec!["test".into()],
860            }],
861            starting_scene: "test-scene".into(),
862            asset_roots: vec!["assets/dreamwell".into()],
863            waymark_packs: vec!["waymark/".into()],
864            profiles: vec![BuildProfile {
865                id: "default".into(),
866                scene_id: "test-scene".into(),
867                render_path: "Dreamwell".into(),
868                quality_preset: None,
869                cli_args: vec!["--dream".into()],
870            }],
871            simulation: None,
872        }
873    }
874
875    #[test]
876    fn dream_file_roundtrip() {
877        let scene = sample_scene();
878        let dir = std::env::temp_dir().join("dreamwell_test_roundtrip");
879        std::fs::create_dir_all(&dir).unwrap();
880        let path = dir.join("test.dream");
881
882        write_dream_file(&path, &scene).unwrap();
883        let loaded = read_dream_file(&path).unwrap();
884
885        assert_eq!(scene.scene_id, loaded.scene_id);
886        assert_eq!(scene.name, loaded.name);
887        assert_eq!(scene.objects.len(), loaded.objects.len());
888        assert_eq!(scene.directional_lights.len(), loaded.directional_lights.len());
889        assert_eq!(scene.point_lights.len(), loaded.point_lights.len());
890        assert_eq!(scene.pois.len(), loaded.pois.len());
891        assert_eq!(scene.objects[0].id, loaded.objects[0].id);
892        assert_eq!(scene.objects[0].scale, loaded.objects[0].scale);
893
894        std::fs::remove_dir_all(&dir).ok();
895    }
896
897    #[test]
898    fn dream_file_rejects_bad_magic() {
899        let mut data = vec![0u8; 64];
900        data[0..8].copy_from_slice(b"NOTDREAM");
901        let result = deserialize_dream_scene(&data);
902        assert!(result.is_err());
903        assert!(result.unwrap_err().contains("invalid magic bytes"));
904    }
905
906    #[test]
907    fn dream_file_rejects_bad_version() {
908        let scene = sample_scene();
909        let content = rmp_serde::to_vec(&scene).unwrap();
910        let hash = fnv1a_64(&content);
911        let mut data = Vec::new();
912        data.extend_from_slice(DREAM_MAGIC);
913        data.extend_from_slice(&99u32.to_le_bytes()); // bad version
914        data.extend_from_slice(&0u32.to_le_bytes());
915        data.extend_from_slice(&(content.len() as u64).to_le_bytes());
916        data.extend_from_slice(&hash.to_le_bytes());
917        data.extend_from_slice(&content);
918
919        let result = deserialize_dream_scene(&data);
920        assert!(result.is_err());
921        assert!(result.unwrap_err().contains("unsupported version 99"));
922    }
923
924    #[test]
925    fn dream_file_rejects_bad_hash() {
926        let scene = sample_scene();
927        let content = rmp_serde::to_vec(&scene).unwrap();
928        let mut data = Vec::new();
929        data.extend_from_slice(DREAM_MAGIC);
930        data.extend_from_slice(&DREAM_VERSION.to_le_bytes());
931        data.extend_from_slice(&0u32.to_le_bytes());
932        data.extend_from_slice(&(content.len() as u64).to_le_bytes());
933        data.extend_from_slice(&0xDEADBEEFu64.to_le_bytes()); // wrong hash
934        data.extend_from_slice(&content);
935
936        let result = deserialize_dream_scene(&data);
937        assert!(result.is_err());
938        assert!(result.unwrap_err().contains("attestation hash mismatch"));
939    }
940
941    #[test]
942    fn dream_file_rejects_too_small() {
943        let result = deserialize_dream_scene(&[0u8; 16]);
944        assert!(result.is_err());
945        assert!(result.unwrap_err().contains("too small"));
946    }
947
948    #[test]
949    fn tapestry_roundtrip() {
950        let tapestry = sample_tapestry();
951        let dir = std::env::temp_dir().join("dreamwell_test_tapestry");
952        std::fs::create_dir_all(&dir).unwrap();
953        let path = dir.join("tapestry.dream");
954
955        write_tapestry(&path, &tapestry).unwrap();
956        let loaded = read_tapestry(&path).unwrap();
957
958        assert_eq!(tapestry.project_name, loaded.project_name);
959        assert_eq!(tapestry.project_id, loaded.project_id);
960        assert_eq!(tapestry.scenes.len(), loaded.scenes.len());
961        assert_eq!(tapestry.profiles.len(), loaded.profiles.len());
962        assert_eq!(tapestry.starting_scene, loaded.starting_scene);
963
964        std::fs::remove_dir_all(&dir).ok();
965    }
966
967    #[test]
968    fn scene_json_compile() {
969        let scene = sample_scene();
970        let json = serde_json::to_string_pretty(&scene).unwrap();
971        let compiled = compile_scene_json(&json).unwrap();
972        assert_eq!(compiled.scene_id, "test-scene");
973        assert_eq!(compiled.objects.len(), 1);
974    }
975
976    #[test]
977    fn tapestry_json_compile() {
978        let tapestry = sample_tapestry();
979        let json = serde_json::to_string_pretty(&tapestry).unwrap();
980        let compiled = compile_tapestry_json(&json).unwrap();
981        assert_eq!(compiled.project_id, "test-project");
982        assert_eq!(compiled.scenes.len(), 1);
983    }
984
985    #[test]
986    fn reality_check_valid_scene() {
987        let scene = sample_scene();
988        let (warnings, errors) = reality_check(&scene);
989        assert!(errors.is_empty(), "Unexpected errors: {errors:?}");
990        assert!(warnings.is_empty(), "Unexpected warnings: {warnings:?}");
991    }
992
993    #[test]
994    fn reality_check_catches_empty_id() {
995        let mut scene = sample_scene();
996        scene.scene_id = String::new();
997        let (_, errors) = reality_check(&scene);
998        assert!(errors.iter().any(|e| e.contains("scene_id is empty")));
999    }
1000
1001    #[test]
1002    fn reality_check_catches_duplicate_object_ids() {
1003        let mut scene = sample_scene();
1004        scene.objects.push(scene.objects[0].clone());
1005        let (_, errors) = reality_check(&scene);
1006        assert!(errors.iter().any(|e| e.contains("duplicate object id")));
1007    }
1008
1009    #[test]
1010    fn reality_check_catches_bad_asset_ref() {
1011        let mut scene = sample_scene();
1012        scene.objects[0].asset_ref = Some("nonexistent".into());
1013        let (_, errors) = reality_check(&scene);
1014        assert!(errors.iter().any(|e| e.contains("unknown asset")));
1015    }
1016
1017    #[test]
1018    fn reality_check_catches_bad_topology() {
1019        let mut scene = sample_scene();
1020        scene.topology_layer = 15;
1021        let (_, errors) = reality_check(&scene);
1022        assert!(errors.iter().any(|e| e.contains("topology_layer 15 > 9")));
1023    }
1024
1025    #[test]
1026    fn default_feature_flags() {
1027        let flags = FeatureFlags::default();
1028        assert!(flags.quantum_culling);
1029        assert!(flags.ssao);
1030        assert!(flags.bloom);
1031        assert!(!flags.rtx_gi);
1032        assert!(!flags.rtx_shadows);
1033    }
1034
1035    #[test]
1036    fn default_physics() {
1037        let phys = PhysicsDefaults::default();
1038        assert_eq!(phys.gravity, [0.0, -9.81, 0.0]);
1039    }
1040
1041    #[test]
1042    fn compile_keynote_scene_from_project() {
1043        // Compile the real keynote scene.json from the benchmark project
1044        let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1045            .parent()
1046            .unwrap()
1047            .parent()
1048            .unwrap()
1049            .join("dreamwell-benchmark-project");
1050        let scene_json_path = project_root.join("scenes").join("keynote").join("scene.json");
1051        if !scene_json_path.exists() {
1052            // Skip if project folder not present (CI environments)
1053            return;
1054        }
1055        let json_str = std::fs::read_to_string(&scene_json_path).unwrap();
1056        let scene = compile_scene_json(&json_str).unwrap();
1057
1058        assert_eq!(scene.scene_id, "keynote");
1059        assert_eq!(scene.name, "Dreamwell Keynote Benchmark");
1060        assert_eq!(scene.topology_layer, 6); // Area
1061        assert!(
1062            scene.objects.len() >= 15,
1063            "Expected 15+ objects, got {}",
1064            scene.objects.len()
1065        );
1066        assert_eq!(scene.directional_lights.len(), 2);
1067        assert_eq!(scene.point_lights.len(), 8);
1068        assert!(scene.colliders.len() >= 5);
1069        assert_eq!(scene.render_path, "Dreamwell");
1070        assert!(scene.feature_flags.quantum_culling);
1071        assert!(scene.feature_flags.bloom);
1072        assert!(scene.feature_flags.dreammatter);
1073
1074        // Reality check
1075        let (warnings, errors) = reality_check(&scene);
1076        assert!(errors.is_empty(), "Keynote scene has errors: {errors:?}");
1077        for w in &warnings {
1078            eprintln!("  warning: {w}");
1079        }
1080
1081        // Round-trip: JSON → DreamSceneV1 → .dream binary → DreamSceneV1
1082        let dir = std::env::temp_dir().join("dreamwell_keynote_compile_test");
1083        std::fs::create_dir_all(&dir).unwrap();
1084        let dream_path = dir.join("keynote.dream");
1085        write_dream_file(&dream_path, &scene).unwrap();
1086        let reloaded = read_dream_file(&dream_path).unwrap();
1087        assert_eq!(reloaded.scene_id, "keynote");
1088        assert_eq!(reloaded.objects.len(), scene.objects.len());
1089        assert_eq!(reloaded.directional_lights.len(), 2);
1090        assert_eq!(reloaded.point_lights.len(), 8);
1091        std::fs::remove_dir_all(&dir).ok();
1092    }
1093
1094    #[test]
1095    fn compile_all_scenes_from_project() {
1096        let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1097            .parent()
1098            .unwrap()
1099            .parent()
1100            .unwrap()
1101            .join("dreamwell-benchmark-project");
1102        let scenes_dir = project_root.join("scenes");
1103        if !scenes_dir.exists() {
1104            return;
1105        }
1106
1107        let expected = [
1108            "keynote",
1109            "spawn_test",
1110            "micro_dreamlet",
1111            "avatar_demo",
1112            "ray_tracing_demo",
1113            "ray_tracing_gallery",
1114            "stress_test",
1115        ];
1116        let mut compiled = 0;
1117        for scene_name in &expected {
1118            let json_path = scenes_dir.join(scene_name).join("scene.json");
1119            if !json_path.exists() {
1120                continue;
1121            }
1122            let json_str = std::fs::read_to_string(&json_path)
1123                .unwrap_or_else(|e| panic!("Failed to read {scene_name}/scene.json: {e}"));
1124            let scene = compile_scene_json(&json_str).unwrap_or_else(|e| panic!("Failed to compile {scene_name}: {e}"));
1125            assert_eq!(scene.scene_id, *scene_name, "scene_id mismatch for {scene_name}");
1126            assert!(!scene.name.is_empty(), "empty name for {scene_name}");
1127
1128            let (warnings, errors) = reality_check(&scene);
1129            assert!(errors.is_empty(), "{scene_name} reality check errors: {errors:?}");
1130
1131            // Round-trip through .dream binary
1132            let dir = std::env::temp_dir().join(format!("dreamwell_compile_{scene_name}"));
1133            std::fs::create_dir_all(&dir).unwrap();
1134            let dream_path = dir.join("scene.dream");
1135            write_dream_file(&dream_path, &scene).unwrap();
1136            let reloaded = read_dream_file(&dream_path).unwrap();
1137            assert_eq!(reloaded.scene_id, scene.scene_id);
1138            assert_eq!(reloaded.objects.len(), scene.objects.len());
1139            std::fs::remove_dir_all(&dir).ok();
1140
1141            compiled += 1;
1142            for w in &warnings {
1143                eprintln!("  {scene_name} warning: {w}");
1144            }
1145        }
1146        assert!(compiled >= 5, "Expected to compile at least 5 scenes, got {compiled}");
1147    }
1148
1149    #[test]
1150    fn compile_tapestry_from_project() {
1151        let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1152            .parent()
1153            .unwrap()
1154            .parent()
1155            .unwrap()
1156            .join("dreamwell-benchmark-project");
1157        let tapestry_json_path = project_root.join("tapestry.json");
1158        if !tapestry_json_path.exists() {
1159            return;
1160        }
1161        let json_str = std::fs::read_to_string(&tapestry_json_path).unwrap();
1162        let tapestry = compile_tapestry_json(&json_str).unwrap();
1163
1164        assert_eq!(tapestry.project_id, "dreamwell-benchmarks");
1165        assert_eq!(tapestry.starting_scene, "keynote");
1166        assert!(
1167            tapestry.scenes.len() >= 6,
1168            "Expected 6+ scenes, got {}",
1169            tapestry.scenes.len()
1170        );
1171        assert!(
1172            tapestry.profiles.len() >= 6,
1173            "Expected 6+ profiles, got {}",
1174            tapestry.profiles.len()
1175        );
1176
1177        // Verify all scene_ids in profiles reference valid scenes
1178        for profile in &tapestry.profiles {
1179            assert!(
1180                tapestry.scenes.iter().any(|s| s.scene_id == profile.scene_id),
1181                "Profile '{}' references unknown scene '{}'",
1182                profile.id,
1183                profile.scene_id
1184            );
1185        }
1186
1187        // Round-trip
1188        let dir = std::env::temp_dir().join("dreamwell_tapestry_compile_test");
1189        std::fs::create_dir_all(&dir).unwrap();
1190        let dream_path = dir.join("tapestry.dream");
1191        write_tapestry(&dream_path, &tapestry).unwrap();
1192        let reloaded = read_tapestry(&dream_path).unwrap();
1193        assert_eq!(reloaded.project_id, "dreamwell-benchmarks");
1194        assert_eq!(reloaded.scenes.len(), tapestry.scenes.len());
1195        std::fs::remove_dir_all(&dir).ok();
1196    }
1197}