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