Skip to main content

repose_render_wgpu/
depth_composite.rs

1//! Depth-capable scene composite for [`WgpuCallback`](super::WgpuCallback)s.
2//!
3//! The shared UI pass this crate executes carries stencil but no depth ops
4//! (`depth_ops: None`), so pipelines that write depth are rejected inside
5//! it by validation. This module is the supported path for depth-tested
6//! content (3D viewports): render the scene into a caller-owned offscreen
7//! target (with its own depth texture) during `prepare`, then draw the
8//! target back as a fullscreen triangle in `paint`.
9//!
10//! One [`DepthComposite`] per viewport id (same ownership split as the
11//! sprite batch in `repame-sprite`): each id owns its target + pipelines,
12//! so overlapping viewports never share depth. The blit honors the UI
13//! stencil contract (`LessEqual`) and touches no depth, so clips keep
14//! working while depth stays viewport-local.
15
16use std::collections::HashMap;
17
18use super::{CallbackResources, ScreenDescriptor};
19
20/// Fullscreen textured triangle: samples the offscreen scene 1:1.
21const BLIT_WGSL: &str = r#"
22@group(0) @binding(0) var scene_tex: texture_2d<f32>;
23@group(0) @binding(1) var scene_smp: sampler;
24struct VsOut {
25    @builtin(position) pos: vec4<f32>,
26    @location(0) uv: vec2<f32>,
27};
28@vertex
29fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
30    let x = f32(i / 2u) * 4.0 - 1.0;
31    let y = f32(i % 2u) * 4.0 - 1.0;
32    var out: VsOut;
33    out.pos = vec4<f32>(x, y, 0.0, 1.0);
34    out.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
35    return out;
36}
37@fragment
38fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
39    return textureSample(scene_tex, scene_smp, in.uv);
40}
41"#;
42
43/// Viewport-owned offscreen scene target + depth buffer + blit pipeline.
44///
45/// Created through [`DepthComposite::ensure`] (per viewport id), drawn into
46/// with [`DepthComposite::begin_scene`], composited back with
47/// [`DepthComposite::blit`]. Textures recreate on format/sample/size
48/// change; buffer contents are per-frame and never retained.
49#[derive(Default)]
50pub struct DepthComposite {
51    targets: HashMap<String, Target>,
52}
53
54struct Target {
55    key: (wgpu::TextureFormat, u32, u32, u32),
56    #[allow(dead_code)]
57    scene: wgpu::Texture,
58    scene_view: wgpu::TextureView,
59    #[allow(dead_code)]
60    depth: wgpu::Texture,
61    depth_view: wgpu::TextureView,
62    depth_format: wgpu::TextureFormat,
63    blit_pipeline: wgpu::RenderPipeline,
64    blit_bind: wgpu::BindGroup,
65}
66
67impl DepthComposite {
68    /// Fetch the composite store from callback resources (creating it on
69    /// first use). One store per render pass; ids disambiguate viewports.
70    pub fn get(resources: &mut CallbackResources) -> &mut Self {
71        resources.get_or_insert_with::<Self>()
72    }
73
74    /// Ensure the offscreen target for `id` at `w`x`h` (recreates on
75    /// format/sample/size change, like every other viewport-owned target).
76    /// Dimensions clamp to >= 1.
77    #[allow(clippy::too_many_arguments)] // (device, screen, id, w, h) — mirrors ensure_resources conventions
78    pub fn ensure(
79        &mut self,
80        device: &wgpu::Device,
81        screen: &ScreenDescriptor,
82        id: &str,
83        w: u32,
84        h: u32,
85    ) {
86        let w = w.max(1);
87        let h = h.max(1);
88        let key = (screen.target_format, screen.sample_count, w, h);
89        if self.targets.get(id).is_some_and(|t| t.key == key) {
90            return;
91        }
92        let depth_format = wgpu::TextureFormat::Depth24PlusStencil8;
93        let scene = device.create_texture(&wgpu::TextureDescriptor {
94            label: Some("depth_composite_scene"),
95            size: wgpu::Extent3d {
96                width: w,
97                height: h,
98                depth_or_array_layers: 1,
99            },
100            mip_level_count: 1,
101            sample_count: 1,
102            dimension: wgpu::TextureDimension::D2,
103            format: screen.target_format,
104            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
105            view_formats: &[],
106        });
107        let scene_view = scene.create_view(&wgpu::TextureViewDescriptor::default());
108        let depth = device.create_texture(&wgpu::TextureDescriptor {
109            label: Some("depth_composite_depth"),
110            size: wgpu::Extent3d {
111                width: w,
112                height: h,
113                depth_or_array_layers: 1,
114            },
115            mip_level_count: 1,
116            sample_count: 1,
117            dimension: wgpu::TextureDimension::D2,
118            format: depth_format,
119            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
120            view_formats: &[],
121        });
122        let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default());
123        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
124            label: Some("depth_composite_blit"),
125            source: wgpu::ShaderSource::Wgsl(BLIT_WGSL.into()),
126        });
127        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
128            label: Some("depth_composite_blit_bgl"),
129            entries: &[
130                wgpu::BindGroupLayoutEntry {
131                    binding: 0,
132                    visibility: wgpu::ShaderStages::FRAGMENT,
133                    ty: wgpu::BindingType::Texture {
134                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
135                        view_dimension: wgpu::TextureViewDimension::D2,
136                        multisampled: false,
137                    },
138                    count: None,
139                },
140                wgpu::BindGroupLayoutEntry {
141                    binding: 1,
142                    visibility: wgpu::ShaderStages::FRAGMENT,
143                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
144                    count: None,
145                },
146            ],
147        });
148        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
149            label: Some("depth_composite_blit_sampler"),
150            address_mode_u: wgpu::AddressMode::ClampToEdge,
151            address_mode_v: wgpu::AddressMode::ClampToEdge,
152            address_mode_w: wgpu::AddressMode::ClampToEdge,
153            mag_filter: wgpu::FilterMode::Linear,
154            min_filter: wgpu::FilterMode::Linear,
155            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
156            lod_min_clamp: 0.0,
157            lod_max_clamp: 1.0,
158            compare: None,
159            anisotropy_clamp: 1,
160            border_color: None,
161        });
162        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
163            label: Some("depth_composite_blit_bg"),
164            layout: &layout,
165            entries: &[
166                wgpu::BindGroupEntry {
167                    binding: 0,
168                    resource: wgpu::BindingResource::TextureView(&scene_view),
169                },
170                wgpu::BindGroupEntry {
171                    binding: 1,
172                    resource: wgpu::BindingResource::Sampler(&sampler),
173                },
174            ],
175        });
176        let pipe_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
177            label: Some("depth_composite_blit_pl"),
178            bind_group_layouts: &[Some(&layout)],
179            immediate_size: 0,
180        });
181        let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
182            label: Some("depth_composite_blit"),
183            layout: Some(&pipe_layout),
184            vertex: wgpu::VertexState {
185                module: &shader,
186                entry_point: Some("vs_main"),
187                buffers: &[],
188                compilation_options: Default::default(),
189            },
190            fragment: Some(wgpu::FragmentState {
191                module: &shader,
192                entry_point: Some("fs_main"),
193                targets: &[Some(wgpu::ColorTargetState {
194                    format: screen.target_format,
195                    blend: Some(wgpu::BlendState::REPLACE),
196                    write_mask: wgpu::ColorWrites::ALL,
197                })],
198                compilation_options: Default::default(),
199            }),
200            primitive: wgpu::PrimitiveState {
201                topology: wgpu::PrimitiveTopology::TriangleList,
202                ..Default::default()
203            },
204            // depth ops disabled, so the blit never disturbs UI depth/stencil.
205            depth_stencil: Some(wgpu::DepthStencilState {
206                format: depth_format,
207                depth_write_enabled: Some(false),
208                depth_compare: Some(wgpu::CompareFunction::Always),
209                stencil: wgpu::StencilState {
210                    front: wgpu::StencilFaceState {
211                        compare: wgpu::CompareFunction::LessEqual,
212                        ..Default::default()
213                    },
214                    back: wgpu::StencilFaceState {
215                        compare: wgpu::CompareFunction::LessEqual,
216                        ..Default::default()
217                    },
218                    ..Default::default()
219                },
220                bias: wgpu::DepthBiasState::default(),
221            }),
222            multisample: wgpu::MultisampleState {
223                count: screen.sample_count,
224                mask: !0,
225                alpha_to_coverage_enabled: false,
226            },
227            multiview_mask: None,
228            cache: None,
229        });
230        self.targets.insert(
231            id.to_string(),
232            Target {
233                key,
234                scene,
235                scene_view,
236                depth,
237                depth_view,
238                depth_format,
239                blit_pipeline,
240                blit_bind: bind,
241            },
242        );
243    }
244
245    /// Begin the offscreen scene pass for `id` (clearing color to `clear`
246    /// and depth to 1.0). The caller issues its depth-tested draws inside
247    /// the returned pass, then ends it; [`blit`](Self::blit) composites in
248    /// `paint`. Returns `false` when `id` has no target (call [`ensure`](Self::ensure) first).
249    pub fn begin_scene<'a>(
250        &'a self,
251        id: &str,
252        encoder: &'a mut wgpu::CommandEncoder,
253        clear: [f32; 4],
254    ) -> Option<wgpu::RenderPass<'a>> {
255        let t = self.targets.get(id)?;
256        let (w, h) = (t.key.2 as f32, t.key.3 as f32);
257        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
258            label: Some("depth_composite_scene"),
259            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
260                view: &t.scene_view,
261                depth_slice: None,
262                resolve_target: None,
263                ops: wgpu::Operations {
264                    load: wgpu::LoadOp::Clear(wgpu::Color {
265                        r: clear[0] as f64,
266                        g: clear[1] as f64,
267                        b: clear[2] as f64,
268                        a: clear[3] as f64,
269                    }),
270                    store: wgpu::StoreOp::Store,
271                },
272            })],
273            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
274                view: &t.depth_view,
275                depth_ops: Some(wgpu::Operations {
276                    load: wgpu::LoadOp::Clear(1.0),
277                    store: wgpu::StoreOp::Store,
278                }),
279                stencil_ops: Some(wgpu::Operations {
280                    load: wgpu::LoadOp::Clear(0),
281                    store: wgpu::StoreOp::Store,
282                }),
283            }),
284            timestamp_writes: None,
285            occlusion_query_set: None,
286            multiview_mask: None,
287        });
288        pass.set_viewport(0.0, 0.0, w, h, 0.0, 1.0);
289        Some(pass)
290    }
291
292    /// Depth format backing the scene target (for caller pipelines).
293    pub fn depth_format(&self, id: &str) -> Option<wgpu::TextureFormat> {
294        self.targets.get(id).map(|t| t.depth_format)
295    }
296
297    /// Composite the offscreen scene for `id` into the main pass. The
298    /// renderer has already set the viewport to the callback rect, which
299    /// matches the offscreen texture 1:1 (both come from the painted frame
300    /// geometry). No-op when `id` has no target.
301    pub fn blit(&self, id: &str, rpass: &mut wgpu::RenderPass<'_>) {
302        let Some(t) = self.targets.get(id) else {
303            return;
304        };
305        rpass.set_pipeline(&t.blit_pipeline);
306        rpass.set_bind_group(0, &t.blit_bind, &[]);
307        rpass.draw(0..3, 0..1);
308    }
309}