frust_engine/gpu/bindings.rs
1//! The registry of caller-owned textures a frame's strip passes sample, and
2//! the per-frame batching that binds each of them in turn.
3//!
4//! The engine draws two kinds of image. An atlas-backed one lives in the
5//! renderer's own array texture, addressed by layer and rectangle, and every
6//! draw of the frame reads it through the same binding. An *external* one is a
7//! texture the host created and still owns — a video frame, a plugin's import,
8//! an offscreen page some other pass rendered — which the engine never copies
9//! and never packs. It is bound as a whole texture instead, and the strip
10//! shader's image path reads it through a second binding chosen by the encoded
11//! paint's source-kind bit (see `shaders/helpers.wgsl`).
12//!
13//! One binding, many textures: a frame that draws two different external
14//! textures cannot bind both at once, so its instances are split into *runs* —
15//! maximal spans of consecutive instances sharing one texture — and each run is
16//! drawn with that texture bound. [`ExternalRuns`] assigns the slot numbers the
17//! renderer's pass segments name a run by; the renderer builds one group-1 bind
18//! group per slot and sets it between runs.
19//!
20//! Nothing here allocates a texture or copies a texel. [`ExternalTextures`] is
21//! a plain map the host writes through
22//! `EngineRenderer::bind_texture`/`unbind_texture`, and it is generic over the
23//! stored view type — like `frust_gpu::TextureRegistry`, so its own behaviour
24//! stays host-testable with a stand-in value and no GPU device in the loop.
25//!
26//! The *extent* half of the same registry lives on the compiler side, in
27//! [`crate::compile::external::ExternalExtents`]: the walk needs it to compose
28//! a paint transform, while only a pass needs the view. The one call that
29//! writes both keeps them in step.
30
31use std::collections::HashMap;
32
33use frust_gpu::SceneTextureId;
34use vello_common::encode::EncodedExternalTexture;
35
36use super::atlas::{extend_mode, pack_image_offset, pack_image_params, pack_image_size, pack_tint};
37use super::paint_texture::{GpuEncodedImage, GpuEncodedPaint};
38
39/// Every externally owned texture currently bound, keyed by the
40/// [`SceneTextureId`] its owner minted.
41///
42/// The stored view must be a non-array 2D view of a float-sampleable texture
43/// whose texture carries `wgpu::TextureUsages::TEXTURE_BINDING`; only mip level
44/// 0 is ever read. `wgpu` rejects anything else when the bind group is built,
45/// which is the frame the mistake surfaces on.
46///
47/// Generic over the view type (default `wgpu::TextureView`, the real engine's)
48/// so the map's own insert/replace/remove behaviour is testable without a
49/// device.
50#[derive(Debug)]
51pub struct ExternalTextures<V = wgpu::TextureView> {
52 /// Keyed by the id's opaque value rather than by [`SceneTextureId`]
53 /// itself: a display list names an external texture by that same plain
54 /// `u64` (`frust_scene::Command::SceneTexture`), and a lookup from a
55 /// compiled frame has nothing but that number to ask with.
56 views: HashMap<u64, V>,
57}
58
59impl<V> Default for ExternalTextures<V> {
60 fn default() -> Self {
61 Self {
62 views: HashMap::new(),
63 }
64 }
65}
66
67impl<V> ExternalTextures<V> {
68 /// An empty registry.
69 #[must_use]
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 /// Registers `view` under `id`, returning whatever was registered before.
75 pub fn bind(&mut self, id: SceneTextureId, view: V) -> Option<V> {
76 self.views.insert(id.get(), view)
77 }
78
79 /// Removes and returns the view registered under `id`.
80 pub fn unbind(&mut self, id: SceneTextureId) -> Option<V> {
81 self.views.remove(&id.get())
82 }
83
84 /// The view registered under `id`.
85 #[must_use]
86 pub fn get(&self, id: SceneTextureId) -> Option<&V> {
87 self.views.get(&id.get())
88 }
89
90 /// The view registered under the opaque id a display list names, which is
91 /// the only form a compiled frame carries.
92 #[must_use]
93 pub fn view(&self, key: u64) -> Option<&V> {
94 self.views.get(&key)
95 }
96
97 /// How many textures are bound.
98 #[must_use]
99 pub fn len(&self) -> usize {
100 self.views.len()
101 }
102
103 /// Whether nothing is bound.
104 #[must_use]
105 pub fn is_empty(&self) -> bool {
106 self.views.is_empty()
107 }
108}
109
110/// The distinct external textures one frame draws with, in the order it first
111/// names them.
112///
113/// A frame's instances are drawn in painter order, and the single external
114/// binding has to be re-set whenever that order crosses from one texture to
115/// another. Rather than carry the id on every instance, each encoded paint
116/// takes a *slot* — its index here — and the renderer's pass segments carry
117/// that slot, so a run of instances sharing a texture is one segment and one
118/// bind-group set.
119///
120/// Filled while the frame's paints are lowered and read while its passes are
121/// recorded; cleared and refilled per frame, never growing past the number of
122/// external textures a single frame draws.
123#[derive(Debug, Default)]
124pub struct ExternalRuns {
125 keys: Vec<u64>,
126}
127
128impl ExternalRuns {
129 /// An empty set of runs.
130 #[must_use]
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 /// Forgets the previous frame's textures, keeping the allocation.
136 pub fn clear(&mut self) {
137 self.keys.clear();
138 }
139
140 /// The slot `key` is drawn under, assigning one if this frame has not
141 /// named it yet.
142 ///
143 /// A linear scan rather than a map: a frame draws a handful of external
144 /// textures at most — the binding is re-set between runs, so a scene that
145 /// interleaved hundreds would be paying far more for the pass breaks than
146 /// for the lookup.
147 ///
148 /// `None` once the slot numbering would leave `u32`, which no real frame
149 /// reaches; the paint is then left unresolved and its draws skipped, which
150 /// is the same answer an unbound texture gets.
151 pub fn slot_of(&mut self, key: u64) -> Option<u32> {
152 if let Some(index) = self.keys.iter().position(|held| *held == key) {
153 return u32::try_from(index).ok();
154 }
155 let slot = u32::try_from(self.keys.len()).ok()?;
156 self.keys.push(key);
157 Some(slot)
158 }
159
160 /// The texture each slot names, indexed by slot number.
161 #[must_use]
162 pub fn keys(&self) -> &[u64] {
163 &self.keys
164 }
165
166 /// The texture drawn under `slot`.
167 #[must_use]
168 pub fn key_at(&self, slot: u32) -> Option<u64> {
169 self.keys.get(slot as usize).copied()
170 }
171
172 /// How many distinct external textures the frame draws.
173 #[must_use]
174 pub fn len(&self) -> usize {
175 self.keys.len()
176 }
177
178 /// Whether the frame draws none.
179 #[must_use]
180 pub fn is_empty(&self) -> bool {
181 self.keys.is_empty()
182 }
183}
184
185/// Lower one encoded external texture into the record the strip shader samples.
186///
187/// The external counterpart of [`super::atlas::lower_encoded_image`], and the
188/// same record type: the shader reads both through one `GpuEncodedImage`
189/// layout and picks the binding from the source-kind bit
190/// [`pack_image_params`] sets here. What differs is where the numbers come
191/// from — the source region is the caller's own texel rectangle rather than an
192/// atlas allocation, there is no atlas layer to name (hence the zero index,
193/// which the shader never reads on this path) and no padding, since nothing
194/// was packed beside it.
195///
196/// The transform is already the inverse mapping — device space into texel
197/// space — because that is the direction the shader applies it in; narrowing
198/// it to `f32` here is what the record and the WGSL both read it at.
199#[must_use]
200pub fn lower_encoded_external(entry: &EncodedExternalTexture) -> GpuEncodedPaint {
201 let region = entry.source_region;
202 let (tint, tint_mode) = pack_tint(entry.tint);
203
204 GpuEncodedPaint::Image(GpuEncodedImage {
205 image_params: pack_image_params(
206 entry.sampler.quality as u32,
207 extend_mode(entry.sampler.x_extend),
208 extend_mode(entry.sampler.y_extend),
209 0,
210 true,
211 ),
212 image_size: pack_image_size(
213 region.x1.saturating_sub(region.x0),
214 region.y1.saturating_sub(region.y0),
215 ),
216 image_offset: pack_image_offset(region.x0, region.y0),
217 transform: entry.transform.as_coeffs().map(|coeff| coeff as f32),
218 tint,
219 tint_mode,
220 image_padding: 0,
221 })
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 use peniko::{ImageQuality, ImageSampler};
229 use vello_common::TextureId;
230 use vello_common::geometry::RectU16;
231 use vello_common::kurbo::Affine;
232
233 /// A registry over plain values, which is all the map's own behaviour
234 /// needs — minting a real `wgpu::TextureView` would require a device.
235 fn registry() -> ExternalTextures<&'static str> {
236 ExternalTextures::new()
237 }
238
239 fn entry(region: RectU16) -> EncodedExternalTexture {
240 EncodedExternalTexture {
241 texture_id: TextureId(1),
242 source_region: region,
243 sampler: ImageSampler {
244 quality: ImageQuality::Medium,
245 ..ImageSampler::default()
246 },
247 may_have_transparency: true,
248 transform: Affine::IDENTITY,
249 tint: None,
250 }
251 }
252
253 #[test]
254 fn a_bound_view_is_reachable_by_id_and_by_the_scenes_own_number() {
255 let mut textures = registry();
256 let id = fresh_id();
257
258 assert!(textures.bind(id, "view").is_none());
259
260 assert_eq!(textures.get(id), Some(&"view"));
261 assert_eq!(textures.view(id.get()), Some(&"view"));
262 assert_eq!(textures.len(), 1);
263 }
264
265 #[test]
266 fn rebinding_returns_the_previous_view() {
267 let mut textures = registry();
268 let id = fresh_id();
269 textures.bind(id, "first");
270
271 assert_eq!(textures.bind(id, "second"), Some("first"));
272 assert_eq!(textures.get(id), Some(&"second"));
273 }
274
275 #[test]
276 fn unbinding_returns_the_view_and_empties_the_registry() {
277 let mut textures = registry();
278 let id = fresh_id();
279 textures.bind(id, "view");
280
281 assert_eq!(textures.unbind(id), Some("view"));
282 assert!(textures.is_empty());
283 assert_eq!(textures.view(id.get()), None);
284 }
285
286 #[test]
287 fn an_unbound_id_resolves_to_nothing() {
288 let textures = registry();
289
290 assert_eq!(textures.view(4_242), None);
291 }
292
293 #[test]
294 fn a_slot_is_assigned_once_per_texture_and_reused_after() {
295 let mut runs = ExternalRuns::new();
296
297 assert_eq!(runs.slot_of(7), Some(0));
298 assert_eq!(runs.slot_of(9), Some(1));
299 assert_eq!(runs.slot_of(7), Some(0), "the same texture keeps its slot");
300 assert_eq!(runs.keys(), &[7, 9]);
301 assert_eq!(runs.key_at(1), Some(9));
302 assert_eq!(runs.key_at(2), None);
303 }
304
305 #[test]
306 fn clearing_forgets_the_previous_frames_textures() {
307 let mut runs = ExternalRuns::new();
308 runs.slot_of(7);
309
310 runs.clear();
311
312 assert!(runs.is_empty());
313 assert_eq!(runs.slot_of(9), Some(0), "slots restart each frame");
314 }
315
316 #[test]
317 fn a_lowered_external_record_names_the_external_source_and_no_atlas_layer() {
318 let record = lower_encoded_external(&entry(RectU16 {
319 x0: 0,
320 y0: 0,
321 x1: 64,
322 y1: 32,
323 }));
324
325 let GpuEncodedPaint::Image(image) = record else {
326 panic!("an external texture lowers to the image record");
327 };
328 assert_eq!(
329 (image.image_params >> 14) & 0x1,
330 1,
331 "the source-kind bit selects the external binding"
332 );
333 assert_eq!((image.image_params >> 6) & 0xFF, 0, "no atlas layer");
334 assert_eq!(image.image_size, pack_image_size(64, 32));
335 assert_eq!(image.image_offset, pack_image_offset(0, 0));
336 assert_eq!(image.image_padding, 0, "nothing was packed beside it");
337 }
338
339 #[test]
340 fn a_sub_rectangle_lowers_as_its_own_offset_and_extent() {
341 let record = lower_encoded_external(&entry(RectU16 {
342 x0: 8,
343 y0: 4,
344 x1: 24,
345 y1: 20,
346 }));
347
348 let GpuEncodedPaint::Image(image) = record else {
349 panic!("an external texture lowers to the image record");
350 };
351 assert_eq!(image.image_offset, pack_image_offset(8, 4));
352 assert_eq!(image.image_size, pack_image_size(16, 16));
353 }
354
355 /// A fresh [`SceneTextureId`], minted the only way this crate can: off a
356 /// texture handle built over stand-in values.
357 fn fresh_id() -> SceneTextureId {
358 frust_gpu::Texture::new(
359 (),
360 (),
361 frust_gpu::TextureDesc {
362 width: 1,
363 height: 1,
364 format: wgpu::TextureFormat::Rgba8Unorm,
365 usage: wgpu::TextureUsages::TEXTURE_BINDING,
366 label: None,
367 },
368 )
369 .as_scene_texture()
370 }
371}