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}