frust_gpu/texture.rs
1//! Texture handles and render-target descriptors: [`TextureDesc`],
2//! [`Texture`], [`RenderTarget`]/[`Attachment`], and the two id types a
3//! texture crosses a layer boundary under.
4//!
5//! [`TextureId`] is a caller-assigned handle for an external binding (a
6//! plugin import, a platform surface's backing texture); [`SceneTextureId`]
7//! is minted by [`Texture::as_scene_texture`] and is the key a future
8//! render-engine [`TextureRegistry`] resolves `Command::SceneTexture`
9//! against — the same process-wide-`AtomicU64`, stable-across-`Clone`
10//! mechanism `frust_scene::ShaderProgram::id` uses.
11
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15/// Process-unique id counter backing [`SceneTextureId::mint`].
16static NEXT_SCENE_TEXTURE_ID: AtomicU64 = AtomicU64::new(1);
17
18/// The bit [`SceneTextureId::for_shader_program`] sets so an offscreen shader
19/// effect's target id can never be one [`SceneTextureId::mint`] hands out.
20///
21/// The id space is split three ways by the top two bits, so every space is
22/// disjoint from the others by construction rather than by luck:
23///
24/// - bit 63 set — a shader program's own offscreen target
25/// ([`SceneTextureId::for_shader_program`]);
26/// - bit 62 set, bit 63 clear — a texture-bus frame source
27/// ([`SceneTextureId::for_texture_source`], see
28/// [`TEXTURE_SOURCE_NAMESPACE`]);
29/// - both clear — everything [`SceneTextureId::mint`] hands out.
30///
31/// `mint` counts up from 1 off a single process-wide counter and refuses to
32/// reach bit 62 (2^62 mints — more textures than a process can create), so a
33/// minted id never carries either reserved bit. Reserving the top bit is what
34/// lets a fragment program's own ordinal be reused as the id its rendered
35/// target is registered under without ever colliding with a host texture
36/// carrying the same ordinal; the bit below it does the same for a texture
37/// source's ordinal.
38const SHADER_PROGRAM_NAMESPACE: u64 = 1 << 63;
39
40/// The bit [`SceneTextureId::for_texture_source`] sets: the namespace of the
41/// renderer's external-texture frame bus, whose registrations derive their id
42/// from an ordinal of their own rather than minting one. See
43/// [`SHADER_PROGRAM_NAMESPACE`] for how the three id spaces stay disjoint.
44const TEXTURE_SOURCE_NAMESPACE: u64 = 1 << 62;
45
46/// Both reserved namespace bits — what an ordinal is masked by before a
47/// namespace bit is set on it, so a derived id can only ever carry its own
48/// namespace's bit.
49const RESERVED_BITS: u64 = SHADER_PROGRAM_NAMESPACE | TEXTURE_SOURCE_NAMESPACE;
50
51/// Describes a texture to create: dimensions, format, usage, and an optional
52/// debug label.
53///
54/// `sample_count` is deliberately **not** a field here — this crate creates
55/// no multisampled texture, and [`Self::sample_count`] always answers `1`;
56/// there is no setter to request anything else.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct TextureDesc {
59 /// Texture width in texels.
60 pub width: u32,
61 /// Texture height in texels.
62 pub height: u32,
63 /// The texture's pixel format.
64 pub format: wgpu::TextureFormat,
65 /// How the texture may be used (render attachment, sampled binding,
66 /// copy source/destination, …).
67 pub usage: wgpu::TextureUsages,
68 /// Optional debug label, surfaced in adapter/validation diagnostics.
69 pub label: Option<String>,
70}
71
72impl TextureDesc {
73 /// The sample count every texture built from this descriptor uses.
74 /// Always `1` — no MSAA. Not a field, so there is nothing to set.
75 pub const fn sample_count(&self) -> u32 {
76 1
77 }
78}
79
80/// A caller-assigned id for a texture crossing an external binding boundary
81/// — a plugin's imported texture, or a platform surface's backing texture.
82///
83/// Distinct from [`SceneTextureId`]: this one is supplied by the caller, not
84/// minted by this crate.
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
86pub struct TextureId(pub u64);
87
88/// A process-unique id [`Texture::as_scene_texture`] mints once per
89/// [`Texture`] and shares across `Clone` — the [`TextureRegistry`] key
90/// `Command::SceneTexture` resolves against.
91///
92/// Mirrors `frust_scene::ShaderProgram::id`: minted fresh only at
93/// construction (never re-minted on `Clone`), off a process-wide
94/// [`AtomicU64`] so two distinct textures never collide even when built on
95/// different threads.
96#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
97pub struct SceneTextureId(u64);
98
99impl SceneTextureId {
100 /// Mints a fresh, process-unique id.
101 ///
102 /// [`Texture::new`] mints one per texture and shares it across `Clone`,
103 /// and that stays the ordinary route. This is public for the caller that
104 /// has no [`Texture`] to mint from: a texture rendered *externally* — one
105 /// whose `wgpu::TextureView` its owner creates, re-creates and binds
106 /// itself every frame — still needs one stable id to be named by across
107 /// those re-creations, and minting here is what hands it one out of the
108 /// same process-wide counter. The shader-program and texture-source id
109 /// spaces stay reserved against every such mint by construction (see
110 /// [`Self::for_shader_program`] and [`Self::for_texture_source`]).
111 ///
112 /// # Panics
113 ///
114 /// After 2^62 - 1 mints, rather than handing out an id inside a reserved
115 /// namespace — a count no process reaches.
116 #[must_use]
117 pub fn mint() -> Self {
118 let raw = NEXT_SCENE_TEXTURE_ID.fetch_add(1, Ordering::Relaxed);
119 assert!(
120 raw & RESERVED_BITS == 0,
121 "the minted scene-texture id space is exhausted"
122 );
123 Self(raw)
124 }
125
126 /// The id under which the offscreen target of the fragment program
127 /// `program_id` is registered.
128 ///
129 /// Derived from the program's own ordinal rather than minted, because both
130 /// halves of the seam have to name the same texture while holding
131 /// different things: the pre-pass that renders the program has the target,
132 /// and the display-list walk that draws it has only
133 /// `frust_scene::ShaderProgram::id`. A pure function of the program id
134 /// lets each compute the id the other used, with no shared side table to
135 /// keep in step.
136 ///
137 /// Disjoint from [`Self::mint`]'s range and from
138 /// [`Self::for_texture_source`]'s by construction — see
139 /// [`SHADER_PROGRAM_NAMESPACE`] — so a host texture, a texture source and
140 /// a shader target can never answer to the same id. A `program_id` large
141 /// enough to reach a reserved bit itself is impossible for the same
142 /// counting reason its minter is.
143 #[must_use]
144 pub const fn for_shader_program(program_id: u64) -> Self {
145 Self(program_id | SHADER_PROGRAM_NAMESPACE)
146 }
147
148 /// The id under which the external-texture frame bus registers its
149 /// `source`-th frame source.
150 ///
151 /// Derived from an ordinal the bus counts itself rather than minted, for
152 /// the same reason [`Self::for_shader_program`] is: the namespace is the
153 /// bus's own, so an ordinary external pass can be refused an id inside it
154 /// and a bus registration can never displace one a caller minted. The
155 /// ordinal is masked below both reserved bits before the namespace bit is
156 /// set, so the result is never a shader-program id either — disjoint from
157 /// [`Self::mint`] and [`Self::for_shader_program`] by construction (see
158 /// [`SHADER_PROGRAM_NAMESPACE`]).
159 #[must_use]
160 pub const fn for_texture_source(source: u64) -> Self {
161 Self((source & !RESERVED_BITS) | TEXTURE_SOURCE_NAMESPACE)
162 }
163
164 /// The opaque value this id wraps.
165 ///
166 /// The counterpart of `frust_scene::ShaderProgram::id`, and for the same
167 /// reason: the scene layer carries an externally owned texture as a plain
168 /// `u64` so it depends on no GPU crate, and a caller that registered a
169 /// [`Texture`] needs some way to say which one a display-list command
170 /// means. This only reads back what [`Self::mint`] handed out; it never
171 /// mints.
172 #[must_use]
173 pub const fn get(self) -> u64 {
174 self.0
175 }
176
177 /// Whether `self` carries the bit [`Self::for_shader_program`] sets —
178 /// i.e. whether it names a shader program's own offscreen target rather
179 /// than something an ordinary caller minted.
180 ///
181 /// Lets a caller outside this module test which namespace an id belongs
182 /// to without the reserved bit itself being exported — a check on the id
183 /// stays valid even if the bit's exact value ever changed.
184 #[must_use]
185 pub const fn is_shader_program(self) -> bool {
186 self.0 & SHADER_PROGRAM_NAMESPACE != 0
187 }
188
189 /// Whether `self` names a texture-bus frame source
190 /// ([`Self::for_texture_source`]) rather than something an ordinary
191 /// caller minted or a shader program's target.
192 ///
193 /// The namespace counterpart of [`Self::is_shader_program`], for the same
194 /// reason: a registry that must refuse the bus's ids to anyone but the bus
195 /// tests membership without the reserved bit being exported.
196 #[must_use]
197 pub const fn is_texture_source(self) -> bool {
198 self.0 & RESERVED_BITS == TEXTURE_SOURCE_NAMESPACE
199 }
200}
201
202/// A GPU texture: the underlying texture/view pair, the [`TextureDesc`] it
203/// was created from, and a [`SceneTextureId`] minted once at construction
204/// and shared across `Clone`.
205///
206/// Generic over the wrapped texture (`T`, default `wgpu::Texture`) and view
207/// (`V`, default `wgpu::TextureView`) types rather than hardcoded to them, so
208/// [`Self::as_scene_texture`]'s id-uniqueness and clone-stability contract
209/// stays host-testable with plain stand-in values — no GPU device needed to
210/// mint a real `wgpu::Texture`/`wgpu::TextureView` pair just to exercise the
211/// id logic. The real engine always instantiates the default `Texture`
212/// (`Texture<wgpu::Texture, wgpu::TextureView>`).
213#[derive(Clone, Debug)]
214pub struct Texture<T = wgpu::Texture, V = wgpu::TextureView> {
215 texture: T,
216 view: V,
217 desc: TextureDesc,
218 scene_id: SceneTextureId,
219}
220
221impl<T, V> Texture<T, V> {
222 /// Wraps an already-created texture/view pair with the [`TextureDesc`]
223 /// it was built from, minting a fresh [`SceneTextureId`].
224 ///
225 /// Device/texture/view creation itself is a caller concern (this crate's
226 /// device-init work is tracked separately) — this constructor only
227 /// assembles the handle and mints its scene id.
228 pub fn new(texture: T, view: V, desc: TextureDesc) -> Self {
229 Self {
230 texture,
231 view,
232 desc,
233 scene_id: SceneTextureId::mint(),
234 }
235 }
236
237 /// The underlying texture.
238 pub fn texture(&self) -> &T {
239 &self.texture
240 }
241
242 /// The texture's full-extent view.
243 pub fn view(&self) -> &V {
244 &self.view
245 }
246
247 /// `(width, height)` in texels, from the originating [`TextureDesc`].
248 pub fn size(&self) -> (u32, u32) {
249 (self.desc.width, self.desc.height)
250 }
251
252 /// The texture's pixel format, from the originating [`TextureDesc`].
253 pub fn format(&self) -> wgpu::TextureFormat {
254 self.desc.format
255 }
256
257 /// The originating [`TextureDesc`].
258 pub fn desc(&self) -> &TextureDesc {
259 &self.desc
260 }
261
262 /// The process-unique [`SceneTextureId`] minted for this texture at
263 /// construction, stable across `Clone`.
264 pub fn as_scene_texture(&self) -> SceneTextureId {
265 self.scene_id
266 }
267}
268
269/// One attachment of a [`RenderTarget`]: the view rendered into, and the
270/// load/store ops framing that render pass's access to it.
271///
272/// Generic over the clear-value type (`V`) because a color attachment's
273/// clear value is `wgpu::Color` while a depth attachment's is `f32` — the
274/// same split `wgpu::RenderPassColorAttachment`/`RenderPassDepthStencilAttachment`
275/// draw between themselves.
276#[derive(Clone, Debug)]
277pub struct Attachment<'a, V> {
278 /// The view rendered into.
279 pub view: &'a wgpu::TextureView,
280 /// How the attachment's prior contents are treated at pass start.
281 pub load: wgpu::LoadOp<V>,
282 /// Whether the attachment's contents are kept or discarded at pass end.
283 pub store: wgpu::StoreOp,
284}
285
286/// A [`RenderTarget`]'s color attachment: clear value is `wgpu::Color`.
287pub type ColorAttachment<'a> = Attachment<'a, wgpu::Color>;
288
289/// A [`RenderTarget`]'s depth attachment: clear value is a plain `f32`
290/// depth, matching `wgpu::RenderPassDepthStencilAttachment`'s depth op.
291pub type DepthAttachment<'a> = Attachment<'a, f32>;
292
293/// The set of attachments a render pass targets: zero or more color
294/// attachments plus an optional depth attachment.
295#[derive(Clone, Debug, Default)]
296pub struct RenderTarget<'a> {
297 /// Color attachments, in binding order.
298 pub color: Vec<ColorAttachment<'a>>,
299 /// The depth attachment, if the pass writes depth.
300 pub depth: Option<DepthAttachment<'a>>,
301}
302
303/// The engine's live map from a minted [`SceneTextureId`] to the view
304/// `Command::SceneTexture` resolves against.
305///
306/// Generic over the stored view type (default `wgpu::TextureView`, the real
307/// engine's usage) rather than hardcoded to it, so the registry's own
308/// insert/get/remove behavior stays host-testable with a plain stand-in
309/// value — no GPU device needed to exercise the map logic itself.
310#[derive(Debug)]
311pub struct TextureRegistry<V = wgpu::TextureView> {
312 views: HashMap<SceneTextureId, V>,
313}
314
315impl<V> Default for TextureRegistry<V> {
316 fn default() -> Self {
317 Self {
318 views: HashMap::new(),
319 }
320 }
321}
322
323impl<V> TextureRegistry<V> {
324 /// An empty registry.
325 pub fn new() -> Self {
326 Self::default()
327 }
328
329 /// Inserts (or replaces) the view registered under `id`, returning the
330 /// previous entry, if any.
331 pub fn insert(&mut self, id: SceneTextureId, view: V) -> Option<V> {
332 self.views.insert(id, view)
333 }
334
335 /// The view registered under `id`, if any.
336 pub fn get(&self, id: SceneTextureId) -> Option<&V> {
337 self.views.get(&id)
338 }
339
340 /// Removes and returns the view registered under `id`, if any.
341 pub fn remove(&mut self, id: SceneTextureId) -> Option<V> {
342 self.views.remove(&id)
343 }
344
345 /// The number of registered entries.
346 pub fn len(&self) -> usize {
347 self.views.len()
348 }
349
350 /// Whether the registry holds no entries.
351 pub fn is_empty(&self) -> bool {
352 self.views.is_empty()
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn fake_desc() -> TextureDesc {
361 TextureDesc {
362 width: 64,
363 height: 64,
364 format: wgpu::TextureFormat::Rgba8Unorm,
365 usage: wgpu::TextureUsages::TEXTURE_BINDING,
366 label: Some("test".to_string()),
367 }
368 }
369
370 #[test]
371 fn sample_count_is_always_one_and_has_no_setter() {
372 // `TextureDesc` exposes no `sample_count` field to set — the type
373 // only has `sample_count()`, which always answers `1`.
374 let desc = fake_desc();
375 assert_eq!(desc.sample_count(), 1);
376 }
377
378 /// A stand-in `Texture` with no GPU dependency: `u32` fills in for both
379 /// the wrapped texture and view, so `Texture::new` and
380 /// `as_scene_texture` are exercised exactly as the real
381 /// `Texture<wgpu::Texture, wgpu::TextureView>` would, with no device.
382 fn fake_texture() -> Texture<u32, u32> {
383 Texture::new(0, 0, fake_desc())
384 }
385
386 #[test]
387 fn a_shader_program_id_never_collides_with_a_minted_one() {
388 // Every minted id stays in the counter's own range; every shader
389 // target id carries the reserved bit, so the two can never meet.
390 for _ in 0..64 {
391 let minted = fake_texture().as_scene_texture();
392 assert_eq!(minted.get() & SHADER_PROGRAM_NAMESPACE, 0);
393 assert_ne!(minted, SceneTextureId::for_shader_program(minted.get()));
394 }
395 }
396
397 #[test]
398 fn is_shader_program_distinguishes_the_two_namespaces() {
399 let minted = fake_texture().as_scene_texture();
400 let shader = SceneTextureId::for_shader_program(minted.get());
401 assert!(!minted.is_shader_program());
402 assert!(shader.is_shader_program());
403 }
404
405 #[test]
406 fn the_three_id_namespaces_are_disjoint() {
407 // Every minted id carries neither reserved bit; every texture-source
408 // id carries exactly its own; every shader id carries the top bit.
409 // Derived from the same ordinals, so a collision would show here.
410 for _ in 0..64 {
411 let minted = SceneTextureId::mint();
412 let source = SceneTextureId::for_texture_source(minted.get());
413 let shader = SceneTextureId::for_shader_program(minted.get());
414
415 assert_eq!(minted.get() & RESERVED_BITS, 0);
416 assert!(!minted.is_shader_program() && !minted.is_texture_source());
417
418 assert!(source.is_texture_source());
419 assert!(!source.is_shader_program());
420 assert_eq!(source.get() & RESERVED_BITS, TEXTURE_SOURCE_NAMESPACE);
421
422 assert!(shader.is_shader_program());
423 assert!(!shader.is_texture_source());
424
425 assert_ne!(minted, source);
426 assert_ne!(minted, shader);
427 assert_ne!(source, shader);
428 }
429 }
430
431 #[test]
432 fn a_texture_source_id_is_a_pure_function_of_its_ordinal() {
433 assert_eq!(
434 SceneTextureId::for_texture_source(3),
435 SceneTextureId::for_texture_source(3)
436 );
437 assert_ne!(
438 SceneTextureId::for_texture_source(3),
439 SceneTextureId::for_texture_source(4)
440 );
441 }
442
443 #[test]
444 fn a_texture_source_ordinal_carrying_a_reserved_bit_stays_out_of_the_shader_space() {
445 // An ordinal no counter reaches, but the mask must still keep the
446 // result inside the texture-source namespace alone.
447 let id = SceneTextureId::for_texture_source(u64::MAX);
448 assert!(id.is_texture_source());
449 assert!(!id.is_shader_program());
450 }
451
452 #[test]
453 fn a_shader_program_id_is_a_pure_function_of_the_program() {
454 // Both halves of the seam derive the same id from the same program,
455 // and distinct programs stay distinct.
456 assert_eq!(
457 SceneTextureId::for_shader_program(9),
458 SceneTextureId::for_shader_program(9)
459 );
460 assert_ne!(
461 SceneTextureId::for_shader_program(9),
462 SceneTextureId::for_shader_program(10)
463 );
464 }
465
466 #[test]
467 fn as_scene_texture_mints_unique_ids() {
468 let a = fake_texture();
469 let b = fake_texture();
470 assert_ne!(a.as_scene_texture(), b.as_scene_texture());
471 }
472
473 #[test]
474 fn as_scene_texture_mints_unique_ids_across_threads() {
475 let handles: Vec<_> = (0..8)
476 .map(|_| std::thread::spawn(|| fake_texture().as_scene_texture()))
477 .collect();
478 let mut ids: Vec<SceneTextureId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
479 ids.sort_by_key(|id| id.0);
480 ids.dedup();
481 assert_eq!(ids.len(), 8, "all ids must be unique across threads");
482 }
483
484 #[test]
485 fn as_scene_texture_is_stable_across_clone() {
486 let a = fake_texture();
487 let b = a.clone();
488 assert_eq!(a.as_scene_texture(), b.as_scene_texture());
489 }
490
491 #[test]
492 fn texture_id_wraps_the_caller_supplied_value() {
493 let id = TextureId(42);
494 assert_eq!(id.0, 42);
495 assert_eq!(id, TextureId(42));
496 assert_ne!(id, TextureId(43));
497 }
498
499 #[test]
500 fn render_target_defaults_to_no_attachments() {
501 let target: RenderTarget = RenderTarget::default();
502 assert!(target.color.is_empty());
503 assert!(target.depth.is_none());
504 }
505
506 #[test]
507 fn registry_insert_get_remove_round_trip() {
508 let mut registry: TextureRegistry<&'static str> = TextureRegistry::new();
509 let id = SceneTextureId::mint();
510 assert!(registry.is_empty());
511
512 assert_eq!(registry.insert(id, "view-a"), None);
513 assert_eq!(registry.len(), 1);
514 assert_eq!(registry.get(id), Some(&"view-a"));
515
516 assert_eq!(registry.insert(id, "view-b"), Some("view-a"));
517 assert_eq!(registry.get(id), Some(&"view-b"));
518
519 assert_eq!(registry.remove(id), Some("view-b"));
520 assert_eq!(registry.get(id), None);
521 assert!(registry.is_empty());
522 }
523
524 #[test]
525 fn registry_get_and_remove_miss_on_unknown_id() {
526 let mut registry: TextureRegistry<u32> = TextureRegistry::new();
527 let unknown = SceneTextureId::mint();
528 assert_eq!(registry.get(unknown), None);
529 assert_eq!(registry.remove(unknown), None);
530 }
531
532 #[test]
533 fn registry_distinguishes_ids() {
534 let mut registry: TextureRegistry<u32> = TextureRegistry::new();
535 let a = SceneTextureId::mint();
536 let b = SceneTextureId::mint();
537 registry.insert(a, 1);
538 registry.insert(b, 2);
539 assert_eq!(registry.get(a), Some(&1));
540 assert_eq!(registry.get(b), Some(&2));
541 assert_eq!(registry.len(), 2);
542 }
543}