Skip to main content

frust_engine/gpu/
depth.rs

1//! The depth attachment the opaque strip pass establishes and the alpha pass
2//! tests against.
3//!
4//! The engine's early-z scheme needs one [`DEPTH_FORMAT`] attachment matching
5//! the frame's target extent. Two things own such an attachment, and which one
6//! does is the caller's call, not the engine's:
7//!
8//! 1. **The caller.** A host that already ran a 3D pass into the same target
9//!    has a depth buffer with meaningful contents, and the 2D pass has to test
10//!    against it rather than against one of its own. That caller passes it in
11//!    as `EngineTarget::depth`, and — because it has usually just cleared the
12//!    buffer for its own pass — tells the engine so through
13//!    [`DepthAttachment::set_pre_cleared`], which turns the frame's depth clear
14//!    into a load. Clearing a depth buffer a 3D pass just populated would throw
15//!    that pass's occlusion away.
16//! 2. **The engine.** With no caller-supplied attachment the engine allocates
17//!    one lazily on the first frame that needs it and keeps it across frames,
18//!    reallocating only when the extent changes ([`DepthAttachment::resize`]).
19//!
20//! The texture itself is plain `RENDER_ATTACHMENT`, single mip, single sample:
21//! nothing ever samples or copies it, so it needs no other usage, and a
22//! downlevel target could not offer one anyway.
23//!
24//! # What a caller sharing the buffer has to agree with
25//!
26//! Three facts, and together they are the whole contract — a renderer that
27//! honours them records its own passes into the same encoder, against this
28//! same attachment, and gets correct occlusion in either order.
29//!
30//! - **The comparison, and which end is near.** [`DEPTH_COMPARE`] is
31//!   `LessEqual` and [`DEPTH_CLEAR`] is the far plane, so a *nearer* fragment
32//!   carries a *smaller* z. The engine's own draws sit at the back of that
33//!   range — `strip.wgsl` maps the backmost draw to `z = 1.0` and the rest
34//!   towards `1.0 - painter_index / 2^24` — so anything a caller writes in
35//!   front of the far plane occludes the 2D frame wherever it lands.
36//! - **The extent.** A depth attachment must match its colour attachment's
37//!   extent exactly; wgpu refuses the pass otherwise.
38//!   [`depth_texture_descriptor`] is the descriptor the engine itself would
39//!   have used, so a caller allocating the shared buffer through it agrees by
40//!   construction rather than by arithmetic of its own.
41//! - **Who clears.** Whichever pass runs first owns the clear and the other
42//!   loads; the engine's half of that statement is
43//!   [`DepthAttachment::set_pre_cleared`]. The frame's *colour* clear is not
44//!   negotiable the same way — a frame always clears its colour target to the
45//!   base colour — so content painted into that target ahead of the frame
46//!   keeps its depth and loses its pixels, and a host that needs it visible
47//!   records it after the frame rather than before.
48
49pub use super::pipelines::DEPTH_FORMAT;
50
51/// The depth value a frame clears its attachment to.
52///
53/// `strip.wgsl` maps the backmost draw to `z = 1.0` and every draw in front of
54/// it to a smaller z, so the far plane is the only clear value under which the
55/// backmost draw still passes a [`DEPTH_COMPARE`] test.
56pub const DEPTH_CLEAR: f32 = 1.0;
57
58/// The comparison every depth-testing engine pipeline runs.
59///
60/// Named beside [`DEPTH_CLEAR`] because the two together are what a caller
61/// sharing the attachment builds its own pipeline against: the comparison, and
62/// which end of the range is near. `LessEqual` rather than `Less` so the
63/// backmost draw still passes against a buffer cleared to [`DEPTH_CLEAR`], and
64/// so two draws quantized to the same 24-bit depth both land.
65///
66/// The pipelines state it in their own depth state as well; the engine's
67/// integration suite pins the two to agree rather than leaving a second copy
68/// free to drift.
69pub const DEPTH_COMPARE: wgpu::CompareFunction = wgpu::CompareFunction::LessEqual;
70
71/// The usage every depth attachment is created with.
72///
73/// Attachment only: the depth buffer is never sampled, never copied out, and
74/// never read back, so nothing else belongs here.
75pub const DEPTH_USAGE: wgpu::TextureUsages = wgpu::TextureUsages::RENDER_ATTACHMENT;
76
77/// The descriptor for a depth attachment covering a `width` x `height` target.
78///
79/// Both extents are floored at 1: a zero-extent texture cannot be created, and
80/// a frame that asked for one has no pixels to depth-test anyway.
81#[must_use]
82pub fn depth_texture_descriptor(width: u32, height: u32) -> wgpu::TextureDescriptor<'static> {
83    wgpu::TextureDescriptor {
84        label: Some("frust-engine depth texture"),
85        size: wgpu::Extent3d {
86            width: width.max(1),
87            height: height.max(1),
88            depth_or_array_layers: 1,
89        },
90        mip_level_count: 1,
91        sample_count: 1,
92        dimension: wgpu::TextureDimension::D2,
93        format: DEPTH_FORMAT,
94        usage: DEPTH_USAGE,
95        view_formats: &[],
96    }
97}
98
99/// The load operation a frame's first depth-using pass applies — the surface
100/// opening, which clears colour and depth and draws the opaque strips in one
101/// pass, or the bare clear pass when the renderer records them apart (see
102/// `crate::renderer`'s *The frame's passes*). Every later pass loads.
103///
104/// `pre_cleared` is the caller's statement that the attachment it supplied
105/// already holds the depth it wants tested against.
106#[must_use]
107pub fn depth_load_op(pre_cleared: bool) -> wgpu::LoadOp<f32> {
108    if pre_cleared {
109        wgpu::LoadOp::Load
110    } else {
111        wgpu::LoadOp::Clear(DEPTH_CLEAR)
112    }
113}
114
115/// A depth texture the engine allocated and keeps across frames.
116#[derive(Debug)]
117pub struct DepthTexture {
118    texture: wgpu::Texture,
119    view: wgpu::TextureView,
120    width: u32,
121    height: u32,
122}
123
124impl DepthTexture {
125    /// Allocates a depth attachment covering a `width` x `height` target.
126    #[must_use]
127    pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
128        let descriptor = depth_texture_descriptor(width, height);
129        let texture = device.create_texture(&descriptor);
130        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
131        Self {
132            texture,
133            view,
134            width: descriptor.size.width,
135            height: descriptor.size.height,
136        }
137    }
138
139    /// The underlying texture.
140    #[must_use]
141    pub fn texture(&self) -> &wgpu::Texture {
142        &self.texture
143    }
144
145    /// The full-extent view a render pass attaches to.
146    #[must_use]
147    pub fn view(&self) -> &wgpu::TextureView {
148        &self.view
149    }
150
151    /// The `(width, height)` the texture was created at — the *floored*
152    /// extent, so a target of zero width answers 1 here.
153    #[must_use]
154    pub fn size(&self) -> (u32, u32) {
155        (self.width, self.height)
156    }
157
158    /// Whether this texture is the attachment a `width` x `height` target
159    /// needs.
160    ///
161    /// Exact, not "at least": a depth attachment must match its color
162    /// attachment's extent, so a larger leftover from a previous size is no
163    /// more usable than a smaller one.
164    #[must_use]
165    pub fn matches(&self, width: u32, height: u32) -> bool {
166        (self.width, self.height) == (width.max(1), height.max(1))
167    }
168}
169
170/// The engine's depth attachment across frames: an owned texture when the
171/// caller supplies none, plus the caller's pre-cleared statement.
172#[derive(Debug, Default)]
173pub struct DepthAttachment {
174    owned: Option<DepthTexture>,
175    pre_cleared: bool,
176}
177
178impl DepthAttachment {
179    /// An attachment owning nothing yet, clearing its depth every frame.
180    #[must_use]
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// Records whether the attachment a caller supplies already holds the
186    /// depth it wants tested against, in which case the frame loads it instead
187    /// of clearing it.
188    ///
189    /// Sticky across frames: a host compositing 2D over 3D does so every
190    /// frame, so it states this once rather than per frame.
191    ///
192    /// It is a statement about *ordering*, not about the buffer's contents: it
193    /// is `true` when the caller's own pass ran first in the encoder and the
194    /// frame must test against what that pass left, and `false` when the frame
195    /// runs first and the caller's pass loads the depth the frame establishes.
196    pub fn set_pre_cleared(&mut self, pre_cleared: bool) {
197        self.pre_cleared = pre_cleared;
198    }
199
200    /// Whether a caller-supplied attachment is treated as pre-cleared.
201    #[must_use]
202    pub fn is_pre_cleared(&self) -> bool {
203        self.pre_cleared
204    }
205
206    /// The load operation this frame's depth clear resolves to.
207    ///
208    /// Only a caller-supplied attachment can be pre-cleared — an engine-owned
209    /// one holds nothing but the previous frame's depth, which is never what
210    /// this frame wants to test against.
211    #[must_use]
212    pub fn load_op(&self, caller_supplied: bool) -> wgpu::LoadOp<f32> {
213        depth_load_op(caller_supplied && self.pre_cleared)
214    }
215
216    /// The engine-owned attachment, if one has been allocated.
217    #[must_use]
218    pub fn owned(&self) -> Option<&DepthTexture> {
219        self.owned.as_ref()
220    }
221
222    /// The engine-owned attachment's view, if one has been allocated.
223    #[must_use]
224    pub fn owned_view(&self) -> Option<&wgpu::TextureView> {
225        self.owned.as_ref().map(DepthTexture::view)
226    }
227
228    /// Allocates the engine-owned attachment if there is none at this extent,
229    /// and returns its view.
230    pub fn ensure(&mut self, device: &wgpu::Device, width: u32, height: u32) -> &wgpu::TextureView {
231        if self
232            .owned
233            .as_ref()
234            .is_some_and(|depth| !depth.matches(width, height))
235        {
236            self.owned = None;
237        }
238        self.owned
239            .get_or_insert_with(|| DepthTexture::new(device, width, height))
240            .view()
241    }
242
243    /// Drops the engine-owned attachment, keeping the pre-cleared statement.
244    pub fn discard(&mut self) {
245        self.owned = None;
246    }
247
248    /// Re-establishes the engine-owned attachment at a new extent.
249    ///
250    /// Only reallocates when one was already owned: a renderer that has never
251    /// needed a depth buffer should not start holding one because the surface
252    /// changed size. Doing the work here rather than on the next frame keeps
253    /// the reallocation off the frame path, where a resize storm would
254    /// otherwise pay for it mid-encode.
255    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
256        if self.owned.is_some() {
257            self.owned = Some(DepthTexture::new(device, width, height));
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn the_descriptor_is_a_single_sample_attachment_only_depth_target() {
268        let descriptor = depth_texture_descriptor(800, 600);
269        assert_eq!(descriptor.format, DEPTH_FORMAT);
270        assert_eq!(descriptor.usage, wgpu::TextureUsages::RENDER_ATTACHMENT);
271        assert_eq!(descriptor.sample_count, 1);
272        assert_eq!(descriptor.mip_level_count, 1);
273        assert_eq!(descriptor.size.width, 800);
274        assert_eq!(descriptor.size.height, 600);
275        assert_eq!(descriptor.size.depth_or_array_layers, 1);
276    }
277
278    #[test]
279    fn a_zero_extent_is_floored_to_one_rather_than_refused() {
280        let descriptor = depth_texture_descriptor(0, 0);
281        assert_eq!((descriptor.size.width, descriptor.size.height), (1, 1));
282    }
283
284    #[test]
285    fn a_pre_cleared_attachment_loads_instead_of_clearing() {
286        assert_eq!(depth_load_op(false), wgpu::LoadOp::Clear(DEPTH_CLEAR));
287        assert_eq!(depth_load_op(true), wgpu::LoadOp::Load);
288    }
289
290    #[test]
291    fn only_a_caller_supplied_attachment_can_be_pre_cleared() {
292        let mut depth = DepthAttachment::new();
293        assert!(!depth.is_pre_cleared());
294        assert_eq!(depth.load_op(true), wgpu::LoadOp::Clear(DEPTH_CLEAR));
295
296        depth.set_pre_cleared(true);
297        assert!(depth.is_pre_cleared());
298        assert_eq!(depth.load_op(true), wgpu::LoadOp::Load);
299        // An engine-owned buffer holds only the previous frame's depth, so the
300        // statement does not carry over to it.
301        assert_eq!(depth.load_op(false), wgpu::LoadOp::Clear(DEPTH_CLEAR));
302    }
303
304    #[test]
305    fn an_attachment_owns_nothing_until_a_frame_asks_for_one() {
306        let depth = DepthAttachment::new();
307        assert!(depth.owned().is_none());
308        assert!(depth.owned_view().is_none());
309    }
310}