concinnity_render/render_graph/types.rs
1// src/render_graph/types.rs
2//
3// Shared, backend-agnostic types for the render graph: resource handles,
4// resource descriptions, state / access enums, and the small structs the
5// compile pass emits. The graph tracks *order*, *barriers*, and
6// *lifetimes*: it does not allocate transient GPU resources (those stay
7// backend-owned).
8
9use concinnity_core::math::floor;
10use core::num::NonZeroU32;
11
12// The set operations every flag newtype in this module shares. `$noun` names
13// what a bit means so the generated rustdoc reads naturally per type.
14macro_rules! flag_set_ops {
15 ($ty:ident, $noun:literal) => {
16 impl $ty {
17 #[doc = concat!("The empty ", $noun, " set.")]
18 pub const fn empty() -> Self {
19 Self(0)
20 }
21
22 #[doc = concat!("Every ", $noun, " in either set.")]
23 pub const fn union(self, other: Self) -> Self {
24 Self(self.0 | other.0)
25 }
26
27 #[doc = concat!("Whether every ", $noun, " in `other` is set here.")]
28 pub const fn contains(self, other: Self) -> bool {
29 (self.0 & other.0) == other.0
30 }
31 }
32
33 impl core::ops::BitOr for $ty {
34 type Output = Self;
35 fn bitor(self, rhs: Self) -> Self {
36 self.union(rhs)
37 }
38 }
39 };
40}
41
42/// One side of a `PassBuilder::read_*` / `write_*` declaration. The
43/// resource is a small dense index into the graph's resource arena; the
44/// `version` increments on every write so a read-after-write chain
45/// (`main → decals → fog` writing the same hdr_resolve) is an unambiguous
46/// DAG.
47///
48/// Each `TextureHandle` / `BufferHandle` pairs the resource id with the
49/// version it refers to. `write_*` returns a new handle pointing at the
50/// post-write version; the old handle stays valid (and refers to the
51/// pre-write version) so a pass can still legally read the prior content
52/// if it wants.
53#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
54pub struct TextureHandle {
55 pub(super) resource: ResourceId,
56 pub(super) version: u32,
57}
58
59impl TextureHandle {
60 // Sentinel for "no texture", used by the per-frame graph builder
61 // for conditional passes (SSR off, TAA off, ...) so the call sites
62 // stay branchless. The compile pass treats reads / writes of an
63 // invalid handle as no-ops.
64 pub(crate) const INVALID: Self = Self {
65 resource: ResourceId::INVALID,
66 version: 0,
67 };
68
69 // `true` when this handle was produced by a valid `create_*` /
70 // `import_*` call; `false` when it's the `INVALID` sentinel.
71 pub(crate) fn is_valid(self) -> bool {
72 self.resource.is_valid()
73 }
74}
75
76// Buffer counterpart to [`TextureHandle`]. Same handle / version model.
77#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
78pub(crate) struct BufferHandle {
79 pub(super) resource: ResourceId,
80 pub(super) version: u32,
81}
82
83impl BufferHandle {
84 pub(crate) const INVALID: Self = Self {
85 resource: ResourceId::INVALID,
86 version: 0,
87 };
88
89 pub(crate) fn is_valid(self) -> bool {
90 self.resource.is_valid()
91 }
92}
93
94/// Dense resource identifier. `u32::MAX` reserved as the "invalid"
95/// sentinel; everything else is a valid index into the compiled graph's
96/// `resources` Vec. The executor uses `index()` to look up a resource's
97/// realised GPU object.
98#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
99pub struct ResourceId(pub(super) u32);
100
101impl ResourceId {
102 pub(crate) const INVALID: Self = Self(u32::MAX);
103
104 pub(crate) fn is_valid(self) -> bool {
105 self.0 != u32::MAX
106 }
107
108 /// The resource's stable index into `CompiledGraph.resources`.
109 pub fn index(self) -> usize {
110 self.0 as usize
111 }
112}
113
114/// What kind of work an executor encodes for a pass: render vs compute.
115/// The graph cares about this only enough to pick the right
116/// `MTLRenderPassDescriptor` / `MTLComputePassDescriptor` analogue per
117/// backend; the actual encoding stays in the per-backend `encode_*`
118/// methods. Blit passes are not yet in scope (today's engine has none).
119#[derive(Copy, Clone, Debug, Eq, PartialEq)]
120pub enum PassKind {
121 /// A render pass.
122 Render,
123 /// A compute pass.
124 Compute,
125}
126
127// Whether a resource is engine-owned (the graph references it) or
128// declared inside the graph (the graph tracks its lifetime; the graph
129// does not yet own its allocation).
130#[derive(Copy, Clone, Debug, Eq, PartialEq)]
131pub enum ResourceOrigin {
132 // Engine owns the GPU object; the graph just references it by
133 // handle. Most of today's `MtlContext` targets enter the graph via
134 // `import_texture` / `import_buffer`.
135 Imported,
136 // Graph-tracked resource declared via `create_texture` /
137 // `create_buffer`. The backend asserts it has a target of matching
138 // shape; the graph does not yet allocate from a pool with aliasing.
139 Transient,
140}
141
142/// Coarse per-resource state used by the barrier deriver. The
143/// executor maps each to the backend's concrete state: for Vulkan a
144/// `VkImageLayout` + `VkAccessFlags` pair, for DirectX a
145/// `D3D12_RESOURCE_STATES`, for Metal mostly a no-op except `useResource`
146/// on the ICB-driven cull pass.
147#[derive(Copy, Clone, Debug, Eq, PartialEq)]
148pub enum ResourceState {
149 /// Initial state before any pass uses the resource. Reading from
150 /// `Undefined` is a compile error (a read with no producer); writing
151 /// to it is always legal and transitions the state to a writer
152 /// variant below.
153 Undefined,
154 /// A pass reads the resource (sampled texture, uniform / SSBO,
155 /// indirect-args buffer, depth read, ...). Multiple consecutive
156 /// reads are coalesced: they don't insert barriers between
157 /// themselves.
158 Read,
159 /// A pass writes the resource (render target, depth-stencil target,
160 /// storage write, blend write). A second write after a read inserts
161 /// a Write→Read→Write barrier chain; consecutive writes by the same
162 /// pass do not, but consecutive writes across passes do.
163 Write,
164}
165
166/// How a graph resource a backend drives from `barriers_before` is used, so
167/// the backend can translate the coarse `ResourceState` into a concrete native
168/// state: the same `Write` means a colour render target for one resource and a
169/// depth-stencil target for another, which map to different
170/// `D3D12_RESOURCE_STATES` / `vk::ImageLayout`s. The backend resolver assigns a
171/// class to each migrated resource; the backend's barrier translator maps
172/// `(class, state)` to its native state. Extend as resources of new kinds
173/// (storage / compute targets, ...) migrate.
174#[derive(Copy, Clone, Debug, Eq, PartialEq)]
175pub enum GraphResourceClass {
176 /// Sampled colour render target (e.g. the SSAO occlusion `ao_output`).
177 ColorTarget,
178 /// Sampled depth-stencil render target (e.g. the CSM `shadow_map`).
179 DepthTarget,
180 /// Compute-written, shader-sampled storage image (e.g. the volumetric-fog
181 /// `fog_froxel_volume`): a compute pass writes it (DirectX UNORDERED_ACCESS /
182 /// Vulkan GENERAL) and a later fragment pass samples it. Unlike the two
183 /// target classes its `Write` happens in the compute stage, so the translated
184 /// state pairs a storage-write layout with the compute pipeline stage.
185 StorageImage,
186 /// Compute-written buffer consumed as draw arguments (e.g. the GPU cull pass's
187 /// `draw_args`): a compute pass writes it and the drawing pass reads it through
188 /// the indirect-draw stage, which is neither of the shader stages `ReadStages`
189 /// models. Buffers carry no layout, so a transition on this class is a pure
190 /// execution + memory dependency.
191 IndirectBuffer,
192 /// Compute-written, shader-read buffer (e.g. the clustered-lighting
193 /// `cluster_light_list`). Like `IndirectBuffer` it has no layout; its read side
194 /// follows the consuming stage union, so it reads as a shader resource.
195 StorageBuffer,
196 /// Buffer both written and read through an unordered-access view (e.g. the
197 /// two-pass cull's `cull_status`, which phase 1 writes and phase 2 reads with
198 /// the same binding). Distinct from `StorageBuffer` because its read is not a
199 /// shader-resource read: on DirectX it never leaves `UNORDERED_ACCESS`, so its
200 /// ordering comes from a UAV barrier rather than a state transition.
201 UnorderedBuffer,
202}
203
204impl GraphResourceClass {
205 /// The class a texture of `usage` belongs to. Declared usage is the single
206 /// source of truth for it: a backend resolves a resource label to its GPU
207 /// object, but never restates what kind of resource it is, so the two
208 /// executors cannot disagree about (say) whether the shadow map is a depth
209 /// target. Depth-stencil wins over storage wins over render target, since a
210 /// target declaring several is used in the most constrained of them.
211 pub(crate) const fn for_texture_usage(usage: TextureUsage) -> Self {
212 if usage.contains(TextureUsage::DEPTH_STENCIL) {
213 GraphResourceClass::DepthTarget
214 } else if usage.contains(TextureUsage::STORAGE) {
215 GraphResourceClass::StorageImage
216 } else {
217 GraphResourceClass::ColorTarget
218 }
219 }
220
221 /// The class a buffer of `usage` belongs to, on the same declared-usage rule
222 /// as [`Self::for_texture_usage`]. Indirect arguments win over the read-write
223 /// binding, since a buffer declaring both is consumed as draw arguments.
224 pub(crate) const fn for_buffer_usage(usage: BufferUsage) -> Self {
225 if usage.contains(BufferUsage::INDIRECT) {
226 GraphResourceClass::IndirectBuffer
227 } else if usage.contains(BufferUsage::UNORDERED) {
228 GraphResourceClass::UnorderedBuffer
229 } else {
230 GraphResourceClass::StorageBuffer
231 }
232 }
233
234 /// Whether this class names a buffer rather than an image. Buffers have no
235 /// layout, so a backend emits a buffer / global memory barrier for them and an
236 /// image barrier for everything else.
237 pub const fn is_buffer(self) -> bool {
238 matches!(
239 self,
240 GraphResourceClass::IndirectBuffer
241 | GraphResourceClass::StorageBuffer
242 | GraphResourceClass::UnorderedBuffer
243 )
244 }
245}
246
247/// Which shader stage(s) read a graph resource across a contiguous read-run
248/// (the passes that read one resource version before the next writer). Carried
249/// on a barrier whose Read side spans this run so a backend can satisfy it in a
250/// single transition: a write made visible to both a compute consumer and a
251/// fragment consumer needs one barrier covering both stages, not a per-consumer
252/// read-to-read barrier (which would not carry the producing write). Derived
253/// from each reading pass's `PassKind` (a render pass samples in the fragment
254/// stage, a compute pass in the compute stage); empty on a barrier with no Read
255/// side (a write-only producer transition). Add bits as passes read in stages
256/// the two current ones do not model.
257#[derive(Copy, Clone, Debug, Eq, PartialEq)]
258pub struct ReadStages(u32);
259
260flag_set_ops!(ReadStages, "stage");
261
262impl ReadStages {
263 /// A render pass's sampled read (DirectX PIXEL_SHADER_RESOURCE / Vulkan
264 /// FRAGMENT_SHADER stage).
265 pub const FRAGMENT: Self = Self(1 << 0);
266 /// A compute pass's read (DirectX NON_PIXEL_SHADER_RESOURCE / Vulkan
267 /// COMPUTE_SHADER stage).
268 pub const COMPUTE: Self = Self(1 << 1);
269
270 /// Whether no stage is set.
271 pub const fn is_empty(self) -> bool {
272 self.0 == 0
273 }
274
275 /// The stage a pass of `kind` reads a resource in: render passes sample in
276 /// the fragment stage, compute passes in the compute stage. This is the one
277 /// place a `PassKind` becomes a read stage, so the approximation lives here:
278 /// a render pass that sampled in the vertex / geometry stage would be
279 /// labelled FRAGMENT. No graph-driven resource is read that way today; if
280 /// one ever is, carry an explicit per-read stage instead of deriving it.
281 pub(crate) const fn for_pass_kind(kind: PassKind) -> Self {
282 match kind {
283 PassKind::Render => Self::FRAGMENT,
284 PassKind::Compute => Self::COMPUTE,
285 }
286 }
287}
288
289/// One barrier the executor must insert before a pass runs. Per-backend
290/// interpretation: Vulkan emits `vkCmdPipelineBarrier`; DirectX emits
291/// `D3D12_RESOURCE_BARRIER`; Metal mostly ignores them (implicit hazard
292/// tracking) but may translate `from: Write, to: Read` on the cull ICB
293/// path into an explicit `useResource(.Write)` declaration.
294#[derive(Copy, Clone, Debug, Eq, PartialEq)]
295pub struct BarrierOp {
296 pub(super) resource: ResourceId,
297 pub(super) from: ResourceState,
298 pub(super) to: ResourceState,
299 // Stage union of this barrier's Read side (see `ReadStages`): the consuming
300 // run's stages for a `* -> Read` transition, the prior run's stages for a
301 // `Read -> Write` (WAR), empty when neither side is Read. The backend
302 // translator targets this union so one transition covers every consuming
303 // stage.
304 pub(super) read_stages: ReadStages,
305}
306
307impl BarrierOp {
308 /// Pass-local accessors so callers don't have to import `ResourceId`.
309 /// Returns the resource's stable index, the same value the executor
310 /// uses to look the resource up in `CompiledGraph.resources`.
311 pub fn resource_index(self) -> usize {
312 self.resource.index()
313 }
314 /// Accessor for the transition's source state, paired with `to_state`.
315 pub fn source_state(self) -> ResourceState {
316 self.from
317 }
318 /// The transition's destination state.
319 pub fn to_state(self) -> ResourceState {
320 self.to
321 }
322 /// Stage union of this barrier's Read side: the consuming run's stages for a
323 /// `* -> Read` transition (the backend must make the producing write visible
324 /// to all of them), the prior run's stages for a `Read -> Write` (WAR).
325 /// Empty when neither side is Read.
326 pub fn read_stages(self) -> ReadStages {
327 self.read_stages
328 }
329}
330
331// Inclusive `[first, last]` range over pass indices in the compiled
332// graph's `passes` Vec. Used to describe a transient resource's
333// lifetime so an aliaser can overlap non-overlapping lifetimes.
334#[derive(Copy, Clone, Debug, Eq, PartialEq)]
335pub struct PassRange {
336 pub first: usize,
337 pub last: usize,
338}
339
340/// Texture-shape description carried by both imported and transient
341/// resources. This is the authoritative shape: the aliaser sizes a
342/// resource from it and each backend's transient pool translates it into
343/// a native descriptor, so a desc that disagrees with what the backend
344/// would have created is a defect rather than a documentation slip.
345#[derive(Copy, Clone, Debug, PartialEq)]
346pub struct TextureDesc {
347 /// Width in pixels, or a fraction of the drawable.
348 pub width: TextureSize,
349 /// Height in pixels, or a fraction of the drawable.
350 pub height: TextureSize,
351 /// Depth extent of a 3D texture, 1 for 2D and 2D-array textures. Distinct
352 /// from `array_layers`: 3D slices mip down with the other two axes, array
353 /// layers do not.
354 pub depth: u32,
355 /// Texel format.
356 pub format: PixelFormat,
357 /// MSAA sample count, 1 for non-multisample. The graph doesn't care
358 /// what value this is; the backend executor maps it to its API's
359 /// sample-count enum.
360 pub sample_count: u32,
361 /// Number of array layers. 1 for plain 2D, 6 for cube, N for CSM
362 /// shadow-map arrays.
363 pub array_layers: u32,
364 /// Mip levels in the chain, 1 for a single-level target. A resource whose
365 /// consumers sample coarser levels (the Hi-Z pyramid, a bloom octave chain)
366 /// carries its real count, since the levels past 0 are a third of the
367 /// footprint the aliaser packs.
368 pub mip_levels: u32,
369 /// How passes bind the texture.
370 pub usage: TextureUsage,
371 /// The value this target is cleared to at the head of the pass that writes
372 /// it. Part of the shape rather than the encoder's business because D3D12
373 /// bakes an *optimized* clear value into the resource at creation: a placed
374 /// resource created with one value and cleared to another is both a
375 /// debug-layer warning and a real decompression cost, and the pool creates
376 /// the resource while the feature owns the clear. A target whose background
377 /// means something (roughness clears to 1.0 = fully rough, so untouched
378 /// pixels reflect nothing) reads wrong on its first frame if these disagree.
379 pub clear: ClearValue,
380}
381
382/// What a target's clear resolves to. Split by kind rather than carried as four
383/// floats so a depth target cannot silently be given a colour.
384#[derive(Copy, Clone, Debug, PartialEq)]
385pub enum ClearValue {
386 /// Colour clear value, linear RGBA.
387 Color([f32; 4]),
388 /// Depth clear value; stencil is always 0 (no engine target has stencil).
389 Depth(f32),
390}
391
392// Buffer-shape description. Size is optional because some
393// imported buffers grow dynamically per-frame (the GPU object data
394// buffer, the per-emitter spawn ring, ...): the graph then just
395// tracks the dependency, not the size.
396#[derive(Copy, Clone, Debug, PartialEq)]
397pub(crate) struct BufferDesc {
398 pub size_bytes: Option<NonZeroU32>,
399 pub usage: BufferUsage,
400}
401
402// How a texture is sized. Two non-absolute variants let bloom mips and
403// full-resolution targets express their size without the graph needing
404// to know the swapchain dimensions at declaration time.
405#[derive(Copy, Clone, Debug, PartialEq)]
406pub enum TextureSize {
407 // Fixed pixel count. CSM shadow-map slices use this
408 // (`Absolute(2048)`); the rest of the engine's targets follow the
409 // drawable.
410 Absolute(u32),
411 // Tracks the swapchain drawable's width or height.
412 Drawable,
413 // Scaled fraction of the drawable, floored to >= 1 by the executor.
414 // Bloom mips chain through this (`DrawableScaled(0.5)^n`).
415 DrawableScaled(f32),
416}
417
418/// Backend-agnostic pixel format. Maps to `MTLPixelFormat` /
419/// `vk::Format` / `DXGI_FORMAT` per executor. Only the formats the
420/// engine actually uses are enumerated; extend as new passes need new
421/// targets.
422#[derive(Copy, Clone, Debug, PartialEq, Eq)]
423pub enum PixelFormat {
424 /// 16-bit float RGBA, the HDR working format.
425 Rgba16Float,
426 /// 8-bit unorm RGBA.
427 Rgba8Unorm,
428 /// 16-bit float RG.
429 Rg16Float,
430 /// 8-bit unorm single channel.
431 R8Unorm,
432 /// 32-bit float single channel.
433 R32Float,
434 /// 32-bit float depth.
435 Depth32Float,
436 /// Whatever format the swapchain presents.
437 BgraSwapchain,
438}
439
440impl PixelFormat {
441 /// Bytes per texel (per sample). Used by the aliasing planner to size a
442 /// resource's memory footprint. The engine uses only single-plane,
443 /// power-of-two formats, so this is one byte count per variant.
444 pub(crate) const fn bytes_per_texel(self) -> u32 {
445 match self {
446 PixelFormat::Rgba16Float => 8,
447 PixelFormat::Rgba8Unorm
448 | PixelFormat::Rg16Float
449 | PixelFormat::R32Float
450 | PixelFormat::Depth32Float
451 | PixelFormat::BgraSwapchain => 4,
452 PixelFormat::R8Unorm => 1,
453 }
454 }
455
456 /// Whether this is a depth format. The aliasing planner keeps depth and
457 /// colour resources in separate memory pools because their backend memory
458 /// requirements (heap flags / memory type) differ; the finer per-usage
459 /// compatibility is the backend's concern when it realises the plan.
460 pub const fn is_depth(self) -> bool {
461 matches!(self, PixelFormat::Depth32Float)
462 }
463}
464
465impl TextureSize {
466 // Resolve to a concrete pixel count against the current drawable extent.
467 // `DrawableScaled` floors to >= 1 so a mip-scaled target never degenerates
468 // to zero.
469 pub fn resolve(self, drawable: u32) -> u32 {
470 match self {
471 TextureSize::Absolute(n) => n.max(1),
472 TextureSize::Drawable => drawable.max(1),
473 TextureSize::DrawableScaled(f) => (floor(drawable as f32 * f) as u32).max(1),
474 }
475 }
476}
477
478// Levels in a full mip chain for a target of `width` x `height`:
479// `floor(log2(max(w, h))) + 1`. Power-of-two sources end exactly at 1x1;
480// non-power-of-two sources stop one level short of 1x1 on the smaller axis,
481// which is what each backend's Hi-Z build already does.
482pub(crate) const fn full_mip_levels(width: u32, height: u32) -> u32 {
483 let m = if width > height { width } else { height };
484 let m = if m < 1 { 1 } else { m };
485 32 - m.leading_zeros()
486}
487
488impl TextureDesc {
489 /// A single-sample, single-mip, single-layer 2D texture: the shape almost
490 /// every graph target has. The axes that differ are added by the `with_*`
491 /// methods below, so a desc that names one is saying something.
492 pub(crate) const fn texture_2d(
493 width: TextureSize,
494 height: TextureSize,
495 format: PixelFormat,
496 usage: TextureUsage,
497 ) -> Self {
498 Self {
499 width,
500 height,
501 depth: 1,
502 format,
503 sample_count: 1,
504 array_layers: 1,
505 mip_levels: 1,
506 usage,
507 // Zero / far, which is what every target the graph models clears to
508 // unless it says otherwise via `with_clear_color`.
509 clear: if format.is_depth() {
510 ClearValue::Depth(1.0)
511 } else {
512 ClearValue::Color([0.0; 4])
513 },
514 }
515 }
516
517 /// A single-mip 3D texture of `depth` slices (the volumetric-fog froxel
518 /// volume). Distinct from an array: the slices are a sampled third axis.
519 pub(crate) const fn volume_3d(
520 width: TextureSize,
521 height: TextureSize,
522 depth: u32,
523 format: PixelFormat,
524 usage: TextureUsage,
525 ) -> Self {
526 Self {
527 depth,
528 ..Self::texture_2d(width, height, format, usage)
529 }
530 }
531
532 /// This desc with the MSAA sample count replaced.
533 pub(crate) const fn with_sample_count(self, sample_count: u32) -> Self {
534 Self {
535 sample_count,
536 ..self
537 }
538 }
539
540 /// This desc with the array-layer count replaced.
541 pub(crate) const fn with_array_layers(self, array_layers: u32) -> Self {
542 Self {
543 array_layers,
544 ..self
545 }
546 }
547
548 /// This desc with the mip-level count replaced.
549 pub(crate) const fn with_mip_levels(self, mip_levels: u32) -> Self {
550 Self { mip_levels, ..self }
551 }
552
553 /// Override the colour a target clears to. Only for a target whose cleared
554 /// background carries meaning; see `clear`.
555 pub(crate) const fn with_clear_color(self, color: [f32; 4]) -> Self {
556 Self {
557 clear: ClearValue::Color(color),
558 ..self
559 }
560 }
561
562 /// Resolved pixel extent at the given drawable extent, as the backend must
563 /// create it: `(width, height, depth)`.
564 pub fn extent(&self, drawable_w: u32, drawable_h: u32) -> (u32, u32, u32) {
565 (
566 self.width.resolve(drawable_w),
567 self.height.resolve(drawable_h),
568 self.depth.max(1),
569 )
570 }
571
572 /// The resource's memory footprint in bytes at the given drawable extent:
573 /// the summed texel count of every mip level, times bytes-per-texel,
574 /// sample count and array layers. The aliasing planner sums and packs
575 /// these. Sample count multiplies the whole chain, which is exact for the
576 /// only shape that carries both (a multisample target is single-mip).
577 pub fn byte_size(&self, drawable_w: u32, drawable_h: u32) -> u64 {
578 let (w, h, d) = self.extent(drawable_w, drawable_h);
579 let mut texels: u64 = 0;
580 for level in 0..self.mip_levels.max(1) {
581 let at = |n: u32| n.checked_shr(level).unwrap_or(0).max(1) as u64;
582 texels += at(w) * at(h) * at(d);
583 }
584 texels
585 * self.format.bytes_per_texel() as u64
586 * self.sample_count.max(1) as u64
587 * self.array_layers.max(1) as u64
588 }
589}
590
591/// Bitset describing how a texture can be used. The graph doesn't
592/// enforce these against declared reads / writes (executors do);
593/// the field exists so the aliaser can match transient resources to
594/// pool entries with the right usage flags.
595#[derive(Copy, Clone, Debug, PartialEq, Eq)]
596pub struct TextureUsage(pub u32);
597
598impl TextureUsage {
599 /// Sampled or otherwise read from a shader.
600 pub const SHADER_READ: Self = Self(1 << 0);
601 /// Bound as a colour render target.
602 pub const RENDER_TARGET: Self = Self(1 << 1);
603 /// Bound as a depth / stencil target.
604 pub const DEPTH_STENCIL: Self = Self(1 << 2);
605 /// Bound for read-write shader storage.
606 pub const STORAGE: Self = Self(1 << 3);
607 /// Source of a copy.
608 pub const TRANSFER_SRC: Self = Self(1 << 4);
609 /// Destination of a copy.
610 pub const TRANSFER_DST: Self = Self(1 << 5);
611}
612
613flag_set_ops!(TextureUsage, "usage");
614
615/// Buffer-side counterpart to [`TextureUsage`]. Same bitset shape.
616#[derive(Copy, Clone, Debug, PartialEq, Eq)]
617pub struct BufferUsage(pub u32);
618
619impl BufferUsage {
620 /// Bound for read-write shader storage.
621 pub const STORAGE: Self = Self(1 << 1);
622 /// Read as indirect draw / dispatch arguments.
623 pub(crate) const INDIRECT: Self = Self(1 << 4);
624 /// Consumers access this buffer through the same read-write binding the
625 /// producer wrote through, rather than a read-only view of it. It therefore
626 /// never transitions to a read state, and ordering between the producer and
627 /// the consumer comes from an execution barrier instead of a state change.
628 /// The two-pass cull's `cull_status` is the case: both phases bind it the
629 /// same way.
630 pub(crate) const UNORDERED: Self = Self(1 << 7);
631}
632
633flag_set_ops!(BufferUsage, "usage");
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
640 fn invalid_handle_is_invalid() {
641 assert!(!TextureHandle::INVALID.is_valid());
642 assert!(!BufferHandle::INVALID.is_valid());
643 }
644
645 #[test]
646 fn texture_usage_bitset_round_trips() {
647 let u = TextureUsage::SHADER_READ | TextureUsage::RENDER_TARGET;
648 assert!(u.contains(TextureUsage::SHADER_READ));
649 assert!(u.contains(TextureUsage::RENDER_TARGET));
650 assert!(!u.contains(TextureUsage::STORAGE));
651 assert_eq!(
652 u.union(TextureUsage::STORAGE).0,
653 TextureUsage::SHADER_READ.0 | TextureUsage::RENDER_TARGET.0 | TextureUsage::STORAGE.0
654 );
655 }
656
657 #[test]
658 fn pass_range_is_inclusive() {
659 let r = PassRange { first: 2, last: 5 };
660 assert_eq!(r.first, 2);
661 assert_eq!(r.last, 5);
662 }
663
664 #[test]
665 fn pixel_format_texel_size_and_depth() {
666 assert_eq!(PixelFormat::Rgba16Float.bytes_per_texel(), 8);
667 assert_eq!(PixelFormat::Rgba8Unorm.bytes_per_texel(), 4);
668 assert_eq!(PixelFormat::Rg16Float.bytes_per_texel(), 4);
669 assert_eq!(PixelFormat::R32Float.bytes_per_texel(), 4);
670 assert_eq!(PixelFormat::Depth32Float.bytes_per_texel(), 4);
671 assert_eq!(PixelFormat::BgraSwapchain.bytes_per_texel(), 4);
672 assert_eq!(PixelFormat::R8Unorm.bytes_per_texel(), 1);
673 assert!(PixelFormat::Depth32Float.is_depth());
674 assert!(!PixelFormat::Rgba8Unorm.is_depth());
675 assert!(!PixelFormat::R8Unorm.is_depth());
676 }
677
678 #[test]
679 fn texture_size_resolves_and_floors_to_one() {
680 assert_eq!(TextureSize::Absolute(2048).resolve(720), 2048);
681 assert_eq!(TextureSize::Absolute(0).resolve(720), 1);
682 assert_eq!(TextureSize::Drawable.resolve(720), 720);
683 assert_eq!(TextureSize::Drawable.resolve(0), 1);
684 assert_eq!(TextureSize::DrawableScaled(0.5).resolve(720), 360);
685 // floor(1 * 0.5) = 0, floored back up to 1 so a mip never degenerates.
686 assert_eq!(TextureSize::DrawableScaled(0.5).resolve(1), 1);
687 }
688
689 #[test]
690 fn texture_desc_byte_size_multiplies_every_factor() {
691 let base = TextureDesc::texture_2d(
692 TextureSize::Drawable,
693 TextureSize::Drawable,
694 PixelFormat::Rgba16Float, // 8 bytes / texel
695 TextureUsage::RENDER_TARGET,
696 );
697 assert_eq!(base.byte_size(4, 2), 4 * 2 * 8);
698 // Sample count and array layers both multiply in.
699 let multi = base.with_sample_count(4).with_array_layers(6);
700 assert_eq!(multi.byte_size(4, 2), 4 * 2 * 8 * 4 * 6);
701 // A zero sample count / layer count clamps to 1 rather than zeroing.
702 let degenerate = TextureDesc {
703 sample_count: 0,
704 array_layers: 0,
705 mip_levels: 0,
706 ..base
707 };
708 assert_eq!(degenerate.byte_size(4, 2), 4 * 2 * 8);
709 }
710
711 #[test]
712 fn byte_size_sums_the_mip_chain() {
713 // A 3-level chain over 8x8: 64 + 16 + 4 texels, not 64. Under-counting
714 // the tail is what would let the aliaser undersize a shared slot.
715 let chain = TextureDesc::texture_2d(
716 TextureSize::Drawable,
717 TextureSize::Drawable,
718 PixelFormat::R8Unorm,
719 TextureUsage::SHADER_READ,
720 )
721 .with_mip_levels(3);
722 assert_eq!(chain.byte_size(8, 8), 64 + 16 + 4);
723 // Levels floor at one texel rather than vanishing, so an over-long
724 // chain keeps adding 1 instead of 0.
725 let over_long = chain.with_mip_levels(6);
726 assert_eq!(over_long.byte_size(8, 8), 64 + 16 + 4 + 1 + 1 + 1);
727 }
728
729 #[test]
730 fn byte_size_counts_volume_slices_and_mips_them() {
731 // 3D depth multiplies like the other two axes, and mips down with
732 // them -- which is what separates it from `array_layers`.
733 let volume = TextureDesc::volume_3d(
734 TextureSize::Drawable,
735 TextureSize::Drawable,
736 4,
737 PixelFormat::R8Unorm,
738 TextureUsage::STORAGE,
739 );
740 assert_eq!(volume.byte_size(8, 8), 8 * 8 * 4);
741 assert_eq!(
742 volume.with_mip_levels(2).byte_size(8, 8),
743 8 * 8 * 4 + 4 * 4 * 2
744 );
745 // An array of the same nominal size does not shrink its layer count.
746 let array = TextureDesc::texture_2d(
747 TextureSize::Drawable,
748 TextureSize::Drawable,
749 PixelFormat::R8Unorm,
750 TextureUsage::STORAGE,
751 )
752 .with_array_layers(4)
753 .with_mip_levels(2);
754 assert_eq!(array.byte_size(8, 8), (8 * 8 + 4 * 4) * 4);
755 }
756
757 #[test]
758 fn constructors_default_the_uninteresting_axes() {
759 let d = TextureDesc::texture_2d(
760 TextureSize::Drawable,
761 TextureSize::Absolute(7),
762 PixelFormat::Rgba8Unorm,
763 TextureUsage::SHADER_READ,
764 );
765 assert_eq!(
766 (d.depth, d.sample_count, d.array_layers, d.mip_levels),
767 (1, 1, 1, 1)
768 );
769 assert_eq!(d.extent(3, 99), (3, 7, 1));
770 let v = TextureDesc::volume_3d(
771 TextureSize::Absolute(2),
772 TextureSize::Absolute(3),
773 5,
774 PixelFormat::Rgba8Unorm,
775 TextureUsage::STORAGE,
776 );
777 assert_eq!(v.extent(0, 0), (2, 3, 5));
778 assert_eq!(v.array_layers, 1, "a volume is not an array");
779 }
780
781 #[test]
782 fn full_mip_levels_matches_the_backends_hiz_chain() {
783 // Mirrors each backend's `hiz_mip_count`: floor(log2(max)) + 1.
784 assert_eq!(full_mip_levels(1, 1), 1);
785 assert_eq!(full_mip_levels(2, 1), 2);
786 assert_eq!(full_mip_levels(1920, 1080), 11);
787 assert_eq!(full_mip_levels(1024, 1024), 11);
788 // Zero on either axis still yields a one-level chain.
789 assert_eq!(full_mip_levels(0, 0), 1);
790 }
791
792 #[test]
793 fn read_stages_bitset_and_pass_kind() {
794 let both = ReadStages::FRAGMENT | ReadStages::COMPUTE;
795 assert!(both.contains(ReadStages::FRAGMENT));
796 assert!(both.contains(ReadStages::COMPUTE));
797 assert!(!both.is_empty());
798 assert!(ReadStages::empty().is_empty());
799 assert!(!ReadStages::empty().contains(ReadStages::FRAGMENT));
800 // union with empty is the identity.
801 assert_eq!(both.union(ReadStages::empty()), both);
802 // A render pass reads in the fragment stage, a compute pass in compute.
803 assert_eq!(
804 ReadStages::for_pass_kind(PassKind::Render),
805 ReadStages::FRAGMENT
806 );
807 assert_eq!(
808 ReadStages::for_pass_kind(PassKind::Compute),
809 ReadStages::COMPUTE
810 );
811 }
812
813 #[test]
814 fn class_follows_declared_usage() {
815 // Declared usage is the single source of truth for a resource's barrier
816 // class, so the precedence between overlapping bits is pinned here rather
817 // than restated by each backend.
818 let tex = |usage| GraphResourceClass::for_texture_usage(usage);
819 assert_eq!(
820 tex(TextureUsage::RENDER_TARGET | TextureUsage::SHADER_READ),
821 GraphResourceClass::ColorTarget
822 );
823 assert_eq!(
824 tex(TextureUsage::DEPTH_STENCIL | TextureUsage::SHADER_READ),
825 GraphResourceClass::DepthTarget
826 );
827 assert_eq!(
828 tex(TextureUsage::STORAGE | TextureUsage::SHADER_READ),
829 GraphResourceClass::StorageImage
830 );
831 // Depth wins over storage, storage over render target: a target declaring
832 // several is used in the most constrained of them.
833 assert_eq!(
834 tex(TextureUsage::DEPTH_STENCIL | TextureUsage::STORAGE | TextureUsage::RENDER_TARGET),
835 GraphResourceClass::DepthTarget
836 );
837 assert_eq!(
838 tex(TextureUsage::STORAGE | TextureUsage::RENDER_TARGET),
839 GraphResourceClass::StorageImage
840 );
841
842 let buf = |usage| GraphResourceClass::for_buffer_usage(usage);
843 assert_eq!(buf(BufferUsage::STORAGE), GraphResourceClass::StorageBuffer);
844 assert_eq!(
845 buf(BufferUsage::STORAGE | BufferUsage::UNORDERED),
846 GraphResourceClass::UnorderedBuffer
847 );
848 assert_eq!(
849 buf(BufferUsage::STORAGE | BufferUsage::INDIRECT),
850 GraphResourceClass::IndirectBuffer
851 );
852 // Indirect wins over the read-write binding: a buffer declaring both is
853 // consumed as draw arguments.
854 assert_eq!(
855 buf(BufferUsage::INDIRECT | BufferUsage::UNORDERED),
856 GraphResourceClass::IndirectBuffer
857 );
858 // Every buffer class reports as a buffer; no image class does.
859 for c in [
860 GraphResourceClass::IndirectBuffer,
861 GraphResourceClass::StorageBuffer,
862 GraphResourceClass::UnorderedBuffer,
863 ] {
864 assert!(c.is_buffer(), "{c:?}");
865 }
866 for c in [
867 GraphResourceClass::ColorTarget,
868 GraphResourceClass::DepthTarget,
869 GraphResourceClass::StorageImage,
870 ] {
871 assert!(!c.is_buffer(), "{c:?}");
872 }
873 }
874
875 #[test]
876 fn buffer_usage_bitset_ops() {
877 let u = BufferUsage::STORAGE | BufferUsage::INDIRECT;
878 assert!(u.contains(BufferUsage::STORAGE));
879 assert!(u.contains(BufferUsage::INDIRECT));
880 assert!(!u.contains(BufferUsage::UNORDERED));
881 assert_eq!(BufferUsage::empty().0, 0);
882 assert_eq!(
883 u.union(BufferUsage::UNORDERED).0,
884 BufferUsage::STORAGE.0 | BufferUsage::INDIRECT.0 | BufferUsage::UNORDERED.0
885 );
886 }
887
888 #[test]
889 fn barrier_op_accessors_expose_the_transition() {
890 let op = BarrierOp {
891 resource: ResourceId(4),
892 from: ResourceState::Read,
893 to: ResourceState::Write,
894 read_stages: ReadStages::FRAGMENT,
895 };
896 assert_eq!(op.resource_index(), 4);
897 assert_eq!(op.source_state(), ResourceState::Read);
898 assert_eq!(op.to_state(), ResourceState::Write);
899 assert_eq!(op.read_stages(), ReadStages::FRAGMENT);
900 }
901}