diff --git a/Cargo.lock b/Cargo.lock index 4fd8bee..81c8548 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,6 +1369,7 @@ dependencies = [ "bincode", "criterion", "euca-math", + "euca-reflect", "log", "rayon", "serde", @@ -1455,6 +1456,7 @@ dependencies = [ "euca-input", "euca-math", "euca-physics", + "euca-reflect", "euca-render", "euca-scene", "log", diff --git a/crates/euca-ecs/Cargo.toml b/crates/euca-ecs/Cargo.toml index d519943..aaf4df6 100644 --- a/crates/euca-ecs/Cargo.toml +++ b/crates/euca-ecs/Cargo.toml @@ -11,6 +11,7 @@ categories = ["game-development", "data-structures"] [dependencies] euca-math = { path = "../euca-math" } +euca-reflect = { path = "../euca-reflect" } log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/euca-ecs/src/lib.rs b/crates/euca-ecs/src/lib.rs index e6009d1..e4b806d 100644 --- a/crates/euca-ecs/src/lib.rs +++ b/crates/euca-ecs/src/lib.rs @@ -34,4 +34,4 @@ pub use shared::SharedWorld; pub use snapshot::{EntitySnapshot, WorldSnapshot}; pub use system::{AccessSystem, IntoSystem, LabeledSystem, System}; pub use system_param::{Res, ResMut, SystemAccess}; -pub use world::World; +pub use world::{ReflectComponentFns, World}; diff --git a/crates/euca-ecs/src/world.rs b/crates/euca-ecs/src/world.rs index 136f45a..67ab9cd 100644 --- a/crates/euca-ecs/src/world.rs +++ b/crates/euca-ecs/src/world.rs @@ -7,6 +7,7 @@ use crate::entity::{Entity, EntityAllocator}; use crate::event::Events; use crate::query::QueryCache; use crate::resource::Resources; +use euca_reflect::Reflect; #[derive(Clone, Copy, Debug)] pub(crate) struct EntityLocation { @@ -14,6 +15,23 @@ pub(crate) struct EntityLocation { pub row: usize, } +/// Type-erased accessor for getting/inserting a component via [`Reflect`]. +/// +/// Registered once per component type via [`World::register_reflect`]. Stores +/// function pointers that close over the concrete type, allowing the world to +/// read and write components without knowing `T` at the call site. +#[derive(Clone, Copy)] +pub struct ReflectComponentFns { + /// The `Reflect::type_name()` of this component. + pub type_name: &'static str, + /// The ECS component id for this type. + pub component_id: ComponentId, + /// Clone the component out of the world as a boxed `Reflect` value. + get_fn: fn(&World, Entity) -> Option>, + /// Insert a component from a boxed `Reflect` value. + insert_fn: fn(&mut World, Entity, Box), +} + /// The ECS world: owns all entities, components, archetypes, resources, and events. /// /// The `World` is the central data structure of the ECS. All entity and @@ -58,6 +76,8 @@ pub struct World { /// `Query::new_cached(&World)` can update the cache through a shared reference. /// `RwLock` (unlike `RefCell`) is `Sync`, required for `par_for_each`. pub(crate) query_cache: RwLock, + /// Reflection bridge: type-erased accessors keyed by `Reflect::type_name()`. + reflect_components: HashMap<&'static str, ReflectComponentFns>, } impl World { @@ -75,6 +95,7 @@ impl World { resources: Resources::new(), events: Events::new(), query_cache: crate::query::new_query_cache_lock(), + reflect_components: HashMap::new(), } } @@ -307,6 +328,17 @@ impl World { self.entities.alive_count() } + /// Returns all currently alive entities in the world. + /// + /// Collects entities from all archetypes. The order is deterministic + /// for a given world state but should not be relied upon. + pub fn all_entities(&self) -> Vec { + self.archetypes + .iter() + .flat_map(|arch| arch.entities.iter().copied()) + .collect() + } + /// Returns the number of distinct archetypes in the world. #[inline] pub fn archetype_count(&self) -> usize { @@ -450,6 +482,66 @@ impl World { self.events.update(); } + // ── Reflection bridge ── + + /// Register a component type for reflection-based (type-erased) access. + /// + /// After registration, [`get_reflect`](Self::get_reflect) and + /// [`insert_reflect`](Self::insert_reflect) can read/write this component + /// using only the type name string (no generic parameter needed). + /// + /// The type must implement [`Reflect`] and [`Clone`] so that values can be + /// extracted from the world without moving them. + pub fn register_reflect(&mut self) { + let comp_id = self.components.register::(); + let sample = T::default(); + let name = sample.type_name(); + self.reflect_components.insert( + name, + ReflectComponentFns { + type_name: name, + component_id: comp_id, + get_fn: |world, entity| world.get::(entity).map(|c| c.clone_reflect()), + insert_fn: |world, entity, val| { + if let Some(concrete) = val.as_any().downcast_ref::() { + world.insert(entity, concrete.clone()); + } + }, + }, + ); + } + + /// Iterate the type names of all reflection-registered component types. + pub fn reflect_component_names(&self) -> impl Iterator { + self.reflect_components.keys().copied() + } + + /// Get a component from an entity by type name, returning a cloned + /// boxed [`Reflect`] value. Returns `None` if the entity does not + /// have the component or the type name is not registered. + pub fn get_reflect(&self, entity: Entity, type_name: &str) -> Option> { + let fns = self.reflect_components.get(type_name)?; + (fns.get_fn)(self, entity) + } + + /// Insert a component into an entity by type name. The boxed value + /// must be the correct concrete type (matching what was registered). + /// + /// Returns `false` if the type name is not registered. + pub fn insert_reflect( + &mut self, + entity: Entity, + type_name: &str, + value: Box, + ) -> bool { + let fns = match self.reflect_components.get(type_name) { + Some(f) => *f, + None => return false, + }; + (fns.insert_fn)(self, entity, value); + true + } + // ── Internals ── fn locate(&self, entity: Entity) -> Option { diff --git a/crates/euca-editor/Cargo.toml b/crates/euca-editor/Cargo.toml index c0ac978..19c11f5 100644 --- a/crates/euca-editor/Cargo.toml +++ b/crates/euca-editor/Cargo.toml @@ -9,7 +9,7 @@ categories = ["game-development"] [dependencies] euca-ecs = { path = "../euca-ecs" } euca-math = { path = "../euca-math" } -euca-reflect = { path = "../euca-reflect" } +euca-reflect = { path = "../euca-reflect", features = ["json"] } euca-scene = { path = "../euca-scene" } euca-core = { path = "../euca-core" } euca-render = { path = "../euca-render" } diff --git a/crates/euca-editor/src/lib.rs b/crates/euca-editor/src/lib.rs index c3df1a0..4ffc478 100644 --- a/crates/euca-editor/src/lib.rs +++ b/crates/euca-editor/src/lib.rs @@ -9,7 +9,8 @@ pub use panels::{ hierarchy_panel, inspector_panel, terrain_panel, toolbar_panel, }; pub use scene_file::{ - PrefabRegistry, SCENE_VERSION, SceneEntity, SceneFile, load_scene_into_world, + PrefabRegistry, ReflectSceneEntity, SCENE_VERSION, SceneEntity, SceneFile, SceneFileV3, + load_scene_into_world, load_scene_v3_into_world, }; pub use undo::UndoHistory; diff --git a/crates/euca-editor/src/scene_file.rs b/crates/euca-editor/src/scene_file.rs index 28c64d0..5cf3275 100644 --- a/crates/euca-editor/src/scene_file.rs +++ b/crates/euca-editor/src/scene_file.rs @@ -1,4 +1,8 @@ +use std::collections::HashMap; + use euca_ecs::{Entity, Query, World}; +use euca_reflect::TypeRegistry; +use euca_reflect::json::{reflect_from_json, reflect_to_json}; use euca_render::{MaterialRef, MeshRenderer}; use euca_scene::{GlobalTransform, LocalTransform}; use serde::{Deserialize, Serialize}; @@ -105,7 +109,23 @@ impl SceneFile { } /// Deserialize from JSON with automatic version migration. + /// + /// Detects the format version: + /// - v1/v2: parsed as [`SceneFile`] (legacy hardcoded components) + /// - v3+: parsed as [`SceneFileV3`] (reflection-based), **not** returned + /// here. Use [`SceneFileV3::from_json`] directly for v3 scenes. + /// + /// Returns `Err` if the JSON is v3+ (callers should use `SceneFileV3::from_json`). pub fn from_json(json: &str) -> Result { + // Peek at version to reject v3+ scenes that should use SceneFileV3. + if let Ok(v) = serde_json::from_str::(json) + && v.version >= 3 + { + return Err(format!( + "Scene version {} requires SceneFileV3::from_json", + v.version + )); + } let mut scene: SceneFile = serde_json::from_str(json).map_err(|e| format!("Scene deserialization failed: {e}"))?; scene.migrate(); @@ -258,6 +278,134 @@ impl PrefabRegistry { } } +// ── V3 Scene Format (reflection-driven) ── + +/// Helper for peeking at the `version` field without fully parsing. +#[derive(Deserialize)] +struct VersionProbe { + #[serde(default = "default_version")] + version: u32, +} + +/// V3 entity: stores components as `type_name -> JSON object`. +/// +/// Each value is the output of [`reflect_to_json`], which includes a +/// `__type` discriminator for struct-typed components. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ReflectSceneEntity { + pub components: HashMap, +} + +/// Reflection-based scene file (v3+). +/// +/// Unlike the legacy [`SceneFile`] which hardcodes a fixed set of +/// components, `SceneFileV3` serializes *any* component that has been +/// registered for reflection via [`World::register_reflect`]. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SceneFileV3 { + /// Format version. Always `3` for this format. + pub version: u32, + /// Entities and their reflected components. + pub entities: Vec, +} + +impl SceneFileV3 { + /// Capture the current world state using reflection. + /// + /// For each alive entity, iterates all reflection-registered component + /// names and serializes whichever ones the entity actually has. + /// Components that are registered but absent on a given entity are + /// silently skipped. + pub fn capture(world: &World) -> Self { + let entities = world + .all_entities() + .into_iter() + .filter_map(|entity| { + let mut components = HashMap::new(); + for name in world.reflect_component_names() { + if let Some(val) = world.get_reflect(entity, name) + && let Ok(json) = reflect_to_json(val.as_ref()) + { + components.insert(name.to_string(), json); + } + } + // Only include entities that have at least one reflected component. + if components.is_empty() { + None + } else { + Some(ReflectSceneEntity { components }) + } + }) + .collect(); + + Self { + version: 3, + entities, + } + } + + /// Serialize to pretty JSON. + pub fn to_json(&self) -> String { + serde_json::to_string_pretty(self).expect("Scene V3 serialization failed") + } + + /// Deserialize a v3 scene from JSON. + pub fn from_json(json: &str) -> Result { + let scene: SceneFileV3 = serde_json::from_str(json) + .map_err(|e| format!("Scene V3 deserialization failed: {e}"))?; + if scene.version < 3 { + return Err(format!( + "Expected scene version >= 3, got {}. Use SceneFile::from_json for v1/v2.", + scene.version + )); + } + Ok(scene) + } +} + +/// Load a v3 scene into the world using reflection. +/// +/// For each entity in the scene, spawns a new empty entity and inserts +/// components via [`World::insert_reflect`]. Components whose `__type` +/// is not found in the `type_registry` are skipped with a log warning. +/// +/// Returns the list of spawned entities. +pub fn load_scene_v3_into_world( + world: &mut World, + scene: &SceneFileV3, + type_registry: &TypeRegistry, +) -> Vec { + let mut spawned = Vec::new(); + for re in &scene.entities { + let entity = world.spawn_empty(); + for json_val in re.components.values() { + // The __type field inside the JSON object tells us the type name. + let type_name = match json_val.get("__type").and_then(|v| v.as_str()) { + Some(tn) => tn, + None => { + // Primitive component stored without __type — skip. + log::info!("Skipping component without __type field"); + continue; + } + }; + match reflect_from_json(json_val, type_registry) { + Some(val) => { + if !world.insert_reflect(entity, type_name, val) { + log::info!( + "Skipping unregistered reflect component '{type_name}' during scene load" + ); + } + } + None => { + log::info!("Failed to deserialize component '{type_name}' from scene JSON"); + } + } + } + spawned.push(entity); + } + spawned +} + #[cfg(test)] mod tests { use super::*; @@ -319,4 +467,171 @@ mod tests { assert!(registry.prefabs.contains_key("soldier")); assert_eq!(registry.prefabs["soldier"].entities.len(), 1); } + + // ── V3 reflection-driven tests ── + + /// Test component with Reflect derive. + #[derive(Clone, Debug, Default, PartialEq, euca_reflect::Reflect)] + struct TestHealth { + current: f32, + max: f32, + } + + /// Test tuple-struct component with Reflect derive. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, euca_reflect::Reflect)] + struct TestTeam(u8); + + #[test] + fn v3_capture_and_load_roundtrip() { + let mut world = World::new(); + let mut type_reg = TypeRegistry::new(); + + // Register types for both the ECS reflection bridge and the type registry. + world.register_reflect::(); + world.register_reflect::(); + type_reg.register::(); + type_reg.register::(); + + // Spawn an entity with both components. + let e = world.spawn(TestHealth { + current: 80.0, + max: 100.0, + }); + world.insert(e, TestTeam(2)); + + // Capture. + let scene = SceneFileV3::capture(&world); + assert_eq!(scene.version, 3); + assert_eq!(scene.entities.len(), 1); + assert!(scene.entities[0].components.contains_key("TestHealth")); + assert!(scene.entities[0].components.contains_key("TestTeam")); + + // Serialize and deserialize. + let json = scene.to_json(); + let restored = SceneFileV3::from_json(&json).unwrap(); + assert_eq!(restored.entities.len(), 1); + + // Load into a fresh world. + let mut world2 = World::new(); + world2.register_reflect::(); + world2.register_reflect::(); + let spawned = load_scene_v3_into_world(&mut world2, &restored, &type_reg); + assert_eq!(spawned.len(), 1); + + let loaded_health = world2 + .get_reflect(spawned[0], "TestHealth") + .expect("TestHealth should exist"); + let h = loaded_health.as_any().downcast_ref::().unwrap(); + assert_eq!(h.current, 80.0); + assert_eq!(h.max, 100.0); + + let loaded_team = world2 + .get_reflect(spawned[0], "TestTeam") + .expect("TestTeam should exist"); + let t = loaded_team.as_any().downcast_ref::().unwrap(); + assert_eq!(t.0, 2); + } + + #[test] + fn v2_scene_still_loads() { + // V2 JSON should parse normally. + let v2_json = r#"{"version": 2, "entities": [{"position": [1, 2, 3], "scale": [1, 1, 1], "rotation": [0, 0, 0, 1], "mesh": "cube", "material": 0}]}"#; + let scene = SceneFile::from_json(v2_json).unwrap(); + assert_eq!(scene.version, SCENE_VERSION); + assert_eq!(scene.entities.len(), 1); + } + + #[test] + fn v3_json_rejected_by_v2_parser() { + let v3_json = r#"{"version": 3, "entities": []}"#; + assert!(SceneFile::from_json(v3_json).is_err()); + } + + #[test] + fn v3_multiple_components_on_same_entity() { + #[derive(Clone, Debug, Default, PartialEq, euca_reflect::Reflect)] + struct Score { + value: f32, + } + + let mut world = World::new(); + let mut type_reg = TypeRegistry::new(); + + world.register_reflect::(); + world.register_reflect::(); + world.register_reflect::(); + type_reg.register::(); + type_reg.register::(); + type_reg.register::(); + + let e = world.spawn(TestHealth { + current: 50.0, + max: 50.0, + }); + world.insert(e, TestTeam(1)); + world.insert(e, Score { value: 42.0 }); + + let scene = SceneFileV3::capture(&world); + assert_eq!(scene.entities[0].components.len(), 3); + + let json = scene.to_json(); + let restored = SceneFileV3::from_json(&json).unwrap(); + + let mut world2 = World::new(); + world2.register_reflect::(); + world2.register_reflect::(); + world2.register_reflect::(); + let spawned = load_scene_v3_into_world(&mut world2, &restored, &type_reg); + + let s = world2 + .get_reflect(spawned[0], "Score") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + assert_eq!(s.value, 42.0); + } + + #[test] + fn unregistered_components_skipped_in_capture() { + #[derive(Clone, Debug, Default)] + struct Invisible { + _data: f32, + } + + let mut world = World::new(); + let type_reg = TypeRegistry::new(); + + world.register_reflect::(); + + let e = world.spawn(TestHealth { + current: 10.0, + max: 10.0, + }); + // Invisible is not registered for reflection. + world.insert(e, Invisible { _data: 999.0 }); + + let scene = SceneFileV3::capture(&world); + assert_eq!(scene.entities.len(), 1); + // Only TestHealth should be captured. + assert!(scene.entities[0].components.contains_key("TestHealth")); + assert_eq!(scene.entities[0].components.len(), 1); + } + + #[test] + fn get_reflect_returns_none_without_component() { + let mut world = World::new(); + world.register_reflect::(); + let e = world.spawn_empty(); + assert!(world.get_reflect(e, "TestHealth").is_none()); + } + + #[test] + fn insert_reflect_returns_false_for_unregistered() { + let mut world = World::new(); + let e = world.spawn_empty(); + let val: Box = Box::new(42_f32); + assert!(!world.insert_reflect(e, "NotRegistered", val)); + } } diff --git a/crates/euca-gameplay/Cargo.toml b/crates/euca-gameplay/Cargo.toml index 22c99ac..4359e05 100644 --- a/crates/euca-gameplay/Cargo.toml +++ b/crates/euca-gameplay/Cargo.toml @@ -12,6 +12,7 @@ euca-ecs = { path = "../euca-ecs" } euca-input = { path = "../euca-input" } euca-math = { path = "../euca-math" } euca-physics = { path = "../euca-physics" } +euca-reflect = { path = "../euca-reflect" } euca-render = { path = "../euca-render" } euca-scene = { path = "../euca-scene" } euca-ai = { path = "../euca-ai" } diff --git a/crates/euca-gameplay/src/health.rs b/crates/euca-gameplay/src/health.rs index 149e487..055a6da 100644 --- a/crates/euca-gameplay/src/health.rs +++ b/crates/euca-gameplay/src/health.rs @@ -5,6 +5,7 @@ //! Systems: `apply_damage_system`, `death_check_system`. use euca_ecs::{Entity, Events, Query, World}; +use euca_reflect::Reflect; use crate::combat_math::{self, DamageType}; use crate::stats::DamageResistance; @@ -13,7 +14,7 @@ use crate::stats::DamageResistance; /// /// Damage pipeline: `DamageEvent` -> `apply_damage_system` reduces `current` /// -> `death_check_system` adds `Dead` marker + emits `DeathEvent` when `current <= 0`. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Reflect)] pub struct Health { /// Current hit points (clamped to `[0, max]` by systems). pub current: f32, @@ -21,6 +22,15 @@ pub struct Health { pub max: f32, } +impl Default for Health { + fn default() -> Self { + Self { + current: 100.0, + max: 100.0, + } + } +} + impl Health { /// Create a fully healed entity with the given maximum HP. pub fn new(max: f32) -> Self { diff --git a/crates/euca-gameplay/src/teams.rs b/crates/euca-gameplay/src/teams.rs index 83ed0f5..b022869 100644 --- a/crates/euca-gameplay/src/teams.rs +++ b/crates/euca-gameplay/src/teams.rs @@ -5,6 +5,7 @@ use euca_ecs::{Entity, Events, Query, World}; use euca_math::Vec3; +use euca_reflect::Reflect; use euca_scene::LocalTransform; use crate::cleanup::CorpseTimer; @@ -12,7 +13,7 @@ use crate::combat::{CurrentTarget, EntityRole}; use crate::health::{Dead, DeathEvent, Health, LastAttacker}; /// Which team this entity belongs to. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)] pub struct Team(pub u8); /// Marks an entity as a spawn location for a specific team. diff --git a/crates/euca-reflect/src/lib.rs b/crates/euca-reflect/src/lib.rs index dd6c89b..7c507b1 100644 --- a/crates/euca-reflect/src/lib.rs +++ b/crates/euca-reflect/src/lib.rs @@ -104,6 +104,8 @@ impl_reflect_primitive!(i32, "i32"); impl_reflect_primitive!(u32, "u32"); impl_reflect_primitive!(i64, "i64"); impl_reflect_primitive!(u64, "u64"); +impl_reflect_primitive!(u8, "u8"); +impl_reflect_primitive!(u16, "u16"); impl_reflect_primitive!(bool, "bool"); impl_reflect_primitive!(String, "String"); @@ -123,6 +125,8 @@ pub mod json { "u32" => Ok(any.downcast_ref::().copied().unwrap().into()), "i64" => Ok(any.downcast_ref::().copied().unwrap().into()), "u64" => Ok(any.downcast_ref::().copied().unwrap().into()), + "u8" => Ok(any.downcast_ref::().copied().unwrap().into()), + "u16" => Ok(any.downcast_ref::().copied().unwrap().into()), "bool" => Ok(any.downcast_ref::().copied().unwrap().into()), "String" => Ok(any.downcast_ref::().cloned().unwrap().into()), other => Err(format!("unsupported reflect type: {other}")), @@ -153,6 +157,12 @@ pub mod json { Ok(serde_json::Value::Object(map)) } } + /// Deserialize a [`Reflect`] value from a JSON object. + /// + /// The JSON must contain a `__type` field matching a registered type name. + /// Remaining fields are deserialized and applied via [`Reflect::set_field`]. + /// Field type hints from the default instance guide numeric coercion so + /// that `u8`, `u16`, `i32` etc. are restored with the correct concrete type. pub fn reflect_from_json( json: &serde_json::Value, registry: &TypeRegistry, @@ -164,29 +174,49 @@ pub mod json { if k == "__type" { continue; } - if let Some(fv) = json_to_val(v, registry) { + // Use field type info from the default instance to guide + // numeric coercion so that u8/u16/i32/etc. round-trip correctly. + let type_hint = inst.field_ref(k).map(|f| f.type_name()); + if let Some(fv) = json_to_val_hinted(v, registry, type_hint) { inst.set_field(k, fv.as_ref()); } } Some(inst) } - fn json_to_val(v: &serde_json::Value, reg: &TypeRegistry) -> Option> { + + /// Convert a JSON value to a boxed [`Reflect`] value. + /// + /// When `type_hint` is provided (from the target field's `type_name()`), + /// numeric values are coerced to the exact primitive type expected. + fn json_to_val_hinted( + v: &serde_json::Value, + reg: &TypeRegistry, + type_hint: Option<&str>, + ) -> Option> { match v { - serde_json::Value::Number(n) => { - if let Some(f) = n.as_f64() { - Some(Box::new(f as f32)) - } else if let Some(i) = n.as_i64() { - Some(Box::new(i)) - } else { - n.as_u64().map(|u| Box::new(u) as Box) - } - } + serde_json::Value::Number(n) => coerce_number(n, type_hint), serde_json::Value::Bool(b) => Some(Box::new(*b)), serde_json::Value::String(s) => Some(Box::new(s.clone())), serde_json::Value::Object(_) => reflect_from_json(v, reg), _ => None, } } + + /// Coerce a JSON number to the concrete numeric [`Reflect`] type + /// indicated by `type_hint`. Falls back to `f32` when no hint is given. + fn coerce_number(n: &serde_json::Number, type_hint: Option<&str>) -> Option> { + match type_hint { + Some("u8") => Some(Box::new(n.as_u64()? as u8)), + Some("u16") => Some(Box::new(n.as_u64()? as u16)), + Some("u32") => Some(Box::new(n.as_u64()? as u32)), + Some("u64") => Some(Box::new(n.as_u64()?)), + Some("i32") => Some(Box::new(n.as_i64()? as i32)), + Some("i64") => Some(Box::new(n.as_i64()?)), + Some("f64") => Some(Box::new(n.as_f64()?)), + // Unknown type hint or no hint: default to f32. + _ => Some(Box::new(n.as_f64()? as f32)), + } + } } #[cfg(test)] diff --git a/crates/euca-render/src/renderer.rs b/crates/euca-render/src/renderer.rs index b2fe66d..b007225 100644 --- a/crates/euca-render/src/renderer.rs +++ b/crates/euca-render/src/renderer.rs @@ -2316,7 +2316,11 @@ impl Renderer { self.metalfx_low_res_depth_view.as_ref().unwrap(), ) } else { - (&self.msaa_hdr_view, Some(resolve_target), &self.depth_texture) + ( + &self.msaa_hdr_view, + Some(resolve_target), + &self.depth_texture, + ) }; { @@ -2540,43 +2544,43 @@ impl Renderer { &self.metalfx_output, ) { - let (jitter_x, jitter_y) = (camera.jitter[0], camera.jitter[1]); - rhi.encode_metalfx_upscale( - encoder, - upscaler.as_ref(), - low_color, - low_depth, - &self.velocity_textures.velocity_texture, - output_tex, - jitter_x, - jitter_y, - self.metalfx_reset_history, - ); - self.metalfx_reset_history = false; + let (jitter_x, jitter_y) = (camera.jitter[0], camera.jitter[1]); + rhi.encode_metalfx_upscale( + encoder, + upscaler.as_ref(), + low_color, + low_depth, + &self.velocity_textures.velocity_texture, + output_tex, + jitter_x, + jitter_y, + self.metalfx_reset_history, + ); + self.metalfx_reset_history = false; - // Blit MetalFX output into the post-process ping buffer so - // downstream passes (TAA, motion blur, DoF) read the upscaled image. - let (sw, sh) = rhi.surface_size(); - rhi.copy_texture_to_texture( - encoder, - &euca_rhi::TexelCopyTextureInfo { - texture: output_tex, - mip_level: 0, - origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 }, - aspect: euca_rhi::TextureAspect::All, - }, - &euca_rhi::TexelCopyTextureInfo { - texture: self.post_process_stack.ping_texture(), - mip_level: 0, - origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 }, - aspect: euca_rhi::TextureAspect::All, - }, - euca_rhi::Extent3d { - width: sw, - height: sh, - depth_or_array_layers: 1, - }, - ); + // Blit MetalFX output into the post-process ping buffer so + // downstream passes (TAA, motion blur, DoF) read the upscaled image. + let (sw, sh) = rhi.surface_size(); + rhi.copy_texture_to_texture( + encoder, + &euca_rhi::TexelCopyTextureInfo { + texture: output_tex, + mip_level: 0, + origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 }, + aspect: euca_rhi::TextureAspect::All, + }, + &euca_rhi::TexelCopyTextureInfo { + texture: self.post_process_stack.ping_texture(), + mip_level: 0, + origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 }, + aspect: euca_rhi::TextureAspect::All, + }, + euca_rhi::Extent3d { + width: sw, + height: sh, + depth_or_array_layers: 1, + }, + ); } // GPU compute particles: update (compute dispatch) then draw (render pass). @@ -2985,8 +2989,7 @@ impl Renderer { | euca_rhi::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); - let output_view = - rhi.create_texture_view(&output, &euca_rhi::TextureViewDesc::default()); + let output_view = rhi.create_texture_view(&output, &euca_rhi::TextureViewDesc::default()); // Create the MetalFX temporal scaler (panics on unsupported hardware). let upscaler = rhi.create_temporal_upscaler( diff --git a/crates/euca-rhi/src/metal_backend.rs b/crates/euca-rhi/src/metal_backend.rs index c10a7cf..512bfa3 100644 --- a/crates/euca-rhi/src/metal_backend.rs +++ b/crates/euca-rhi/src/metal_backend.rs @@ -1584,7 +1584,9 @@ impl RenderDevice for MetalDevice { reset: bool, ) { if let Some(scaler) = upscaler.downcast_ref::() { - scaler.encode(encoder, color, depth, motion, output, jitter_x, jitter_y, reset); + scaler.encode( + encoder, color, depth, motion, output, jitter_x, jitter_y, reset, + ); } else { log::warn!("encode_metalfx_upscale: upscaler is not a MetalFXUpscaler — skipping"); }