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}