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}