From fc57777a456c98891f0ab0504c1035fbc890a365 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 19 May 2024 12:56:03 -0400 Subject: [PATCH] render: move render targets to be graph slots, create present passes and base passes 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 --- .vscode/launch.json | 18 +++ lyra-game/src/render/graph/execution_path.rs | 7 +- lyra-game/src/render/graph/mod.rs | 108 ++++++++---------- lyra-game/src/render/graph/pass.rs | 36 +++++- lyra-game/src/render/graph/passes/base.rs | 87 +++++++++++--- lyra-game/src/render/graph/passes/mod.rs | 9 +- .../src/render/graph/passes/present_pass.rs | 52 +++++++++ lyra-game/src/render/graph/passes/triangle.rs | 9 +- lyra-game/src/render/renderer.rs | 47 +++----- 9 files changed, 256 insertions(+), 117 deletions(-) create mode 100644 lyra-game/src/render/graph/passes/present_pass.rs 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(); } }