render: get sub render graphs working and create a simple test of them
This commit is contained in:
parent
5f1a61ef52
commit
6c1bff5768
|
@ -20,7 +20,7 @@ pub use render_target::*;
|
||||||
|
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use tracing::{debug_span, instrument, trace, warn};
|
use tracing::{debug_span, instrument, trace, warn};
|
||||||
use wgpu::ComputePass;
|
use wgpu::{CommandEncoder, ComputePass};
|
||||||
|
|
||||||
use super::resource::{ComputePipeline, Pipeline, RenderPipeline};
|
use super::resource::{ComputePipeline, Pipeline, RenderPipeline};
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ struct PassEntry {
|
||||||
pipeline: Rc<RefCell<Option<PipelineResource>>>,
|
pipeline: Rc<RefCell<Option<PipelineResource>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct BindGroupEntry {
|
pub struct BindGroupEntry {
|
||||||
pub label: RenderGraphLabelValue,
|
pub label: RenderGraphLabelValue,
|
||||||
/// BindGroup
|
/// BindGroup
|
||||||
|
@ -99,6 +100,7 @@ pub struct BindGroupEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[derive(Clone)]
|
||||||
struct ResourcedSlot {
|
struct ResourcedSlot {
|
||||||
label: RenderGraphLabelValue,
|
label: RenderGraphLabelValue,
|
||||||
ty: SlotType,
|
ty: SlotType,
|
||||||
|
@ -255,6 +257,10 @@ impl RenderGraph {
|
||||||
*pipeline = Some(res);
|
*pipeline = Some(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for sub in self.sub_graphs.values_mut() {
|
||||||
|
sub.setup(device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, world))]
|
#[instrument(skip(self, world))]
|
||||||
|
@ -306,6 +312,12 @@ impl RenderGraph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_encoder(&self) -> CommandEncoder {
|
||||||
|
self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("graph encoder"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub fn render(&mut self) {
|
pub fn render(&mut self) {
|
||||||
let mut sorted: VecDeque<RenderGraphLabelValue> = petgraph::algo::toposort(&self.node_graph, None)
|
let mut sorted: VecDeque<RenderGraphLabelValue> = petgraph::algo::toposort(&self.node_graph, None)
|
||||||
|
@ -313,9 +325,14 @@ impl RenderGraph {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| self.node_graph[i.clone()].clone())
|
.map(|i| self.node_graph[i.clone()].clone())
|
||||||
.collect();
|
.collect();
|
||||||
//debug!("Render graph execution order: {:?}", sorted);
|
|
||||||
|
|
||||||
let mut encoders = Vec::with_capacity(self.nodes.len() / 2);
|
// A bit of 'encoder hot potato' is played using this.
|
||||||
|
// Although the encoder is an option, its only an option so ownership of it can be given
|
||||||
|
// to the context for the time of the node execution.
|
||||||
|
// After the node is executed, the encoder is taken back. If the node is a presenter node,
|
||||||
|
// the encoder will be submitted and a new one will be made.
|
||||||
|
let mut encoder = Some(self.create_encoder());
|
||||||
|
|
||||||
while let Some(pass_label) = sorted.pop_front() {
|
while let Some(pass_label) = sorted.pop_front() {
|
||||||
let pass = self.nodes.get(&pass_label).unwrap();
|
let pass = self.nodes.get(&pass_label).unwrap();
|
||||||
let pass_inn = pass.inner.clone();
|
let pass_inn = pass.inner.clone();
|
||||||
|
@ -323,48 +340,37 @@ impl RenderGraph {
|
||||||
let pass_desc = pass.desc.clone();
|
let pass_desc = pass.desc.clone();
|
||||||
let pass_desc = (*pass_desc).borrow();
|
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
|
// clone of the Rc's is required to appease the borrow checker
|
||||||
let device = self.device.clone();
|
let device = self.device.clone();
|
||||||
let queue = self.queue.clone();
|
let queue = self.queue.clone();
|
||||||
let mut context = RenderGraphContext::new(device, queue, encoder, pass_label.clone());
|
|
||||||
|
// create a new encoder since the last one was presented
|
||||||
|
if encoder.is_none() {
|
||||||
|
encoder = Some(self.create_encoder());
|
||||||
|
}
|
||||||
|
|
||||||
// all encoders need to be submitted before a presenter node is executed.
|
// all encoders need to be submitted before a presenter node is executed.
|
||||||
if pass_desc.ty == NodeType::Presenter {
|
if pass_desc.ty == NodeType::Presenter {
|
||||||
trace!("Submitting {} encoderd before presenting", encoders.len());
|
trace!("Submitting encoder before presenting");
|
||||||
self.queue.submit(encoders.drain(..));
|
|
||||||
|
let finished = encoder.take().unwrap().finish();
|
||||||
|
self.queue.submit(std::iter::once(finished));
|
||||||
|
|
||||||
|
// Do not create a new encoder yet since this node may be the last node.
|
||||||
|
// A new encoder can be made on the next iteration of this loop.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut context = RenderGraphContext::new(device, queue, encoder.take(), pass_label.clone());
|
||||||
|
|
||||||
trace!("Executing {:?}", pass_label.0);
|
trace!("Executing {:?}", pass_label.0);
|
||||||
let mut inner = pass_inn.borrow_mut();
|
let mut inner = pass_inn.borrow_mut();
|
||||||
inner.execute(self, &pass_desc, &mut context);
|
inner.execute(self, &pass_desc, &mut context);
|
||||||
|
|
||||||
if let Some(encoder) = context.encoder {
|
encoder = context.encoder;
|
||||||
encoders.push(encoder.finish());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !encoders.is_empty() {
|
if let Some(encoder) = encoder {
|
||||||
warn!(
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
"{} 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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,8 +416,9 @@ impl RenderGraph {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn bind_group_layout<L: Into<RenderGraphLabelValue>>(&self, label: L) -> &Rc<wgpu::BindGroupLayout> {
|
pub fn bind_group_layout<L: Into<RenderGraphLabelValue>>(&self, label: L) -> &Rc<wgpu::BindGroupLayout> {
|
||||||
self.try_bind_group_layout(label)
|
let l = label.into();
|
||||||
.expect("Unknown id for bind group layout")
|
self.try_bind_group_layout(l.clone())
|
||||||
|
.unwrap_or_else(|| panic!("Unknown label '{:?}' for bind group layout", l.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_edge(&mut self, from: impl RenderGraphLabel, to: impl RenderGraphLabel)
|
pub fn add_edge(&mut self, from: impl RenderGraphLabel, to: impl RenderGraphLabel)
|
||||||
|
@ -478,9 +485,67 @@ impl RenderGraph {
|
||||||
pub fn sub_graph_mut<L: Into<RenderGraphLabelValue>>(&mut self, label: L) -> Option<&mut RenderGraph> {
|
pub fn sub_graph_mut<L: Into<RenderGraphLabelValue>>(&mut self, label: L) -> Option<&mut RenderGraph> {
|
||||||
self.sub_graphs.get_mut(&label.into())
|
self.sub_graphs.get_mut(&label.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_sub_graph<L: Into<RenderGraphLabelValue>>(&mut self, label: L, sub: RenderGraph) {
|
||||||
|
self.sub_graphs.insert(label.into(), sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SubGraphNode(RenderGraphLabelValue);
|
/// Clone rendering resources (slots, bind groups, etc.) to a sub graph.
|
||||||
|
fn clone_resources_to_sub(&mut self, sub_graph: RenderGraphLabelValue, slots: Vec<RenderGraphLabelValue>) {
|
||||||
|
// instead of inserting the slots to the sub graph as they are extracted from the parent graph,
|
||||||
|
// they are done separately to make the borrow checker happy. If this is not done,
|
||||||
|
// the borrow checker complains about multiple mutable borrows (or an inmutable borrow
|
||||||
|
// while mutable borrowing) to self; caused by borrowing the sub graph from self, and
|
||||||
|
// self.slots.
|
||||||
|
let mut collected_slots = VecDeque::new();
|
||||||
|
let mut collected_bind_groups = VecDeque::new();
|
||||||
|
|
||||||
|
for slot in slots.iter() {
|
||||||
|
let mut found_res = false;
|
||||||
|
|
||||||
|
// Since slots and bind groups may go by the same label,
|
||||||
|
// there must be a way to collect both of them. A flag variable is used to detect
|
||||||
|
// if neither was found.
|
||||||
|
|
||||||
|
if let Some(slot_res) = self.slots.get(slot) {
|
||||||
|
collected_slots.push_back(slot_res.clone());
|
||||||
|
found_res = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bg_res) = self.bind_groups.get(slot) {
|
||||||
|
collected_bind_groups.push_back(bg_res.clone());
|
||||||
|
found_res = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found_res {
|
||||||
|
panic!("sub graph is missing {:?} input slot or bind group", slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sg = self.sub_graph_mut(sub_graph.clone()).unwrap();
|
||||||
|
while let Some(res) = collected_slots.pop_front() {
|
||||||
|
sg.slots.insert(res.label.clone(), res);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(bg) = collected_bind_groups.pop_front() {
|
||||||
|
sg.bind_groups.insert(bg.label.clone(), bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SubGraphNode {
|
||||||
|
subg: RenderGraphLabelValue,
|
||||||
|
slots: Vec<RenderGraphLabelValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubGraphNode {
|
||||||
|
pub fn new<L: Into<RenderGraphLabelValue>>(sub_label: L, slot_labels: Vec<RenderGraphLabelValue>) -> Self {
|
||||||
|
Self {
|
||||||
|
subg: sub_label.into(),
|
||||||
|
slots: slot_labels,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Node for SubGraphNode {
|
impl Node for SubGraphNode {
|
||||||
fn desc<'a, 'b>(&'a mut self, _: &'b mut RenderGraph) -> NodeDesc {
|
fn desc<'a, 'b>(&'a mut self, _: &'b mut RenderGraph) -> NodeDesc {
|
||||||
|
@ -488,8 +553,10 @@ impl Node for SubGraphNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut World, _: &mut RenderGraphContext) {
|
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut World, _: &mut RenderGraphContext) {
|
||||||
let sg = graph.sub_graph_mut(self.0.clone())
|
graph.clone_resources_to_sub(self.subg.clone(), self.slots.clone());
|
||||||
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.0));
|
|
||||||
|
let sg = graph.sub_graph_mut(self.subg.clone())
|
||||||
|
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.subg));
|
||||||
sg.prepare(world);
|
sg.prepare(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,8 +566,10 @@ impl Node for SubGraphNode {
|
||||||
_: &NodeDesc,
|
_: &NodeDesc,
|
||||||
_: &mut RenderGraphContext,
|
_: &mut RenderGraphContext,
|
||||||
) {
|
) {
|
||||||
let sg = graph.sub_graph_mut(self.0.clone())
|
graph.clone_resources_to_sub(self.subg.clone(), self.slots.clone());
|
||||||
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.0));
|
|
||||||
|
let sg = graph.sub_graph_mut(self.subg.clone())
|
||||||
|
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.subg));
|
||||||
sg.render();
|
sg.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use wgpu::util::DeviceExt;
|
||||||
use crate::render::{
|
use crate::render::{
|
||||||
graph::{
|
graph::{
|
||||||
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
|
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
|
||||||
}, renderer::ScreenSize, resource::{ComputePipelineDescriptor, PipelineDescriptor, Shader}
|
}, renderer::ScreenSize, resource::{ComputePipeline, ComputePipelineDescriptor, Shader}
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{BasePassSlots, LightBasePassSlots};
|
use super::{BasePassSlots, LightBasePassSlots};
|
||||||
|
@ -26,12 +26,14 @@ pub enum LightCullComputePassSlots {
|
||||||
|
|
||||||
pub struct LightCullComputePass {
|
pub struct LightCullComputePass {
|
||||||
workgroup_size: glam::UVec2,
|
workgroup_size: glam::UVec2,
|
||||||
|
pipeline: Option<ComputePipeline>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LightCullComputePass {
|
impl LightCullComputePass {
|
||||||
pub fn new(screen_size: winit::dpi::PhysicalSize<u32>) -> Self {
|
pub fn new(screen_size: winit::dpi::PhysicalSize<u32>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workgroup_size: glam::UVec2::new(screen_size.width, screen_size.height),
|
workgroup_size: glam::UVec2::new(screen_size.width, screen_size.height),
|
||||||
|
pipeline: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,18 +43,6 @@ impl Node for LightCullComputePass {
|
||||||
&mut self,
|
&mut self,
|
||||||
graph: &mut crate::render::graph::RenderGraph,
|
graph: &mut crate::render::graph::RenderGraph,
|
||||||
) -> crate::render::graph::NodeDesc {
|
) -> 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 = main_rt.size();
|
|
||||||
|
|
||||||
// initialize some buffers with empty data
|
// initialize some buffers with empty data
|
||||||
let mut contents = Vec::<u8>::new();
|
let mut contents = Vec::<u8>::new();
|
||||||
let contents_len =
|
let contents_len =
|
||||||
|
@ -165,16 +155,16 @@ impl Node for LightCullComputePass {
|
||||||
label: Some("light_indices_grid_bind_group"),
|
label: Some("light_indices_grid_bind_group"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
drop(main_rt);
|
//drop(main_rt);
|
||||||
|
|
||||||
let depth_tex_bgl = graph.bind_group_layout(BasePassSlots::DepthTexture);
|
/* let depth_tex_bgl = graph.bind_group_layout(BasePassSlots::DepthTexture);
|
||||||
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
|
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
|
||||||
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
|
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
|
||||||
let screen_size_bgl = graph.bind_group_layout(BasePassSlots::ScreenSize);
|
let screen_size_bgl = graph.bind_group_layout(BasePassSlots::ScreenSize); */
|
||||||
|
|
||||||
let mut desc = NodeDesc::new(
|
let mut desc = NodeDesc::new(
|
||||||
NodeType::Compute,
|
NodeType::Compute,
|
||||||
Some(PipelineDescriptor::Compute(ComputePipelineDescriptor {
|
/* Some(PipelineDescriptor::Compute(ComputePipelineDescriptor {
|
||||||
label: Some("light_cull_pipeline".into()),
|
label: Some("light_cull_pipeline".into()),
|
||||||
push_constant_ranges: vec![],
|
push_constant_ranges: vec![],
|
||||||
layouts: vec![
|
layouts: vec![
|
||||||
|
@ -186,7 +176,8 @@ impl Node for LightCullComputePass {
|
||||||
],
|
],
|
||||||
shader,
|
shader,
|
||||||
shader_entry_point: "cs_main".into(),
|
shader_entry_point: "cs_main".into(),
|
||||||
})),
|
})), */
|
||||||
|
None,
|
||||||
vec![(
|
vec![(
|
||||||
&LightCullComputePassSlots::LightIndicesGridGroup,
|
&LightCullComputePassSlots::LightIndicesGridGroup,
|
||||||
light_indices_bg,
|
light_indices_bg,
|
||||||
|
@ -194,11 +185,6 @@ impl Node for LightCullComputePass {
|
||||||
)],
|
)],
|
||||||
);
|
);
|
||||||
|
|
||||||
desc.add_texture_view_slot(
|
|
||||||
BasePassSlots::WindowTextureView,
|
|
||||||
SlotAttribute::Input,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
desc.add_buffer_slot(
|
desc.add_buffer_slot(
|
||||||
BasePassSlots::ScreenSize,
|
BasePassSlots::ScreenSize,
|
||||||
SlotAttribute::Input,
|
SlotAttribute::Input,
|
||||||
|
@ -214,7 +200,7 @@ impl Node for LightCullComputePass {
|
||||||
desc
|
desc
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&mut self, _graph: &mut RenderGraph, world: &mut World, context: &mut RenderGraphContext) {
|
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut World, context: &mut RenderGraphContext) {
|
||||||
context.queue_buffer_write_with(LightCullComputePassSlots::IndexCounterBuffer, 0, 0);
|
context.queue_buffer_write_with(LightCullComputePassSlots::IndexCounterBuffer, 0, 0);
|
||||||
|
|
||||||
let screen_size = world.get_resource::<ScreenSize>();
|
let screen_size = world.get_resource::<ScreenSize>();
|
||||||
|
@ -222,6 +208,37 @@ impl Node for LightCullComputePass {
|
||||||
self.workgroup_size = screen_size.xy();
|
self.workgroup_size = screen_size.xy();
|
||||||
todo!("Resize buffers and other resources");
|
todo!("Resize buffers and other resources");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.pipeline.is_none() {
|
||||||
|
let device = graph.device();
|
||||||
|
|
||||||
|
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 light_indices_bg_layout = graph.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup);
|
||||||
|
|
||||||
|
let shader = Rc::new(Shader {
|
||||||
|
label: Some("light_cull_comp_shader".into()),
|
||||||
|
source: include_str!("../../shaders/light_cull.comp.wgsl").to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline = ComputePipeline::create(device, &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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.pipeline = Some(pipeline);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(
|
fn execute(
|
||||||
|
@ -230,11 +247,7 @@ impl Node for LightCullComputePass {
|
||||||
_: &crate::render::graph::NodeDesc,
|
_: &crate::render::graph::NodeDesc,
|
||||||
context: &mut RenderGraphContext,
|
context: &mut RenderGraphContext,
|
||||||
) {
|
) {
|
||||||
let label = context.label.clone();
|
let pipeline = self.pipeline.as_mut().unwrap();
|
||||||
|
|
||||||
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 {
|
let mut pass = context.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||||
label: Some("light_cull_pass"),
|
label: Some("light_cull_pass"),
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::{
|
||||||
render::{
|
render::{
|
||||||
desc_buf_lay::DescVertexBufferLayout, graph::{
|
desc_buf_lay::DescVertexBufferLayout, graph::{
|
||||||
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext
|
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext
|
||||||
}, 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
|
}, material::{Material, MaterialUniform}, render_buffer::{BufferStorage, BufferWrapper}, render_job::RenderJob, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, texture::RenderTexture, transform_buffer_storage::{TransformBuffers, TransformGroup}, vertex::Vertex
|
||||||
},
|
},
|
||||||
DeltaTime,
|
DeltaTime,
|
||||||
};
|
};
|
||||||
|
@ -61,6 +61,9 @@ pub struct MeshPass {
|
||||||
entity_meshes: FxHashMap<Entity, uuid::Uuid>,
|
entity_meshes: FxHashMap<Entity, uuid::Uuid>,
|
||||||
|
|
||||||
default_texture: Option<RenderTexture>,
|
default_texture: Option<RenderTexture>,
|
||||||
|
|
||||||
|
pipeline: Option<RenderPipeline>,
|
||||||
|
material_bgl: Option<Rc<wgpu::BindGroupLayout>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MeshPass {
|
impl MeshPass {
|
||||||
|
@ -209,7 +212,7 @@ impl Node for MeshPass {
|
||||||
let device = graph.device();
|
let device = graph.device();
|
||||||
|
|
||||||
let transforms = TransformBuffers::new(device);
|
let transforms = TransformBuffers::new(device);
|
||||||
let transform_bgl = transforms.bindgroup_layout.clone();
|
//let transform_bgl = transforms.bindgroup_layout.clone();
|
||||||
self.transforms = Some(transforms);
|
self.transforms = Some(transforms);
|
||||||
|
|
||||||
let texture_bind_group_layout = Rc::new(RenderTexture::create_layout(&device));
|
let texture_bind_group_layout = Rc::new(RenderTexture::create_layout(&device));
|
||||||
|
@ -222,6 +225,7 @@ impl Node for MeshPass {
|
||||||
.contents(&[MaterialUniform::default()])
|
.contents(&[MaterialUniform::default()])
|
||||||
.finish_parts(device);
|
.finish_parts(device);
|
||||||
let material_bgl = Rc::new(material_bgl);
|
let material_bgl = Rc::new(material_bgl);
|
||||||
|
self.material_bgl = Some(material_bgl.clone());
|
||||||
let material_bg = Rc::new(material_bg);
|
let material_bg = Rc::new(material_bg);
|
||||||
|
|
||||||
self.material_buffer = Some(material_buf);
|
self.material_buffer = Some(material_buf);
|
||||||
|
@ -231,13 +235,13 @@ impl Node for MeshPass {
|
||||||
self.default_texture = Some(RenderTexture::from_bytes(&device, &graph.queue, texture_bind_group_layout.clone(), bytes, "default_texture").unwrap());
|
self.default_texture = Some(RenderTexture::from_bytes(&device, &graph.queue, texture_bind_group_layout.clone(), bytes, "default_texture").unwrap());
|
||||||
|
|
||||||
// get surface config format
|
// get surface config format
|
||||||
let main_rt = graph.slot_value(BasePassSlots::MainRenderTarget)
|
/* let main_rt = graph.slot_value(BasePassSlots::MainRenderTarget)
|
||||||
.and_then(|s| s.as_render_target())
|
.and_then(|s| s.as_render_target())
|
||||||
.expect("missing main render target");
|
.expect("missing main render target");
|
||||||
let surface_config_format = main_rt.format();
|
let surface_config_format = main_rt.format();
|
||||||
drop(main_rt);
|
drop(main_rt); */
|
||||||
|
|
||||||
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
|
/* let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
|
||||||
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
|
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
|
||||||
let light_grid_bgl = graph
|
let light_grid_bgl = graph
|
||||||
.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup);
|
.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup);
|
||||||
|
@ -245,11 +249,12 @@ impl Node for MeshPass {
|
||||||
let shader = Rc::new(Shader {
|
let shader = Rc::new(Shader {
|
||||||
label: Some("base_shader".into()),
|
label: Some("base_shader".into()),
|
||||||
source: include_str!("../../shaders/base.wgsl").to_string(),
|
source: include_str!("../../shaders/base.wgsl").to_string(),
|
||||||
});
|
}); */
|
||||||
|
|
||||||
let desc = NodeDesc::new(
|
let desc = NodeDesc::new(
|
||||||
NodeType::Render,
|
NodeType::Render,
|
||||||
Some(PipelineDescriptor::Render(RenderPipelineDescriptor {
|
None,
|
||||||
|
/* Some(PipelineDescriptor::Render(RenderPipelineDescriptor {
|
||||||
label: Some("meshes".into()),
|
label: Some("meshes".into()),
|
||||||
layouts: vec![
|
layouts: vec![
|
||||||
texture_bind_group_layout.clone(),
|
texture_bind_group_layout.clone(),
|
||||||
|
@ -287,7 +292,7 @@ impl Node for MeshPass {
|
||||||
primitive: wgpu::PrimitiveState::default(),
|
primitive: wgpu::PrimitiveState::default(),
|
||||||
multisample: wgpu::MultisampleState::default(),
|
multisample: wgpu::MultisampleState::default(),
|
||||||
multiview: None,
|
multiview: None,
|
||||||
})),
|
})), */
|
||||||
vec![
|
vec![
|
||||||
(&MeshesPassSlots::Material, material_bg, Some(material_bgl)),
|
(&MeshesPassSlots::Material, material_bg, Some(material_bgl)),
|
||||||
],
|
],
|
||||||
|
@ -296,8 +301,8 @@ impl Node for MeshPass {
|
||||||
desc
|
desc
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, _graph, world, context))]
|
#[instrument(skip(self, graph, world, context))]
|
||||||
fn prepare(&mut self, _graph: &mut RenderGraph, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) {
|
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) {
|
||||||
let device = &context.device;
|
let device = &context.device;
|
||||||
let queue = &context.queue;
|
let queue = &context.queue;
|
||||||
let render_limits = device.limits();
|
let render_limits = device.limits();
|
||||||
|
@ -432,6 +437,67 @@ impl Node for MeshPass {
|
||||||
|
|
||||||
let transforms = self.transforms.as_mut().unwrap();
|
let transforms = self.transforms.as_mut().unwrap();
|
||||||
transforms.send_to_gpu(&queue);
|
transforms.send_to_gpu(&queue);
|
||||||
|
|
||||||
|
if self.pipeline.is_none() {
|
||||||
|
let device = graph.device();
|
||||||
|
|
||||||
|
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.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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.pipeline = Some(RenderPipeline::create(device, &RenderPipelineDescriptor {
|
||||||
|
label: Some("meshes".into()),
|
||||||
|
layouts: vec![
|
||||||
|
self.texture_bind_group_layout.as_ref().unwrap().clone(),
|
||||||
|
//transform_bgl
|
||||||
|
self.transforms.as_ref().unwrap().bindgroup_layout.clone(),
|
||||||
|
camera_bgl.clone(),
|
||||||
|
lights_bgl.clone(),
|
||||||
|
self.material_bgl.as_ref().unwrap().clone(),
|
||||||
|
self.texture_bind_group_layout.as_ref().unwrap().clone(),
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(
|
fn execute(
|
||||||
|
@ -466,8 +532,9 @@ impl Node for MeshPass {
|
||||||
let material_bg = graph
|
let material_bg = graph
|
||||||
.bind_group(MeshesPassSlots::Material);
|
.bind_group(MeshesPassSlots::Material);
|
||||||
|
|
||||||
let pipeline = graph.pipeline(context.label.clone())
|
/* let pipeline = graph.pipeline(context.label.clone())
|
||||||
.expect("Failed to find pipeline for MeshPass");
|
.expect("Failed to find pipeline for MeshPass"); */
|
||||||
|
let pipeline = self.pipeline.as_ref().unwrap();
|
||||||
|
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("Render Pass"),
|
label: Some("Render Pass"),
|
||||||
|
@ -495,7 +562,7 @@ impl Node for MeshPass {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
pass.set_pipeline(&pipeline.as_render());
|
pass.set_pipeline(&pipeline);
|
||||||
|
|
||||||
//let material_buffer_bg = self.material_buffer.as_ref().unwrap().bindgroup();
|
//let material_buffer_bg = self.material_buffer.as_ref().unwrap().bindgroup();
|
||||||
let default_texture = self.default_texture.as_ref().unwrap();
|
let default_texture = self.default_texture.as_ref().unwrap();
|
||||||
|
|
|
@ -15,3 +15,6 @@ pub use present_pass::*;
|
||||||
|
|
||||||
mod init;
|
mod init;
|
||||||
pub use init::*;
|
pub use init::*;
|
||||||
|
|
||||||
|
mod tint;
|
||||||
|
pub use tint::*;
|
|
@ -27,6 +27,25 @@ impl RenderTarget {
|
||||||
Self(RenderTargetInner::Surface { surface, config })
|
Self(RenderTargetInner::Surface { surface, config })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_texture(device: &wgpu::Device, format: wgpu::TextureFormat, size: math::UVec2) -> Self {
|
||||||
|
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: None,
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: size.x,
|
||||||
|
height: size.y,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
Self(RenderTargetInner::Texture { texture: Arc::new(tex) })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format(&self) -> wgpu::TextureFormat {
|
pub fn format(&self) -> wgpu::TextureFormat {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
RenderTargetInner::Surface { config, .. } => config.format,
|
RenderTargetInner::Surface { config, .. } => config.format,
|
||||||
|
@ -63,8 +82,10 @@ impl RenderTarget {
|
||||||
surface.configure(device, config);
|
surface.configure(device, config);
|
||||||
},
|
},
|
||||||
RenderTargetInner::Texture { texture } => {
|
RenderTargetInner::Texture { texture } => {
|
||||||
let _ = texture;
|
let format = texture.format();
|
||||||
todo!()
|
let size = self.size();
|
||||||
|
|
||||||
|
*self = Self::new_texture(device, format, size);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,11 @@ use std::ops::{Deref, DerefMut};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use lyra_ecs::World;
|
use lyra_ecs::World;
|
||||||
|
use lyra_game_derive::RenderGraphLabel;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
use crate::render::graph::{BasePass, BasePassLabel, BasePassSlots, LightBasePass, LightBasePassLabel, LightCullComputePass, LightCullComputePassLabel, MeshPass, MeshesPassLabel, PresentPass, PresentPassLabel, RenderTarget};
|
use crate::render::graph::{BasePass, BasePassLabel, BasePassSlots, LightBasePass, LightBasePassLabel, LightCullComputePass, LightCullComputePassLabel, MeshPass, MeshesPassLabel, PresentPass, PresentPassLabel, RenderGraphLabelValue, RenderTarget, SubGraphNode, TintPass, TintPassLabel, TintPassSlots};
|
||||||
|
|
||||||
use super::graph::RenderGraph;
|
use super::graph::RenderGraph;
|
||||||
use super::{resource::RenderPipeline, render_job::RenderJob};
|
use super::{resource::RenderPipeline, render_job::RenderJob};
|
||||||
|
@ -30,6 +31,9 @@ impl DerefMut for ScreenSize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, RenderGraphLabel)]
|
||||||
|
struct TestSubGraphLabel;
|
||||||
|
|
||||||
pub trait Renderer {
|
pub trait Renderer {
|
||||||
fn prepare(&mut self, main_world: &mut World);
|
fn prepare(&mut self, main_world: &mut World);
|
||||||
fn render(&mut self) -> Result<(), wgpu::SurfaceError>;
|
fn render(&mut self) -> Result<(), wgpu::SurfaceError>;
|
||||||
|
@ -123,37 +127,99 @@ impl BasicRenderer {
|
||||||
let device = Arc::new(device);
|
let device = Arc::new(device);
|
||||||
let queue = Arc::new(queue);
|
let queue = Arc::new(queue);
|
||||||
|
|
||||||
let mut g = RenderGraph::new(device.clone(), queue.clone());
|
let surface_size = wgpu::Extent3d {
|
||||||
|
width: config.width,
|
||||||
|
height: config.height,
|
||||||
|
depth_or_array_layers: 1
|
||||||
|
};
|
||||||
let surface_target = RenderTarget::from_surface(surface, config);
|
let surface_target = RenderTarget::from_surface(surface, config);
|
||||||
|
|
||||||
|
let headless_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("headless_texture"),
|
||||||
|
size: surface_size,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: surface_format,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT // we'll be rendering to it
|
||||||
|
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let headless_target = RenderTarget::from(headless_texture);
|
||||||
|
|
||||||
|
|
||||||
|
let mut main_graph = RenderGraph::new(device.clone(), queue.clone());
|
||||||
|
|
||||||
debug!("Adding base pass");
|
debug!("Adding base pass");
|
||||||
g.add_node(BasePassLabel, BasePass::new(surface_target));
|
main_graph.add_node(BasePassLabel, BasePass::new(surface_target));
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut forward_plus_graph = RenderGraph::new(device.clone(), queue.clone());
|
||||||
|
|
||||||
debug!("Adding light base pass");
|
debug!("Adding light base pass");
|
||||||
g.add_node(LightBasePassLabel, LightBasePass::new());
|
forward_plus_graph.add_node(LightBasePassLabel, LightBasePass::new());
|
||||||
|
|
||||||
debug!("Adding light cull compute pass");
|
debug!("Adding light cull compute pass");
|
||||||
g.add_node(LightCullComputePassLabel, LightCullComputePass::new(size));
|
forward_plus_graph.add_node(LightCullComputePassLabel, LightCullComputePass::new(size));
|
||||||
|
|
||||||
|
debug!("Adding mesh pass");
|
||||||
|
forward_plus_graph.add_node(MeshesPassLabel, MeshPass::new());
|
||||||
|
|
||||||
|
forward_plus_graph.add_edge(LightBasePassLabel, LightCullComputePassLabel);
|
||||||
|
|
||||||
|
main_graph.add_sub_graph(TestSubGraphLabel, forward_plus_graph);
|
||||||
|
main_graph.add_node(TestSubGraphLabel, SubGraphNode::new(TestSubGraphLabel,
|
||||||
|
vec![
|
||||||
|
RenderGraphLabelValue::from(BasePassSlots::WindowTextureView),
|
||||||
|
RenderGraphLabelValue::from(BasePassSlots::MainRenderTarget),
|
||||||
|
RenderGraphLabelValue::from(BasePassSlots::DepthTexture),
|
||||||
|
RenderGraphLabelValue::from(BasePassSlots::DepthTextureView),
|
||||||
|
RenderGraphLabelValue::from(BasePassSlots::Camera),
|
||||||
|
RenderGraphLabelValue::from(BasePassSlots::ScreenSize),
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let present_pass_label = PresentPassLabel::new(BasePassSlots::Frame);//TintPassSlots::Frame);
|
||||||
|
let p = PresentPass::from_node_label(present_pass_label.clone());
|
||||||
|
main_graph.add_node(p.label.clone(), p);
|
||||||
|
|
||||||
|
main_graph.add_edge(BasePassLabel, TestSubGraphLabel);
|
||||||
|
main_graph.add_edge(TestSubGraphLabel, present_pass_label);
|
||||||
|
|
||||||
|
/* debug!("Adding base pass");
|
||||||
|
g.add_node(BasePassLabel, BasePass::new(surface_target));
|
||||||
|
|
||||||
//debug!("Adding triangle pass");
|
//debug!("Adding triangle pass");
|
||||||
//g.add_node(TrianglePass::new());
|
//g.add_node(TrianglePass::new());
|
||||||
|
|
||||||
debug!("Adding mesh pass");
|
|
||||||
g.add_node(MeshesPassLabel, MeshPass::new());
|
|
||||||
|
|
||||||
debug!("Adding present pass");
|
debug!("Adding present pass");
|
||||||
let present_pass_label = PresentPassLabel::new(BasePassSlots::Frame);
|
let present_pass_label = PresentPassLabel::new(BasePassSlots::Frame);//TintPassSlots::Frame);
|
||||||
let p = PresentPass::from_node_label(present_pass_label.clone());
|
let p = PresentPass::from_node_label(present_pass_label.clone());
|
||||||
g.add_node(p.label.clone(), p);
|
g.add_node(p.label.clone(), p); */
|
||||||
|
|
||||||
g.add_edge(BasePassLabel, LightBasePassLabel);
|
/* debug!("adding tint pass");
|
||||||
|
g.add_node(TintPassLabel, TintPass::new(surface_target));
|
||||||
|
|
||||||
|
g.add_edge(BasePassLabel, TintPassLabel);
|
||||||
|
g.add_edge(LightCullComputePassLabel, TintPassLabel);
|
||||||
|
g.add_edge(MeshesPassLabel, TintPassLabel);
|
||||||
|
|
||||||
|
g.add_edge(TintPassLabel, present_pass_label.clone());
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* g.add_edge(BasePassLabel, LightBasePassLabel);
|
||||||
g.add_edge(LightBasePassLabel, LightCullComputePassLabel);
|
g.add_edge(LightBasePassLabel, LightCullComputePassLabel);
|
||||||
g.add_edge(BasePassLabel, MeshesPassLabel);
|
g.add_edge(BasePassLabel, MeshesPassLabel);
|
||||||
|
|
||||||
// make sure that present runs last
|
|
||||||
g.add_edge(BasePassLabel, present_pass_label.clone());
|
g.add_edge(BasePassLabel, present_pass_label.clone());
|
||||||
g.add_edge(LightCullComputePassLabel, present_pass_label.clone());
|
g.add_edge(LightCullComputePassLabel, present_pass_label.clone());
|
||||||
g.add_edge(MeshesPassLabel, present_pass_label.clone());
|
g.add_edge(MeshesPassLabel, present_pass_label.clone()); */
|
||||||
|
|
||||||
g.setup(&device);
|
main_graph.setup(&device);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
window,
|
window,
|
||||||
|
@ -169,7 +235,7 @@ impl BasicRenderer {
|
||||||
render_pipelines: Default::default(),
|
render_pipelines: Default::default(),
|
||||||
render_jobs: Default::default(),
|
render_jobs: Default::default(),
|
||||||
|
|
||||||
graph: g,
|
graph: main_graph,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue