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/lyra-game/src/render/graph/execution_path.rs b/lyra-game/src/render/graph/execution_path.rs index ab080ad..bb16ed8 100644 --- a/lyra-game/src/render/graph/execution_path.rs +++ b/lyra-game/src/render/graph/execution_path.rs @@ -11,9 +11,11 @@ pub struct GraphExecutionPath { } impl GraphExecutionPath { - pub fn new(built_in_slots: FxHashSet, pass_descriptions: Vec<&RenderGraphPassDesc>) -> Self { + pub fn new(pass_descriptions: Vec<&RenderGraphPassDesc>) -> Self { // collect all the output slots let mut total_outputs = HashMap::new(); + total_outputs.reserve(pass_descriptions.len()); + for desc in pass_descriptions.iter() { for slot in desc.output_slots() { total_outputs.insert(slot.name.clone(), SlotOwnerPair { @@ -28,9 +30,6 @@ impl GraphExecutionPath { // find the node inputs let mut inputs = vec![]; for slot in desc.input_slots() { - // If the slot is built in to the graph, no need to care about the sorting. - if built_in_slots.contains(&slot.id) { continue; } - let inp = total_outputs.get(&slot.name) .expect(&format!("failed to find slot: '{}', ensure that there is a pass outputting it", slot.name)); inputs.push(*inp); diff --git a/lyra-game/src/render/graph/mod.rs b/lyra-game/src/render/graph/mod.rs index 043b742..e485576 100644 --- a/lyra-game/src/render/graph/mod.rs +++ b/lyra-game/src/render/graph/mod.rs @@ -17,8 +17,8 @@ pub use slot_desc::*; mod execution_path; -use rustc_hash::{FxHashMap, FxHashSet}; -use tracing::{debug_span, instrument, trace}; +use rustc_hash::FxHashMap; +use tracing::{debug_span, instrument, trace, warn}; use self::execution_path::GraphExecutionPath; @@ -38,6 +38,7 @@ pub struct BindGroupEntry { pub layout: Option>, } +#[allow(dead_code)] struct ResourcedSlot { name: String, //slot: RenderPassSlot, @@ -54,6 +55,13 @@ pub struct PipelineResource { 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, @@ -69,42 +77,21 @@ pub struct RenderGraph { pipelines: FxHashMap, current_id: u64, exec_path: Option, - - pub(crate) surface_config: wgpu::SurfaceConfiguration, } impl RenderGraph { - pub fn new( - device: Rc, - queue: Rc, - surface_config: wgpu::SurfaceConfiguration, - ) -> Self { - let mut slots = FxHashMap::default(); - let mut slot_names = HashMap::default(); - - slots.insert( - 0, - ResourcedSlot { - name: "window_texture_view".to_string(), - ty: SlotType::TextureView, - // this will get set in prepare stage. - value: SlotValue::None, - }, - ); - slot_names.insert("window_texture_view".to_string(), 0u64); - + pub fn new(device: Rc, queue: Rc) -> Self { Self { device, queue, - slots, - slot_names, + slots: Default::default(), + slot_names: Default::default(), passes: Default::default(), bind_groups: Default::default(), bind_group_names: Default::default(), pipelines: Default::default(), current_id: 1, exec_path: None, - surface_config, } } @@ -138,6 +125,11 @@ impl RenderGraph { slot.name, desc.name ); + trace!( + "Found existing slot for {}, changing id to {}", + slot.name, id + ); + // if there is a slot of the same name slot.id = *id; } else { @@ -229,61 +221,61 @@ impl RenderGraph { } // create the execution path for the graph. This will be executed in `RenderGraph::render` - let builtin = { - let mut h = FxHashSet::default(); - h.insert(0u64); // include the base pass - h - }; let descs = self.passes.values().map(|p| &*p.desc).collect(); - let path = GraphExecutionPath::new(builtin, descs); + let path = GraphExecutionPath::new(descs); trace!( "Found {} steps in the rendergraph to execute", path.queue.len() ); + self.exec_path = Some(path); } - #[instrument(skip(self, surface))] - pub fn render(&mut self, surface: &wgpu::Surface) { + #[instrument(skip(self))] + pub fn render(&mut self) { let mut path = self.exec_path.take().unwrap(); - let output = surface.get_current_texture().unwrap(); - let view = output - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - // update the window texture view slot. - let window_tv_slot = self.slots.get_mut(&0).unwrap(); // 0 is window_texture_view - debug_assert_eq!( - window_tv_slot.name.as_str(), - "window_texture_view", - "unexpected slot where 'window_texture_view' should be" - ); - window_tv_slot.value = SlotValue::TextureView(Rc::new(view)); - - let mut encoders = vec![]; + let mut encoders = Vec::with_capacity(self.passes.len() / 2); while let Some(pass_id) = path.queue.pop_front() { let pass = self.passes.get(&pass_id).unwrap(); let pass_inn = pass.inner.clone(); let pass_desc = pass.desc.clone(); let label = format!("{} Encoder", pass_desc.name); - let encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some(&label), - }); + // encoders are not needed for presenter nodes. + let encoder = if pass_desc.pass_type == RenderPassType::Presenter { + None + } else { + Some( + self.device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some(&label), + }), + ) + }; + let queue = self.queue.clone(); // clone is required to appease the borrow checker - let mut context = RenderGraphContext::new(&queue, Some(encoder)); + let mut context = RenderGraphContext::new(&queue, encoder); + + // all encoders need to be submitted before a presenter node is executed. + if pass_desc.pass_type == RenderPassType::Presenter { + self.queue.submit(encoders.drain(..)); + } let mut inner = pass_inn.borrow_mut(); inner.execute(self, &*pass_desc, &mut context); - encoders.push(context.encoder.unwrap().finish()); + if let Some(encoder) = context.encoder { + encoders.push(encoder.finish()); + } } - self.queue.submit(encoders.into_iter()); - output.present(); + 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, id: u64) -> Option<&SlotValue> { diff --git a/lyra-game/src/render/graph/pass.rs b/lyra-game/src/render/graph/pass.rs index 6c134f3..8a92ce4 100644 --- a/lyra-game/src/render/graph/pass.rs +++ b/lyra-game/src/render/graph/pass.rs @@ -1,16 +1,17 @@ -use std::{collections::HashMap, num::NonZeroU32, rc::Rc}; +use std::{cell::{Ref, RefCell, RefMut}, collections::HashMap, num::NonZeroU32, rc::Rc}; use lyra_ecs::World; use crate::render::resource::RenderPipelineDescriptor; -use super::{RenderGraph, RenderGraphContext}; +use super::{RenderGraph, RenderGraphContext, RenderTarget}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum RenderPassType { Compute, #[default] Render, + Presenter, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -19,15 +20,19 @@ pub enum SlotType { Sampler, Texture, Buffer, + RenderTarget, } #[derive(Debug, Clone)] pub enum SlotValue { None, + /// The value will be set during a later phase of the render graph. + Lazy, TextureView(Rc), Sampler(Rc), Texture(Rc), Buffer(Rc), + RenderTarget(Rc>), } impl SlotValue { @@ -52,6 +57,20 @@ impl SlotValue { _ => None, } } + + pub fn as_render_target(&self) -> Option> { + match self { + Self::RenderTarget(v) => Some(v.borrow()), + _ => None, + } + } + + pub fn as_render_target_mut(&mut self) -> Option> { + match self { + Self::RenderTarget(v) => Some(v.borrow_mut()), + _ => None, + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum SlotAttribute { @@ -162,6 +181,11 @@ impl RenderGraphPassDesc { } pub fn add_slot(&mut self, slot: RenderPassSlot) { + debug_assert!( + !(slot.attribute == SlotAttribute::Input && slot.value.is_some()), + "input slots should not have values" + ); + self.slot_names.insert(slot.name.clone(), slot.id); self.slots.push(slot); } @@ -175,7 +199,7 @@ impl RenderGraphPassDesc { value: Option, ) { debug_assert!( - matches!(value, None | Some(SlotValue::Buffer(_))), + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Buffer(_))), "slot value is not a buffer" ); @@ -198,7 +222,7 @@ impl RenderGraphPassDesc { value: Option, ) { debug_assert!( - matches!(value, None | Some(SlotValue::Texture(_))), + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Texture(_))), "slot value is not a texture" ); @@ -221,7 +245,7 @@ impl RenderGraphPassDesc { value: Option, ) { debug_assert!( - matches!(value, None | Some(SlotValue::TextureView(_))), + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::TextureView(_))), "slot value is not a texture view" ); @@ -244,7 +268,7 @@ impl RenderGraphPassDesc { value: Option, ) { debug_assert!( - matches!(value, None | Some(SlotValue::Sampler(_))), + matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Sampler(_))), "slot value is not a sampler" ); diff --git a/lyra-game/src/render/graph/passes/base.rs b/lyra-game/src/render/graph/passes/base.rs index feaee62..ed23fc7 100644 --- a/lyra-game/src/render/graph/passes/base.rs +++ b/lyra-game/src/render/graph/passes/base.rs @@ -1,47 +1,100 @@ -use glam::UVec2; +use std::{cell::RefCell, rc::Rc}; -use crate::render::{camera::CameraUniform, graph::{BufferInitDescriptor, RenderGraphPass, RenderGraphPassDesc, RenderPassType, SlotAttribute, SlotDescriptor}}; +use crate::render::graph::{RenderGraphContext, RenderGraphPass, RenderGraphPassDesc, RenderPassSlot, RenderPassType, RenderTarget, SlotAttribute, SlotType, SlotValue}; /// Supplies some basic things other passes needs. /// /// screen size buffer, camera buffer, #[derive(Default)] -pub struct BasePass; +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, + main_rt_id: u64, + window_tv_id: u64, +} impl BasePass { - pub fn new() -> Self { - Self::default() + pub fn new(surface: wgpu::Surface, surface_config: wgpu::SurfaceConfiguration) -> Self { + Self { + temp_render_target: Some(RenderTarget { + surface, + surface_config, + current_texture: None, + }), + main_rt_id: 0, + window_tv_id: 0, + } } } impl RenderGraphPass for BasePass { - fn desc(&self, graph: &mut crate::render::graph::RenderGraph, id: &mut u64) -> crate::render::graph::RenderGraphPassDesc { - let mut desc = RenderGraphPassDesc::new(*id, "BasePass", RenderPassType::Compute); - *id += 1; + fn desc(&mut self, graph: &mut crate::render::graph::RenderGraph) -> crate::render::graph::RenderGraphPassDesc { + let mut desc = RenderGraphPassDesc::new( + graph.next_id(), + "base", + RenderPassType::Render, + None, + vec![], + ); - desc.add_buffer_slot(*id, "screen_size_buffer", SlotAttribute::Output, Some(SlotDescriptor::BufferInit(BufferInitDescriptor { + /* desc.add_buffer_slot(*id, "screen_size_buffer", SlotAttribute::Output, Some(SlotDescriptor::BufferInit(BufferInitDescriptor { label: Some("B_ScreenSize".to_string()), contents: bytemuck::bytes_of(&UVec2::new(800, 600)).to_vec(), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }))); - *id += 1; desc.add_buffer_slot(*id, "camera_buffer", SlotAttribute::Output, Some(SlotDescriptor::BufferInit(BufferInitDescriptor { label: Some("B_Camera".to_string()), contents: bytemuck::bytes_of(&CameraUniform::default()).to_vec(), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }))); - *id += 1; + *id += 1; */ + + self.main_rt_id = graph.next_id(); + let render_target = self.temp_render_target.take().unwrap(); + desc.add_slot( + RenderPassSlot { + ty: SlotType::RenderTarget, + attribute: SlotAttribute::Output, + id: self.main_rt_id, + name: "main_render_target".into(), + value: Some(SlotValue::RenderTarget(Rc::new(RefCell::new(render_target)))), + } + ); + self.window_tv_id = graph.next_id(); + desc.add_texture_view_slot( + self.window_tv_id, + "window_texture_view", + SlotAttribute::Output, + Some(SlotValue::Lazy), + ); desc } - fn prepare(&mut self, world: &mut lyra_ecs::World) { - let _ = world; - todo!() + 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::RenderGraphPassDesc, context: &mut crate::render::graph::RenderGraphContext) { - let _ = (graph, desc, context); - todo!() + fn execute(&mut self, graph: &mut crate::render::graph::RenderGraph, _desc: &crate::render::graph::RenderGraphPassDesc, _context: &mut crate::render::graph::RenderGraphContext) { + let tv_slot = graph.slot_value_mut(self.main_rt_id) + .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!"); + + 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(self.window_tv_id) + .expect("somehow the window texture view slot is missing"); + *tv_slot = SlotValue::TextureView(Rc::new(view)); + + } } \ No newline at end of file diff --git a/lyra-game/src/render/graph/passes/mod.rs b/lyra-game/src/render/graph/passes/mod.rs index 0b8c295..a8dbd06 100644 --- a/lyra-game/src/render/graph/passes/mod.rs +++ b/lyra-game/src/render/graph/passes/mod.rs @@ -1,8 +1,7 @@ /* mod light_cull_compute; pub use light_cull_compute::*; -mod base; -pub use base::*; + mod depth_prepass; pub use depth_prepass::*; */ @@ -10,5 +9,11 @@ pub use depth_prepass::*; */ /* mod simple_phong; pub use simple_phong::*; */ +mod base; +pub use base::*; + +mod present_pass; +pub use present_pass::*; + mod triangle; pub use triangle::*; \ 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..221837a --- /dev/null +++ b/lyra-game/src/render/graph/passes/present_pass.rs @@ -0,0 +1,52 @@ +use crate::render::graph::{RenderGraphContext, RenderGraphPass, RenderGraphPassDesc, RenderPassSlot, RenderPassType, SlotAttribute, SlotType}; + +/// Supplies some basic things other passes needs. +/// +/// screen size buffer, camera buffer, +pub struct PresentPass { + render_target_slot: String, +} + +impl PresentPass { + pub fn new(render_target_slot: &str) -> Self { + Self { + render_target_slot: render_target_slot.into(), + } + } +} + +impl RenderGraphPass for PresentPass { + fn desc(&mut self, graph: &mut crate::render::graph::RenderGraph) -> crate::render::graph::RenderGraphPassDesc { + let mut desc = RenderGraphPassDesc::new( + graph.next_id(), + &format!("present_{}", self.render_target_slot), + RenderPassType::Presenter, + None, + vec![], + ); + + desc.add_slot( + RenderPassSlot { + ty: SlotType::RenderTarget, + attribute: SlotAttribute::Input, + id: graph.next_id(), + name: self.render_target_slot.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::RenderGraphPassDesc, _context: &mut crate::render::graph::RenderGraphContext) { + let id = graph.slot_id(&self.render_target_slot) + .expect(&format!("render target slot '{}' for PresentPass is missing", self.render_target_slot)); + let mut slot = graph.slot_value_mut(id).unwrap().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/passes/triangle.rs b/lyra-game/src/render/graph/passes/triangle.rs index 937f03b..5464dd2 100644 --- a/lyra-game/src/render/graph/passes/triangle.rs +++ b/lyra-game/src/render/graph/passes/triangle.rs @@ -44,6 +44,13 @@ impl RenderGraphPass for TrianglePass { let color_bgl = Rc::new(color_bgl); let color_bg = Rc::new(color_bg); + let main_rt = graph.slot_id("main_render_target") + .and_then(|s| graph.slot_value(s)) + .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 mut desc = RenderGraphPassDesc::new( graph.next_id(), "TrianglePass", @@ -61,7 +68,7 @@ impl RenderGraphPass for TrianglePass { module: shader, entry_point: "fs_main".into(), targets: vec![Some(wgpu::ColorTargetState { - format: graph.surface_config.format, + format: surface_config_format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 23f4cb6..6a9ed8a 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; use std::rc::Rc; @@ -12,7 +13,7 @@ use wgpu::Limits; use winit::window::Window; use crate::math::Transform; -use crate::render::graph::TrianglePass; +use crate::render::graph::{BasePass, PresentPass, TrianglePass}; use crate::render::material::MaterialUniform; use crate::render::render_buffer::BufferWrapperBuilder; @@ -82,10 +83,8 @@ pub struct InterpTransform { } 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, @@ -214,7 +213,7 @@ impl BasicRenderer { 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 g = RenderGraph::new(device.clone(), queue.clone(), config.clone()); + let mut g = RenderGraph::new(device.clone(), queue.clone()); /* debug!("Adding base pass"); g.add_pass(TrianglePass::new()); debug!("Adding depth pre-pass"); @@ -222,16 +221,18 @@ impl BasicRenderer { debug!("Adding light cull compute pass"); g.add_pass(LightCullComputePass::new(size)); */ + debug!("Adding base pass"); + g.add_pass(BasePass::new(surface, config)); debug!("Adding triangle pass"); g.add_pass(TrianglePass::new()); + debug!("Adding present pass"); + g.add_pass(PresentPass::new("main_render_target")); g.setup(&device); - let mut s = Self { + Self { window, - surface, device, queue, - config, size, clear_color: wgpu::Color { r: 0.1, @@ -244,21 +245,7 @@ impl BasicRenderer { render_limits, graph: g, - }; - - // create the default pipelines - /* let mut pipelines = rustc_hash::FxHashMap::default(); - pipelines.insert(0, Arc::new(RenderPipeline::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 + } } } @@ -270,7 +257,7 @@ impl Renderer for BasicRenderer { #[instrument(skip(self))] fn render(&mut self) -> Result<(), wgpu::SurfaceError> { - self.graph.render(&self.surface); + self.graph.render(); Ok(()) } @@ -279,15 +266,17 @@ impl Renderer for BasicRenderer { 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; - - // tell other things of updated resize - self.surface.configure(&self.device, &self.config); + + // update surface config and the surface + let mut rt = self.graph.slot_value_mut(self.graph.slot_id("main_render_target").unwrap()) + .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); + // 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); - self.graph.surface_config = self.config.clone(); } }