Skip to main content

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}