diff --git a/.lapce/run.toml b/.lapce/run.toml new file mode 100644 index 0000000..eccecf1 --- /dev/null +++ b/.lapce/run.toml @@ -0,0 +1,29 @@ +# The run config is used for both run mode and debug mode + +[[configs]] +# the name of this task +name = "Example 'simple_scene'" + +# the type of the debugger. If not set, it can't be debugged but can still be run +type = "lldb" + +# the program to run +program = "../../target/debug/simple_scene" + +# the program arguments, e.g. args = ["arg1", "arg2"], optional +# args = [] + +# current working directory, optional +cwd = "${workspace}/examples/simple_scene" + +# environment variables, optional +# [configs.env] +# VAR1 = "VAL1" +# VAR2 = "VAL2" + +# task to run before the run/debug session is started, optional +[configs.prelaunch] +program = "cargo" +args = [ + "build", +] diff --git a/.vscode/launch.json b/.vscode/launch.json index 26d7b96..a69d883 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,6 +22,24 @@ "args": [], "cwd": "${workspaceFolder}/examples/testbed" }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example simple_scene", + "cargo": { + "args": [ + "build", + "--manifest-path", "${workspaceFolder}/examples/simple_scene/Cargo.toml" + //"--bin=testbed", + ], + "filter": { + "name": "simple_scene", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}/examples/simple_scene" + }, { "type": "lldb", "request": "launch", diff --git a/Cargo.lock b/Cargo.lock index eb90419..b5960e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,6 +384,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bind_match" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171f0236f66c7be99f32060539c2bade94033ded356ecf4c9dc9b1e6198326cd" + [[package]] name = "bit-set" version = "0.5.3" @@ -959,6 +965,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -1853,6 +1865,7 @@ dependencies = [ "anyhow", "async-std", "async-trait", + "bind_match", "bytemuck", "cfg-if", "gilrs-core", @@ -1861,10 +1874,12 @@ dependencies = [ "instant", "itertools 0.11.0", "lyra-ecs", + "lyra-game-derive", "lyra-math", "lyra-reflect", "lyra-resource", "lyra-scene", + "petgraph", "quote", "rustc-hash", "syn 2.0.51", @@ -1880,6 +1895,15 @@ dependencies = [ "winit", ] +[[package]] +name = "lyra-game-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.51", +] + [[package]] name = "lyra-math" version = "0.1.0" @@ -2507,6 +2531,16 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.1.0", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -3065,6 +3099,16 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simple_scene" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "lyra-engine", + "tracing", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 8c361d6..cb59c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ members = [ "examples/many-lights", "examples/fixed-timestep-rotating-model", - "examples/lua-scripting" + "examples/lua-scripting", + "examples/simple_scene" ] [features] @@ -27,3 +28,9 @@ tracy = ["lyra-game/tracy"] [dependencies] lyra-game = { path = "lyra-game" } lyra-scripting = { path = "lyra-scripting", optional = true } + +[profile.dev] +opt-level = 1 + +[profile.release] +debug = true \ No newline at end of file diff --git a/examples/fixed-timestep-rotating-model/Cargo.toml b/examples/fixed-timestep-rotating-model/Cargo.toml index 8d7fa61..e6e20ee 100644 --- a/examples/fixed-timestep-rotating-model/Cargo.toml +++ b/examples/fixed-timestep-rotating-model/Cargo.toml @@ -9,14 +9,4 @@ 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 +fps_counter = "3.0.0" \ No newline at end of file diff --git a/examples/lua-scripting/Cargo.toml b/examples/lua-scripting/Cargo.toml index 41cedba..1cb7cc3 100644 --- a/examples/lua-scripting/Cargo.toml +++ b/examples/lua-scripting/Cargo.toml @@ -10,13 +10,3 @@ 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/many-lights/Cargo.toml b/examples/many-lights/Cargo.toml index f552f46..cb5b996 100644 --- a/examples/many-lights/Cargo.toml +++ b/examples/many-lights/Cargo.toml @@ -9,14 +9,4 @@ 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 +fps_counter = "3.0.0" \ No newline at end of file diff --git a/examples/simple_scene/Cargo.toml b/examples/simple_scene/Cargo.toml new file mode 100644 index 0000000..0e157fc --- /dev/null +++ b/examples/simple_scene/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "simple_scene" +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" \ No newline at end of file diff --git a/examples/simple_scene/scripts/test.lua b/examples/simple_scene/scripts/test.lua new file mode 100644 index 0000000..bac6e90 --- /dev/null +++ b/examples/simple_scene/scripts/test.lua @@ -0,0 +1,59 @@ +---Return the userdata's name from its metatable +---@param val userdata +---@return string +function udname(val) + return getmetatable(val).__name +end + +function on_init() + local cube = world:request_res("../assets/cube-texture-embedded.gltf") + print("Loaded textured cube (" .. udname(cube) .. ")") + + cube:wait_until_loaded() + local scenes = cube:scenes() + local cube_scene = scenes[1] + + local pos = Transform.from_translation(Vec3.new(0, 0, -8.0)) + + local e = world:spawn(pos, cube_scene) + print("spawned entity " .. tostring(e)) +end + +--[[ function on_first() + print("Lua's first function was called") +end + +function on_pre_update() + print("Lua's pre-update function was called") +end ]] + +function on_update() + --[[ ---@type number + local dt = world:resource(DeltaTime) + local act = world:resource(ActionHandler) + ---@type number + local move_objs = act:get_axis("ObjectsMoveUpDown") + + world:view(function (t) + if move_objs ~= nil then + t:translate(0, move_objs * 0.35 * dt, 0) + return t + end + end, Transform) ]] + + ---@type number + local dt = world:resource(DeltaTime) + + world:view(function (t) + t:translate(0, 0.15 * dt, 0) + return t + end, Transform) +end + +--[[ function on_post_update() + print("Lua's post-update function was called") +end + +function on_last() + print("Lua's last function was called") +end ]] \ No newline at end of file diff --git a/examples/simple_scene/src/main.rs b/examples/simple_scene/src/main.rs new file mode 100644 index 0000000..64eca82 --- /dev/null +++ b/examples/simple_scene/src/main.rs @@ -0,0 +1,149 @@ +use lyra_engine::{ + assets::{gltf::Gltf, ResourceManager}, + 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, + }, +}; + +#[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, -2.0), + )); */ + + let cube_gltf = resman + .request::("../assets/cube-texture-embedded.gltf") + .unwrap(); + + cube_gltf.wait_recurse_dependencies_load(); + let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0]; + drop(resman); + + world.spawn(( + cube_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(0.0, 0.0, -2.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, 5.5); + world.spawn((camera, FreeFlyCamera::default())); +} \ No newline at end of file diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 0ea0b05..bc28133 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.1" edition = "2021" [dependencies] +lyra-game-derive = { path = "./lyra-game-derive" } lyra-resource = { path = "../lyra-resource" } lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] } lyra-reflect = { path = "../lyra-reflect", features = [ "math" ] } @@ -35,6 +36,8 @@ itertools = "0.11.0" thiserror = "1.0.56" unique = "0.9.1" rustc-hash = "1.1.0" +petgraph = { version = "0.6.5", features = ["matrix_graph"] } +bind_match = "0.1.2" [features] tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/lyra-game-derive/Cargo.toml b/lyra-game/lyra-game-derive/Cargo.toml new file mode 100644 index 0000000..a176391 --- /dev/null +++ b/lyra-game/lyra-game-derive/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lyra-game-derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.70" +quote = "1.0.33" +syn = "2.0.41" \ No newline at end of file diff --git a/lyra-game/lyra-game-derive/src/lib.rs b/lyra-game/lyra-game-derive/src/lib.rs new file mode 100644 index 0000000..1de240a --- /dev/null +++ b/lyra-game/lyra-game-derive/src/lib.rs @@ -0,0 +1,35 @@ +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(RenderGraphLabel)] +pub fn derive_render_graph_label(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let type_ident = &input.ident; + + proc_macro::TokenStream::from(quote! { + impl #impl_generics crate::render::graph::RenderGraphLabel for #type_ident #ty_generics #where_clause { + fn rc_clone(&self) -> std::rc::Rc { + std::rc::Rc::new(self.clone()) + } + + /* fn as_dyn(&self) -> &dyn crate::render::graph::RenderGraphLabel { + &self + } + + fn as_partial_eq(&self) -> &dyn PartialEq { + self + } */ + + fn as_label_hash(&self) -> u64 { + let tyid = ::std::any::TypeId::of::(); + + let mut s = ::std::hash::DefaultHasher::new(); + ::std::hash::Hash::hash(&tyid, &mut s); + ::std::hash::Hash::hash(self, &mut s); + ::std::hash::Hasher::finish(&s) + } + } + }) +} \ No newline at end of file diff --git a/lyra-game/src/game.rs b/lyra-game/src/game.rs index 5e624d7..8ccf6c3 100755 --- a/lyra-game/src/game.rs +++ b/lyra-game/src/game.rs @@ -57,10 +57,12 @@ struct GameLoop { } impl GameLoop { - pub async fn new(window: Arc, world: World, staged_exec: StagedExecutor) -> GameLoop { + pub async fn new(window: Arc, mut world: World, staged_exec: StagedExecutor) -> Self { + let renderer = BasicRenderer::create_with_window(&mut world, window.clone()).await; + Self { window: Arc::clone(&window), - renderer: Box::new(BasicRenderer::create_with_window(window).await), + renderer: Box::new(renderer), world, staged_exec, @@ -68,7 +70,7 @@ impl GameLoop { } pub async fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize) { - self.renderer.on_resize(new_size); + self.renderer.on_resize(&mut self.world, new_size); } pub async fn on_init(&mut self) { diff --git a/lyra-game/src/lib.rs b/lyra-game/src/lib.rs index 47de0fb..83d7615 100644 --- a/lyra-game/src/lib.rs +++ b/lyra-game/src/lib.rs @@ -1,6 +1,7 @@ #![feature(hash_extract_if)] #![feature(lint_reasons)] #![feature(trait_alias)] +#![feature(map_many_mut)] extern crate self as lyra_engine; diff --git a/lyra-game/src/render/graph/mod.rs b/lyra-game/src/render/graph/mod.rs new file mode 100644 index 0000000..89111f3 --- /dev/null +++ b/lyra-game/src/render/graph/mod.rs @@ -0,0 +1,549 @@ +mod node; +use std::{ + cell::{Ref, RefCell}, collections::{HashMap, VecDeque}, fmt::Debug, hash::Hash, rc::Rc, sync::Arc +}; + +use lyra_ecs::World; +pub use node::*; + +mod passes; +pub use passes::*; + +mod slot_desc; +pub use slot_desc::*; + +use rustc_hash::FxHashMap; +use tracing::{debug_span, instrument, trace, warn}; +use wgpu::ComputePass; + +use super::resource::{ComputePipeline, Pipeline, RenderPipeline}; + +pub trait RenderGraphLabel: Debug + 'static { + fn rc_clone(&self) -> Rc; + //fn as_dyn(&self) -> &dyn RenderGraphLabel; + //fn as_partial_eq(&self) -> &dyn PartialEq; + fn as_label_hash(&self) -> u64; + + fn label_eq_rc(&self, other: &Rc) -> bool { + self.as_label_hash() == other.as_label_hash() + } + + fn label_eq(&self, other: &dyn RenderGraphLabel) -> bool { + self.as_label_hash() == other.as_label_hash() + } +} + +pub struct RenderGraphHash(u64); + +#[derive(Clone)] +pub struct RenderGraphLabelValue(Rc); + +impl Debug for RenderGraphLabelValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl From for RenderGraphLabelValue { + fn from(value: L) -> Self { + Self(Rc::new(value)) + } +} + +impl From> for RenderGraphLabelValue { + fn from(value: Rc) -> Self { + Self(value) + } +} + +impl From<&Rc> for RenderGraphLabelValue { + fn from(value: &Rc) -> Self { + Self(value.clone()) + } +} + +impl Hash for RenderGraphLabelValue { + fn hash(&self, state: &mut H) { + state.write_u64(self.0.as_label_hash()); + } +} + +impl PartialEq for RenderGraphLabelValue { + fn eq(&self, other: &Self) -> bool { + self.0.label_eq_rc(&other.0) + } +} + +impl Eq for RenderGraphLabelValue {} + +struct PassEntry { + inner: Arc>, + desc: Rc>, + /// The index of the pass in the execution graph + graph_index: petgraph::matrix_graph::NodeIndex, + pipeline: Rc>>, +} + +pub struct BindGroupEntry { + pub label: RenderGraphLabelValue, + /// BindGroup + pub bg: Rc, + /// BindGroupLayout + pub layout: Option>, +} + +#[allow(dead_code)] +struct ResourcedSlot { + label: RenderGraphLabelValue, + ty: SlotType, + value: SlotValue, +} + +/// Stores the pipeline and other resources it uses. +/// +/// This stores the bind groups that have been created for it +pub struct PipelineResource { + pub pipeline: Pipeline, + /// Lookup map for bind groups using names + pub bg_layout_name_lookup: HashMap, +} + +#[derive(Debug)] +pub struct RenderTarget { + pub surface: wgpu::Surface, + pub surface_config: wgpu::SurfaceConfiguration, + pub current_texture: Option, +} + +pub struct RenderGraph { + device: Rc, + queue: Rc, + slots: FxHashMap, + nodes: FxHashMap, + bind_groups: FxHashMap, + /// A directed graph describing the execution path of the RenderGraph + execution_graph: petgraph::matrix_graph::DiMatrix, usize>, +} + +impl RenderGraph { + pub fn new(device: Rc, queue: Rc) -> Self { + Self { + device, + queue, + slots: Default::default(), + nodes: Default::default(), + bind_groups: Default::default(), + execution_graph: Default::default(), + } + } + + pub fn device(&self) -> &wgpu::Device { + &*self.device + } + + /// Add a [`Node`] to the RenderGraph. + /// + /// When the node is added, its [`Node::desc`] method will be executed. + /// + /// Additionally, all [`Slot`](node::NodeSlot)s of the node will be iterated, + /// 1. Ensuring that there are no two slots of the same name, with different value types + /// 2. Changing the id of insert slots to match the id of the output slot of the same name. + /// * This means that the id of insert slots **ARE NOT STABLE**. **DO NOT** rely on them to + /// not change. The IDs of output slots do stay the same. + /// 3. Ensuring that no two slots share the same ID when the names do not match. + #[instrument(skip(self, pass), level = "debug")] + pub fn add_pass(&mut self, label: impl RenderGraphLabel, mut pass: P) { + let mut desc = pass.desc(self); + + // collect all the slots of the pass + for slot in &mut desc.slots { + if let Some(other) = self + .slots + .get_mut(&slot.label) + //.map(|s| (id, s)) + //.and_then(|id| self.slots.get_mut(id).map(|s| (id, s))) + { + debug_assert_eq!( + slot.ty, other.ty, + "slot {:?} in pass {:?} does not match existing slot of same name", + slot.label, label + ); + + /* trace!( + "Found existing slot for {:?}, changing id to {}", + slot.label, + id + ); */ + + // if there is a slot of the same name + //slot.id = *id; + } else { + debug_assert!(!self.slots.contains_key(&slot.label), + "Reuse of id detected in render graph! Pass: {:?}, slot: {:?}", + label, slot.label, + ); + + let res_slot = ResourcedSlot { + label: slot.label.clone(), + ty: slot.ty, + value: slot.value.clone().unwrap_or(SlotValue::None), + }; + + self.slots.insert(slot.label.clone(), res_slot); + } + } + + // get clones of the bind groups and layouts + for (label, bg, bgl) in &desc.bind_groups { + self.bind_groups.insert(label.clone(), BindGroupEntry { + label: label.clone(), + bg: bg.clone(), + layout: bgl.clone(), + }); + } + + let label: RenderGraphLabelValue = label.into(); + let index = self.execution_graph.add_node(label.clone()); + + self.nodes.insert( + label, + PassEntry { + inner: Arc::new(RefCell::new(pass)), + desc: Rc::new(RefCell::new(desc)), + graph_index: index, + pipeline: Rc::new(RefCell::new(None)), + }, + ); + } + + /// Creates all buffers required for the passes, also creates an internal execution path. + /// + /// This only needs to be ran when the [`Node`]s in the graph change, or they are removed or + /// added. + #[instrument(skip(self, device))] + pub fn setup(&mut self, device: &wgpu::Device) { + // For all passes, create their pipelines + for pass in self.nodes.values_mut() { + let desc = (*pass.desc).borrow(); + if let Some(pipeline_desc) = &desc.pipeline_desc { + let pipeline = match desc.ty { + NodeType::Render => Pipeline::Render(RenderPipeline::create( + device, + pipeline_desc + .as_render_pipeline_descriptor() + .expect("got compute pipeline descriptor in a render pass"), + )), + NodeType::Compute => Pipeline::Compute(ComputePipeline::create( + device, + pipeline_desc + .as_compute_pipeline_descriptor() + .expect("got render pipeline descriptor in a compute pass"), + )), + NodeType::Presenter | NodeType::Node => { + panic!("Present or Node RenderGraph passes should not have a pipeline descriptor!"); + } + }; + + drop(desc); + let res = PipelineResource { + pipeline, + bg_layout_name_lookup: Default::default(), + }; + + let mut pipeline = pass.pipeline.borrow_mut(); + *pipeline = Some(res); + } + } + } + + #[instrument(skip(self, world))] + pub fn prepare(&mut self, world: &mut World) { + // prepare all passes + let mut buffer_writes = VecDeque::::new(); + // reserve some buffer writes. not all nodes write so half the amount of them is probably + // fine. + buffer_writes.reserve(self.nodes.len() / 2); + + for (label, pass) in &mut self.nodes { + let mut context = RenderGraphContext::new(&self.device, &self.queue, None, label.clone()); + let mut inner = pass.inner.borrow_mut(); + inner.prepare(world, &mut context); + buffer_writes.append(&mut context.buffer_writes); + } + + { + // Queue all buffer writes to the gpu + let s = debug_span!("queue_buffer_writes"); + let _e = s.enter(); + + while let Some(bufwr) = buffer_writes.pop_front() { + let slot = self + .slots + .get(&bufwr.target_slot) + .expect(&format!( + "Failed to find slot '{:?}' for buffer write", + bufwr.target_slot + )); + let buf = slot + .value + .as_buffer() + .expect(&format!("Slot '{:?}' is not a buffer", bufwr.target_slot)); + + self.queue.write_buffer(buf, bufwr.offset, &bufwr.bytes); + } + } + } + + #[instrument(skip(self))] + pub fn render(&mut self) { + let mut sorted: VecDeque = petgraph::algo::toposort(&self.execution_graph, None) + .expect("RenderGraph had cycled!") + .iter() + .map(|i| self.execution_graph[i.clone()].clone()) + .collect(); + //debug!("Render graph execution order: {:?}", sorted); + + let mut encoders = Vec::with_capacity(self.nodes.len() / 2); + while let Some(pass_label) = sorted.pop_front() { + let pass = self.nodes.get(&pass_label).unwrap(); + let pass_inn = pass.inner.clone(); + + let pass_desc = pass.desc.clone(); + let pass_desc = (*pass_desc).borrow(); + + let label = format!("{:?} Encoder", pass_label.0); + + // encoders are not needed for presenter nodes. + let encoder = if pass_desc.ty.should_have_pipeline() { + Some( + self.device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some(&label), + }), + ) + } else { + None + }; + + // clone of the Rc's is required to appease the borrow checker + let device = self.device.clone(); + let queue = self.queue.clone(); + let mut context = RenderGraphContext::new(&device, &queue, encoder, pass_label.clone()); + + // all encoders need to be submitted before a presenter node is executed. + if pass_desc.ty == NodeType::Presenter { + trace!("Submitting {} encoderd before presenting", encoders.len()); + self.queue.submit(encoders.drain(..)); + } + + trace!("Executing {:?}", pass_label.0); + let mut inner = pass_inn.borrow_mut(); + inner.execute(self, &pass_desc, &mut context); + + if let Some(encoder) = context.encoder { + encoders.push(encoder.finish()); + } + } + + if !encoders.is_empty() { + warn!( + "{} encoders were not submitted in the same render cycle they were created. \ + Make sure there is a presenting pass at the end. You may still see something, \ + however it will be delayed a render cycle.", + encoders.len() + ); + self.queue.submit(encoders.into_iter()); + } + } + + pub fn slot_value>(&self, label: L) -> Option<&SlotValue> { + self.slots.get(&label.into()).map(|s| &s.value) + } + + pub fn slot_value_mut>(&mut self, label: L) -> Option<&mut SlotValue> { + self.slots.get_mut(&label.into()).map(|s| &mut s.value) + } + + pub fn node_desc>(&self, label: L) -> Option> { + self.nodes.get(&label.into()).map(|s| (*s.desc).borrow()) + } + + #[inline(always)] + pub fn pipeline>(&self, label: L) -> Option> { + self.nodes.get(&label.into()) + .and_then(|p| { + let v = p.pipeline.borrow(); + + match &*v { + Some(_) => Some(Ref::map(v, |p| &p.as_ref().unwrap().pipeline)), + None => None, + } + }) + } + + #[inline(always)] + pub fn try_bind_group>(&self, label: L) -> Option<&Rc> { + self.bind_groups.get(&label.into()).map(|e| &e.bg) + } + + #[inline(always)] + pub fn bind_group>(&self, label: L) -> &Rc { + self.try_bind_group(label).expect("Unknown id for bind group") + } + + #[inline(always)] + pub fn try_bind_group_layout>(&self, label: L) -> Option<&Rc> { + self.bind_groups.get(&label.into()).and_then(|e| e.layout.as_ref()) + } + + #[inline(always)] + pub fn bind_group_layout>(&self, label: L) -> &Rc { + self.try_bind_group_layout(label) + .expect("Unknown id for bind group layout") + } + + pub fn add_edge(&mut self, from: impl RenderGraphLabel, to: impl RenderGraphLabel) + { + let from = RenderGraphLabelValue::from(from); + let to = RenderGraphLabelValue::from(to); + + let from_idx = self + .nodes + .iter() + .find(|p| *p.0 == from) + .map(|p| p.1.graph_index) + .expect("Failed to find from pass"); + let to_idx = self + .nodes + .iter() + .find(|p| *p.0 == to) + .map(|p| p.1.graph_index) + .expect("Failed to find to pass"); + + debug_assert_ne!(from_idx, to_idx, "cannot add edges between the same node"); + + self.execution_graph.add_edge(from_idx, to_idx, ()); + } + + /// Utility method for setting the bind groups for a pass. + /// + /// The parameter `bind_groups` can be used to specify the labels of a bind group, and the + /// index of the bind group in the pipeline for the pass. If a bind group of the provided + /// name is not found in the graph, a panic will occur. + /// + /// # Example: + /// ```rust,nobuild + /// graph.set_bind_groups( + /// &mut pass, + /// &[ + /// // retrieves the `BasePassSlots::DepthTexture` bind group and sets the index 0 in the + /// // pass to it. + /// (&BasePassSlots::DepthTexture, 0), + /// (&BasePassSlots::Camera, 1), + /// (&LightBasePassSlots::Lights, 2), + /// (&LightCullComputePassSlots::LightIndicesGridGroup, 3), + /// (&BasePassSlots::ScreenSize, 4), + /// ], + /// ); + /// ``` + /// + /// # Panics + /// Panics if a bind group of a provided name is not found. + pub fn set_bind_groups<'a>( + &'a self, + pass: &mut ComputePass<'a>, + bind_groups: &[(&dyn RenderGraphLabel, u32)], + ) { + for (label, index) in bind_groups { + let bg = self + .bind_group(label.rc_clone()); + //.expect(&format!("Could not find bind group '{:?}'", label)); + + pass.set_bind_group(*index, bg, &[]); + } + } +} + +/// A queued write to a GPU buffer targeting a graph slot. +pub(crate) struct GraphBufferWrite { + /// The name of the slot that has the resource that will be written + target_slot: RenderGraphLabelValue, + offset: u64, + bytes: Vec, +} + +#[allow(dead_code)] +pub struct RenderGraphContext<'a> { + /// The [`wgpu::CommandEncoder`] used to encode GPU operations. + /// + /// This is `None` during the `prepare` stage. + pub encoder: Option, + /// The gpu device that is being used. + pub device: &'a wgpu::Device, + pub queue: &'a wgpu::Queue, + pub(crate) buffer_writes: VecDeque, + renderpass_desc: Vec>, + /// The label of this Node. + pub label: RenderGraphLabelValue, +} + +impl<'a> RenderGraphContext<'a> { + pub(crate) fn new(device: &'a wgpu::Device, queue: &'a wgpu::Queue, encoder: Option, label: RenderGraphLabelValue) -> Self { + Self { + encoder, + device, + queue, + buffer_writes: Default::default(), + renderpass_desc: vec![], + label, + } + } + + pub fn begin_render_pass( + &'a mut self, + desc: wgpu::RenderPassDescriptor<'a, 'a>, + ) -> wgpu::RenderPass { + self.encoder + .as_mut() + .expect( + "RenderGraphContext is missing a command encoder. This is likely \ + because you are trying to run render commands in the prepare stage.", + ) + .begin_render_pass(&desc) + } + + pub fn begin_compute_pass(&mut self, desc: &wgpu::ComputePassDescriptor) -> wgpu::ComputePass { + self.encoder + .as_mut() + .expect( + "RenderGraphContext is missing a command encoder. This is likely \ + because you are trying to run render commands in the prepare stage.", + ) + .begin_compute_pass(desc) + } + + /// Queue a data write to a buffer at that is contained in `target_slot`. + /// + /// This does not submit the data to the GPU immediately, or add it to the `wgpu::Queue`. The + /// data will be submitted to the GPU queue right after the prepare stage for all passes + /// is ran. + #[instrument(skip(self, bytes), level="trace", fields(size = bytes.len()))] + pub fn queue_buffer_write(&mut self, target_slot: impl RenderGraphLabel, offset: u64, bytes: &[u8]) { + self.buffer_writes.push_back(GraphBufferWrite { + target_slot: target_slot.into(), + offset, + bytes: bytes.to_vec(), + }) + } + + /// Queue a data write of a type that to a buffer at that is contained in `target_slot`. + #[instrument(skip(self, bytes), level="trace", fields(size = std::mem::size_of::()))] + pub fn queue_buffer_write_with( + &mut self, + target_slot: impl RenderGraphLabel, + offset: u64, + bytes: T, + ) { + self.queue_buffer_write(target_slot, offset, bytemuck::bytes_of(&bytes)); + } +} diff --git a/lyra-game/src/render/graph/node.rs b/lyra-game/src/render/graph/node.rs new file mode 100644 index 0000000..806594e --- /dev/null +++ b/lyra-game/src/render/graph/node.rs @@ -0,0 +1,365 @@ +use std::{cell::{Ref, RefCell, RefMut}, num::NonZeroU32, rc::Rc}; + +use bind_match::bind_match; +use lyra_ecs::World; + +use crate::render::resource::PipelineDescriptor; + +use super::{RenderGraph, RenderGraphContext, RenderGraphLabel, RenderGraphLabelValue, RenderTarget}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum NodeType { + /// A node doesn't render, compute, or present anything. This likely means it injects data into the graph. + #[default] + Node, + /// A Compute pass node type. + Compute, + /// A render pass node type. + Render, + /// A node that presents render results to a render target. + Presenter, +} + +impl NodeType { + /// Returns a boolean indicating if the node should have a [`Pipeline`](crate::render::resource::Pipeline). + pub fn should_have_pipeline(&self) -> bool { + match self { + NodeType::Node => false, + NodeType::Compute => true, + NodeType::Render => true, + NodeType::Presenter => false, + } + } +} + +/// The type of data that is stored in a [`Node`] slot. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SlotType { + TextureView, + Sampler, + Texture, + Buffer, + RenderTarget, +} + +/// The value of a slot in a [`Node`]. +#[derive(Debug, Clone)] +pub enum SlotValue { + /// This slot doesn't have any value + None, + /// The value will be set during a later phase of the render graph. To see the type of value + /// this will be set to, see the slots type. + Lazy, + TextureView(Rc), + Sampler(Rc), + Texture(Rc), + Buffer(Rc), + RenderTarget(Rc>), +} + +impl SlotValue { + pub fn as_texture_view(&self) -> Option<&Rc> { + bind_match!(self, Self::TextureView(v) => v) + } + + pub fn as_sampler(&self) -> Option<&Rc> { + bind_match!(self, Self::Sampler(v) => v) + } + + pub fn as_texture(&self) -> Option<&Rc> { + bind_match!(self, Self::Texture(v) => v) + } + + pub fn as_buffer(&self) -> Option<&Rc> { + bind_match!(self, Self::Buffer(v) => v) + } + + pub fn as_render_target(&self) -> Option> { + bind_match!(self, Self::RenderTarget(v) => v.borrow()) + } + + pub fn as_render_target_mut(&mut self) -> Option> { + bind_match!(self, Self::RenderTarget(v) => v.borrow_mut()) + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SlotAttribute { + /// This slot inputs a value into the node, expecting another node to `Output` it. + Input, + /// This slot outputs a value from the node, providing the value to other nodes that + /// `Input`it. + Output, +} + +#[derive(Clone)] +pub struct NodeSlot { + /// The type of the value that this slot inputs/outputs. + pub ty: SlotType, + /// The way this slot uses the value. Defines if this slot is an output or input. + pub attribute: SlotAttribute, + /// The identifying label of this slot. + pub label: RenderGraphLabelValue, + /// The value of the slot. + /// This is `None` if the slot is a `SlotAttribute::Input` type. + pub value: Option, +} + +#[derive(Clone)] +pub struct PipelineShaderDesc { + pub label: Option, + pub source: String, + pub entry_point: String, +} + +#[derive(Clone)] +pub struct RenderGraphPipelineInfo { + /// Debug label of the pipeline. This will show up in graphics debuggers for easy identification. + pub label: Option, + /// The layout of bind groups for this pipeline. + pub bind_group_layouts: Vec>, + /// The descriptor of the vertex shader. + pub vertex: PipelineShaderDesc, + /// The properties of the pipeline at the primitive assembly and rasterization level. + pub primitive: wgpu::PrimitiveState, + /// The effect of draw calls on the depth and stencil aspects of the output target, if any. + pub depth_stencil: Option, + /// The multi-sampling properties of the pipeline. + pub multisample: wgpu::MultisampleState, + /// The compiled fragment stage, its entry point, and the color targets. + pub fragment: Option, + /// If the pipeline will be used with a multiview render pass, this indicates how many array + /// layers the attachments will have. + pub multiview: Option, +} + +impl RenderGraphPipelineInfo { + pub fn new( + label: &str, + bind_group_layouts: Vec, + vertex: PipelineShaderDesc, + primitive: wgpu::PrimitiveState, + depth_stencil: Option, + multisample: wgpu::MultisampleState, + fragment: Option, + multiview: Option, + ) -> Self { + Self { + label: Some(label.to_string()), + bind_group_layouts: bind_group_layouts + .into_iter() + .map(|bgl| Rc::new(bgl)) + .collect(), + vertex, + primitive, + depth_stencil, + multisample, + fragment, + multiview, + } + } +} + +/// Descriptor of a Node in a [`RenderGraph`]. +pub struct NodeDesc { + /// The [`NodeType`] of the node. + pub ty: NodeType, + /// The slots that the Node uses. + /// This defines the resources that the node uses and creates in the graph. + pub slots: Vec, + //slot_label_lookup: HashMap, + /// An optional pipeline descriptor for the Node. + /// This is `None` if the Node type is not a node that requires a pipeline + /// (see [`NodeType::should_have_pipeline`]). + pub pipeline_desc: Option, + /// The bind groups that this Node creates. + /// This makes the bind groups accessible to other Nodes. + pub bind_groups: Vec<( + RenderGraphLabelValue, + Rc, + Option>, + )>, +} + +impl NodeDesc { + /// Create a new node descriptor. + pub fn new( + pass_type: NodeType, + pipeline_desc: Option, + bind_groups: Vec<(&dyn RenderGraphLabel, Rc, Option>)>, + ) -> Self { + Self { + ty: pass_type, + slots: vec![], + pipeline_desc, + bind_groups: bind_groups + .into_iter() + .map(|bg| (bg.0.rc_clone().into(), bg.1, bg.2)) + .collect(), + } + } + + /// Add a slot to the descriptor. + /// + /// In debug builds, there is an assert that triggers if the slot is an input slot and has + /// a value set. + pub fn add_slot(&mut self, slot: NodeSlot) { + debug_assert!( + !(slot.attribute == SlotAttribute::Input && slot.value.is_some()), + "input slots should not have values" + ); + + self.slots.push(slot); + } + + /// Add a buffer slot to the descriptor. + /// + /// In debug builds, there is an assert that triggers if the slot is an input slot and has + /// a value set. There is also an assert that is triggered if this slot value is not `None`, + /// `SlotValue::Lazy` or a `Buffer`. + #[inline(always)] + pub fn add_buffer_slot( + &mut self, + label: L, + attribute: SlotAttribute, + value: Option, + ) { + debug_assert!( + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Buffer(_))), + "slot value is not a buffer" + ); + + let slot = NodeSlot { + label: label.into(), + ty: SlotType::Buffer, + attribute, + value, + }; + self.add_slot(slot); + } + + /// Add a slot that stores a [`wgpu::Texture`] to the descriptor. + /// + /// In debug builds, there is an assert that triggers if the slot is an input slot and has + /// a value set. There is also an assert that is triggered if this slot value is not `None`, + /// `SlotValue::Lazy` or a `SlotValue::Texture`. + #[inline(always)] + pub fn add_texture_slot( + &mut self, + label: L, + attribute: SlotAttribute, + value: Option, + ) { + debug_assert!( + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Texture(_))), + "slot value is not a texture" + ); + + let slot = NodeSlot { + label: label.into(), + ty: SlotType::Texture, + attribute, + value, + }; + self.add_slot(slot); + } + + /// Add a slot that stores a [`wgpu::TextureView`] to the descriptor. + /// + /// In debug builds, there is an assert that triggers if the slot is an input slot and has + /// a value set. There is also an assert that is triggered if this slot value is not `None`, + /// `SlotValue::Lazy` or a `SlotValue::TextureView`. + #[inline(always)] + pub fn add_texture_view_slot( + &mut self, + label: L, + attribute: SlotAttribute, + value: Option, + ) { + debug_assert!( + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::TextureView(_))), + "slot value is not a texture view" + ); + + let slot = NodeSlot { + label: label.into(), + ty: SlotType::TextureView, + attribute, + value, + }; + self.add_slot(slot); + } + + /// Add a slot that stores a [`wgpu::Sampler`] to the descriptor. + /// + /// In debug builds, there is an assert that triggers if the slot is an input slot and has + /// a value set. There is also an assert that is triggered if this slot value is not `None`, + /// `SlotValue::Lazy` or a `SlotValue::Sampler`. + #[inline(always)] + pub fn add_sampler_slot( + &mut self, + label: L, + attribute: SlotAttribute, + value: Option, + ) { + debug_assert!( + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Sampler(_))), + "slot value is not a sampler" + ); + + let slot = NodeSlot { + label: label.into(), + ty: SlotType::Sampler, + attribute, + value, + }; + self.add_slot(slot); + } + + /// Returns all input slots that the descriptor defines. + pub fn input_slots(&self) -> Vec<&NodeSlot> { + self.slots + .iter() + .filter(|s| s.attribute == SlotAttribute::Input) + .collect() + } + + /// Returns all output slots that the descriptor defines. + pub fn output_slots(&self) -> Vec<&NodeSlot> { + self.slots + .iter() + .filter(|s| s.attribute == SlotAttribute::Output) + .collect() + } +} + +/// A node that can be executed and scheduled in a [`RenderGraph`]. +/// +/// A node can be used for rendering, computing data on the GPU, collecting data from the main +/// world and writing it to GPU buffers, or presenting renders to a surface. +/// +/// The [`RenderGraph`] is ran in phases. The first phase is `prepare`, then `execute`. When a node +/// is first added to a RenderGraph, its [`Node::desc`] function will be ran. The descriptor +/// describes all resources the node requires for execution during the `execute` phase. +pub trait Node: 'static { + /// Retrieve a descriptor of the Node. + fn desc<'a, 'b>(&'a mut self, graph: &'b mut RenderGraph) -> NodeDesc; + + /// Prepare the node for rendering. + /// + /// This phase runs before `execute` and is meant to be used to collect data from the World + /// and write to GPU buffers. + fn prepare(&mut self, world: &mut World, context: &mut RenderGraphContext); + + /// Execute the node. + /// + /// Parameters: + /// * `graph` - The RenderGraph that this node is a part of. Can be used to get bind groups and bind to them. + /// * `desc` - The descriptor of this node. + /// * `context` - The rendering graph context. + fn execute( + &mut self, + graph: &mut RenderGraph, + desc: &NodeDesc, + context: &mut RenderGraphContext, + ); +} diff --git a/lyra-game/src/render/graph/passes/base.rs b/lyra-game/src/render/graph/passes/base.rs new file mode 100644 index 0000000..e524398 --- /dev/null +++ b/lyra-game/src/render/graph/passes/base.rs @@ -0,0 +1,199 @@ +use std::{cell::RefCell, rc::Rc}; + +use glam::UVec2; +use lyra_game_derive::RenderGraphLabel; +use tracing::warn; +use winit::dpi::PhysicalSize; + +use crate::{ + render::{ + camera::{CameraUniform, RenderCamera}, + graph::{ + RenderGraphContext, Node, NodeDesc, NodeSlot, + NodeType, RenderTarget, SlotAttribute, SlotType, SlotValue, + }, + render_buffer::BufferWrapper, texture::RenderTexture, + }, + scene::CameraComponent, +}; + +#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)] +pub struct BasePassLabel; + +#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)] +pub enum BasePassSlots { + DepthTexture, + ScreenSize, + Camera, + MainRenderTarget, + WindowTextureView, + DepthTextureView, +} + +/// Supplies some basic things other passes needs. +/// +/// screen size buffer, camera buffer, +#[derive(Default)] +pub struct BasePass { + /// Temporary storage for the main render target + /// + /// This should be Some when the pass is first created then after its added to + /// the render graph it will be None and stay None. + temp_render_target: Option, + screen_size: glam::UVec2, +} + +impl BasePass { + pub fn new(surface: wgpu::Surface, surface_config: wgpu::SurfaceConfiguration) -> Self { + let size = glam::UVec2::new(surface_config.width, surface_config.height); + + Self { + temp_render_target: Some(RenderTarget { + surface, + surface_config, + current_texture: None, + }), + screen_size: size, + ..Default::default() + } + } +} + +impl Node for BasePass { + fn desc( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + ) -> crate::render::graph::NodeDesc { + let render_target = self.temp_render_target.take().unwrap(); + self.screen_size = UVec2::new( + render_target.surface_config.width, + render_target.surface_config.height, + ); + + let (screen_size_bgl, screen_size_bg, screen_size_buf, _) = BufferWrapper::builder() + .buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST) + .label_prefix("ScreenSize") + .visibility(wgpu::ShaderStages::COMPUTE) + .buffer_dynamic_offset(false) + .contents(&[self.screen_size]) + .finish_parts(&graph.device()); + let screen_size_bgl = Rc::new(screen_size_bgl); + let screen_size_bg = Rc::new(screen_size_bg); + + let (camera_bgl, camera_bg, camera_buf, _) = BufferWrapper::builder() + .buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST) + .label_prefix("camera") + .visibility(wgpu::ShaderStages::all()) + .buffer_dynamic_offset(false) + .contents(&[CameraUniform::default()]) + .finish_parts(&graph.device()); + let camera_bgl = Rc::new(camera_bgl); + let camera_bg = Rc::new(camera_bg); + + // create the depth texture using the utility struct, then take all the required fields + let mut depth_texture = RenderTexture::create_depth_texture(&graph.device(), &render_target.surface_config, "depth_texture"); + depth_texture.create_bind_group(&graph.device); + + let dt_bg_pair = depth_texture.bindgroup_pair.unwrap(); + let depth_texture_bg = Rc::new(dt_bg_pair.bindgroup); + let depth_texture_bgl = dt_bg_pair.layout; + let depth_texture_view = Rc::new(depth_texture.view); + + let mut desc = NodeDesc::new( + NodeType::Node, + None, + vec![ + // TODO: Make this a trait maybe? + // Could impl it for (RenderGraphLabel, wgpu::BindGroup) and also + // (RenderGraphLabel, wgpu::BindGroup, wgpu::BindGroupLabel) AND + // (RenderGraphLabel, wgpu::BindGroup, Option) + // + // This could make it slightly easier to create this + (&BasePassSlots::DepthTexture, depth_texture_bg, Some(depth_texture_bgl)), + (&BasePassSlots::ScreenSize, screen_size_bg, Some(screen_size_bgl)), + (&BasePassSlots::Camera, camera_bg, Some(camera_bgl)), + ], + ); + + desc.add_slot(NodeSlot { + ty: SlotType::RenderTarget, + attribute: SlotAttribute::Output, + label: BasePassSlots::MainRenderTarget.into(), + value: Some(SlotValue::RenderTarget(Rc::new(RefCell::new( + render_target, + )))), + }); + desc.add_texture_view_slot( + BasePassSlots::WindowTextureView, + SlotAttribute::Output, + Some(SlotValue::Lazy), + ); + desc.add_texture_view_slot( + BasePassSlots::DepthTextureView, + SlotAttribute::Output, + Some(SlotValue::TextureView(depth_texture_view)), + ); + desc.add_buffer_slot( + BasePassSlots::ScreenSize, + SlotAttribute::Output, + Some(SlotValue::Buffer(Rc::new(screen_size_buf))), + ); + desc.add_buffer_slot( + BasePassSlots::Camera, + SlotAttribute::Output, + Some(SlotValue::Buffer(Rc::new(camera_buf))), + ); + + desc + } + + fn prepare(&mut self, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) { + if let Some(camera) = world.view_iter::<&mut CameraComponent>().next() { + let mut render_cam = + RenderCamera::new(PhysicalSize::new(self.screen_size.x, self.screen_size.y)); + let uniform = render_cam.calc_view_projection(&camera); + + context.queue_buffer_write_with(BasePassSlots::Camera, 0, uniform) + } else { + warn!("Missing camera!"); + } + } + + fn execute( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + _desc: &crate::render::graph::NodeDesc, + context: &mut crate::render::graph::RenderGraphContext, + ) { + let tv_slot = graph + .slot_value_mut(BasePassSlots::MainRenderTarget) + .expect("somehow the main render target slot is missing"); + let mut rt = tv_slot.as_render_target_mut().unwrap(); + debug_assert!( + !rt.current_texture.is_some(), + "main render target surface was not presented!" + ); + + // update the screen size buffer if the size changed. + if rt.surface_config.width != self.screen_size.x + || rt.surface_config.height != self.screen_size.y + { + self.screen_size = UVec2::new(rt.surface_config.width, rt.surface_config.height); + context.queue_buffer_write_with(BasePassSlots::ScreenSize, 0, self.screen_size) + } + + let surface_tex = rt.surface.get_current_texture().unwrap(); + let view = surface_tex + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + rt.current_texture = Some(surface_tex); + drop(rt); // must be manually dropped for borrow checker when getting texture view slot + + // store the surface texture to the slot + let tv_slot = graph + .slot_value_mut(BasePassSlots::WindowTextureView) + .expect("somehow the window texture view slot is missing"); + *tv_slot = SlotValue::TextureView(Rc::new(view)); + } +} diff --git a/lyra-game/src/render/graph/passes/light_base.rs b/lyra-game/src/render/graph/passes/light_base.rs new file mode 100644 index 0000000..62c3416 --- /dev/null +++ b/lyra-game/src/render/graph/passes/light_base.rs @@ -0,0 +1,74 @@ +use lyra_game_derive::RenderGraphLabel; + +use crate::render::{ + graph::{ + RenderGraphContext, Node, NodeDesc, NodeType, SlotAttribute, + SlotValue, + }, + light::LightUniformBuffers, +}; + +#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)] +pub struct LightBasePassLabel; + +#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)] +pub enum LightBasePassSlots { + Lights +} + +/// Supplies some basic things other passes needs. +/// +/// screen size buffer, camera buffer, +#[derive(Default)] +pub struct LightBasePass { + light_buffers: Option, +} + +impl LightBasePass { + pub fn new() -> Self { + Self::default() + } +} + +impl Node for LightBasePass { + fn desc( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + ) -> crate::render::graph::NodeDesc { + let device = &graph.device; + self.light_buffers = Some(LightUniformBuffers::new(device)); + let light_buffers = self.light_buffers.as_ref().unwrap(); + + let mut desc = NodeDesc::new( + NodeType::Node, + None, + vec![( + &LightBasePassSlots::Lights, + light_buffers.bind_group.clone(), + Some(light_buffers.bind_group_layout.clone()), + )], + ); + + desc.add_buffer_slot( + LightBasePassSlots::Lights, + SlotAttribute::Output, + Some(SlotValue::Buffer(light_buffers.buffer.clone())), + ); + + desc + } + + fn prepare(&mut self, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) { + let tick = world.current_tick(); + let lights = self.light_buffers.as_mut().unwrap(); + lights.update_lights(context.queue, tick, world); + } + + fn execute( + &mut self, + _graph: &mut crate::render::graph::RenderGraph, + _desc: &crate::render::graph::NodeDesc, + _context: &mut crate::render::graph::RenderGraphContext, + ) { + } +} diff --git a/lyra-game/src/render/graph/passes/light_cull_compute.rs b/lyra-game/src/render/graph/passes/light_cull_compute.rs new file mode 100644 index 0000000..7b5c487 --- /dev/null +++ b/lyra-game/src/render/graph/passes/light_cull_compute.rs @@ -0,0 +1,252 @@ +use std::{mem, rc::Rc}; + +use lyra_ecs::World; +use lyra_game_derive::RenderGraphLabel; +use wgpu::util::DeviceExt; + +use crate::render::{ + graph::{ + RenderGraphContext, Node, NodeDesc, NodeType, SlotAttribute, + SlotValue, + }, + resource::{ComputePipelineDescriptor, PipelineDescriptor, Shader}, +}; + +use super::{BasePassSlots, LightBasePassSlots}; + +#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)] +pub struct LightCullComputePassLabel; + +#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)] +pub enum LightCullComputePassSlots { + LightGridTexture, + LightGridTextureView, + IndexCounterBuffer, + LightIndicesGridGroup, +} + +pub struct LightCullComputePass { + workgroup_size: glam::UVec2, +} + +impl LightCullComputePass { + pub fn new(screen_size: winit::dpi::PhysicalSize) -> Self { + Self { + workgroup_size: glam::UVec2::new(screen_size.width, screen_size.height), + } + } +} + +impl Node for LightCullComputePass { + fn desc( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + ) -> crate::render::graph::NodeDesc { + let shader = Rc::new(Shader { + label: Some("light_cull_comp_shader".into()), + source: include_str!("../../shaders/light_cull.comp.wgsl").to_string(), + }); + + // get the size of the work group for the grid + let main_rt = graph + .slot_value(BasePassSlots::MainRenderTarget) + .and_then(|s| s.as_render_target()) + .expect("missing main render target"); + self.workgroup_size = + glam::UVec2::new(main_rt.surface_config.width, main_rt.surface_config.height); + + // initialize some buffers with empty data + let mut contents = Vec::::new(); + let contents_len = + self.workgroup_size.x * self.workgroup_size.y * mem::size_of::() as u32; + contents.resize(contents_len as _, 0); + + let device = graph.device(); + let light_indices_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("light_indices_buffer"), + contents: &contents, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + }); + + let light_index_counter_buffer = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("light_index_counter_buffer"), + contents: &bytemuck::cast_slice(&[0]), + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + }); + + let light_indices_bg_layout = Rc::new(device.create_bind_group_layout( + &wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: false }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::ReadWrite, + format: wgpu::TextureFormat::Rg32Uint, // vec2 + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: false }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + label: Some("light_indices_grid_bgl"), + }, + )); + + let size = wgpu::Extent3d { + width: self.workgroup_size.x, + height: self.workgroup_size.y, + depth_or_array_layers: 1, + }; + let grid_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("light_grid_tex"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rg32Uint, // vec2 + usage: wgpu::TextureUsages::STORAGE_BINDING, + view_formats: &[], + }); + + let grid_texture_view = grid_texture.create_view(&wgpu::TextureViewDescriptor { + label: Some("light_grid_texview"), + format: Some(wgpu::TextureFormat::Rg32Uint), // vec2 + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let light_indices_bg = Rc::new(device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &light_indices_bg_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &light_indices_buffer, + offset: 0, + size: None, // the entire light buffer is needed + }), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&grid_texture_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &light_index_counter_buffer, + offset: 0, + size: None, // the entire light buffer is needed + }), + }, + ], + label: Some("light_indices_grid_bind_group"), + })); + + drop(main_rt); + + let depth_tex_bgl = graph.bind_group_layout(BasePassSlots::DepthTexture); + let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera); + let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights); + let screen_size_bgl = graph.bind_group_layout(BasePassSlots::ScreenSize); + + let mut desc = NodeDesc::new( + NodeType::Compute, + Some(PipelineDescriptor::Compute(ComputePipelineDescriptor { + label: Some("light_cull_pipeline".into()), + push_constant_ranges: vec![], + layouts: vec![ + depth_tex_bgl.clone(), + camera_bgl.clone(), + lights_bgl.clone(), + light_indices_bg_layout.clone(), + screen_size_bgl.clone(), + ], + shader, + shader_entry_point: "cs_main".into(), + })), + vec![( + &LightCullComputePassSlots::LightIndicesGridGroup, + light_indices_bg, + Some(light_indices_bg_layout), + )], + ); + + desc.add_texture_view_slot( + BasePassSlots::WindowTextureView, + SlotAttribute::Input, + None, + ); + desc.add_buffer_slot( + BasePassSlots::ScreenSize, + SlotAttribute::Input, + None, + ); + desc.add_buffer_slot(BasePassSlots::Camera, SlotAttribute::Input, None); + desc.add_buffer_slot( + LightCullComputePassSlots::IndexCounterBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(Rc::new(light_index_counter_buffer))), + ); + + desc + } + + fn prepare(&mut self, _world: &mut World, _context: &mut RenderGraphContext) {} + + fn execute( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + _: &crate::render::graph::NodeDesc, + context: &mut RenderGraphContext, + ) { + let label = context.label.clone(); + + let pipeline = graph.pipeline(label) + .expect("Failed to find Pipeline for LightCullComputePass"); + let pipeline = pipeline.as_compute(); + + let mut pass = context.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("light_cull_pass"), + }); + + pass.set_pipeline(pipeline); + + graph.set_bind_groups( + &mut pass, + &[ + (&BasePassSlots::DepthTexture, 0), + (&BasePassSlots::Camera, 1), + (&LightBasePassSlots::Lights, 2), + (&LightCullComputePassSlots::LightIndicesGridGroup, 3), + (&BasePassSlots::ScreenSize, 4), + ], + ); + + pass.dispatch_workgroups(self.workgroup_size.x, self.workgroup_size.y, 1); + } +} diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs new file mode 100644 index 0000000..c773ecb --- /dev/null +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -0,0 +1,556 @@ +use std::{collections::{HashSet, VecDeque}, rc::Rc}; + +use glam::Vec3; +use itertools::izip; +use lyra_ecs::{query::{filter::{Has, Not, Or}, Entities, Res, TickOf}, relation::{ChildOf, RelationOriginComponent}, Component, Entity}; +use lyra_game_derive::RenderGraphLabel; +use lyra_math::Transform; +use lyra_resource::{gltf::Mesh, ResHandle}; +use lyra_scene::{SceneGraph, WorldTransform}; +use rustc_hash::FxHashMap; +use tracing::{debug, instrument, warn}; +use uuid::Uuid; +use wgpu::util::DeviceExt; + +use crate::{ + render::{ + desc_buf_lay::DescVertexBufferLayout, graph::{ + RenderGraphContext, Node, NodeDesc, + NodeType, + }, material::{Material, MaterialUniform}, render_buffer::{BufferStorage, BufferWrapper}, render_job::RenderJob, resource::{FragmentState, PipelineDescriptor, RenderPipelineDescriptor, Shader, VertexState}, texture::RenderTexture, transform_buffer_storage::{TransformBuffers, TransformGroup}, vertex::Vertex + }, + DeltaTime, +}; + +use super::{BasePassSlots, LightBasePassSlots, LightCullComputePassSlots}; + +type MeshHandle = ResHandle; +type SceneHandle = ResHandle; + +#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)] +pub struct MeshesPassLabel; + +#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)] +pub enum MeshesPassSlots { + Material +} + +struct MeshBufferStorage { + buffer_vertex: BufferStorage, + buffer_indices: Option<(wgpu::IndexFormat, BufferStorage)>, + + // maybe this should just be a Uuid and the material can be retrieved though + // MeshPass's `material_buffers` field? + material: Option>, +} + +#[derive(Clone, Debug, Component)] +struct InterpTransform { + last_transform: Transform, + alpha: f32, +} + +#[derive(Default)] +pub struct MeshPass { + transforms: Option, + mesh_buffers: FxHashMap, + render_jobs: VecDeque, + + texture_bind_group_layout: Option>, + material_buffer: Option, + material_buffers: FxHashMap>, + entity_meshes: FxHashMap, + + default_texture: Option, +} + +impl MeshPass { + pub fn new() -> Self { + Self::default() + } + + /// Checks if the mesh buffers in the GPU need to be updated. + #[instrument(skip(self, device, queue, mesh_han))] + fn check_mesh_buffers(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, mesh_han: &ResHandle) { + let mesh_uuid = mesh_han.uuid(); + + if let (Some(mesh), Some(buffers)) = (mesh_han.data_ref(), self.mesh_buffers.get_mut(&mesh_uuid)) { + // check if the buffer sizes dont match. If they dont, completely remake the buffers + let vertices = mesh.position().unwrap(); + if buffers.buffer_vertex.count() != vertices.len() { + debug!("Recreating buffers for mesh {}", mesh_uuid.to_string()); + let (vert, idx) = self.create_vertex_index_buffers(device, &mesh); + + // have to re-get buffers because of borrow checker + let buffers = self.mesh_buffers.get_mut(&mesh_uuid).unwrap(); + buffers.buffer_indices = idx; + buffers.buffer_vertex = vert; + + return; + } + + // update vertices + let vertex_buffer = buffers.buffer_vertex.buffer(); + let vertices = vertices.as_slice(); + // align the vertices to 4 bytes (u32 is 4 bytes, which is wgpu::COPY_BUFFER_ALIGNMENT) + let (_, vertices, _) = bytemuck::pod_align_to::(vertices); + queue.write_buffer(vertex_buffer, 0, bytemuck::cast_slice(vertices)); + + // update the indices if they're given + if let Some(index_buffer) = buffers.buffer_indices.as_ref() { + let aligned_indices = match mesh.indices.as_ref().unwrap() { + // U16 indices need to be aligned to u32, for wpgu, which are 4-bytes in size. + lyra_resource::gltf::MeshIndices::U16(v) => bytemuck::pod_align_to::(v).1, + lyra_resource::gltf::MeshIndices::U32(v) => bytemuck::pod_align_to::(v).1, + }; + + let index_buffer = index_buffer.1.buffer(); + queue.write_buffer(index_buffer, 0, bytemuck::cast_slice(aligned_indices)); + } + } + } + + #[instrument(skip(self, device, mesh))] + fn create_vertex_index_buffers(&mut self, device: &wgpu::Device, mesh: &Mesh) -> (BufferStorage, Option<(wgpu::IndexFormat, BufferStorage)>) { + let positions = mesh.position().unwrap(); + let tex_coords: Vec = mesh.tex_coords().cloned() + .unwrap_or_else(|| vec![glam::Vec2::new(0.0, 0.0); positions.len()]); + let normals = mesh.normals().unwrap(); + + assert!(positions.len() == tex_coords.len() && positions.len() == normals.len()); + + let mut vertex_inputs = vec![]; + for (v, t, n) in izip!(positions.iter(), tex_coords.iter(), normals.iter()) { + vertex_inputs.push(Vertex::new(*v, *t, *n)); + } + + let vertex_buffer = device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(vertex_inputs.as_slice()),//vertex_combined.as_slice(), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages:: COPY_DST, + } + ); + let vertex_buffer = BufferStorage::new(vertex_buffer, 0, vertex_inputs.len()); + + let indices = match mesh.indices.as_ref() { + Some(indices) => { + let (idx_type, len, contents) = match indices { + lyra_resource::gltf::MeshIndices::U16(v) => (wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v)), + lyra_resource::gltf::MeshIndices::U32(v) => (wgpu::IndexFormat::Uint32, v.len(), bytemuck::cast_slice(v)), + }; + + let index_buffer = device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("Index Buffer"), + contents, + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages:: COPY_DST, + } + ); + + let buffer_indices = BufferStorage::new(index_buffer, 0, len); + + Some((idx_type, buffer_indices)) + }, + None => { + None + } + }; + + ( vertex_buffer, indices ) + } + + #[instrument(skip(self, device, queue, mesh))] + fn create_mesh_buffers(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, mesh: &Mesh) -> MeshBufferStorage { + let (vertex_buffer, buffer_indices) = self.create_vertex_index_buffers(device, mesh); + + let material = mesh.material.as_ref() + .expect("Material resource not loaded yet"); + let material_ref = material.data_ref() + .unwrap(); + + let material = self.material_buffers.entry(material.uuid()) + .or_insert_with(|| { + debug!(uuid=material.uuid().to_string(), "Sending material to gpu"); + Rc::new(Material::from_resource(&device, &queue, self.texture_bind_group_layout.clone().unwrap(), &material_ref)) + }); + + // TODO: support material uniforms from multiple uniforms + let uni = MaterialUniform::from(&**material); + queue.write_buffer(&self.material_buffer.as_ref().unwrap(), 0, bytemuck::bytes_of(&uni)); + + MeshBufferStorage { + buffer_vertex: vertex_buffer, + buffer_indices, + material: Some(material.clone()), + } + } + + /// Processes the mesh for the renderer, storing and creating buffers as needed. Returns true if a new mesh was processed. + #[instrument(skip(self, device, queue, mesh, entity))] + fn process_mesh(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, entity: Entity, mesh: &Mesh, mesh_uuid: Uuid) -> bool { + #[allow(clippy::map_entry)] + if !self.mesh_buffers.contains_key(&mesh_uuid) { + // create the mesh's buffers + let buffers = self.create_mesh_buffers(device, queue, mesh); + self.mesh_buffers.insert(mesh_uuid, buffers); + self.entity_meshes.insert(entity, mesh_uuid); + + true + } else { false } + } +} + +impl Node for MeshPass { + fn desc( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + ) -> crate::render::graph::NodeDesc { + + let device = graph.device(); + + let transforms = TransformBuffers::new(device); + let transform_bgl = transforms.bindgroup_layout.clone(); + self.transforms = Some(transforms); + + let texture_bind_group_layout = Rc::new(RenderTexture::create_layout(&device)); + self.texture_bind_group_layout = Some(texture_bind_group_layout.clone()); + + let (material_bgl, material_bg, material_buf, _) = BufferWrapper::builder() + .label_prefix("material") + .visibility(wgpu::ShaderStages::FRAGMENT) + .buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST) + .contents(&[MaterialUniform::default()]) + .finish_parts(device); + let material_bgl = Rc::new(material_bgl); + let material_bg = Rc::new(material_bg); + + self.material_buffer = Some(material_buf); + + // load the default texture + let bytes = include_bytes!("../../default_texture.png"); + self.default_texture = Some(RenderTexture::from_bytes(&device, &graph.queue, texture_bind_group_layout.clone(), bytes, "default_texture").unwrap()); + + // get surface config format + let main_rt = graph.slot_value(BasePassSlots::MainRenderTarget) + .and_then(|s| s.as_render_target()) + .expect("missing main render target"); + let surface_config_format = main_rt.surface_config.format; + drop(main_rt); + + let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera); + let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights); + let light_grid_bgl = graph + .bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup); + + let shader = Rc::new(Shader { + label: Some("base_shader".into()), + source: include_str!("../../shaders/base.wgsl").to_string(), + }); + + let desc = NodeDesc::new( + NodeType::Render, + Some(PipelineDescriptor::Render(RenderPipelineDescriptor { + label: Some("meshes".into()), + layouts: vec![ + texture_bind_group_layout.clone(), + transform_bgl, + camera_bgl.clone(), + lights_bgl.clone(), + material_bgl.clone(), + texture_bind_group_layout, + light_grid_bgl.clone(), + ], + push_constant_ranges: vec![], + vertex: VertexState { + module: shader.clone(), + entry_point: "vs_main".into(), + buffers: vec![ + Vertex::desc().into(), + ], + }, + fragment: Some(FragmentState { + module: shader, + entry_point: "fs_main".into(), + targets: vec![Some(wgpu::ColorTargetState { + format: surface_config_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + depth_stencil: Some(wgpu::DepthStencilState { + format: RenderTexture::DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), // TODO: stencil buffer + bias: wgpu::DepthBiasState::default(), + }), + primitive: wgpu::PrimitiveState::default(), + multisample: wgpu::MultisampleState::default(), + multiview: None, + })), + vec![ + (&MeshesPassSlots::Material, material_bg, Some(material_bgl)), + ], + ); + + desc + } + + #[instrument(skip(self, world, context))] + fn prepare(&mut self, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) { + let device = context.device; + let queue = context.queue; + let render_limits = device.limits(); + + let last_epoch = world.current_tick(); + let mut alive_entities = HashSet::new(); + + let view = world.view_iter::<( + Entities, + &Transform, + TickOf, + Or< + (&MeshHandle, TickOf), + (&SceneHandle, TickOf) + >, + Option<&mut InterpTransform>, + Res, + )>(); + + // 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); + + // Interpolate the transform for this entity using a component. + // If the entity does not have the component then it will be queued to be added + // to it after all the entities are prepared for rendering. + 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 + } + }; + + { + // expand the transform buffers if they need to be. + // this is done in its own scope to avoid multiple mutable references to self at + // once; aka, make the borrow checker happy + let transforms = self.transforms.as_mut().unwrap(); + if transforms.needs_expand() { + debug!("Expanding transform buffers"); + transforms.expand_buffers(device); + } + } + + 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(device, queue, entity, &*mesh, mesh_han.uuid()) + && mesh_epoch == last_epoch { + self.check_mesh_buffers(device, queue, &mesh_han); + } + + let transforms = self.transforms.as_mut().unwrap(); + let group = TransformGroup::EntityRes(entity, mesh_han.uuid()); + let transform_id = transforms.update_or_push(device, queue, &render_limits, + group, interp_transform.calculate_mat4(), glam::Mat3::from_quat(interp_transform.rotation)); + + let material = mesh.material.as_ref().unwrap() + .data_ref().unwrap(); + let shader = material.shader_uuid.unwrap_or(0); + let job = RenderJob::new(entity, shader, mesh_han.uuid(), transform_id); + self.render_jobs.push_back(job); + } + } + + if let Some((scene_han, scene_epoch)) = scene_pair { + if let Some(scene) = scene_han.data_ref() { + 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(); + } + + for (mesh_han, pos) in scene.world().view_iter::<(&MeshHandle, &WorldTransform)>() { + if let Some(mesh) = mesh_han.data_ref() { + 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 + // dont need to be resent to the gpu. + if !self.process_mesh(device, queue, entity, &*mesh, mesh_han.uuid()) + && scene_epoch == last_epoch { + self.check_mesh_buffers(device, queue, &mesh_han); + } + + let transforms = self.transforms.as_mut().unwrap(); + let scene_mesh_group = TransformGroup::Res(scene_han.uuid(), mesh_han.uuid()); + let group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); + let transform_id = transforms.update_or_push(device, queue, &render_limits, + group, mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation) ); + + let material = mesh.material.as_ref().unwrap() + .data_ref().unwrap(); + let shader = material.shader_uuid.unwrap_or(0); + let job = RenderJob::new(entity, shader, mesh_han.uuid(), transform_id); + self.render_jobs.push_back(job); + } + } + } + } + } + + for (en, interp) in component_queue { + world.insert(en, interp); + } + + let transforms = self.transforms.as_mut().unwrap(); + transforms.send_to_gpu(queue); + } + + fn execute( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + _: &crate::render::graph::NodeDesc, + context: &mut crate::render::graph::RenderGraphContext, + ) { + let encoder = context.encoder.as_mut().unwrap(); + + let view = graph + .slot_value(BasePassSlots::WindowTextureView) + .unwrap() + .as_texture_view() + .expect("BasePassSlots::WindowTextureView was not a TextureView slot"); + + let depth_view = graph + .slot_value(BasePassSlots::DepthTextureView) + .unwrap() + .as_texture_view() + .expect("BasePassSlots::DepthTextureView was not a TextureView slot"); + + let camera_bg = graph + .bind_group(BasePassSlots::Camera); + + let lights_bg = graph + .bind_group(LightBasePassSlots::Lights); + + let light_grid_bg = graph + .bind_group(LightCullComputePassSlots::LightIndicesGridGroup); + + let material_bg = graph + .bind_group(MeshesPassSlots::Material); + + let pipeline = graph.pipeline(context.label.clone()) + .expect("Failed to find pipeline for MeshPass"); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.1, + g: 0.2, + b: 0.3, + a: 1.0, + }), + store: true, + }, + })], + // enable depth buffer + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, + }), + }); + + pass.set_pipeline(&pipeline.as_render()); + + //let material_buffer_bg = self.material_buffer.as_ref().unwrap().bindgroup(); + let default_texture = self.default_texture.as_ref().unwrap(); + let transforms = self.transforms.as_mut().unwrap(); + + while let Some(job) = self.render_jobs.pop_front() { + // get the mesh (containing vertices) and the buffers from storage + let buffers = self.mesh_buffers.get(&job.mesh_uuid); + if buffers.is_none() { + warn!("Skipping job since its mesh is missing {:?}", job.mesh_uuid); + continue; + } + let buffers = buffers.unwrap(); + + // Bind the optional texture + if let Some(tex) = buffers.material.as_ref() + .and_then(|m| m.diffuse_texture.as_ref()) { + pass.set_bind_group(0, tex.bind_group(), &[]); + } else { + pass.set_bind_group(0, default_texture.bind_group(), &[]); + } + + if let Some(tex) = buffers.material.as_ref() + .and_then(|m| m.specular.as_ref()) + .and_then(|s| s.texture.as_ref().or(s.color_texture.as_ref())) { + pass.set_bind_group(5, tex.bind_group(), &[]); + } else { + pass.set_bind_group(5, default_texture.bind_group(), &[]); + } + + // Get the bindgroup for job's transform and bind to it using an offset. + let bindgroup = transforms.bind_group(job.transform_id); + let offset = transforms.buffer_offset(job.transform_id); + pass.set_bind_group(1, bindgroup, &[ offset, ]); + + pass.set_bind_group(2, &camera_bg, &[]); + pass.set_bind_group(3, &lights_bg, &[]); + pass.set_bind_group(4, &material_bg, &[]); + + pass.set_bind_group(6, &light_grid_bg, &[]); + + // if this mesh uses indices, use them to draw the mesh + if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() { + let indices_len = indices.count() as u32; + + pass.set_vertex_buffer(buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..)); + pass.set_index_buffer(indices.buffer().slice(..), *idx_type); + pass.draw_indexed(0..indices_len, 0, 0..1); + } else { + let vertex_count = buffers.buffer_vertex.count(); + + pass.set_vertex_buffer(buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..)); + pass.draw(0..vertex_count as u32, 0..1); + } + } + } +} diff --git a/lyra-game/src/render/graph/passes/mod.rs b/lyra-game/src/render/graph/passes/mod.rs new file mode 100644 index 0000000..967d440 --- /dev/null +++ b/lyra-game/src/render/graph/passes/mod.rs @@ -0,0 +1,14 @@ +mod light_cull_compute; +pub use light_cull_compute::*; + +mod base; +pub use base::*; + +mod meshes; +pub use meshes::*; + +mod light_base; +pub use light_base::*; + +mod present_pass; +pub use present_pass::*; \ No newline at end of file diff --git a/lyra-game/src/render/graph/passes/present_pass.rs b/lyra-game/src/render/graph/passes/present_pass.rs new file mode 100644 index 0000000..af7d52d --- /dev/null +++ b/lyra-game/src/render/graph/passes/present_pass.rs @@ -0,0 +1,64 @@ +use std::hash::Hash; + +use lyra_game_derive::RenderGraphLabel; + +use crate::render::graph::{RenderGraphContext, RenderGraphLabel, RenderGraphLabelValue, Node, NodeDesc, NodeSlot, NodeType, SlotAttribute, SlotType}; + +#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] +pub struct PresentPassLabel(RenderGraphLabelValue); + +impl PresentPassLabel { + pub fn new(label: impl RenderGraphLabel) -> Self { + Self(label.into()) + } +} + +/// Supplies some basic things other passes needs. +/// +/// screen size buffer, camera buffer, +pub struct PresentPass { + /// Label of this pass + pub label: PresentPassLabel, +} + +impl PresentPass { + pub fn new(render_target_slot: impl RenderGraphLabel) -> Self { + Self { + //render_target_slot: render_target_slot.rc_clone(), + label: PresentPassLabel::new(render_target_slot), + } + } +} + +impl Node for PresentPass { + fn desc(&mut self, _graph: &mut crate::render::graph::RenderGraph) -> crate::render::graph::NodeDesc { + let mut desc = NodeDesc::new( + NodeType::Presenter, + None, + vec![], + ); + + desc.add_slot( + NodeSlot { + ty: SlotType::RenderTarget, + attribute: SlotAttribute::Input, + label: self.label.0.clone(), + value: None, + } + ); + + desc + } + + fn prepare(&mut self, _world: &mut lyra_ecs::World, _context: &mut RenderGraphContext) { + + } + + fn execute(&mut self, graph: &mut crate::render::graph::RenderGraph, _desc: &crate::render::graph::NodeDesc, _context: &mut crate::render::graph::RenderGraphContext) { + let mut slot = graph.slot_value_mut(self.label.0.clone()) + .expect(&format!("render target slot '{:?}' for PresentPass is missing", self.label.0)) + .as_render_target_mut().unwrap(); + let surf_tex = slot.current_texture.take().unwrap(); + surf_tex.present(); + } +} \ No newline at end of file diff --git a/lyra-game/src/render/graph/slot_desc.rs b/lyra-game/src/render/graph/slot_desc.rs new file mode 100644 index 0000000..5c56eab --- /dev/null +++ b/lyra-game/src/render/graph/slot_desc.rs @@ -0,0 +1,210 @@ +use std::{mem, num::{NonZeroU32, NonZeroU8}}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TextureViewDescriptor { + /// The label of the texture that this view will be created from. + pub texture_label: String, + /// Format of the texture view. At this time, it must be the same as the underlying format of the texture. + pub format: Option, + /// The dimension of the texture view. For 1D textures, this must be `D1`. For 2D textures it must be one of + /// `D2`, `D2Array`, `Cube`, and `CubeArray`. For 3D textures it must be `D3` + pub dimension: Option, + /// Aspect of the texture. Color textures must be [`TextureAspect::All`]. + pub aspect: wgpu::TextureAspect, + /// Base mip level. + pub base_mip_level: u32, + /// Mip level count. + /// If `Some(count)`, `base_mip_level + count` must be less or equal to underlying texture mip count. + /// If `None`, considered to include the rest of the mipmap levels, but at least 1 in total. + pub mip_level_count: Option, + /// Base array layer. + pub base_array_layer: u32, + /// Layer count. + /// If `Some(count)`, `base_array_layer + count` must be less or equal to the underlying array count. + /// If `None`, considered to include the rest of the array layers, but at least 1 in total. + pub array_layer_count: Option, +} + +impl TextureViewDescriptor { + pub fn default_view(texture_label: &str) -> Self { + let d = wgpu::TextureViewDescriptor::default(); + + Self { + texture_label: texture_label.to_string(), + format: d.format, + dimension: d.dimension, + aspect: d.aspect, + base_array_layer: d.base_array_layer, + base_mip_level: d.base_mip_level, + mip_level_count: d.mip_level_count, + array_layer_count: d.array_layer_count, + } + } + + pub fn as_wgpu<'a>(&self, label: Option<&'a str>) -> wgpu::TextureViewDescriptor<'a> { + wgpu::TextureViewDescriptor { + label, + format: self.format, + dimension: self.dimension, + aspect: self.aspect, + base_mip_level: self.base_mip_level, + mip_level_count: self.mip_level_count, + base_array_layer: self.base_array_layer, + array_layer_count: self.array_layer_count, + } + } +} + + +#[derive(Clone, Debug, PartialEq)] +pub struct SamplerDescriptor { + /// The label of the texture that this view will be created from. + pub texture_label: String, + /// How to deal with out of bounds accesses in the u (i.e. x) direction + pub address_mode_u: wgpu::AddressMode, + /// How to deal with out of bounds accesses in the v (i.e. y) direction + pub address_mode_v: wgpu::AddressMode, + /// How to deal with out of bounds accesses in the w (i.e. z) direction + pub address_mode_w: wgpu::AddressMode, + /// How to filter the texture when it needs to be magnified (made larger) + pub mag_filter: wgpu::FilterMode, + /// How to filter the texture when it needs to be minified (made smaller) + pub min_filter: wgpu::FilterMode, + /// How to filter between mip map levels + pub mipmap_filter: wgpu::FilterMode, + /// Minimum level of detail (i.e. mip level) to use + pub lod_min_clamp: f32, + /// Maximum level of detail (i.e. mip level) to use + pub lod_max_clamp: f32, + /// If this is enabled, this is a comparison sampler using the given comparison function. + pub compare: Option, + /// Valid values: 1, 2, 4, 8, and 16. + pub anisotropy_clamp: Option, + /// Border color to use when address_mode is [`AddressMode::ClampToBorder`] + pub border_color: Option, +} + +impl SamplerDescriptor { + pub fn default_sampler(texture_label: &str) -> Self { + let d = wgpu::SamplerDescriptor::default(); + + Self { + texture_label: texture_label.to_string(), + address_mode_u: d.address_mode_u, + address_mode_v: d.address_mode_v, + address_mode_w: d.address_mode_w, + mag_filter: d.mag_filter, + min_filter: d.min_filter, + mipmap_filter: d.mipmap_filter, + lod_min_clamp: d.lod_min_clamp, + lod_max_clamp: d.lod_max_clamp, + compare: d.compare, + anisotropy_clamp: d.anisotropy_clamp, + border_color: d.border_color, + } + } +} + +#[repr(C)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct TextureDescriptor { + /// Size of the texture. All components must be greater than zero. For a + /// regular 1D/2D texture, the unused sizes will be 1. For 2DArray textures, + /// Z is the number of 2D textures in that array. + pub size: wgpu::Extent3d, + /// Mip count of texture. For a texture with no extra mips, this must be 1. + pub mip_level_count: u32, + /// Sample count of texture. If this is not 1, texture must have [`BindingType::Texture::multisampled`] set to true. + pub sample_count: u32, + /// Dimensions of the texture. + pub dimension: wgpu::TextureDimension, + /// Format of the texture. + pub format: wgpu::TextureFormat, + /// Allowed usages of the texture. If used in other ways, the operation will panic. + pub usage: wgpu::TextureUsages, + /// Specifies what view formats will be allowed when calling create_view() on this texture. + /// + /// View formats of the same format as the texture are always allowed. + /// + /// Note: currently, only the srgb-ness is allowed to change. (ex: Rgba8Unorm texture + Rgba8UnormSrgb view) + pub view_formats: Vec, +} + +impl TextureDescriptor { + pub fn as_wgpu<'a>(&'a self, label: Option<&'a str>) -> wgpu::TextureDescriptor<'a> { + wgpu::TextureDescriptor { + label, + size: self.size, + mip_level_count: self.mip_level_count, + sample_count: self.sample_count, + dimension: self.dimension, + format: self.format, + usage: self.usage, + view_formats: &self.view_formats, + } + } +} + +#[repr(C)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct BufferDescriptor { + /// Size of a buffer. + pub size: wgpu::BufferAddress, + /// Usages of a buffer. If the buffer is used in any way that isn't specified here, the operation + /// will panic. + pub usage: wgpu::BufferUsages, + /// Allows a buffer to be mapped immediately after they are made. It does not have to be [`BufferUsages::MAP_READ`] or + /// [`BufferUsages::MAP_WRITE`], all buffers are allowed to be mapped at creation. + /// + /// If this is `true`, [`size`](#structfield.size) must be a multiple of + /// [`COPY_BUFFER_ALIGNMENT`]. + pub mapped_at_creation: bool, +} + +impl BufferDescriptor { + pub fn new(usage: wgpu::BufferUsages, mapped_at_creation: bool) -> Self { + Self { + size: mem::size_of::() as _, + usage, + mapped_at_creation, + } + } + + pub fn as_wgpu<'a>(&self, label: Option<&'a str>) -> wgpu::BufferDescriptor<'a> { + wgpu::BufferDescriptor { + label, + size: self.size, + usage: self.usage, + mapped_at_creation: self.mapped_at_creation, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct BufferInitDescriptor { + /// Debug label of a buffer. This will show up in graphics debuggers for easy identification. + pub label: Option, + /// Contents of a buffer on creation. + pub contents: Vec, + /// Usages of a buffer. If the buffer is used in any way that isn't specified here, the operation + /// will panic. + pub usage: wgpu::BufferUsages, +} + +impl BufferInitDescriptor { + pub fn new(label: Option<&str>, data: &T, usage: wgpu::BufferUsages) -> Self { + Self { + label: label.map(|s| s.to_string()), + contents: bytemuck::bytes_of(data).to_vec(), + usage, + } + } + + pub fn as_wgpu<'a>(&'a self, label: Option<&'a str>) -> wgpu::util::BufferInitDescriptor<'a> { + wgpu::util::BufferInitDescriptor { + label, + contents: &self.contents, + usage: self.usage, + } + } +} \ No newline at end of file diff --git a/lyra-game/src/render/light/mod.rs b/lyra-game/src/render/light/mod.rs index ca37007..91b9ee6 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -6,14 +6,12 @@ use lyra_ecs::{Entity, Tick, World, query::{Entities, TickOf}}; pub use point::*; pub use spotlight::*; -use std::{collections::{HashMap, VecDeque}, marker::PhantomData, mem}; +use std::{collections::{HashMap, VecDeque}, marker::PhantomData, mem, rc::Rc}; use crate::math::Transform; use self::directional::DirectionalLight; -use super::render_buffer::BindGroupPair; - const MAX_LIGHT_COUNT: usize = 16; /// A struct that stores a list of lights in a wgpu::Buffer. @@ -100,8 +98,10 @@ impl LightBuffer { } pub(crate) struct LightUniformBuffers { - pub buffer: wgpu::Buffer, - pub bind_group_pair: BindGroupPair, + pub buffer: Rc, + //pub bind_group_pair: BindGroupPair, + pub bind_group: Rc, + pub bind_group_layout: Rc, pub light_indexes: HashMap, dead_indices: VecDeque, pub current_light_idx: u32, @@ -158,8 +158,9 @@ impl LightUniformBuffers { }); Self { - buffer, - bind_group_pair: BindGroupPair::new(bindgroup, bindgroup_layout), + buffer: Rc::new(buffer), + bind_group: Rc::new(bindgroup), + bind_group_layout: Rc::new(bindgroup_layout), light_indexes: Default::default(), current_light_idx: 0, dead_indices: VecDeque::new(), diff --git a/lyra-game/src/render/mod.rs b/lyra-game/src/render/mod.rs index d7985d5..d1e39d5 100755 --- a/lyra-game/src/render/mod.rs +++ b/lyra-game/src/render/mod.rs @@ -1,5 +1,5 @@ pub mod renderer; -pub mod render_pipeline; +pub mod resource; pub mod vertex; pub mod desc_buf_lay; pub mod render_buffer; @@ -12,5 +12,6 @@ pub mod camera; pub mod window; pub mod transform_buffer_storage; pub mod light; -pub mod light_cull_compute; -pub mod avec; \ No newline at end of file +//pub mod light_cull_compute; +pub mod avec; +pub mod graph; \ No newline at end of file diff --git a/lyra-game/src/render/render_buffer.rs b/lyra-game/src/render/render_buffer.rs index 794e09e..ed40980 100755 --- a/lyra-game/src/render/render_buffer.rs +++ b/lyra-game/src/render/render_buffer.rs @@ -134,6 +134,15 @@ impl BufferWrapper { "BufferWrapper is missing bindgroup pair! Cannot set bind group on RenderPass!", ).bindgroup } + + /// Take the bind group layout, the bind group, and the buffer out of the wrapper. + pub fn parts(self) -> (Option>, Option, wgpu::Buffer) { + if let Some(pair) = self.bindgroup_pair { + (Some(pair.layout), Some(pair.bindgroup), self.inner_buf) + } else { + (None, None, self.inner_buf) + } + } } /// Struct used for building a BufferWrapper @@ -221,7 +230,7 @@ impl BufferWrapperBuilder { /// * `contents` - The contents to initialize the buffer with. /// /// If a field is missing, a panic will occur. - pub fn finish(self, device: &wgpu::Device) -> BufferWrapper { + pub fn finish_parts(self, device: &wgpu::Device) -> (wgpu::BindGroupLayout, wgpu::BindGroup, wgpu::Buffer, usize) { let buf_usage = self.buffer_usage.expect("Buffer usage was not set"); let buffer = if let Some(contents) = self.contents.as_ref() { device.create_buffer_init( @@ -293,10 +302,25 @@ impl BufferWrapperBuilder { } }; - BufferWrapper { + /* BufferWrapper { bindgroup_pair: Some(bg_pair), inner_buf: buffer, len: Some(self.count.unwrap_or_default() as usize), + } */ + + (Rc::try_unwrap(bg_pair.layout).unwrap(), bg_pair.bindgroup, buffer, self.count.unwrap_or_default() as usize) + } + + pub fn finish(self, device: &wgpu::Device) -> BufferWrapper { + let (bgl, bg, buff, len) = self.finish_parts(device); + + BufferWrapper { + bindgroup_pair: Some(BindGroupPair { + layout: Rc::new(bgl), + bindgroup: bg + }), + inner_buf: buff, + len: Some(len), } } } diff --git a/lyra-game/src/render/render_pipeline.rs b/lyra-game/src/render/render_pipeline.rs deleted file mode 100755 index cf073e9..0000000 --- a/lyra-game/src/render/render_pipeline.rs +++ /dev/null @@ -1,90 +0,0 @@ -use wgpu::{PipelineLayout, RenderPipeline, VertexBufferLayout, BindGroupLayout}; - -use super::texture::RenderTexture; - -pub struct FullRenderPipeline { - layout: PipelineLayout, - wgpu_pipeline: RenderPipeline, -} - -impl FullRenderPipeline { - pub fn new(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration, shader: &wgpu::ShaderModule, buffer_layouts: Vec, bind_group_layouts: Vec<&BindGroupLayout>) -> FullRenderPipeline { - // Extract the layouts from all the jobs - /* let mut buffer_layouts = vec![]; - let mut bind_group_layouts = vec![]; - for job in jobs.iter() { - // Push layout for the vertex buffer, index buffer doesn't need one - buffer_layouts.push(Vertex::desc()); - - if let Some(layout) = job.mesh().texture_layout.as_ref() { - bind_group_layouts.push(layout); - } - } */ - - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Render Pipeline Layout"), - bind_group_layouts: &bind_group_layouts, - push_constant_ranges: &[], - }); - - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: shader, - entry_point: "vs_main", - buffers: &buffer_layouts, - }, - fragment: Some(wgpu::FragmentState { - module: shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format: config.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE - polygon_mode: wgpu::PolygonMode::Fill, - // Requires Features::DEPTH_CLIP_CONTROL - unclipped_depth: false, - // Requires Features::CONSERVATIVE_RASTERIZATION - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: RenderTexture::DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::Less, - stencil: wgpu::StencilState::default(), // TODO: stencil buffer - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - }); - - Self { - layout: render_pipeline_layout, - wgpu_pipeline: render_pipeline, - } - } - - #[allow(dead_code)] - pub fn get_layout(&self) -> &PipelineLayout { - &self.layout - } - - #[allow(dead_code)] - pub fn get_wgpu_pipeline(&self) -> &RenderPipeline { - &self.wgpu_pipeline - } -} \ No newline at end of file diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index cd7f127..b73a0b9 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -1,51 +1,41 @@ -use std::collections::{VecDeque, HashSet}; +use std::collections::VecDeque; +use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::sync::Arc; -use std::borrow::Cow; -use glam::Vec3; -use itertools::izip; -use lyra_ecs::query::filter::{Has, Not, Or}; -use lyra_ecs::relation::{ChildOf, RelationOriginComponent}; -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}; -use uuid::Uuid; -use wgpu::{BindGroupLayout, Limits}; -use wgpu::util::DeviceExt; use winit::window::Window; -use crate::math::Transform; -use crate::render::material::MaterialUniform; -use crate::render::render_buffer::BufferWrapperBuilder; -use crate::scene::CameraComponent; -use crate::DeltaTime; +use crate::render::graph::{BasePass, BasePassLabel, BasePassSlots, LightBasePass, LightBasePassLabel, LightCullComputePass, LightCullComputePassLabel, MeshPass, MeshesPassLabel, PresentPass, PresentPassLabel}; -use super::camera::{RenderCamera, CameraUniform}; -use super::desc_buf_lay::DescVertexBufferLayout; -use super::light::LightUniformBuffers; -use super::light_cull_compute::LightCullCompute; -use super::material::Material; -use super::render_buffer::BufferWrapper; -use super::texture::RenderTexture; -use super::transform_buffer_storage::{TransformBuffers, TransformGroup}; -use super::vertex::Vertex; -use super::{render_pipeline::FullRenderPipeline, render_buffer::BufferStorage, render_job::RenderJob}; +use super::graph::RenderGraph; +use super::{resource::RenderPipeline, render_job::RenderJob}; -use lyra_resource::{gltf::Mesh, ResHandle}; +#[derive(Clone, Copy, Debug)] +pub struct ScreenSize(glam::UVec2); -type MeshHandle = ResHandle; -type SceneHandle = ResHandle; +impl Deref for ScreenSize { + type Target = glam::UVec2; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ScreenSize { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} pub trait Renderer { fn prepare(&mut self, main_world: &mut World); fn render(&mut self) -> Result<(), wgpu::SurfaceError>; - fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize); + fn on_resize(&mut self, world: &mut World, new_size: winit::dpi::PhysicalSize); fn surface_size(&self) -> winit::dpi::PhysicalSize; - fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc); + fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc); } pub trait RenderPass { @@ -54,65 +44,25 @@ pub trait RenderPass { fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize); } -struct MeshBufferStorage { - buffer_vertex: BufferStorage, - buffer_indices: Option<(wgpu::IndexFormat, BufferStorage)>, - - //#[allow(dead_code)] - //render_texture: Option, - material: Option>, - - // The index of the transform for this entity. - // The tuple is structured like this: (transform index, index of transform inside the buffer) - //transform_index: TransformBufferIndices, -} - -#[derive(Clone, Debug, Component)] -pub struct InterpTransform { - last_transform: Transform, - alpha: f32, -} - pub struct BasicRenderer { - pub surface: wgpu::Surface, pub device: Rc, // device does not need to be mutable, no need for refcell pub queue: Rc, - pub config: wgpu::SurfaceConfiguration, pub size: winit::dpi::PhysicalSize, pub window: Arc, pub clear_color: wgpu::Color, - pub render_pipelines: rustc_hash::FxHashMap>, + pub render_pipelines: rustc_hash::FxHashMap>, pub render_jobs: VecDeque, - 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, - - render_limits: Limits, - - inuse_camera: RenderCamera, - camera_buffer: BufferWrapper, - //camera_bind_group: wgpu::BindGroup, - - bgl_texture: Rc, - default_texture: RenderTexture, - depth_buffer_texture: RenderTexture, - - material_buffer: BufferWrapper, - - light_buffers: LightUniformBuffers, - - light_cull_compute: LightCullCompute, + graph: RenderGraph, } impl BasicRenderer { - #[instrument(skip(window))] - pub async fn create_with_window(window: Arc) -> BasicRenderer { + #[instrument(skip(world, window))] + pub async fn create_with_window(world: &mut World, window: Arc) -> BasicRenderer { let size = window.inner_size(); + world.add_resource(ScreenSize(glam::UVec2::new(size.width, size.height))); // Get a GPU handle let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { @@ -150,11 +100,8 @@ impl BasicRenderer { None, ).await.unwrap(); - let render_limits = device.limits(); let surface_caps = surface.get_capabilities(&adapter); - let present_mode = surface_caps.present_modes[0]; - debug!("present mode: {:?}", present_mode); let surface_format = surface_caps.formats.iter() @@ -166,53 +113,48 @@ impl BasicRenderer { format: surface_format, width: size.width, height: size.height, - present_mode: wgpu::PresentMode::Immediate, + present_mode: wgpu::PresentMode::default(), //wgpu::PresentMode::Mailbox, // "Fast Vsync" alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], }; surface.configure(&device, &config); - let bgl_texture = Rc::new(RenderTexture::create_layout(&device)); - - let shader_src = include_str!("shaders/base.wgsl"); - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader"), - source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(shader_src)), - }); - - let transform_buffers = TransformBuffers::new(&device); - let camera_buffer = BufferWrapper::builder() - .buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST) - .contents(&[CameraUniform::default()]) - .label_prefix("Camera") - .visibility(wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE) - .buffer_dynamic_offset(false) - .finish(&device); - - let mut depth_texture = RenderTexture::create_depth_texture(&device, &config, "Tex_Depth"); - - // load the default texture - let bytes = include_bytes!("default_texture.png"); - let default_texture = RenderTexture::from_bytes(&device, &queue, bgl_texture.clone(), bytes, "default_texture").unwrap(); - - let light_uniform_buffers = LightUniformBuffers::new(&device); - - let mat_buffer = BufferWrapperBuilder::new() - .buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST) - .visibility(wgpu::ShaderStages::FRAGMENT) - .contents(&[MaterialUniform::default()]) - .finish(&device); - let device = Rc::new(device); let queue = Rc::new(queue); - let light_cull_compute = LightCullCompute::new(device.clone(), queue.clone(), size, &light_uniform_buffers, &camera_buffer, &mut depth_texture); - let mut s = Self { + let mut g = RenderGraph::new(device.clone(), queue.clone()); + + debug!("Adding base pass"); + g.add_pass(BasePassLabel, BasePass::new(surface, config)); + debug!("Adding light base pass"); + g.add_pass(LightBasePassLabel, LightBasePass::new()); + debug!("Adding light cull compute pass"); + g.add_pass(LightCullComputePassLabel, LightCullComputePass::new(size)); + //debug!("Adding triangle pass"); + //g.add_pass(TrianglePass::new()); + + debug!("Adding mesh pass"); + g.add_pass(MeshesPassLabel, MeshPass::new()); + + debug!("Adding present pass"); + let p = PresentPass::new(BasePassSlots::MainRenderTarget); + g.add_pass(p.label.clone(), p); + + g.add_edge(BasePassLabel, LightBasePassLabel); + g.add_edge(LightBasePassLabel, LightCullComputePassLabel); + g.add_edge(BasePassLabel, MeshesPassLabel); + + // make sure that present runs last + g.add_edge(BasePassLabel, PresentPassLabel::new(BasePassSlots::MainRenderTarget)); + g.add_edge(LightCullComputePassLabel, PresentPassLabel::new(BasePassSlots::MainRenderTarget)); + g.add_edge(MeshesPassLabel, PresentPassLabel::new(BasePassSlots::MainRenderTarget)); + + g.setup(&device); + + Self { window, - surface, device, queue, - config, size, clear_color: wgpu::Color { r: 0.1, @@ -222,446 +164,40 @@ impl BasicRenderer { }, render_pipelines: Default::default(), render_jobs: Default::default(), - mesh_buffers: Default::default(), - material_buffers: Default::default(), - entity_meshes: Default::default(), - render_limits, - transform_buffers, - - inuse_camera: RenderCamera::new(size), - camera_buffer, - - bgl_texture, - default_texture, - depth_buffer_texture: depth_texture, - - light_buffers: light_uniform_buffers, - material_buffer: mat_buffer, - light_cull_compute, - }; - - // create the default pipelines - 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, - s.camera_buffer.bindgroup_layout().unwrap(), - &s.light_buffers.bind_group_pair.layout, &s.material_buffer.bindgroup_pair.as_ref().unwrap().layout, - &s.bgl_texture, - &s.light_cull_compute.light_indices_grid.bg_pair.layout, - ]))); - s.render_pipelines = pipelines; - - s - } - - /// 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(); - - if let (Some(mesh), Some(buffers)) = (meshh.data_ref(), self.mesh_buffers.get_mut(&mesh_uuid)) { - // check if the buffer sizes dont match. If they dont, completely remake the buffers - let vertices = mesh.position().unwrap(); - if buffers.buffer_vertex.count() != vertices.len() { - debug!("Recreating buffers for mesh {}", mesh_uuid.to_string()); - let (vert, idx) = self.create_vertex_index_buffers(&mesh); - - // have to re-get buffers because of borrow checker - let buffers = self.mesh_buffers.get_mut(&mesh_uuid).unwrap(); - buffers.buffer_indices = idx; - buffers.buffer_vertex = vert; - - return; - } - - // update vertices - let vertex_buffer = buffers.buffer_vertex.buffer(); - let vertices = vertices.as_slice(); - // align the vertices to 4 bytes (u32 is 4 bytes, which is wgpu::COPY_BUFFER_ALIGNMENT) - let (_, vertices, _) = bytemuck::pod_align_to::(vertices); - self.queue.write_buffer(vertex_buffer, 0, bytemuck::cast_slice(vertices)); - - // update the indices if they're given - if let Some(index_buffer) = buffers.buffer_indices.as_ref() { - let aligned_indices = match mesh.indices.as_ref().unwrap() { - // U16 indices need to be aligned to u32, for wpgu, which are 4-bytes in size. - lyra_resource::gltf::MeshIndices::U16(v) => bytemuck::pod_align_to::(v).1, - lyra_resource::gltf::MeshIndices::U32(v) => bytemuck::pod_align_to::(v).1, - }; - - let index_buffer = index_buffer.1.buffer(); - self.queue.write_buffer(index_buffer, 0, bytemuck::cast_slice(aligned_indices)); - } + graph: g, } } - - #[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() - .unwrap_or_else(|| vec![glam::Vec2::new(0.0, 0.0); positions.len()]); - let normals = mesh.normals().unwrap(); - - assert!(positions.len() == tex_coords.len() && positions.len() == normals.len()); - - let mut vertex_inputs = vec![]; - for (v, t, n) in izip!(positions.iter(), tex_coords.iter(), normals.iter()) { - vertex_inputs.push(Vertex::new(*v, *t, *n)); - } - - let vertex_buffer = self.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Vertex Buffer"), - contents: bytemuck::cast_slice(vertex_inputs.as_slice()),//vertex_combined.as_slice(), - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages:: COPY_DST, - } - ); - let vertex_buffer = BufferStorage::new(vertex_buffer, 0, vertex_inputs.len()); - - let indices = match mesh.indices.as_ref() { - Some(indices) => { - let (idx_type, len, contents) = match indices { - lyra_resource::gltf::MeshIndices::U16(v) => (wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v)), - lyra_resource::gltf::MeshIndices::U32(v) => (wgpu::IndexFormat::Uint32, v.len(), bytemuck::cast_slice(v)), - }; - - let index_buffer = self.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Index Buffer"), - contents, - usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages:: COPY_DST, - } - ); - - let buffer_indices = BufferStorage::new(index_buffer, 0, len); - - Some((idx_type, buffer_indices)) - }, - None => { - None - } - }; - - ( 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); - - let material = mesh.material.as_ref() - .expect("Material resource not loaded yet"); - let material_ref = material.data_ref() - .unwrap(); - - let material = self.material_buffers.entry(material.uuid()) - .or_insert_with(|| { - debug!(uuid=material.uuid().to_string(), "Sending material to gpu"); - Rc::new(Material::from_resource(&self.device, &self.queue, self.bgl_texture.clone(), &material_ref)) - }); - - // TODO: support material uniforms from multiple uniforms - let uni = MaterialUniform::from(&**material); - self.queue.write_buffer(&self.material_buffer.inner_buf, 0, bytemuck::bytes_of(&uni)); - - MeshBufferStorage { - buffer_vertex: vertex_buffer, - buffer_indices, - material: Some(material.clone()), - } - } - - /// 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() { - self.transform_buffers.expand_buffers(&self.device); - } - - self.transform_buffers.update_or_insert(&self.queue, &self.render_limits, - entity, || ( transform.calculate_mat4(), glam::Mat3::from_quat(transform.rotation) )); */ - - #[allow(clippy::map_entry)] - if !self.mesh_buffers.contains_key(&mesh_uuid) { - // create the mesh's buffers - let buffers = self.create_mesh_buffers(mesh); - self.mesh_buffers.insert(mesh_uuid, buffers); - self.entity_meshes.insert(entity, mesh_uuid); - - true - } else { false } - } } 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 mut alive_entities = HashSet::new(); - - let view = main_world.view_iter::<( - Entities, - &Transform, - TickOf, - Or< - (&MeshHandle, TickOf), - (&SceneHandle, TickOf) - >, - Option<&mut InterpTransform>, - Res, - )>(); - - // 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); - - 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, interp_transform, &*mesh, mesh_han.uuid()) - && 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.device, &self.queue, &self.render_limits, - group, interp_transform.calculate_mat4(), glam::Mat3::from_quat(interp_transform.rotation)); - - let material = mesh.material.as_ref().unwrap() - .data_ref().unwrap(); - let shader = material.shader_uuid.unwrap_or(0); - let job = RenderJob::new(entity, shader, mesh_han.uuid(), transform_id); - self.render_jobs.push_back(job); - } - } - - if let Some((scene_han, scene_epoch)) = scene_pair { - if let Some(scene) = scene_han.data_ref() { - 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(); - } - - for (mesh_han, pos) in scene.world().view_iter::<(&MeshHandle, &WorldTransform)>() { - if let Some(mesh) = mesh_han.data_ref() { - 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 - // dont need to be resent to the gpu. - if !self.process_mesh(entity, mesh_interpo, &*mesh, mesh_han.uuid()) - && scene_epoch == last_epoch { - 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.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(); - let shader = material.shader_uuid.unwrap_or(0); - let job = RenderJob::new(entity, shader, mesh_han.uuid(), transform_id); - self.render_jobs.push_back(job); - } - } - } - } - } - - for (en, interp) in component_queue { - main_world.insert(en, interp); - } - - // collect dead entities - 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 - if self.mesh_buffers.len() != alive_entities.len() { - let removed_entities: Vec = self.entity_meshes - .extract_if(|e, _| !alive_entities.contains(e)) - .map(|(_, v)| v) - .collect(); - 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); - self.camera_buffer.write_buffer(&self.queue, 0, &[uniform]); - } else { - warn!("Missing camera!"); - } - - self.light_buffers.update_lights(&self.queue, last_epoch, main_world); + self.graph.prepare(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()); - - self.light_cull_compute.compute(&self.camera_buffer, &self.light_buffers, &self.depth_buffer_texture); - - let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Basic Renderer's Encoder") - }); - - // Create a new variable scope for the render pass - { - // There's only one render pass currently - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(self.clear_color), - store: true, - }, - })], - // enable depth buffer - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_buffer_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: true, - }), - stencil_ops: None, - }), - }); - - // Pop off jobs from the queue as they're being processed - while let Some(job) = self.render_jobs.pop_front() { - if let Some(pipeline) = self.render_pipelines.get(&job.shader_id) { - // specify to use this pipeline - render_pass.set_pipeline(pipeline.get_wgpu_pipeline()); - - // get the mesh (containing vertices) and the buffers from storage - let buffers = self.mesh_buffers.get(&job.mesh_uuid); - if buffers.is_none() { - warn!("Skipping job since its mesh is missing {:?}", job.mesh_uuid); - continue; - } - let buffers = buffers.unwrap(); - /* let buffers = self.mesh_buffers.get(&job.mesh_uuid) - .expect("missing render job mesh"); */ - - // Bind the optional texture - if let Some(tex) = buffers.material.as_ref() - .and_then(|m| m.diffuse_texture.as_ref()) { - render_pass.set_bind_group(0, tex.bind_group(), &[]); - } else { - render_pass.set_bind_group(0, self.default_texture.bind_group(), &[]); - } - - if let Some(tex) = buffers.material.as_ref() - .and_then(|m| m.specular.as_ref()) - .and_then(|s| s.texture.as_ref().or(s.color_texture.as_ref())) { - render_pass.set_bind_group(5, tex.bind_group(), &[]); - } else { - render_pass.set_bind_group(5, self.default_texture.bind_group(), &[]); - } - - // 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, ]); - - render_pass.set_bind_group(2, &self.camera_buffer.bindgroup(), &[]); - render_pass.set_bind_group(3, &self.light_buffers.bind_group_pair.bindgroup, &[]); - render_pass.set_bind_group(4, &self.material_buffer.bindgroup_pair.as_ref().unwrap().bindgroup, &[]); - - render_pass.set_bind_group(6, &self.light_cull_compute.light_indices_grid.bg_pair.bindgroup, &[]); - - // if this mesh uses indices, use them to draw the mesh - if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() { - let indices_len = indices.count() as u32; - - render_pass.set_vertex_buffer(buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..)); - render_pass.set_index_buffer(indices.buffer().slice(..), *idx_type); - render_pass.draw_indexed(0..indices_len, 0, 0..1); - } else { - let vertex_count = buffers.buffer_vertex.count(); - - render_pass.set_vertex_buffer(buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..)); - render_pass.draw(0..vertex_count as u32, 0..1); - } - } - } - } - - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); + self.graph.render(); Ok(()) } - #[instrument(skip(self))] - fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize) { + #[instrument(skip(world, self))] + fn on_resize(&mut self, world: &mut World, new_size: winit::dpi::PhysicalSize) { if new_size.width > 0 && new_size.height > 0 { self.size = new_size; - self.config.width = new_size.width; - self.config.height = new_size.height; + + // update surface config and the surface + let mut rt = self.graph.slot_value_mut(BasePassSlots::MainRenderTarget) + .unwrap().as_render_target_mut().unwrap(); + rt.surface_config.width = new_size.width; + rt.surface_config.height = new_size.height; + rt.surface.configure(&self.device, &rt.surface_config); - // tell other things of updated resize - self.surface.configure(&self.device, &self.config); - - let create_bindgroup = self.depth_buffer_texture.bindgroup_pair.is_some(); - self.depth_buffer_texture = RenderTexture::create_depth_texture(&self.device, &self.config, "Depth Buffer Texture"); - if create_bindgroup { - self.depth_buffer_texture.create_bind_group(&self.device); - } - - self.inuse_camera.update_aspect_ratio(self.size); - self.light_cull_compute.update_screen_size(new_size); + // update screen size resource in ecs + let mut world_ss = world.get_resource_mut::(); + world_ss.0 = glam::UVec2::new(new_size.width, new_size.height); } } @@ -669,7 +205,7 @@ impl Renderer for BasicRenderer { self.size } - fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc) { + fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc) { self.render_pipelines.insert(shader_id, pipeline); } } \ No newline at end of file diff --git a/lyra-game/src/render/resource/compute_pipeline.rs b/lyra-game/src/render/resource/compute_pipeline.rs new file mode 100644 index 0000000..71b48a5 --- /dev/null +++ b/lyra-game/src/render/resource/compute_pipeline.rs @@ -0,0 +1,107 @@ +use std::{ops::Deref, rc::Rc}; + +use wgpu::PipelineLayout; + +use super::Shader; + +//#[derive(Debug, Clone)] +pub struct ComputePipelineDescriptor { + pub label: Option, + pub layouts: Vec>, + pub push_constant_ranges: Vec, + // TODO: make this a ResHandle + /// The compiled shader module for the stage. + pub shader: Rc, + /// The entry point in the compiled shader. + /// There must be a function in the shader with the same name. + pub shader_entry_point: String, +} + +impl ComputePipelineDescriptor { + /// Create the [`wgpu::PipelineLayout`] for this pipeline + pub(crate) fn create_layout(&self, device: &wgpu::Device) -> wgpu::PipelineLayout { + let bgs = self + .layouts + .iter() + .map(|bg| bg.as_ref()) + .collect::>(); + + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, //self.label.as_ref().map(|s| format!("{}Layout", s)), + bind_group_layouts: &bgs, + push_constant_ranges: &self.push_constant_ranges, + }) + } +} + +pub struct ComputePipeline { + layout: Option, + wgpu_pipeline: wgpu::ComputePipeline, +} + +impl Deref for ComputePipeline { + type Target = wgpu::ComputePipeline; + + fn deref(&self) -> &Self::Target { + &self.wgpu_pipeline + } +} + +impl From for ComputePipeline { + fn from(value: wgpu::ComputePipeline) -> Self { + Self { + layout: None, + wgpu_pipeline: value, + } + } +} + +impl ComputePipeline { + /// Creates a new compute pipeline on the `device`. + /// + /// Parameters: + /// * `device` - The device to create the pipeline on. + /// * `desc` - The discriptor of the compute pipeline + pub fn create(device: &wgpu::Device, desc: &ComputePipelineDescriptor) -> ComputePipeline { + // create the layout only if bind groups layouts were specified + let layout = if !desc.layouts.is_empty() { + Some(desc.create_layout(device)) + } else { + None + }; + + // an Rc was used here so that this shader could be reused by the fragment stage if + // they share the same shader. I tried to do it without an Rc but couldn't get past + // the borrow checker + let compiled_shader = Rc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: desc.shader.label.as_ref().map(|s| s.as_str()), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + &desc.shader.source, + )), + })); + + let desc = wgpu::ComputePipelineDescriptor { + label: desc.label.as_deref(), + layout: layout.as_ref(), + module: &compiled_shader, + entry_point: &desc.shader_entry_point, + }; + + let pipeline = device.create_compute_pipeline(&desc); + + Self { + layout, + wgpu_pipeline: pipeline, + } + } + + #[inline(always)] + pub fn layout(&self) -> Option<&PipelineLayout> { + self.layout.as_ref() + } + + #[inline(always)] + pub fn wgpu_pipeline(&self) -> &wgpu::ComputePipeline { + &self.wgpu_pipeline + } +} diff --git a/lyra-game/src/render/resource/mod.rs b/lyra-game/src/render/resource/mod.rs new file mode 100644 index 0000000..34ccb1d --- /dev/null +++ b/lyra-game/src/render/resource/mod.rs @@ -0,0 +1,11 @@ +mod shader; +pub use shader::*; + +mod pipeline; +pub use pipeline::*; + +mod compute_pipeline; +pub use compute_pipeline::*; + +mod render_pipeline; +pub use render_pipeline::*; \ No newline at end of file diff --git a/lyra-game/src/render/resource/pipeline.rs b/lyra-game/src/render/resource/pipeline.rs new file mode 100644 index 0000000..d66eb45 --- /dev/null +++ b/lyra-game/src/render/resource/pipeline.rs @@ -0,0 +1,86 @@ +use super::{compute_pipeline::ComputePipeline, render_pipeline::RenderPipeline, ComputePipelineDescriptor, RenderPipelineDescriptor}; + +pub enum PipelineDescriptor { + Render(RenderPipelineDescriptor), + Compute(ComputePipelineDescriptor), +} + +impl PipelineDescriptor { + pub fn as_render_pipeline_descriptor(&self) -> Option<&RenderPipelineDescriptor> { + match self { + Self::Render(r) => Some(r), + _ => None, + } + } + + pub fn as_compute_pipeline_descriptor(&self) -> Option<&ComputePipelineDescriptor> { + match self { + Self::Compute(c) => Some(c), + _ => None, + } + } +} + +pub enum Pipeline { + Render(RenderPipeline), + Compute(ComputePipeline), +} + +impl Into for Pipeline { + fn into(self) -> RenderPipeline { + match self { + Self::Render(r) => r, + _ => panic!("Pipeline is not a RenderPipeline"), + } + } +} + +impl Into for Pipeline { + fn into(self) -> ComputePipeline { + match self { + Self::Compute(c) => c, + _ => panic!("Pipeline is not a RenderPipeline"), + } + } +} + +impl Pipeline { + pub fn bind_group_layout(&self, index: u32) -> wgpu::BindGroupLayout { + match self { + Pipeline::Render(r) => r.get_bind_group_layout(index), + Pipeline::Compute(c) => c.get_bind_group_layout(index), + } + } + + /// Gets self as a render pipeline, panics if self is not a render pipeline. + pub fn as_render(&self) -> &RenderPipeline { + match self { + Self::Render(r) => r, + _ => panic!("Pipeline is not a RenderPipeline") + } + } + + /// Gets self as a render pipeline, returns `None` if self is not a render pipeline. + pub fn try_as_render(&self) -> Option<&RenderPipeline> { + match self { + Self::Render(r) => Some(r), + _ => None, + } + } + + /// Gets self as a compute pipeline, panics if self is not a compute pipeline. + pub fn as_compute(&self) -> &ComputePipeline { + match self { + Self::Compute(r) => r, + _ => panic!("Pipeline is not a ComputePipeline") + } + } + + /// Gets self as a compute pipeline, returns `None` if self is not a compute pipeline. + pub fn try_as_compute(&self) -> Option<&ComputePipeline> { + match self { + Self::Compute(c) => Some(c), + _ => None, + } + } +} \ No newline at end of file diff --git a/lyra-game/src/render/resource/render_pipeline.rs b/lyra-game/src/render/resource/render_pipeline.rs new file mode 100755 index 0000000..a93ce36 --- /dev/null +++ b/lyra-game/src/render/resource/render_pipeline.rs @@ -0,0 +1,148 @@ +use std::{num::NonZeroU32, ops::Deref, rc::Rc}; + +use wgpu::PipelineLayout; + +use super::{FragmentState, VertexState}; + +//#[derive(Debug, Clone)] +pub struct RenderPipelineDescriptor { + pub label: Option, + pub layouts: Vec>, + pub push_constant_ranges: Vec, + pub vertex: VertexState, + pub fragment: Option, + pub primitive: wgpu::PrimitiveState, + pub depth_stencil: Option, + pub multisample: wgpu::MultisampleState, + pub multiview: Option, +} + +impl RenderPipelineDescriptor { + /// Create the [`wgpu::PipelineLayout`] for this pipeline + pub(crate) fn create_layout(&self, device: &wgpu::Device) -> wgpu::PipelineLayout { + let bgs = self + .layouts + .iter() + .map(|bg| bg.as_ref()) + .collect::>(); + + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, //self.label.as_ref().map(|s| format!("{}Layout", s)), + bind_group_layouts: &bgs, + push_constant_ranges: &self.push_constant_ranges, + }) + } +} + +pub struct RenderPipeline { + layout: Option, + wgpu_pipeline: wgpu::RenderPipeline, +} + +impl Deref for RenderPipeline { + type Target = wgpu::RenderPipeline; + + fn deref(&self) -> &Self::Target { + &self.wgpu_pipeline + } +} + +impl From for RenderPipeline { + fn from(value: wgpu::RenderPipeline) -> Self { + Self { + layout: None, + wgpu_pipeline: value, + } + } +} + +impl RenderPipeline { + /// Creates the default render pipeline + /// + /// Parameters: + /// * `device` - The device to create the pipeline on. + /// * `config` - The surface config to use to create the pipeline. + /// * `label` - The label of the pipeline. + /// * `shader` - The compiled shader of the pipeline. + /// * `vertex_entry_point` - The entry point name of the vertex shader + pub fn create(device: &wgpu::Device, desc: &RenderPipelineDescriptor) -> RenderPipeline { + // create the layout only if bind groups layouts were specified + let layout = if !desc.layouts.is_empty() { + Some(desc.create_layout(device)) + } else { + None + }; + + let vrtx_buffs = desc + .vertex + .buffers + .iter() + .map(|vbl| wgpu::VertexBufferLayout { + array_stride: vbl.array_stride, + step_mode: vbl.step_mode, + attributes: &vbl.attributes, + }) + .collect::>(); + + // an Rc was used here so that this shader could be reused by the fragment stage if + // they share the same shader. I tried to do it without an Rc but couldn't get past + // the borrow checker + let vrtx_shad = Rc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: desc.vertex.module.label.as_ref().map(|s| s.as_str()), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + &desc.vertex.module.source, + )), + })); + let vrtx_state = wgpu::VertexState { + module: &*vrtx_shad, + entry_point: &desc.vertex.entry_point, + buffers: &vrtx_buffs, + }; + + let frag_module = desc.fragment.as_ref().map(|f| { + if f.module == desc.vertex.module { + vrtx_shad.clone() + } else { + Rc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: f.module.label.as_ref().map(|s| s.as_str()), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(&f.module.source)), + })) + } + }); + + let fm = frag_module.as_ref(); + let fstate = desc.fragment.as_ref().map(move |f| wgpu::FragmentState { + module: fm.unwrap(), + entry_point: &f.entry_point, + targets: &f.targets, + }); + + let render_desc = wgpu::RenderPipelineDescriptor { + label: desc.label.as_deref(), + layout: layout.as_ref(), + vertex: vrtx_state, + primitive: desc.primitive, + depth_stencil: desc.depth_stencil.clone(), + multisample: desc.multisample, + fragment: fstate, + multiview: desc.multiview, + }; + + let render_pipeline = device.create_render_pipeline(&render_desc); + + Self { + layout, + wgpu_pipeline: render_pipeline, + } + } + + #[inline(always)] + pub fn layout(&self) -> Option<&PipelineLayout> { + self.layout.as_ref() + } + + #[inline(always)] + pub fn wgpu_pipeline(&self) -> &wgpu::RenderPipeline { + &self.wgpu_pipeline + } +} diff --git a/lyra-game/src/render/resource/shader.rs b/lyra-game/src/render/resource/shader.rs new file mode 100644 index 0000000..71c9669 --- /dev/null +++ b/lyra-game/src/render/resource/shader.rs @@ -0,0 +1,50 @@ +use std::rc::Rc; + +#[derive(Debug, Default, Clone)] +pub struct VertexBufferLayout { + pub array_stride: wgpu::BufferAddress, + pub step_mode: wgpu::VertexStepMode, + pub attributes: Vec, +} + +impl<'a> From> for VertexBufferLayout { + fn from(value: wgpu::VertexBufferLayout) -> Self { + Self { + array_stride: value.array_stride, + step_mode: value.step_mode, + attributes: value.attributes.to_vec(), + } + } +} + +/// Describes the vertex stage in a render pipeline. +#[derive(Debug, Clone)] +pub struct VertexState { + // TODO: make this a ResHandle + /// The compiled shader module for the stage. + pub module: Rc, + /// The entry point in the compiled shader. + /// There must be a function in the shader with the same name. + pub entry_point: String, + /// The format of the vertex buffers used with this pipeline. + pub buffers: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Shader { + pub label: Option, + pub source: String, +} + +/// Describes the fragment stage in the render pipeline. +#[derive(Debug, Clone)] +pub struct FragmentState { + // TODO: make this a ResHandle + /// The compiled shader module for the stage. + pub module: Rc, + /// The entry point in the compiled shader. + /// There must be a function in the shader with the same name. + pub entry_point: String, + /// The color state of the render targets. + pub targets: Vec>, +} \ No newline at end of file diff --git a/lyra-game/src/render/shaders/triangle.wgsl b/lyra-game/src/render/shaders/triangle.wgsl new file mode 100644 index 0000000..171ddc0 --- /dev/null +++ b/lyra-game/src/render/shaders/triangle.wgsl @@ -0,0 +1,22 @@ +struct VertexOutput { + @builtin(position) clip_position: vec4, +}; + +@group(0) @binding(0) +var u_triangle_color: vec4; + +@vertex +fn vs_main( + @builtin(vertex_index) in_vertex_index: u32, +) -> VertexOutput { + var out: VertexOutput; + let x = f32(1 - i32(in_vertex_index)) * 0.5; + let y = f32(i32(in_vertex_index & 1u) * 2 - 1) * 0.5; + out.clip_position = vec4(x, y, 0.0, 1.0); + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return vec4(u_triangle_color.xyz, 1.0); +} \ No newline at end of file diff --git a/lyra-game/src/render/texture.rs b/lyra-game/src/render/texture.rs index 64e2b59..c3927dd 100755 --- a/lyra-game/src/render/texture.rs +++ b/lyra-game/src/render/texture.rs @@ -273,7 +273,10 @@ impl RenderTexture { }; let texture = device.create_texture(&desc); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let view = texture.create_view(&wgpu::TextureViewDescriptor { + format: Some(wgpu::TextureFormat::Depth32Float), + ..Default::default() + }); let sampler = device.create_sampler( &wgpu::SamplerDescriptor { // 4. address_mode_u: wgpu::AddressMode::ClampToEdge, diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index 9fae477..34796c4 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -1,4 +1,4 @@ -use std::{collections::{HashMap, VecDeque}, hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}, num::NonZeroU64}; +use std::{collections::{HashMap, VecDeque}, hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}, num::NonZeroU64, rc::Rc}; use lyra_ecs::Entity; use tracing::instrument; @@ -162,7 +162,7 @@ impl CachedValMap, //groups: CachedValMap, //groups: SlotMap, entries: Vec, @@ -192,7 +192,7 @@ impl TransformBuffers { }); let mut s = Self { - bindgroup_layout, + bindgroup_layout: Rc::new(bindgroup_layout), entries: Default::default(), max_transform_count: (limits.max_uniform_buffer_binding_size) as usize / (limits.min_uniform_buffer_offset_alignment as usize), //(mem::size_of::()), limits, @@ -209,6 +209,7 @@ impl TransformBuffers { /// /// 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. + #[instrument(skip(self, queue))] pub fn send_to_gpu(&mut self, queue: &wgpu::Queue) { self.next_index = 0; diff --git a/lyra-resource/Cargo.toml b/lyra-resource/Cargo.toml index 9e5e149..51b160d 100644 --- a/lyra-resource/Cargo.toml +++ b/lyra-resource/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] } -lyra-reflect = { path = "../lyra-reflect" } +lyra-reflect = { path = "../lyra-reflect", features = [ "math" ] } lyra-math = { path = "../lyra-math" } lyra-scene = { path = "../lyra-scene" } anyhow = "1.0.75" diff --git a/lyra-resource/src/gltf/material.rs b/lyra-resource/src/gltf/material.rs index 3a24d9d..372d7ee 100644 --- a/lyra-resource/src/gltf/material.rs +++ b/lyra-resource/src/gltf/material.rs @@ -34,10 +34,10 @@ impl From> for PbrRoughness { #[derive(Clone, Debug, Default, Reflect)] pub struct PbrGlossiness { /// The rgba diffuse color of the material - pub diffuse_color: glam::Vec4, + pub diffuse_color: lyra_math::Vec4, // The base color texture // pub diffuse_texture // TODO - pub specular: glam::Vec3, + pub specular: lyra_math::Vec3, /// The glossiness factor of the material. /// From 0.0 (no glossiness) to 1.0 (full glossiness) pub glossiness: f32, @@ -101,7 +101,7 @@ pub struct Specular { pub factor: f32, /// The color of the specular reflection - pub color_factor: glam::Vec3, + pub color_factor: lyra_math::Vec3, /// A texture that defines the strength of the specular reflection, /// stored in the alpha (`A`) channel. This will be multiplied by @@ -140,7 +140,7 @@ pub struct Material { //pub pbr_roughness: PbrRoughness, /// The RGBA base color of the model. If a texture is supplied with `base_color_texture`, this value /// will tint the texture. If a texture is not provided, this value would be the color of the Material. - pub base_color: glam::Vec4, + pub base_color: lyra_math::Vec4, /// The metalness of the material /// From 0.0 (non-metal) to 1.0 (metal) pub metallic: f32, diff --git a/lyra-resource/src/gltf/mesh.rs b/lyra-resource/src/gltf/mesh.rs index a23affe..3cddd3c 100644 --- a/lyra-resource/src/gltf/mesh.rs +++ b/lyra-resource/src/gltf/mesh.rs @@ -50,9 +50,9 @@ impl From> for MeshIndices { #[repr(C)] #[derive(Clone, Debug, PartialEq, Reflect)] pub enum VertexAttributeData { - Vec2(Vec), - Vec3(Vec), - Vec4(Vec), + Vec2(Vec), + Vec3(Vec), + Vec4(Vec), } impl VertexAttributeData { diff --git a/lyra-resource/src/resource_manager.rs b/lyra-resource/src/resource_manager.rs index 4d2f0ba..99e01b0 100644 --- a/lyra-resource/src/resource_manager.rs +++ b/lyra-resource/src/resource_manager.rs @@ -374,7 +374,7 @@ pub(crate) mod tests { use instant::Instant; - use crate::{Image, ResourceData, Texture}; + use crate::{Image, ResourceData}; use super::*; @@ -431,10 +431,10 @@ pub(crate) mod tests { #[test] fn ensure_single() { let man = ResourceManager::new(); - let res = man.request::(&get_image("squiggles.png")).unwrap(); + let res = man.request::(&get_image("squiggles.png")).unwrap(); assert_eq!(Arc::strong_count(&res.handle.res), 3); - let resagain = man.request::(&get_image("squiggles.png")).unwrap(); + let resagain = man.request::(&get_image("squiggles.png")).unwrap(); assert_eq!(Arc::strong_count(&resagain.handle.res), 4); } @@ -442,7 +442,7 @@ pub(crate) mod tests { #[test] fn ensure_none() { let man = ResourceManager::new(); - let res = man.request::(&get_image("squigglesfff.png")).unwrap(); + let res = man.request::(&get_image("squigglesfff.png")).unwrap(); //let err = res.err().unwrap(); // 1 second should be enough to run into an error @@ -468,7 +468,7 @@ pub(crate) mod tests { #[test] fn reload_image() { let man = ResourceManager::new(); - let res = man.request::(&get_image("squiggles.png")).unwrap(); + let res = man.request::(&get_image("squiggles.png")).unwrap(); busy_wait_resource(&res, 10.0); let img = res.data_ref(); img.unwrap(); @@ -489,7 +489,7 @@ pub(crate) mod tests { std::fs::copy(orig_path, &image_path).unwrap(); let man = ResourceManager::new(); - let res = man.request::(&image_path).unwrap(); + let res = man.request::(&image_path).unwrap(); busy_wait_resource(&res, 10.0); let img = res.data_ref(); img.unwrap();