frust_gpu/encoder.rs
1//! The seam a 3D pass and the 2D engine share when recording GPU work:
2//! [`CommandBuffer`], a thin wrapper around one `wgpu::CommandEncoder`.
3//!
4//! A renderer built on top of `frust-gpu` never owns a `wgpu::CommandEncoder`
5//! directly — it is handed a [`CommandBuffer`] for the duration of one
6//! recording call and must honour the borrowing discipline documented on the
7//! type itself. That discipline, not a new abstraction over `wgpu::RenderPass`
8//! itself, is the point of this module: [`CommandBuffer::render_pass`] builds
9//! a pass from the crate's own [`RenderTarget`]/[`Attachment`](crate::texture::Attachment)
10//! types so a caller never hand-assembles a `wgpu::RenderPassDescriptor`, and
11//! [`CommandBuffer::encoder_mut`] is the escape hatch for whatever recording
12//! this type does not model (staged uploads, a renderer's own pass shape).
13//!
14//! # The two caller rules
15//!
16//! Sharing one encoder between two renderers — a 3D pass of the caller's own
17//! and the 2D strip renderer above this crate — turns on two rules the
18//! borrowing invariant below cannot express by itself. Both are stated here
19//! because both are things a caller otherwise gets wrong silently.
20//!
21//! ## 1. Depth-clear ownership
22//!
23//! When both renderers attach the same depth buffer, **exactly one of them
24//! clears it, and it is whichever records first**; every pass after that loads
25//! what the previous one stored. Clearing twice throws the first pass's
26//! occlusion away, and does it with no diagnostic at all — the image simply
27//! comes back with the wrong half of it drawn. The 2D renderer's half of the
28//! statement is its `set_depth_pre_cleared` switch, which turns its own
29//! frame-opening depth clear into a load.
30//!
31//! Two facts ride along with the clear, because an attachment is only really
32//! shared if both sides read it the same way. The **comparison and the
33//! direction** must agree: the 2D renderer tests `LessEqual` against a
34//! `Depth24Plus` buffer whose far plane is `1.0`, so nearer geometry carries
35//! the smaller z, and a pass that inverted either would be occluded exactly
36//! where it should not be. And the depth attachment's **extent must equal the
37//! colour attachment's** — wgpu refuses the pass outright otherwise, which is
38//! at least a loud failure, but sizing the shared buffer against the target is
39//! still the caller's job.
40//!
41//! Colour is not shared on those terms: the 2D renderer clears its colour
42//! target every frame, so a caller's earlier pass keeps its depth and loses its
43//! pixels. Content that has to stay visible is recorded *after* the frame
44//! rather than before it.
45//!
46//! ## 2. Atlas uploads may submit their own encoder before the scene pass
47//!
48//! "Never submits" holds for scene work, and glyph-atlas upload is the one
49//! carve-out: the atlas is replayed on an encoder of *its own*, submitted ahead
50//! of the scene pass, so its content is committed before the pass that samples
51//! it reads it (vello_hybrid render/wgpu/mod.rs:428-430). That replay touches
52//! neither this buffer nor the caller's and records no scene draw — which is
53//! why a caller must not read "the renderer issued a submit" as a contract
54//! violation on its own. Any submit that is not this one is.
55
56use crate::texture::RenderTarget;
57
58/// One in-flight `wgpu::CommandEncoder`, handed to a renderer under a strict
59/// borrowing contract.
60///
61/// # Invariant
62///
63/// A renderer handed this encoder records only render passes and staged
64/// uploads; it never submits, never begins a pass it does not end before
65/// returning, holds no borrow past return; the caller may record its own
66/// passes before and after and submit once. Exception: glyph-atlas uploads
67/// may submit their OWN encoder so atlas content is committed before the
68/// scene pass reads it (vello_hybrid render/wgpu/mod.rs:428-430).
69///
70/// A caller recording depth-writing passes of its own around a renderer's owes
71/// the module-level *two caller rules* on top of this: depth-clear ownership,
72/// with the comparison, direction and extent that ride along with it.
73pub struct CommandBuffer {
74 encoder: wgpu::CommandEncoder,
75}
76
77impl CommandBuffer {
78 /// Begins recording a fresh command buffer on `device`.
79 pub fn new(device: &wgpu::Device, label: Option<&str>) -> Self {
80 Self {
81 encoder: device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }),
82 }
83 }
84
85 /// Begins a render pass over `target`'s color and depth attachments.
86 ///
87 /// The returned `wgpu::RenderPass` borrows this buffer's encoder; per the
88 /// type-level invariant, a caller must end it (drop it, or call `.end()`)
89 /// before recording anything else through [`Self::encoder_mut`], starting
90 /// another pass, or calling [`Self::finish`].
91 pub fn render_pass<'e>(
92 &'e mut self,
93 target: &RenderTarget<'_>,
94 label: Option<&str>,
95 ) -> wgpu::RenderPass<'e> {
96 let color_attachments: Vec<Option<wgpu::RenderPassColorAttachment<'_>>> = target
97 .color
98 .iter()
99 .map(|attachment| {
100 Some(wgpu::RenderPassColorAttachment {
101 view: attachment.view,
102 depth_slice: None,
103 resolve_target: None,
104 ops: wgpu::Operations {
105 load: attachment.load,
106 store: attachment.store,
107 },
108 })
109 })
110 .collect();
111 let depth_stencil_attachment =
112 target
113 .depth
114 .as_ref()
115 .map(|attachment| wgpu::RenderPassDepthStencilAttachment {
116 view: attachment.view,
117 depth_ops: Some(wgpu::Operations {
118 load: attachment.load,
119 store: attachment.store,
120 }),
121 stencil_ops: None,
122 });
123 self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
124 label,
125 color_attachments: &color_attachments,
126 depth_stencil_attachment,
127 timestamp_writes: None,
128 occlusion_query_set: None,
129 multiview_mask: None,
130 })
131 }
132
133 /// The borrowing seam: direct mutable access to the underlying encoder
134 /// for recording this type does not model — a staged buffer/texture
135 /// upload, or a pass shape this crate does not own.
136 ///
137 /// A caller reaching for this still owes the type-level invariant: no
138 /// submit, and no pass left open across a return.
139 pub fn encoder_mut(&mut self) -> &mut wgpu::CommandEncoder {
140 &mut self.encoder
141 }
142
143 /// Ends recording and hands back the finished `wgpu::CommandBuffer` for
144 /// the caller to submit — once, alongside anything else it recorded
145 /// before or after this buffer's own passes.
146 pub fn finish(self) -> wgpu::CommandBuffer {
147 self.encoder.finish()
148 }
149}