Skip to main content

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}