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}