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.
100///
101/// `pre_cleared` is the caller's statement that the attachment it supplied
102/// already holds the depth it wants tested against.
103#[must_use]
104pub fn depth_load_op(pre_cleared: bool) -> wgpu::LoadOp<f32> {
105    if pre_cleared {
106        wgpu::LoadOp::Load
107    } else {
108        wgpu::LoadOp::Clear(DEPTH_CLEAR)
109    }
110}
111
112/// A depth texture the engine allocated and keeps across frames.
113#[derive(Debug)]
114pub struct DepthTexture {
115    texture: wgpu::Texture,
116    view: wgpu::TextureView,
117    width: u32,
118    height: u32,
119}
120
121impl DepthTexture {
122    /// Allocates a depth attachment covering a `width` x `height` target.
123    #[must_use]
124    pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
125        let descriptor = depth_texture_descriptor(width, height);
126        let texture = device.create_texture(&descriptor);
127        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
128        Self {
129            texture,
130            view,
131            width: descriptor.size.width,
132            height: descriptor.size.height,
133        }
134    }
135
136    /// The underlying texture.
137    #[must_use]
138    pub fn texture(&self) -> &wgpu::Texture {
139        &self.texture
140    }
141
142    /// The full-extent view a render pass attaches to.
143    #[must_use]
144    pub fn view(&self) -> &wgpu::TextureView {
145        &self.view
146    }
147
148    /// The `(width, height)` the texture was created at — the *floored*
149    /// extent, so a target of zero width answers 1 here.
150    #[must_use]
151    pub fn size(&self) -> (u32, u32) {
152        (self.width, self.height)
153    }
154
155    /// Whether this texture is the attachment a `width` x `height` target
156    /// needs.
157    ///
158    /// Exact, not "at least": a depth attachment must match its color
159    /// attachment's extent, so a larger leftover from a previous size is no
160    /// more usable than a smaller one.
161    #[must_use]
162    pub fn matches(&self, width: u32, height: u32) -> bool {
163        (self.width, self.height) == (width.max(1), height.max(1))
164    }
165}
166
167/// The engine's depth attachment across frames: an owned texture when the
168/// caller supplies none, plus the caller's pre-cleared statement.
169#[derive(Debug, Default)]
170pub struct DepthAttachment {
171    owned: Option<DepthTexture>,
172    pre_cleared: bool,
173}
174
175impl DepthAttachment {
176    /// An attachment owning nothing yet, clearing its depth every frame.
177    #[must_use]
178    pub fn new() -> Self {
179        Self::default()
180    }
181
182    /// Records whether the attachment a caller supplies already holds the
183    /// depth it wants tested against, in which case the frame loads it instead
184    /// of clearing it.
185    ///
186    /// Sticky across frames: a host compositing 2D over 3D does so every
187    /// frame, so it states this once rather than per frame.
188    ///
189    /// It is a statement about *ordering*, not about the buffer's contents: it
190    /// is `true` when the caller's own pass ran first in the encoder and the
191    /// frame must test against what that pass left, and `false` when the frame
192    /// runs first and the caller's pass loads the depth the frame establishes.
193    pub fn set_pre_cleared(&mut self, pre_cleared: bool) {
194        self.pre_cleared = pre_cleared;
195    }
196
197    /// Whether a caller-supplied attachment is treated as pre-cleared.
198    #[must_use]
199    pub fn is_pre_cleared(&self) -> bool {
200        self.pre_cleared
201    }
202
203    /// The load operation this frame's depth clear resolves to.
204    ///
205    /// Only a caller-supplied attachment can be pre-cleared — an engine-owned
206    /// one holds nothing but the previous frame's depth, which is never what
207    /// this frame wants to test against.
208    #[must_use]
209    pub fn load_op(&self, caller_supplied: bool) -> wgpu::LoadOp<f32> {
210        depth_load_op(caller_supplied && self.pre_cleared)
211    }
212
213    /// The engine-owned attachment, if one has been allocated.
214    #[must_use]
215    pub fn owned(&self) -> Option<&DepthTexture> {
216        self.owned.as_ref()
217    }
218
219    /// The engine-owned attachment's view, if one has been allocated.
220    #[must_use]
221    pub fn owned_view(&self) -> Option<&wgpu::TextureView> {
222        self.owned.as_ref().map(DepthTexture::view)
223    }
224
225    /// Allocates the engine-owned attachment if there is none at this extent,
226    /// and returns its view.
227    pub fn ensure(&mut self, device: &wgpu::Device, width: u32, height: u32) -> &wgpu::TextureView {
228        if self
229            .owned
230            .as_ref()
231            .is_some_and(|depth| !depth.matches(width, height))
232        {
233            self.owned = None;
234        }
235        self.owned
236            .get_or_insert_with(|| DepthTexture::new(device, width, height))
237            .view()
238    }
239
240    /// Drops the engine-owned attachment, keeping the pre-cleared statement.
241    pub fn discard(&mut self) {
242        self.owned = None;
243    }
244
245    /// Re-establishes the engine-owned attachment at a new extent.
246    ///
247    /// Only reallocates when one was already owned: a renderer that has never
248    /// needed a depth buffer should not start holding one because the surface
249    /// changed size. Doing the work here rather than on the next frame keeps
250    /// the reallocation off the frame path, where a resize storm would
251    /// otherwise pay for it mid-encode.
252    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
253        if self.owned.is_some() {
254            self.owned = Some(DepthTexture::new(device, width, height));
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn the_descriptor_is_a_single_sample_attachment_only_depth_target() {
265        let descriptor = depth_texture_descriptor(800, 600);
266        assert_eq!(descriptor.format, DEPTH_FORMAT);
267        assert_eq!(descriptor.usage, wgpu::TextureUsages::RENDER_ATTACHMENT);
268        assert_eq!(descriptor.sample_count, 1);
269        assert_eq!(descriptor.mip_level_count, 1);
270        assert_eq!(descriptor.size.width, 800);
271        assert_eq!(descriptor.size.height, 600);
272        assert_eq!(descriptor.size.depth_or_array_layers, 1);
273    }
274
275    #[test]
276    fn a_zero_extent_is_floored_to_one_rather_than_refused() {
277        let descriptor = depth_texture_descriptor(0, 0);
278        assert_eq!((descriptor.size.width, descriptor.size.height), (1, 1));
279    }
280
281    #[test]
282    fn a_pre_cleared_attachment_loads_instead_of_clearing() {
283        assert_eq!(depth_load_op(false), wgpu::LoadOp::Clear(DEPTH_CLEAR));
284        assert_eq!(depth_load_op(true), wgpu::LoadOp::Load);
285    }
286
287    #[test]
288    fn only_a_caller_supplied_attachment_can_be_pre_cleared() {
289        let mut depth = DepthAttachment::new();
290        assert!(!depth.is_pre_cleared());
291        assert_eq!(depth.load_op(true), wgpu::LoadOp::Clear(DEPTH_CLEAR));
292
293        depth.set_pre_cleared(true);
294        assert!(depth.is_pre_cleared());
295        assert_eq!(depth.load_op(true), wgpu::LoadOp::Load);
296        // An engine-owned buffer holds only the previous frame's depth, so the
297        // statement does not carry over to it.
298        assert_eq!(depth.load_op(false), wgpu::LoadOp::Clear(DEPTH_CLEAR));
299    }
300
301    #[test]
302    fn an_attachment_owns_nothing_until_a_frame_asks_for_one() {
303        let depth = DepthAttachment::new();
304        assert!(depth.owned().is_none());
305        assert!(depth.owned_view().is_none());
306    }
307}