render: move render targets to be graph slots, create present passes and base passes
ci/woodpecker/pr/debug Pipeline failed Details
ci/woodpecker/pr/release Pipeline failed Details

Since the render graph no longer has default slots, base passes must be created that supply things like render targets. This also makes it easier to render offscreen to some other surface that is not the window, or just some other texture
This commit is contained in:
SeanOMik 2024-05-19 12:56:03 -04:00
parent 8c3446389c
commit fc57777a45
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
9 changed files with 256 additions and 117 deletions

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
// 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());
}
}
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());
output.present();
}
}
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();
}
}