Implement a Render Graph #16

Merged
SeanOMik merged 20 commits from feature/render-graph into main 2024-06-15 22:54:47 +00:00
9 changed files with 256 additions and 117 deletions
Showing only changes of commit fc57777a45 - Show all commits

18
.vscode/launch.json vendored
View File

@ -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",

View File

@ -11,9 +11,11 @@ pub struct GraphExecutionPath {
}
impl GraphExecutionPath {
pub fn new(built_in_slots: FxHashSet<u64>, 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);

View File

@ -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<Rc<wgpu::BindGroupLayout>>,
}
#[allow(dead_code)]
struct ResourcedSlot {
name: String,
//slot: RenderPassSlot,
@ -54,6 +55,13 @@ pub struct PipelineResource {
pub bg_layout_name_lookup: HashMap<String, u32>,
}
#[derive(Debug)]
pub struct RenderTarget {
pub surface: wgpu::Surface,
pub surface_config: wgpu::SurfaceConfiguration,
pub current_texture: Option<wgpu::SurfaceTexture>,
}
pub struct RenderGraph {
device: Rc<wgpu::Device>,
queue: Rc<wgpu::Queue>,
@ -69,42 +77,21 @@ pub struct RenderGraph {
pipelines: FxHashMap<u64, PipelineResource>,
current_id: u64,
exec_path: Option<GraphExecutionPath>,
pub(crate) surface_config: wgpu::SurfaceConfiguration,
}
impl RenderGraph {
pub fn new(
device: Rc<wgpu::Device>,
queue: Rc<wgpu::Queue>,
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<wgpu::Device>, queue: Rc<wgpu::Queue>) -> 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> {

View File

@ -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<wgpu::TextureView>),
Sampler(Rc<wgpu::Sampler>),
Texture(Rc<wgpu::Texture>),
Buffer(Rc<wgpu::Buffer>),
RenderTarget(Rc<RefCell<RenderTarget>>),
}
impl SlotValue {
@ -52,6 +57,20 @@ impl SlotValue {
_ => None,
}
}
pub fn as_render_target(&self) -> Option<Ref<RenderTarget>> {
match self {
Self::RenderTarget(v) => Some(v.borrow()),
_ => None,
}
}
pub fn as_render_target_mut(&mut self) -> Option<RefMut<RenderTarget>> {
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<SlotValue>,
) {
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<SlotValue>,
) {
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<SlotValue>,
) {
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<SlotValue>,
) {
debug_assert!(
matches!(value, None | Some(SlotValue::Sampler(_))),
matches!(value, None | Some(SlotValue::Lazy) | Some(SlotValue::Sampler(_))),
"slot value is not a sampler"
);

View File

@ -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<RenderTarget>,
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));
}
}

View File

@ -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::*;

View File

@ -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();
}
}

View File

@ -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,
})],

View File

@ -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<wgpu::Device>, // device does not need to be mutable, no need for refcell
pub queue: Rc<wgpu::Queue>,
pub config: wgpu::SurfaceConfiguration,
pub size: winit::dpi::PhysicalSize<u32>,
pub window: Arc<Window>,
@ -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<u32>) {
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::<ScreenSize>();
world_ss.0 = glam::UVec2::new(new_size.width, new_size.height);
self.graph.surface_config = self.config.clone();
}
}