From 87dc7cc83485de62e62634556156f48a566d5312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 9 Nov 2026 06:45:47 +0100 Subject: [PATCH 1/2] renderer: retained scene nodes go through the depth prepass The depth prepass held only the cached models; retained scene-graph nodes were drawn in the main pass with the depth-writing scene pipeline, so every overlapping fragment of them ran the full lighting shader. A chess board over its table, for instance, lit the table's pixels under the board too. Scene::render_depth_prepass draws the opaque world-layer nodes depth-only in the prepass (the scene depth pipeline, the node's base material for the cutout), and the main pass draws them with the depth-equal pipelines the cached models use: scene_pipeline_prepassed, and the layered materials' opaque_pipeline(uv1, prepassed). Both passes iterate one shared selection (Scene::opaque_node_draws: visibility, layer, culling, GPU-driven, blend, refraction, active LOD), so each main-pass fragment finds the depth the prepass wrote for it. wasm, where the prepass draws nothing, and the prepass diagnostic skips keep the depth-writing pipeline. Pixel 4a (Adreno 618), the web3d chess game in a game, quality off: main_hdr_pass 118 ms -> 70 ms of GPU time, image unchanged. --- native/shared/src/renderer/scene_pass.rs | 32 +++++++- native/shared/src/scene.rs | 98 +++++++++++++++++------- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/native/shared/src/renderer/scene_pass.rs b/native/shared/src/renderer/scene_pass.rs index 20aceaea..aa879e0e 100644 --- a/native/shared/src/renderer/scene_pass.rs +++ b/native/shared/src/renderer/scene_pass.rs @@ -146,6 +146,13 @@ impl Renderer { let inline_visibility_recorded = false; #[cfg(feature = "models3d")] let mut virtual_visibility_recorded = false; + // Whether the opaque retained nodes get their depth here, and so take + // the depth-equal pipelines in the main pass. Not on wasm, where the + // prepass draws nothing (above). + let scene_nodes_prepassed = cfg!(not(target_arch = "wasm32")) + && !self.dbg_skip("prepass") + && !self.dbg_skip("prepass_draws") + && !self.dbg_skip("scene_graph"); if !self.dbg_skip("prepass") { let prepass_ts = profiler.pass_timestamp_writes("depth_prepass"); let color_attachments = [self.gpu_driven.visibility_raster_attachment()]; @@ -238,6 +245,15 @@ impl Renderer { } } } + // The opaque retained nodes, so the main pass shades each of + // their pixels once (see Scene::render_depth_prepass). + if !self.dbg_skip("scene_graph") { + scene.render_depth_prepass( + &mut pass, + self.gpu_driven.submitting(), + self.imported_refraction_enabled, + ); + } #[cfg(feature = "models3d")] if virtual_visibility_prepared { virtual_visibility_recorded = @@ -652,17 +668,25 @@ impl Renderer { } } - // Retained scene-graph nodes are not in the prepass, so they still need - // the depth-writing pipeline. + // Retained scene-graph nodes went through the depth prepass with + // the cached models (`scene_nodes_prepassed`): shade each pixel + // once with the depth-equal pipelines. Without the prepass + // (wasm, or a diagnostic skip) they keep the depth-writing one. if !self.dbg_skip("scene_graph") { // SH-055 diag - pass.set_pipeline(&self.scene_pipeline); + let scene_pipeline = if scene_nodes_prepassed { + &self.scene_pipeline_prepassed + } else { + &self.scene_pipeline + }; + pass.set_pipeline(scene_pipeline); scene.render_with_material_specializations( &mut pass, self.gpu_driven.submitting(), self.imported_refraction_enabled, - &self.scene_pipeline, + scene_pipeline, self.scene_layered_pbr_resources.as_ref(), + scene_nodes_prepassed, ); } } diff --git a/native/shared/src/scene.rs b/native/shared/src/scene.rs index de9b9466..be1abced 100644 --- a/native/shared/src/scene.rs +++ b/native/shared/src/scene.rs @@ -2260,6 +2260,7 @@ impl SceneGraph { None, None, 0, + false, ); } @@ -2270,6 +2271,7 @@ impl SceneGraph { imported_refraction_enabled: bool, base_pipeline: &'a wgpu::RenderPipeline, layered: Option<&'a crate::renderer::layered_pbr_scene::SceneLayeredPbrResources>, + prepassed: bool, ) { self.render_with_material_pipeline_selection( pass, @@ -2278,6 +2280,7 @@ impl SceneGraph { Some(base_pipeline), layered, 0, + prepassed, ); } @@ -2296,23 +2299,21 @@ impl SceneGraph { Some(base_pipeline), layered, render_layer, + false, ); } - fn render_with_material_pipeline_selection<'a>( + /// The opaque retained nodes one camera pass draws in `render_layer`, with + /// the vertex/index buffers of each node's active LOD. The depth prepass + /// and the main pass both draw exactly this set, so every main-pass + /// fragment finds the depth the prepass wrote for it. + fn opaque_node_draws<'a>( &'a self, - pass: &mut wgpu::RenderPass<'a>, gpu_driven_enabled: bool, imported_refraction_enabled: bool, - base_pipeline: Option<&'a wgpu::RenderPipeline>, - layered: Option<&'a crate::renderer::layered_pbr_scene::SceneLayeredPbrResources>, render_layer: u32, - ) { - // The renderer enters with the ordinary scene pipeline already bound. - // Public callers that do not opt into specialization retain the old - // contract and incur no per-node pipeline selection. - let mut current_pipeline = base_pipeline.map(|_| (false, false)); - for (_handle, node) in self.nodes.iter() { + ) -> impl Iterator, &'a wgpu::Buffer, &'a wgpu::Buffer, u32)> + 'a { + self.nodes.iter().filter_map(move |(_handle, node)| { if !node.visible || node.render_layer != render_layer || node.gi_only @@ -2322,19 +2323,19 @@ impl SceneGraph { // attachment and must not inherit those world-only results. || (render_layer == 0 && (!node.in_view_frustum || node.occluded)) { - continue; + return None; } if gpu_driven_enabled && self.gpu_driven_scene_active && scene_node_gpu_driven_ready(node, imported_refraction_enabled) { - continue; + return None; } if node.material.alpha_mode == MaterialAlphaMode::Blend { - continue; + return None; } if imported_refraction_enabled && node.material.transmission.is_active() { - continue; + return None; } // Active LOD overrides the base buffers for the camera pass // (shadows/picking/BLAS keep using the base geometry). @@ -2343,22 +2344,63 @@ impl SceneGraph { .get(node.active_lod.max(0) as usize) .filter(|_| node.active_lod >= 0); let (vb, ib, index_count) = match active_lod { - Some(lod) => { - let (Some(vb), Some(ib)) = (lod.gpu_vb.as_ref(), lod.gpu_ib.as_ref()) else { - continue; - }; - (vb, ib, lod.gpu_index_count) - } - None => { - let Some(vb) = &node.gpu_vb else { continue }; - let Some(ib) = &node.gpu_ib else { continue }; - (vb, ib, node.gpu_index_count) - } + Some(lod) => (lod.gpu_vb.as_ref()?, lod.gpu_ib.as_ref()?, lod.gpu_index_count), + None => (node.gpu_vb.as_ref()?, node.gpu_ib.as_ref()?, node.gpu_index_count), }; - let Some(bg) = &node.gpu_uniform_bg else { + node.gpu_uniform_bg.as_ref()?; + node.gpu_material_bg.as_ref()?; + Some((node, active_lod, vb, ib, index_count)) + }) + } + + /// Depth-only draws of the opaque retained nodes in the world layer, into + /// the depth prepass (the caller binds the depth pipeline, lighting and + /// joints). The main pass then shades each of their pixels once, with the + /// depth-equal `prepassed` pipelines, instead of every overlapping + /// fragment; on a tiler (Adreno) a depth-tested opaque prepass is also what + /// keeps early-Z working for the rest of the pass. The base material bind + /// group serves layered materials too: the depth stage reads only the + /// cutout. + pub(crate) fn render_depth_prepass<'a>( + &'a self, + pass: &mut wgpu::RenderPass<'a>, + gpu_driven_enabled: bool, + imported_refraction_enabled: bool, + ) { + for (node, _lod, vb, ib, index_count) in + self.opaque_node_draws(gpu_driven_enabled, imported_refraction_enabled, 0) + { + let (Some(bg), Some(material)) = (&node.gpu_uniform_bg, &node.gpu_material_bg) else { continue; }; - let Some(base_material) = &node.gpu_material_bg else { + pass.set_bind_group(0, bg, &[]); + pass.set_bind_group(2, material, &[]); + pass.set_vertex_buffer(0, vb.slice(..)); + pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32); + pass.draw_indexed(0..index_count, 0, 0..1); + } + } + + #[allow(clippy::too_many_arguments)] + fn render_with_material_pipeline_selection<'a>( + &'a self, + pass: &mut wgpu::RenderPass<'a>, + gpu_driven_enabled: bool, + imported_refraction_enabled: bool, + base_pipeline: Option<&'a wgpu::RenderPipeline>, + layered: Option<&'a crate::renderer::layered_pbr_scene::SceneLayeredPbrResources>, + render_layer: u32, + prepassed: bool, + ) { + // The renderer enters with the ordinary scene pipeline already bound. + // Public callers that do not opt into specialization retain the old + // contract and incur no per-node pipeline selection. + let mut current_pipeline = base_pipeline.map(|_| (false, false)); + for (node, active_lod, vb, ib, index_count) in + self.opaque_node_draws(gpu_driven_enabled, imported_refraction_enabled, render_layer) + { + let (Some(bg), Some(base_material)) = (&node.gpu_uniform_bg, &node.gpu_material_bg) + else { continue; }; let layered_material = layered.and_then(|_| node.gpu_layered_material_bg.as_ref()); @@ -2377,7 +2419,7 @@ impl SceneGraph { let key = (layered_material.is_some(), uses_uv1); if current_pipeline != Some(key) { if let (Some(layered), Some(_)) = (layered, layered_material) { - pass.set_pipeline(layered.opaque_pipeline(uses_uv1, false)); + pass.set_pipeline(layered.opaque_pipeline(uses_uv1, prepassed)); } else { pass.set_pipeline(base_pipeline); } From 70b115a29e80d13214571a059723b155b015bad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 9 Nov 2026 11:49:11 +0100 Subject: [PATCH 2/2] prepass: retained nodes cull back faces in both passes; translucent nodes stay out The retained nodes' main-pass pipelines (scene_pipeline, the layered opaque ones) cull back faces, so their prepass and their depth-equal pipeline now do too (scene_node_depth_pipeline, scene_node_pipeline_prepassed). Nodes with a fractional opacity blend over what lies behind them, so they stay out of the prepass and keep the depth-writing pipelines (SceneNode::depth_prepassed). Also rustfmt. --- native/shared/src/renderer/lazy_pipeline.rs | 2 + native/shared/src/renderer/mod.rs | 196 ++++++++++++-------- native/shared/src/renderer/scene_pass.rs | 15 +- native/shared/src/scene.rs | 64 +++++-- 4 files changed, 172 insertions(+), 105 deletions(-) diff --git a/native/shared/src/renderer/lazy_pipeline.rs b/native/shared/src/renderer/lazy_pipeline.rs index 26b1473a..0bbee5e4 100644 --- a/native/shared/src/renderer/lazy_pipeline.rs +++ b/native/shared/src/renderer/lazy_pipeline.rs @@ -397,6 +397,8 @@ impl super::Renderer { wanted.extend([ self.scene_depth_pipeline.warm_handle(), self.scene_pipeline_prepassed.warm_handle(), + self.scene_node_depth_pipeline.warm_handle(), + self.scene_node_pipeline_prepassed.warm_handle(), self.scene_pipeline.warm_handle(), self.scene_compose_pipeline.warm_handle(), self.composite_pipeline.warm_handle(), diff --git a/native/shared/src/renderer/mod.rs b/native/shared/src/renderer/mod.rs index 137f9ab4..5e134fa6 100644 --- a/native/shared/src/renderer/mod.rs +++ b/native/shared/src/renderer/mod.rs @@ -1650,6 +1650,10 @@ pub struct Renderer { scene_depth_visibility_pipeline: Option, /// EN-044 — cached-model pipeline for the prepassed path (no depth write, Equal). pub scene_pipeline_prepassed: LazyRenderPipeline, + /// The retained scene nodes' depth prepass and prepassed pipeline: as + /// above, with scene_pipeline's back-face culling. + scene_node_depth_pipeline: LazyRenderPipeline, + scene_node_pipeline_prepassed: LazyRenderPipeline, pub scene_material_layout: wgpu::BindGroupLayout, scene_sheen_albedo_lut: Option, scene_layered_pbr_resources: Option, @@ -3978,6 +3982,7 @@ impl Renderer { &scene_shader, "scene_depth_prepass_pipeline", false, + None, ); // EN-044 — the cached-model pipeline for the PREPASSED path. @@ -3995,90 +4000,102 @@ impl Renderer { // surface's depth is bit-identical to what the prepass wrote — same vertex // stage, same uniforms, same wind. // - // scene_pipeline (Less, write) stays for the retained scene-graph nodes, - // which are not in the prepass. - let scene_pipeline_prepassed = LazyRenderPipeline::new( - &device, - &wgpu::RenderPipelineDescriptor { - label: Some("scene_pipeline_prepassed"), - layout: Some(&scene_pipeline_layout), - vertex: wgpu::VertexState { - // Original module for the vertex stage — EN-044's prepass/main - // depth bit-exactness must not depend on the fragment strip. - module: &scene_shader, - entry_point: Some("vs_main_scene"), - buffers: &[Vertex3D::desc()], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - // SH-055 — discard-free variant so Adreno keeps LRZ/early-Z - // (see scene_shader_prepassed above). Depth is Equal-tested - // against the prepass, so cutout shape is preserved exactly. - module: &scene_shader_prepassed, - entry_point: Some("fs_main_scene"), - // SH-055 — `lean_mrt` (Android): see pipeline_3d's comment above. - #[cfg(lean_mrt)] - targets: &[ - Some(wgpu::ColorTargetState { - format: HDR_FORMAT, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }), - None, - Some(wgpu::ColorTargetState { - format: VELOCITY_FORMAT, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - }), - None, - ], - #[cfg(not(lean_mrt))] - targets: &[ - Some(wgpu::ColorTargetState { - format: HDR_FORMAT, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }), - Some(wgpu::ColorTargetState { - format: MATERIAL_FORMAT, - // Replace blend so the material slot reflects - // the topmost-fragment material, not blended. - blend: None, - write_mask: wgpu::ColorWrites::ALL, - }), - Some(wgpu::ColorTargetState { - format: VELOCITY_FORMAT, - blend: None, - write_mask: wgpu::ColorWrites::ALL, + // scene_pipeline (Less, write) stays for the retained scene-graph nodes + // the prepass does not draw (SceneNode::depth_prepassed). + // + // The retained scene nodes take the back-face-culled variant, like + // scene_pipeline: their prepass culls the same faces. + let create_scene_pipeline_prepassed = + |label: &'static str, cull_mode: Option| { + LazyRenderPipeline::new( + &device, + &wgpu::RenderPipelineDescriptor { + label: Some(label), + layout: Some(&scene_pipeline_layout), + vertex: wgpu::VertexState { + // Original module for the vertex stage — EN-044's prepass/main + // depth bit-exactness must not depend on the fragment strip. + module: &scene_shader, + entry_point: Some("vs_main_scene"), + buffers: &[Vertex3D::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + // SH-055 — discard-free variant so Adreno keeps LRZ/early-Z + // (see scene_shader_prepassed above). Depth is Equal-tested + // against the prepass, so cutout shape is preserved exactly. + module: &scene_shader_prepassed, + entry_point: Some("fs_main_scene"), + // SH-055 — `lean_mrt` (Android): see pipeline_3d's comment above. + #[cfg(lean_mrt)] + targets: &[ + Some(wgpu::ColorTargetState { + format: HDR_FORMAT, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + }), + None, + Some(wgpu::ColorTargetState { + format: VELOCITY_FORMAT, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + }), + None, + ], + #[cfg(not(lean_mrt))] + targets: &[ + Some(wgpu::ColorTargetState { + format: HDR_FORMAT, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + }), + Some(wgpu::ColorTargetState { + format: MATERIAL_FORMAT, + // Replace blend so the material slot reflects + // the topmost-fragment material, not blended. + blend: None, + write_mask: wgpu::ColorWrites::ALL, + }), + Some(wgpu::ColorTargetState { + format: VELOCITY_FORMAT, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + }), + Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8Unorm, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + }), + ], + compilation_options: Default::default(), }), - Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, - blend: None, - write_mask: wgpu::ColorWrites::ALL, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: DEPTH_FORMAT, + depth_write_enabled: Some(false), + depth_compare: Some(wgpu::CompareFunction::Equal), + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), }), - ], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: DEPTH_FORMAT, - depth_write_enabled: Some(false), - depth_compare: Some(wgpu::CompareFunction::Equal), - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState::default(), - multiview_mask: None, - cache: None, - }, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }, + ) + }; + let scene_pipeline_prepassed = + create_scene_pipeline_prepassed("scene_pipeline_prepassed", None); + let scene_node_pipeline_prepassed = create_scene_pipeline_prepassed( + "scene_node_pipeline_prepassed", + Some(wgpu::Face::Back), ); // Default flat-normal 1×1 texture for meshes that have tangents @@ -8021,8 +8038,21 @@ impl Renderer { &scene_shader, "scene_depth_visibility_prepass_pipeline", true, + None, ) }); + // The retained scene nodes' prepass: scene_pipeline culls their back + // faces, so their depth must not include them either. Its colour slot + // matches the prepass attachments (the packed visibility target when + // routing is on, as for scene_depth_visibility_pipeline). + let scene_node_depth_pipeline = scene_pass::create_scene_depth_pipeline( + &device, + &scene_pipeline_layout, + &scene_shader, + "scene_node_depth_prepass_pipeline", + cfg!(not(target_arch = "wasm32")) && gpu_driven.visibility_routing_enabled(), + Some(wgpu::Face::Back), + ); let vsm_gpu_casters = vsm_gpu_casters::VsmGpuCasters::new(&device, gpu_driven.enabled()); let transient_pool = transient::TransientPool::new(); let frame_plan_cache = graph::PlanCache::new(); @@ -8663,6 +8693,8 @@ impl Renderer { #[cfg(not(target_arch = "wasm32"))] scene_depth_visibility_pipeline, scene_pipeline_prepassed, + scene_node_depth_pipeline, + scene_node_pipeline_prepassed, froxel, scene_material_layout, scene_sheen_albedo_lut: None, diff --git a/native/shared/src/renderer/scene_pass.rs b/native/shared/src/renderer/scene_pass.rs index aa879e0e..516cc8a2 100644 --- a/native/shared/src/renderer/scene_pass.rs +++ b/native/shared/src/renderer/scene_pass.rs @@ -13,6 +13,7 @@ pub(super) fn create_scene_depth_pipeline( shader: &wgpu::ShaderModule, label: &'static str, visibility_target: bool, + cull_mode: Option, ) -> super::lazy_pipeline::LazyRenderPipeline { let targets = [visibility_target.then_some(wgpu::ColorTargetState { format: visibility_buffer::VISIBILITY_FORMAT, @@ -41,7 +42,7 @@ pub(super) fn create_scene_depth_pipeline( topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, - cull_mode: None, + cull_mode, polygon_mode: wgpu::PolygonMode::Fill, unclipped_depth: false, conservative: false, @@ -248,6 +249,7 @@ impl Renderer { // The opaque retained nodes, so the main pass shades each of // their pixels once (see Scene::render_depth_prepass). if !self.dbg_skip("scene_graph") { + pass.set_pipeline(&self.scene_node_depth_pipeline); scene.render_depth_prepass( &mut pass, self.gpu_driven.submitting(), @@ -674,19 +676,14 @@ impl Renderer { // (wasm, or a diagnostic skip) they keep the depth-writing one. if !self.dbg_skip("scene_graph") { // SH-055 diag - let scene_pipeline = if scene_nodes_prepassed { - &self.scene_pipeline_prepassed - } else { - &self.scene_pipeline - }; - pass.set_pipeline(scene_pipeline); + pass.set_pipeline(&self.scene_pipeline); scene.render_with_material_specializations( &mut pass, self.gpu_driven.submitting(), self.imported_refraction_enabled, - scene_pipeline, + &self.scene_pipeline, self.scene_layered_pbr_resources.as_ref(), - scene_nodes_prepassed, + scene_nodes_prepassed.then_some(&*self.scene_node_pipeline_prepassed), ); } } diff --git a/native/shared/src/scene.rs b/native/shared/src/scene.rs index be1abced..be2f1e34 100644 --- a/native/shared/src/scene.rs +++ b/native/shared/src/scene.rs @@ -309,6 +309,14 @@ pub struct LodLevel { } impl SceneNode { + /// Whether the depth prepass draws this opaque node, so that the main + /// pass shades it with the depth-equal pipelines. Not with a fractional + /// opacity: it blends over what lies behind it, and its prepass depth + /// would reject that. (Non-finite opacity draws as 1, as in the uniforms.) + pub(crate) fn depth_prepassed(&self) -> bool { + !self.material.opacity.is_finite() || self.material.opacity >= 1.0 + } + fn new() -> Self { Self { vertices: Vec::new(), @@ -2260,7 +2268,7 @@ impl SceneGraph { None, None, 0, - false, + None, ); } @@ -2271,7 +2279,7 @@ impl SceneGraph { imported_refraction_enabled: bool, base_pipeline: &'a wgpu::RenderPipeline, layered: Option<&'a crate::renderer::layered_pbr_scene::SceneLayeredPbrResources>, - prepassed: bool, + prepassed_pipeline: Option<&'a wgpu::RenderPipeline>, ) { self.render_with_material_pipeline_selection( pass, @@ -2280,7 +2288,7 @@ impl SceneGraph { Some(base_pipeline), layered, 0, - prepassed, + prepassed_pipeline, ); } @@ -2299,7 +2307,7 @@ impl SceneGraph { Some(base_pipeline), layered, render_layer, - false, + None, ); } @@ -2312,7 +2320,15 @@ impl SceneGraph { gpu_driven_enabled: bool, imported_refraction_enabled: bool, render_layer: u32, - ) -> impl Iterator, &'a wgpu::Buffer, &'a wgpu::Buffer, u32)> + 'a { + ) -> impl Iterator< + Item = ( + &'a SceneNode, + Option<&'a LodLevel>, + &'a wgpu::Buffer, + &'a wgpu::Buffer, + u32, + ), + > + 'a { self.nodes.iter().filter_map(move |(_handle, node)| { if !node.visible || node.render_layer != render_layer @@ -2344,8 +2360,16 @@ impl SceneGraph { .get(node.active_lod.max(0) as usize) .filter(|_| node.active_lod >= 0); let (vb, ib, index_count) = match active_lod { - Some(lod) => (lod.gpu_vb.as_ref()?, lod.gpu_ib.as_ref()?, lod.gpu_index_count), - None => (node.gpu_vb.as_ref()?, node.gpu_ib.as_ref()?, node.gpu_index_count), + Some(lod) => ( + lod.gpu_vb.as_ref()?, + lod.gpu_ib.as_ref()?, + lod.gpu_index_count, + ), + None => ( + node.gpu_vb.as_ref()?, + node.gpu_ib.as_ref()?, + node.gpu_index_count, + ), }; node.gpu_uniform_bg.as_ref()?; node.gpu_material_bg.as_ref()?; @@ -2370,6 +2394,9 @@ impl SceneGraph { for (node, _lod, vb, ib, index_count) in self.opaque_node_draws(gpu_driven_enabled, imported_refraction_enabled, 0) { + if !node.depth_prepassed() { + continue; + } let (Some(bg), Some(material)) = (&node.gpu_uniform_bg, &node.gpu_material_bg) else { continue; }; @@ -2390,15 +2417,20 @@ impl SceneGraph { base_pipeline: Option<&'a wgpu::RenderPipeline>, layered: Option<&'a crate::renderer::layered_pbr_scene::SceneLayeredPbrResources>, render_layer: u32, - prepassed: bool, + prepassed_pipeline: Option<&'a wgpu::RenderPipeline>, ) { // The renderer enters with the ordinary scene pipeline already bound. // Public callers that do not opt into specialization retain the old - // contract and incur no per-node pipeline selection. - let mut current_pipeline = base_pipeline.map(|_| (false, false)); - for (node, active_lod, vb, ib, index_count) in - self.opaque_node_draws(gpu_driven_enabled, imported_refraction_enabled, render_layer) - { + // contract and incur no per-node pipeline selection. With + // `prepassed_pipeline`, the nodes the depth prepass drew + // (`SceneNode::depth_prepassed`) take it, or the layered prepassed one; + // the rest keep the depth-writing pipelines. + let mut current_pipeline = base_pipeline.map(|_| (false, false, false)); + for (node, active_lod, vb, ib, index_count) in self.opaque_node_draws( + gpu_driven_enabled, + imported_refraction_enabled, + render_layer, + ) { let (Some(bg), Some(base_material)) = (&node.gpu_uniform_bg, &node.gpu_material_bg) else { continue; @@ -2416,10 +2448,14 @@ impl SceneGraph { continue; } if let Some(base_pipeline) = base_pipeline { - let key = (layered_material.is_some(), uses_uv1); + let prepassed = prepassed_pipeline.is_some() && node.depth_prepassed(); + let key = (layered_material.is_some(), uses_uv1, prepassed); if current_pipeline != Some(key) { if let (Some(layered), Some(_)) = (layered, layered_material) { pass.set_pipeline(layered.opaque_pipeline(uses_uv1, prepassed)); + } else if let (true, Some(prepassed_pipeline)) = (prepassed, prepassed_pipeline) + { + pass.set_pipeline(prepassed_pipeline); } else { pass.set_pipeline(base_pipeline); }