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