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::{CallbackRenderPass, CallbackResources, ScreenDescriptor};
19
20const MAX_DEPTH_RESOURCE_BYTES: u64 = 256 * 1024 * 1024;
21
22pub trait BlitRenderPass {
23    fn set_blit_pipeline(&mut self, pipeline: &wgpu::RenderPipeline);
24    fn set_blit_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]);
25    fn draw_blit_triangle(&mut self);
26}
27
28impl BlitRenderPass for wgpu::RenderPass<'_> {
29    fn set_blit_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
30        self.set_pipeline(pipeline);
31    }
32
33    fn set_blit_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
34        self.set_bind_group(index, bind_group, offsets);
35    }
36
37    fn draw_blit_triangle(&mut self) {
38        self.draw(0..3, 0..1);
39    }
40}
41
42impl BlitRenderPass for CallbackRenderPass<'_, '_> {
43    fn set_blit_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
44        self.set_pipeline(pipeline);
45    }
46
47    fn set_blit_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
48        self.set_bind_group(index, bind_group, offsets);
49    }
50
51    fn draw_blit_triangle(&mut self) {
52        self.draw(0..3, 0..1);
53    }
54}
55
56/// Fullscreen textured triangle: samples the offscreen scene 1:1.
57const BLIT_WGSL: &str = r#"
58@group(0) @binding(0) var scene_tex: texture_2d<f32>;
59@group(0) @binding(1) var scene_smp: sampler;
60struct VsOut {
61    @builtin(position) pos: vec4<f32>,
62    @location(0) uv: vec2<f32>,
63};
64@vertex
65fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
66    let x = f32(i / 2u) * 4.0 - 1.0;
67    let y = f32(i % 2u) * 4.0 - 1.0;
68    var out: VsOut;
69    out.pos = vec4<f32>(x, y, 0.0, 1.0);
70    out.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
71    return out;
72}
73@fragment
74fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
75    return textureSample(scene_tex, scene_smp, in.uv);
76}
77"#;
78
79/// Viewport-owned offscreen scene target + depth buffer + blit pipeline.
80///
81/// Created through [`DepthComposite::ensure`] (per viewport id), drawn into
82/// with [`DepthComposite::begin_scene`], composited back with
83/// [`DepthComposite::blit`]. Textures recreate on format/sample/size
84/// change; buffer contents are per-frame and never retained.
85#[derive(Default)]
86pub struct DepthComposite {
87    targets: HashMap<String, Target>,
88    next_tick: u64,
89    frame_index: u64,
90    bytes_total: u64,
91}
92
93struct Target {
94    key: (wgpu::TextureFormat, u32, u32, u32),
95    #[allow(dead_code)]
96    scene: wgpu::Texture,
97    scene_view: wgpu::TextureView,
98    #[allow(dead_code)]
99    depth: wgpu::Texture,
100    depth_view: wgpu::TextureView,
101    depth_format: wgpu::TextureFormat,
102    blit_pipeline: wgpu::RenderPipeline,
103    blit_bind: wgpu::BindGroup,
104    last_used_tick: u64,
105    last_used_frame: u64,
106    bytes: u64,
107}
108
109impl DepthComposite {
110    /// Fetch the composite store from callback resources (creating it on
111    /// first use). One store per render pass; ids disambiguate viewports.
112    pub fn get(resources: &mut CallbackResources) -> &mut Self {
113        resources.get_or_insert_with::<Self>()
114    }
115
116    pub fn begin_frame(&mut self) {
117        self.frame_index = self.frame_index.wrapping_add(1);
118    }
119
120    pub fn end_frame(&mut self) {
121        if self.frame_index == 0 {
122            return;
123        }
124        let frame_index = self.frame_index;
125        let mut removed = 0u64;
126        self.targets.retain(|_, target| {
127            if target.last_used_frame == frame_index {
128                true
129            } else {
130                removed = removed.saturating_add(target.bytes);
131                false
132            }
133        });
134        self.bytes_total = self.bytes_total.saturating_sub(removed);
135    }
136
137    /// Ensure the offscreen target for `id` at `w`x`h` (recreates on
138    /// format/sample/size change, like every other viewport-owned target).
139    /// Dimensions clamp to >= 1.
140    #[allow(clippy::too_many_arguments)] // (device, screen, id, w, h) — mirrors ensure_resources conventions
141    pub fn ensure(
142        &mut self,
143        device: &wgpu::Device,
144        screen: &ScreenDescriptor,
145        id: &str,
146        w: u32,
147        h: u32,
148    ) {
149        let w = w.max(1);
150        let h = h.max(1);
151        let key = (screen.target_format, screen.sample_count, w, h);
152        self.next_tick = self.next_tick.wrapping_add(1);
153        let tick = self.next_tick;
154        if let Some(target) = self.targets.get_mut(id)
155            && target.key == key
156        {
157            target.last_used_tick = tick;
158            target.last_used_frame = self.frame_index;
159            return;
160        }
161        let depth_format = wgpu::TextureFormat::Depth24PlusStencil8;
162        let scene_bytes = screen
163            .target_format
164            .theoretical_memory_footprint(wgpu::Extent3d {
165                width: w,
166                height: h,
167                depth_or_array_layers: 1,
168            });
169        let depth_bytes = depth_format.theoretical_memory_footprint(wgpu::Extent3d {
170            width: w,
171            height: h,
172            depth_or_array_layers: 1,
173        });
174        let bytes = scene_bytes.saturating_add(depth_bytes);
175        if w > device.limits().max_texture_dimension_2d
176            || h > device.limits().max_texture_dimension_2d
177            || bytes > MAX_DEPTH_RESOURCE_BYTES
178        {
179            return;
180        }
181        if let Some(old) = self.targets.remove(id) {
182            self.bytes_total = self.bytes_total.saturating_sub(old.bytes);
183        }
184        while self.bytes_total.saturating_add(bytes) > MAX_DEPTH_RESOURCE_BYTES {
185            let Some(oldest) = self
186                .targets
187                .iter()
188                .min_by_key(|(_, target)| target.last_used_tick)
189                .map(|(id, _)| id.clone())
190            else {
191                return;
192            };
193            if let Some(old) = self.targets.remove(&oldest) {
194                self.bytes_total = self.bytes_total.saturating_sub(old.bytes);
195            }
196        }
197        let scene = device.create_texture(&wgpu::TextureDescriptor {
198            label: Some("depth_composite_scene"),
199            size: wgpu::Extent3d {
200                width: w,
201                height: h,
202                depth_or_array_layers: 1,
203            },
204            mip_level_count: 1,
205            sample_count: 1,
206            dimension: wgpu::TextureDimension::D2,
207            format: screen.target_format,
208            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
209            view_formats: &[],
210        });
211        let scene_view = scene.create_view(&wgpu::TextureViewDescriptor::default());
212        let depth = device.create_texture(&wgpu::TextureDescriptor {
213            label: Some("depth_composite_depth"),
214            size: wgpu::Extent3d {
215                width: w,
216                height: h,
217                depth_or_array_layers: 1,
218            },
219            mip_level_count: 1,
220            sample_count: 1,
221            dimension: wgpu::TextureDimension::D2,
222            format: depth_format,
223            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
224            view_formats: &[],
225        });
226        let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default());
227        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
228            label: Some("depth_composite_blit"),
229            source: wgpu::ShaderSource::Wgsl(BLIT_WGSL.into()),
230        });
231        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
232            label: Some("depth_composite_blit_bgl"),
233            entries: &[
234                wgpu::BindGroupLayoutEntry {
235                    binding: 0,
236                    visibility: wgpu::ShaderStages::FRAGMENT,
237                    ty: wgpu::BindingType::Texture {
238                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
239                        view_dimension: wgpu::TextureViewDimension::D2,
240                        multisampled: false,
241                    },
242                    count: None,
243                },
244                wgpu::BindGroupLayoutEntry {
245                    binding: 1,
246                    visibility: wgpu::ShaderStages::FRAGMENT,
247                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
248                    count: None,
249                },
250            ],
251        });
252        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
253            label: Some("depth_composite_blit_sampler"),
254            address_mode_u: wgpu::AddressMode::ClampToEdge,
255            address_mode_v: wgpu::AddressMode::ClampToEdge,
256            address_mode_w: wgpu::AddressMode::ClampToEdge,
257            mag_filter: wgpu::FilterMode::Linear,
258            min_filter: wgpu::FilterMode::Linear,
259            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
260            lod_min_clamp: 0.0,
261            lod_max_clamp: 1.0,
262            compare: None,
263            anisotropy_clamp: 1,
264            border_color: None,
265        });
266        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
267            label: Some("depth_composite_blit_bg"),
268            layout: &layout,
269            entries: &[
270                wgpu::BindGroupEntry {
271                    binding: 0,
272                    resource: wgpu::BindingResource::TextureView(&scene_view),
273                },
274                wgpu::BindGroupEntry {
275                    binding: 1,
276                    resource: wgpu::BindingResource::Sampler(&sampler),
277                },
278            ],
279        });
280        let pipe_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
281            label: Some("depth_composite_blit_pl"),
282            bind_group_layouts: &[Some(&layout)],
283            immediate_size: 0,
284        });
285        let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
286            label: Some("depth_composite_blit"),
287            layout: Some(&pipe_layout),
288            vertex: wgpu::VertexState {
289                module: &shader,
290                entry_point: Some("vs_main"),
291                buffers: &[],
292                compilation_options: Default::default(),
293            },
294            fragment: Some(wgpu::FragmentState {
295                module: &shader,
296                entry_point: Some("fs_main"),
297                targets: &[Some(wgpu::ColorTargetState {
298                    format: screen.target_format,
299                    blend: Some(wgpu::BlendState::REPLACE),
300                    write_mask: wgpu::ColorWrites::ALL,
301                })],
302                compilation_options: Default::default(),
303            }),
304            primitive: wgpu::PrimitiveState {
305                topology: wgpu::PrimitiveTopology::TriangleList,
306                ..Default::default()
307            },
308            // depth ops disabled, so the blit never disturbs UI depth/stencil.
309            depth_stencil: Some(wgpu::DepthStencilState {
310                format: depth_format,
311                depth_write_enabled: Some(false),
312                depth_compare: Some(wgpu::CompareFunction::Always),
313                stencil: wgpu::StencilState {
314                    front: wgpu::StencilFaceState {
315                        compare: wgpu::CompareFunction::LessEqual,
316                        ..Default::default()
317                    },
318                    back: wgpu::StencilFaceState {
319                        compare: wgpu::CompareFunction::LessEqual,
320                        ..Default::default()
321                    },
322                    read_mask: 0xFF,
323                    write_mask: 0,
324                },
325                bias: wgpu::DepthBiasState::default(),
326            }),
327            multisample: wgpu::MultisampleState {
328                count: screen.sample_count,
329                mask: !0,
330                alpha_to_coverage_enabled: false,
331            },
332            multiview_mask: None,
333            cache: None,
334        });
335        self.targets.insert(
336            id.to_string(),
337            Target {
338                key,
339                scene,
340                scene_view,
341                depth,
342                depth_view,
343                depth_format,
344                blit_pipeline,
345                blit_bind: bind,
346                last_used_tick: tick,
347                last_used_frame: self.frame_index,
348                bytes,
349            },
350        );
351        self.bytes_total = self.bytes_total.saturating_add(bytes);
352        if self.targets.len() > 64 {
353            let oldest = self
354                .targets
355                .iter()
356                .min_by_key(|(_, target)| target.last_used_tick)
357                .map(|(id, _)| id.clone());
358            if let Some(id) = oldest
359                && let Some(old) = self.targets.remove(&id)
360            {
361                self.bytes_total = self.bytes_total.saturating_sub(old.bytes);
362            }
363        }
364    }
365
366    /// Begin the offscreen scene pass for `id` (clearing color to `clear`
367    /// and depth to 1.0). The caller issues its depth-tested draws inside
368    /// the returned pass, then ends it; [`blit`](Self::blit) composites in
369    /// `paint`. Returns `false` when `id` has no target (call [`ensure`](Self::ensure) first).
370    pub fn begin_scene<'a>(
371        &'a self,
372        id: &str,
373        encoder: &'a mut wgpu::CommandEncoder,
374        clear: [f32; 4],
375    ) -> Option<wgpu::RenderPass<'a>> {
376        let t = self.targets.get(id)?;
377        let (w, h) = (t.key.2 as f32, t.key.3 as f32);
378        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
379            label: Some("depth_composite_scene"),
380            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
381                view: &t.scene_view,
382                depth_slice: None,
383                resolve_target: None,
384                ops: wgpu::Operations {
385                    load: wgpu::LoadOp::Clear(wgpu::Color {
386                        r: clear[0] as f64,
387                        g: clear[1] as f64,
388                        b: clear[2] as f64,
389                        a: clear[3] as f64,
390                    }),
391                    store: wgpu::StoreOp::Store,
392                },
393            })],
394            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
395                view: &t.depth_view,
396                depth_ops: Some(wgpu::Operations {
397                    load: wgpu::LoadOp::Clear(1.0),
398                    store: wgpu::StoreOp::Store,
399                }),
400                stencil_ops: Some(wgpu::Operations {
401                    load: wgpu::LoadOp::Clear(0),
402                    store: wgpu::StoreOp::Store,
403                }),
404            }),
405            timestamp_writes: None,
406            occlusion_query_set: None,
407            multiview_mask: None,
408        });
409        pass.set_viewport(0.0, 0.0, w, h, 0.0, 1.0);
410        Some(pass)
411    }
412
413    /// Depth format backing the scene target (for caller pipelines).
414    pub fn depth_format(&self, id: &str) -> Option<wgpu::TextureFormat> {
415        self.targets.get(id).map(|t| t.depth_format)
416    }
417
418    /// Composite the offscreen scene for `id` into the main pass. The
419    /// renderer has already set the viewport to the callback rect, which
420    /// matches the offscreen texture 1:1 (both come from the painted frame
421    /// geometry). No-op when `id` has no target.
422    pub fn blit<P: BlitRenderPass>(&self, id: &str, rpass: &mut P) {
423        let Some(t) = self.targets.get(id) else {
424            return;
425        };
426        rpass.set_blit_pipeline(&t.blit_pipeline);
427        rpass.set_blit_bind_group(0, &t.blit_bind, &[]);
428        rpass.draw_blit_triangle();
429    }
430
431    pub fn blit_callback(&self, id: &str, rpass: &mut CallbackRenderPass<'_, '_>) {
432        self.blit(id, rpass);
433    }
434}