From 3c73e1d7e27643bca6ae2891344906bfc4229b42 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 18 Apr 2024 22:38:15 -0400 Subject: [PATCH 1/8] render: only run system_update_world_transforms for scenes that were modified --- lyra-game/src/render/renderer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 9506e7e..5fbcfdd 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -466,8 +466,10 @@ impl Renderer for BasicRenderer { if let Some((scene_han, scene_epoch)) = scene_pair { if let Some(scene) = scene_han.data_ref() { - let view = scene.world().view::<(Entities, &mut WorldTransform, &Transform, Not>>)>(); - lyra_scene::system_update_world_transforms(scene.world(), view).unwrap(); + if scene_epoch == last_epoch { + let view = scene.world().view::<(Entities, &mut WorldTransform, &Transform, Not>>)>(); + lyra_scene::system_update_world_transforms(scene.world(), view).unwrap(); + } let interpo_pos = self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); From 246705b80b4459fdc1c2309574cb3d975b4176c0 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 19 Apr 2024 23:37:08 -0400 Subject: [PATCH 2/8] game: some profiling improvements --- Cargo.lock | 139 ++++++++++++++++++ lyra-game/Cargo.toml | 8 +- lyra-game/src/game.rs | 27 ++-- lyra-game/src/render/light_cull_compute.rs | 7 +- lyra-game/src/render/renderer.rs | 11 +- .../src/render/transform_buffer_storage.rs | 5 + 6 files changed, 183 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eaf2077..6cc0899 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -1087,6 +1096,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1747,6 +1769,19 @@ dependencies = [ "value-bag", ] +[[package]] +name = "loom" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e045d70ddfbc984eacfa964ded019534e8f6cbf36f6410aee0ed5cefa5a9175" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lyra-ecs" version = "0.1.0" @@ -1804,6 +1839,7 @@ dependencies = [ "tracing-appender", "tracing-log 0.1.4", "tracing-subscriber", + "tracing-tracy", "uuid", "wgpu", "winit", @@ -1914,6 +1950,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.1" @@ -2655,6 +2700,50 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -2758,6 +2847,12 @@ dependencies = [ "base64 0.21.5", ] +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + [[package]] name = "ryu" version = "1.0.16" @@ -3355,14 +3450,49 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log 0.2.0", ] +[[package]] +name = "tracing-tracy" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6024d04f84a69fd0d1dc1eee3a2b070bd246530a0582f9982ae487cb6c703614" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracy-client", +] + +[[package]] +name = "tracy-client" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fb931a64ff88984f86d3e9bcd1ae8843aa7fe44dd0f8097527bc172351741d" +dependencies = [ + "loom", + "once_cell", + "tracy-client-sys", +] + +[[package]] +name = "tracy-client-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d104d610dfa9dd154535102cc9c6164ae1fa37842bc2d9e83f9ac82b0ae0882" +dependencies = [ + "cc", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -3803,6 +3933,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.52.0" diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index f270e09..ca45d9b 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -11,11 +11,14 @@ lyra-math = { path = "../lyra-math" } lyra-scene = { path = "../lyra-scene" } winit = "0.28.1" +wgpu = "0.15.1" + tracing = "0.1.37" tracing-subscriber = { version = "0.3.16", features = [ "tracing-log" ] } tracing-log = "0.1.3" tracing-appender = "0.2.2" -wgpu = "0.15.1" +tracing-tracy = { version = "0.11.0", optional = true } + async-std = { version = "1.12.0", features = [ "unstable", "attributes" ] } cfg-if = "1" bytemuck = { version = "1.12", features = [ "derive" ] } @@ -30,3 +33,6 @@ quote = "1.0.29" uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } itertools = "0.11.0" thiserror = "1.0.56" + +[features] +tracy = ["dep:tracing-tracy"] \ No newline at end of file diff --git a/lyra-game/src/game.rs b/lyra-game/src/game.rs index 32f7cbd..d9e036e 100755 --- a/lyra-game/src/game.rs +++ b/lyra-game/src/game.rs @@ -3,7 +3,7 @@ use std::{sync::Arc, collections::VecDeque, ptr::NonNull}; use async_std::task::block_on; use lyra_ecs::{World, system::{System, IntoSystem}}; -use tracing::{info, error, Level}; +use tracing::{error, info, Level}; use tracing_appender::non_blocking; use tracing_subscriber::{ layer::SubscriberExt, @@ -344,15 +344,22 @@ impl Game { pub async fn run(&mut self) { // init logging let (stdout_layer, _stdout_nb) = non_blocking(std::io::stdout()); - tracing_subscriber::registry() - .with(fmt::layer().with_writer(stdout_layer)) - .with(filter::Targets::new() - // done by prefix, so it includes all lyra subpackages - .with_target("lyra", Level::DEBUG) - .with_target("wgpu", Level::WARN) - .with_target("winit", Level::DEBUG) - .with_default(Level::INFO)) - .init(); + { + let t = tracing_subscriber::registry() + .with(fmt::layer().with_writer(stdout_layer)); + + #[cfg(feature = "tracy")] + t.with(tracing_tracy::TracyLayer::default()); + + t.with(filter::Targets::new() + // done by prefix, so it includes all lyra subpackages + .with_target("lyra", Level::DEBUG) + .with_target("wgpu", Level::WARN) + .with_target("winit", Level::DEBUG) + .with_default(Level::INFO)) + .init(); + } + let world = self.world.take().unwrap_or_default(); diff --git a/lyra-game/src/render/light_cull_compute.rs b/lyra-game/src/render/light_cull_compute.rs index 66fd876..f8a6a95 100644 --- a/lyra-game/src/render/light_cull_compute.rs +++ b/lyra-game/src/render/light_cull_compute.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, mem, rc::Rc}; use glam::UVec2; +use tracing::instrument; use wgpu::{util::DeviceExt, ComputePipeline}; use winit::dpi::PhysicalSize; @@ -202,6 +203,7 @@ impl LightCullCompute { } } + #[instrument(skip(self))] pub fn update_screen_size(&mut self, size: PhysicalSize) { self.screen_size_buffer.write_buffer(&self.queue, 0, &[UVec2::new(size.width, size.height)]); @@ -212,7 +214,9 @@ impl LightCullCompute { self.light_indices_grid = Self::create_grid(&self.device, self.workgroup_size); } + #[instrument(skip(self, camera_buffers, lights_buffers, depth_texture))] pub fn compute(&mut self, camera_buffers: &BufferWrapper, lights_buffers: &LightUniformBuffers, depth_texture: &RenderTexture) { + self.cleanup(); let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("LightCullCompute"), }); @@ -234,8 +238,7 @@ impl LightCullCompute { } self.queue.submit(std::iter::once(encoder.finish())); - self.device.poll(wgpu::Maintain::Wait); - self.cleanup(); + //self.device.poll(wgpu::Maintain::Wait); } pub fn cleanup(&mut self) { diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 5fbcfdd..ff79b1a 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -12,7 +12,7 @@ use lyra_ecs::{Entity, Tick}; use lyra_ecs::query::{Entities, TickOf}; use lyra_ecs::World; use lyra_scene::{SceneGraph, WorldTransform}; -use tracing::{debug, warn}; +use tracing::{debug, instrument, warn}; use uuid::Uuid; use wgpu::{BindGroupLayout, Limits}; use wgpu::util::DeviceExt; @@ -113,6 +113,7 @@ pub struct BasicRenderer { } impl BasicRenderer { + #[instrument(skip(window))] pub async fn create_with_window(window: Arc) -> BasicRenderer { let size = window.inner_size(); @@ -260,6 +261,7 @@ impl BasicRenderer { } /// Checks if the mesh buffers in the GPU need to be updated. + #[instrument(skip(self, _entity, meshh))] fn check_mesh_buffers(&mut self, _entity: Entity, meshh: &ResHandle) { let mesh_uuid = meshh.uuid(); @@ -299,6 +301,7 @@ impl BasicRenderer { } } + #[instrument(skip(self, mesh))] fn create_vertex_index_buffers(&mut self, mesh: &Mesh) -> (BufferStorage, Option<(wgpu::IndexFormat, BufferStorage)>) { let positions = mesh.position().unwrap(); let tex_coords: Vec = mesh.tex_coords().cloned() @@ -348,6 +351,7 @@ impl BasicRenderer { ( vertex_buffer, indices ) } + #[instrument(skip(self, mesh))] fn create_mesh_buffers(&mut self, mesh: &Mesh) -> MeshBufferStorage { let (vertex_buffer, buffer_indices) = self.create_vertex_index_buffers(mesh); @@ -374,6 +378,7 @@ impl BasicRenderer { } /// Processes the mesh for the renderer, storing and creating buffers as needed. Returns true if a new mesh was processed. + #[instrument(skip(self, transform, mesh, entity))] fn process_mesh(&mut self, entity: Entity, transform: Transform, mesh: &Mesh, mesh_uuid: Uuid) -> bool { let _ = transform; /* if self.transform_buffers.should_expand() { @@ -394,6 +399,7 @@ impl BasicRenderer { } else { false } } + #[instrument(skip(self, now, transform, entity))] fn interpolate_transforms(&mut self, now: Instant, last_epoch: Tick, entity: Entity, transform: &Transform, transform_epoch: Tick) -> Transform { let cached = match self.entity_last_transforms.get_mut(&entity) { Some(last) if transform_epoch == last_epoch => { @@ -429,6 +435,7 @@ impl BasicRenderer { } impl Renderer for BasicRenderer { + #[instrument(skip(self, main_world))] fn prepare(&mut self, main_world: &mut World) { let last_epoch = main_world.current_tick(); let now_inst = Instant::now(); @@ -526,6 +533,7 @@ impl Renderer for BasicRenderer { self.light_buffers.update_lights(&self.queue, last_epoch, main_world); } + #[instrument(skip(self))] fn render(&mut self) -> Result<(), wgpu::SurfaceError> { let output = self.surface.get_current_texture()?; let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); @@ -626,6 +634,7 @@ impl Renderer for BasicRenderer { Ok(()) } + #[instrument(skip(self))] fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize) { if new_size.width > 0 && new_size.height > 0 { self.size = new_size; diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index 9044715..bd9e85d 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -1,6 +1,7 @@ use std::{collections::{HashMap, VecDeque}, hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}, num::NonZeroU64}; use lyra_ecs::Entity; +use tracing::instrument; use uuid::Uuid; use wgpu::Limits; @@ -212,6 +213,7 @@ impl TransformBuffers { /// /// # Panics /// Panics if the `entity_group` is not already inside of the buffers. + #[instrument(skip(self, queue, limits, entity_group, transform, normal_matrix))] pub fn update_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { let index = *self.groups.get(entity_group.into()) .expect("Use 'push_transform' for new entities"); @@ -228,6 +230,7 @@ impl TransformBuffers { } /// Push a new transform into the buffers. + #[instrument(skip(self, queue, limits, entity_group, transform, normal_matrix))] pub fn push_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { self.groups.insert(entity_group.into(), || { // this closure is only called when there are no values that can be reused, @@ -264,6 +267,7 @@ impl TransformBuffers { /// Update an existing transform group or if its not existing yet, pushes it to the buffer. /// /// Returns: the index that the transform is at in the buffers. + #[instrument(skip(self, queue, limits, group, transform_fn))] pub fn update_or_push(&mut self, queue: &wgpu::Queue, limits: &Limits, group: TransformGroup, transform_fn: F) -> TransformIndex where F: Fn() -> (glam::Mat4, glam::Mat3) { @@ -279,6 +283,7 @@ impl TransformBuffers { /// /// This object has a chain of uniform buffers, when the buffers are expanded, a new /// "chain-link" is created. + #[instrument(skip(self, device))] pub fn expand_buffers(&mut self, device: &wgpu::Device) { let limits = device.limits(); let max_buffer_sizes = self.max_transform_count as u64 * limits.min_uniform_buffer_offset_alignment as u64; From 24e1c0281e07d005fa70e659b4af9f44ecb36f02 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sat, 20 Apr 2024 00:08:25 -0400 Subject: [PATCH 3/8] Make tracy profiling an optional feature, create 'many-lights' example --- Cargo.lock | 21 ++ Cargo.toml | 3 +- examples/many-lights/Cargo.toml | 19 ++ examples/many-lights/src/main.rs | 189 ++++++++++++++++++ lyra-game/src/game.rs | 2 +- lyra-game/src/render/renderer.rs | 8 + .../src/render/transform_buffer_storage.rs | 7 + lyra-game/src/stage.rs | 8 +- 8 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 examples/many-lights/Cargo.toml create mode 100644 examples/many-lights/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 6cc0899..e5d9d61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -996,6 +996,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fps_counter" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff23a4d90ba4b859f370ee3c12ca3b1ca80d8ee144b279e135f6852cdadd6dd6" +dependencies = [ + "instant", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1950,6 +1959,18 @@ dependencies = [ "libc", ] +[[package]] +name = "many-lights" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "fps_counter", + "lyra-engine", + "rand 0.8.5", + "tracing", +] + [[package]] name = "matchers" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4091b3d..f285537 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,12 @@ members = [ "lyra-ecs", "lyra-reflect", "lyra-scripting", - "lyra-game", "lyra-math", "lyra-scene"] + "lyra-game", "lyra-math", "lyra-scene", "examples/many-lights"] [features] scripting = ["dep:lyra-scripting"] lua_scripting = ["scripting", "lyra-scripting/lua"] +tracy = ["lyra-game/tracy"] [dependencies] lyra-game = { path = "lyra-game" } diff --git a/examples/many-lights/Cargo.toml b/examples/many-lights/Cargo.toml new file mode 100644 index 0000000..e8e5536 --- /dev/null +++ b/examples/many-lights/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "many-lights" +version = "0.1.0" +edition = "2021" + +[dependencies] +lyra-engine = { path = "../../", features = ["tracy"] } +anyhow = "1.0.75" +async-std = "1.12.0" +tracing = "0.1.37" +rand = "0.8.5" +fps_counter = "3.0.0" + +[target.x86_64-unknown-linux-gnu] +linker = "/usr/bin/clang" +rustflags = ["-Clink-arg=-fuse-ld=lld", "-Clink-arg=-Wl,--no-rosegment"] + +[profile.release] +debug = true \ No newline at end of file diff --git a/examples/many-lights/src/main.rs b/examples/many-lights/src/main.rs new file mode 100644 index 0000000..a3b17a2 --- /dev/null +++ b/examples/many-lights/src/main.rs @@ -0,0 +1,189 @@ +use lyra_engine::{assets::{gltf::Gltf, ResourceManager}, ecs::query::{Res, ResMut, View}, game::Game, input::{Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput}, math::{self, Quat, Transform, Vec3}, render::light::{directional::DirectionalLight, PointLight}, scene::{CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN}, DeltaTime}; +use rand::Rng; +use tracing::info; + +const MAX_POINT_LIGHT_RANGE: f32 = 1.0; +const MIN_POINT_LIGHT_RANGE: f32 = 0.5; +const POINT_LIGHT_MAX_INTENSITY: f32 = 1.0; +const POINT_LIGHT_MIN_INTENSITY: f32 = 0.3; +const POINT_LIGHT_CUBE_SCALE: f32 = 0.2; +const POINT_LIGHT_NUM: u32 = 500; + +const POINT_LIGHT_MAX_X: f32 = 9.0; +const POINT_LIGHT_MIN_X: f32 = -9.0; + +const POINT_LIGHT_MAX_Y: f32 = 3.0; +const POINT_LIGHT_MIN_Y: f32 = 0.5; + +const POINT_LIGHT_MAX_Z: f32 = 4.0; +const POINT_LIGHT_MIN_Z: f32 = -5.0; + +#[async_std::main] +async fn main() { + + let action_handler_plugin = |game: &mut Game| { + let action_handler = ActionHandler::builder() + .add_layout(LayoutId::from(0)) + + .add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis)) + .add_action("Debug", Action::new(ActionKind::Button)) + + .add_mapping(ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0)) + .bind(ACTLBL_MOVE_FORWARD_BACKWARD, &[ + ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0), + ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0) + ]) + .bind(ACTLBL_MOVE_LEFT_RIGHT, &[ + ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0) + ]) + .bind(ACTLBL_MOVE_UP_DOWN, &[ + ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0), + ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0) + ]) + .bind(ACTLBL_LOOK_LEFT_RIGHT, &[ + ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(), + ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0), + ]) + .bind(ACTLBL_LOOK_UP_DOWN, &[ + ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(), + ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0), + ]) + .bind(ACTLBL_LOOK_ROLL, &[ + ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0), + ]) + .bind("Debug", &[ + ActionSource::Keyboard(KeyCode::B).into_binding(), + ]) + .finish() + ).finish(); + + let world = game.world_mut(); + world.add_resource(action_handler); + game.with_plugin(InputActionPlugin); + }; + + Game::initialize().await + .with_plugin(lyra_engine::DefaultPlugins) + .with_plugin(setup_scene_plugin) + .with_plugin(action_handler_plugin) + .with_plugin(camera_debug_plugin) + .with_plugin(FreeFlyCameraPlugin) + .run().await; +} + +fn setup_scene_plugin(game: &mut Game) { + let fps_counter = |mut counter: ResMut, delta: Res| -> anyhow::Result<()> { + let tick = counter.tick(); + + info!("FPS: {}, frame time: {}", tick, **delta); + + Ok(()) + }; + game.with_system("fps_counter", fps_counter, &[]); + + let world = game.world_mut(); + world.add_resource(fps_counter::FPSCounter::new()); + + let resman = world.get_resource_mut::(); + let cube_gltf = resman.request::("../assets/texture-sep/texture-sep.gltf").unwrap(); + + cube_gltf.wait_recurse_dependencies_load(); + let cube_mesh = &cube_gltf.data_ref() + .unwrap().meshes[0]; + + let sponza_model = resman.request::("../assets/sponza/Sponza.gltf").unwrap(); + drop(resman); + + sponza_model.wait_recurse_dependencies_load(); + let sponza_scene = &sponza_model.data_ref() + .unwrap().scenes[0]; + + world.spawn(( + sponza_scene.clone(), + Transform::from_xyz(0.0, 0.0, 0.0), + )); + + { + let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0); + light_tran.scale = Vec3::new(0.5, 0.5, 0.5); + light_tran.rotate_x(math::Angle::Degrees(-45.0)); + light_tran.rotate_y(math::Angle::Degrees(25.0)); + world.spawn(( + DirectionalLight { + enabled: true, + color: Vec3::ONE, + intensity: 0.15 + //..Default::default() + }, + light_tran, + )); + } + + let x_range = POINT_LIGHT_MIN_X..POINT_LIGHT_MAX_X; + let y_range = POINT_LIGHT_MIN_Y..POINT_LIGHT_MAX_Y; + let z_range = POINT_LIGHT_MIN_Z..POINT_LIGHT_MAX_Z; + + let mut rand = rand::thread_rng(); + let mut rand_vec3 = || -> Vec3 { + let x = rand.gen_range(x_range.clone()); + let y = rand.gen_range(y_range.clone()); + let z = rand.gen_range(z_range.clone()); + + Vec3::new(x, y, z) + }; + + let mut rand = rand::thread_rng(); + for _ in 0..POINT_LIGHT_NUM { + let range = rand.gen_range(MIN_POINT_LIGHT_RANGE..MAX_POINT_LIGHT_RANGE); + let intensity = rand.gen_range(POINT_LIGHT_MIN_INTENSITY..POINT_LIGHT_MAX_INTENSITY); + let color = rand_vec3().normalize(); + + world.spawn(( + PointLight { + enabled: true, + color, + intensity, + range, + ..Default::default() + }, + Transform::new( + rand_vec3(), + Quat::IDENTITY, + Vec3::new(POINT_LIGHT_CUBE_SCALE, POINT_LIGHT_CUBE_SCALE, POINT_LIGHT_CUBE_SCALE), + ), + cube_mesh.clone(), + )); + } + + let mut camera = CameraComponent::new_3d(); + // these values were taken by manually positioning the camera in the scene. + camera.transform = Transform::new( + Vec3::new(-10.0, 0.94, -0.28), + Quat::from_xyzw(0.03375484, -0.7116095, 0.0342693, 0.70092666), + Vec3::ONE + ); + world.spawn(( camera, FreeFlyCamera::default() )); +} + +fn camera_debug_plugin(game: &mut Game) { + let sys = |handler: Res, view: View<&mut CameraComponent>| -> anyhow::Result<()> { + if handler.was_action_just_pressed("Debug") { + for mut cam in view.into_iter() { + cam.debug = !cam.debug; + } + } + + Ok(()) + }; + + game.with_system("camera_debug_trigger", sys, &[]); +} \ No newline at end of file diff --git a/lyra-game/src/game.rs b/lyra-game/src/game.rs index d9e036e..5e624d7 100755 --- a/lyra-game/src/game.rs +++ b/lyra-game/src/game.rs @@ -349,7 +349,7 @@ impl Game { .with(fmt::layer().with_writer(stdout_layer)); #[cfg(feature = "tracy")] - t.with(tracing_tracy::TracyLayer::default()); + let t = t.with(tracing_tracy::TracyLayer::default()); t.with(filter::Targets::new() // done by prefix, so it includes all lyra subpackages diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index ff79b1a..cca268b 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -458,6 +458,10 @@ impl Renderer for BasicRenderer { && mesh_epoch == last_epoch { self.check_mesh_buffers(entity, &mesh_han); } + + if self.transform_buffers.needs_expand() { + self.transform_buffers.expand_buffers(&self.device); + } let group = TransformGroup::EntityRes(entity, mesh_han.uuid()); let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, @@ -492,6 +496,10 @@ impl Renderer for BasicRenderer { self.check_mesh_buffers(entity, &mesh_han); } + if self.transform_buffers.needs_expand() { + self.transform_buffers.expand_buffers(&self.device); + } + let scene_mesh_group = TransformGroup::Res(scene_han.uuid(), mesh_han.uuid()); let group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index bd9e85d..b62d3fd 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -370,6 +370,13 @@ impl TransformBuffers { pub fn buffer_offset(&self, transform_index: TransformIndex) -> u32 { Self::get_buffer_offset(&self.limits, transform_index) } + + /// Returns a boolean indicating if the buffers need to be expanded + pub fn needs_expand(&self) -> bool { + self.entries.last() + .map(|entry| entry.len >= self.max_transform_count) + .unwrap_or(false) + } } #[repr(C)] diff --git a/lyra-game/src/stage.rs b/lyra-game/src/stage.rs index 3c5ceb8..2f82381 100644 --- a/lyra-game/src/stage.rs +++ b/lyra-game/src/stage.rs @@ -1,7 +1,7 @@ use std::{hash::{Hash, DefaultHasher, Hasher}, collections::{HashMap, HashSet, VecDeque}, ptr::NonNull, fmt::Debug}; use lyra_ecs::{system::{GraphExecutor, GraphExecutorError, System}, World}; -use tracing::info_span; +use tracing::{info_span, instrument}; #[derive(thiserror::Error, Debug)] pub enum StagedExecutorError { @@ -112,6 +112,7 @@ impl StagedExecutor { /// Execute the staged systems in order. /// /// If `stop_on_error` is false but errors are encountered, those errors will be returned in a Vec. + #[instrument(skip(self, world, stop_on_error))] pub fn execute(&mut self, world: NonNull, stop_on_error: bool) -> Result, StagedExecutorError> { let mut stack = VecDeque::new(); let mut visited = HashSet::new(); @@ -120,13 +121,14 @@ impl StagedExecutor { self.topological_sort(&mut stack, &mut visited, node)?; } - let stage_span = info_span!("stage_exec", stage=tracing::field::Empty); + //let stage_span = info_span!("stage_exec", stage=tracing::field::Empty); let mut errors = vec![]; while let Some(node) = stack.pop_front() { let stage = self.stages.get_mut(&node).unwrap(); - stage_span.record("stage", stage.name.clone()); + let stage_span = info_span!("stage_exec", stage=stage.name.clone()); + //stage_span.record("stage", stage.name.clone()); let _e = stage_span.enter(); if let Err(e) = stage.exec.execute(world, stop_on_error) { From 8eac563229d9b43373df17e36686286eb1c2ee9b Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 21 Apr 2024 00:54:45 -0400 Subject: [PATCH 4/8] render: significantly improve performance of TransformBuffers Before the changes, a release build of 'many-lights' was running at about 130fps, now its 430fps --- Cargo.lock | 7 + examples/many-lights/Cargo.toml | 3 + lyra-ecs/src/system/graph.rs | 9 +- lyra-game/Cargo.toml | 5 +- lyra-game/src/render/avec.rs | 292 ++++++++++++++++++ lyra-game/src/render/mod.rs | 3 +- lyra-game/src/render/renderer.rs | 20 +- lyra-game/src/render/shaders/base.wgsl | 16 +- .../src/render/transform_buffer_storage.rs | 188 +++++------ 9 files changed, 410 insertions(+), 133 deletions(-) create mode 100644 lyra-game/src/render/avec.rs diff --git a/Cargo.lock b/Cargo.lock index e5d9d61..6df55fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1849,6 +1849,7 @@ dependencies = [ "tracing-log 0.1.4", "tracing-subscriber", "tracing-tracy", + "unique", "uuid", "wgpu", "winit", @@ -3565,6 +3566,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unique" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d360722e1f3884f5b14d332185f02ff111f771f0c76a313268fe6af1409aba96" + [[package]] name = "url" version = "2.5.0" diff --git a/examples/many-lights/Cargo.toml b/examples/many-lights/Cargo.toml index e8e5536..f552f46 100644 --- a/examples/many-lights/Cargo.toml +++ b/examples/many-lights/Cargo.toml @@ -15,5 +15,8 @@ fps_counter = "3.0.0" linker = "/usr/bin/clang" rustflags = ["-Clink-arg=-fuse-ld=lld", "-Clink-arg=-Wl,--no-rosegment"] +[profile.dev] +opt-level = 1 + [profile.release] debug = true \ No newline at end of file diff --git a/lyra-ecs/src/system/graph.rs b/lyra-ecs/src/system/graph.rs index 5282532..b497cad 100644 --- a/lyra-ecs/src/system/graph.rs +++ b/lyra-ecs/src/system/graph.rs @@ -1,6 +1,6 @@ use std::{collections::{HashMap, VecDeque, HashSet}, ptr::NonNull}; -use tracing::{debug_span, info_span}; +use tracing::{debug_span, info_span, instrument}; use super::System; @@ -60,6 +60,7 @@ impl GraphExecutor { } /// Executes the systems in the graph + #[instrument(skip(self, world_ptr, stop_on_error))] pub fn execute(&mut self, mut world_ptr: NonNull, stop_on_error: bool) -> Result, GraphExecutorError> { let mut stack = VecDeque::new(); @@ -71,13 +72,11 @@ impl GraphExecutor { let mut possible_errors = Vec::new(); - let sys_span = info_span!("graph_exec", system=tracing::field::Empty); - while let Some(node) = stack.pop_front() { let system = self.systems.get_mut(node.as_str()).unwrap(); - sys_span.record("system", system.name.clone()); - let _e = sys_span.enter(); + let span = info_span!("graph_exec", system=system.name.clone()); + let _e = span.enter(); if let Err(e) = system.system.execute(world_ptr) .map_err(|e| GraphExecutorError::SystemError(node, e)) { diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index ca45d9b..d4e6009 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -21,7 +21,7 @@ tracing-tracy = { version = "0.11.0", optional = true } async-std = { version = "1.12.0", features = [ "unstable", "attributes" ] } cfg-if = "1" -bytemuck = { version = "1.12", features = [ "derive" ] } +bytemuck = { version = "1.12", features = [ "derive", "min_const_generics" ] } image = { version = "0.24", default-features = false, features = ["png", "jpeg"] } anyhow = "1.0" instant = "0.1" @@ -33,6 +33,7 @@ quote = "1.0.29" uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } itertools = "0.11.0" thiserror = "1.0.56" +unique = "0.9.1" [features] -tracy = ["dep:tracing-tracy"] \ No newline at end of file +tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/src/render/avec.rs b/lyra-game/src/render/avec.rs new file mode 100644 index 0000000..7bf1cff --- /dev/null +++ b/lyra-game/src/render/avec.rs @@ -0,0 +1,292 @@ +use std::{alloc::Layout, cmp, marker::PhantomData, mem}; + +use std::{alloc, ptr}; +use unique::Unique; + +/// A [`Vec`] with its elements aligned to a runtime alignment value. +pub struct AVec { + buf: Unique, + cap: usize, + len: usize, + align: usize, + _marker: PhantomData, +} + +impl AVec { + // Tiny Vecs are dumb. Skip to: + // - 8 if the element size is 1, because any heap allocators are likely + // to round up a request of less than 8 bytes to at least 8 bytes. + // - 4 if elements are moderate-sized (<= 1 KiB). + // - 1 otherwise, to avoid wasting too much space for very short Vecs. + // + // Taken from Rust's standard library RawVec + pub(crate) const MIN_NON_ZERO_CAP: usize = if mem::size_of::() == 1 { + 8 + } else if mem::size_of::() <= 1024 { + 4 + } else { + 1 + }; + + #[inline] + pub fn new(alignment: usize) -> Self { + debug_assert!(mem::size_of::() > 0, "ZSTs not yet supported"); + + Self { + buf: Unique::dangling(), + cap: 0, + len: 0, + align: alignment, + _marker: PhantomData + } + } + + /// Constructs a new, empty `AVec` with at least the specified capacity. + /// + /// The aligned vector will be able to hold at least `capacity` elements without reallocating. + /// This method may allocate for more elements than `capacity`. If `capacity` is zero, + /// the vector will not allocate. + /// + /// # Panics + /// + /// Panics if the capacity exceeds `usize::MAX` bytes. + #[inline] + pub fn with_capacity(alignment: usize, capacity: usize) -> Self { + let mut s = Self::new(alignment); + + if capacity > 0 { + unsafe { + s.grow_amortized(0, capacity); + } + } + + s + } + + /// Calculates the size of the 'slot' for a single **aligned** item. + #[inline(always)] + fn slot_size(&self) -> usize { + let a = self.align - 1; + mem::align_of::() + (a) & !a + } + + /// # Panics + /// + /// Panics if the new capacity exceeds `usize::MAX` bytes. + #[inline] + unsafe fn grow_amortized(&mut self, len: usize, additional: usize) { + debug_assert!(additional > 0); + + let required_cap = len.checked_add(additional) + .expect("Capacity overflow"); + + let cap = cmp::max(self.cap * 2, required_cap); + let cap = cmp::max(Self::MIN_NON_ZERO_CAP, cap); + + let new_layout = Layout::from_size_align_unchecked(cap * self.slot_size(), self.align); + + let ptr = alloc::alloc(new_layout); + self.buf = Unique::new_unchecked(ptr); + self.cap = cap; + } + + /// # Panics + /// + /// Panics if the new capacity exceeds `usize::MAX` bytes. + #[inline] + unsafe fn grow_exact(&mut self, len: usize, additional: usize) { + debug_assert!(additional > 0); + + let cap = len.checked_add(additional) + .expect("Capacity overflow"); + + let new_layout = Layout::from_size_align_unchecked(cap * self.slot_size(), self.align); + + let ptr = alloc::alloc(new_layout); + self.buf = Unique::new_unchecked(ptr); + self.cap = cap; + } + + /// Reserves capacity for at least `additional` more elements. + /// + /// The collection may reserve more space to speculatively avoid frequent reallocations. + /// After calling `reserve`, capacity will be greater than or equal to + /// `self.len() + additional`. Does nothing if capacity is already sufficient. + /// + /// # Panics + /// + /// Panics if the new capacity exceeds `usize::MAX` bytes. + #[inline] + pub fn reserve(&mut self, additional: usize) { + debug_assert!(additional > 0); + + let remaining = self.capacity().wrapping_sub(self.len); + + if additional > remaining { + unsafe { self.grow_amortized(self.len, additional) }; + } + } + + /// Reserves capacity for `additional` more elements. + /// + /// Unlike [`reserve`], this will not over-allocate to speculatively avoid frequent + /// reallocations. After calling `reserve_exact`, capacity will be equal to + /// `self.len() + additional`. Does nothing if the capacity is already sufficient. + /// + /// Prefer [`reserve`] if future insertions are expected. + /// + /// # Panics + /// + /// Panics if the new capacity exceeds `usize::MAX` bytes. + #[inline] + pub fn reserve_exact(&mut self, additional: usize) { + let remaining = self.capacity().wrapping_sub(self.len); + + if additional > remaining { + unsafe { self.grow_exact(self.len, additional) }; + } + } + + /// Appends an element to the back of the collection. + /// + /// # Panics + /// + /// Panics if the new capacity exceeds `usize::MAX` bytes. + #[inline] + pub fn push(&mut self, val: T) { + if self.len == self.cap { + self.reserve(self.slot_size()); + } + + unsafe { + // SAFETY: the length is ensured to be less than the capacity. + self.set_at_unchecked(self.len, val); + } + + self.len += 1; + } + + /// Sets an element at position `idx` within the vector to `val`. + /// + /// # Unsafe + /// + /// If `self.len > idx`, bytes past the length of the vector will be written to, potentially + /// also writing past the capacity of the vector. + #[inline(always)] + unsafe fn set_at_unchecked(&mut self, idx: usize, val: T) { + let ptr = self.buf + .as_ptr() + .add(idx * self.slot_size()); + + std::ptr::write(ptr.cast::(), val); + } + + /// Sets an element at position `idx` within the vector to `val`. + /// + /// # Panics + /// + /// Panics if `idx >= self.len`. + #[inline(always)] + pub fn set_at(&mut self, idx: usize, val: T) { + assert!(self.len > idx); + + unsafe { + self.set_at_unchecked(idx, val); + } + } + + /// Shortens the vector, keeping the first `len` elements and dropping the rest. + /// + /// If `len` is greater or equal to the vector’s current length, this has no effect. + #[inline] + pub fn truncate(&mut self, len: usize) { + if len > self.len { + return; + } + + unsafe { + // drop each element past the new length + for i in len..self.len { + let ptr = self.buf.as_ptr() + .add(i * self.slot_size()) + .cast::(); + + ptr::drop_in_place(ptr); + } + } + + self.len = len; + } + + #[inline(always)] + pub fn as_ptr(&self) -> *const u8 { + self.buf.as_ptr() + } + + #[inline(always)] + pub fn as_mut_ptr(&self) -> *mut u8 { + self.buf.as_ptr() + } + + /// Returns the alignment of the elements in the vector. + #[inline(always)] + pub fn align(&self) -> usize { + self.align + } + + /// Returns the length of the vector. + #[inline(always)] + pub fn len(&self) -> usize { + self.len + } + + /// Returns the capacity of the vector. + /// + /// The capacity is the amount of elements that the vector can store without reallocating. + #[inline(always)] + pub fn capacity(&self) -> usize { + self.cap + } +} + +impl AVec { + /// Resized the `AVec` in-place so that `len` is equal to `new_len`. + /// + /// If `new_len` is greater than `len`, the `AVec` is extended by the difference, and + /// each additional slot is filled with `value`. If `new_len` is less than `len`, + /// the `AVec` will be truncated by to be `new_len` + /// + /// This method requires `T` to implement [`Clone`] in order to clone the passed value. + /// + /// # Panics + /// + /// Panics if the new capacity exceeds `usize::MAX` bytes. + #[inline] + pub fn resize(&mut self, new_len: usize, value: T) { + if new_len > self.len { + self.reserve(new_len - self.len); + + unsafe { + let mut ptr = self.buf + .as_ptr().add(self.len * self.slot_size()); + + // write all elements besides the last one + for _ in 1..new_len { + std::ptr::write(ptr.cast::(), value.clone()); + ptr = ptr.add(self.slot_size()); + self.len += 1; + } + + if new_len > 0 { + // the last element can be written without cloning + std::ptr::write(ptr.cast::(), value.clone()); + self.len += 1; + } + + self.len = new_len; + } + } else { + self.truncate(new_len); + } + } +} \ No newline at end of file diff --git a/lyra-game/src/render/mod.rs b/lyra-game/src/render/mod.rs index 5475230..d7985d5 100755 --- a/lyra-game/src/render/mod.rs +++ b/lyra-game/src/render/mod.rs @@ -12,4 +12,5 @@ pub mod camera; pub mod window; pub mod transform_buffer_storage; pub mod light; -pub mod light_cull_compute; \ No newline at end of file +pub mod light_cull_compute; +pub mod avec; \ No newline at end of file diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index cca268b..d4f95d8 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -169,7 +169,7 @@ impl BasicRenderer { format: surface_format, width: size.width, height: size.height, - present_mode, + present_mode: wgpu::PresentMode::Immediate, alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], }; @@ -448,7 +448,8 @@ impl Renderer for BasicRenderer { alive_entities.insert(entity); if let Some((mesh_han, mesh_epoch)) = mesh_pair { - let interop_pos = self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); + // TODO: speed up interpolating transforms + let interop_pos = *transform; //self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); if let Some(mesh) = mesh_han.data_ref() { // if process mesh did not just create a new mesh, and the epoch @@ -464,8 +465,8 @@ impl Renderer for BasicRenderer { } let group = TransformGroup::EntityRes(entity, mesh_han.uuid()); - let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, - group, || ( interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation) )); + let transform_id = self.transform_buffers.update_or_push(&self.device, &self.queue, &self.render_limits, + group, interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation)); let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); @@ -482,7 +483,8 @@ impl Renderer for BasicRenderer { lyra_scene::system_update_world_transforms(scene.world(), view).unwrap(); } - let interpo_pos = self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); + // TODO: speed up interpolating transforms + let interpo_pos = *transform; //self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); for (mesh_han, pos) in scene.world().view_iter::<(&MeshHandle, &WorldTransform)>() { if let Some(mesh) = mesh_han.data_ref() { @@ -502,8 +504,8 @@ impl Renderer for BasicRenderer { let scene_mesh_group = TransformGroup::Res(scene_han.uuid(), mesh_han.uuid()); let group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); - let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, - group, || ( mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation) )); + let transform_id = self.transform_buffers.update_or_push(&self.device, &self.queue, &self.render_limits, + group, mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation) ); let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); @@ -517,7 +519,7 @@ impl Renderer for BasicRenderer { } // collect dead entities - self.transform_buffers.tick(); + self.transform_buffers.send_to_gpu(&self.queue); // when buffer storage length does not match the amount of iterated entities, // remove all dead entities, and their buffers, if they weren't iterated over @@ -611,7 +613,7 @@ impl Renderer for BasicRenderer { // Get the bindgroup for job's transform and bind to it using an offset. let bindgroup = self.transform_buffers.bind_group(job.transform_id); let offset = self.transform_buffers.buffer_offset(job.transform_id); - render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); + render_pass.set_bind_group(1, bindgroup, &[ offset, ]); render_pass.set_bind_group(2, &self.camera_buffer.bindgroup(), &[]); render_pass.set_bind_group(3, &self.light_buffers.bind_group_pair.bindgroup, &[]); diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index d059aa0..9e86e04 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -21,6 +21,11 @@ struct VertexOutput { @location(2) world_normal: vec3, } +struct TransformData { + transform: mat4x4, + normal_matrix: mat4x4, +} + struct CameraUniform { view: mat4x4, inverse_projection: mat4x4, @@ -51,9 +56,7 @@ struct Lights { }; @group(1) @binding(0) -var u_model_transform: mat4x4; -@group(1) @binding(1) -var u_model_normal_matrix: mat4x4; +var u_model_transform_data: TransformData; @group(2) @binding(0) var u_camera: CameraUniform; @@ -68,13 +71,14 @@ fn vs_main( var out: VertexOutput; out.tex_coords = model.tex_coords; - out.clip_position = u_camera.view_projection * u_model_transform * vec4(model.position, 1.0); + out.clip_position = u_camera.view_projection * u_model_transform_data.transform * vec4(model.position, 1.0); // the normal mat is actually only a mat3x3, but there's a bug in wgpu: https://github.com/gfx-rs/wgpu-rs/issues/36 - let normal_mat = mat3x3(u_model_normal_matrix[0].xyz, u_model_normal_matrix[1].xyz, u_model_normal_matrix[2].xyz); + let normal_mat4 = u_model_transform_data.normal_matrix; + let normal_mat = mat3x3(normal_mat4[0].xyz, normal_mat4[1].xyz, normal_mat4[2].xyz); out.world_normal = normalize(normal_mat * model.normal, ); - var world_position: vec4 = u_model_transform * vec4(model.position, 1.0); + var world_position: vec4 = u_model_transform_data.transform * vec4(model.position, 1.0); out.world_position = world_position.xyz; return out; diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index b62d3fd..9fae477 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -7,6 +7,8 @@ use wgpu::Limits; use std::mem; +use crate::render::avec::AVec; + /// A group id created from a [`TransformGroup`]. /// /// This is mainly created so that [`TransformGroup::OwnedGroup`] can use another group inside of it. @@ -67,8 +69,10 @@ pub struct TransformIndex { struct BufferEntry { pub len: usize, pub bindgroup: wgpu::BindGroup, - pub transform_buffer: wgpu::Buffer, - pub normal_buffer: wgpu::Buffer, + pub buffer: wgpu::Buffer, + transforms: AVec, + //pub normal_buffer: wgpu::Buffer, + } /// A HashMap that caches values for reuse. @@ -159,10 +163,12 @@ impl CachedValMap, + //groups: CachedValMap, + //groups: SlotMap, entries: Vec, limits: wgpu::Limits, max_transform_count: usize, + next_index: usize, } impl TransformBuffers { @@ -181,26 +187,16 @@ impl TransformBuffers { }, count: None, }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: None, - }, - count: None, - } ], label: Some("transform_bind_group_layout"), }); let mut s = Self { bindgroup_layout, - groups: Default::default(), entries: Default::default(), - max_transform_count: (limits.max_uniform_buffer_binding_size / 2) as usize / (mem::size_of::()), + max_transform_count: (limits.max_uniform_buffer_binding_size) as usize / (limits.min_uniform_buffer_offset_alignment as usize), //(mem::size_of::()), limits, + next_index: 0, }; // create the first uniform buffer @@ -209,73 +205,59 @@ impl TransformBuffers { s } - /// Update an existing transform in the buffers. + /// Write the transform buffers to the gpu. /// - /// # Panics - /// Panics if the `entity_group` is not already inside of the buffers. - #[instrument(skip(self, queue, limits, entity_group, transform, normal_matrix))] - pub fn update_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { - let index = *self.groups.get(entity_group.into()) - .expect("Use 'push_transform' for new entities"); - let entry = self.entries.get_mut(index.entry_index).unwrap(); + /// This uses [`wgpu::Queue::write_buffer`], so the write is not immediately submitted, + /// and instead enqueued internally to happen at the start of the next submit() call. + pub fn send_to_gpu(&mut self, queue: &wgpu::Queue) { + self.next_index = 0; - let normal_matrix = glam::Mat4::from_mat3(normal_matrix); + for entry in &mut self.entries { + entry.len = 0; - // write the transform and normal to the end of the transform - let offset = Self::get_buffer_offset(limits, index) as _; - queue.write_buffer(&entry.transform_buffer, offset, bytemuck::bytes_of(&transform)); - queue.write_buffer(&entry.normal_buffer, offset, bytemuck::bytes_of(&normal_matrix)); + let p = entry.transforms.as_ptr(); + let bytes = unsafe { std::slice::from_raw_parts(p as *const u8, entry.transforms.len() * entry.transforms.align()) }; - index - } - - /// Push a new transform into the buffers. - #[instrument(skip(self, queue, limits, entity_group, transform, normal_matrix))] - pub fn push_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { - self.groups.insert(entity_group.into(), || { - // this closure is only called when there are no values that can be reused, - // so we get a brand new index at the end of the last entry in the chain. - let last = self.entries.last_mut().unwrap(); - - // ensure the gpu buffer is not overflown - debug_assert!(last.len < self.max_transform_count, - "Transform buffer is filled and 'next_indices' was not incremented! \ - Was a new buffer created?"); - - let tidx = last.len; - last.len += 1; - - TransformIndex { - entry_index: self.entries.len() - 1, - transform_index: tidx - } - }); - - self.update_transform(queue, limits, entity_group, transform, normal_matrix) - } - - /// Collect the dead transforms and prepare self to check next time. - pub fn tick(&mut self) { - self.groups.update(); - } - - /// Returns a boolean indicating if the buffer contains this group. - pub fn contains(&self, group: TransformGroup) -> bool { - self.groups.contains(group.into()) + queue.write_buffer(&entry.buffer, 0, bytes); + } } /// Update an existing transform group or if its not existing yet, pushes it to the buffer. /// /// Returns: the index that the transform is at in the buffers. - #[instrument(skip(self, queue, limits, group, transform_fn))] - pub fn update_or_push(&mut self, queue: &wgpu::Queue, limits: &Limits, group: TransformGroup, transform_fn: F) -> TransformIndex - where F: Fn() -> (glam::Mat4, glam::Mat3) + #[instrument(skip(self, device, queue, limits, group, transform, normal_matrix))] + #[inline(always)] + pub fn update_or_push(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, limits: &Limits, group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { - let (transform, normal_matrix) = transform_fn(); - if self.contains(group) { - self.update_transform(queue, limits, group, transform, normal_matrix) - } else { - self.push_transform(queue, limits, group, transform, normal_matrix) + // maybe will be used at some point again + let _ = (queue, limits, group); + + let normal_matrix = glam::Mat4::from_mat3(normal_matrix); + + let index = self.next_index; + self.next_index += 1; + + // the index of the entry to put the transform into + let entry_index = index / self.max_transform_count; + // the index of the transform in the buffer + let transform_index = index % self.max_transform_count; + + if entry_index >= self.entries.len() { + self.expand_buffers(device); + } + + let entry = self.entries.get_mut(entry_index).unwrap(); + + // write the transform and normal to the end of the transform + entry.transforms.set_at(transform_index, TransformNormalMatPair { + transform, + normal_mat: normal_matrix, + }); + entry.len += 1; + + TransformIndex { + entry_index: 0, + transform_index: index, } } @@ -297,21 +279,9 @@ impl TransformBuffers { } ); - let normal_mat_buffer = device.create_buffer( - &wgpu::BufferDescriptor { - label: Some(&format!("B_NormalMatrix_{}", self.entries.len())), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - size: max_buffer_sizes, - mapped_at_creation: false, - } - ); + let tran_stride = mem::size_of::(); - let tran_stride = mem::size_of::(); - // although a normal matrix only needs to be a mat3, there's a weird issue with - // misalignment from wgpu or spirv-cross: https://github.com/gfx-rs/wgpu-rs/issues/36 - let norm_stride = mem::size_of::(); - - let transform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &self.bindgroup_layout, entries: &[ wgpu::BindGroupEntry { @@ -324,42 +294,34 @@ impl TransformBuffers { } ) }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: &normal_mat_buffer, - offset: 0, - size: Some(NonZeroU64::new(norm_stride as u64).unwrap()) - } - ) - } ], label: Some("BG_Transforms"), }); + let mut transforms = AVec::new(limits.min_uniform_buffer_offset_alignment as _); + transforms.resize(self.max_transform_count, TransformNormalMatPair { + transform: glam::Mat4::IDENTITY, + normal_mat: glam::Mat4::IDENTITY, + }); + let entry = BufferEntry { - bindgroup: transform_bind_group, - transform_buffer, - normal_buffer: normal_mat_buffer, + bindgroup, + buffer: transform_buffer, len: 0, + + transforms, }; self.entries.push(entry); } /// Returns the bind group for the transform index. + #[inline(always)] pub fn bind_group(&self, transform_id: TransformIndex) -> &wgpu::BindGroup { - let entry = self.entries.get(transform_id.entry_index).unwrap(); + let entry_index = transform_id.transform_index / self.max_transform_count; + let entry = self.entries.get(entry_index).unwrap(); &entry.bindgroup } - /// Get the buffer offset for a transform using wgpu limits. - /// - /// If its possible to borrow immutably, use [`TransformBuffers::buffer_offset`]. - fn get_buffer_offset(limits: &wgpu::Limits, transform_index: TransformIndex) -> u32 { - transform_index.transform_index as u32 * limits.min_uniform_buffer_offset_alignment as u32 - } - /// Returns the offset of the transform inside the bind group buffer. /// /// ```nobuild @@ -367,15 +329,21 @@ impl TransformBuffers { /// let offset = transform_buffers.buffer_offset(job.transform_id); /// render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); /// ``` + #[inline(always)] pub fn buffer_offset(&self, transform_index: TransformIndex) -> u32 { - Self::get_buffer_offset(&self.limits, transform_index) + //Self::get_buffer_offset(&self.limits, transform_index) + let transform_index = transform_index.transform_index % self.max_transform_count; + let t = transform_index as u32 * self.limits.min_uniform_buffer_offset_alignment as u32; + //debug!("offset: {t}"); + t } /// Returns a boolean indicating if the buffers need to be expanded pub fn needs_expand(&self) -> bool { - self.entries.last() + false + /* self.entries.last() .map(|entry| entry.len >= self.max_transform_count) - .unwrap_or(false) + .unwrap_or(false) */ } } From 337ce18e8c731afba47900d98a5b6f35846ebe25 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Mon, 22 Apr 2024 00:20:42 -0400 Subject: [PATCH 5/8] ecs: update existing components on entity in World::insert --- lyra-ecs/src/query/filter/not.rs | 2 +- lyra-ecs/src/query/filter/or.rs | 2 +- lyra-ecs/src/world.rs | 54 +++++++++++++++++++++++++------- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lyra-ecs/src/query/filter/not.rs b/lyra-ecs/src/query/filter/not.rs index 4257261..a50550b 100644 --- a/lyra-ecs/src/query/filter/not.rs +++ b/lyra-ecs/src/query/filter/not.rs @@ -5,7 +5,7 @@ use crate::{query::{AsQuery, Query}, Archetype, World}; /// This means that entities that `Q` fetches are skipped, and entities that /// `Q` does not fetch are not skipped. /// -/// ```rust,nobuild +/// ```nobuild /// // Iterate over entities that has a transform, and are not the origin of a `ChildOf` relationship. /// for (en, pos, _) in world /// .view::<(Entities, &Transform, Not>>)>() diff --git a/lyra-ecs/src/query/filter/or.rs b/lyra-ecs/src/query/filter/or.rs index fd1e8f2..3fc38b8 100644 --- a/lyra-ecs/src/query/filter/or.rs +++ b/lyra-ecs/src/query/filter/or.rs @@ -36,7 +36,7 @@ impl<'a, Q1: Query, Q2: Query> Fetch<'a> for OrFetch<'a, Q1, Q2> { /// /// This checks if `Q1` can fetch before checking `Q2`. /// -/// ```rust,nobuild +/// ```nobuild /// for (en, pos, _) in world /// .view::<(Entities, &Transform, Or, Has>)>() /// .iter() diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index ee6ebcd..45e85cd 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, collections::HashMap, ptr::NonNull}; +use std::{any::{Any, TypeId}, collections::HashMap, ptr::NonNull}; use atomic_refcell::{AtomicRef, AtomicRefMut}; @@ -128,22 +128,39 @@ impl World { } } - /// Insert a bundle into an existing entity. If the components are already existing on the - /// entity, they will be updated, else the entity will be moved to a different Archetype - /// that can store the entity. That may involve creating a new Archetype. + /// Insert a component bundle into an existing entity. + /// + /// If the components are already existing on the entity, they will be updated, else the + /// entity will be moved to a different Archetype that can store the entity. That may + /// involve creating a new Archetype. pub fn insert(&mut self, entity: Entity, bundle: B) where B: Bundle { - // TODO: If the entity already has the components in `bundle`, update the values of the - // components with the bundle. - let tick = self.tick(); let record = self.entities.entity_record(entity).unwrap(); let current_arch = self.archetypes.get(&record.id).unwrap(); let current_arch_len = current_arch.len(); + let mut contains_all = true; + for id in bundle.type_ids() { + contains_all = contains_all && current_arch.get_column(id).is_some(); + } + + if contains_all { + let current_arch = self.archetypes.get_mut(&record.id).unwrap(); + let entry_idx = *current_arch.entity_indexes() + .get(&entity).unwrap(); + + bundle.take(|ptr, id, _info| { + let col = current_arch.get_column_mut(id).unwrap(); + unsafe { col.set_at(entry_idx.0 as _, ptr, tick) }; + }); + + return; + } + // contains the type ids for the old component columns + the ids for the new components let mut combined_column_types: Vec = current_arch.columns.iter().map(|c| c.info.type_id()).collect(); combined_column_types.extend(bundle.type_ids()); @@ -227,7 +244,7 @@ impl World { /// A method used for debugging implementation details of the ECS. /// /// Here's an example of the output: - /// ``` + /// ```nobuild /// Entities /// 1 in archetype 0 at 0 /// 0 in archetype 1 at 0 @@ -271,7 +288,7 @@ impl World { /// the contents of entities inside the archetypes. /// /// Below is a template of the output: - /// ``` + /// ```nobuild /// Entities /// %ENTITY_ID% in archetype %ARCHETYPE_ID% at %INDEX% /// Arch ID -- %ARCHETYPE_LEN% entities @@ -618,8 +635,6 @@ mod tests { insert_and_assert(&mut world, e1, v2s[0], v3s[0]); println!("Entity 1 is good"); - assert_eq!(world.archetypes.len(), 1); - println!("Empty archetype was removed"); } #[test] @@ -655,4 +670,21 @@ mod tests { assert!(tick >= world_tick); } } + + /// Tests replacing components using World::insert + #[test] + fn entity_insert_replace() { + let mut world = World::new(); + let first = world.spawn((Vec2::new(10.0, 10.0),)); + let second = world.spawn((Vec2::new(5.0, 5.0),)); + + world.insert(first, Vec2::new(50.0, 50.0)); + + let pos = world.view_one::<&mut Vec2>(first).get().unwrap(); + assert_eq!(*pos, Vec2::new(50.0, 50.0)); + drop(pos); + + let pos = world.view_one::<&mut Vec2>(second).get().unwrap(); + assert_eq!(*pos, Vec2::new(5.0, 5.0)); + } } \ No newline at end of file From e2c6b557bb578fe9bc6963b009b027b9e94e4be5 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Mon, 22 Apr 2024 01:07:35 -0400 Subject: [PATCH 6/8] render: improve performance of transform interpolation by using ecs components --- Cargo.lock | 1 + lyra-ecs/src/query/mod.rs | 4 + lyra-ecs/src/query/optional.rs | 76 +++++++++++++++++ lyra-game/Cargo.toml | 1 + lyra-game/src/render/renderer.rs | 142 +++++++++++++++---------------- 5 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 lyra-ecs/src/query/optional.rs diff --git a/Cargo.lock b/Cargo.lock index 6df55fd..3691e9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1842,6 +1842,7 @@ dependencies = [ "lyra-resource", "lyra-scene", "quote", + "rustc-hash", "syn 2.0.51", "thiserror", "tracing", diff --git a/lyra-ecs/src/query/mod.rs b/lyra-ecs/src/query/mod.rs index 8738608..fa39907 100644 --- a/lyra-ecs/src/query/mod.rs +++ b/lyra-ecs/src/query/mod.rs @@ -27,6 +27,10 @@ mod world; #[allow(unused_imports)] pub use world::*; +mod optional; +#[allow(unused_imports)] +pub use optional::*; + pub mod dynamic; pub mod filter; diff --git a/lyra-ecs/src/query/optional.rs b/lyra-ecs/src/query/optional.rs new file mode 100644 index 0000000..e6c59ca --- /dev/null +++ b/lyra-ecs/src/query/optional.rs @@ -0,0 +1,76 @@ +use crate::{Archetype, World}; + +use super::{AsQuery, Fetch, Query}; + +#[derive(Default)] +pub struct OptionalFetcher<'a, Q: AsQuery> { + fetcher: Option<::Fetch<'a>>, +} + +impl<'a, Q: AsQuery> Fetch<'a> for OptionalFetcher<'a, Q> { + type Item = Option<::Item<'a>>; + + fn dangling() -> Self { + unreachable!() + } + + unsafe fn get_item(&mut self, entity: crate::ArchetypeEntityId) -> Self::Item { + self.fetcher.as_mut() + .map(|f| f.get_item(entity)) + } + + fn can_visit_item(&mut self, entity: crate::ArchetypeEntityId) -> bool { + self.fetcher.as_mut() + .map(|f| f.can_visit_item(entity)) + .unwrap_or(true) + } +} + +#[derive(Default)] +pub struct Optional { + query: Q::Query, +} + +impl Copy for Optional { } + +impl Clone for Optional { + fn clone(&self) -> Self { + Self { query: self.query.clone() } + } +} + +impl Query for Optional { + type Item<'a> = Option<::Item<'a>>; + + type Fetch<'a> = OptionalFetcher<'a, Q>; + + fn new() -> Self { + Optional { + query: Q::Query::new(), + } + } + + fn can_visit_archetype(&self, _: &Archetype) -> bool { + true + } + + unsafe fn fetch<'a>(&self, world: &'a World, arch: &'a Archetype, tick: crate::Tick) -> Self::Fetch<'a> { + let fetcher = if self.query.can_visit_archetype(arch) { + Some(self.query.fetch(world, arch, tick)) + } else { + None + }; + + OptionalFetcher { + fetcher, + } + } +} + +impl AsQuery for Optional { + type Query = Self; +} + +impl AsQuery for Option { + type Query = Optional; +} \ No newline at end of file diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index d4e6009..0ea0b05 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -34,6 +34,7 @@ uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } itertools = "0.11.0" thiserror = "1.0.56" unique = "0.9.1" +rustc-hash = "1.1.0" [features] tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index d4f95d8..cd7f127 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -1,15 +1,14 @@ -use std::collections::{HashMap, VecDeque, HashSet}; +use std::collections::{VecDeque, HashSet}; use std::rc::Rc; use std::sync::Arc; use std::borrow::Cow; use glam::Vec3; -use instant::Instant; use itertools::izip; use lyra_ecs::query::filter::{Has, Not, Or}; use lyra_ecs::relation::{ChildOf, RelationOriginComponent}; -use lyra_ecs::{Entity, Tick}; -use lyra_ecs::query::{Entities, TickOf}; +use lyra_ecs::{Component, Entity}; +use lyra_ecs::query::{Entities, Res, TickOf}; use lyra_ecs::World; use lyra_scene::{SceneGraph, WorldTransform}; use tracing::{debug, instrument, warn}; @@ -22,6 +21,7 @@ use crate::math::Transform; use crate::render::material::MaterialUniform; use crate::render::render_buffer::BufferWrapperBuilder; use crate::scene::CameraComponent; +use crate::DeltaTime; use super::camera::{RenderCamera, CameraUniform}; use super::desc_buf_lay::DescVertexBufferLayout; @@ -67,12 +67,10 @@ struct MeshBufferStorage { //transform_index: TransformBufferIndices, } -#[derive(Clone, Debug)] -pub struct CachedTransform { - last_updated_at: Option, - cached_at: Instant, - to_transform: Transform, - from_transform: Transform, +#[derive(Clone, Debug, Component)] +pub struct InterpTransform { + last_transform: Transform, + alpha: f32, } pub struct BasicRenderer { @@ -85,13 +83,12 @@ pub struct BasicRenderer { pub clear_color: wgpu::Color, - pub render_pipelines: HashMap>, + pub render_pipelines: rustc_hash::FxHashMap>, pub render_jobs: VecDeque, - mesh_buffers: HashMap, // TODO: clean up left over buffers from deleted entities/components - material_buffers: HashMap>, - entity_meshes: HashMap, - entity_last_transforms: HashMap, + mesh_buffers: rustc_hash::FxHashMap, // TODO: clean up left over buffers from deleted entities/components + material_buffers: rustc_hash::FxHashMap>, + entity_meshes: rustc_hash::FxHashMap, transform_buffers: TransformBuffers, @@ -223,11 +220,11 @@ impl BasicRenderer { b: 0.3, a: 1.0, }, - render_pipelines: HashMap::new(), - render_jobs: VecDeque::new(), - mesh_buffers: HashMap::new(), - material_buffers: HashMap::new(), - entity_meshes: HashMap::new(), + render_pipelines: Default::default(), + render_jobs: Default::default(), + mesh_buffers: Default::default(), + material_buffers: Default::default(), + entity_meshes: Default::default(), render_limits, transform_buffers, @@ -238,7 +235,6 @@ impl BasicRenderer { bgl_texture, default_texture, depth_buffer_texture: depth_texture, - entity_last_transforms: HashMap::new(), light_buffers: light_uniform_buffers, material_buffer: mat_buffer, @@ -246,7 +242,7 @@ impl BasicRenderer { }; // create the default pipelines - let mut pipelines = HashMap::new(); + let mut pipelines = rustc_hash::FxHashMap::default(); pipelines.insert(0, Arc::new(FullRenderPipeline::new(&s.device, &s.config, &shader, vec![super::vertex::Vertex::desc(),], vec![&s.bgl_texture, &s.transform_buffers.bindgroup_layout, @@ -398,64 +394,68 @@ impl BasicRenderer { true } else { false } } - - #[instrument(skip(self, now, transform, entity))] - fn interpolate_transforms(&mut self, now: Instant, last_epoch: Tick, entity: Entity, transform: &Transform, transform_epoch: Tick) -> Transform { - let cached = match self.entity_last_transforms.get_mut(&entity) { - Some(last) if transform_epoch == last_epoch => { - last.from_transform = last.to_transform; - last.to_transform = *transform; - last.last_updated_at = Some(last.cached_at); - last.cached_at = now; - - last.clone() - }, - Some(last) => last.clone(), - None => { - let cached = CachedTransform { - last_updated_at: None, - cached_at: now, - from_transform: *transform, - to_transform: *transform, - }; - self.entity_last_transforms.insert(entity, cached.clone()); - cached - } - }; - - let fixed_time = match cached.last_updated_at { - Some(last_updated_at) => cached.cached_at - last_updated_at, - None => now - cached.cached_at - }.as_secs_f32(); - let accumulator = (now - cached.cached_at).as_secs_f32(); - let alpha = accumulator / fixed_time; - - cached.from_transform.lerp(cached.to_transform, alpha) - } } impl Renderer for BasicRenderer { #[instrument(skip(self, main_world))] fn prepare(&mut self, main_world: &mut World) { let last_epoch = main_world.current_tick(); - let now_inst = Instant::now(); let mut alive_entities = HashSet::new(); - let view = main_world.view_iter::<(Entities, &Transform, TickOf, - Or<(&MeshHandle, TickOf), (&SceneHandle, TickOf)>)>(); + let view = main_world.view_iter::<( + Entities, + &Transform, + TickOf, + Or< + (&MeshHandle, TickOf), + (&SceneHandle, TickOf) + >, + Option<&mut InterpTransform>, + Res, + )>(); - for (entity, transform, transform_epoch, (mesh_pair, scene_pair)) in view { + // used to store InterpTransform components to add to entities later + let mut component_queue: Vec<(Entity, InterpTransform)> = vec![]; + + for ( + entity, + transform, + _transform_epoch, + ( + mesh_pair, + scene_pair + ), + interp_tran, + delta_time, + ) in view + { alive_entities.insert(entity); - if let Some((mesh_han, mesh_epoch)) = mesh_pair { - // TODO: speed up interpolating transforms - let interop_pos = *transform; //self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); + let interp_transform = match interp_tran { + Some(mut interp_transform) => { + // found in https://youtu.be/YJB1QnEmlTs?t=472 + interp_transform.alpha = 1.0 - interp_transform.alpha.powf(**delta_time); + + interp_transform.last_transform = interp_transform.last_transform.lerp(*transform, interp_transform.alpha); + interp_transform.last_transform + }, + None => { + let interp = InterpTransform { + last_transform: *transform, + alpha: 0.5, + }; + component_queue.push((entity, interp)); + *transform + } + }; + + if let Some((mesh_han, mesh_epoch)) = mesh_pair { if let Some(mesh) = mesh_han.data_ref() { // if process mesh did not just create a new mesh, and the epoch // shows that the scene has changed, verify that the mesh buffers // dont need to be resent to the gpu. - if !self.process_mesh(entity, interop_pos, &*mesh, mesh_han.uuid()) + if !self.process_mesh(entity, interp_transform, &*mesh, mesh_han.uuid()) && mesh_epoch == last_epoch { self.check_mesh_buffers(entity, &mesh_han); } @@ -466,7 +466,7 @@ impl Renderer for BasicRenderer { let group = TransformGroup::EntityRes(entity, mesh_han.uuid()); let transform_id = self.transform_buffers.update_or_push(&self.device, &self.queue, &self.render_limits, - group, interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation)); + group, interp_transform.calculate_mat4(), glam::Mat3::from_quat(interp_transform.rotation)); let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); @@ -483,12 +483,9 @@ impl Renderer for BasicRenderer { lyra_scene::system_update_world_transforms(scene.world(), view).unwrap(); } - // TODO: speed up interpolating transforms - let interpo_pos = *transform; //self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); - for (mesh_han, pos) in scene.world().view_iter::<(&MeshHandle, &WorldTransform)>() { if let Some(mesh) = mesh_han.data_ref() { - let mesh_interpo = interpo_pos + **pos; + let mesh_interpo = interp_transform + **pos; // if process mesh did not just create a new mesh, and the epoch // shows that the scene has changed, verify that the mesh buffers @@ -518,6 +515,10 @@ impl Renderer for BasicRenderer { } } + for (en, interp) in component_queue { + main_world.insert(en, interp); + } + // collect dead entities self.transform_buffers.send_to_gpu(&self.queue); @@ -531,10 +532,9 @@ impl Renderer for BasicRenderer { self.mesh_buffers.retain(|u, _| !removed_entities.contains(u)); } + // update camera uniform if let Some(camera) = main_world.view_iter::<&mut CameraComponent>().next() { let uniform = self.inuse_camera.calc_view_projection(&camera); - //let pos = camera.transform.translation; - //let uniform = CameraUniform::new(view_mat, *view_proj, pos); self.camera_buffer.write_buffer(&self.queue, 0, &[uniform]); } else { warn!("Missing camera!"); From 53837d469bd96b5e0827e469893b7b167ac1f334 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Wed, 24 Apr 2024 00:28:01 -0400 Subject: [PATCH 7/8] ecs: fix BatchedSystem, implement ways for `Criteria`s to modify the world before and after execution --- lyra-ecs/src/system/batched.rs | 48 +++++++++++++++++++++++++++------ lyra-ecs/src/system/criteria.rs | 20 ++++++++------ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/lyra-ecs/src/system/batched.rs b/lyra-ecs/src/system/batched.rs index 00de666..d8f4d64 100644 --- a/lyra-ecs/src/system/batched.rs +++ b/lyra-ecs/src/system/batched.rs @@ -1,9 +1,9 @@ use lyra_ecs::World; -use tracing::debug_span; +use tracing::{debug_span, instrument}; use crate::Access; -use super::{System, Criteria, IntoSystem}; +use super::{Criteria, GraphExecutorError, IntoSystem, System}; /// A system that executes a batch of systems in order that they were given. /// You can optionally add criteria that must pass before the systems are @@ -13,6 +13,7 @@ pub struct BatchedSystem { systems: Vec>, criteria: Vec>, criteria_checks: u32, + did_run: bool, } impl BatchedSystem { @@ -47,6 +48,7 @@ impl System for BatchedSystem { } } + #[instrument(skip(self, world))] fn execute(&mut self, world: std::ptr::NonNull) -> anyhow::Result<()> { let mut can_run = true; let mut check_again = false; @@ -69,13 +71,26 @@ impl System for BatchedSystem { } if can_run { + for criteria in self.criteria.iter_mut() { + criteria.modify_world(world); + } + for (idx, system) in self.systems.iter_mut().enumerate() { - let sys_span = debug_span!("batch", system=tracing::field::Empty); - sys_span.record("system", idx); - let _e = sys_span.enter(); + let span = debug_span!("batch", system=idx); + let _e = span.enter(); system.execute(world)?; + + /* let deferred_span = debug_span!("deferred_exec"); + let _e = deferred_span.enter(); + + if let Err(e) = system.execute_deferred(world) + .map_err(|e| GraphExecutorError::Command(e)) { + return Err(e.into()); + } */ } + + self.did_run = true; } if check_again { @@ -87,9 +102,26 @@ impl System for BatchedSystem { Ok(()) } - - fn execute_deferred(&mut self, _: std::ptr::NonNull) -> anyhow::Result<()> { - todo!() + + #[instrument(skip(self, world))] + fn execute_deferred(&mut self, world: std::ptr::NonNull) -> anyhow::Result<()> { + if self.did_run { + for (idx, system) in self.systems.iter_mut().enumerate() { + let span = debug_span!("batch", system=idx); + let _e = span.enter(); + + system.execute_deferred(world) + .map_err(|e| GraphExecutorError::Command(e))?; + } + + for criteria in self.criteria.iter_mut() { + criteria.undo_world_modifications(world); + } + } + + self.did_run = false; + + Ok(()) } } diff --git a/lyra-ecs/src/system/criteria.rs b/lyra-ecs/src/system/criteria.rs index d0a2868..95e4699 100644 --- a/lyra-ecs/src/system/criteria.rs +++ b/lyra-ecs/src/system/criteria.rs @@ -26,13 +26,17 @@ pub trait Criteria { /// * `world` - The ecs world. /// * `check_count` - The amount of times the Criteria has been checked this tick. fn can_run(&mut self, world: NonNull, check_count: u32) -> CriteriaSchedule; -} -impl Criteria for F - where F: FnMut(&mut World, u32) -> CriteriaSchedule -{ - fn can_run(&mut self, mut world: NonNull, check_count: u32) -> CriteriaSchedule { - let world_mut = unsafe { world.as_mut() }; - self(world_mut, check_count) - } + /// Modify the world after the [`Criteria`] in the system batch allows the systems to run. + /// + /// This can be great if this Criteria limits the execution of systems based off of resources. + /// A `FixedTimestep` criteria would use this to replace the [`DeltaTime`] resource in the + /// world to match the timestep time. + fn modify_world(&mut self, world: NonNull); + + /// Undo modifications to the world after the systems in the batch have been executed. + /// + /// The `FixedTimestep` criteria (see docs for [`Criteria::modify_world`]) uses this + /// to replace the [`DeltaTime`] resource with its original value before it was replaced. + fn undo_world_modifications(&mut self, world: NonNull); } \ No newline at end of file From db501015d0a0a7c9c966d471f036782be2ca5d61 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Wed, 24 Apr 2024 00:30:30 -0400 Subject: [PATCH 8/8] Create an example project to test transform interpolation --- Cargo.lock | 12 + Cargo.toml | 2 +- .../fixed-timestep-rotating-model/Cargo.toml | 22 ++ .../fixed-timestep-rotating-model/src/main.rs | 241 ++++++++++++++++++ examples/testbed/src/main.rs | 17 ++ lyra-ecs/src/world.rs | 2 +- lyra-game/src/delta_time.rs | 8 +- 7 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 examples/fixed-timestep-rotating-model/Cargo.toml create mode 100644 examples/fixed-timestep-rotating-model/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 3691e9b..b503c59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -947,6 +947,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fixed-timestep-rotating-model" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "fps_counter", + "lyra-engine", + "rand 0.8.5", + "tracing", +] + [[package]] name = "flate2" version = "1.0.28" diff --git a/Cargo.toml b/Cargo.toml index f285537..5a25f9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ "lyra-ecs", "lyra-reflect", "lyra-scripting", - "lyra-game", "lyra-math", "lyra-scene", "examples/many-lights"] + "lyra-game", "lyra-math", "lyra-scene", "examples/many-lights", "examples/fixed-timestep-rotating-model"] [features] scripting = ["dep:lyra-scripting"] diff --git a/examples/fixed-timestep-rotating-model/Cargo.toml b/examples/fixed-timestep-rotating-model/Cargo.toml new file mode 100644 index 0000000..8d7fa61 --- /dev/null +++ b/examples/fixed-timestep-rotating-model/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "fixed-timestep-rotating-model" +version = "0.1.0" +edition = "2021" + +[dependencies] +lyra-engine = { path = "../../", features = ["tracy"] } +anyhow = "1.0.75" +async-std = "1.12.0" +tracing = "0.1.37" +rand = "0.8.5" +fps_counter = "3.0.0" + +[target.x86_64-unknown-linux-gnu] +linker = "/usr/bin/clang" +rustflags = ["-Clink-arg=-fuse-ld=lld", "-Clink-arg=-Wl,--no-rosegment"] + +[profile.dev] +opt-level = 1 + +[profile.release] +debug = true \ No newline at end of file diff --git a/examples/fixed-timestep-rotating-model/src/main.rs b/examples/fixed-timestep-rotating-model/src/main.rs new file mode 100644 index 0000000..cadcd51 --- /dev/null +++ b/examples/fixed-timestep-rotating-model/src/main.rs @@ -0,0 +1,241 @@ +use std::ptr::NonNull; + +use lyra_engine::{ + assets::{gltf::Gltf, ResourceManager}, + ecs::{ + query::{Res, ResMut, View}, + system::{BatchedSystem, Criteria, CriteriaSchedule, IntoSystem}, + World, + }, + game::Game, + input::{ + Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, + InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, + }, + math::{self, Transform, Vec3}, + render::light::directional::DirectionalLight, + scene::{ + CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, + ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, + ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN, + }, + DeltaTime, +}; +use tracing::info; + +#[async_std::main] +async fn main() { + let action_handler_plugin = |game: &mut Game| { + let action_handler = ActionHandler::builder() + .add_layout(LayoutId::from(0)) + .add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis)) + .add_action("Debug", Action::new(ActionKind::Button)) + .add_mapping( + ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0)) + .bind( + ACTLBL_MOVE_FORWARD_BACKWARD, + &[ + ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0), + ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0), + ], + ) + .bind( + ACTLBL_MOVE_LEFT_RIGHT, + &[ + ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0), + ], + ) + .bind( + ACTLBL_MOVE_UP_DOWN, + &[ + ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0), + ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0), + ], + ) + .bind( + ACTLBL_LOOK_LEFT_RIGHT, + &[ + ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(), + ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0), + ], + ) + .bind( + ACTLBL_LOOK_UP_DOWN, + &[ + ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(), + ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0), + ], + ) + .bind( + ACTLBL_LOOK_ROLL, + &[ + ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0), + ], + ) + .bind( + "Debug", + &[ActionSource::Keyboard(KeyCode::B).into_binding()], + ) + .finish(), + ) + .finish(); + + let world = game.world_mut(); + world.add_resource(action_handler); + game.with_plugin(InputActionPlugin); + }; + + Game::initialize() + .await + .with_plugin(lyra_engine::DefaultPlugins) + .with_plugin(setup_scene_plugin) + .with_plugin(action_handler_plugin) + //.with_plugin(camera_debug_plugin) + .with_plugin(FreeFlyCameraPlugin) + .run() + .await; +} + +fn setup_scene_plugin(game: &mut Game) { + let world = game.world_mut(); + let resman = world.get_resource_mut::(); + let camera_gltf = resman + .request::("../assets/AntiqueCamera.glb") + .unwrap(); + + camera_gltf.wait_recurse_dependencies_load(); + let camera_mesh = &camera_gltf.data_ref().unwrap().scenes[0]; + drop(resman); + + world.spawn(( + camera_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(0.0, -5.0, 0.0), + )); + + { + let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0); + light_tran.scale = Vec3::new(0.5, 0.5, 0.5); + light_tran.rotate_x(math::Angle::Degrees(-45.0)); + light_tran.rotate_y(math::Angle::Degrees(25.0)); + world.spawn(( + DirectionalLight { + enabled: true, + color: Vec3::ONE, + intensity: 0.15, //..Default::default() + }, + light_tran, + )); + } + + let mut camera = CameraComponent::new_3d(); + camera.transform.translation += math::Vec3::new(0.0, 0.0, 1.5); + world.spawn((camera, FreeFlyCamera::default())); + + let fps_counter = |mut counter: ResMut, + delta: Res| -> anyhow::Result<()> { + let tick = counter.tick(); + + info!("FPS: {}, frame time: {}", tick, **delta); + + Ok(()) + }; + + world.add_resource(fps_counter::FPSCounter::new()); + + let rotate_system = |dt: Res, view: View<&mut Transform>| -> anyhow::Result<()> { + const SPEED: f32 = 4.0; + let dt = **dt; + + for mut transform in view.iter() { + info!("rotation: {:?}", transform.rotation); + transform.rotate_y(math::Angle::Degrees(SPEED * dt)); + } + + Ok(()) + }; + + let mut sys = BatchedSystem::new(); + sys.with_criteria(FixedTimestep::new(60)); + sys.with_system(rotate_system.into_system()); + sys.with_system(fps_counter.into_system()); + + game.with_system("fixed_timestep", sys, &[]); +} + +struct FixedTimestep { + max_tps: u32, + fixed_time: f32, + accumulator: f32, + old_dt: Option, +} + +#[allow(dead_code)] +impl FixedTimestep { + pub fn new(max_tps: u32) -> Self { + Self { + max_tps, + fixed_time: Self::calc_fixed_time(max_tps), + accumulator: 0.0, + old_dt: None, + } + } + + fn calc_fixed_time(max_tps: u32) -> f32 { + 1.0 / max_tps as f32 + } + + fn set_tps(&mut self, tps: u32) { + self.max_tps = tps; + self.fixed_time = Self::calc_fixed_time(tps); + } + + fn tps(&self) -> u32 { + self.max_tps + } + + fn fixed_time(&self) -> f32 { + self.fixed_time + } +} + +impl Criteria for FixedTimestep { + fn can_run(&mut self, mut world: NonNull, check_count: u32) -> CriteriaSchedule { + let world = unsafe { world.as_mut() }; + if check_count == 0 { + let delta_time = world.get_resource::(); + self.accumulator += **delta_time; + } + + if self.accumulator >= self.fixed_time { + self.accumulator -= self.fixed_time; + return CriteriaSchedule::YesAndLoop; + } + + CriteriaSchedule::No + } + + fn modify_world(&mut self, mut world: NonNull) { + let world = unsafe { world.as_mut() }; + self.old_dt = world.try_get_resource().map(|r| *r); + + world.add_resource(DeltaTime::from(self.fixed_time)); + } + + fn undo_world_modifications(&mut self, mut world: NonNull) { + let world = unsafe { world.as_mut() }; + world.add_resource( + self.old_dt + .expect("DeltaTime resource was somehow never got from the world"), + ); + } +} \ No newline at end of file diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index 22fe0d5..d5057c5 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -7,6 +7,7 @@ struct FixedTimestep { max_tps: u32, fixed_time: f32, accumulator: f32, + old_dt: Option, } #[allow(dead_code)] @@ -16,6 +17,7 @@ impl FixedTimestep { max_tps, fixed_time: Self::calc_fixed_time(max_tps), accumulator: 0.0, + old_dt: None, } } @@ -52,6 +54,21 @@ impl Criteria for FixedTimestep { CriteriaSchedule::No } + + fn modify_world(&mut self, mut world: NonNull) { + let world = unsafe { world.as_mut() }; + self.old_dt = world.try_get_resource().map(|r| *r); + + world.add_resource(DeltaTime::from(self.fixed_time)); + } + + fn undo_world_modifications(&mut self, mut world: NonNull) { + let world = unsafe { world.as_mut() }; + world.add_resource( + self.old_dt + .expect("DeltaTime resource was somehow never got from the world"), + ); + } } #[derive(Clone)] diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index 45e85cd..47baab5 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -1,4 +1,4 @@ -use std::{any::{Any, TypeId}, collections::HashMap, ptr::NonNull}; +use std::{any::TypeId, collections::HashMap, ptr::NonNull}; use atomic_refcell::{AtomicRef, AtomicRefMut}; diff --git a/lyra-game/src/delta_time.rs b/lyra-game/src/delta_time.rs index 2c19944..4d9a31e 100644 --- a/lyra-game/src/delta_time.rs +++ b/lyra-game/src/delta_time.rs @@ -4,9 +4,15 @@ use lyra_reflect::Reflect; use crate::{plugin::Plugin, game::GameStages}; -#[derive(Clone, Component, Default, Reflect)] +#[derive(Clone, Copy, Component, Default, Reflect)] pub struct DeltaTime(f32, #[reflect(skip)] Option); +impl From for DeltaTime { + fn from(value: f32) -> Self { + DeltaTime(value, None) + } +} + impl std::ops::Deref for DeltaTime { type Target = f32;