use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use crate::hash::fnv1a_64;
use crate::waymark::schema::{GridConfig, SimulationConfig, SpatialConfig};
pub const DREAM_MAGIC: &[u8; 8] = b"DREAMWL\0";
pub const DREAM_VERSION: u32 = 1;
pub const SCENE_SCHEMA_VERSION: &str = "dreamwell_scene_v1.0.0";
pub const TAPESTRY_SCHEMA_VERSION: &str = "dreamwell_tapestry_v1.0.0";
pub const FLAG_COMPRESSED: u32 = 1 << 0;
pub const FLAG_SIGNED: u32 = 1 << 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DreamSceneV1 {
pub scene_id: String,
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_scene_schema")]
pub schema_version: String,
#[serde(default = "default_topology_layer")]
pub topology_layer: u8,
#[serde(default)]
pub starting_zone_id: String,
#[serde(default)]
pub world_id: Option<String>,
#[serde(default = "default_camera")]
pub default_camera: String,
#[serde(default)]
pub allowed_cameras: Vec<String>,
#[serde(default)]
pub objects: Vec<SceneObject>,
#[serde(default)]
pub directional_lights: Vec<DirectionalLightDef>,
#[serde(default)]
pub point_lights: Vec<PointLightDef>,
#[serde(default)]
pub spot_lights: Vec<SpotLightDef>,
#[serde(default)]
pub colliders: Vec<ColliderDef>,
#[serde(default)]
pub physics_defaults: PhysicsDefaults,
#[serde(default)]
pub pois: Vec<PoiDef>,
#[serde(default)]
pub asset_refs: Vec<AssetRef>,
#[serde(default = "default_render_path")]
pub render_path: String,
#[serde(default)]
pub quality_preset: Option<String>,
#[serde(default)]
pub feature_flags: FeatureFlags,
#[serde(default)]
pub waymark_pack: Option<serde_json::Value>,
#[serde(default)]
pub grid: GridConfig,
#[serde(default)]
pub spatial: SpatialConfig,
#[serde(default)]
pub simulation: SimulationConfig,
}
fn default_scene_schema() -> String {
SCENE_SCHEMA_VERSION.to_string()
}
fn default_topology_layer() -> u8 {
6 }
fn default_camera() -> String {
"ThirdPerson".to_string()
}
fn default_render_path() -> String {
"Dreamwell".to_string()
}
impl Default for DreamSceneV1 {
fn default() -> Self {
Self {
scene_id: String::new(),
name: String::new(),
description: String::new(),
schema_version: default_scene_schema(),
topology_layer: default_topology_layer(),
starting_zone_id: String::new(),
world_id: None,
default_camera: default_camera(),
allowed_cameras: Vec::new(),
objects: Vec::new(),
directional_lights: Vec::new(),
point_lights: Vec::new(),
spot_lights: Vec::new(),
colliders: Vec::new(),
physics_defaults: PhysicsDefaults::default(),
pois: Vec::new(),
asset_refs: Vec::new(),
render_path: default_render_path(),
quality_preset: None,
feature_flags: FeatureFlags::default(),
waymark_pack: None,
grid: GridConfig::default(),
spatial: SpatialConfig::default(),
simulation: SimulationConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneObject {
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default = "default_kind")]
pub kind: String,
#[serde(default)]
pub position: [f32; 3],
#[serde(default)]
pub rotation: [f32; 3],
#[serde(default = "default_scale")]
pub scale: [f32; 3],
#[serde(default)]
pub parent_id: Option<String>,
#[serde(default)]
pub asset_ref: Option<String>,
#[serde(default)]
pub material: Option<MaterialDef>,
#[serde(default)]
pub primitive: Option<String>,
#[serde(default)]
pub properties: HashMap<String, serde_json::Value>,
}
fn default_kind() -> String {
"Mesh".to_string()
}
fn default_scale() -> [f32; 3] {
[1.0, 1.0, 1.0]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectionalLightDef {
#[serde(default)]
pub id: String,
pub direction: [f32; 3],
#[serde(default = "default_light_color")]
pub color: [f32; 3],
#[serde(default = "default_one")]
pub intensity: f32,
#[serde(default)]
pub cast_shadows: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PointLightDef {
#[serde(default)]
pub id: String,
pub position: [f32; 3],
#[serde(default = "default_light_color")]
pub color: [f32; 3],
#[serde(default = "default_one")]
pub intensity: f32,
#[serde(default = "default_light_range")]
pub range: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpotLightDef {
#[serde(default)]
pub id: String,
pub position: [f32; 3],
pub direction: [f32; 3],
#[serde(default = "default_light_color")]
pub color: [f32; 3],
#[serde(default = "default_one")]
pub intensity: f32,
#[serde(default = "default_light_range")]
pub range: f32,
#[serde(default = "default_inner_cone")]
pub inner_cone_degrees: f32,
#[serde(default = "default_outer_cone")]
pub outer_cone_degrees: f32,
}
fn default_light_color() -> [f32; 3] {
[1.0, 1.0, 1.0]
}
fn default_one() -> f32 {
1.0
}
fn default_light_range() -> f32 {
20.0
}
fn default_inner_cone() -> f32 {
30.0
}
fn default_outer_cone() -> f32 {
45.0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColliderDef {
#[serde(default)]
pub id: String,
pub shape: String,
#[serde(default)]
pub position: [f32; 3],
#[serde(default)]
pub dimensions: HashMap<String, f32>,
#[serde(default = "default_one")]
pub restitution: f32,
#[serde(default = "default_friction")]
pub friction: f32,
#[serde(default)]
pub is_static: bool,
}
fn default_friction() -> f32 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhysicsDefaults {
#[serde(default = "default_gravity")]
pub gravity: [f32; 3],
#[serde(default = "default_friction")]
pub friction: f32,
#[serde(default = "default_one")]
pub atmosphere_density: f32,
}
fn default_gravity() -> [f32; 3] {
[0.0, -9.81, 0.0]
}
impl Default for PhysicsDefaults {
fn default() -> Self {
Self {
gravity: default_gravity(),
friction: default_friction(),
atmosphere_density: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaterialDef {
#[serde(default = "default_base_color")]
pub base_color: [f32; 4],
#[serde(default = "default_roughness")]
pub roughness: f32,
#[serde(default)]
pub metallic: f32,
#[serde(default)]
pub emissive: [f32; 3],
#[serde(default = "default_one")]
pub emissive_strength: f32,
#[serde(default)]
pub double_sided: bool,
}
fn default_base_color() -> [f32; 4] {
[0.8, 0.8, 0.8, 1.0]
}
fn default_roughness() -> f32 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoiDef {
pub id: String,
pub position: [f32; 3],
#[serde(default = "default_poi_radius")]
pub interaction_radius: f32,
#[serde(default)]
pub property_tag: String,
#[serde(default)]
pub animation_binding: Option<String>,
#[serde(default)]
pub transition_target: Option<TransitionTarget>,
}
fn default_poi_radius() -> f32 {
2.0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransitionTarget {
pub zone_id: String,
#[serde(default)]
pub layer: Option<String>,
#[serde(default)]
pub camera_profile: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetRef {
pub key: String,
pub path: String,
#[serde(default = "default_asset_kind")]
pub kind: String,
}
fn default_asset_kind() -> String {
"gltf".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureFlags {
#[serde(default)]
pub quantum_culling: bool,
#[serde(default)]
pub rtx_gi: bool,
#[serde(default)]
pub rtx_shadows: bool,
#[serde(default)]
pub ssao: bool,
#[serde(default)]
pub ssr: bool,
#[serde(default)]
pub ssgi: bool,
#[serde(default)]
pub taa: bool,
#[serde(default)]
pub volumetric_fog: bool,
#[serde(default)]
pub dof: bool,
#[serde(default)]
pub bloom: bool,
#[serde(default)]
pub motion_vectors: bool,
#[serde(default)]
pub hiz_culling: bool,
#[serde(default)]
pub dream_tsr: bool,
#[serde(default)]
pub dreamphysics: bool,
#[serde(default)]
pub dreammatter: bool,
#[serde(default)]
pub procedural_terrain: bool,
#[serde(default)]
pub fbx_avatar: bool,
}
impl Default for FeatureFlags {
fn default() -> Self {
Self {
quantum_culling: true,
rtx_gi: false,
rtx_shadows: false,
ssao: true,
ssr: true,
ssgi: true,
taa: true,
volumetric_fog: false,
dof: false,
bloom: true,
motion_vectors: true,
hiz_culling: true,
dream_tsr: false,
dreamphysics: true,
dreammatter: true,
procedural_terrain: false,
fbx_avatar: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TapestryV1 {
#[serde(default = "default_tapestry_schema")]
pub schema_version: String,
pub project_name: String,
pub project_id: String,
#[serde(default = "default_engine_version")]
pub engine_version: String,
#[serde(default)]
pub scenes: Vec<SceneEntry>,
#[serde(default)]
pub starting_scene: String,
#[serde(default)]
pub asset_roots: Vec<String>,
#[serde(default)]
pub waymark_packs: Vec<String>,
#[serde(default)]
pub profiles: Vec<BuildProfile>,
#[serde(default)]
pub simulation: Option<SimulationConfig>,
}
fn default_tapestry_schema() -> String {
TAPESTRY_SCHEMA_VERSION.to_string()
}
fn default_engine_version() -> String {
"1.0.0".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneEntry {
pub scene_id: String,
pub name: String,
pub path: String,
#[serde(default)]
pub topology_layer: String,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildProfile {
pub id: String,
pub scene_id: String,
#[serde(default = "default_render_path")]
pub render_path: String,
#[serde(default)]
pub quality_preset: Option<String>,
#[serde(default)]
pub cli_args: Vec<String>,
}
pub fn write_dream_file(path: &Path, scene: &DreamSceneV1) -> Result<(), String> {
let content = rmp_serde::to_vec(scene).map_err(|e| format!("dream_serialize:{e}"))?;
let hash = fnv1a_64(&content);
let mut file = std::fs::File::create(path).map_err(|e| format!("dream_create:{e}"))?;
file.write_all(DREAM_MAGIC).map_err(|e| format!("dream_write:{e}"))?;
file.write_all(&DREAM_VERSION.to_le_bytes())
.map_err(|e| format!("dream_write:{e}"))?;
file.write_all(&0u32.to_le_bytes())
.map_err(|e| format!("dream_write:{e}"))?;
file.write_all(&(content.len() as u64).to_le_bytes())
.map_err(|e| format!("dream_write:{e}"))?;
file.write_all(&hash.to_le_bytes())
.map_err(|e| format!("dream_write:{e}"))?;
file.write_all(&content).map_err(|e| format!("dream_write:{e}"))?;
Ok(())
}
pub fn read_dream_file(path: &Path) -> Result<DreamSceneV1, String> {
let data = std::fs::read(path).map_err(|e| format!("dream_read:{e}"))?;
deserialize_dream_scene(&data)
}
pub fn deserialize_dream_scene(data: &[u8]) -> Result<DreamSceneV1, String> {
if data.len() < 32 {
return Err("dream_validate:file too small (< 32 bytes)".into());
}
if &data[0..8] != DREAM_MAGIC {
return Err("dream_validate:invalid magic bytes (expected DREAMWL\\0)".into());
}
let version = u32::from_le_bytes(data[8..12].try_into().map_err(|_| "dream_validate:bad version bytes")?);
if version != DREAM_VERSION {
return Err(format!(
"dream_validate:unsupported version {version} (expected {DREAM_VERSION})"
));
}
let content_len = u64::from_le_bytes(
data[16..24]
.try_into()
.map_err(|_| "dream_validate:bad content_len bytes")?,
) as usize;
let expected_hash = u64::from_le_bytes(data[24..32].try_into().map_err(|_| "dream_validate:bad hash bytes")?);
if data.len() < 32 + content_len {
return Err(format!(
"dream_validate:content truncated (expected {content_len} bytes, got {})",
data.len() - 32
));
}
let content = &data[32..32 + content_len];
let actual_hash = fnv1a_64(content);
if actual_hash != expected_hash {
return Err(format!(
"dream_validate:attestation hash mismatch (expected {expected_hash:#x}, got {actual_hash:#x})"
));
}
rmp_serde::from_slice(content).map_err(|e| format!("dream_deserialize:{e}"))
}
pub fn write_tapestry(path: &Path, tapestry: &TapestryV1) -> Result<(), String> {
let content = rmp_serde::to_vec(tapestry).map_err(|e| format!("tapestry_serialize:{e}"))?;
let hash = fnv1a_64(&content);
let mut file = std::fs::File::create(path).map_err(|e| format!("tapestry_create:{e}"))?;
file.write_all(DREAM_MAGIC).map_err(|e| format!("tapestry_write:{e}"))?;
file.write_all(&DREAM_VERSION.to_le_bytes())
.map_err(|e| format!("tapestry_write:{e}"))?;
file.write_all(&FLAG_SIGNED.to_le_bytes())
.map_err(|e| format!("tapestry_write:{e}"))?;
file.write_all(&(content.len() as u64).to_le_bytes())
.map_err(|e| format!("tapestry_write:{e}"))?;
file.write_all(&hash.to_le_bytes())
.map_err(|e| format!("tapestry_write:{e}"))?;
file.write_all(&content).map_err(|e| format!("tapestry_write:{e}"))?;
Ok(())
}
pub fn read_tapestry(path: &Path) -> Result<TapestryV1, String> {
let data = std::fs::read(path).map_err(|e| format!("tapestry_read:{e}"))?;
if data.len() < 32 {
return Err("tapestry_validate:file too small".into());
}
if &data[0..8] != DREAM_MAGIC {
return Err("tapestry_validate:invalid magic bytes".into());
}
let version = u32::from_le_bytes(data[8..12].try_into().map_err(|_| "tapestry_validate:bad version")?);
if version != DREAM_VERSION {
return Err(format!("tapestry_validate:unsupported version {version}"));
}
let content_len = u64::from_le_bytes(
data[16..24]
.try_into()
.map_err(|_| "tapestry_validate:bad content_len")?,
) as usize;
let expected_hash = u64::from_le_bytes(data[24..32].try_into().map_err(|_| "tapestry_validate:bad hash")?);
if data.len() < 32 + content_len {
return Err("tapestry_validate:content truncated".into());
}
let content = &data[32..32 + content_len];
let actual_hash = fnv1a_64(content);
if actual_hash != expected_hash {
return Err("tapestry_validate:attestation hash mismatch".into());
}
rmp_serde::from_slice(content).map_err(|e| format!("tapestry_deserialize:{e}"))
}
pub fn compile_scene_json(scene_json: &str) -> Result<DreamSceneV1, String> {
serde_json::from_str(scene_json).map_err(|e| format!("scene_compile:{e}"))
}
pub fn compile_tapestry_json(tapestry_json: &str) -> Result<TapestryV1, String> {
serde_json::from_str(tapestry_json).map_err(|e| format!("tapestry_compile:{e}"))
}
pub fn compile_scene_dir(scene_dir: &Path) -> Result<(), String> {
let json_path = scene_dir.join("scene.json");
let dream_path = scene_dir.join("scene.dream");
let json_str = std::fs::read_to_string(&json_path).map_err(|e| format!("scene_read:{json_path:?}:{e}"))?;
let scene = compile_scene_json(&json_str)?;
write_dream_file(&dream_path, &scene)?;
log::info!("Compiled {} → {}", json_path.display(), dream_path.display());
Ok(())
}
pub fn compile_project(project_dir: &Path) -> Result<(), String> {
let tapestry_json_path = project_dir.join("tapestry.json");
if tapestry_json_path.exists() {
let json_str = std::fs::read_to_string(&tapestry_json_path).map_err(|e| format!("tapestry_read:{e}"))?;
let tapestry = compile_tapestry_json(&json_str)?;
write_tapestry(&project_dir.join("tapestry.dream"), &tapestry)?;
log::info!("Compiled tapestry.dream");
}
let scenes_dir = project_dir.join("scenes");
if scenes_dir.is_dir() {
let entries = std::fs::read_dir(&scenes_dir).map_err(|e| format!("scenes_scan:{e}"))?;
for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let scene_json = entry.path().join("scene.json");
if scene_json.exists() {
compile_scene_dir(&entry.path())?;
}
}
}
}
Ok(())
}
pub fn reality_check(scene: &DreamSceneV1) -> (Vec<String>, Vec<String>) {
let mut warnings = Vec::new();
let mut errors = Vec::new();
if scene.scene_id.is_empty() {
errors.push("reality_check:scene_id is empty".into());
}
if scene.name.is_empty() {
warnings.push("reality_check:scene name is empty".into());
}
if scene.schema_version != SCENE_SCHEMA_VERSION {
warnings.push(format!(
"reality_check:schema_version mismatch (got '{}', expected '{SCENE_SCHEMA_VERSION}')",
scene.schema_version
));
}
if scene.topology_layer > 9 {
errors.push(format!("reality_check:topology_layer {} > 9", scene.topology_layer));
}
let mut obj_ids = std::collections::HashSet::new();
for obj in &scene.objects {
if obj.id.is_empty() {
errors.push("reality_check:object with empty id".into());
}
if !obj_ids.insert(&obj.id) {
errors.push(format!("reality_check:duplicate object id '{}'", obj.id));
}
for &v in &obj.position {
if !v.is_finite() {
errors.push(format!("reality_check:object '{}' has non-finite position", obj.id));
break;
}
}
if let Some(ref key) = obj.asset_ref {
if !scene.asset_refs.iter().any(|a| &a.key == key) {
errors.push(format!(
"reality_check:object '{}' references unknown asset '{key}'",
obj.id
));
}
}
}
for light in &scene.directional_lights {
let len_sq: f32 = light.direction.iter().map(|v| v * v).sum();
if len_sq < 0.001 {
warnings.push(format!(
"reality_check:directional light '{}' has near-zero direction",
light.id
));
}
}
for poi in &scene.pois {
if poi.interaction_radius <= 0.0 {
errors.push(format!("reality_check:POI '{}' has non-positive radius", poi.id));
}
}
(warnings, errors)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_scene() -> DreamSceneV1 {
DreamSceneV1 {
scene_id: "test-scene".into(),
name: "Test Scene".into(),
description: "Unit test scene".into(),
objects: vec![SceneObject {
id: "ground".into(),
name: "Ground Plane".into(),
kind: "Mesh".into(),
position: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0],
scale: [80.0, 1.0, 80.0],
parent_id: None,
asset_ref: None,
material: Some(MaterialDef {
base_color: [0.3, 0.5, 0.2, 1.0],
roughness: 0.8,
metallic: 0.0,
emissive: [0.0, 0.0, 0.0],
emissive_strength: 0.0,
double_sided: false,
}),
primitive: Some("Plane".into()),
properties: HashMap::new(),
}],
directional_lights: vec![DirectionalLightDef {
id: "sun".into(),
direction: [0.4, -0.7, 0.3],
color: [1.0, 0.95, 0.85],
intensity: 1.2,
cast_shadows: true,
}],
point_lights: vec![PointLightDef {
id: "fill".into(),
position: [5.0, 3.0, 0.0],
color: [0.6, 0.8, 1.0],
intensity: 0.5,
range: 15.0,
}],
pois: vec![PoiDef {
id: "demo-poi".into(),
position: [8.0, 0.0, 0.0],
interaction_radius: 2.0,
property_tag: "interact.demo.spawn".into(),
animation_binding: None,
transition_target: None,
}],
..Default::default()
}
}
fn sample_tapestry() -> TapestryV1 {
TapestryV1 {
schema_version: TAPESTRY_SCHEMA_VERSION.into(),
project_name: "Test Project".into(),
project_id: "test-project".into(),
engine_version: "1.0.0".into(),
scenes: vec![SceneEntry {
scene_id: "test-scene".into(),
name: "Test Scene".into(),
path: "scenes/test/scene.dream".into(),
topology_layer: "Area".into(),
tags: vec!["test".into()],
}],
starting_scene: "test-scene".into(),
asset_roots: vec!["assets/dreamwell".into()],
waymark_packs: vec!["waymark/".into()],
profiles: vec![BuildProfile {
id: "default".into(),
scene_id: "test-scene".into(),
render_path: "Dreamwell".into(),
quality_preset: None,
cli_args: vec!["--dream".into()],
}],
simulation: None,
}
}
#[test]
fn dream_file_roundtrip() {
let scene = sample_scene();
let dir = std::env::temp_dir().join("dreamwell_test_roundtrip");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test.dream");
write_dream_file(&path, &scene).unwrap();
let loaded = read_dream_file(&path).unwrap();
assert_eq!(scene.scene_id, loaded.scene_id);
assert_eq!(scene.name, loaded.name);
assert_eq!(scene.objects.len(), loaded.objects.len());
assert_eq!(scene.directional_lights.len(), loaded.directional_lights.len());
assert_eq!(scene.point_lights.len(), loaded.point_lights.len());
assert_eq!(scene.pois.len(), loaded.pois.len());
assert_eq!(scene.objects[0].id, loaded.objects[0].id);
assert_eq!(scene.objects[0].scale, loaded.objects[0].scale);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn dream_file_rejects_bad_magic() {
let mut data = vec![0u8; 64];
data[0..8].copy_from_slice(b"NOTDREAM");
let result = deserialize_dream_scene(&data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid magic bytes"));
}
#[test]
fn dream_file_rejects_bad_version() {
let scene = sample_scene();
let content = rmp_serde::to_vec(&scene).unwrap();
let hash = fnv1a_64(&content);
let mut data = Vec::new();
data.extend_from_slice(DREAM_MAGIC);
data.extend_from_slice(&99u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&(content.len() as u64).to_le_bytes());
data.extend_from_slice(&hash.to_le_bytes());
data.extend_from_slice(&content);
let result = deserialize_dream_scene(&data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unsupported version 99"));
}
#[test]
fn dream_file_rejects_bad_hash() {
let scene = sample_scene();
let content = rmp_serde::to_vec(&scene).unwrap();
let mut data = Vec::new();
data.extend_from_slice(DREAM_MAGIC);
data.extend_from_slice(&DREAM_VERSION.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&(content.len() as u64).to_le_bytes());
data.extend_from_slice(&0xDEADBEEFu64.to_le_bytes()); data.extend_from_slice(&content);
let result = deserialize_dream_scene(&data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("attestation hash mismatch"));
}
#[test]
fn dream_file_rejects_too_small() {
let result = deserialize_dream_scene(&[0u8; 16]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too small"));
}
#[test]
fn tapestry_roundtrip() {
let tapestry = sample_tapestry();
let dir = std::env::temp_dir().join("dreamwell_test_tapestry");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("tapestry.dream");
write_tapestry(&path, &tapestry).unwrap();
let loaded = read_tapestry(&path).unwrap();
assert_eq!(tapestry.project_name, loaded.project_name);
assert_eq!(tapestry.project_id, loaded.project_id);
assert_eq!(tapestry.scenes.len(), loaded.scenes.len());
assert_eq!(tapestry.profiles.len(), loaded.profiles.len());
assert_eq!(tapestry.starting_scene, loaded.starting_scene);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn scene_json_compile() {
let scene = sample_scene();
let json = serde_json::to_string_pretty(&scene).unwrap();
let compiled = compile_scene_json(&json).unwrap();
assert_eq!(compiled.scene_id, "test-scene");
assert_eq!(compiled.objects.len(), 1);
}
#[test]
fn tapestry_json_compile() {
let tapestry = sample_tapestry();
let json = serde_json::to_string_pretty(&tapestry).unwrap();
let compiled = compile_tapestry_json(&json).unwrap();
assert_eq!(compiled.project_id, "test-project");
assert_eq!(compiled.scenes.len(), 1);
}
#[test]
fn reality_check_valid_scene() {
let scene = sample_scene();
let (warnings, errors) = reality_check(&scene);
assert!(errors.is_empty(), "Unexpected errors: {errors:?}");
assert!(warnings.is_empty(), "Unexpected warnings: {warnings:?}");
}
#[test]
fn reality_check_catches_empty_id() {
let mut scene = sample_scene();
scene.scene_id = String::new();
let (_, errors) = reality_check(&scene);
assert!(errors.iter().any(|e| e.contains("scene_id is empty")));
}
#[test]
fn reality_check_catches_duplicate_object_ids() {
let mut scene = sample_scene();
scene.objects.push(scene.objects[0].clone());
let (_, errors) = reality_check(&scene);
assert!(errors.iter().any(|e| e.contains("duplicate object id")));
}
#[test]
fn reality_check_catches_bad_asset_ref() {
let mut scene = sample_scene();
scene.objects[0].asset_ref = Some("nonexistent".into());
let (_, errors) = reality_check(&scene);
assert!(errors.iter().any(|e| e.contains("unknown asset")));
}
#[test]
fn reality_check_catches_bad_topology() {
let mut scene = sample_scene();
scene.topology_layer = 15;
let (_, errors) = reality_check(&scene);
assert!(errors.iter().any(|e| e.contains("topology_layer 15 > 9")));
}
#[test]
fn default_feature_flags() {
let flags = FeatureFlags::default();
assert!(flags.quantum_culling);
assert!(flags.ssao);
assert!(flags.bloom);
assert!(!flags.rtx_gi);
assert!(!flags.rtx_shadows);
}
#[test]
fn default_physics() {
let phys = PhysicsDefaults::default();
assert_eq!(phys.gravity, [0.0, -9.81, 0.0]);
}
#[test]
fn compile_keynote_scene_from_project() {
let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("dreamwell-benchmark-project");
let scene_json_path = project_root.join("scenes").join("keynote").join("scene.json");
if !scene_json_path.exists() {
return;
}
let json_str = std::fs::read_to_string(&scene_json_path).unwrap();
let scene = compile_scene_json(&json_str).unwrap();
assert_eq!(scene.scene_id, "keynote");
assert_eq!(scene.name, "Dreamwell Keynote Benchmark");
assert_eq!(scene.topology_layer, 6); assert!(
scene.objects.len() >= 15,
"Expected 15+ objects, got {}",
scene.objects.len()
);
assert_eq!(scene.directional_lights.len(), 2);
assert_eq!(scene.point_lights.len(), 8);
assert!(scene.colliders.len() >= 5);
assert_eq!(scene.render_path, "Dreamwell");
assert!(scene.feature_flags.quantum_culling);
assert!(scene.feature_flags.bloom);
assert!(scene.feature_flags.dreammatter);
let (warnings, errors) = reality_check(&scene);
assert!(errors.is_empty(), "Keynote scene has errors: {errors:?}");
for w in &warnings {
eprintln!(" warning: {w}");
}
let dir = std::env::temp_dir().join("dreamwell_keynote_compile_test");
std::fs::create_dir_all(&dir).unwrap();
let dream_path = dir.join("keynote.dream");
write_dream_file(&dream_path, &scene).unwrap();
let reloaded = read_dream_file(&dream_path).unwrap();
assert_eq!(reloaded.scene_id, "keynote");
assert_eq!(reloaded.objects.len(), scene.objects.len());
assert_eq!(reloaded.directional_lights.len(), 2);
assert_eq!(reloaded.point_lights.len(), 8);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn compile_all_scenes_from_project() {
let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("dreamwell-benchmark-project");
let scenes_dir = project_root.join("scenes");
if !scenes_dir.exists() {
return;
}
let expected = [
"keynote",
"spawn_test",
"micro_dreamlet",
"avatar_demo",
"ray_tracing_demo",
"ray_tracing_gallery",
"stress_test",
];
let mut compiled = 0;
for scene_name in &expected {
let json_path = scenes_dir.join(scene_name).join("scene.json");
if !json_path.exists() {
continue;
}
let json_str = std::fs::read_to_string(&json_path)
.unwrap_or_else(|e| panic!("Failed to read {scene_name}/scene.json: {e}"));
let scene = compile_scene_json(&json_str).unwrap_or_else(|e| panic!("Failed to compile {scene_name}: {e}"));
assert_eq!(scene.scene_id, *scene_name, "scene_id mismatch for {scene_name}");
assert!(!scene.name.is_empty(), "empty name for {scene_name}");
let (warnings, errors) = reality_check(&scene);
assert!(errors.is_empty(), "{scene_name} reality check errors: {errors:?}");
let dir = std::env::temp_dir().join(format!("dreamwell_compile_{scene_name}"));
std::fs::create_dir_all(&dir).unwrap();
let dream_path = dir.join("scene.dream");
write_dream_file(&dream_path, &scene).unwrap();
let reloaded = read_dream_file(&dream_path).unwrap();
assert_eq!(reloaded.scene_id, scene.scene_id);
assert_eq!(reloaded.objects.len(), scene.objects.len());
std::fs::remove_dir_all(&dir).ok();
compiled += 1;
for w in &warnings {
eprintln!(" {scene_name} warning: {w}");
}
}
assert!(compiled >= 5, "Expected to compile at least 5 scenes, got {compiled}");
}
#[test]
fn compile_tapestry_from_project() {
let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("dreamwell-benchmark-project");
let tapestry_json_path = project_root.join("tapestry.json");
if !tapestry_json_path.exists() {
return;
}
let json_str = std::fs::read_to_string(&tapestry_json_path).unwrap();
let tapestry = compile_tapestry_json(&json_str).unwrap();
assert_eq!(tapestry.project_id, "dreamwell-benchmarks");
assert_eq!(tapestry.starting_scene, "keynote");
assert!(
tapestry.scenes.len() >= 6,
"Expected 6+ scenes, got {}",
tapestry.scenes.len()
);
assert!(
tapestry.profiles.len() >= 6,
"Expected 6+ profiles, got {}",
tapestry.profiles.len()
);
for profile in &tapestry.profiles {
assert!(
tapestry.scenes.iter().any(|s| s.scene_id == profile.scene_id),
"Profile '{}' references unknown scene '{}'",
profile.id,
profile.scene_id
);
}
let dir = std::env::temp_dir().join("dreamwell_tapestry_compile_test");
std::fs::create_dir_all(&dir).unwrap();
let dream_path = dir.join("tapestry.dream");
write_tapestry(&dream_path, &tapestry).unwrap();
let reloaded = read_tapestry(&dream_path).unwrap();
assert_eq!(reloaded.project_id, "dreamwell-benchmarks");
assert_eq!(reloaded.scenes.len(), tapestry.scenes.len());
std::fs::remove_dir_all(&dir).ok();
}
}