use concinnity_core::math::floor;
use core::num::NonZeroU32;
macro_rules! flag_set_ops {
($ty:ident, $noun:literal) => {
impl $ty {
#[doc = concat!("The empty ", $noun, " set.")]
pub const fn empty() -> Self {
Self(0)
}
#[doc = concat!("Every ", $noun, " in either set.")]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[doc = concat!("Whether every ", $noun, " in `other` is set here.")]
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
}
impl core::ops::BitOr for $ty {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
};
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct TextureHandle {
pub(super) resource: ResourceId,
pub(super) version: u32,
}
impl TextureHandle {
pub(crate) const INVALID: Self = Self {
resource: ResourceId::INVALID,
version: 0,
};
pub(crate) fn is_valid(self) -> bool {
self.resource.is_valid()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct BufferHandle {
pub(super) resource: ResourceId,
pub(super) version: u32,
}
impl BufferHandle {
pub(crate) const INVALID: Self = Self {
resource: ResourceId::INVALID,
version: 0,
};
pub(crate) fn is_valid(self) -> bool {
self.resource.is_valid()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct ResourceId(pub(super) u32);
impl ResourceId {
pub(crate) const INVALID: Self = Self(u32::MAX);
pub(crate) fn is_valid(self) -> bool {
self.0 != u32::MAX
}
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PassKind {
Render,
Compute,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ResourceOrigin {
Imported,
Transient,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ResourceState {
Undefined,
Read,
Write,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum GraphResourceClass {
ColorTarget,
DepthTarget,
StorageImage,
IndirectBuffer,
StorageBuffer,
UnorderedBuffer,
}
impl GraphResourceClass {
pub(crate) const fn for_texture_usage(usage: TextureUsage) -> Self {
if usage.contains(TextureUsage::DEPTH_STENCIL) {
GraphResourceClass::DepthTarget
} else if usage.contains(TextureUsage::STORAGE) {
GraphResourceClass::StorageImage
} else {
GraphResourceClass::ColorTarget
}
}
pub(crate) const fn for_buffer_usage(usage: BufferUsage) -> Self {
if usage.contains(BufferUsage::INDIRECT) {
GraphResourceClass::IndirectBuffer
} else if usage.contains(BufferUsage::UNORDERED) {
GraphResourceClass::UnorderedBuffer
} else {
GraphResourceClass::StorageBuffer
}
}
pub const fn is_buffer(self) -> bool {
matches!(
self,
GraphResourceClass::IndirectBuffer
| GraphResourceClass::StorageBuffer
| GraphResourceClass::UnorderedBuffer
)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ReadStages(u32);
flag_set_ops!(ReadStages, "stage");
impl ReadStages {
pub const FRAGMENT: Self = Self(1 << 0);
pub const COMPUTE: Self = Self(1 << 1);
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub(crate) const fn for_pass_kind(kind: PassKind) -> Self {
match kind {
PassKind::Render => Self::FRAGMENT,
PassKind::Compute => Self::COMPUTE,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct BarrierOp {
pub(super) resource: ResourceId,
pub(super) from: ResourceState,
pub(super) to: ResourceState,
pub(super) read_stages: ReadStages,
}
impl BarrierOp {
pub fn resource_index(self) -> usize {
self.resource.index()
}
pub fn source_state(self) -> ResourceState {
self.from
}
pub fn to_state(self) -> ResourceState {
self.to
}
pub fn read_stages(self) -> ReadStages {
self.read_stages
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct PassRange {
pub first: usize,
pub last: usize,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TextureDesc {
pub width: TextureSize,
pub height: TextureSize,
pub depth: u32,
pub format: PixelFormat,
pub sample_count: u32,
pub array_layers: u32,
pub mip_levels: u32,
pub usage: TextureUsage,
pub clear: ClearValue,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ClearValue {
Color([f32; 4]),
Depth(f32),
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) struct BufferDesc {
pub size_bytes: Option<NonZeroU32>,
pub usage: BufferUsage,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TextureSize {
Absolute(u32),
Drawable,
DrawableScaled(f32),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PixelFormat {
Rgba16Float,
Rgba8Unorm,
Rg16Float,
R8Unorm,
R32Float,
Depth32Float,
BgraSwapchain,
}
impl PixelFormat {
pub(crate) const fn bytes_per_texel(self) -> u32 {
match self {
PixelFormat::Rgba16Float => 8,
PixelFormat::Rgba8Unorm
| PixelFormat::Rg16Float
| PixelFormat::R32Float
| PixelFormat::Depth32Float
| PixelFormat::BgraSwapchain => 4,
PixelFormat::R8Unorm => 1,
}
}
pub const fn is_depth(self) -> bool {
matches!(self, PixelFormat::Depth32Float)
}
}
impl TextureSize {
pub fn resolve(self, drawable: u32) -> u32 {
match self {
TextureSize::Absolute(n) => n.max(1),
TextureSize::Drawable => drawable.max(1),
TextureSize::DrawableScaled(f) => (floor(drawable as f32 * f) as u32).max(1),
}
}
}
pub(crate) const fn full_mip_levels(width: u32, height: u32) -> u32 {
let m = if width > height { width } else { height };
let m = if m < 1 { 1 } else { m };
32 - m.leading_zeros()
}
impl TextureDesc {
pub(crate) const fn texture_2d(
width: TextureSize,
height: TextureSize,
format: PixelFormat,
usage: TextureUsage,
) -> Self {
Self {
width,
height,
depth: 1,
format,
sample_count: 1,
array_layers: 1,
mip_levels: 1,
usage,
clear: if format.is_depth() {
ClearValue::Depth(1.0)
} else {
ClearValue::Color([0.0; 4])
},
}
}
pub(crate) const fn volume_3d(
width: TextureSize,
height: TextureSize,
depth: u32,
format: PixelFormat,
usage: TextureUsage,
) -> Self {
Self {
depth,
..Self::texture_2d(width, height, format, usage)
}
}
pub(crate) const fn with_sample_count(self, sample_count: u32) -> Self {
Self {
sample_count,
..self
}
}
pub(crate) const fn with_array_layers(self, array_layers: u32) -> Self {
Self {
array_layers,
..self
}
}
pub(crate) const fn with_mip_levels(self, mip_levels: u32) -> Self {
Self { mip_levels, ..self }
}
pub(crate) const fn with_clear_color(self, color: [f32; 4]) -> Self {
Self {
clear: ClearValue::Color(color),
..self
}
}
pub fn extent(&self, drawable_w: u32, drawable_h: u32) -> (u32, u32, u32) {
(
self.width.resolve(drawable_w),
self.height.resolve(drawable_h),
self.depth.max(1),
)
}
pub fn byte_size(&self, drawable_w: u32, drawable_h: u32) -> u64 {
let (w, h, d) = self.extent(drawable_w, drawable_h);
let mut texels: u64 = 0;
for level in 0..self.mip_levels.max(1) {
let at = |n: u32| n.checked_shr(level).unwrap_or(0).max(1) as u64;
texels += at(w) * at(h) * at(d);
}
texels
* self.format.bytes_per_texel() as u64
* self.sample_count.max(1) as u64
* self.array_layers.max(1) as u64
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct TextureUsage(pub u32);
impl TextureUsage {
pub const SHADER_READ: Self = Self(1 << 0);
pub const RENDER_TARGET: Self = Self(1 << 1);
pub const DEPTH_STENCIL: Self = Self(1 << 2);
pub const STORAGE: Self = Self(1 << 3);
pub const TRANSFER_SRC: Self = Self(1 << 4);
pub const TRANSFER_DST: Self = Self(1 << 5);
}
flag_set_ops!(TextureUsage, "usage");
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct BufferUsage(pub u32);
impl BufferUsage {
pub const STORAGE: Self = Self(1 << 1);
pub(crate) const INDIRECT: Self = Self(1 << 4);
pub(crate) const UNORDERED: Self = Self(1 << 7);
}
flag_set_ops!(BufferUsage, "usage");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_handle_is_invalid() {
assert!(!TextureHandle::INVALID.is_valid());
assert!(!BufferHandle::INVALID.is_valid());
}
#[test]
fn texture_usage_bitset_round_trips() {
let u = TextureUsage::SHADER_READ | TextureUsage::RENDER_TARGET;
assert!(u.contains(TextureUsage::SHADER_READ));
assert!(u.contains(TextureUsage::RENDER_TARGET));
assert!(!u.contains(TextureUsage::STORAGE));
assert_eq!(
u.union(TextureUsage::STORAGE).0,
TextureUsage::SHADER_READ.0 | TextureUsage::RENDER_TARGET.0 | TextureUsage::STORAGE.0
);
}
#[test]
fn pass_range_is_inclusive() {
let r = PassRange { first: 2, last: 5 };
assert_eq!(r.first, 2);
assert_eq!(r.last, 5);
}
#[test]
fn pixel_format_texel_size_and_depth() {
assert_eq!(PixelFormat::Rgba16Float.bytes_per_texel(), 8);
assert_eq!(PixelFormat::Rgba8Unorm.bytes_per_texel(), 4);
assert_eq!(PixelFormat::Rg16Float.bytes_per_texel(), 4);
assert_eq!(PixelFormat::R32Float.bytes_per_texel(), 4);
assert_eq!(PixelFormat::Depth32Float.bytes_per_texel(), 4);
assert_eq!(PixelFormat::BgraSwapchain.bytes_per_texel(), 4);
assert_eq!(PixelFormat::R8Unorm.bytes_per_texel(), 1);
assert!(PixelFormat::Depth32Float.is_depth());
assert!(!PixelFormat::Rgba8Unorm.is_depth());
assert!(!PixelFormat::R8Unorm.is_depth());
}
#[test]
fn texture_size_resolves_and_floors_to_one() {
assert_eq!(TextureSize::Absolute(2048).resolve(720), 2048);
assert_eq!(TextureSize::Absolute(0).resolve(720), 1);
assert_eq!(TextureSize::Drawable.resolve(720), 720);
assert_eq!(TextureSize::Drawable.resolve(0), 1);
assert_eq!(TextureSize::DrawableScaled(0.5).resolve(720), 360);
assert_eq!(TextureSize::DrawableScaled(0.5).resolve(1), 1);
}
#[test]
fn texture_desc_byte_size_multiplies_every_factor() {
let base = TextureDesc::texture_2d(
TextureSize::Drawable,
TextureSize::Drawable,
PixelFormat::Rgba16Float, TextureUsage::RENDER_TARGET,
);
assert_eq!(base.byte_size(4, 2), 4 * 2 * 8);
let multi = base.with_sample_count(4).with_array_layers(6);
assert_eq!(multi.byte_size(4, 2), 4 * 2 * 8 * 4 * 6);
let degenerate = TextureDesc {
sample_count: 0,
array_layers: 0,
mip_levels: 0,
..base
};
assert_eq!(degenerate.byte_size(4, 2), 4 * 2 * 8);
}
#[test]
fn byte_size_sums_the_mip_chain() {
let chain = TextureDesc::texture_2d(
TextureSize::Drawable,
TextureSize::Drawable,
PixelFormat::R8Unorm,
TextureUsage::SHADER_READ,
)
.with_mip_levels(3);
assert_eq!(chain.byte_size(8, 8), 64 + 16 + 4);
let over_long = chain.with_mip_levels(6);
assert_eq!(over_long.byte_size(8, 8), 64 + 16 + 4 + 1 + 1 + 1);
}
#[test]
fn byte_size_counts_volume_slices_and_mips_them() {
let volume = TextureDesc::volume_3d(
TextureSize::Drawable,
TextureSize::Drawable,
4,
PixelFormat::R8Unorm,
TextureUsage::STORAGE,
);
assert_eq!(volume.byte_size(8, 8), 8 * 8 * 4);
assert_eq!(
volume.with_mip_levels(2).byte_size(8, 8),
8 * 8 * 4 + 4 * 4 * 2
);
let array = TextureDesc::texture_2d(
TextureSize::Drawable,
TextureSize::Drawable,
PixelFormat::R8Unorm,
TextureUsage::STORAGE,
)
.with_array_layers(4)
.with_mip_levels(2);
assert_eq!(array.byte_size(8, 8), (8 * 8 + 4 * 4) * 4);
}
#[test]
fn constructors_default_the_uninteresting_axes() {
let d = TextureDesc::texture_2d(
TextureSize::Drawable,
TextureSize::Absolute(7),
PixelFormat::Rgba8Unorm,
TextureUsage::SHADER_READ,
);
assert_eq!(
(d.depth, d.sample_count, d.array_layers, d.mip_levels),
(1, 1, 1, 1)
);
assert_eq!(d.extent(3, 99), (3, 7, 1));
let v = TextureDesc::volume_3d(
TextureSize::Absolute(2),
TextureSize::Absolute(3),
5,
PixelFormat::Rgba8Unorm,
TextureUsage::STORAGE,
);
assert_eq!(v.extent(0, 0), (2, 3, 5));
assert_eq!(v.array_layers, 1, "a volume is not an array");
}
#[test]
fn full_mip_levels_matches_the_backends_hiz_chain() {
assert_eq!(full_mip_levels(1, 1), 1);
assert_eq!(full_mip_levels(2, 1), 2);
assert_eq!(full_mip_levels(1920, 1080), 11);
assert_eq!(full_mip_levels(1024, 1024), 11);
assert_eq!(full_mip_levels(0, 0), 1);
}
#[test]
fn read_stages_bitset_and_pass_kind() {
let both = ReadStages::FRAGMENT | ReadStages::COMPUTE;
assert!(both.contains(ReadStages::FRAGMENT));
assert!(both.contains(ReadStages::COMPUTE));
assert!(!both.is_empty());
assert!(ReadStages::empty().is_empty());
assert!(!ReadStages::empty().contains(ReadStages::FRAGMENT));
assert_eq!(both.union(ReadStages::empty()), both);
assert_eq!(
ReadStages::for_pass_kind(PassKind::Render),
ReadStages::FRAGMENT
);
assert_eq!(
ReadStages::for_pass_kind(PassKind::Compute),
ReadStages::COMPUTE
);
}
#[test]
fn class_follows_declared_usage() {
let tex = |usage| GraphResourceClass::for_texture_usage(usage);
assert_eq!(
tex(TextureUsage::RENDER_TARGET | TextureUsage::SHADER_READ),
GraphResourceClass::ColorTarget
);
assert_eq!(
tex(TextureUsage::DEPTH_STENCIL | TextureUsage::SHADER_READ),
GraphResourceClass::DepthTarget
);
assert_eq!(
tex(TextureUsage::STORAGE | TextureUsage::SHADER_READ),
GraphResourceClass::StorageImage
);
assert_eq!(
tex(TextureUsage::DEPTH_STENCIL | TextureUsage::STORAGE | TextureUsage::RENDER_TARGET),
GraphResourceClass::DepthTarget
);
assert_eq!(
tex(TextureUsage::STORAGE | TextureUsage::RENDER_TARGET),
GraphResourceClass::StorageImage
);
let buf = |usage| GraphResourceClass::for_buffer_usage(usage);
assert_eq!(buf(BufferUsage::STORAGE), GraphResourceClass::StorageBuffer);
assert_eq!(
buf(BufferUsage::STORAGE | BufferUsage::UNORDERED),
GraphResourceClass::UnorderedBuffer
);
assert_eq!(
buf(BufferUsage::STORAGE | BufferUsage::INDIRECT),
GraphResourceClass::IndirectBuffer
);
assert_eq!(
buf(BufferUsage::INDIRECT | BufferUsage::UNORDERED),
GraphResourceClass::IndirectBuffer
);
for c in [
GraphResourceClass::IndirectBuffer,
GraphResourceClass::StorageBuffer,
GraphResourceClass::UnorderedBuffer,
] {
assert!(c.is_buffer(), "{c:?}");
}
for c in [
GraphResourceClass::ColorTarget,
GraphResourceClass::DepthTarget,
GraphResourceClass::StorageImage,
] {
assert!(!c.is_buffer(), "{c:?}");
}
}
#[test]
fn buffer_usage_bitset_ops() {
let u = BufferUsage::STORAGE | BufferUsage::INDIRECT;
assert!(u.contains(BufferUsage::STORAGE));
assert!(u.contains(BufferUsage::INDIRECT));
assert!(!u.contains(BufferUsage::UNORDERED));
assert_eq!(BufferUsage::empty().0, 0);
assert_eq!(
u.union(BufferUsage::UNORDERED).0,
BufferUsage::STORAGE.0 | BufferUsage::INDIRECT.0 | BufferUsage::UNORDERED.0
);
}
#[test]
fn barrier_op_accessors_expose_the_transition() {
let op = BarrierOp {
resource: ResourceId(4),
from: ResourceState::Read,
to: ResourceState::Write,
read_stages: ReadStages::FRAGMENT,
};
assert_eq!(op.resource_index(), 4);
assert_eq!(op.source_state(), ResourceState::Read);
assert_eq!(op.to_state(), ResourceState::Write);
assert_eq!(op.read_stages(), ReadStages::FRAGMENT);
}
}