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 staged upload may leave one more thing on the encoder: the deferred
71/// re-map of the staging slot it was copied from
72/// (`wgpu::CommandEncoder::map_buffer_on_submit`, see [`crate::staging`]).
73/// It runs only when the caller submits, after the caller's own work in the
74/// same submission, and is dropped harmlessly with an encoder that is never
75/// submitted — it asks nothing of the caller beyond the one submit.
76///
77/// A caller recording depth-writing passes of its own around a renderer's owes
78/// the module-level *two caller rules* on top of this: depth-clear ownership,
79/// with the comparison, direction and extent that ride along with it.
80pub struct CommandBuffer {
81 encoder: wgpu::CommandEncoder,
82}
83
84impl CommandBuffer {
85 /// Begins recording a fresh command buffer on `device`.
86 pub fn new(device: &wgpu::Device, label: Option<&str>) -> Self {
87 Self {
88 encoder: device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }),
89 }
90 }
91
92 /// Begins a render pass over `target`'s color and depth attachments.
93 ///
94 /// The returned `wgpu::RenderPass` borrows this buffer's encoder; per the
95 /// type-level invariant, a caller must end it (drop it, or call `.end()`)
96 /// before recording anything else through [`Self::encoder_mut`], starting
97 /// another pass, or calling [`Self::finish`].
98 pub fn render_pass<'e>(
99 &'e mut self,
100 target: &RenderTarget<'_>,
101 label: Option<&str>,
102 ) -> wgpu::RenderPass<'e> {
103 let color_attachments: Vec<Option<wgpu::RenderPassColorAttachment<'_>>> = target
104 .color
105 .iter()
106 .map(|attachment| {
107 Some(wgpu::RenderPassColorAttachment {
108 view: attachment.view,
109 depth_slice: None,
110 resolve_target: None,
111 ops: wgpu::Operations {
112 load: attachment.load,
113 store: attachment.store,
114 },
115 })
116 })
117 .collect();
118 let depth_stencil_attachment =
119 target
120 .depth
121 .as_ref()
122 .map(|attachment| wgpu::RenderPassDepthStencilAttachment {
123 view: attachment.view,
124 depth_ops: Some(wgpu::Operations {
125 load: attachment.load,
126 store: attachment.store,
127 }),
128 stencil_ops: None,
129 });
130 self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
131 label,
132 color_attachments: &color_attachments,
133 depth_stencil_attachment,
134 timestamp_writes: None,
135 occlusion_query_set: None,
136 multiview_mask: None,
137 })
138 }
139
140 /// The borrowing seam: direct mutable access to the underlying encoder
141 /// for recording this type does not model — a staged buffer/texture
142 /// upload, or a pass shape this crate does not own.
143 ///
144 /// A caller reaching for this still owes the type-level invariant: no
145 /// submit, and no pass left open across a return.
146 pub fn encoder_mut(&mut self) -> &mut wgpu::CommandEncoder {
147 &mut self.encoder
148 }
149
150 /// Ends recording and hands back the finished `wgpu::CommandBuffer` for
151 /// the caller to submit — once, alongside anything else it recorded
152 /// before or after this buffer's own passes.
153 pub fn finish(self) -> wgpu::CommandBuffer {
154 self.encoder.finish()
155 }
156}