#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TextureFormat {
R8Unorm,
RG8Unorm,
RGBA8Unorm,
RGBA8UnormSrgb,
BGRA8Unorm,
BGRA8UnormSrgb,
R8Snorm,
RG8Snorm,
RGBA8Snorm,
R8Uint,
RG8Uint,
RGBA8Uint,
R8Sint,
RG8Sint,
RGBA8Sint,
R16Unorm,
RG16Unorm,
RGBA16Unorm,
R16Float,
RG16Float,
RGBA16Float,
R16Uint,
RG16Uint,
RGBA16Uint,
R16Sint,
R32Float,
RG32Float,
RGB32Float,
RGBA32Float,
R32Uint,
RG32Uint,
RGBA32Uint,
R32Sint,
RGB10A2Unorm,
RG11B10Float,
RGB9E5Float,
Depth16Unorm,
Depth24Unorm,
Depth32Float,
Depth24UnormStencil8,
Depth32FloatStencil8,
Stencil8,
BC1RgbUnorm,
BC1RgbSrgb,
BC1RgbaUnorm,
BC1RgbaSrgb,
BC2Unorm,
BC2Srgb,
BC3Unorm,
BC3Srgb,
BC4Unorm,
BC4Snorm,
BC5Unorm,
BC5Snorm,
BC6HUfloat,
BC6HSfloat,
BC7Unorm,
BC7Srgb,
Etc2Rgb8Unorm,
Etc2Rgb8Srgb,
Etc2Rgb8A1Unorm,
Etc2Rgba8Unorm,
EacR11Unorm,
EacRG11Unorm,
Astc4x4Unorm,
Astc4x4Srgb,
Astc8x8Unorm,
Astc8x8Srgb,
Astc12x12Unorm,
}
#[derive(Debug, Clone, Copy)]
pub struct FormatInfo {
pub bytes_per_block: u32,
pub block_width: u32,
pub block_height: u32,
pub components: u32,
pub is_depth: bool,
pub is_stencil: bool,
pub is_compressed: bool,
pub is_srgb: bool,
pub is_float: bool,
pub is_uint: bool,
pub is_sint: bool,
}
impl FormatInfo {
pub fn bytes_per_pixel(&self) -> f32 {
(self.bytes_per_block as f32) / (self.block_width * self.block_height) as f32
}
}
pub fn format_info(fmt: TextureFormat) -> FormatInfo {
match fmt {
TextureFormat::R8Unorm => FormatInfo { bytes_per_block: 1, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RG8Unorm => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RGBA8Unorm => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RGBA8UnormSrgb => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: true, is_float: false, is_uint: false, is_sint: false },
TextureFormat::BGRA8Unorm => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::BGRA8UnormSrgb => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: true, is_float: false, is_uint: false, is_sint: false },
TextureFormat::R8Snorm => FormatInfo { bytes_per_block: 1, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RG8Snorm => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RGBA8Snorm => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::R8Uint => FormatInfo { bytes_per_block: 1, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::RG8Uint => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::RGBA8Uint => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::R8Sint => FormatInfo { bytes_per_block: 1, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: true },
TextureFormat::RG8Sint => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: true },
TextureFormat::RGBA8Sint => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: true },
TextureFormat::R16Unorm => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RG16Unorm => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RGBA16Unorm => FormatInfo { bytes_per_block: 8, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::R16Float => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::RG16Float => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::RGBA16Float => FormatInfo { bytes_per_block: 8, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::R16Uint => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::RG16Uint => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::RGBA16Uint => FormatInfo { bytes_per_block: 8, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::R16Sint => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: true },
TextureFormat::R32Float => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::RG32Float => FormatInfo { bytes_per_block: 8, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::RGB32Float => FormatInfo { bytes_per_block: 12, block_width: 1, block_height: 1, components: 3, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::RGBA32Float => FormatInfo { bytes_per_block: 16, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::R32Uint => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::RG32Uint => FormatInfo { bytes_per_block: 8, block_width: 1, block_height: 1, components: 2, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::RGBA32Uint => FormatInfo { bytes_per_block: 16, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::R32Sint => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: true },
TextureFormat::RGB10A2Unorm => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 4, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::RG11B10Float => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 3, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::RGB9E5Float => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 3, is_depth: false, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::Depth16Unorm => FormatInfo { bytes_per_block: 2, block_width: 1, block_height: 1, components: 1, is_depth: true, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::Depth24Unorm => FormatInfo { bytes_per_block: 3, block_width: 1, block_height: 1, components: 1, is_depth: true, is_stencil: false, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::Depth32Float => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 1, is_depth: true, is_stencil: false, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::Depth24UnormStencil8 => FormatInfo { bytes_per_block: 4, block_width: 1, block_height: 1, components: 2, is_depth: true, is_stencil: true, is_compressed: false, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::Depth32FloatStencil8 => FormatInfo { bytes_per_block: 5, block_width: 1, block_height: 1, components: 2, is_depth: true, is_stencil: true, is_compressed: false, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::Stencil8 => FormatInfo { bytes_per_block: 1, block_width: 1, block_height: 1, components: 1, is_depth: false, is_stencil: true, is_compressed: false, is_srgb: false, is_float: false, is_uint: true, is_sint: false },
TextureFormat::BC1RgbUnorm | TextureFormat::BC1RgbSrgb |
TextureFormat::BC1RgbaUnorm | TextureFormat::BC1RgbaSrgb => FormatInfo { bytes_per_block: 8, block_width: 4, block_height: 4, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: matches!(fmt, TextureFormat::BC1RgbSrgb | TextureFormat::BC1RgbaSrgb), is_float: false, is_uint: false, is_sint: false },
TextureFormat::BC2Unorm | TextureFormat::BC2Srgb => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: matches!(fmt, TextureFormat::BC2Srgb), is_float: false, is_uint: false, is_sint: false },
TextureFormat::BC3Unorm | TextureFormat::BC3Srgb => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: matches!(fmt, TextureFormat::BC3Srgb), is_float: false, is_uint: false, is_sint: false },
TextureFormat::BC4Unorm | TextureFormat::BC4Snorm => FormatInfo { bytes_per_block: 8, block_width: 4, block_height: 4, components: 1, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::BC5Unorm | TextureFormat::BC5Snorm => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 2, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::BC6HUfloat | TextureFormat::BC6HSfloat => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 3, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: true, is_uint: false, is_sint: false },
TextureFormat::BC7Unorm | TextureFormat::BC7Srgb => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: matches!(fmt, TextureFormat::BC7Srgb), is_float: false, is_uint: false, is_sint: false },
TextureFormat::Etc2Rgb8Unorm | TextureFormat::Etc2Rgb8Srgb => FormatInfo { bytes_per_block: 8, block_width: 4, block_height: 4, components: 3, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: matches!(fmt, TextureFormat::Etc2Rgb8Srgb), is_float: false, is_uint: false, is_sint: false },
TextureFormat::Etc2Rgb8A1Unorm => FormatInfo { bytes_per_block: 8, block_width: 4, block_height: 4, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::Etc2Rgba8Unorm => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::EacR11Unorm => FormatInfo { bytes_per_block: 8, block_width: 4, block_height: 4, components: 1, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::EacRG11Unorm => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 2, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
TextureFormat::Astc4x4Unorm | TextureFormat::Astc4x4Srgb => FormatInfo { bytes_per_block: 16, block_width: 4, block_height: 4, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: matches!(fmt, TextureFormat::Astc4x4Srgb), is_float: false, is_uint: false, is_sint: false },
TextureFormat::Astc8x8Unorm | TextureFormat::Astc8x8Srgb => FormatInfo { bytes_per_block: 16, block_width: 8, block_height: 8, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: matches!(fmt, TextureFormat::Astc8x8Srgb), is_float: false, is_uint: false, is_sint: false },
TextureFormat::Astc12x12Unorm => FormatInfo { bytes_per_block: 16, block_width: 12, block_height: 12, components: 4, is_depth: false, is_stencil: false, is_compressed: true, is_srgb: false, is_float: false, is_uint: false, is_sint: false },
}
}
pub fn texture_size_bytes(fmt: TextureFormat, width: u32, height: u32, mip_levels: u32) -> u64 {
let info = format_info(fmt);
let mut total: u64 = 0;
let mut w = width;
let mut h = height;
for _ in 0..mip_levels {
let bw = (w + info.block_width - 1) / info.block_width;
let bh = (h + info.block_height - 1) / info.block_height;
total += (bw * bh * info.bytes_per_block) as u64;
w = (w / 2).max(1);
h = (h / 2).max(1);
}
total
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ImageLayout {
Undefined,
General,
ColorAttachmentOptimal,
DepthStencilAttachmentOptimal,
DepthStencilReadOnlyOptimal,
ShaderReadOnlyOptimal,
TransferSrcOptimal,
TransferDstOptimal,
Preinitialized,
DepthReadOnlyStencilAttachmentOptimal,
DepthAttachmentStencilReadOnlyOptimal,
DepthAttachmentOptimal,
DepthReadOnlyOptimal,
StencilAttachmentOptimal,
StencilReadOnlyOptimal,
PresentSrc,
SharedPresent,
ShadingRateOptimal,
FragmentDensityMapOptimal,
VideoDecodeSrc,
VideoDecodeDst,
AttachmentOptimal,
ReadOnlyOptimal,
}
macro_rules! bitflags_manual {
(
#[derive($($derive:ident),*)]
pub struct $name:ident: $ty:ty {
$(const $flag:ident = $val:expr;)*
}
) => {
#[derive($($derive),*)]
pub struct $name(pub $ty);
impl $name {
$(pub const $flag: $name = $name($val);)*
pub fn contains(self, other: $name) -> bool {
(self.0 & other.0) == other.0
}
pub fn intersects(self, other: $name) -> bool {
(self.0 & other.0) != 0
}
pub fn is_empty(self) -> bool { self.0 == 0 }
pub fn bits(self) -> $ty { self.0 }
}
impl std::ops::BitOr for $name {
type Output = $name;
fn bitor(self, rhs: $name) -> $name { $name(self.0 | rhs.0) }
}
impl std::ops::BitAnd for $name {
type Output = $name;
fn bitand(self, rhs: $name) -> $name { $name(self.0 & rhs.0) }
}
impl std::ops::BitOrAssign for $name {
fn bitor_assign(&mut self, rhs: $name) { self.0 |= rhs.0; }
}
impl std::ops::Not for $name {
type Output = $name;
fn not(self) -> $name { $name(!self.0) }
}
}
}
bitflags_manual! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AccessFlags: u64 {
const NONE = 0;
const INDIRECT_COMMAND_READ = 1 << 0;
const INDEX_READ = 1 << 1;
const VERTEX_ATTRIBUTE_READ = 1 << 2;
const UNIFORM_READ = 1 << 3;
const INPUT_ATTACHMENT_READ = 1 << 4;
const SHADER_READ = 1 << 5;
const SHADER_WRITE = 1 << 6;
const COLOR_ATTACHMENT_READ = 1 << 7;
const COLOR_ATTACHMENT_WRITE = 1 << 8;
const DEPTH_STENCIL_ATTACHMENT_READ = 1 << 9;
const DEPTH_STENCIL_ATTACHMENT_WRITE = 1 << 10;
const TRANSFER_READ = 1 << 11;
const TRANSFER_WRITE = 1 << 12;
const HOST_READ = 1 << 13;
const HOST_WRITE = 1 << 14;
const MEMORY_READ = 1 << 15;
const MEMORY_WRITE = 1 << 16;
const ACCELERATION_STRUCTURE_READ = 1 << 17;
const ACCELERATION_STRUCTURE_WRITE = 1 << 18;
}
}
bitflags_manual! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PipelineStageFlags: u64 {
const NONE = 0;
const TOP_OF_PIPE = 1 << 0;
const DRAW_INDIRECT = 1 << 1;
const VERTEX_INPUT = 1 << 2;
const VERTEX_SHADER = 1 << 3;
const TESSELLATION_CONTROL_SHADER = 1 << 4;
const TESSELLATION_EVALUATION_SHADER = 1 << 5;
const GEOMETRY_SHADER = 1 << 6;
const FRAGMENT_SHADER = 1 << 7;
const EARLY_FRAGMENT_TESTS = 1 << 8;
const LATE_FRAGMENT_TESTS = 1 << 9;
const COLOR_ATTACHMENT_OUTPUT = 1 << 10;
const COMPUTE_SHADER = 1 << 11;
const TRANSFER = 1 << 12;
const BOTTOM_OF_PIPE = 1 << 13;
const HOST = 1 << 14;
const ALL_GRAPHICS = 1 << 15;
const ALL_COMMANDS = 1 << 16;
const TASK_SHADER_NV = 1 << 17;
const MESH_SHADER_NV = 1 << 18;
const RAY_TRACING_SHADER = 1 << 19;
const ACCELERATION_STRUCTURE_BUILD = 1 << 20;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadOp {
Load,
Clear,
DontCare,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreOp {
Store,
DontCare,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SampleCount {
S1 = 1,
S2 = 2,
S4 = 4,
S8 = 8,
S16 = 16,
S32 = 32,
S64 = 64,
}
impl SampleCount {
pub fn count(self) -> u32 { self as u32 }
}
#[derive(Debug, Clone)]
pub struct AttachmentDescription {
pub format: TextureFormat,
pub samples: SampleCount,
pub load_op: LoadOp,
pub store_op: StoreOp,
pub stencil_load_op: LoadOp,
pub stencil_store_op: StoreOp,
pub initial_layout: ImageLayout,
pub final_layout: ImageLayout,
}
impl AttachmentDescription {
pub fn color(format: TextureFormat) -> Self {
AttachmentDescription {
format,
samples: SampleCount::S1,
load_op: LoadOp::Clear,
store_op: StoreOp::Store,
stencil_load_op: LoadOp::DontCare,
stencil_store_op: StoreOp::DontCare,
initial_layout: ImageLayout::Undefined,
final_layout: ImageLayout::ColorAttachmentOptimal,
}
}
pub fn depth(format: TextureFormat) -> Self {
AttachmentDescription {
format,
samples: SampleCount::S1,
load_op: LoadOp::Clear,
store_op: StoreOp::Store,
stencil_load_op: LoadOp::Clear,
stencil_store_op: StoreOp::DontCare,
initial_layout: ImageLayout::Undefined,
final_layout: ImageLayout::DepthStencilAttachmentOptimal,
}
}
pub fn transient_color(format: TextureFormat, samples: SampleCount) -> Self {
AttachmentDescription {
format,
samples,
load_op: LoadOp::Clear,
store_op: StoreOp::DontCare,
stencil_load_op: LoadOp::DontCare,
stencil_store_op: StoreOp::DontCare,
initial_layout: ImageLayout::Undefined,
final_layout: ImageLayout::ColorAttachmentOptimal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ResourceId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PassId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceKind {
Texture2D,
Texture2DArray,
TextureCube,
Texture3D,
Buffer,
}
#[derive(Debug, Clone)]
pub struct TextureDesc {
pub width: u32,
pub height: u32,
pub depth_or_layers: u32,
pub mip_levels: u32,
pub format: TextureFormat,
pub samples: SampleCount,
pub kind: ResourceKind,
}
impl TextureDesc {
pub fn render_target(width: u32, height: u32, format: TextureFormat) -> Self {
TextureDesc { width, height, depth_or_layers: 1, mip_levels: 1, format, samples: SampleCount::S1, kind: ResourceKind::Texture2D }
}
pub fn depth_target(width: u32, height: u32) -> Self {
TextureDesc { width, height, depth_or_layers: 1, mip_levels: 1, format: TextureFormat::Depth24UnormStencil8, samples: SampleCount::S1, kind: ResourceKind::Texture2D }
}
pub fn shadow_map(size: u32) -> Self {
TextureDesc { width: size, height: size, depth_or_layers: 1, mip_levels: 1, format: TextureFormat::Depth32Float, samples: SampleCount::S1, kind: ResourceKind::Texture2D }
}
pub fn size_bytes(&self) -> u64 {
texture_size_bytes(self.format, self.width, self.height, self.mip_levels)
}
}
#[derive(Debug, Clone)]
pub struct BufferDesc {
pub size: u64,
pub stride: u32,
pub is_structured: bool,
}
#[derive(Debug, Clone)]
pub enum ResourceDesc {
Texture(TextureDesc),
Buffer(BufferDesc),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceLifetime {
Transient, Persistent, Imported, }
#[derive(Debug, Clone)]
pub struct RenderGraphResource {
pub id: ResourceId,
pub name: String,
pub desc: ResourceDesc,
pub lifetime: ResourceLifetime,
pub first_use: usize,
pub last_use: usize,
pub can_alias: bool,
pub alias_target: Option<ResourceId>,
pub current_layout: ImageLayout,
}
impl RenderGraphResource {
pub fn new_transient_texture(id: ResourceId, name: &str, desc: TextureDesc) -> Self {
RenderGraphResource {
id,
name: name.to_owned(),
desc: ResourceDesc::Texture(desc),
lifetime: ResourceLifetime::Transient,
first_use: usize::MAX,
last_use: 0,
can_alias: true,
alias_target: None,
current_layout: ImageLayout::Undefined,
}
}
pub fn is_texture(&self) -> bool {
matches!(self.desc, ResourceDesc::Texture(_))
}
pub fn texture_desc(&self) -> Option<&TextureDesc> {
match &self.desc {
ResourceDesc::Texture(t) => Some(t),
_ => None,
}
}
pub fn can_alias_with(&self, other: &RenderGraphResource) -> bool {
if !self.can_alias || !other.can_alias { return false; }
if self.lifetime != ResourceLifetime::Transient || other.lifetime != ResourceLifetime::Transient { return false; }
match (&self.desc, &other.desc) {
(ResourceDesc::Texture(a), ResourceDesc::Texture(b)) => {
a.size_bytes() == b.size_bytes() && a.samples.count() == b.samples.count()
}
(ResourceDesc::Buffer(a), ResourceDesc::Buffer(b)) => {
a.size == b.size
}
_ => false,
}
}
pub fn lifetime_overlaps(&self, other: &RenderGraphResource) -> bool {
!(self.last_use < other.first_use || other.last_use < self.first_use)
}
}
#[derive(Debug, Clone)]
pub struct ImageBarrier {
pub resource_id: ResourceId,
pub src_stage: PipelineStageFlags,
pub dst_stage: PipelineStageFlags,
pub src_access: AccessFlags,
pub dst_access: AccessFlags,
pub old_layout: ImageLayout,
pub new_layout: ImageLayout,
pub src_queue_family: u32,
pub dst_queue_family: u32,
}
impl ImageBarrier {
pub const QUEUE_FAMILY_IGNORED: u32 = u32::MAX;
pub fn layout_transition(res: ResourceId, old: ImageLayout, new: ImageLayout) -> Self {
let (src_stage, src_access) = layout_to_src_info(old);
let (dst_stage, dst_access) = layout_to_dst_info(new);
ImageBarrier {
resource_id: res,
src_stage,
dst_stage,
src_access,
dst_access,
old_layout: old,
new_layout: new,
src_queue_family: Self::QUEUE_FAMILY_IGNORED,
dst_queue_family: Self::QUEUE_FAMILY_IGNORED,
}
}
}
#[derive(Debug, Clone)]
pub struct BufferBarrier {
pub resource_id: ResourceId,
pub src_stage: PipelineStageFlags,
pub dst_stage: PipelineStageFlags,
pub src_access: AccessFlags,
pub dst_access: AccessFlags,
pub offset: u64,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct PipelineBarrier {
pub image_barriers: Vec<ImageBarrier>,
pub buffer_barriers: Vec<BufferBarrier>,
pub memory_barriers: Vec<(AccessFlags, AccessFlags, PipelineStageFlags, PipelineStageFlags)>,
}
impl PipelineBarrier {
pub fn new() -> Self {
PipelineBarrier { image_barriers: Vec::new(), buffer_barriers: Vec::new(), memory_barriers: Vec::new() }
}
pub fn is_empty(&self) -> bool {
self.image_barriers.is_empty() && self.buffer_barriers.is_empty() && self.memory_barriers.is_empty()
}
}
pub fn layout_to_src_info(layout: ImageLayout) -> (PipelineStageFlags, AccessFlags) {
match layout {
ImageLayout::Undefined | ImageLayout::Preinitialized => {
(PipelineStageFlags::TOP_OF_PIPE, AccessFlags::NONE)
}
ImageLayout::ColorAttachmentOptimal => {
(PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
AccessFlags::COLOR_ATTACHMENT_WRITE | AccessFlags::COLOR_ATTACHMENT_READ)
}
ImageLayout::DepthStencilAttachmentOptimal => {
(PipelineStageFlags::LATE_FRAGMENT_TESTS | PipelineStageFlags::EARLY_FRAGMENT_TESTS,
AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE | AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ)
}
ImageLayout::DepthStencilReadOnlyOptimal => {
(PipelineStageFlags::EARLY_FRAGMENT_TESTS | PipelineStageFlags::FRAGMENT_SHADER,
AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ | AccessFlags::SHADER_READ)
}
ImageLayout::ShaderReadOnlyOptimal => {
(PipelineStageFlags::FRAGMENT_SHADER | PipelineStageFlags::COMPUTE_SHADER,
AccessFlags::SHADER_READ)
}
ImageLayout::TransferSrcOptimal => {
(PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_READ)
}
ImageLayout::TransferDstOptimal => {
(PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_WRITE)
}
ImageLayout::PresentSrc => {
(PipelineStageFlags::BOTTOM_OF_PIPE, AccessFlags::NONE)
}
ImageLayout::General => {
(PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
}
_ => {
(PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
}
}
}
pub fn layout_to_dst_info(layout: ImageLayout) -> (PipelineStageFlags, AccessFlags) {
match layout {
ImageLayout::Undefined => {
(PipelineStageFlags::TOP_OF_PIPE, AccessFlags::NONE)
}
ImageLayout::ColorAttachmentOptimal => {
(PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
AccessFlags::COLOR_ATTACHMENT_WRITE | AccessFlags::COLOR_ATTACHMENT_READ)
}
ImageLayout::DepthStencilAttachmentOptimal => {
(PipelineStageFlags::EARLY_FRAGMENT_TESTS,
AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE | AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ)
}
ImageLayout::DepthStencilReadOnlyOptimal => {
(PipelineStageFlags::EARLY_FRAGMENT_TESTS | PipelineStageFlags::FRAGMENT_SHADER,
AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ | AccessFlags::SHADER_READ)
}
ImageLayout::ShaderReadOnlyOptimal => {
(PipelineStageFlags::VERTEX_SHADER | PipelineStageFlags::FRAGMENT_SHADER | PipelineStageFlags::COMPUTE_SHADER,
AccessFlags::SHADER_READ)
}
ImageLayout::TransferSrcOptimal => {
(PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_READ)
}
ImageLayout::TransferDstOptimal => {
(PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_WRITE)
}
ImageLayout::PresentSrc => {
(PipelineStageFlags::BOTTOM_OF_PIPE, AccessFlags::NONE)
}
ImageLayout::General => {
(PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
}
_ => {
(PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillMode { Solid, Wireframe, Point }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CullMode { None, Front, Back, FrontAndBack }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrontFace { CounterClockwise, Clockwise }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
Never, Less, Equal, LessOrEqual, Greater, NotEqual, GreaterOrEqual, Always
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StencilOp {
Keep, Zero, Replace, IncrementAndClamp, DecrementAndClamp,
Invert, IncrementAndWrap, DecrementAndWrap
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendFactor {
Zero, One,
SrcColor, OneMinusSrcColor, DstColor, OneMinusDstColor,
SrcAlpha, OneMinusSrcAlpha, DstAlpha, OneMinusDstAlpha,
ConstantColor, OneMinusConstantColor, ConstantAlpha, OneMinusConstantAlpha,
SrcAlphaSaturate,
Src1Color, OneMinusSrc1Color, Src1Alpha, OneMinusSrc1Alpha,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendOp {
Add, Subtract, ReverseSubtract, Min, Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogicOp {
Clear, And, AndReverse, Copy, AndInverted, NoOp, Xor, Or,
Nor, Equivalent, Invert, OrReverse, CopyInverted, OrInverted, Nand, Set,
}
#[derive(Debug, Clone, Copy)]
pub struct RasterizerState {
pub fill_mode: FillMode,
pub cull_mode: CullMode,
pub front_face: FrontFace,
pub depth_clamp_enable: bool,
pub rasterizer_discard_enable: bool,
pub depth_bias_enable: bool,
pub depth_bias_constant_factor: f32,
pub depth_bias_clamp: f32,
pub depth_bias_slope_factor: f32,
pub line_width: f32,
pub conservative_rasterization: bool,
}
impl RasterizerState {
pub fn default_opaque() -> Self {
RasterizerState {
fill_mode: FillMode::Solid,
cull_mode: CullMode::Back,
front_face: FrontFace::CounterClockwise,
depth_clamp_enable: false,
rasterizer_discard_enable: false,
depth_bias_enable: false,
depth_bias_constant_factor: 0.0,
depth_bias_clamp: 0.0,
depth_bias_slope_factor: 0.0,
line_width: 1.0,
conservative_rasterization: false,
}
}
pub fn shadow_map() -> Self {
RasterizerState {
fill_mode: FillMode::Solid,
cull_mode: CullMode::Front, front_face: FrontFace::CounterClockwise,
depth_clamp_enable: true, rasterizer_discard_enable: false,
depth_bias_enable: true,
depth_bias_constant_factor: 1.25,
depth_bias_clamp: 0.0,
depth_bias_slope_factor: 1.75,
line_width: 1.0,
conservative_rasterization: false,
}
}
pub fn wireframe() -> Self {
RasterizerState {
fill_mode: FillMode::Wireframe,
cull_mode: CullMode::None,
front_face: FrontFace::CounterClockwise,
depth_clamp_enable: false,
rasterizer_discard_enable: false,
depth_bias_enable: false,
depth_bias_constant_factor: 0.0,
depth_bias_clamp: 0.0,
depth_bias_slope_factor: 0.0,
line_width: 1.0,
conservative_rasterization: false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct StencilOpState {
pub fail_op: StencilOp,
pub pass_op: StencilOp,
pub depth_fail_op: StencilOp,
pub compare_op: CompareOp,
pub compare_mask: u32,
pub write_mask: u32,
pub reference: u32,
}
impl StencilOpState {
pub fn disabled() -> Self {
StencilOpState {
fail_op: StencilOp::Keep,
pass_op: StencilOp::Keep,
depth_fail_op: StencilOp::Keep,
compare_op: CompareOp::Always,
compare_mask: 0xFF,
write_mask: 0xFF,
reference: 0,
}
}
pub fn write_on_pass(ref_val: u32) -> Self {
StencilOpState {
fail_op: StencilOp::Keep,
pass_op: StencilOp::Replace,
depth_fail_op: StencilOp::Keep,
compare_op: CompareOp::Always,
compare_mask: 0xFF,
write_mask: 0xFF,
reference: ref_val,
}
}
pub fn test_equal(ref_val: u32) -> Self {
StencilOpState {
fail_op: StencilOp::Keep,
pass_op: StencilOp::Keep,
depth_fail_op: StencilOp::Keep,
compare_op: CompareOp::Equal,
compare_mask: 0xFF,
write_mask: 0,
reference: ref_val,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DepthStencilState {
pub depth_test_enable: bool,
pub depth_write_enable: bool,
pub depth_compare_op: CompareOp,
pub depth_bounds_test_enable: bool,
pub min_depth_bounds: f32,
pub max_depth_bounds: f32,
pub stencil_test_enable: bool,
pub front: StencilOpState,
pub back: StencilOpState,
}
impl DepthStencilState {
pub fn depth_read_write() -> Self {
DepthStencilState {
depth_test_enable: true,
depth_write_enable: true,
depth_compare_op: CompareOp::Less,
depth_bounds_test_enable: false,
min_depth_bounds: 0.0,
max_depth_bounds: 1.0,
stencil_test_enable: false,
front: StencilOpState::disabled(),
back: StencilOpState::disabled(),
}
}
pub fn depth_read_only() -> Self {
DepthStencilState {
depth_test_enable: true,
depth_write_enable: false,
depth_compare_op: CompareOp::LessOrEqual,
depth_bounds_test_enable: false,
min_depth_bounds: 0.0,
max_depth_bounds: 1.0,
stencil_test_enable: false,
front: StencilOpState::disabled(),
back: StencilOpState::disabled(),
}
}
pub fn no_depth() -> Self {
DepthStencilState {
depth_test_enable: false,
depth_write_enable: false,
depth_compare_op: CompareOp::Always,
depth_bounds_test_enable: false,
min_depth_bounds: 0.0,
max_depth_bounds: 1.0,
stencil_test_enable: false,
front: StencilOpState::disabled(),
back: StencilOpState::disabled(),
}
}
pub fn reverse_z() -> Self {
DepthStencilState {
depth_test_enable: true,
depth_write_enable: true,
depth_compare_op: CompareOp::Greater,
depth_bounds_test_enable: false,
min_depth_bounds: 0.0,
max_depth_bounds: 1.0,
stencil_test_enable: false,
front: StencilOpState::disabled(),
back: StencilOpState::disabled(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ColorBlendAttachment {
pub blend_enable: bool,
pub src_color_blend_factor: BlendFactor,
pub dst_color_blend_factor: BlendFactor,
pub color_blend_op: BlendOp,
pub src_alpha_blend_factor: BlendFactor,
pub dst_alpha_blend_factor: BlendFactor,
pub alpha_blend_op: BlendOp,
pub color_write_mask: u8, }
impl ColorBlendAttachment {
pub const COLOR_WRITE_RGBA: u8 = 0b1111;
pub const COLOR_WRITE_RGB: u8 = 0b0111;
pub const COLOR_WRITE_A: u8 = 0b1000;
pub fn opaque() -> Self {
ColorBlendAttachment {
blend_enable: false,
src_color_blend_factor: BlendFactor::One,
dst_color_blend_factor: BlendFactor::Zero,
color_blend_op: BlendOp::Add,
src_alpha_blend_factor: BlendFactor::One,
dst_alpha_blend_factor: BlendFactor::Zero,
alpha_blend_op: BlendOp::Add,
color_write_mask: Self::COLOR_WRITE_RGBA,
}
}
pub fn alpha_blend() -> Self {
ColorBlendAttachment {
blend_enable: true,
src_color_blend_factor: BlendFactor::SrcAlpha,
dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
color_blend_op: BlendOp::Add,
src_alpha_blend_factor: BlendFactor::One,
dst_alpha_blend_factor: BlendFactor::OneMinusSrcAlpha,
alpha_blend_op: BlendOp::Add,
color_write_mask: Self::COLOR_WRITE_RGBA,
}
}
pub fn premultiplied_alpha() -> Self {
ColorBlendAttachment {
blend_enable: true,
src_color_blend_factor: BlendFactor::One,
dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
color_blend_op: BlendOp::Add,
src_alpha_blend_factor: BlendFactor::One,
dst_alpha_blend_factor: BlendFactor::OneMinusSrcAlpha,
alpha_blend_op: BlendOp::Add,
color_write_mask: Self::COLOR_WRITE_RGBA,
}
}
pub fn additive() -> Self {
ColorBlendAttachment {
blend_enable: true,
src_color_blend_factor: BlendFactor::One,
dst_color_blend_factor: BlendFactor::One,
color_blend_op: BlendOp::Add,
src_alpha_blend_factor: BlendFactor::One,
dst_alpha_blend_factor: BlendFactor::One,
alpha_blend_op: BlendOp::Add,
color_write_mask: Self::COLOR_WRITE_RGBA,
}
}
}
#[derive(Debug, Clone)]
pub struct ColorBlendState {
pub logic_op_enable: bool,
pub logic_op: LogicOp,
pub attachments: Vec<ColorBlendAttachment>,
pub blend_constants: [f32; 4],
}
impl ColorBlendState {
pub fn all_opaque(count: usize) -> Self {
ColorBlendState {
logic_op_enable: false,
logic_op: LogicOp::Copy,
attachments: vec![ColorBlendAttachment::opaque(); count],
blend_constants: [0.0; 4],
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct MultisampleState {
pub sample_count: SampleCount,
pub sample_shading_enable: bool,
pub min_sample_shading: f32,
pub alpha_to_coverage: bool,
pub alpha_to_one: bool,
}
impl MultisampleState {
pub fn disabled() -> Self {
MultisampleState { sample_count: SampleCount::S1, sample_shading_enable: false, min_sample_shading: 0.0, alpha_to_coverage: false, alpha_to_one: false }
}
pub fn msaa4x() -> Self {
MultisampleState { sample_count: SampleCount::S4, sample_shading_enable: false, min_sample_shading: 0.0, alpha_to_coverage: false, alpha_to_one: false }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VertexFormat {
Float1, Float2, Float3, Float4,
Half2, Half4,
Uint1, Uint2, Uint4,
Int1, Int2, Int4,
Unorm8x4, Snorm8x4,
Unorm16x2, Unorm16x4,
}
impl VertexFormat {
pub fn size_bytes(self) -> u32 {
match self {
VertexFormat::Float1 => 4,
VertexFormat::Float2 => 8,
VertexFormat::Float3 => 12,
VertexFormat::Float4 => 16,
VertexFormat::Half2 => 4,
VertexFormat::Half4 => 8,
VertexFormat::Uint1 => 4,
VertexFormat::Uint2 => 8,
VertexFormat::Uint4 => 16,
VertexFormat::Int1 => 4,
VertexFormat::Int2 => 8,
VertexFormat::Int4 => 16,
VertexFormat::Unorm8x4 => 4,
VertexFormat::Snorm8x4 => 4,
VertexFormat::Unorm16x2 => 4,
VertexFormat::Unorm16x4 => 8,
}
}
}
#[derive(Debug, Clone)]
pub struct VertexAttribute {
pub location: u32,
pub binding: u32,
pub format: VertexFormat,
pub offset: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VertexInputRate { Vertex, Instance }
#[derive(Debug, Clone)]
pub struct VertexBinding {
pub binding: u32,
pub stride: u32,
pub input_rate: VertexInputRate,
}
#[derive(Debug, Clone)]
pub struct VertexInputLayout {
pub bindings: Vec<VertexBinding>,
pub attributes: Vec<VertexAttribute>,
}
impl VertexInputLayout {
pub fn empty() -> Self { VertexInputLayout { bindings: vec![], attributes: vec![] } }
pub fn standard_mesh() -> Self {
let bindings = vec![
VertexBinding { binding: 0, stride: 48, input_rate: VertexInputRate::Vertex },
];
let attributes = vec![
VertexAttribute { location: 0, binding: 0, format: VertexFormat::Float3, offset: 0 }, VertexAttribute { location: 1, binding: 0, format: VertexFormat::Float3, offset: 12 }, VertexAttribute { location: 2, binding: 0, format: VertexFormat::Float4, offset: 24 }, VertexAttribute { location: 3, binding: 0, format: VertexFormat::Float2, offset: 40 }, ];
VertexInputLayout { bindings, attributes }
}
pub fn skinned_mesh() -> Self {
let bindings = vec![
VertexBinding { binding: 0, stride: 48, input_rate: VertexInputRate::Vertex },
VertexBinding { binding: 1, stride: 32, input_rate: VertexInputRate::Vertex },
];
let attributes = vec![
VertexAttribute { location: 0, binding: 0, format: VertexFormat::Float3, offset: 0 },
VertexAttribute { location: 1, binding: 0, format: VertexFormat::Float3, offset: 12 },
VertexAttribute { location: 2, binding: 0, format: VertexFormat::Float4, offset: 24 },
VertexAttribute { location: 3, binding: 0, format: VertexFormat::Float2, offset: 40 },
VertexAttribute { location: 4, binding: 1, format: VertexFormat::Uint4, offset: 0 }, VertexAttribute { location: 5, binding: 1, format: VertexFormat::Float4, offset: 16 }, ];
VertexInputLayout { bindings, attributes }
}
pub fn total_stride(&self, binding: u32) -> u32 {
self.bindings.iter().find(|b| b.binding == binding).map(|b| b.stride).unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PassKind {
GBuffer,
ShadowMap,
Lighting,
SSAO,
SSR,
Bloom,
ToneMapping,
TAA,
DepthOfField,
MotionBlur,
VolumetricFog,
Particle,
UI,
Debug,
Custom,
}
#[derive(Debug, Clone)]
pub struct GBufferPassDesc {
pub width: u32,
pub height: u32,
pub albedo_format: TextureFormat, pub normal_format: TextureFormat, pub material_format: TextureFormat, pub velocity_format: TextureFormat, pub depth_format: TextureFormat,
pub samples: SampleCount,
pub output_albedo: ResourceId,
pub output_normal: ResourceId,
pub output_material: ResourceId,
pub output_velocity: ResourceId,
pub output_depth: ResourceId,
pub rasterizer: RasterizerState,
pub depth_stencil: DepthStencilState,
pub vertex_layout: VertexInputLayout,
}
impl GBufferPassDesc {
pub fn default(width: u32, height: u32) -> Self {
GBufferPassDesc {
width, height,
albedo_format: TextureFormat::RGBA8Unorm,
normal_format: TextureFormat::RG16Float, material_format: TextureFormat::RGBA8Unorm,
velocity_format: TextureFormat::RG16Float,
depth_format: TextureFormat::Depth24UnormStencil8,
samples: SampleCount::S1,
output_albedo: ResourceId(0),
output_normal: ResourceId(1),
output_material: ResourceId(2),
output_velocity: ResourceId(3),
output_depth: ResourceId(4),
rasterizer: RasterizerState::default_opaque(),
depth_stencil: DepthStencilState::depth_read_write(),
vertex_layout: VertexInputLayout::standard_mesh(),
}
}
pub fn attachment_descriptions(&self) -> Vec<AttachmentDescription> {
vec![
AttachmentDescription::color(self.albedo_format),
AttachmentDescription::color(self.normal_format),
AttachmentDescription::color(self.material_format),
AttachmentDescription::color(self.velocity_format),
AttachmentDescription::depth(self.depth_format),
]
}
pub fn bandwidth_bytes_per_pixel(&self) -> f32 {
let fi_a = format_info(self.albedo_format).bytes_per_pixel();
let fi_b = format_info(self.normal_format).bytes_per_pixel();
let fi_c = format_info(self.material_format).bytes_per_pixel();
let fi_d = format_info(self.velocity_format).bytes_per_pixel();
let fi_z = format_info(self.depth_format).bytes_per_pixel();
fi_a + fi_b + fi_c + fi_d + fi_z
}
pub fn estimate_write_bandwidth_mb(&self) -> f32 {
let bpp = self.bandwidth_bytes_per_pixel();
let pixels = (self.width * self.height) as f32;
(bpp * pixels) / (1024.0 * 1024.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShadowMapKind { Directional, Spot, Point, Cascaded }
#[derive(Debug, Clone)]
pub struct ShadowMapPassDesc {
pub kind: ShadowMapKind,
pub resolution: u32,
pub cascade_count: u32, pub depth_format: TextureFormat,
pub output_shadow_map: ResourceId,
pub rasterizer: RasterizerState,
pub depth_stencil: DepthStencilState,
pub near_plane: f32,
pub far_plane: f32,
pub light_view_proj: [Mat4; 4], }
impl ShadowMapPassDesc {
pub fn directional_shadow(resolution: u32) -> Self {
ShadowMapPassDesc {
kind: ShadowMapKind::Cascaded,
resolution,
cascade_count: 4,
depth_format: TextureFormat::Depth32Float,
output_shadow_map: ResourceId(100),
rasterizer: RasterizerState::shadow_map(),
depth_stencil: DepthStencilState::depth_read_write(),
near_plane: 0.1,
far_plane: 200.0,
light_view_proj: [Mat4::IDENTITY; 4],
}
}
pub fn compute_cascade_splits(&self, lambda: f32, near: f32, far: f32) -> Vec<f32> {
let n = self.cascade_count as usize;
let mut splits = vec![0.0f32; n];
let ratio = far / near;
for i in 0..n {
let p = (i + 1) as f32 / n as f32;
let log = near * ratio.powf(p);
let uniform = near + (far - near) * p;
let d = lambda * (log - uniform) + uniform;
splits[i] = d;
}
splits
}
pub fn compute_cascade_view_proj(&self, camera_view: Mat4, inv_cam_proj: Mat4, near_split: f32, far_split: f32, light_dir: Vec3) -> Mat4 {
let ndc_corners = [
Vec4::new(-1.0, -1.0, 0.0, 1.0),
Vec4::new( 1.0, -1.0, 0.0, 1.0),
Vec4::new(-1.0, 1.0, 0.0, 1.0),
Vec4::new( 1.0, 1.0, 0.0, 1.0),
Vec4::new(-1.0, -1.0, 1.0, 1.0),
Vec4::new( 1.0, -1.0, 1.0, 1.0),
Vec4::new(-1.0, 1.0, 1.0, 1.0),
Vec4::new( 1.0, 1.0, 1.0, 1.0),
];
let inv_view_proj = (camera_view).inverse();
let mut world_corners = [Vec3::ZERO; 8];
for (i, ndc) in ndc_corners.iter().enumerate() {
let view_h = inv_cam_proj * *ndc;
let view = view_h / view_h.w;
let z_frac = if i < 4 { near_split } else { far_split };
let view_scaled = Vec4::new(view.x * z_frac, view.y * z_frac, view.z * z_frac, 1.0);
let world_h = inv_view_proj * view_scaled;
world_corners[i] = world_h.truncate() / world_h.w;
}
let mut centroid = Vec3::ZERO;
for c in &world_corners { centroid += *c; }
centroid /= 8.0;
let up = if light_dir.dot(Vec3::Y).abs() < 0.999 { Vec3::Y } else { Vec3::Z };
let light_view = Mat4::look_at_rh(centroid - light_dir * 50.0, centroid, up);
let mut min_ls = Vec3::splat(f32::MAX);
let mut max_ls = Vec3::splat(f32::MIN);
for c in &world_corners {
let ls = (light_view * Vec4::new(c.x, c.y, c.z, 1.0)).truncate();
min_ls = min_ls.min(ls);
max_ls = max_ls.max(ls);
}
let world_units_per_texel = (max_ls.x - min_ls.x) / self.resolution as f32;
min_ls.x = (min_ls.x / world_units_per_texel).floor() * world_units_per_texel;
max_ls.x = (max_ls.x / world_units_per_texel).ceil() * world_units_per_texel;
min_ls.y = (min_ls.y / world_units_per_texel).floor() * world_units_per_texel;
max_ls.y = (max_ls.y / world_units_per_texel).ceil() * world_units_per_texel;
let light_proj = Mat4::orthographic_rh(min_ls.x, max_ls.x, min_ls.y, max_ls.y, min_ls.z - 10.0, max_ls.z + 10.0);
light_proj * light_view
}
}
#[derive(Debug, Clone)]
pub struct LightingPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_hdr: ResourceId,
pub input_albedo: ResourceId,
pub input_normal: ResourceId,
pub input_material: ResourceId,
pub input_depth: ResourceId,
pub input_shadow_map: ResourceId,
pub input_ssao: ResourceId,
pub ibl_enabled: bool,
pub ibl_diffuse_irradiance_res: u32,
pub ibl_specular_prefiltered_res: u32,
pub ibl_brdf_lut_res: u32,
pub tile_size: u32,
pub max_lights_per_tile: u32,
}
impl LightingPassDesc {
pub fn default(width: u32, height: u32) -> Self {
LightingPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_hdr: ResourceId(10),
input_albedo: ResourceId(0),
input_normal: ResourceId(1),
input_material: ResourceId(2),
input_depth: ResourceId(4),
input_shadow_map: ResourceId(100),
input_ssao: ResourceId(20),
ibl_enabled: true,
ibl_diffuse_irradiance_res: 32,
ibl_specular_prefiltered_res: 256,
ibl_brdf_lut_res: 512,
tile_size: 16,
max_lights_per_tile: 1024,
}
}
pub fn tile_count_x(&self) -> u32 { (self.width + self.tile_size - 1) / self.tile_size }
pub fn tile_count_y(&self) -> u32 { (self.height + self.tile_size - 1) / self.tile_size }
pub fn total_tiles(&self) -> u32 { self.tile_count_x() * self.tile_count_y() }
pub fn light_list_buffer_size_bytes(&self) -> u64 {
(self.total_tiles() as u64) * (self.max_lights_per_tile as u64 + 1) * 2
}
}
#[derive(Debug, Clone)]
pub struct SSAOPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_ao: ResourceId,
pub input_depth: ResourceId,
pub input_normal: ResourceId,
pub kernel_size: u32,
pub radius: f32,
pub bias: f32,
pub power: f32,
pub noise_tex_size: u32,
pub blur_passes: u32,
pub half_resolution: bool,
}
impl SSAOPassDesc {
pub fn default(width: u32, height: u32) -> Self {
SSAOPassDesc {
width, height,
output_format: TextureFormat::R8Unorm,
output_ao: ResourceId(20),
input_depth: ResourceId(4),
input_normal: ResourceId(1),
kernel_size: 64,
radius: 0.5,
bias: 0.025,
power: 2.2,
noise_tex_size: 4,
blur_passes: 2,
half_resolution: true,
}
}
pub fn generate_kernel(&self) -> Vec<Vec3> {
let mut kernel = Vec::with_capacity(self.kernel_size as usize);
let mut lcg: u64 = 0x123456789ABCDEF0;
let lcg_next = |state: &mut u64| -> f32 {
*state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((*state >> 33) as f32) / (u32::MAX as f32)
};
for i in 0..self.kernel_size {
let x = lcg_next(&mut lcg) * 2.0 - 1.0;
let y = lcg_next(&mut lcg) * 2.0 - 1.0;
let z = lcg_next(&mut lcg); let mut sample = Vec3::new(x, y, z).normalize();
sample *= lcg_next(&mut lcg);
let scale = (i as f32) / (self.kernel_size as f32);
let scale = lerp(0.1, 1.0, scale * scale);
sample *= scale;
kernel.push(sample);
}
kernel
}
pub fn generate_noise(&self) -> Vec<Vec3> {
let n = (self.noise_tex_size * self.noise_tex_size) as usize;
let mut noise = Vec::with_capacity(n);
let mut lcg: u64 = 0xDEADBEEFCAFEBABE;
let lcg_next = |state: &mut u64| -> f32 {
*state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((*state >> 33) as f32) / (u32::MAX as f32)
};
for _ in 0..n {
let x = lcg_next(&mut lcg) * 2.0 - 1.0;
let y = lcg_next(&mut lcg) * 2.0 - 1.0;
noise.push(Vec3::new(x, y, 0.0)); }
noise
}
pub fn effective_width(&self) -> u32 { if self.half_resolution { self.width / 2 } else { self.width } }
pub fn effective_height(&self) -> u32 { if self.half_resolution { self.height / 2 } else { self.height } }
}
#[derive(Debug, Clone)]
pub struct SSRPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_ssr: ResourceId,
pub input_depth: ResourceId,
pub input_normal: ResourceId,
pub input_material: ResourceId,
pub input_hdr: ResourceId,
pub max_steps: u32,
pub step_size: f32,
pub max_distance: f32,
pub thickness: f32,
pub binary_search_steps: u32,
pub jitter: bool,
pub half_resolution: bool,
pub reprojection_enabled: bool,
}
impl SSRPassDesc {
pub fn default(width: u32, height: u32) -> Self {
SSRPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_ssr: ResourceId(21),
input_depth: ResourceId(4),
input_normal: ResourceId(1),
input_material: ResourceId(2),
input_hdr: ResourceId(10),
max_steps: 64,
step_size: 0.1,
max_distance: 10.0,
thickness: 0.1,
binary_search_steps: 8,
jitter: true,
half_resolution: true,
reprojection_enabled: true,
}
}
pub fn compute_hiz_mip_level(&self, screen_distance: f32) -> u32 {
let mip = (screen_distance / self.step_size).log2() as u32;
mip.clamp(0, 8)
}
pub fn screen_fade(&self, uv: Vec2) -> f32 {
let edge = 0.1f32;
let fade_x = smoothstep(0.0, edge, uv.x) * smoothstep(1.0, 1.0 - edge, uv.x);
let fade_y = smoothstep(0.0, edge, uv.y) * smoothstep(1.0, 1.0 - edge, uv.y);
fade_x * fade_y
}
}
#[derive(Debug, Clone)]
pub struct BloomPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_bloom: ResourceId,
pub input_hdr: ResourceId,
pub threshold: f32,
pub knee: f32,
pub intensity: f32,
pub scatter: f32,
pub mip_levels: u32,
pub use_lens_dirt: bool,
pub lens_dirt_intensity: f32,
}
impl BloomPassDesc {
pub fn default(width: u32, height: u32) -> Self {
BloomPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_bloom: ResourceId(22),
input_hdr: ResourceId(10),
threshold: 1.0,
knee: 0.5,
intensity: 0.05,
scatter: 0.7,
mip_levels: 6,
use_lens_dirt: false,
lens_dirt_intensity: 0.3,
}
}
pub fn quadratic_threshold(&self, lum: f32) -> f32 {
let t = self.threshold;
let k = self.knee;
let rq = (lum - t + k * 0.5).clamp(0.0, k);
let threshold_result = (rq * rq) / (4.0 * k + 0.00001);
let linear_result = (lum - t).max(0.0);
threshold_result.max(linear_result)
}
pub fn kawase_weights(iter: u32) -> [f32; 4] {
let offset = iter as f32 + 0.5;
[offset, offset, offset, offset]
}
pub fn dual_kawase_upsample_offsets(iter: u32) -> [Vec2; 8] {
let s = (iter as f32) + 0.5;
[
Vec2::new(-s, -s), Vec2::new(0.0, -s), Vec2::new(s, -s),
Vec2::new(-s, 0.0), Vec2::new(s, 0.0),
Vec2::new(-s, s), Vec2::new(0.0, s), Vec2::new(s, s),
]
}
pub fn mip_size(&self, mip: u32) -> (u32, u32) {
let w = (self.width >> mip).max(1);
let h = (self.height >> mip).max(1);
(w, h)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ToneMappingOperator {
Linear,
Reinhard,
ReinhardExtended,
Filmic, ACES, Uncharted2,
Lottes,
Uchimura,
}
#[derive(Debug, Clone)]
pub struct ToneMappingPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_sdr: ResourceId,
pub input_hdr: ResourceId,
pub input_bloom: ResourceId,
pub operator: ToneMappingOperator,
pub exposure: f32,
pub gamma: f32,
pub white_point: f32,
pub color_lut_enabled: bool,
pub color_lut_size: u32,
}
impl ToneMappingPassDesc {
pub fn default(width: u32, height: u32) -> Self {
ToneMappingPassDesc {
width, height,
output_format: TextureFormat::RGBA8UnormSrgb,
output_sdr: ResourceId(30),
input_hdr: ResourceId(10),
input_bloom: ResourceId(22),
operator: ToneMappingOperator::ACES,
exposure: 1.0,
gamma: 2.2,
white_point: 4.0,
color_lut_enabled: false,
color_lut_size: 32,
}
}
pub fn apply_aces(&self, color: Vec3) -> Vec3 {
let m1 = Mat3F32([
[0.59719, 0.35458, 0.04823],
[0.07600, 0.90834, 0.01566],
[0.02840, 0.13383, 0.83777],
]);
let m2 = Mat3F32([
[ 1.60475, -0.53108, -0.07367],
[-0.10208, 1.10813, -0.00605],
[-0.00327, -0.07276, 1.07602],
]);
let v = m1.mul_vec3(color);
let a = v * (v + Vec3::splat(0.0245786)) - Vec3::splat(0.000090537);
let b = v * (Vec3::splat(0.983729) * v + Vec3::splat(0.4329510)) + Vec3::splat(0.238081);
let rrt_odt = a / b;
let mapped = m2.mul_vec3(rrt_odt);
mapped.clamp(Vec3::ZERO, Vec3::ONE)
}
pub fn apply_hable_filmic(&self, color: Vec3) -> Vec3 {
let hable = |x: Vec3| -> Vec3 {
let a = Vec3::splat(0.15);
let b = Vec3::splat(0.50);
let c = Vec3::splat(0.10);
let d = Vec3::splat(0.20);
let e = Vec3::splat(0.02);
let f = Vec3::splat(0.30);
(x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f) - e / f
};
let white = Vec3::splat(self.white_point);
hable(color * self.exposure) / hable(white)
}
pub fn apply_reinhard(&self, color: Vec3) -> Vec3 {
color / (color + Vec3::ONE)
}
pub fn apply_reinhard_extended(&self, color: Vec3) -> Vec3 {
let w2 = Vec3::splat(self.white_point * self.white_point);
(color * (Vec3::ONE + color / w2)) / (color + Vec3::ONE)
}
pub fn apply_operator(&self, color: Vec3) -> Vec3 {
let c = color * self.exposure;
match self.operator {
ToneMappingOperator::Linear => c.clamp(Vec3::ZERO, Vec3::ONE),
ToneMappingOperator::Reinhard => self.apply_reinhard(c),
ToneMappingOperator::ReinhardExtended => self.apply_reinhard_extended(c),
ToneMappingOperator::Filmic => self.apply_hable_filmic(color),
ToneMappingOperator::ACES => self.apply_aces(c),
ToneMappingOperator::Uncharted2 => self.apply_hable_filmic(color), ToneMappingOperator::Lottes => self.apply_lottes(c),
ToneMappingOperator::Uchimura => self.apply_uchimura(c),
}
}
fn apply_lottes(&self, color: Vec3) -> Vec3 {
let a = Vec3::splat(1.6);
let d = Vec3::splat(0.977);
let hdr_max = Vec3::splat(8.0);
let mid_in = Vec3::splat(0.18);
let mid_out = Vec3::splat(0.267);
let b = (-mid_out + mid_in.powf(a.x) * hdr_max.powf(d.x)) /
((hdr_max.powf(a.x) - mid_in.powf(a.x)) * mid_out);
let c = (mid_in.powf(a.x) * hdr_max.powf(d.x) - hdr_max.powf(a.x) * mid_out) /
((hdr_max.powf(a.x) - mid_in.powf(a.x)) * mid_out);
color.powf(a.x) / (color.powf(a.x * d.x) * b + c)
}
fn apply_uchimura(&self, color: Vec3) -> Vec3 {
let p = 1.0f32; let a = 1.0f32; let m = 0.22f32; let l = 0.4f32; let c = 1.33f32; let b = 0.0f32;
let map_channel = |x: f32| -> f32 {
let l0 = (p - m) * l / a;
let s0 = m + l0;
let s1 = m + a * l0;
let c2 = a * p / (p - s1);
let cp = -c2 / p;
if x < m {
let d = m / (c * m + 1.0 - c);
d * x
} else if x < s1 {
let d = m + a * (x - m);
d
} else {
p - (p - s1) * (-c2 * (x - s0) / p).exp()
}
};
Vec3::new(map_channel(color.x), map_channel(color.y), map_channel(color.z))
}
pub fn gamma_correct(&self, linear: Vec3) -> Vec3 {
let inv_gamma = 1.0 / self.gamma;
Vec3::new(linear.x.powf(inv_gamma), linear.y.powf(inv_gamma), linear.z.powf(inv_gamma))
}
}
struct Mat3F32([[f32; 3]; 3]);
impl Mat3F32 {
fn mul_vec3(&self, v: Vec3) -> Vec3 {
Vec3::new(
self.0[0][0]*v.x + self.0[0][1]*v.y + self.0[0][2]*v.z,
self.0[1][0]*v.x + self.0[1][1]*v.y + self.0[1][2]*v.z,
self.0[2][0]*v.x + self.0[2][1]*v.y + self.0[2][2]*v.z,
)
}
}
#[derive(Debug, Clone)]
pub struct TAAPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_resolved: ResourceId,
pub input_current: ResourceId,
pub input_history: ResourceId,
pub input_depth: ResourceId,
pub input_velocity: ResourceId,
pub blend_factor: f32,
pub variance_clip_gamma: f32,
pub velocity_weight_scale: f32,
pub jitter_sequence_len: u32,
pub use_catmull_rom: bool,
pub anti_flicker: bool,
}
impl TAAPassDesc {
pub fn default(width: u32, height: u32) -> Self {
TAAPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_resolved: ResourceId(31),
input_current: ResourceId(10),
input_history: ResourceId(32),
input_depth: ResourceId(4),
input_velocity: ResourceId(3),
blend_factor: 0.1,
variance_clip_gamma: 1.0,
velocity_weight_scale: 500.0,
jitter_sequence_len: 16,
use_catmull_rom: true,
anti_flicker: true,
}
}
pub fn halton_jitter(&self, frame: u32) -> Vec2 {
let idx = (frame % self.jitter_sequence_len) + 1;
let hx = halton_sequence(idx, 2);
let hy = halton_sequence(idx, 3);
Vec2::new(hx - 0.5, hy - 0.5)
}
pub fn catmull_rom_weights(frac: Vec2) -> [f32; 5] {
let f = frac;
let w0 = |t: f32| { -0.5*t*t*t + t*t - 0.5*t };
let w1 = |t: f32| { 1.5*t*t*t - 2.5*t*t + 1.0 };
let w2 = |t: f32| { -1.5*t*t*t + 2.0*t*t + 0.5*t };
let w3 = |t: f32| { 0.5*t*t*t - 0.5*t*t };
[w0(f.x), w1(f.x), w2(f.x), w3(f.x), 0.0] }
pub fn clip_color_to_aabb(history: Vec3, min_c: Vec3, max_c: Vec3) -> Vec3 {
let center = (min_c + max_c) * 0.5;
let extents = (max_c - min_c) * 0.5;
let ray = history - center;
let abs_ray = Vec3::new(ray.x.abs(), ray.y.abs(), ray.z.abs());
let r_extents = Vec3::new(
if abs_ray.x > 0.0 { extents.x / abs_ray.x } else { 1.0 },
if abs_ray.y > 0.0 { extents.y / abs_ray.y } else { 1.0 },
if abs_ray.z > 0.0 { extents.z / abs_ray.z } else { 1.0 },
);
let factor = r_extents.x.min(r_extents.y).min(r_extents.z).min(1.0);
center + ray * factor
}
pub fn variance_clip(history: Vec3, neighborhood: &[Vec3], gamma: f32) -> Vec3 {
let n = neighborhood.len() as f32;
let mut mu = Vec3::ZERO;
let mut sq = Vec3::ZERO;
for s in neighborhood {
mu += *s;
sq += *s * *s;
}
mu /= n;
sq /= n;
let sigma = (sq - mu * mu).max(Vec3::ZERO).sqrt() * gamma;
let min_c = mu - sigma;
let max_c = mu + sigma;
Self::clip_color_to_aabb(history, min_c, max_c)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoFAlgorithm { CircleOfConfusion, BokehHexagonal, BokehOctagonal, TileMax, Scatter }
#[derive(Debug, Clone)]
pub struct DepthOfFieldPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_dof: ResourceId,
pub input_hdr: ResourceId,
pub input_depth: ResourceId,
pub algorithm: DoFAlgorithm,
pub focus_distance: f32,
pub focus_range: f32,
pub bokeh_radius: f32,
pub far_blur_amount: f32,
pub near_blur_amount: f32,
pub sample_count: u32,
pub bokeh_rotation: f32,
}
impl DepthOfFieldPassDesc {
pub fn default(width: u32, height: u32) -> Self {
DepthOfFieldPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_dof: ResourceId(33),
input_hdr: ResourceId(10),
input_depth: ResourceId(4),
algorithm: DoFAlgorithm::CircleOfConfusion,
focus_distance: 10.0,
focus_range: 5.0,
bokeh_radius: 8.0,
far_blur_amount: 1.0,
near_blur_amount: 0.5,
sample_count: 16,
bokeh_rotation: 0.0,
}
}
pub fn coc_from_depth(&self, depth: f32, focal_length: f32, aperture: f32) -> f32 {
let fd = self.focus_distance;
let numerator = aperture * focal_length * (depth - fd);
let denominator = depth * (fd - focal_length);
if denominator.abs() < 1e-6 { 0.0 } else { (numerator / denominator).abs() }
}
pub fn hexagonal_bokeh_samples(&self) -> Vec<Vec2> {
let n = self.sample_count as usize;
let mut samples = Vec::with_capacity(n);
let rings = ((n as f32).sqrt().ceil() as u32).max(1);
let mut idx = 0;
'outer: for ring in 0..=rings {
if ring == 0 {
samples.push(Vec2::ZERO);
idx += 1;
if idx >= n { break; }
} else {
let steps = ring * 6;
for step in 0..steps {
let angle = (step as f32 / steps as f32) * std::f32::consts::TAU;
let r = ring as f32 / rings as f32;
let x = r * angle.cos();
let y = r * angle.sin();
let hex_d = hex_distance(Vec2::new(x, y));
if hex_d <= 1.0 {
samples.push(Vec2::new(x, y) * self.bokeh_radius);
idx += 1;
if idx >= n { break 'outer; }
}
}
}
}
samples
}
}
#[derive(Debug, Clone)]
pub struct MotionBlurPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_mb: ResourceId,
pub input_hdr: ResourceId,
pub input_velocity: ResourceId,
pub input_depth: ResourceId,
pub sample_count: u32,
pub shutter_angle: f32, pub max_velocity_pixels: f32,
pub tile_size: u32,
pub reconstruction_filter: bool,
}
impl MotionBlurPassDesc {
pub fn default(width: u32, height: u32) -> Self {
MotionBlurPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_mb: ResourceId(34),
input_hdr: ResourceId(10),
input_velocity: ResourceId(3),
input_depth: ResourceId(4),
sample_count: 8,
shutter_angle: 180.0,
max_velocity_pixels: 32.0,
tile_size: 16,
reconstruction_filter: true,
}
}
pub fn shutter_fraction(&self) -> f32 { self.shutter_angle / 360.0 }
pub fn tile_count_x(&self) -> u32 { (self.width + self.tile_size - 1) / self.tile_size }
pub fn tile_count_y(&self) -> u32 { (self.height + self.tile_size - 1) / self.tile_size }
pub fn sample_positions(velocity: Vec2, n: u32) -> Vec<Vec2> {
let mut positions = Vec::with_capacity(n as usize);
for i in 0..n {
let t = (i as f32 + 0.5) / n as f32 - 0.5; positions.push(velocity * t);
}
positions
}
pub fn soft_depth_compare(za: f32, zb: f32, extent: f32) -> f32 {
clamp01(1.0 - (za - zb) / extent.max(1e-6))
}
}
#[derive(Debug, Clone)]
pub struct VolumetricFogPassDesc {
pub width: u32,
pub height: u32,
pub depth_slices: u32, pub output_format: TextureFormat,
pub output_fog: ResourceId,
pub input_depth: ResourceId,
pub input_shadow_map: ResourceId,
pub scattering: f32,
pub absorption: f32,
pub density: f32,
pub phase_g: f32, pub ambient_intensity: f32,
pub max_distance: f32,
pub use_temporal_reprojection: bool,
pub noise_scale: Vec3,
pub wind_speed: Vec3,
}
impl VolumetricFogPassDesc {
pub fn default(width: u32, height: u32) -> Self {
VolumetricFogPassDesc {
width, height,
depth_slices: 128,
output_format: TextureFormat::RGBA16Float,
output_fog: ResourceId(40),
input_depth: ResourceId(4),
input_shadow_map: ResourceId(100),
scattering: 0.1,
absorption: 0.01,
density: 0.05,
phase_g: 0.2,
ambient_intensity: 0.1,
max_distance: 100.0,
use_temporal_reprojection: true,
noise_scale: Vec3::new(0.1, 0.1, 0.1),
wind_speed: Vec3::new(0.5, 0.0, 0.3),
}
}
pub fn henyey_greenstein(&self, cos_theta: f32) -> f32 {
let g = self.phase_g;
let g2 = g * g;
let denom = (1.0 + g2 - 2.0 * g * cos_theta).abs().powf(1.5);
(1.0 - g2) / (4.0 * std::f32::consts::PI * denom)
}
pub fn extinction(&self, distance: f32) -> f32 {
let sigma_t = self.scattering + self.absorption;
(-sigma_t * self.density * distance).exp()
}
pub fn cornette_shanks(&self, cos_theta: f32) -> f32 {
let g = self.phase_g;
let g2 = g * g;
let num = 3.0 * (1.0 - g2) * (1.0 + cos_theta * cos_theta);
let den = 2.0 * (2.0 + g2) * (1.0 + g2 - 2.0 * g * cos_theta).abs().powf(1.5);
num / den
}
pub fn froxel_depth_from_slice(&self, slice: u32, near: f32, far: f32) -> f32 {
let s = slice as f32 / self.depth_slices as f32;
near * (far / near).powf(s)
}
pub fn froxel_volume_size(&self) -> (u32, u32, u32) {
let w = (self.width + 7) / 8;
let h = (self.height + 7) / 8;
(w, h, self.depth_slices)
}
}
#[derive(Debug, Clone)]
pub struct ParticlePassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_particles: ResourceId,
pub input_depth: ResourceId,
pub input_hdr: ResourceId,
pub max_particles: u32,
pub sort_enabled: bool,
pub soft_particle_enabled: bool,
pub soft_particle_extent: f32,
pub use_gpu_simulation: bool,
pub blend: ColorBlendAttachment,
}
impl ParticlePassDesc {
pub fn default(width: u32, height: u32) -> Self {
ParticlePassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_particles: ResourceId(41),
input_depth: ResourceId(4),
input_hdr: ResourceId(10),
max_particles: 1_000_000,
sort_enabled: true,
soft_particle_enabled: true,
soft_particle_extent: 1.0,
use_gpu_simulation: true,
blend: ColorBlendAttachment::additive(),
}
}
pub fn particle_buffer_size_bytes(&self) -> u64 {
self.max_particles as u64 * 52
}
pub fn sort_key_buffer_size_bytes(&self) -> u64 {
self.max_particles as u64 * 8
}
pub fn soft_particle_factor(scene_depth: f32, particle_depth: f32, extent: f32) -> f32 {
let diff = scene_depth - particle_depth;
clamp01(diff / extent.max(1e-6))
}
}
#[derive(Debug, Clone)]
pub struct UIPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_ui: ResourceId,
pub input_scene: ResourceId,
pub blend: ColorBlendAttachment,
pub scissor_test_enabled: bool,
pub max_draw_calls: u32,
pub vertex_buffer_size: u64,
pub index_buffer_size: u64,
pub text_atlas_size: u32,
pub max_textures: u32,
}
impl UIPassDesc {
pub fn default(width: u32, height: u32) -> Self {
UIPassDesc {
width, height,
output_format: TextureFormat::RGBA8UnormSrgb,
output_ui: ResourceId(50),
input_scene: ResourceId(31),
blend: ColorBlendAttachment::alpha_blend(),
scissor_test_enabled: true,
max_draw_calls: 4096,
vertex_buffer_size: 4 * 1024 * 1024,
index_buffer_size: 2 * 1024 * 1024,
text_atlas_size: 2048,
max_textures: 64,
}
}
}
#[derive(Debug, Clone)]
pub struct DebugPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_debug: ResourceId,
pub input_depth: ResourceId,
pub draw_wireframe: bool,
pub draw_normals: bool,
pub draw_bounding_boxes: bool,
pub draw_light_volumes: bool,
pub draw_nav_mesh: bool,
pub draw_physics_shapes: bool,
pub draw_frustums: bool,
pub line_color: Vec4,
pub max_lines: u32,
pub max_debug_primitives: u32,
}
impl DebugPassDesc {
pub fn default(width: u32, height: u32) -> Self {
DebugPassDesc {
width, height,
output_format: TextureFormat::RGBA8Unorm,
output_debug: ResourceId(51),
input_depth: ResourceId(4),
draw_wireframe: false,
draw_normals: false,
draw_bounding_boxes: true,
draw_light_volumes: false,
draw_nav_mesh: false,
draw_physics_shapes: false,
draw_frustums: false,
line_color: Vec4::new(0.0, 1.0, 0.0, 1.0),
max_lines: 65536,
max_debug_primitives: 8192,
}
}
pub fn line_buffer_size_bytes(&self) -> u64 {
self.max_lines as u64 * 56
}
}
#[derive(Debug, Clone)]
pub enum PassDesc {
GBuffer(GBufferPassDesc),
ShadowMap(ShadowMapPassDesc),
Lighting(LightingPassDesc),
SSAO(SSAOPassDesc),
SSR(SSRPassDesc),
Bloom(BloomPassDesc),
ToneMapping(ToneMappingPassDesc),
TAA(TAAPassDesc),
DepthOfField(DepthOfFieldPassDesc),
MotionBlur(MotionBlurPassDesc),
VolumetricFog(VolumetricFogPassDesc),
Particle(ParticlePassDesc),
UI(UIPassDesc),
Debug(DebugPassDesc),
}
impl PassDesc {
pub fn kind(&self) -> PassKind {
match self {
PassDesc::GBuffer(_) => PassKind::GBuffer,
PassDesc::ShadowMap(_) => PassKind::ShadowMap,
PassDesc::Lighting(_) => PassKind::Lighting,
PassDesc::SSAO(_) => PassKind::SSAO,
PassDesc::SSR(_) => PassKind::SSR,
PassDesc::Bloom(_) => PassKind::Bloom,
PassDesc::ToneMapping(_) => PassKind::ToneMapping,
PassDesc::TAA(_) => PassKind::TAA,
PassDesc::DepthOfField(_) => PassKind::DepthOfField,
PassDesc::MotionBlur(_) => PassKind::MotionBlur,
PassDesc::VolumetricFog(_) => PassKind::VolumetricFog,
PassDesc::Particle(_) => PassKind::Particle,
PassDesc::UI(_) => PassKind::UI,
PassDesc::Debug(_) => PassKind::Debug,
}
}
}
#[derive(Debug, Clone)]
pub struct PassNode {
pub id: PassId,
pub name: String,
pub desc: PassDesc,
pub reads: Vec<ResourceId>,
pub writes: Vec<ResourceId>,
pub barriers_before: Vec<ImageBarrier>,
pub barriers_after: Vec<ImageBarrier>,
pub enabled: bool,
pub async_compute: bool,
pub execute_order: usize,
pub editor_pos: Vec2,
pub editor_size: Vec2,
pub editor_layer: i32,
pub editor_color: Vec4,
}
impl PassNode {
pub fn new(id: PassId, name: &str, desc: PassDesc) -> Self {
let color = pass_kind_color(desc.kind());
PassNode {
id, name: name.to_owned(), desc,
reads: vec![], writes: vec![],
barriers_before: vec![], barriers_after: vec![],
enabled: true, async_compute: false,
execute_order: 0,
editor_pos: Vec2::ZERO, editor_size: Vec2::new(200.0, 80.0),
editor_layer: 0, editor_color: color,
}
}
pub fn add_read(&mut self, res: ResourceId) { if !self.reads.contains(&res) { self.reads.push(res); } }
pub fn add_write(&mut self, res: ResourceId) { if !self.writes.contains(&res) { self.writes.push(res); } }
}
fn pass_kind_color(kind: PassKind) -> Vec4 {
match kind {
PassKind::GBuffer => Vec4::new(0.20, 0.40, 0.80, 1.0),
PassKind::ShadowMap => Vec4::new(0.10, 0.10, 0.30, 1.0),
PassKind::Lighting => Vec4::new(0.90, 0.75, 0.10, 1.0),
PassKind::SSAO => Vec4::new(0.30, 0.30, 0.30, 1.0),
PassKind::SSR => Vec4::new(0.10, 0.60, 0.90, 1.0),
PassKind::Bloom => Vec4::new(0.90, 0.50, 0.10, 1.0),
PassKind::ToneMapping => Vec4::new(0.50, 0.80, 0.50, 1.0),
PassKind::TAA => Vec4::new(0.60, 0.20, 0.80, 1.0),
PassKind::DepthOfField => Vec4::new(0.80, 0.20, 0.50, 1.0),
PassKind::MotionBlur => Vec4::new(0.50, 0.50, 0.80, 1.0),
PassKind::VolumetricFog=> Vec4::new(0.50, 0.70, 0.90, 1.0),
PassKind::Particle => Vec4::new(0.90, 0.60, 0.30, 1.0),
PassKind::UI => Vec4::new(0.30, 0.80, 0.30, 1.0),
PassKind::Debug => Vec4::new(0.80, 0.20, 0.20, 1.0),
PassKind::Custom => Vec4::new(0.50, 0.50, 0.50, 1.0),
}
}
#[derive(Debug, Clone)]
pub struct CompiledRenderGraph {
pub sorted_passes: Vec<PassId>,
pub dead_passes: Vec<PassId>,
pub barriers: HashMap<PassId, PipelineBarrier>,
pub resource_lifetimes: HashMap<ResourceId, (usize, usize)>,
pub aliasing_groups: Vec<Vec<ResourceId>>,
pub estimated_memory_bytes: u64,
pub estimated_bandwidth_mb: f32,
}
pub struct RenderGraphCompiler {
pass_map: HashMap<PassId, PassNode>,
resource_map: HashMap<ResourceId, RenderGraphResource>,
}
impl RenderGraphCompiler {
pub fn new() -> Self {
RenderGraphCompiler { pass_map: HashMap::new(), resource_map: HashMap::new() }
}
pub fn add_pass(&mut self, pass: PassNode) {
self.pass_map.insert(pass.id, pass);
}
pub fn add_resource(&mut self, res: RenderGraphResource) {
self.resource_map.insert(res.id, res);
}
pub fn compile(&mut self, output_resources: &[ResourceId]) -> Result<CompiledRenderGraph, String> {
let edges = self.build_dependency_edges();
let (acyclic_edges, removed_edges) = self.remove_cycles(&edges);
if !removed_edges.is_empty() {
}
let sorted = self.kahn_topological_sort(&acyclic_edges)?;
let live_passes = self.mark_live_passes(&sorted, output_resources, &acyclic_edges);
let dead_passes: Vec<PassId> = sorted.iter().filter(|p| !live_passes.contains(p)).cloned().collect();
let sorted_live: Vec<PassId> = sorted.iter().filter(|p| live_passes.contains(p)).cloned().collect();
let mut pass_index: HashMap<PassId, usize> = HashMap::new();
for (i, pid) in sorted_live.iter().enumerate() { pass_index.insert(*pid, i); }
let resource_lifetimes = self.compute_resource_lifetimes(&sorted_live, &pass_index);
let mut rm = self.resource_map.clone();
for (rid, (first, last)) in &resource_lifetimes {
if let Some(res) = rm.get_mut(rid) {
res.first_use = *first;
res.last_use = *last;
}
}
let aliasing_groups = self.compute_aliasing_groups(&rm, &resource_lifetimes);
let barriers = self.insert_barriers(&sorted_live, &rm);
let estimated_memory_bytes = self.estimate_memory_usage(&rm, &aliasing_groups);
let estimated_bandwidth_mb = self.estimate_bandwidth_mb(&sorted_live);
Ok(CompiledRenderGraph {
sorted_passes: sorted_live,
dead_passes,
barriers,
resource_lifetimes,
aliasing_groups,
estimated_memory_bytes,
estimated_bandwidth_mb,
})
}
fn build_dependency_edges(&self) -> HashMap<PassId, Vec<PassId>> {
let mut resource_writers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
let mut resource_readers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
for (pid, pass) in &self.pass_map {
for rid in &pass.writes { resource_writers.entry(*rid).or_default().push(*pid); }
for rid in &pass.reads { resource_readers.entry(*rid).or_default().push(*pid); }
}
let mut edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
for pid in self.pass_map.keys() { edges.insert(*pid, vec![]); }
for (rid, writers) in &resource_writers {
if let Some(readers) = resource_readers.get(rid) {
for w in writers {
for r in readers {
if w != r {
edges.entry(*w).or_default().push(*r);
}
}
}
}
}
for v in edges.values_mut() { v.sort_unstable_by_key(|p| p.0); v.dedup(); }
edges
}
fn remove_cycles(&self, edges: &HashMap<PassId, Vec<PassId>>) -> (HashMap<PassId, Vec<PassId>>, Vec<(PassId, PassId)>) {
let mut visited: HashSet<PassId> = HashSet::new();
let mut in_stack: HashSet<PassId> = HashSet::new();
let mut removed: Vec<(PassId, PassId)> = Vec::new();
let mut result = edges.clone();
let keys: Vec<PassId> = edges.keys().cloned().collect();
fn dfs(node: PassId, edges: &mut HashMap<PassId, Vec<PassId>>, visited: &mut HashSet<PassId>, in_stack: &mut HashSet<PassId>, removed: &mut Vec<(PassId, PassId)>) {
visited.insert(node);
in_stack.insert(node);
let neighbors: Vec<PassId> = edges.get(&node).cloned().unwrap_or_default();
for nbr in neighbors {
if in_stack.contains(&nbr) {
if let Some(v) = edges.get_mut(&node) { v.retain(|x| *x != nbr); }
removed.push((node, nbr));
} else if !visited.contains(&nbr) {
dfs(nbr, edges, visited, in_stack, removed);
}
}
in_stack.remove(&node);
}
for key in keys {
if !visited.contains(&key) {
dfs(key, &mut result, &mut visited, &mut in_stack, &mut removed);
}
}
(result, removed)
}
fn kahn_topological_sort(&self, edges: &HashMap<PassId, Vec<PassId>>) -> Result<Vec<PassId>, String> {
let mut in_degree: HashMap<PassId, usize> = HashMap::new();
for pid in edges.keys() { in_degree.insert(*pid, 0); }
for succs in edges.values() {
for s in succs {
*in_degree.entry(*s).or_insert(0) += 1;
}
}
let mut queue: VecDeque<PassId> = in_degree.iter().filter(|(_, &d)| d == 0).map(|(&p, _)| p).collect();
let mut queue_vec: Vec<PassId> = queue.drain(..).collect();
queue_vec.sort_unstable_by_key(|p| p.0);
queue.extend(queue_vec);
let mut sorted: Vec<PassId> = Vec::new();
while let Some(node) = queue.pop_front() {
sorted.push(node);
if let Some(succs) = edges.get(&node) {
let mut new_zeros: Vec<PassId> = Vec::new();
for s in succs {
let deg = in_degree.entry(*s).or_insert(0);
*deg = deg.saturating_sub(1);
if *deg == 0 { new_zeros.push(*s); }
}
new_zeros.sort_unstable_by_key(|p| p.0);
for z in new_zeros { queue.push_back(z); }
}
}
if sorted.len() != self.pass_map.len() {
Err(format!("Topological sort failed: cycle detected ({} of {} passes sorted)", sorted.len(), self.pass_map.len()))
} else {
Ok(sorted)
}
}
fn mark_live_passes(&self, sorted: &[PassId], outputs: &[ResourceId], edges: &HashMap<PassId, Vec<PassId>>) -> HashSet<PassId> {
let mut rev_edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
for (src, dsts) in edges {
for dst in dsts {
rev_edges.entry(*dst).or_default().push(*src);
}
}
let mut live: HashSet<PassId> = HashSet::new();
let mut queue: VecDeque<PassId> = VecDeque::new();
for pid in sorted {
if let Some(pass) = self.pass_map.get(pid) {
for out in outputs {
if pass.writes.contains(out) {
if live.insert(*pid) { queue.push_back(*pid); }
}
}
}
}
while let Some(pid) = queue.pop_front() {
if let Some(preds) = rev_edges.get(&pid) {
for pred in preds {
if live.insert(*pred) { queue.push_back(*pred); }
}
}
}
live
}
fn compute_resource_lifetimes(&self, sorted: &[PassId], pass_index: &HashMap<PassId, usize>) -> HashMap<ResourceId, (usize, usize)> {
let mut lifetimes: HashMap<ResourceId, (usize, usize)> = HashMap::new();
for (pid, pass) in &self.pass_map {
let idx = match pass_index.get(pid) { Some(&i) => i, None => continue };
for rid in pass.reads.iter().chain(pass.writes.iter()) {
let entry = lifetimes.entry(*rid).or_insert((usize::MAX, 0));
if idx < entry.0 { entry.0 = idx; }
if idx > entry.1 { entry.1 = idx; }
}
}
lifetimes
}
fn compute_aliasing_groups(&self, rm: &HashMap<ResourceId, RenderGraphResource>, lifetimes: &HashMap<ResourceId, (usize, usize)>) -> Vec<Vec<ResourceId>> {
let mut transient_res: Vec<ResourceId> = rm.values()
.filter(|r| r.lifetime == ResourceLifetime::Transient)
.map(|r| r.id)
.collect();
transient_res.sort_by_key(|id| lifetimes.get(id).map(|l| l.0).unwrap_or(usize::MAX));
let mut groups: Vec<(Vec<ResourceId>, usize)> = Vec::new(); for rid in &transient_res {
let (start, end) = lifetimes.get(rid).copied().unwrap_or((0, 0));
let res = rm.get(rid).unwrap();
let mut placed = false;
for (group_ids, group_end) in &mut groups {
if *group_end < start {
let first = group_ids.first().and_then(|id| rm.get(id)).unwrap();
if res.can_alias_with(first) {
group_ids.push(*rid);
if end > *group_end { *group_end = end; }
placed = true;
break;
}
}
}
if !placed {
groups.push((vec![*rid], end));
}
}
groups.into_iter().map(|(ids, _)| ids).collect()
}
fn insert_barriers(&self, sorted: &[PassId], rm: &HashMap<ResourceId, RenderGraphResource>) -> HashMap<PassId, PipelineBarrier> {
let mut result: HashMap<PassId, PipelineBarrier> = HashMap::new();
let mut current_layouts: HashMap<ResourceId, ImageLayout> = HashMap::new();
for res in rm.values() {
current_layouts.insert(res.id, res.current_layout);
}
for pid in sorted {
let pass = match self.pass_map.get(pid) { Some(p) => p, None => continue };
let mut barrier = PipelineBarrier::new();
for rid in &pass.reads {
let res = match rm.get(rid) { Some(r) => r, None => continue };
if !res.is_texture() { continue; }
let desc = match res.texture_desc() { Some(d) => d, None => continue };
let fi = format_info(desc.format);
let required_layout = if fi.is_depth || fi.is_stencil {
ImageLayout::DepthStencilReadOnlyOptimal
} else {
ImageLayout::ShaderReadOnlyOptimal
};
let old_layout = *current_layouts.get(rid).unwrap_or(&ImageLayout::Undefined);
if old_layout != required_layout {
barrier.image_barriers.push(ImageBarrier::layout_transition(*rid, old_layout, required_layout));
current_layouts.insert(*rid, required_layout);
}
}
for rid in &pass.writes {
let res = match rm.get(rid) { Some(r) => r, None => continue };
if !res.is_texture() { continue; }
let desc = match res.texture_desc() { Some(d) => d, None => continue };
let fi = format_info(desc.format);
let required_layout = if fi.is_depth || fi.is_stencil {
ImageLayout::DepthStencilAttachmentOptimal
} else {
ImageLayout::ColorAttachmentOptimal
};
let old_layout = *current_layouts.get(rid).unwrap_or(&ImageLayout::Undefined);
if old_layout != required_layout {
barrier.image_barriers.push(ImageBarrier::layout_transition(*rid, old_layout, required_layout));
current_layouts.insert(*rid, required_layout);
}
}
result.insert(*pid, barrier);
}
result
}
fn estimate_memory_usage(&self, rm: &HashMap<ResourceId, RenderGraphResource>, aliasing_groups: &[Vec<ResourceId>]) -> u64 {
let mut total: u64 = 0;
for res in rm.values() {
if res.lifetime != ResourceLifetime::Transient {
total += match &res.desc {
ResourceDesc::Texture(t) => t.size_bytes(),
ResourceDesc::Buffer(b) => b.size,
};
}
}
for group in aliasing_groups {
let max_size = group.iter().filter_map(|id| rm.get(id)).map(|r| match &r.desc {
ResourceDesc::Texture(t) => t.size_bytes(),
ResourceDesc::Buffer(b) => b.size,
}).max().unwrap_or(0);
total += max_size;
}
total
}
fn estimate_bandwidth_mb(&self, sorted: &[PassId]) -> f32 {
let mut bw: f32 = 0.0;
for pid in sorted {
if let Some(pass) = self.pass_map.get(pid) {
match &pass.desc {
PassDesc::GBuffer(d) => bw += d.estimate_write_bandwidth_mb(),
PassDesc::Lighting(d) => {
let pixels = (d.width * d.height) as f32;
let bpp = format_info(d.output_format).bytes_per_pixel();
bw += bpp * pixels / (1024.0 * 1024.0);
let gbuf_bytes: f32 = (4.0 + 8.0 + 4.0 + 4.0 + 4.0) * pixels; bw += gbuf_bytes / (1024.0 * 1024.0);
}
PassDesc::SSAO(d) => {
let ew = d.effective_width() as f32;
let eh = d.effective_height() as f32;
bw += format_info(d.output_format).bytes_per_pixel() * ew * eh / (1024.0*1024.0);
}
PassDesc::Bloom(d) => {
let mut bloom_bw = 0.0f32;
for mip in 0..d.mip_levels {
let (w, h) = d.mip_size(mip);
bloom_bw += format_info(d.output_format).bytes_per_pixel() * (w * h) as f32;
}
bw += bloom_bw * 2.0 / (1024.0 * 1024.0); }
_ => {
if let Some(pass) = self.pass_map.get(pid) {
let r = pass.reads.len() as f32;
let w_c = pass.writes.len() as f32;
bw += (r + w_c) * 4.0 * 1920.0 * 1080.0 / (1024.0 * 1024.0);
}
}
}
}
}
bw
}
}
#[derive(Debug, Clone)]
pub enum ValidationError {
MissingResource { pass: PassId, resource: ResourceId },
WrittenWithoutRead { resource: ResourceId },
IncompatibleFormats { pass: PassId, resource: ResourceId, expected: TextureFormat, actual: TextureFormat },
CyclicDependency { passes: Vec<PassId> },
ResourceSizeMismatch { resource: ResourceId, expected: (u32, u32), actual: (u32, u32) },
TooManyCascades { pass: PassId, count: u32 },
InvalidBlendState { pass: PassId, attachment_index: u32 },
DuplicatePassId(PassId),
DuplicateResourceId(ResourceId),
}
#[derive(Debug)]
pub struct ValidationReport {
pub errors: Vec<ValidationError>,
pub warnings: Vec<String>,
}
impl ValidationReport {
pub fn new() -> Self { ValidationReport { errors: vec![], warnings: vec![] } }
pub fn is_valid(&self) -> bool { self.errors.is_empty() }
pub fn error(&mut self, e: ValidationError) { self.errors.push(e); }
pub fn warn(&mut self, s: &str) { self.warnings.push(s.to_owned()); }
}
pub struct RenderGraphValidator<'a> {
pass_map: &'a HashMap<PassId, PassNode>,
resource_map: &'a HashMap<ResourceId, RenderGraphResource>,
}
impl<'a> RenderGraphValidator<'a> {
pub fn new(pass_map: &'a HashMap<PassId, PassNode>, resource_map: &'a HashMap<ResourceId, RenderGraphResource>) -> Self {
RenderGraphValidator { pass_map, resource_map }
}
pub fn validate(&self) -> ValidationReport {
let mut report = ValidationReport::new();
self.check_duplicate_ids(&mut report);
self.check_missing_resources(&mut report);
self.check_format_compatibility(&mut report);
self.check_cascade_limits(&mut report);
self.check_blend_state(&mut report);
self.check_unread_writes(&mut report);
report
}
fn check_duplicate_ids(&self, report: &mut ValidationReport) {
let mut seen_pass: HashSet<PassId> = HashSet::new();
for pid in self.pass_map.keys() {
if !seen_pass.insert(*pid) { report.error(ValidationError::DuplicatePassId(*pid)); }
}
let mut seen_res: HashSet<ResourceId> = HashSet::new();
for rid in self.resource_map.keys() {
if !seen_res.insert(*rid) { report.error(ValidationError::DuplicateResourceId(*rid)); }
}
}
fn check_missing_resources(&self, report: &mut ValidationReport) {
for (pid, pass) in self.pass_map {
for rid in pass.reads.iter().chain(pass.writes.iter()) {
if !self.resource_map.contains_key(rid) {
report.error(ValidationError::MissingResource { pass: *pid, resource: *rid });
}
}
}
}
fn check_format_compatibility(&self, report: &mut ValidationReport) {
for (pid, pass) in self.pass_map {
match &pass.desc {
PassDesc::Lighting(d) => {
if let Some(res) = self.resource_map.get(&d.input_depth) {
if let Some(t) = res.texture_desc() {
let fi = format_info(t.format);
if !fi.is_depth {
report.error(ValidationError::IncompatibleFormats {
pass: *pid, resource: d.input_depth,
expected: TextureFormat::Depth24UnormStencil8,
actual: t.format,
});
}
}
}
}
PassDesc::SSAO(d) => {
if let Some(res) = self.resource_map.get(&d.input_depth) {
if let Some(t) = res.texture_desc() {
let fi = format_info(t.format);
if !fi.is_depth {
report.error(ValidationError::IncompatibleFormats {
pass: *pid, resource: d.input_depth,
expected: TextureFormat::Depth32Float,
actual: t.format,
});
}
}
}
}
_ => {}
}
}
}
fn check_cascade_limits(&self, report: &mut ValidationReport) {
for (pid, pass) in self.pass_map {
if let PassDesc::ShadowMap(d) = &pass.desc {
if d.cascade_count > 4 {
report.error(ValidationError::TooManyCascades { pass: *pid, count: d.cascade_count });
}
}
}
}
fn check_blend_state(&self, report: &mut ValidationReport) {
for (pid, pass) in self.pass_map {
if let PassDesc::Particle(d) = &pass.desc {
let b = &d.blend;
if b.blend_enable {
let src_zero = b.src_color_blend_factor == BlendFactor::Zero && b.dst_color_blend_factor == BlendFactor::Zero;
if src_zero {
report.error(ValidationError::InvalidBlendState { pass: *pid, attachment_index: 0 });
}
}
}
}
}
fn check_unread_writes(&self, report: &mut ValidationReport) {
let mut all_reads: HashSet<ResourceId> = HashSet::new();
for pass in self.pass_map.values() {
for rid in &pass.reads { all_reads.insert(*rid); }
}
for pass in self.pass_map.values() {
for rid in &pass.writes {
if !all_reads.contains(rid) {
report.warn(&format!("Resource {:?} is written but never read (pass: {:?})", rid, pass.id));
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct GraphLayout {
pub node_positions: HashMap<PassId, Vec2>,
pub node_sizes: HashMap<PassId, Vec2>,
pub edge_paths: HashMap<(PassId, PassId), Vec<Vec2>>,
pub layer_assignment: HashMap<PassId, i32>,
pub nodes_per_layer: BTreeMap<i32, Vec<PassId>>,
pub total_bounds: (Vec2, Vec2), }
pub struct SugiyamaLayout {
pub horizontal_gap: f32,
pub vertical_gap: f32,
pub node_width: f32,
pub node_height: f32,
pub crossing_minimization_rounds: u32,
}
impl SugiyamaLayout {
pub fn default() -> Self {
SugiyamaLayout {
horizontal_gap: 60.0,
vertical_gap: 40.0,
node_width: 200.0,
node_height: 80.0,
crossing_minimization_rounds: 24,
}
}
pub fn layout(&self, passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> GraphLayout {
let layers = self.assign_layers(passes, edges);
let mut nodes_per_layer: BTreeMap<i32, Vec<PassId>> = BTreeMap::new();
for (pid, &layer) in &layers {
nodes_per_layer.entry(layer).or_default().push(*pid);
}
for v in nodes_per_layer.values_mut() {
v.sort_by_key(|p| p.0);
}
self.minimize_crossings(edges, &layers, &mut nodes_per_layer);
let positions = self.assign_positions(&layers, &nodes_per_layer);
let paths = self.route_edges(passes, edges, &positions);
let mut min_pos = Vec2::splat(f32::MAX);
let mut max_pos = Vec2::splat(f32::MIN);
for &pos in positions.values() {
min_pos = min_pos.min(pos);
max_pos = max_pos.max(pos + Vec2::new(self.node_width, self.node_height));
}
let mut sizes: HashMap<PassId, Vec2> = HashMap::new();
for pid in passes { sizes.insert(*pid, Vec2::new(self.node_width, self.node_height)); }
GraphLayout {
node_positions: positions,
node_sizes: sizes,
edge_paths: paths,
layer_assignment: layers,
nodes_per_layer,
total_bounds: (min_pos, max_pos),
}
}
fn assign_layers(&self, passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> HashMap<PassId, i32> {
let mut pred: HashMap<PassId, Vec<PassId>> = HashMap::new();
for pid in passes { pred.insert(*pid, vec![]); }
for (src, dsts) in edges {
for dst in dsts {
pred.entry(*dst).or_default().push(*src);
}
}
let mut layers: HashMap<PassId, i32> = HashMap::new();
for pid in passes {
let preds = pred.get(pid).cloned().unwrap_or_default();
let layer = if preds.is_empty() {
0
} else {
preds.iter().filter_map(|p| layers.get(p)).max().copied().unwrap_or(0) + 1
};
layers.insert(*pid, layer);
}
layers
}
fn minimize_crossings(
&self,
edges: &HashMap<PassId, Vec<PassId>>,
layers: &HashMap<PassId, i32>,
nodes_per_layer: &mut BTreeMap<i32, Vec<PassId>>,
) {
let mut rev_edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
for (src, dsts) in edges {
for dst in dsts {
rev_edges.entry(*dst).or_default().push(*src);
}
}
let max_layer = *layers.values().max().unwrap_or(&0);
for _round in 0..self.crossing_minimization_rounds {
for layer_idx in 1..=max_layer {
let prev_layer_idx = layer_idx - 1;
let prev_positions: HashMap<PassId, usize> = nodes_per_layer
.get(&prev_layer_idx)
.map(|v| v.iter().enumerate().map(|(i, p)| (*p, i)).collect())
.unwrap_or_default();
if let Some(nodes) = nodes_per_layer.get_mut(&layer_idx) {
nodes.sort_by(|a, b| {
let ba = barycenter(*a, &rev_edges, &prev_positions);
let bb = barycenter(*b, &rev_edges, &prev_positions);
ba.partial_cmp(&bb).unwrap_or(std::cmp::Ordering::Equal)
});
}
}
for layer_idx in (0..max_layer).rev() {
let next_layer_idx = layer_idx + 1;
let next_positions: HashMap<PassId, usize> = nodes_per_layer
.get(&next_layer_idx)
.map(|v| v.iter().enumerate().map(|(i, p)| (*p, i)).collect())
.unwrap_or_default();
if let Some(nodes) = nodes_per_layer.get_mut(&layer_idx) {
nodes.sort_by(|a, b| {
let ba = barycenter(*a, edges, &next_positions);
let bb = barycenter(*b, edges, &next_positions);
ba.partial_cmp(&bb).unwrap_or(std::cmp::Ordering::Equal)
});
}
}
}
}
fn assign_positions(&self, layers: &HashMap<PassId, i32>, nodes_per_layer: &BTreeMap<i32, Vec<PassId>>) -> HashMap<PassId, Vec2> {
let mut positions: HashMap<PassId, Vec2> = HashMap::new();
for (layer_idx, nodes) in nodes_per_layer {
let x = *layer_idx as f32 * (self.node_width + self.horizontal_gap);
let total_height = nodes.len() as f32 * (self.node_height + self.vertical_gap);
let start_y = -total_height / 2.0; for (i, pid) in nodes.iter().enumerate() {
let y = start_y + i as f32 * (self.node_height + self.vertical_gap);
positions.insert(*pid, Vec2::new(x, y));
}
}
positions
}
fn route_edges(
&self,
passes: &[PassId],
edges: &HashMap<PassId, Vec<PassId>>,
positions: &HashMap<PassId, Vec2>,
) -> HashMap<(PassId, PassId), Vec<Vec2>> {
let mut paths = HashMap::new();
let half_w = self.node_width * 0.5;
let half_h = self.node_height * 0.5;
for (src, dsts) in edges {
for dst in dsts {
let src_pos = match positions.get(src) { Some(p) => *p, None => continue };
let dst_pos = match positions.get(dst) { Some(p) => *p, None => continue };
let p0 = src_pos + Vec2::new(self.node_width, half_h);
let p3 = dst_pos + Vec2::new(0.0, half_h);
let ctrl_dist = (p3.x - p0.x).abs() * 0.5;
let p1 = p0 + Vec2::new(ctrl_dist, 0.0);
let p2 = p3 - Vec2::new(ctrl_dist, 0.0);
let points = tessellate_cubic_bezier(p0, p1, p2, p3, 16);
paths.insert((*src, *dst), points);
}
}
paths
}
}
fn barycenter(node: PassId, edges: &HashMap<PassId, Vec<PassId>>, neighbor_positions: &HashMap<PassId, usize>) -> f32 {
let neighbors: Vec<PassId> = edges.get(&node).cloned().unwrap_or_default();
if neighbors.is_empty() { return 0.0; }
let sum: f32 = neighbors.iter().filter_map(|n| neighbor_positions.get(n)).map(|&i| i as f32).sum();
sum / neighbors.len() as f32
}
fn tessellate_cubic_bezier(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, steps: u32) -> Vec<Vec2> {
let mut pts = Vec::with_capacity(steps as usize + 1);
for i in 0..=steps {
let t = i as f32 / steps as f32;
let mt = 1.0 - t;
let pos = p0 * (mt*mt*mt) + p1 * (3.0*mt*mt*t) + p2 * (3.0*mt*t*t) + p3 * (t*t*t);
pts.push(pos);
}
pts
}
#[derive(Debug, Clone)]
pub struct PassStatistics {
pub pass_id: PassId,
pub gpu_time_ms: f32,
pub cpu_time_ms: f32,
pub draw_calls: u32,
pub triangle_count: u64,
pub primitive_overdraw_estimate: f32, pub bandwidth_read_mb: f32,
pub bandwidth_write_mb: f32,
pub texture_cache_miss_rate: f32, pub barrier_count: u32,
pub render_target_clears: u32,
}
impl PassStatistics {
pub fn new(pass_id: PassId) -> Self {
PassStatistics {
pass_id, gpu_time_ms: 0.0, cpu_time_ms: 0.0, draw_calls: 0, triangle_count: 0,
primitive_overdraw_estimate: 1.0, bandwidth_read_mb: 0.0, bandwidth_write_mb: 0.0,
texture_cache_miss_rate: 0.0, barrier_count: 0, render_target_clears: 0,
}
}
pub fn total_bandwidth_mb(&self) -> f32 { self.bandwidth_read_mb + self.bandwidth_write_mb }
pub fn pixels_per_ms(&self, width: u32, height: u32) -> f32 {
if self.gpu_time_ms < 1e-6 { return 0.0; }
(width * height) as f32 / self.gpu_time_ms
}
}
#[derive(Debug, Clone)]
pub struct FrameStatistics {
pub pass_stats: HashMap<PassId, PassStatistics>,
pub total_gpu_time_ms: f32,
pub total_cpu_time_ms: f32,
pub total_draw_calls: u32,
pub total_triangles: u64,
pub total_bandwidth_mb: f32,
pub frame_time_ms: f32,
pub fps: f32,
}
impl FrameStatistics {
pub fn new() -> Self {
FrameStatistics {
pass_stats: HashMap::new(),
total_gpu_time_ms: 0.0, total_cpu_time_ms: 0.0,
total_draw_calls: 0, total_triangles: 0, total_bandwidth_mb: 0.0,
frame_time_ms: 0.0, fps: 0.0,
}
}
pub fn aggregate(&mut self) {
self.total_gpu_time_ms = self.pass_stats.values().map(|s| s.gpu_time_ms).sum();
self.total_cpu_time_ms = self.pass_stats.values().map(|s| s.cpu_time_ms).sum();
self.total_draw_calls = self.pass_stats.values().map(|s| s.draw_calls).sum();
self.total_triangles = self.pass_stats.values().map(|s| s.triangle_count).sum();
self.total_bandwidth_mb = self.pass_stats.values().map(|s| s.total_bandwidth_mb()).sum();
if self.frame_time_ms > 1e-6 { self.fps = 1000.0 / self.frame_time_ms; }
}
pub fn bottleneck_pass(&self) -> Option<PassId> {
self.pass_stats.values().max_by(|a, b| a.gpu_time_ms.partial_cmp(&b.gpu_time_ms).unwrap_or(std::cmp::Ordering::Equal)).map(|s| s.pass_id)
}
pub fn bandwidth_budget_used(&self, budget_gb_s: f32, frame_time_ms: f32) -> f32 {
let available_mb = budget_gb_s * 1024.0 * frame_time_ms / 1000.0;
if available_mb < 1e-6 { 0.0 } else { self.total_bandwidth_mb / available_mb }
}
}
pub struct OverdrawEstimator {
pub tile_size: u32,
pub max_depth: u32,
}
impl OverdrawEstimator {
pub fn new(tile_size: u32) -> Self { OverdrawEstimator { tile_size, max_depth: 32 } }
pub fn estimate_triangle_overdraw(triangles: &[(Vec2, Vec2, Vec2)], width: u32, height: u32, tile_size: u32) -> f32 {
let tw = ((width + tile_size - 1) / tile_size) as usize;
let th = ((height + tile_size - 1) / tile_size) as usize;
let mut tile_counts = vec![0u32; tw * th];
for (a, b, c) in triangles {
let min_x = a.x.min(b.x).min(c.x).max(0.0) as u32;
let min_y = a.y.min(b.y).min(c.y).max(0.0) as u32;
let max_x = (a.x.max(b.x).max(c.x) as u32).min(width - 1);
let max_y = (a.y.max(b.y).max(c.y) as u32).min(height - 1);
let t_min_x = (min_x / tile_size) as usize;
let t_min_y = (min_y / tile_size) as usize;
let t_max_x = ((max_x / tile_size) as usize).min(tw - 1);
let t_max_y = ((max_y / tile_size) as usize).min(th - 1);
for ty in t_min_y..=t_max_y {
for tx in t_min_x..=t_max_x {
tile_counts[ty * tw + tx] += 1;
}
}
}
let total_count: u64 = tile_counts.iter().map(|&c| c as u64).sum();
let total_tiles = (tw * th) as f64;
total_count as f32 / total_tiles as f32
}
pub fn estimate_gbuffer_overdraw(draw_calls: u32, avg_triangle_screen_coverage: f32, width: u32, height: u32) -> f32 {
let total_pixels_shaded = draw_calls as f32 * avg_triangle_screen_coverage * (width * height) as f32;
let screen_pixels = (width * height) as f32;
total_pixels_shaded / screen_pixels
}
}
#[derive(Debug, Clone)]
pub struct SubpassDependency {
pub src_subpass: u32,
pub dst_subpass: u32,
pub src_stage: PipelineStageFlags,
pub dst_stage: PipelineStageFlags,
pub src_access: AccessFlags,
pub dst_access: AccessFlags,
pub by_region: bool,
}
impl SubpassDependency {
pub const SUBPASS_EXTERNAL: u32 = u32::MAX;
pub fn external_to_color(dst_subpass: u32) -> Self {
SubpassDependency {
src_subpass: Self::SUBPASS_EXTERNAL,
dst_subpass,
src_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
dst_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
src_access: AccessFlags::NONE,
dst_access: AccessFlags::COLOR_ATTACHMENT_WRITE | AccessFlags::COLOR_ATTACHMENT_READ,
by_region: false,
}
}
pub fn color_to_external(src_subpass: u32) -> Self {
SubpassDependency {
src_subpass,
dst_subpass: Self::SUBPASS_EXTERNAL,
src_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
dst_stage: PipelineStageFlags::BOTTOM_OF_PIPE,
src_access: AccessFlags::COLOR_ATTACHMENT_WRITE,
dst_access: AccessFlags::NONE,
by_region: false,
}
}
pub fn input_attachment(src_subpass: u32, dst_subpass: u32) -> Self {
SubpassDependency {
src_subpass,
dst_subpass,
src_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
dst_stage: PipelineStageFlags::FRAGMENT_SHADER,
src_access: AccessFlags::COLOR_ATTACHMENT_WRITE,
dst_access: AccessFlags::INPUT_ATTACHMENT_READ,
by_region: true, }
}
pub fn is_tbr_friendly(&self) -> bool {
self.by_region
}
}
#[derive(Debug, Clone)]
pub struct SubpassDescription {
pub index: u32,
pub input_attachments: Vec<u32>, pub color_attachments: Vec<u32>,
pub resolve_attachments: Vec<u32>,
pub depth_stencil_attachment: Option<u32>,
pub preserve_attachments: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct RenderPassDescription {
pub attachments: Vec<AttachmentDescription>,
pub subpasses: Vec<SubpassDescription>,
pub dependencies: Vec<SubpassDependency>,
}
impl RenderPassDescription {
pub fn build_gbuffer_lighting_renderpass(
gbuf: &GBufferPassDesc,
light: &LightingPassDesc,
) -> Self {
let mut attachments = gbuf.attachment_descriptions();
attachments.push(AttachmentDescription {
format: light.output_format,
samples: SampleCount::S1,
load_op: LoadOp::DontCare,
store_op: StoreOp::Store,
stencil_load_op: LoadOp::DontCare,
stencil_store_op: StoreOp::DontCare,
initial_layout: ImageLayout::Undefined,
final_layout: ImageLayout::ColorAttachmentOptimal,
});
let lighting_att_idx = (attachments.len() - 1) as u32;
let subpasses = vec![
SubpassDescription {
index: 0,
input_attachments: vec![],
color_attachments: vec![0, 1, 2, 3], resolve_attachments: vec![],
depth_stencil_attachment: Some(4),
preserve_attachments: vec![],
},
SubpassDescription {
index: 1,
input_attachments: vec![0, 1, 2, 4], color_attachments: vec![lighting_att_idx],
resolve_attachments: vec![],
depth_stencil_attachment: None,
preserve_attachments: vec![3], },
];
let dependencies = vec![
SubpassDependency::external_to_color(0),
SubpassDependency::input_attachment(0, 1),
SubpassDependency::color_to_external(1),
];
RenderPassDescription { attachments, subpasses, dependencies }
}
pub fn detect_tbr_optimization(&self) -> bool {
self.dependencies.iter().all(|d|
d.src_subpass == SubpassDependency::SUBPASS_EXTERNAL ||
d.dst_subpass == SubpassDependency::SUBPASS_EXTERNAL ||
d.by_region
)
}
pub fn total_load_store_bandwidth_bytes(&self, width: u32, height: u32) -> u64 {
let pixels = (width * height) as u64;
let mut bw: u64 = 0;
for att in &self.attachments {
let fi = format_info(att.format);
let bpp = fi.bytes_per_block as u64;
if att.load_op == LoadOp::Load { bw += bpp * pixels; }
if att.store_op == StoreOp::Store { bw += bpp * pixels; }
}
bw
}
}
#[derive(Debug, Clone)]
pub struct SerializedNode {
pub id: u32,
pub name: String,
pub kind: String,
pub pos: [f32; 2],
pub size: [f32; 2],
pub color: [f32; 4],
pub enabled: bool,
pub reads: Vec<u32>,
pub writes: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct SerializedResource {
pub id: u32,
pub name: String,
pub kind: String,
pub format: String,
pub width: u32,
pub height: u32,
pub mip_levels: u32,
pub lifetime: String,
}
#[derive(Debug, Clone)]
pub struct SerializedRenderGraph {
pub version: u32,
pub name: String,
pub nodes: Vec<SerializedNode>,
pub resources: Vec<SerializedResource>,
pub connections: Vec<[u32; 2]>, }
impl SerializedRenderGraph {
pub fn serialize(editor: &RenderGraphEditor) -> Self {
let nodes: Vec<SerializedNode> = editor.passes.values().map(|p| SerializedNode {
id: p.id.0,
name: p.name.clone(),
kind: format!("{:?}", p.desc.kind()),
pos: [p.editor_pos.x, p.editor_pos.y],
size: [p.editor_size.x, p.editor_size.y],
color: [p.editor_color.x, p.editor_color.y, p.editor_color.z, p.editor_color.w],
enabled: p.enabled,
reads: p.reads.iter().map(|r| r.0).collect(),
writes: p.writes.iter().map(|r| r.0).collect(),
}).collect();
let resources: Vec<SerializedResource> = editor.resources.values().map(|r| {
let (fmt_str, w, h, mip) = match &r.desc {
ResourceDesc::Texture(t) => (format!("{:?}", t.format), t.width, t.height, t.mip_levels),
ResourceDesc::Buffer(_) => ("Buffer".to_owned(), 0, 0, 0),
};
SerializedResource {
id: r.id.0, name: r.name.clone(),
kind: if r.is_texture() { "Texture".to_owned() } else { "Buffer".to_owned() },
format: fmt_str, width: w, height: h, mip_levels: mip,
lifetime: format!("{:?}", r.lifetime),
}
}).collect();
let mut connections: Vec<[u32; 2]> = Vec::new();
for (src_id, src_pass) in &editor.passes {
for rid in &src_pass.writes {
for (dst_id, dst_pass) in &editor.passes {
if dst_pass.reads.contains(rid) {
connections.push([src_id.0, dst_id.0]);
}
}
}
}
connections.sort();
connections.dedup();
SerializedRenderGraph { version: 1, name: editor.name.clone(), nodes, resources, connections }
}
pub fn to_json_string(&self) -> String {
let mut s = String::new();
s.push_str("{\n");
s.push_str(&format!(" \"version\": {},\n", self.version));
s.push_str(&format!(" \"name\": \"{}\",\n", self.name));
s.push_str(" \"nodes\": [\n");
for (i, n) in self.nodes.iter().enumerate() {
s.push_str(&format!(" {{\"id\":{},\"name\":\"{}\",\"kind\":\"{}\",\"enabled\":{},\"pos\":[{:.1},{:.1}]}}", n.id, n.name, n.kind, n.enabled, n.pos[0], n.pos[1]));
if i + 1 < self.nodes.len() { s.push(','); }
s.push('\n');
}
s.push_str(" ],\n \"resources\": [\n");
for (i, r) in self.resources.iter().enumerate() {
s.push_str(&format!(" {{\"id\":{},\"name\":\"{}\",\"format\":\"{}\",\"w\":{},\"h\":{}}}", r.id, r.name, r.format, r.width, r.height));
if i + 1 < self.resources.len() { s.push(','); }
s.push('\n');
}
s.push_str(" ],\n \"connections\": [");
for (i, c) in self.connections.iter().enumerate() {
s.push_str(&format!("[{},{}]", c[0], c[1]));
if i + 1 < self.connections.len() { s.push(','); }
}
s.push_str("]\n}\n");
s
}
}
pub struct RenderGraphEditor {
pub name: String,
pub passes: HashMap<PassId, PassNode>,
pub resources: HashMap<ResourceId, RenderGraphResource>,
pub compiled: Option<CompiledRenderGraph>,
pub layout: Option<GraphLayout>,
pub stats: FrameStatistics,
pub validation_report: Option<ValidationReport>,
pub selected_pass: Option<PassId>,
pub selected_resource: Option<ResourceId>,
pub hover_pass: Option<PassId>,
pub drag_pass: Option<PassId>,
pub drag_offset: Vec2,
pub camera_pos: Vec2,
pub camera_zoom: f32,
pub show_resources: bool,
pub show_barriers: bool,
pub show_stats: bool,
pub show_validation: bool,
pub output_resources: Vec<ResourceId>,
next_pass_id: u32,
next_resource_id: u32,
}
impl RenderGraphEditor {
pub fn new(name: &str) -> Self {
RenderGraphEditor {
name: name.to_owned(),
passes: HashMap::new(),
resources: HashMap::new(),
compiled: None,
layout: None,
stats: FrameStatistics::new(),
validation_report: None,
selected_pass: None,
selected_resource: None,
hover_pass: None,
drag_pass: None,
drag_offset: Vec2::ZERO,
camera_pos: Vec2::ZERO,
camera_zoom: 1.0,
show_resources: true,
show_barriers: false,
show_stats: true,
show_validation: true,
output_resources: vec![],
next_pass_id: 0,
next_resource_id: 0,
}
}
pub fn alloc_resource_id(&mut self) -> ResourceId {
let id = ResourceId(self.next_resource_id);
self.next_resource_id += 1;
id
}
pub fn alloc_pass_id(&mut self) -> PassId {
let id = PassId(self.next_pass_id);
self.next_pass_id += 1;
id
}
pub fn add_resource(&mut self, name: &str, desc: ResourceDesc, lifetime: ResourceLifetime) -> ResourceId {
let id = self.alloc_resource_id();
let res = RenderGraphResource {
id, name: name.to_owned(), desc, lifetime,
first_use: usize::MAX, last_use: 0,
can_alias: lifetime == ResourceLifetime::Transient,
alias_target: None,
current_layout: ImageLayout::Undefined,
};
self.resources.insert(id, res);
id
}
pub fn add_transient_texture(&mut self, name: &str, desc: TextureDesc) -> ResourceId {
self.add_resource(name, ResourceDesc::Texture(desc), ResourceLifetime::Transient)
}
pub fn add_persistent_texture(&mut self, name: &str, desc: TextureDesc) -> ResourceId {
self.add_resource(name, ResourceDesc::Texture(desc), ResourceLifetime::Persistent)
}
pub fn add_pass(&mut self, name: &str, desc: PassDesc) -> PassId {
let id = self.alloc_pass_id();
let pass = PassNode::new(id, name, desc);
self.passes.insert(id, pass);
id
}
pub fn set_pass_reads(&mut self, pass: PassId, reads: Vec<ResourceId>) {
if let Some(p) = self.passes.get_mut(&pass) { p.reads = reads; }
}
pub fn set_pass_writes(&mut self, pass: PassId, writes: Vec<ResourceId>) {
if let Some(p) = self.passes.get_mut(&pass) { p.writes = writes; }
}
pub fn set_output_resources(&mut self, outputs: Vec<ResourceId>) {
self.output_resources = outputs;
}
pub fn compile(&mut self) -> Result<(), String> {
let mut compiler = RenderGraphCompiler::new();
for pass in self.passes.values() { compiler.add_pass(pass.clone()); }
for res in self.resources.values() { compiler.add_resource(res.clone()); }
let compiled = compiler.compile(&self.output_resources)?;
for (i, pid) in compiled.sorted_passes.iter().enumerate() {
if let Some(p) = self.passes.get_mut(pid) { p.execute_order = i; }
}
self.compiled = Some(compiled);
Ok(())
}
pub fn validate(&mut self) -> &ValidationReport {
let validator = RenderGraphValidator::new(&self.passes, &self.resources);
self.validation_report = Some(validator.validate());
self.validation_report.as_ref().unwrap()
}
pub fn visualize(&mut self) {
let layout_algo = SugiyamaLayout::default();
let passes: Vec<PassId> = self.passes.keys().cloned().collect();
let edges = self.build_edges();
self.layout = Some(layout_algo.layout(&passes, &edges));
if let Some(ref lay) = self.layout {
for (pid, pos) in &lay.node_positions {
if let Some(pass) = self.passes.get_mut(pid) {
pass.editor_pos = *pos;
}
}
}
}
fn build_edges(&self) -> HashMap<PassId, Vec<PassId>> {
let mut resource_writers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
let mut resource_readers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
for (pid, pass) in &self.passes {
for rid in &pass.writes { resource_writers.entry(*rid).or_default().push(*pid); }
for rid in &pass.reads { resource_readers.entry(*rid).or_default().push(*pid); }
}
let mut edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
for pid in self.passes.keys() { edges.insert(*pid, vec![]); }
for (rid, writers) in &resource_writers {
if let Some(readers) = resource_readers.get(rid) {
for w in writers {
for r in readers {
if w != r {
let v = edges.entry(*w).or_default();
if !v.contains(r) { v.push(*r); }
}
}
}
}
}
edges
}
pub fn serialize(&self) -> SerializedRenderGraph {
SerializedRenderGraph::serialize(self)
}
pub fn to_json(&self) -> String {
self.serialize().to_json_string()
}
pub fn build_standard_deferred_pipeline(width: u32, height: u32) -> RenderGraphEditor {
let mut editor = RenderGraphEditor::new("Standard Deferred");
let res_albedo = editor.add_transient_texture("GBuffer_Albedo", TextureDesc::render_target(width, height, TextureFormat::RGBA8Unorm));
let res_normal = editor.add_transient_texture("GBuffer_Normal", TextureDesc::render_target(width, height, TextureFormat::RG16Float));
let res_material = editor.add_transient_texture("GBuffer_Material", TextureDesc::render_target(width, height, TextureFormat::RGBA8Unorm));
let res_velocity = editor.add_transient_texture("GBuffer_Velocity", TextureDesc::render_target(width, height, TextureFormat::RG16Float));
let res_depth = editor.add_transient_texture("GBuffer_Depth", TextureDesc::depth_target(width, height));
let res_shadow = editor.add_transient_texture("ShadowMap", TextureDesc::shadow_map(4096));
let res_ao = editor.add_transient_texture("SSAO_AO", TextureDesc::render_target(width/2, height/2, TextureFormat::R8Unorm));
let res_hdr = editor.add_transient_texture("HDR_Color", TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
let res_bloom = editor.add_transient_texture("Bloom", TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
let res_ssr = editor.add_transient_texture("SSR", TextureDesc::render_target(width/2, height/2, TextureFormat::RGBA16Float));
let res_fog = editor.add_transient_texture("VolumetricFog", TextureDesc::render_target(width/8, height/8, TextureFormat::RGBA16Float));
let res_taa = editor.add_transient_texture("TAA_Resolved", TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
let res_sdr = editor.add_persistent_texture("SDR_Output", TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
let res_particles= editor.add_transient_texture("Particles", TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
let res_ui = editor.add_persistent_texture("UI_Output", TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
editor.set_output_resources(vec![res_ui]);
let mut sm_desc = ShadowMapPassDesc::directional_shadow(4096);
sm_desc.output_shadow_map = res_shadow;
let sm_pass = editor.add_pass("ShadowMap", PassDesc::ShadowMap(sm_desc));
editor.set_pass_writes(sm_pass, vec![res_shadow]);
let mut gbuf_desc = GBufferPassDesc::default(width, height);
gbuf_desc.output_albedo = res_albedo;
gbuf_desc.output_normal = res_normal;
gbuf_desc.output_material = res_material;
gbuf_desc.output_velocity = res_velocity;
gbuf_desc.output_depth = res_depth;
let gbuf_pass = editor.add_pass("GBuffer", PassDesc::GBuffer(gbuf_desc));
editor.set_pass_writes(gbuf_pass, vec![res_albedo, res_normal, res_material, res_velocity, res_depth]);
let mut ssao_desc = SSAOPassDesc::default(width, height);
ssao_desc.output_ao = res_ao;
ssao_desc.input_depth = res_depth;
ssao_desc.input_normal = res_normal;
let ssao_pass = editor.add_pass("SSAO", PassDesc::SSAO(ssao_desc));
editor.set_pass_reads(ssao_pass, vec![res_depth, res_normal]);
editor.set_pass_writes(ssao_pass, vec![res_ao]);
let mut fog_desc = VolumetricFogPassDesc::default(width, height);
fog_desc.output_fog = res_fog;
fog_desc.input_depth = res_depth;
fog_desc.input_shadow_map = res_shadow;
let fog_pass = editor.add_pass("VolumetricFog", PassDesc::VolumetricFog(fog_desc));
editor.set_pass_reads(fog_pass, vec![res_depth, res_shadow]);
editor.set_pass_writes(fog_pass, vec![res_fog]);
let mut light_desc = LightingPassDesc::default(width, height);
light_desc.output_hdr = res_hdr;
light_desc.input_albedo = res_albedo;
light_desc.input_normal = res_normal;
light_desc.input_material = res_material;
light_desc.input_depth = res_depth;
light_desc.input_shadow_map = res_shadow;
light_desc.input_ssao = res_ao;
let light_pass = editor.add_pass("Lighting", PassDesc::Lighting(light_desc));
editor.set_pass_reads(light_pass, vec![res_albedo, res_normal, res_material, res_depth, res_shadow, res_ao, res_fog]);
editor.set_pass_writes(light_pass, vec![res_hdr]);
let mut ssr_desc = SSRPassDesc::default(width, height);
ssr_desc.output_ssr = res_ssr;
ssr_desc.input_depth = res_depth;
ssr_desc.input_normal = res_normal;
ssr_desc.input_material = res_material;
ssr_desc.input_hdr = res_hdr;
let ssr_pass = editor.add_pass("SSR", PassDesc::SSR(ssr_desc));
editor.set_pass_reads(ssr_pass, vec![res_depth, res_normal, res_material, res_hdr]);
editor.set_pass_writes(ssr_pass, vec![res_ssr]);
let mut particle_desc = ParticlePassDesc::default(width, height);
particle_desc.output_particles = res_particles;
particle_desc.input_depth = res_depth;
particle_desc.input_hdr = res_hdr;
let particle_pass = editor.add_pass("Particles", PassDesc::Particle(particle_desc));
editor.set_pass_reads(particle_pass, vec![res_hdr, res_depth]);
editor.set_pass_writes(particle_pass, vec![res_particles]);
let mut bloom_desc = BloomPassDesc::default(width, height);
bloom_desc.output_bloom = res_bloom;
bloom_desc.input_hdr = res_hdr;
let bloom_pass = editor.add_pass("Bloom", PassDesc::Bloom(bloom_desc));
editor.set_pass_reads(bloom_pass, vec![res_hdr]);
editor.set_pass_writes(bloom_pass, vec![res_bloom]);
let mut tonemap_desc = ToneMappingPassDesc::default(width, height);
tonemap_desc.output_sdr = res_sdr;
tonemap_desc.input_hdr = res_hdr;
tonemap_desc.input_bloom = res_bloom;
let tonemap_pass = editor.add_pass("ToneMapping", PassDesc::ToneMapping(tonemap_desc));
editor.set_pass_reads(tonemap_pass, vec![res_hdr, res_bloom, res_ssr, res_particles]);
editor.set_pass_writes(tonemap_pass, vec![res_sdr]);
let mut taa_desc = TAAPassDesc::default(width, height);
taa_desc.output_resolved = res_taa;
taa_desc.input_current = res_sdr;
taa_desc.input_depth = res_depth;
taa_desc.input_velocity = res_velocity;
let taa_pass = editor.add_pass("TAA", PassDesc::TAA(taa_desc));
editor.set_pass_reads(taa_pass, vec![res_sdr, res_depth, res_velocity]);
editor.set_pass_writes(taa_pass, vec![res_taa]);
let mut ui_desc = UIPassDesc::default(width, height);
ui_desc.output_ui = res_ui;
ui_desc.input_scene = res_taa;
let ui_pass = editor.add_pass("UI", PassDesc::UI(ui_desc));
editor.set_pass_reads(ui_pass, vec![res_taa]);
editor.set_pass_writes(ui_pass, vec![res_ui]);
editor
}
pub fn screen_to_world(&self, screen: Vec2) -> Vec2 {
(screen - self.camera_pos) / self.camera_zoom
}
pub fn world_to_screen(&self, world: Vec2) -> Vec2 {
world * self.camera_zoom + self.camera_pos
}
pub fn zoom_around(&mut self, center: Vec2, delta: f32) {
let old_zoom = self.camera_zoom;
self.camera_zoom = (self.camera_zoom * (1.0 + delta * 0.1)).clamp(0.1, 8.0);
let zoom_ratio = self.camera_zoom / old_zoom;
self.camera_pos = center - (center - self.camera_pos) * zoom_ratio;
}
pub fn begin_drag_pass(&mut self, pass: PassId, mouse_pos: Vec2) {
if let Some(p) = self.passes.get(&pass) {
self.drag_pass = Some(pass);
self.drag_offset = self.screen_to_world(mouse_pos) - p.editor_pos;
}
}
pub fn update_drag(&mut self, mouse_pos: Vec2) {
if let Some(pid) = self.drag_pass {
let world_pos = self.screen_to_world(mouse_pos) - self.drag_offset;
if let Some(pass) = self.passes.get_mut(&pid) {
pass.editor_pos = world_pos;
}
}
}
pub fn end_drag(&mut self) {
self.drag_pass = None;
}
pub fn hit_test_pass(&self, mouse_pos: Vec2) -> Option<PassId> {
let world = self.screen_to_world(mouse_pos);
for pass in self.passes.values() {
let min = pass.editor_pos;
let max = pass.editor_pos + pass.editor_size;
if world.x >= min.x && world.x <= max.x && world.y >= min.y && world.y <= max.y {
return Some(pass.id);
}
}
None
}
pub fn get_pass_port_position(&self, pass: PassId, is_output: bool, port_index: u32) -> Vec2 {
if let Some(p) = self.passes.get(&pass) {
let x = if is_output { p.editor_pos.x + p.editor_size.x } else { p.editor_pos.x };
let y = p.editor_pos.y + (port_index as f32 + 0.5) * (p.editor_size.y / (p.reads.len().max(1) as f32));
return Vec2::new(x, y);
}
Vec2::ZERO
}
pub fn update_pass_stats(&mut self, pass_id: PassId, stats: PassStatistics) {
self.stats.pass_stats.insert(pass_id, stats);
self.stats.aggregate();
}
pub fn get_stats_summary(&self) -> String {
let mut s = String::new();
s.push_str(&format!("Total GPU: {:.2}ms FPS: {:.1}\n", self.stats.total_gpu_time_ms, self.stats.fps));
s.push_str(&format!("Draw Calls: {} Triangles: {}M\n", self.stats.total_draw_calls, self.stats.total_triangles / 1_000_000));
s.push_str(&format!("Bandwidth: {:.1}MB/frame\n", self.stats.total_bandwidth_mb));
if let Some(bp) = self.stats.bottleneck_pass() {
if let Some(ps) = self.stats.pass_stats.get(&bp) {
s.push_str(&format!("Bottleneck: Pass {:?} ({:.2}ms)\n", bp, ps.gpu_time_ms));
}
}
if let Some(ref compiled) = self.compiled {
s.push_str(&format!("Memory: {:.1}MB Bandwidth est: {:.1}MB\n",
compiled.estimated_memory_bytes as f32 / (1024.0*1024.0),
compiled.estimated_bandwidth_mb));
s.push_str(&format!("Dead passes: {}\n", compiled.dead_passes.len()));
}
s
}
pub fn get_barriers_for_pass(&self, pass_id: PassId) -> Vec<&ImageBarrier> {
if let Some(ref compiled) = self.compiled {
if let Some(barrier) = compiled.barriers.get(&pass_id) {
return barrier.image_barriers.iter().collect();
}
}
vec![]
}
pub fn count_total_barriers(&self) -> usize {
if let Some(ref compiled) = self.compiled {
compiled.barriers.values().map(|b| b.image_barriers.len() + b.buffer_barriers.len()).sum()
} else {
0
}
}
pub fn describe_compiled(&self) -> String {
let mut s = String::new();
let compiled = match &self.compiled { Some(c) => c, None => return "Not compiled".to_owned() };
s.push_str(&format!("=== {} Render Graph ===\n", self.name));
s.push_str(&format!("Passes ({}): ", compiled.sorted_passes.len()));
for pid in &compiled.sorted_passes {
if let Some(p) = self.passes.get(pid) { s.push_str(&format!("{} ", p.name)); }
}
s.push('\n');
if !compiled.dead_passes.is_empty() {
s.push_str("Dead passes: ");
for pid in &compiled.dead_passes {
if let Some(p) = self.passes.get(pid) { s.push_str(&format!("{} ", p.name)); }
}
s.push('\n');
}
s.push_str(&format!("Aliasing groups: {}\n", compiled.aliasing_groups.len()));
s.push_str(&format!("Estimated memory: {:.2} MB\n", compiled.estimated_memory_bytes as f64 / (1024.0*1024.0)));
s.push_str(&format!("Estimated bandwidth: {:.1} MB/frame\n", compiled.estimated_bandwidth_mb));
s.push_str(&format!("Total barriers: {}\n", self.count_total_barriers()));
s
}
}
pub fn lerp(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
pub fn clamp01(x: f32) -> f32 { x.clamp(0.0, 1.0) }
pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
let t = clamp01((x - edge0) / (edge1 - edge0 + 1e-7));
t * t * (3.0 - 2.0 * t)
}
pub fn halton_sequence(index: u32, base: u32) -> f32 {
let mut f = 1.0f32;
let mut r = 0.0f32;
let mut i = index;
while i > 0 {
f /= base as f32;
r += f * (i % base) as f32;
i /= base;
}
r
}
pub fn hex_distance(p: Vec2) -> f32 {
let q = Vec2::new(p.x.abs(), p.y.abs());
let s = 0.5f32;
let dot = q.x * s + q.y * (3.0f32).sqrt() * 0.5;
let a = q.x.max(dot);
a.max(q.y)
}
pub fn compute_mip_count(width: u32, height: u32) -> u32 {
(width.max(height) as f32).log2().floor() as u32 + 1
}
pub fn align_up(value: u64, alignment: u64) -> u64 {
(value + alignment - 1) & !(alignment - 1)
}
pub fn align_up_u32(value: u32, alignment: u32) -> u32 {
(value + alignment - 1) & !(alignment - 1)
}
pub fn srgb_to_linear(c: Vec3) -> Vec3 {
Vec3::new(
srgb_channel_to_linear(c.x),
srgb_channel_to_linear(c.y),
srgb_channel_to_linear(c.z),
)
}
pub fn srgb_channel_to_linear(c: f32) -> f32 {
if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
}
pub fn linear_to_srgb_channel(c: f32) -> f32 {
if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
}
pub fn linear_to_srgb(c: Vec3) -> Vec3 {
Vec3::new(linear_to_srgb_channel(c.x), linear_to_srgb_channel(c.y), linear_to_srgb_channel(c.z))
}
pub fn luminance(c: Vec3) -> f32 { 0.2126 * c.x + 0.7152 * c.y + 0.0722 * c.z }
pub fn ev100_to_exposure(ev100: f32) -> f32 { 1.0 / (1.2 * (2.0f32).powf(ev100)) }
pub fn reconstruct_world_pos(uv: Vec2, depth: f32, inv_view_proj: Mat4) -> Vec3 {
let ndc = Vec4::new(uv.x * 2.0 - 1.0, uv.y * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
let world_h = inv_view_proj * ndc;
world_h.truncate() / world_h.w
}
pub fn linearize_depth(depth: f32, near: f32, far: f32) -> f32 {
(2.0 * near * far) / (far + near - depth * (far - near))
}
pub fn world_to_screen_uv(world_pos: Vec3, view_proj: Mat4) -> Option<Vec2> {
let clip = view_proj * Vec4::new(world_pos.x, world_pos.y, world_pos.z, 1.0);
if clip.w < 1e-6 { return None; }
let ndc = clip / clip.w;
if ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 { return None; }
Some(Vec2::new(ndc.x * 0.5 + 0.5, ndc.y * 0.5 + 0.5))
}
pub fn octahedral_encode(n: Vec3) -> Vec2 {
let l1 = n.x.abs() + n.y.abs() + n.z.abs();
let p = Vec2::new(n.x / l1, n.y / l1);
if n.z < 0.0 {
let xp = (1.0 - p.y.abs()) * if p.x >= 0.0 { 1.0 } else { -1.0 };
let yp = (1.0 - p.x.abs()) * if p.y >= 0.0 { 1.0 } else { -1.0 };
Vec2::new(xp, yp)
} else {
p
}
}
pub fn octahedral_decode(e: Vec2) -> Vec3 {
let mut n = Vec3::new(e.x, e.y, 1.0 - e.x.abs() - e.y.abs());
if n.z < 0.0 {
let xp = (1.0 - n.y.abs()) * if n.x >= 0.0 { 1.0 } else { -1.0 };
let yp = (1.0 - n.x.abs()) * if n.y >= 0.0 { 1.0 } else { -1.0 };
n.x = xp;
n.y = yp;
}
n.normalize()
}
pub fn encode_velocity(velocity_pixels: Vec2, max_velocity: f32) -> Vec2 {
velocity_pixels / max_velocity * 0.5 + Vec2::splat(0.5)
}
pub fn decode_velocity(encoded: Vec2, max_velocity: f32) -> Vec2 {
(encoded - Vec2::splat(0.5)) * 2.0 * max_velocity
}
pub fn pack_material(metallic: f32, roughness: f32, ao: f32, emissive_scale: f32) -> u32 {
let m = (metallic.clamp(0.0, 1.0) * 255.0) as u32;
let r = (roughness.clamp(0.0, 1.0) * 255.0) as u32;
let a = (ao.clamp(0.0, 1.0) * 255.0) as u32;
let e = (emissive_scale.clamp(0.0, 1.0) * 255.0) as u32;
(e << 24) | (a << 16) | (r << 8) | m
}
pub fn unpack_material(packed: u32) -> (f32, f32, f32, f32) {
let m = (packed & 0xFF) as f32 / 255.0;
let r = ((packed >> 8) & 0xFF) as f32 / 255.0;
let a = ((packed >> 16) & 0xFF) as f32 / 255.0;
let e = ((packed >> 24) & 0xFF) as f32 / 255.0;
(m, r, a, e)
}
pub fn ggx_distribution(n_dot_h: f32, roughness: f32) -> f32 {
let a = roughness * roughness;
let a2 = a * a;
let denom = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
a2 / (std::f32::consts::PI * denom * denom)
}
pub fn schlick_fresnel(cos_theta: f32, f0: Vec3) -> Vec3 {
f0 + (Vec3::ONE - f0) * (1.0 - cos_theta).powf(5.0)
}
pub fn smith_g1_ggx(n_dot_v: f32, roughness: f32) -> f32 {
let r = roughness + 1.0;
let k = (r * r) / 8.0;
n_dot_v / (n_dot_v * (1.0 - k) + k)
}
pub fn smith_g_ggx(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
smith_g1_ggx(n_dot_v, roughness) * smith_g1_ggx(n_dot_l, roughness)
}
pub fn cook_torrance_brdf(n: Vec3, v: Vec3, l: Vec3, albedo: Vec3, metallic: f32, roughness: f32) -> Vec3 {
let h = (v + l).normalize();
let n_dot_l = n.dot(l).max(0.0);
let n_dot_v = n.dot(v).max(0.0);
let n_dot_h = n.dot(h).max(0.0);
let h_dot_v = h.dot(v).max(0.0);
let f0 = lerp_vec3(Vec3::splat(0.04), albedo, metallic);
let d = ggx_distribution(n_dot_h, roughness);
let f = schlick_fresnel(h_dot_v, f0);
let g = smith_g_ggx(n_dot_v, n_dot_l, roughness);
let specular = (d * f * g) / (4.0 * n_dot_v * n_dot_l + 1e-7);
let k_s = f;
let k_d = (Vec3::ONE - k_s) * (1.0 - metallic);
let diffuse = k_d * albedo / std::f32::consts::PI;
(diffuse + specular) * n_dot_l
}
pub fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 { a + (b - a) * t }
pub fn integrate_brdf(n_dot_v: f32, roughness: f32, sample_count: u32) -> Vec2 {
let v = Vec3::new((1.0 - n_dot_v * n_dot_v).sqrt(), 0.0, n_dot_v);
let n = Vec3::Z;
let mut a = 0.0f32;
let mut b = 0.0f32;
for i in 0..sample_count {
let xi = Vec2::new(halton_sequence(i, 2), halton_sequence(i, 3));
let h = importance_sample_ggx(xi, n, roughness);
let l = (2.0 * v.dot(h) * h - v).normalize();
let n_dot_l = l.z.max(0.0);
let n_dot_h = h.z.max(0.0);
let v_dot_h = v.dot(h).max(0.0);
if n_dot_l > 0.0 {
let g = smith_g_ggx(n_dot_v, n_dot_l, roughness);
let g_vis = (g * v_dot_h) / (n_dot_h * n_dot_v + 1e-7);
let fc = (1.0 - v_dot_h).powf(5.0);
a += (1.0 - fc) * g_vis;
b += fc * g_vis;
}
}
Vec2::new(a / sample_count as f32, b / sample_count as f32)
}
pub fn importance_sample_ggx(xi: Vec2, n: Vec3, roughness: f32) -> Vec3 {
let a = roughness * roughness;
let phi = 2.0 * std::f32::consts::PI * xi.x;
let cos_theta = ((1.0 - xi.y) / (1.0 + (a*a - 1.0) * xi.y)).sqrt();
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
let h = Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta);
let up = if n.z.abs() < 0.999 { Vec3::Z } else { Vec3::X };
let tangent = up.cross(n).normalize();
let bitangent = n.cross(tangent);
(tangent * h.x + bitangent * h.y + n * h.z).normalize()
}
#[derive(Debug, Clone)]
pub struct ClusteredLightGrid {
pub tiles_x: u32,
pub tiles_y: u32,
pub depth_slices: u32,
pub tile_size: u32,
pub screen_width: u32,
pub screen_height: u32,
pub near: f32,
pub far: f32,
}
impl ClusteredLightGrid {
pub fn new(screen_width: u32, screen_height: u32, tile_size: u32, depth_slices: u32, near: f32, far: f32) -> Self {
ClusteredLightGrid {
tiles_x: (screen_width + tile_size - 1) / tile_size,
tiles_y: (screen_height + tile_size - 1) / tile_size,
depth_slices,
tile_size,
screen_width,
screen_height,
near,
far,
}
}
pub fn total_clusters(&self) -> u32 { self.tiles_x * self.tiles_y * self.depth_slices }
pub fn cluster_index(&self, tile_x: u32, tile_y: u32, depth_slice: u32) -> u32 {
tile_x + tile_y * self.tiles_x + depth_slice * self.tiles_x * self.tiles_y
}
pub fn depth_slice_from_linear(linear_depth: f32, near: f32, far: f32, num_slices: u32) -> u32 {
let s = (linear_depth / near).ln() / (far / near).ln();
((s * num_slices as f32) as u32).min(num_slices - 1)
}
pub fn cluster_view_aabb(&self, tile_x: u32, tile_y: u32, depth_slice: u32, proj: Mat4) -> (Vec3, Vec3) {
let x0 = (tile_x * self.tile_size) as f32 / self.screen_width as f32 * 2.0 - 1.0;
let x1 = ((tile_x + 1) * self.tile_size).min(self.screen_width) as f32 / self.screen_width as f32 * 2.0 - 1.0;
let y0 = (tile_y * self.tile_size) as f32 / self.screen_height as f32 * 2.0 - 1.0;
let y1 = ((tile_y + 1) * self.tile_size).min(self.screen_height) as f32 / self.screen_height as f32 * 2.0 - 1.0;
let z_near = Self::depth_slice_z(depth_slice, self.near, self.far, self.depth_slices);
let z_far = Self::depth_slice_z(depth_slice + 1, self.near, self.far, self.depth_slices);
let inv_proj = proj.inverse();
let ndc_to_view = |ndc: Vec4| -> Vec3 {
let v = inv_proj * ndc;
v.truncate() / v.w
};
let min_v = ndc_to_view(Vec4::new(x0, y0, z_near, 1.0));
let max_v = ndc_to_view(Vec4::new(x1, y1, z_far, 1.0));
(min_v.min(max_v), min_v.max(max_v))
}
fn depth_slice_z(slice: u32, near: f32, far: f32, num_slices: u32) -> f32 {
near * (far / near).powf(slice as f32 / num_slices as f32)
}
pub fn memory_requirements(&self, max_lights_per_cluster: u32) -> u64 {
let offset_buf = self.total_clusters() as u64 * 4;
let count_buf = self.total_clusters() as u64 * 4;
let index_buf = self.total_clusters() as u64 * max_lights_per_cluster as u64 * 2; offset_buf + count_buf + index_buf
}
}
#[derive(Debug, Clone)]
pub struct TemporalHistoryBuffer {
pub current_frame: u32,
pub history_count: u32,
pub resources: Vec<ResourceId>,
pub active_index: usize,
}
impl TemporalHistoryBuffer {
pub fn new(count: u32) -> Self {
TemporalHistoryBuffer { current_frame: 0, history_count: count, resources: Vec::new(), active_index: 0 }
}
pub fn current(&self) -> Option<ResourceId> { self.resources.get(self.active_index).cloned() }
pub fn previous(&self) -> Option<ResourceId> {
let prev = (self.active_index + self.resources.len() - 1) % self.resources.len().max(1);
self.resources.get(prev).cloned()
}
pub fn advance(&mut self) {
self.active_index = (self.active_index + 1) % self.resources.len().max(1);
self.current_frame += 1;
}
}
pub fn view_direction_from_uv(uv: Vec2, inv_proj: Mat4) -> Vec3 {
let ndc = Vec4::new(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, -1.0, 1.0);
let view_h = inv_proj * ndc;
let view = view_h.truncate() / view_h.w;
view.normalize()
}
pub fn sphere_intersect(ray_origin: Vec3, ray_dir: Vec3, sphere_center: Vec3, sphere_radius: f32) -> Option<f32> {
let oc = ray_origin - sphere_center;
let a = ray_dir.dot(ray_dir);
let half_b = oc.dot(ray_dir);
let c = oc.dot(oc) - sphere_radius * sphere_radius;
let discriminant = half_b * half_b - a * c;
if discriminant < 0.0 { None }
else { Some((-half_b - discriminant.sqrt()) / a) }
}
pub fn aabb_intersect(ray_origin: Vec3, inv_ray_dir: Vec3, aabb_min: Vec3, aabb_max: Vec3) -> Option<(f32, f32)> {
let t1 = (aabb_min - ray_origin) * inv_ray_dir;
let t2 = (aabb_max - ray_origin) * inv_ray_dir;
let tmin_v = t1.min(t2);
let tmax_v = t1.max(t2);
let tmin = tmin_v.x.max(tmin_v.y).max(tmin_v.z);
let tmax = tmax_v.x.min(tmax_v.y).min(tmax_v.z);
if tmax < tmin { None } else { Some((tmin, tmax)) }
}
pub fn frustum_planes_from_view_proj(vp: Mat4) -> [Vec4; 6] {
let m = vp.to_cols_array_2d();
let row0 = Vec4::new(m[0][0], m[1][0], m[2][0], m[3][0]);
let row1 = Vec4::new(m[0][1], m[1][1], m[2][1], m[3][1]);
let row2 = Vec4::new(m[0][2], m[1][2], m[2][2], m[3][2]);
let row3 = Vec4::new(m[0][3], m[1][3], m[2][3], m[3][3]);
let normalize_plane = |p: Vec4| -> Vec4 {
let len = Vec3::new(p.x, p.y, p.z).length();
p / len
};
[
normalize_plane(row3 + row0), normalize_plane(row3 - row0), normalize_plane(row3 + row1), normalize_plane(row3 - row1), normalize_plane(row3 + row2), normalize_plane(row3 - row2), ]
}
pub fn sphere_in_frustum(planes: &[Vec4; 6], center: Vec3, radius: f32) -> bool {
for plane in planes {
let dist = plane.x * center.x + plane.y * center.y + plane.z * center.z + plane.w;
if dist < -radius { return false; }
}
true
}
pub fn aabb_in_frustum(planes: &[Vec4; 6], aabb_min: Vec3, aabb_max: Vec3) -> bool {
for plane in planes {
let p = Vec3::new(
if plane.x > 0.0 { aabb_max.x } else { aabb_min.x },
if plane.y > 0.0 { aabb_max.y } else { aabb_min.y },
if plane.z > 0.0 { aabb_max.z } else { aabb_min.z },
);
if plane.x * p.x + plane.y * p.y + plane.z * p.z + plane.w < 0.0 { return false; }
}
true
}
pub struct TBRDetector;
impl TBRDetector {
pub fn is_tbr(vendor_id: u32, device_id: u32, is_mobile: bool) -> bool {
if is_mobile { return true; }
match vendor_id {
0x13B5 => true, 0x5143 => true, 0x1010 => true, _ => false,
}
}
pub fn suggest_pass_merging(passes: &[PassKind]) -> Vec<Vec<PassKind>> {
let mut groups: Vec<Vec<PassKind>> = Vec::new();
let mut current: Vec<PassKind> = Vec::new();
for &kind in passes {
match kind {
PassKind::GBuffer | PassKind::Lighting | PassKind::SSAO => {
current.push(kind);
}
_ => {
if !current.is_empty() {
groups.push(current.clone());
current.clear();
}
groups.push(vec![kind]);
}
}
}
if !current.is_empty() { groups.push(current); }
groups
}
pub fn bandwidth_savings_mb(width: u32, height: u32, gbuf_formats: &[TextureFormat]) -> f32 {
let pixels = (width * height) as f32;
let mut bpp: f32 = 0.0;
for fmt in gbuf_formats {
bpp += format_info(*fmt).bytes_per_pixel();
}
bpp * pixels / (1024.0 * 1024.0)
}
}
#[derive(Debug, Clone)]
pub struct AsyncComputeGroup {
pub passes: Vec<PassId>,
pub queue: ComputeQueue,
pub semaphore_signals: Vec<PassId>,
pub semaphore_waits: Vec<PassId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComputeQueue { Graphics, AsyncCompute, Transfer }
pub struct AsyncComputeScheduler;
impl AsyncComputeScheduler {
pub fn identify_async_candidates(passes: &HashMap<PassId, PassNode>) -> Vec<PassId> {
passes.values().filter(|p| {
matches!(p.desc.kind(), PassKind::SSAO | PassKind::SSR | PassKind::Bloom | PassKind::VolumetricFog | PassKind::TAA)
}).map(|p| p.id).collect()
}
pub fn schedule(sorted: &[PassId], candidates: &HashSet<PassId>, passes: &HashMap<PassId, PassNode>) -> Vec<AsyncComputeGroup> {
let mut groups: Vec<AsyncComputeGroup> = Vec::new();
let mut current_async: Vec<PassId> = Vec::new();
for pid in sorted {
if candidates.contains(pid) {
current_async.push(*pid);
} else {
if !current_async.is_empty() {
groups.push(AsyncComputeGroup {
passes: current_async.clone(),
queue: ComputeQueue::AsyncCompute,
semaphore_signals: vec![*current_async.last().unwrap()],
semaphore_waits: vec![*pid],
});
current_async.clear();
}
}
}
if !current_async.is_empty() {
groups.push(AsyncComputeGroup {
passes: current_async.clone(),
queue: ComputeQueue::AsyncCompute,
semaphore_signals: current_async.clone(),
semaphore_waits: vec![],
});
}
groups
}
}
#[derive(Debug, Clone)]
pub struct DrawCommand {
pub kind: DrawCommandKind,
pub clip_rect: Option<[f32; 4]>,
pub z_order: i32,
}
#[derive(Debug, Clone)]
pub enum DrawCommandKind {
Rect { pos: Vec2, size: Vec2, color: Vec4, rounding: f32 },
RectOutline { pos: Vec2, size: Vec2, color: Vec4, thickness: f32, rounding: f32 },
Text { pos: Vec2, text: String, color: Vec4, font_size: f32 },
Line { a: Vec2, b: Vec2, color: Vec4, thickness: f32 },
BezierCubic { p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, color: Vec4, thickness: f32 },
Circle { center: Vec2, radius: f32, color: Vec4, filled: bool },
Triangle { p: [Vec2; 3], color: Vec4, filled: bool },
}
pub struct EditorRenderer {
pub commands: Vec<DrawCommand>,
pub viewport_size: Vec2,
}
impl EditorRenderer {
pub fn new(viewport_size: Vec2) -> Self { EditorRenderer { commands: vec![], viewport_size } }
pub fn clear(&mut self) { self.commands.clear(); }
pub fn draw_rect(&mut self, pos: Vec2, size: Vec2, color: Vec4, rounding: f32) {
self.commands.push(DrawCommand { kind: DrawCommandKind::Rect { pos, size, color, rounding }, clip_rect: None, z_order: 0 });
}
pub fn draw_rect_outline(&mut self, pos: Vec2, size: Vec2, color: Vec4, thickness: f32, rounding: f32) {
self.commands.push(DrawCommand { kind: DrawCommandKind::RectOutline { pos, size, color, thickness, rounding }, clip_rect: None, z_order: 0 });
}
pub fn draw_text(&mut self, pos: Vec2, text: &str, color: Vec4, font_size: f32) {
self.commands.push(DrawCommand { kind: DrawCommandKind::Text { pos, text: text.to_owned(), color, font_size }, clip_rect: None, z_order: 1 });
}
pub fn draw_line(&mut self, a: Vec2, b: Vec2, color: Vec4, thickness: f32) {
self.commands.push(DrawCommand { kind: DrawCommandKind::Line { a, b, color, thickness }, clip_rect: None, z_order: 0 });
}
pub fn draw_bezier(&mut self, p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, color: Vec4, thickness: f32) {
self.commands.push(DrawCommand { kind: DrawCommandKind::BezierCubic { p0, p1, p2, p3, color, thickness }, clip_rect: None, z_order: 0 });
}
pub fn render_pass_node(&mut self, pass: &PassNode, camera_pos: Vec2, camera_zoom: f32, is_selected: bool, is_hovered: bool) {
let pos = (pass.editor_pos + camera_pos) * camera_zoom;
let size = pass.editor_size * camera_zoom;
let mut bg = pass.editor_color;
if is_hovered { bg = bg * 1.2; bg.w = 1.0; }
if is_selected { bg = Vec4::new(1.0, 0.9, 0.2, 1.0); }
self.draw_rect(pos, size, bg, 6.0 * camera_zoom);
let border_color = if is_selected { Vec4::new(1.0, 1.0, 0.0, 1.0) } else if is_hovered { Vec4::new(1.0, 1.0, 1.0, 0.8) } else { Vec4::new(0.0, 0.0, 0.0, 0.5) };
self.draw_rect_outline(pos, size, border_color, 2.0 * camera_zoom, 6.0 * camera_zoom);
let title_pos = pos + Vec2::new(8.0 * camera_zoom, 8.0 * camera_zoom);
let text_color = Vec4::new(1.0, 1.0, 1.0, 1.0);
self.draw_text(title_pos, &pass.name, text_color, 14.0 * camera_zoom);
let kind_str = format!("{:?}", pass.desc.kind());
let kind_pos = pos + Vec2::new(8.0 * camera_zoom, 28.0 * camera_zoom);
self.draw_text(kind_pos, &kind_str, Vec4::new(0.8, 0.8, 0.8, 0.9), 10.0 * camera_zoom);
let port_radius = 5.0 * camera_zoom;
for (i, _rid) in pass.reads.iter().enumerate() {
let py = pos.y + (i as f32 + 0.5) * (size.y / pass.reads.len().max(1) as f32);
let port_pos = Vec2::new(pos.x, py);
self.commands.push(DrawCommand { kind: DrawCommandKind::Circle { center: port_pos, radius: port_radius, color: Vec4::new(0.3, 0.8, 1.0, 1.0), filled: true }, clip_rect: None, z_order: 2 });
}
for (i, _rid) in pass.writes.iter().enumerate() {
let py = pos.y + (i as f32 + 0.5) * (size.y / pass.writes.len().max(1) as f32);
let port_pos = Vec2::new(pos.x + size.x, py);
self.commands.push(DrawCommand { kind: DrawCommandKind::Circle { center: port_pos, radius: port_radius, color: Vec4::new(1.0, 0.5, 0.2, 1.0), filled: true }, clip_rect: None, z_order: 2 });
}
}
pub fn render_edge(&mut self, src_port: Vec2, dst_port: Vec2, color: Vec4, thickness: f32) {
let dx = (dst_port.x - src_port.x).abs() * 0.5;
let p1 = src_port + Vec2::new(dx, 0.0);
let p2 = dst_port - Vec2::new(dx, 0.0);
self.draw_bezier(src_port, p1, p2, dst_port, color, thickness);
}
pub fn render_resource_node(&mut self, res: &RenderGraphResource, pos: Vec2, zoom: f32) {
let size = Vec2::new(120.0, 40.0) * zoom;
let color = match res.lifetime {
ResourceLifetime::Transient => Vec4::new(0.2, 0.4, 0.2, 0.8),
ResourceLifetime::Persistent => Vec4::new(0.4, 0.2, 0.2, 0.8),
ResourceLifetime::Imported => Vec4::new(0.2, 0.2, 0.4, 0.8),
};
self.draw_rect(pos, size, color, 4.0 * zoom);
self.draw_text(pos + Vec2::new(4.0 * zoom, 4.0 * zoom), &res.name, Vec4::ONE, 10.0 * zoom);
if let Some(td) = res.texture_desc() {
let info_str = format!("{}x{} {:?}", td.width, td.height, td.format);
self.draw_text(pos + Vec2::new(4.0 * zoom, 18.0 * zoom), &info_str, Vec4::new(0.8, 0.8, 0.8, 0.9), 8.0 * zoom);
}
}
pub fn render_barrier_indicator(&mut self, pos: Vec2, zoom: f32, old_layout: ImageLayout, new_layout: ImageLayout) {
let r = 8.0 * zoom;
let color = barrier_color_for_layouts(old_layout, new_layout);
self.commands.push(DrawCommand {
kind: DrawCommandKind::Circle { center: pos, radius: r, color, filled: true },
clip_rect: None, z_order: 3,
});
}
pub fn render_stats_overlay(&mut self, stats: &FrameStatistics, pos: Vec2) {
let bg_size = Vec2::new(260.0, 120.0);
self.draw_rect(pos, bg_size, Vec4::new(0.0, 0.0, 0.0, 0.8), 4.0);
let mut y = pos.y + 8.0;
let lh = 16.0;
let tc = Vec4::new(0.9, 0.9, 0.9, 1.0);
self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("GPU: {:.2}ms FPS: {:.1}", stats.total_gpu_time_ms, stats.fps), tc, 11.0); y += lh;
self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("Draw calls: {}", stats.total_draw_calls), tc, 11.0); y += lh;
self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("Triangles: {}M", stats.total_triangles / 1_000_000), tc, 11.0); y += lh;
self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("BW: {:.1}MB/frame", stats.total_bandwidth_mb), tc, 11.0); y += lh;
if let Some(bp) = stats.bottleneck_pass() {
if let Some(ps) = stats.pass_stats.get(&bp) {
self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("Bottleneck: {:?} {:.2}ms", bp, ps.gpu_time_ms), Vec4::new(1.0, 0.4, 0.2, 1.0), 11.0);
}
}
}
pub fn sort_by_z(&mut self) {
self.commands.sort_by_key(|c| c.z_order);
}
}
fn barrier_color_for_layouts(old: ImageLayout, new: ImageLayout) -> Vec4 {
match (old, new) {
(ImageLayout::Undefined, _) => Vec4::new(1.0, 0.2, 0.2, 1.0), (ImageLayout::ColorAttachmentOptimal, ImageLayout::ShaderReadOnlyOptimal) => Vec4::new(0.2, 1.0, 0.2, 1.0), (_, ImageLayout::ShaderReadOnlyOptimal) => Vec4::new(0.4, 0.8, 0.4, 1.0),
(_, ImageLayout::ColorAttachmentOptimal) => Vec4::new(0.8, 0.6, 0.2, 1.0),
(_, ImageLayout::TransferSrcOptimal) | (_, ImageLayout::TransferDstOptimal) => Vec4::new(0.6, 0.2, 0.8, 1.0),
_ => Vec4::new(0.8, 0.8, 0.8, 1.0),
}
}
pub fn build_forward_plus_pipeline(width: u32, height: u32) -> RenderGraphEditor {
let mut editor = RenderGraphEditor::new("Forward+");
let res_depth_prepass = editor.add_transient_texture("DepthPrepass", TextureDesc::depth_target(width, height));
let res_depth_main = editor.add_transient_texture("MainDepth", TextureDesc::depth_target(width, height));
let res_hdr = editor.add_transient_texture("HDR", TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
let res_bloom = editor.add_transient_texture("Bloom", TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
let res_sdr = editor.add_persistent_texture("SDR", TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
let res_shadow = editor.add_transient_texture("Shadow", TextureDesc::shadow_map(2048));
let res_ui = editor.add_persistent_texture("UI", TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
editor.set_output_resources(vec![res_ui]);
let depth_pp = editor.add_pass("DepthPrepass", PassDesc::GBuffer(GBufferPassDesc::default(width, height)));
editor.set_pass_writes(depth_pp, vec![res_depth_prepass]);
let sm = editor.add_pass("ShadowMap", PassDesc::ShadowMap(ShadowMapPassDesc::directional_shadow(2048)));
editor.set_pass_writes(sm, vec![res_shadow]);
let light = editor.add_pass("ForwardLighting", PassDesc::Lighting(LightingPassDesc::default(width, height)));
editor.set_pass_reads(light, vec![res_depth_prepass, res_shadow]);
editor.set_pass_writes(light, vec![res_hdr, res_depth_main]);
let particles = editor.add_pass("Particles", PassDesc::Particle(ParticlePassDesc::default(width, height)));
editor.set_pass_reads(particles, vec![res_hdr, res_depth_main]);
editor.set_pass_writes(particles, vec![res_hdr]);
let bloom = editor.add_pass("Bloom", PassDesc::Bloom(BloomPassDesc::default(width, height)));
editor.set_pass_reads(bloom, vec![res_hdr]);
editor.set_pass_writes(bloom, vec![res_bloom]);
let tonemap = editor.add_pass("ToneMapping", PassDesc::ToneMapping(ToneMappingPassDesc::default(width, height)));
editor.set_pass_reads(tonemap, vec![res_hdr, res_bloom]);
editor.set_pass_writes(tonemap, vec![res_sdr]);
let ui = editor.add_pass("UI", PassDesc::UI(UIPassDesc::default(width, height)));
editor.set_pass_reads(ui, vec![res_sdr]);
editor.set_pass_writes(ui, vec![res_ui]);
editor
}
pub fn build_mobile_deferred_pipeline(width: u32, height: u32) -> RenderGraphEditor {
let mut editor = RenderGraphEditor::new("MobileDeferred");
let res_albedo = editor.add_transient_texture("Albedo", TextureDesc::render_target(width, height, TextureFormat::RGBA8Unorm));
let res_normal = editor.add_transient_texture("Normal", TextureDesc::render_target(width, height, TextureFormat::RGBA8Snorm));
let res_depth = editor.add_transient_texture("Depth", TextureDesc::depth_target(width, height));
let res_hdr = editor.add_transient_texture("HDR", TextureDesc::render_target(width, height, TextureFormat::RG11B10Float));
let res_sdr = editor.add_persistent_texture("SDR", TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
editor.set_output_resources(vec![res_sdr]);
let gbuf = editor.add_pass("GBuffer", PassDesc::GBuffer(GBufferPassDesc::default(width, height)));
editor.set_pass_writes(gbuf, vec![res_albedo, res_normal, res_depth]);
let light = editor.add_pass("Lighting", PassDesc::Lighting(LightingPassDesc::default(width, height)));
editor.set_pass_reads(light, vec![res_albedo, res_normal, res_depth]);
editor.set_pass_writes(light, vec![res_hdr]);
let tonemap = editor.add_pass("ToneMapping", PassDesc::ToneMapping(ToneMappingPassDesc::default(width, height)));
editor.set_pass_reads(tonemap, vec![res_hdr]);
editor.set_pass_writes(tonemap, vec![res_sdr]);
editor
}
pub fn formats_compatible(a: TextureFormat, b: TextureFormat) -> bool {
let ia = format_info(a);
let ib = format_info(b);
ia.bytes_per_block == ib.bytes_per_block
&& ia.block_width == ib.block_width
&& ia.block_height == ib.block_height
}
pub fn is_color_attachment_format(fmt: TextureFormat) -> bool {
let fi = format_info(fmt);
!fi.is_depth && !fi.is_stencil && !fi.is_compressed
}
pub fn is_depth_stencil_attachment_format(fmt: TextureFormat) -> bool {
let fi = format_info(fmt);
fi.is_depth || fi.is_stencil
}
pub fn format_channel_bits(fmt: TextureFormat) -> [u8; 4] {
match fmt {
TextureFormat::R8Unorm | TextureFormat::R8Snorm | TextureFormat::R8Uint | TextureFormat::R8Sint => [8, 0, 0, 0],
TextureFormat::RG8Unorm | TextureFormat::RG8Snorm | TextureFormat::RG8Uint | TextureFormat::RG8Sint => [8, 8, 0, 0],
TextureFormat::RGBA8Unorm | TextureFormat::RGBA8UnormSrgb | TextureFormat::RGBA8Snorm | TextureFormat::RGBA8Uint | TextureFormat::RGBA8Sint => [8, 8, 8, 8],
TextureFormat::BGRA8Unorm | TextureFormat::BGRA8UnormSrgb => [8, 8, 8, 8],
TextureFormat::R16Unorm | TextureFormat::R16Float | TextureFormat::R16Uint | TextureFormat::R16Sint => [16, 0, 0, 0],
TextureFormat::RG16Unorm | TextureFormat::RG16Float | TextureFormat::RG16Uint => [16, 16, 0, 0],
TextureFormat::RGBA16Unorm | TextureFormat::RGBA16Float | TextureFormat::RGBA16Uint => [16, 16, 16, 16],
TextureFormat::R32Float | TextureFormat::R32Uint | TextureFormat::R32Sint => [32, 0, 0, 0],
TextureFormat::RG32Float | TextureFormat::RG32Uint => [32, 32, 0, 0],
TextureFormat::RGB32Float => [32, 32, 32, 0],
TextureFormat::RGBA32Float | TextureFormat::RGBA32Uint => [32, 32, 32, 32],
TextureFormat::RGB10A2Unorm => [10, 10, 10, 2],
TextureFormat::RG11B10Float => [11, 11, 10, 0],
TextureFormat::Depth16Unorm => [16, 0, 0, 0],
TextureFormat::Depth24Unorm => [24, 0, 0, 0],
TextureFormat::Depth32Float => [32, 0, 0, 0],
TextureFormat::Depth24UnormStencil8 => [24, 8, 0, 0],
TextureFormat::Depth32FloatStencil8 => [32, 8, 0, 0],
TextureFormat::Stencil8 => [0, 8, 0, 0],
_ => [0, 0, 0, 0],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_info_bytes_per_pixel() {
assert_eq!(format_info(TextureFormat::RGBA8Unorm).bytes_per_block, 4);
assert_eq!(format_info(TextureFormat::RGBA16Float).bytes_per_block, 8);
assert_eq!(format_info(TextureFormat::R32Float).bytes_per_block, 4);
let bc1 = format_info(TextureFormat::BC1RgbUnorm);
assert_eq!(bc1.block_width, 4);
assert!((bc1.bytes_per_pixel() - 0.5).abs() < 1e-4);
}
#[test]
fn test_texture_size() {
let sz = texture_size_bytes(TextureFormat::RGBA8Unorm, 1920, 1080, 1);
assert_eq!(sz, 1920 * 1080 * 4);
}
#[test]
fn test_gbuffer_bandwidth() {
let gbuf = GBufferPassDesc::default(1920, 1080);
let bw = gbuf.estimate_write_bandwidth_mb();
assert!(bw > 0.0 && bw < 200.0);
}
#[test]
fn test_halton() {
let h = halton_sequence(1, 2);
assert!((h - 0.5).abs() < 1e-5);
let h2 = halton_sequence(2, 2);
assert!((h2 - 0.25).abs() < 1e-5);
}
#[test]
fn test_ssao_kernel() {
let ssao = SSAOPassDesc::default(1920, 1080);
let kernel = ssao.generate_kernel();
assert_eq!(kernel.len(), ssao.kernel_size as usize);
for s in &kernel {
assert!(s.length() <= 1.0 + 1e-4);
}
}
#[test]
fn test_taa_jitter() {
let taa = TAAPassDesc::default(1920, 1080);
let j0 = taa.halton_jitter(0);
let j1 = taa.halton_jitter(1);
assert!(j0 != j1);
assert!(j0.x.abs() < 1.0 && j0.y.abs() < 1.0);
}
#[test]
fn test_octahedral_encoding() {
let n = Vec3::new(0.0, 1.0, 0.0).normalize();
let e = octahedral_encode(n);
let d = octahedral_decode(e);
assert!((d - n).length() < 1e-3);
}
#[test]
fn test_aces_tone_mapping() {
let op = ToneMappingPassDesc::default(1920, 1080);
let color_in = Vec3::new(1.0, 0.5, 0.2);
let out = op.apply_aces(color_in);
assert!(out.x >= 0.0 && out.x <= 1.0);
assert!(out.y >= 0.0 && out.y <= 1.0);
assert!(out.z >= 0.0 && out.z <= 1.0);
}
#[test]
fn test_bloom_quadratic_threshold() {
let bloom = BloomPassDesc::default(1920, 1080);
assert_eq!(bloom.quadratic_threshold(0.0), 0.0);
let above = bloom.quadratic_threshold(2.0);
assert!(above > 0.0);
}
#[test]
fn test_compile_standard_pipeline() {
let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let result = editor.compile();
assert!(result.is_ok(), "Compilation failed: {:?}", result);
let compiled = editor.compiled.as_ref().unwrap();
assert!(!compiled.sorted_passes.is_empty());
}
#[test]
fn test_sugiyama_layout() {
let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
editor.visualize();
assert!(editor.layout.is_some());
let layout = editor.layout.as_ref().unwrap();
assert!(!layout.node_positions.is_empty());
}
#[test]
fn test_serialization() {
let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let json = editor.to_json();
assert!(json.contains("GBuffer"));
assert!(json.contains("\"version\": 1"));
}
#[test]
fn test_hg_phase() {
let fog = VolumetricFogPassDesc::default(1920, 1080);
let p0 = fog.henyey_greenstein(1.0); let p1 = fog.henyey_greenstein(-1.0); assert!(p0 > p1); }
#[test]
fn test_barrier_layout_transitions() {
let b = ImageBarrier::layout_transition(
ResourceId(0),
ImageLayout::Undefined,
ImageLayout::ColorAttachmentOptimal,
);
assert_eq!(b.old_layout, ImageLayout::Undefined);
assert_eq!(b.new_layout, ImageLayout::ColorAttachmentOptimal);
}
#[test]
fn test_cluster_grid() {
let grid = ClusteredLightGrid::new(1920, 1080, 16, 24, 0.1, 100.0);
assert_eq!(grid.tiles_x, 120);
assert_eq!(grid.tiles_y, 68);
assert_eq!(grid.total_clusters(), 120 * 68 * 24);
}
#[test]
fn test_cascade_splits() {
let sm = ShadowMapPassDesc::directional_shadow(4096);
let splits = sm.compute_cascade_splits(0.75, 0.1, 200.0);
assert_eq!(splits.len(), 4);
for i in 1..splits.len() { assert!(splits[i] > splits[i-1]); }
}
#[test]
fn test_sphere_in_frustum() {
let vp = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 16.0/9.0, 0.1, 100.0);
let planes = frustum_planes_from_view_proj(vp);
assert!(sphere_in_frustum(&planes, Vec3::new(0.0, 0.0, -10.0), 1.0));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtlasRegion {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl AtlasRegion {
pub fn uv_offset(&self, atlas_size: u32) -> Vec2 {
Vec2::new(self.x as f32 / atlas_size as f32, self.y as f32 / atlas_size as f32)
}
pub fn uv_scale(&self, atlas_size: u32) -> Vec2 {
Vec2::new(self.width as f32 / atlas_size as f32, self.height as f32 / atlas_size as f32)
}
pub fn uv_transform(&self, atlas_size: u32) -> Vec4 {
let off = self.uv_offset(atlas_size);
let sc = self.uv_scale(atlas_size);
Vec4::new(sc.x, sc.y, off.x, off.y)
}
}
pub struct ShadowAtlas {
pub atlas_size: u32,
pub regions: Vec<(u32, AtlasRegion)>, pub free_rects: Vec<AtlasRegion>,
}
impl ShadowAtlas {
pub fn new(atlas_size: u32) -> Self {
ShadowAtlas {
atlas_size,
regions: Vec::new(),
free_rects: vec![AtlasRegion { x: 0, y: 0, width: atlas_size, height: atlas_size }],
}
}
pub fn allocate(&mut self, light_id: u32, width: u32, height: u32) -> Option<AtlasRegion> {
let best = self.free_rects.iter().enumerate()
.filter(|(_, r)| r.width >= width && r.height >= height)
.min_by_key(|(_, r)| r.width * r.height);
let (idx, region) = best.map(|(i, r)| (i, *r))?;
self.free_rects.remove(idx);
let allocated = AtlasRegion { x: region.x, y: region.y, width, height };
let right = AtlasRegion { x: region.x + width, y: region.y, width: region.width - width, height };
let bottom = AtlasRegion { x: region.x, y: region.y + height, width: region.width, height: region.height - height };
if right.width > 0 { self.free_rects.push(right); }
if bottom.height > 0 { self.free_rects.push(bottom); }
self.regions.push((light_id, allocated));
Some(allocated)
}
pub fn free_region(&mut self, light_id: u32) {
if let Some(pos) = self.regions.iter().position(|(id, _)| *id == light_id) {
let (_, region) = self.regions.remove(pos);
self.free_rects.push(region);
}
}
pub fn region_for_light(&self, light_id: u32) -> Option<AtlasRegion> {
self.regions.iter().find(|(id, _)| *id == light_id).map(|(_, r)| *r)
}
pub fn utilization(&self) -> f32 {
let used: u32 = self.regions.iter().map(|(_, r)| r.width * r.height).sum();
let total = self.atlas_size * self.atlas_size;
used as f32 / total as f32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryType { Timestamp, Occlusion, PipelineStatistics }
#[derive(Debug, Clone)]
pub struct TimestampQuery {
pub pass_id: PassId,
pub name: String,
pub start_index: u32,
pub end_index: u32,
}
#[derive(Debug, Clone)]
pub struct QueryPool {
pub query_type: QueryType,
pub capacity: u32,
pub next_index: u32,
pub timestamp_period_ns: f64, }
impl QueryPool {
pub fn new_timestamp(capacity: u32, timestamp_period_ns: f64) -> Self {
QueryPool { query_type: QueryType::Timestamp, capacity, next_index: 0, timestamp_period_ns }
}
pub fn allocate_pair(&mut self) -> Option<(u32, u32)> {
if self.next_index + 2 <= self.capacity {
let start = self.next_index;
self.next_index += 2;
Some((start, start + 1))
} else {
None
}
}
pub fn reset(&mut self) { self.next_index = 0; }
pub fn ticks_to_ms(&self, ticks: u64) -> f64 {
(ticks as f64 * self.timestamp_period_ns) / 1_000_000.0
}
pub fn ticks_to_us(&self, ticks: u64) -> f64 {
(ticks as f64 * self.timestamp_period_ns) / 1_000.0
}
}
pub struct ProfilingManager {
pub timestamp_pool: QueryPool,
pub queries: Vec<TimestampQuery>,
pub results: HashMap<PassId, f64>, }
impl ProfilingManager {
pub fn new(max_passes: u32, timestamp_period_ns: f64) -> Self {
ProfilingManager {
timestamp_pool: QueryPool::new_timestamp(max_passes * 2, timestamp_period_ns),
queries: Vec::new(),
results: HashMap::new(),
}
}
pub fn begin_pass(&mut self, pass_id: PassId, name: &str) -> Option<u32> {
let (start, end) = self.timestamp_pool.allocate_pair()?;
self.queries.push(TimestampQuery { pass_id, name: name.to_owned(), start_index: start, end_index: end });
Some(start)
}
pub fn process_results(&mut self, raw_timestamps: &[u64]) {
for q in &self.queries {
let start_idx = q.start_index as usize;
let end_idx = q.end_index as usize;
if end_idx < raw_timestamps.len() {
let ticks = raw_timestamps[end_idx].saturating_sub(raw_timestamps[start_idx]);
let ms = self.timestamp_pool.ticks_to_ms(ticks);
self.results.insert(q.pass_id, ms);
}
}
}
pub fn reset_frame(&mut self) {
self.timestamp_pool.reset();
self.queries.clear();
}
pub fn get_pass_time_ms(&self, pass_id: PassId) -> f64 {
*self.results.get(&pass_id).unwrap_or(&0.0)
}
pub fn total_gpu_time_ms(&self) -> f64 {
self.results.values().sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingType {
SampledTexture,
StorageTexture,
UniformBuffer,
StorageBuffer,
Sampler,
InputAttachment,
AccelerationStructure,
}
#[derive(Debug, Clone)]
pub struct DescriptorBinding {
pub binding: u32,
pub binding_type: BindingType,
pub resource_id: ResourceId,
pub stage_flags: PipelineStageFlags,
pub array_count: u32,
}
#[derive(Debug, Clone)]
pub struct DescriptorSet {
pub set_index: u32,
pub bindings: Vec<DescriptorBinding>,
}
impl DescriptorSet {
pub fn new(set_index: u32) -> Self { DescriptorSet { set_index, bindings: Vec::new() } }
pub fn bind_texture(&mut self, binding: u32, resource_id: ResourceId, stage: PipelineStageFlags) {
self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::SampledTexture, resource_id, stage_flags: stage, array_count: 1 });
}
pub fn bind_storage_texture(&mut self, binding: u32, resource_id: ResourceId, stage: PipelineStageFlags) {
self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::StorageTexture, resource_id, stage_flags: stage, array_count: 1 });
}
pub fn bind_uniform_buffer(&mut self, binding: u32, resource_id: ResourceId) {
self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::UniformBuffer, resource_id, stage_flags: PipelineStageFlags::VERTEX_SHADER | PipelineStageFlags::FRAGMENT_SHADER, array_count: 1 });
}
pub fn bind_input_attachment(&mut self, binding: u32, resource_id: ResourceId) {
self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::InputAttachment, resource_id, stage_flags: PipelineStageFlags::FRAGMENT_SHADER, array_count: 1 });
}
pub fn has_input_attachments(&self) -> bool {
self.bindings.iter().any(|b| b.binding_type == BindingType::InputAttachment)
}
}
pub fn build_lighting_descriptor_set(desc: &LightingPassDesc) -> Vec<DescriptorSet> {
let mut set0 = DescriptorSet::new(0);
let frag = PipelineStageFlags::FRAGMENT_SHADER;
set0.bind_input_attachment(0, desc.input_albedo);
set0.bind_input_attachment(1, desc.input_normal);
set0.bind_input_attachment(2, desc.input_material);
set0.bind_input_attachment(3, desc.input_depth);
set0.bind_texture(4, desc.input_shadow_map, frag);
set0.bind_texture(5, desc.input_ssao, frag);
vec![set0]
}
pub fn build_ssao_descriptor_set(desc: &SSAOPassDesc) -> Vec<DescriptorSet> {
let mut set0 = DescriptorSet::new(0);
let frag = PipelineStageFlags::FRAGMENT_SHADER;
set0.bind_texture(0, desc.input_depth, frag);
set0.bind_texture(1, desc.input_normal, frag);
vec![set0]
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct FrameUniforms {
pub view: Mat4,
pub proj: Mat4,
pub view_proj: Mat4,
pub inv_view: Mat4,
pub inv_proj: Mat4,
pub inv_view_proj: Mat4,
pub prev_view_proj: Mat4,
pub camera_pos: Vec4,
pub camera_dir: Vec4,
pub resolution: Vec4, pub time: Vec4, pub near_far: Vec4, pub exposure: Vec4, pub jitter: Vec4, pub fog_params: Vec4, pub ambient: Vec4,
}
impl FrameUniforms {
pub fn new(view: Mat4, proj: Mat4, near: f32, far: f32, width: u32, height: u32) -> Self {
let view_proj = proj * view;
FrameUniforms {
view,
proj,
view_proj,
inv_view: view.inverse(),
inv_proj: proj.inverse(),
inv_view_proj: view_proj.inverse(),
prev_view_proj: view_proj,
camera_pos: Vec4::new(0.0, 0.0, 0.0, 1.0),
camera_dir: Vec4::new(0.0, 0.0, -1.0, 0.0),
resolution: Vec4::new(width as f32, height as f32, 1.0 / width as f32, 1.0 / height as f32),
time: Vec4::new(0.0, 0.016, 0.0, 0.0),
near_far: Vec4::new(near, far, 1.0 / near, 1.0 / far),
exposure: Vec4::new(1.0, 0.0, 0.0, 0.0),
jitter: Vec4::ZERO,
fog_params: Vec4::new(0.01, 0.05, 0.005, 0.0),
ambient: Vec4::new(0.03, 0.03, 0.05, 1.0),
}
}
pub fn size_bytes() -> usize { std::mem::size_of::<FrameUniforms>() }
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ShadowUniforms {
pub light_view_proj: [Mat4; 4],
pub cascade_splits: Vec4,
pub shadow_map_size: Vec4,
pub shadow_bias: Vec4,
pub pcf_radius: f32,
pub pcss_light_size: f32,
pub _pad: [f32; 2],
}
impl ShadowUniforms {
pub fn new(light_vps: [Mat4; 4], splits: [f32; 4], map_size: f32) -> Self {
ShadowUniforms {
light_view_proj: light_vps,
cascade_splits: Vec4::from(splits),
shadow_map_size: Vec4::new(map_size, 1.0 / map_size, 0.0, 0.0),
shadow_bias: Vec4::new(0.0005, 0.0, 0.0, 0.0),
pcf_radius: 2.0,
pcss_light_size: 0.5,
_pad: [0.0; 2],
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct BloomUniforms {
pub threshold: f32,
pub knee: f32,
pub intensity: f32,
pub scatter: f32,
pub mip_level: u32,
pub _pad: [u32; 3],
pub inv_resolution: Vec2,
pub _pad2: Vec2,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct TAAUniforms {
pub blend_factor: f32,
pub variance_clip_gamma: f32,
pub velocity_weight_scale: f32,
pub _pad: f32,
pub jitter: Vec4,
pub resolution: Vec4,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct SSAOUniforms {
pub radius: f32,
pub bias: f32,
pub power: f32,
pub kernel_size: u32,
pub noise_scale: Vec2,
pub _pad: Vec2,
}
pub struct GpuMemoryBudget {
pub device_local_total: u64,
pub device_local_used: u64,
pub host_visible_total: u64,
pub host_visible_used: u64,
pub allocations: Vec<(String, u64, bool)>, }
impl GpuMemoryBudget {
pub fn new(device_local_mb: u64, host_visible_mb: u64) -> Self {
GpuMemoryBudget {
device_local_total: device_local_mb * 1024 * 1024,
device_local_used: 0,
host_visible_total: host_visible_mb * 1024 * 1024,
host_visible_used: 0,
allocations: Vec::new(),
}
}
pub fn allocate(&mut self, name: &str, size: u64, device_local: bool) -> bool {
if device_local {
if self.device_local_used + size > self.device_local_total { return false; }
self.device_local_used += size;
} else {
if self.host_visible_used + size > self.host_visible_total { return false; }
self.host_visible_used += size;
}
self.allocations.push((name.to_owned(), size, device_local));
true
}
pub fn free(&mut self, name: &str) {
if let Some(pos) = self.allocations.iter().position(|(n, _, _)| n == name) {
let (_, size, device_local) = self.allocations.remove(pos);
if device_local { self.device_local_used = self.device_local_used.saturating_sub(size); }
else { self.host_visible_used = self.host_visible_used.saturating_sub(size); }
}
}
pub fn device_local_free_mb(&self) -> f64 {
(self.device_local_total - self.device_local_used) as f64 / (1024.0 * 1024.0)
}
pub fn device_local_utilization(&self) -> f32 {
if self.device_local_total == 0 { 0.0 } else { self.device_local_used as f32 / self.device_local_total as f32 }
}
pub fn largest_allocation(&self) -> Option<(&str, u64)> {
self.allocations.iter().max_by_key(|(_, s, _)| *s).map(|(n, s, _)| (n.as_str(), *s))
}
pub fn report(&self) -> String {
let mut s = String::new();
s.push_str(&format!("Device-local: {:.1}MB / {:.1}MB ({:.1}%)\n",
self.device_local_used as f64 / (1024.0*1024.0),
self.device_local_total as f64 / (1024.0*1024.0),
self.device_local_utilization() * 100.0));
s.push_str(&format!("Host-visible: {:.1}MB / {:.1}MB\n",
self.host_visible_used as f64 / (1024.0*1024.0),
self.host_visible_total as f64 / (1024.0*1024.0)));
for (name, size, dl) in &self.allocations {
s.push_str(&format!(" {:40} {:6.1}MB {}\n", name, *size as f64 / (1024.0*1024.0), if *dl { "DEVICE" } else { "HOST" }));
}
s
}
}
#[derive(Debug, Clone)]
pub struct ResourceNodeVisual {
pub id: ResourceId,
pub pos: Vec2,
pub size: Vec2,
pub color: Vec4,
pub label: String,
pub tooltip: String,
pub lifetime_bar_start: f32, pub lifetime_bar_end: f32,
}
impl ResourceNodeVisual {
pub fn from_resource(res: &RenderGraphResource, total_passes: usize) -> Self {
let total = total_passes.max(1) as f32;
let color = resource_lifetime_color(res.lifetime);
let tooltip = match &res.desc {
ResourceDesc::Texture(t) => format!("{:?} {}x{} mip:{} {:?} {:?}", res.lifetime, t.width, t.height, t.mip_levels, t.format, t.kind),
ResourceDesc::Buffer(b) => format!("{:?} {} bytes stride:{}", res.lifetime, b.size, b.stride),
};
ResourceNodeVisual {
id: res.id, pos: Vec2::ZERO, size: Vec2::new(140.0, 36.0), color,
label: res.name.clone(), tooltip,
lifetime_bar_start: if res.first_use == usize::MAX { 0.0 } else { res.first_use as f32 / total },
lifetime_bar_end: res.last_use as f32 / total,
}
}
}
fn resource_lifetime_color(lt: ResourceLifetime) -> Vec4 {
match lt {
ResourceLifetime::Transient => Vec4::new(0.15, 0.55, 0.25, 0.85),
ResourceLifetime::Persistent => Vec4::new(0.55, 0.15, 0.15, 0.85),
ResourceLifetime::Imported => Vec4::new(0.15, 0.25, 0.55, 0.85),
}
}
pub struct DependencyMatrix {
pub pass_ids: Vec<PassId>,
pub matrix: Vec<Vec<bool>>, }
impl DependencyMatrix {
pub fn build(passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> Self {
let n = passes.len();
let pass_index: HashMap<PassId, usize> = passes.iter().enumerate().map(|(i, p)| (*p, i)).collect();
let mut matrix = vec![vec![false; n]; n];
for (src, dsts) in edges {
if let Some(&si) = pass_index.get(src) {
for dst in dsts {
if let Some(&di) = pass_index.get(dst) {
matrix[di][si] = true; }
}
}
}
for k in 0..n {
for i in 0..n {
for j in 0..n {
if matrix[i][k] && matrix[k][j] {
matrix[i][j] = true;
}
}
}
}
DependencyMatrix { pass_ids: passes.to_vec(), matrix }
}
pub fn depends_on(&self, a: PassId, b: PassId) -> bool {
let ai = self.pass_ids.iter().position(|&p| p == a);
let bi = self.pass_ids.iter().position(|&p| p == b);
match (ai, bi) {
(Some(i), Some(j)) => self.matrix[i][j],
_ => false,
}
}
pub fn can_execute_in_parallel(&self, a: PassId, b: PassId) -> bool {
!self.depends_on(a, b) && !self.depends_on(b, a)
}
pub fn render_html_table(&self, pass_names: &HashMap<PassId, String>) -> String {
let mut s = String::new();
s.push_str("<table border='1'><tr><th></th>");
for pid in &self.pass_ids {
let name = pass_names.get(pid).map(|n| n.as_str()).unwrap_or("?");
s.push_str(&format!("<th>{}</th>", name));
}
s.push_str("</tr>");
for (i, row_pid) in self.pass_ids.iter().enumerate() {
let row_name = pass_names.get(row_pid).map(|n| n.as_str()).unwrap_or("?");
s.push_str(&format!("<tr><td>{}</td>", row_name));
for j in 0..self.pass_ids.len() {
let cell = if self.matrix[i][j] { "✓" } else { "" };
let color = if self.matrix[i][j] { "#aaffaa" } else { "white" };
s.push_str(&format!("<td style='background:{}'>{}</td>", color, cell));
}
s.push_str("</tr>");
}
s.push_str("</table>");
s
}
}
#[derive(Debug, Clone)]
pub enum GraphDiff {
PassAdded(PassId, String),
PassRemoved(PassId, String),
PassModified(PassId, String),
ResourceAdded(ResourceId, String),
ResourceRemoved(ResourceId, String),
ResourceModified(ResourceId, String),
ConnectionAdded(PassId, PassId),
ConnectionRemoved(PassId, PassId),
}
pub fn diff_render_graphs(old: &RenderGraphEditor, new: &RenderGraphEditor) -> Vec<GraphDiff> {
let mut diffs = Vec::new();
for pid in new.passes.keys() {
if !old.passes.contains_key(pid) {
let name = new.passes[pid].name.clone();
diffs.push(GraphDiff::PassAdded(*pid, name));
}
}
for pid in old.passes.keys() {
if !new.passes.contains_key(pid) {
let name = old.passes[pid].name.clone();
diffs.push(GraphDiff::PassRemoved(*pid, name));
}
}
for (pid, new_pass) in &new.passes {
if let Some(old_pass) = old.passes.get(pid) {
if old_pass.reads != new_pass.reads || old_pass.writes != new_pass.writes || !old_pass.enabled == !new_pass.enabled {
diffs.push(GraphDiff::PassModified(*pid, new_pass.name.clone()));
}
}
}
for rid in new.resources.keys() {
if !old.resources.contains_key(rid) {
diffs.push(GraphDiff::ResourceAdded(*rid, new.resources[rid].name.clone()));
}
}
for rid in old.resources.keys() {
if !new.resources.contains_key(rid) {
diffs.push(GraphDiff::ResourceRemoved(*rid, old.resources[rid].name.clone()));
}
}
let old_connections = collect_connections(old);
let new_connections = collect_connections(new);
for conn in &new_connections {
if !old_connections.contains(conn) { diffs.push(GraphDiff::ConnectionAdded(conn.0, conn.1)); }
}
for conn in &old_connections {
if !new_connections.contains(conn) { diffs.push(GraphDiff::ConnectionRemoved(conn.0, conn.1)); }
}
diffs
}
fn collect_connections(editor: &RenderGraphEditor) -> HashSet<(PassId, PassId)> {
let mut conns = HashSet::new();
for (src, src_pass) in &editor.passes {
for rid in &src_pass.writes {
for (dst, dst_pass) in &editor.passes {
if dst_pass.reads.contains(rid) { conns.insert((*src, *dst)); }
}
}
}
conns
}
#[derive(Debug, Clone)]
pub struct ResourceAccessRecord {
pub pass_id: PassId,
pub resource_id: ResourceId,
pub is_write: bool,
pub bytes_accessed: u64,
pub access_mask: AccessFlags,
pub layout: ImageLayout,
}
pub struct BandwidthProfiler {
pub records: Vec<ResourceAccessRecord>,
pub per_resource_read_mb: HashMap<ResourceId, f32>,
pub per_resource_write_mb: HashMap<ResourceId, f32>,
pub per_pass_read_mb: HashMap<PassId, f32>,
pub per_pass_write_mb: HashMap<PassId, f32>,
}
impl BandwidthProfiler {
pub fn new() -> Self {
BandwidthProfiler {
records: Vec::new(),
per_resource_read_mb: HashMap::new(),
per_resource_write_mb: HashMap::new(),
per_pass_read_mb: HashMap::new(),
per_pass_write_mb: HashMap::new(),
}
}
pub fn record(&mut self, pass: PassId, resource: ResourceId, is_write: bool, bytes: u64, access: AccessFlags, layout: ImageLayout) {
self.records.push(ResourceAccessRecord { pass_id: pass, resource_id: resource, is_write, bytes_accessed: bytes, access_mask: access, layout });
}
pub fn compute_totals(&mut self) {
self.per_resource_read_mb.clear();
self.per_resource_write_mb.clear();
self.per_pass_read_mb.clear();
self.per_pass_write_mb.clear();
for rec in &self.records {
let mb = rec.bytes_accessed as f32 / (1024.0 * 1024.0);
if rec.is_write {
*self.per_resource_write_mb.entry(rec.resource_id).or_insert(0.0) += mb;
*self.per_pass_write_mb.entry(rec.pass_id).or_insert(0.0) += mb;
} else {
*self.per_resource_read_mb.entry(rec.resource_id).or_insert(0.0) += mb;
*self.per_pass_read_mb.entry(rec.pass_id).or_insert(0.0) += mb;
}
}
}
pub fn total_bandwidth_mb(&self) -> f32 {
let reads: f32 = self.per_resource_read_mb.values().sum();
let writes: f32 = self.per_resource_write_mb.values().sum();
reads + writes
}
pub fn top_bandwidth_resources(&self, n: usize) -> Vec<(ResourceId, f32)> {
let mut combined: HashMap<ResourceId, f32> = HashMap::new();
for (rid, &r) in &self.per_resource_read_mb { *combined.entry(*rid).or_insert(0.0) += r; }
for (rid, &w) in &self.per_resource_write_mb { *combined.entry(*rid).or_insert(0.0) += w; }
let mut v: Vec<(ResourceId, f32)> = combined.into_iter().collect();
v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
v.truncate(n);
v
}
}
pub struct PipelinePreset;
impl PipelinePreset {
pub fn high_quality_pc(width: u32, height: u32) -> RenderGraphEditor {
let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(width, height);
editor.name = "High Quality PC".to_owned();
for pass in editor.passes.values_mut() { pass.enabled = true; }
editor
}
pub fn medium_quality(width: u32, height: u32) -> RenderGraphEditor {
let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(width, height);
editor.name = "Medium Quality".to_owned();
for pass in editor.passes.values_mut() {
if matches!(pass.desc.kind(), PassKind::SSR) { pass.enabled = false; }
}
editor
}
pub fn mobile(width: u32, height: u32) -> RenderGraphEditor {
let editor = build_mobile_deferred_pipeline(width, height);
editor
}
pub fn shadow_only(resolution: u32) -> RenderGraphEditor {
let mut editor = RenderGraphEditor::new("ShadowOnly");
let res_shadow = editor.add_transient_texture("ShadowMap", TextureDesc::shadow_map(resolution));
editor.set_output_resources(vec![res_shadow]);
let sm_pass = editor.add_pass("ShadowMap", PassDesc::ShadowMap(ShadowMapPassDesc::directional_shadow(resolution)));
editor.set_pass_writes(sm_pass, vec![res_shadow]);
editor
}
}
#[derive(Debug, Clone)]
pub struct NodeAnnotation {
pub pass_id: PassId,
pub title: String,
pub body: String,
pub color: Vec4,
pub pinned: bool,
pub offset: Vec2,
}
impl NodeAnnotation {
pub fn new(pass_id: PassId, title: &str, body: &str) -> Self {
NodeAnnotation { pass_id, title: title.to_owned(), body: body.to_owned(), color: Vec4::new(0.9, 0.85, 0.2, 0.9), pinned: false, offset: Vec2::new(0.0, -80.0) }
}
pub fn world_pos(&self, pass_pos: Vec2) -> Vec2 { pass_pos + self.offset }
}
pub struct AnnotationManager {
pub annotations: HashMap<PassId, Vec<NodeAnnotation>>,
}
impl AnnotationManager {
pub fn new() -> Self { AnnotationManager { annotations: HashMap::new() } }
pub fn add(&mut self, ann: NodeAnnotation) { self.annotations.entry(ann.pass_id).or_default().push(ann); }
pub fn get(&self, pass_id: PassId) -> &[NodeAnnotation] { self.annotations.get(&pass_id).map(|v| v.as_slice()).unwrap_or(&[]) }
pub fn remove_all(&mut self, pass_id: PassId) { self.annotations.remove(&pass_id); }
}
#[derive(Debug, Clone)]
pub enum EditorAction {
AddPass(PassId, String),
RemovePass(PassId, String),
MovePass(PassId, Vec2, Vec2), ConnectResources(PassId, PassId, ResourceId),
DisconnectResources(PassId, PassId, ResourceId),
TogglePassEnabled(PassId, bool), RenamePass(PassId, String, String), SetOutputResource(Vec<ResourceId>, Vec<ResourceId>),
}
pub struct EditorHistory {
pub undo_stack: VecDeque<EditorAction>,
pub redo_stack: VecDeque<EditorAction>,
pub max_history: usize,
}
impl EditorHistory {
pub fn new(max: usize) -> Self {
EditorHistory { undo_stack: VecDeque::new(), redo_stack: VecDeque::new(), max_history: max }
}
pub fn push(&mut self, action: EditorAction) {
if self.undo_stack.len() >= self.max_history {
self.undo_stack.pop_front();
}
self.undo_stack.push_back(action);
self.redo_stack.clear();
}
pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
pub fn peek_undo(&self) -> Option<&EditorAction> { self.undo_stack.back() }
pub fn pop_undo(&mut self) -> Option<EditorAction> { self.undo_stack.pop_back() }
pub fn push_redo(&mut self, action: EditorAction) { self.redo_stack.push_back(action); }
pub fn pop_redo(&mut self) -> Option<EditorAction> { self.redo_stack.pop_back() }
}
#[derive(Debug, Clone)]
pub struct PassGroup {
pub id: u32,
pub name: String,
pub passes: Vec<PassId>,
pub color: Vec4,
pub collapsed: bool,
pub bounds: (Vec2, Vec2), }
impl PassGroup {
pub fn new(id: u32, name: &str, passes: Vec<PassId>, color: Vec4) -> Self {
PassGroup { id, name: name.to_owned(), passes, color, collapsed: false, bounds: (Vec2::ZERO, Vec2::ZERO) }
}
pub fn compute_bounds(&mut self, pass_positions: &HashMap<PassId, Vec2>, pass_sizes: &HashMap<PassId, Vec2>) {
let mut min = Vec2::splat(f32::MAX);
let mut max = Vec2::splat(f32::MIN);
for pid in &self.passes {
if let (Some(&pos), Some(&size)) = (pass_positions.get(pid), pass_sizes.get(pid)) {
min = min.min(pos);
max = max.max(pos + size);
}
}
let padding = Vec2::splat(20.0);
self.bounds = (min - padding, max + padding);
}
pub fn contains_point(&self, pt: Vec2) -> bool {
pt.x >= self.bounds.0.x && pt.x <= self.bounds.1.x &&
pt.y >= self.bounds.0.y && pt.y <= self.bounds.1.y
}
}
pub struct PassGroupManager {
pub groups: Vec<PassGroup>,
next_id: u32,
}
impl PassGroupManager {
pub fn new() -> Self { PassGroupManager { groups: Vec::new(), next_id: 0 } }
pub fn add_group(&mut self, name: &str, passes: Vec<PassId>, color: Vec4) -> u32 {
let id = self.next_id;
self.groups.push(PassGroup::new(id, name, passes, color));
self.next_id += 1;
id
}
pub fn group_for_pass(&self, pass_id: PassId) -> Option<&PassGroup> {
self.groups.iter().find(|g| g.passes.contains(&pass_id))
}
pub fn remove_group(&mut self, id: u32) {
self.groups.retain(|g| g.id != id);
}
}
#[derive(Debug, Clone)]
pub struct CapturedFrame {
pub frame_index: u64,
pub timestamp_ms: f64,
pub pass_order: Vec<PassId>,
pub pass_timings: HashMap<PassId, f64>,
pub resource_transitions: Vec<(PassId, ResourceId, ImageLayout, ImageLayout)>,
pub barrier_count: usize,
pub draw_calls_per_pass: HashMap<PassId, u32>,
pub triangles_per_pass: HashMap<PassId, u64>,
pub notes: Vec<String>,
}
impl CapturedFrame {
pub fn new(frame_index: u64, timestamp_ms: f64) -> Self {
CapturedFrame { frame_index, timestamp_ms, pass_order: Vec::new(), pass_timings: HashMap::new(), resource_transitions: Vec::new(), barrier_count: 0, draw_calls_per_pass: HashMap::new(), triangles_per_pass: HashMap::new(), notes: Vec::new() }
}
pub fn total_gpu_ms(&self) -> f64 { self.pass_timings.values().sum() }
pub fn total_draw_calls(&self) -> u32 { self.draw_calls_per_pass.values().sum() }
pub fn total_triangles(&self) -> u64 { self.triangles_per_pass.values().sum() }
pub fn longest_pass(&self) -> Option<PassId> {
self.pass_timings.iter().max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal)).map(|(&p, _)| p)
}
pub fn passes_over_budget(&self, budget_ms: f64) -> Vec<PassId> {
self.pass_timings.iter().filter(|(_, &t)| t > budget_ms).map(|(&p, _)| p).collect()
}
pub fn timeline_html(&self, pass_names: &HashMap<PassId, String>) -> String {
let total = self.total_gpu_ms().max(1e-6);
let mut s = String::new();
s.push_str("<div style='font-family:monospace;background:#111;padding:8px'>");
for pid in &self.pass_order {
let name = pass_names.get(pid).map(|n| n.as_str()).unwrap_or("?");
let ms = self.pass_timings.get(pid).cloned().unwrap_or(0.0);
let pct = (ms / total * 100.0) as u32;
let width = pct.clamp(1, 100);
let color = if ms > total * 0.2 { "#ff4444" } else if ms > total * 0.1 { "#ffaa22" } else { "#44aa44" };
s.push_str(&format!(
"<div style='display:flex;align-items:center;margin:2px 0'>\
<span style='color:#ccc;width:160px;display:inline-block'>{}</span>\
<div style='width:{}%;background:{};height:14px;display:inline-block'></div>\
<span style='color:#aaa;margin-left:4px'>{:.2}ms</span></div>",
name, width, color, ms
));
}
s.push_str("</div>");
s
}
}
pub struct FrameDebugger {
pub captures: VecDeque<CapturedFrame>,
pub max_captures: usize,
pub is_capturing: bool,
pub current_capture: Option<CapturedFrame>,
}
impl FrameDebugger {
pub fn new(max: usize) -> Self {
FrameDebugger { captures: VecDeque::new(), max_captures: max, is_capturing: false, current_capture: None }
}
pub fn begin_capture(&mut self, frame_index: u64, timestamp_ms: f64) {
self.is_capturing = true;
self.current_capture = Some(CapturedFrame::new(frame_index, timestamp_ms));
}
pub fn record_pass_timing(&mut self, pass: PassId, ms: f64) {
if let Some(ref mut cap) = self.current_capture {
cap.pass_timings.insert(pass, ms);
cap.pass_order.push(pass);
}
}
pub fn record_transition(&mut self, pass: PassId, res: ResourceId, old: ImageLayout, new: ImageLayout) {
if let Some(ref mut cap) = self.current_capture {
cap.resource_transitions.push((pass, res, old, new));
}
}
pub fn end_capture(&mut self) {
if let Some(cap) = self.current_capture.take() {
if self.captures.len() >= self.max_captures { self.captures.pop_front(); }
self.captures.push_back(cap);
}
self.is_capturing = false;
}
pub fn latest(&self) -> Option<&CapturedFrame> { self.captures.back() }
pub fn at_frame(&self, frame_index: u64) -> Option<&CapturedFrame> {
self.captures.iter().find(|c| c.frame_index == frame_index)
}
}
pub struct CriticalPathAnalyzer;
impl CriticalPathAnalyzer {
pub fn find_critical_path(
sorted: &[PassId],
timings: &HashMap<PassId, f64>,
edges: &HashMap<PassId, Vec<PassId>>,
) -> (Vec<PassId>, f64) {
let mut earliest_finish: HashMap<PassId, f64> = HashMap::new();
let mut predecessor: HashMap<PassId, Option<PassId>> = HashMap::new();
for pid in sorted {
let t = timings.get(pid).cloned().unwrap_or(1.0);
let max_pred_finish = edges.iter()
.filter(|(_, dsts)| dsts.contains(pid))
.map(|(src, _)| *earliest_finish.get(src).unwrap_or(&0.0))
.fold(0.0f64, f64::max);
let ef = max_pred_finish + t;
earliest_finish.insert(*pid, ef);
let pred = edges.iter()
.filter(|(_, dsts)| dsts.contains(pid))
.max_by(|(a, _), (b, _)| {
let ta = earliest_finish.get(*a).unwrap_or(&0.0);
let tb = earliest_finish.get(*b).unwrap_or(&0.0);
ta.partial_cmp(tb).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(src, _)| *src);
predecessor.insert(*pid, pred);
}
let end_pass = sorted.iter().max_by(|a, b| {
let ta = earliest_finish.get(*a).unwrap_or(&0.0);
let tb = earliest_finish.get(*b).unwrap_or(&0.0);
ta.partial_cmp(tb).unwrap_or(std::cmp::Ordering::Equal)
});
let mut path = Vec::new();
if let Some(&last) = end_pass {
let total_time = *earliest_finish.get(&last).unwrap_or(&0.0);
let mut current = Some(last);
while let Some(node) = current {
path.push(node);
current = predecessor.get(&node).and_then(|p| *p);
}
path.reverse();
(path, total_time)
} else {
(Vec::new(), 0.0)
}
}
}
#[derive(Debug, Clone)]
pub struct LightCullingPassDesc {
pub width: u32,
pub height: u32,
pub tile_size: u32,
pub max_lights: u32,
pub output_light_indices: ResourceId,
pub output_light_counts: ResourceId,
pub input_depth: ResourceId,
pub depth_prepass: bool,
}
impl LightCullingPassDesc {
pub fn default(width: u32, height: u32) -> Self {
LightCullingPassDesc {
width, height, tile_size: 16, max_lights: 1024,
output_light_indices: ResourceId(200),
output_light_counts: ResourceId(201),
input_depth: ResourceId(4),
depth_prepass: true,
}
}
pub fn tiles_x(&self) -> u32 { (self.width + self.tile_size - 1) / self.tile_size }
pub fn tiles_y(&self) -> u32 { (self.height + self.tile_size - 1) / self.tile_size }
pub fn dispatch_x(&self) -> u32 { self.tiles_x() }
pub fn dispatch_y(&self) -> u32 { self.tiles_y() }
pub fn light_index_buffer_bytes(&self) -> u64 {
self.tiles_x() as u64 * self.tiles_y() as u64 * self.max_lights as u64 * 2
}
pub fn light_count_buffer_bytes(&self) -> u64 {
self.tiles_x() as u64 * self.tiles_y() as u64 * 4
}
}
#[derive(Debug, Clone)]
pub struct DecalPassDesc {
pub width: u32,
pub height: u32,
pub output_albedo: ResourceId,
pub output_normal: ResourceId,
pub input_depth: ResourceId,
pub max_decals: u32,
pub blend: ColorBlendAttachment,
pub depth_stencil: DepthStencilState,
}
impl DecalPassDesc {
pub fn default(width: u32, height: u32) -> Self {
DecalPassDesc {
width, height,
output_albedo: ResourceId(0),
output_normal: ResourceId(1),
input_depth: ResourceId(4),
max_decals: 256,
blend: ColorBlendAttachment::alpha_blend(),
depth_stencil: DepthStencilState::depth_read_only(),
}
}
pub fn decal_clip_bounds(decal_world_to_local: Mat4, view_proj: Mat4) -> (Vec3, Vec3) {
let cube_corners: [Vec3; 8] = [
Vec3::new(-0.5, -0.5, -0.5), Vec3::new(0.5, -0.5, -0.5),
Vec3::new(-0.5, 0.5, -0.5), Vec3::new(0.5, 0.5, -0.5),
Vec3::new(-0.5, -0.5, 0.5), Vec3::new(0.5, -0.5, 0.5),
Vec3::new(-0.5, 0.5, 0.5), Vec3::new(0.5, 0.5, 0.5),
];
let local_to_world = decal_world_to_local.inverse();
let mut min = Vec3::splat(f32::MAX);
let mut max = Vec3::splat(f32::MIN);
for c in &cube_corners {
let world = (local_to_world * Vec4::new(c.x, c.y, c.z, 1.0)).truncate();
let clip = view_proj * Vec4::new(world.x, world.y, world.z, 1.0);
let ndc = if clip.w.abs() > 1e-6 { clip.truncate() / clip.w } else { clip.truncate() };
min = min.min(ndc);
max = max.max(ndc);
}
(min, max)
}
}
#[derive(Debug, Clone)]
pub struct SkyPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_sky: ResourceId,
pub input_depth: ResourceId,
pub model: SkyModel,
pub sun_direction: Vec3,
pub sun_intensity: f32,
pub turbidity: f32, pub ground_albedo: Vec3,
pub ozone_absorption: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkyModel { Preetham, Hosek, PhysicalAtmosphere, PBRSky, Static }
impl SkyPassDesc {
pub fn default(width: u32, height: u32) -> Self {
SkyPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_sky: ResourceId(60),
input_depth: ResourceId(4),
model: SkyModel::Hosek,
sun_direction: Vec3::new(0.0, 1.0, 0.0).normalize(),
sun_intensity: 10.0,
turbidity: 2.0,
ground_albedo: Vec3::new(0.1, 0.1, 0.1),
ozone_absorption: true,
}
}
pub fn hosek_wilkie_simple(&self, view_dir: Vec3) -> Vec3 {
let sun = self.sun_direction.normalize();
let cos_theta = view_dir.y.max(0.0);
let cos_gamma = view_dir.dot(sun).clamp(-1.0, 1.0);
let gamma = cos_gamma.acos();
let theta = cos_theta.acos().min(std::f32::consts::FRAC_PI_2);
let t = self.turbidity;
let a = 0.1787 * t - 1.4630;
let b = -0.3554 * t + 0.4275;
let c = -0.0227 * t + 5.3251;
let d = 0.1206 * t - 2.5771;
let e = -0.0670 * t + 0.3703;
let hosek_f = |theta: f32, gamma: f32| -> f32 {
(1.0 + a * (-b / theta.cos().max(1e-4)).exp()) *
(1.0 + c * (-d * gamma).exp() + e * cos_gamma * cos_gamma)
};
let zenith_luminance = hosek_f(0.0, 0.0_f32.acos());
let sky_lum = hosek_f(theta, gamma) / zenith_luminance.max(1e-6);
let blue_tint = Vec3::new(0.6, 0.8, 1.0);
let base_sky = blue_tint * sky_lum.max(0.0) * 5.0;
let sun_disk = if cos_gamma > 0.9998 {
Vec3::new(1.0, 0.9, 0.7) * self.sun_intensity * 1000.0
} else {
Vec3::ZERO
};
base_sky + sun_disk
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PipelineKey {
pub pass_kind: PassKind,
pub fill_mode: u8, pub cull_mode: u8, pub depth_test: bool,
pub depth_write: bool,
pub blend_enabled: bool,
pub sample_count: u8,
pub output_format_hash: u64,
}
impl PipelineKey {
pub fn from_pass(pass: &PassNode, rasterizer: &RasterizerState, ds: &DepthStencilState, blend: bool, samples: SampleCount, output_fmt: TextureFormat) -> Self {
let fmt_hash = format_hash(output_fmt);
PipelineKey {
pass_kind: pass.desc.kind(),
fill_mode: match rasterizer.fill_mode { FillMode::Solid => 0, FillMode::Wireframe => 1, FillMode::Point => 2 },
cull_mode: match rasterizer.cull_mode { CullMode::None => 0, CullMode::Front => 1, CullMode::Back => 2, CullMode::FrontAndBack => 3 },
depth_test: ds.depth_test_enable,
depth_write: ds.depth_write_enable,
blend_enabled: blend,
sample_count: samples.count() as u8,
output_format_hash: fmt_hash,
}
}
}
fn format_hash(fmt: TextureFormat) -> u64 {
(fmt as u64).wrapping_mul(0x9e3779b97f4a7c15)
}
pub struct PipelineCache {
pub entries: HashMap<PipelineKey, u64>, pub hit_count: u64,
pub miss_count: u64,
pub evict_count: u64,
pub max_entries: usize,
}
impl PipelineCache {
pub fn new(max_entries: usize) -> Self {
PipelineCache { entries: HashMap::new(), hit_count: 0, miss_count: 0, evict_count: 0, max_entries }
}
pub fn get(&mut self, key: &PipelineKey) -> Option<u64> {
if let Some(&handle) = self.entries.get(key) {
self.hit_count += 1;
Some(handle)
} else {
self.miss_count += 1;
None
}
}
pub fn insert(&mut self, key: PipelineKey, handle: u64) {
if self.entries.len() >= self.max_entries {
if let Some(evict_key) = self.entries.keys().next().cloned() {
self.entries.remove(&evict_key);
self.evict_count += 1;
}
}
self.entries.insert(key, handle);
}
pub fn hit_rate(&self) -> f32 {
let total = self.hit_count + self.miss_count;
if total == 0 { 1.0 } else { self.hit_count as f32 / total as f32 }
}
}
pub fn export_dot(editor: &RenderGraphEditor) -> String {
let mut s = String::new();
s.push_str("digraph RenderGraph {\n");
s.push_str(" rankdir=LR;\n");
s.push_str(" node [shape=box, style=filled];\n");
for pass in editor.passes.values() {
let color = color_to_hex(pass.editor_color);
let label = format!("{}\n[{:?}]", pass.name, pass.desc.kind());
s.push_str(&format!(" pass_{} [label=\"{}\", fillcolor=\"{}\"];\n", pass.id.0, label, color));
}
s.push_str(" // resource nodes\n");
for res in editor.resources.values() {
let color = match res.lifetime {
ResourceLifetime::Transient => "#aaffaa",
ResourceLifetime::Persistent => "#ffaaaa",
ResourceLifetime::Imported => "#aaaaff",
};
let desc = match &res.desc {
ResourceDesc::Texture(t) => format!("{}x{} {:?}", t.width, t.height, t.format),
ResourceDesc::Buffer(b) => format!("{}B buffer", b.size),
};
s.push_str(&format!(" res_{} [label=\"{}\\n{}\", shape=ellipse, fillcolor=\"{}\"];\n", res.id.0, res.name, desc, color));
}
s.push_str(" // edges\n");
for pass in editor.passes.values() {
for rid in &pass.reads {
s.push_str(&format!(" res_{} -> pass_{};\n", rid.0, pass.id.0));
}
for rid in &pass.writes {
s.push_str(&format!(" pass_{} -> res_{};\n", pass.id.0, rid.0));
}
}
s.push_str("}\n");
s
}
fn color_to_hex(c: Vec4) -> String {
let r = (c.x.clamp(0.0, 1.0) * 255.0) as u8;
let g = (c.y.clamp(0.0, 1.0) * 255.0) as u8;
let b = (c.z.clamp(0.0, 1.0) * 255.0) as u8;
format!("#{:02X}{:02X}{:02X}", r, g, b)
}
pub fn export_mermaid(editor: &RenderGraphEditor) -> String {
let mut s = String::new();
s.push_str("graph LR\n");
let mut connections: HashSet<(u32, u32)> = HashSet::new();
let mut resource_writers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
let mut resource_readers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
for pass in editor.passes.values() {
for rid in &pass.writes { resource_writers.entry(*rid).or_default().push(pass.id); }
for rid in &pass.reads { resource_readers.entry(*rid).or_default().push(pass.id); }
}
for (rid, writers) in &resource_writers {
if let Some(readers) = resource_readers.get(rid) {
for w in writers {
for r in readers {
if w != r { connections.insert((w.0, r.0)); }
}
}
}
}
for pass in editor.passes.values() {
let kind = format!("{:?}", pass.desc.kind());
s.push_str(&format!(" P{}[{}<br/><i>{}</i>]\n", pass.id.0, pass.name, kind));
}
for (src, dst) in &connections {
s.push_str(&format!(" P{} --> P{}\n", src, dst));
}
s
}
pub struct HotReloadManager {
pub current: RenderGraphEditor,
pub pending: Option<RenderGraphEditor>,
pub last_reload_frame: u64,
pub reload_on_next_frame: bool,
}
impl HotReloadManager {
pub fn new(editor: RenderGraphEditor) -> Self {
HotReloadManager { current: editor, pending: None, last_reload_frame: 0, reload_on_next_frame: false }
}
pub fn stage_reload(&mut self, new_editor: RenderGraphEditor) {
self.pending = Some(new_editor);
self.reload_on_next_frame = true;
}
pub fn apply_reload_if_pending(&mut self, current_frame: u64) -> bool {
if self.reload_on_next_frame {
if let Some(new) = self.pending.take() {
let diffs = diff_render_graphs(&self.current, &new);
self.current = new;
self.last_reload_frame = current_frame;
self.reload_on_next_frame = false;
return !diffs.is_empty();
}
}
false
}
pub fn needs_recompile(&self, current_frame: u64) -> bool {
current_frame == self.last_reload_frame
}
}
#[derive(Debug, Clone)]
pub struct ResolvePassDesc {
pub width: u32,
pub height: u32,
pub format: TextureFormat,
pub input_msaa: ResourceId,
pub output_resolved: ResourceId,
pub sample_count: SampleCount,
}
impl ResolvePassDesc {
pub fn new(width: u32, height: u32, format: TextureFormat, input: ResourceId, output: ResourceId, samples: SampleCount) -> Self {
ResolvePassDesc { width, height, format, input_msaa: input, output_resolved: output, sample_count: samples }
}
pub fn box_filter_weights(samples: SampleCount) -> Vec<f32> {
let n = samples.count() as usize;
vec![1.0 / n as f32; n]
}
pub fn msaa4x_sample_positions() -> [Vec2; 4] {
[
Vec2::new(-0.125, -0.375),
Vec2::new( 0.375, -0.125),
Vec2::new(-0.375, 0.125),
Vec2::new( 0.125, 0.375),
]
}
pub fn msaa8x_sample_positions() -> [Vec2; 8] {
[
Vec2::new( 0.0625, -0.1875), Vec2::new(-0.0625, 0.1875),
Vec2::new( 0.3125, 0.0625), Vec2::new(-0.1875, -0.3125),
Vec2::new(-0.3125, 0.3125), Vec2::new(-0.4375, -0.0625),
Vec2::new( 0.1875, 0.4375), Vec2::new( 0.4375, -0.4375),
]
}
}
#[derive(Debug, Clone)]
pub struct PostFxChain {
pub effects: Vec<PostFxEffect>,
pub input: ResourceId,
pub output: ResourceId,
}
#[derive(Debug, Clone)]
pub enum PostFxEffect {
Bloom(BloomPassDesc),
ToneMapping(ToneMappingPassDesc),
TAA(TAAPassDesc),
DepthOfField(DepthOfFieldPassDesc),
MotionBlur(MotionBlurPassDesc),
VolumetricFog(VolumetricFogPassDesc),
ChromaticAberration { strength: f32, samples: u32 },
FilmGrain { strength: f32, animated: bool },
Vignette { radius: f32, smoothness: f32, color: Vec4 },
LensFlare { threshold: f32, intensity: f32 },
Sharpen { amount: f32 },
CAS { sharpness: f32 }, }
impl PostFxChain {
pub fn default(width: u32, height: u32, input: ResourceId, output: ResourceId) -> Self {
PostFxChain {
effects: vec![
PostFxEffect::Bloom(BloomPassDesc::default(width, height)),
PostFxEffect::ToneMapping(ToneMappingPassDesc::default(width, height)),
PostFxEffect::TAA(TAAPassDesc::default(width, height)),
PostFxEffect::ChromaticAberration { strength: 0.003, samples: 3 },
PostFxEffect::FilmGrain { strength: 0.03, animated: true },
PostFxEffect::Vignette { radius: 0.75, smoothness: 0.45, color: Vec4::new(0.0, 0.0, 0.0, 1.0) },
PostFxEffect::Sharpen { amount: 0.3 },
],
input, output,
}
}
pub fn chromatic_aberration_offset(uv: Vec2, strength: f32, channel: u32) -> Vec2 {
let center = Vec2::splat(0.5);
let dist = uv - center;
let offset = dist * strength * (channel as f32 - 1.0);
uv + offset
}
pub fn film_grain(uv: Vec2, time: f32, strength: f32) -> f32 {
let frame_index = (time * 60.0) as u32;
let p = uv * 1000.0 + Vec2::new((frame_index % 256) as f32, ((frame_index / 256) % 256) as f32);
let n = (p.x * 0.06711056 + p.y * 0.00583715).fract();
let n = (n * 52.9829189).fract();
(n - 0.5) * 2.0 * strength
}
pub fn vignette_factor(uv: Vec2, radius: f32, smoothness: f32) -> f32 {
let dist = (uv - Vec2::splat(0.5)).length() / (radius * std::f32::consts::SQRT_2);
1.0 - smoothstep(1.0 - smoothness, 1.0, dist)
}
pub fn cas_sharpen(center: Vec3, neighbors: [Vec3; 4], sharpness: f32) -> Vec3 {
let min_c = neighbors.iter().fold(center, |acc, &n| acc.min(n));
let max_c = neighbors.iter().fold(center, |acc, &n| acc.max(n));
let w_min = Vec3::ONE / max_c.max(Vec3::splat(1e-6));
let w_max = Vec3::ONE / min_c.max(Vec3::splat(1e-6));
let w = (-(Vec3::ONE / (min_c * 8.0))).max(Vec3::splat(-0.125)) * sharpness;
let sum: Vec3 = neighbors.iter().map(|&n| n * w).fold(Vec3::ZERO, |a, b| a + b);
(center + sum) / (Vec3::ONE + 4.0 * w)
}
}
pub struct CompilationReport {
pub success: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub pass_count: usize,
pub dead_pass_count: usize,
pub resource_count: usize,
pub transient_resource_count: usize,
pub aliasing_group_count: usize,
pub total_barriers: usize,
pub estimated_memory_mb: f32,
pub estimated_bandwidth_mb: f32,
pub compile_time_us: u64,
pub sort_order: Vec<String>,
}
impl CompilationReport {
pub fn from_compiled(compiled: &CompiledRenderGraph, pass_names: &HashMap<PassId, String>) -> Self {
let sort_order: Vec<String> = compiled.sorted_passes.iter()
.map(|pid| pass_names.get(pid).cloned().unwrap_or_else(|| format!("{:?}", pid)))
.collect();
CompilationReport {
success: true,
errors: Vec::new(),
warnings: Vec::new(),
pass_count: compiled.sorted_passes.len(),
dead_pass_count: compiled.dead_passes.len(),
resource_count: compiled.resource_lifetimes.len(),
transient_resource_count: compiled.aliasing_groups.iter().map(|g| g.len()).sum(),
aliasing_group_count: compiled.aliasing_groups.len(),
total_barriers: compiled.barriers.values().map(|b| b.image_barriers.len() + b.buffer_barriers.len()).sum(),
estimated_memory_mb: compiled.estimated_memory_bytes as f32 / (1024.0 * 1024.0),
estimated_bandwidth_mb: compiled.estimated_bandwidth_mb,
compile_time_us: 0,
sort_order,
}
}
pub fn print(&self) -> String {
let mut s = String::new();
if self.success {
s.push_str("[OK] Render graph compiled successfully\n");
} else {
s.push_str("[FAIL] Render graph compilation FAILED\n");
for e in &self.errors { s.push_str(&format!(" ERROR: {}\n", e)); }
}
for w in &self.warnings { s.push_str(&format!(" WARN: {}\n", w)); }
s.push_str(&format!(" Passes: {} ({} dead)\n", self.pass_count, self.dead_pass_count));
s.push_str(&format!(" Resources: {} ({} transient, {} aliasing groups)\n", self.resource_count, self.transient_resource_count, self.aliasing_group_count));
s.push_str(&format!(" Barriers: {}\n", self.total_barriers));
s.push_str(&format!(" Memory: {:.2} MB\n", self.estimated_memory_mb));
s.push_str(&format!(" Bandwidth: {:.1} MB/frame\n", self.estimated_bandwidth_mb));
s.push_str(" Execution order: ");
for (i, name) in self.sort_order.iter().enumerate() {
if i > 0 { s.push_str(" -> "); }
s.push_str(name);
}
s.push('\n');
s
}
}
#[cfg(test)]
mod extended_tests {
use super::*;
#[test]
fn test_shadow_atlas_allocation() {
let mut atlas = ShadowAtlas::new(4096);
let r1 = atlas.allocate(1, 512, 512);
let r2 = atlas.allocate(2, 1024, 1024);
assert!(r1.is_some());
assert!(r2.is_some());
let r1 = r1.unwrap();
assert_eq!(r1.x, 0);
assert_eq!(r1.y, 0);
assert!(atlas.utilization() > 0.0);
}
#[test]
fn test_shadow_atlas_free() {
let mut atlas = ShadowAtlas::new(1024);
atlas.allocate(1, 512, 512);
atlas.free_region(1);
assert!(atlas.regions.is_empty());
}
#[test]
fn test_timestamp_pool() {
let mut pool = QueryPool::new_timestamp(32, 1.0);
let pair = pool.allocate_pair();
assert!(pair.is_some());
let (s, e) = pair.unwrap();
assert_eq!(e, s + 1);
let ms = pool.ticks_to_ms(1_000_000);
assert!((ms - 1.0).abs() < 1e-6);
}
#[test]
fn test_frame_uniforms_size() {
let sz = FrameUniforms::size_bytes();
assert!(sz > 0);
assert_eq!(sz % 16, 0, "FrameUniforms must be 16-byte aligned");
}
#[test]
fn test_dependency_matrix() {
let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let passes: Vec<PassId> = editor.passes.keys().cloned().collect();
let edges = editor.build_edges();
let matrix = DependencyMatrix::build(&passes, &edges);
assert_eq!(matrix.matrix.len(), passes.len());
}
#[test]
fn test_dot_export() {
let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let dot = export_dot(&editor);
assert!(dot.contains("digraph RenderGraph"));
assert!(dot.contains("GBuffer"));
}
#[test]
fn test_mermaid_export() {
let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let mermaid = export_mermaid(&editor);
assert!(mermaid.contains("graph LR"));
}
#[test]
fn test_post_fx_vignette() {
let v = PostFxChain::vignette_factor(Vec2::splat(0.5), 0.75, 0.45);
assert!((v - 1.0).abs() < 0.01, "center should be no vignette");
let v2 = PostFxChain::vignette_factor(Vec2::new(0.0, 0.0), 0.75, 0.45);
assert!(v2 < v, "corner should have more vignette");
}
#[test]
fn test_chromatic_aberration() {
let uv = Vec2::new(0.75, 0.5);
let r = PostFxChain::chromatic_aberration_offset(uv, 0.01, 0);
let g = PostFxChain::chromatic_aberration_offset(uv, 0.01, 1);
let b = PostFxChain::chromatic_aberration_offset(uv, 0.01, 2);
assert!(r != g || g != b || r != b || true); }
#[test]
fn test_light_culling_pass() {
let lc = LightCullingPassDesc::default(1920, 1080);
assert_eq!(lc.tiles_x(), 120);
assert_eq!(lc.tiles_y(), 68);
assert!(lc.light_index_buffer_bytes() > 0);
}
#[test]
fn test_clustered_light_grid_memory() {
let grid = ClusteredLightGrid::new(1920, 1080, 16, 24, 0.1, 100.0);
let mem = grid.memory_requirements(64);
assert!(mem > 0);
}
#[test]
fn test_hosek_sky() {
let sky = SkyPassDesc::default(1920, 1080);
let dir = Vec3::new(0.0, 1.0, 0.0).normalize();
let color = sky.hosek_wilkie_simple(dir);
assert!(color.length() > 0.0);
}
#[test]
fn test_compile_report() {
let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
editor.compile().unwrap();
let compiled = editor.compiled.as_ref().unwrap();
let names: HashMap<PassId, String> = editor.passes.values().map(|p| (p.id, p.name.clone())).collect();
let report = CompilationReport::from_compiled(compiled, &names);
let text = report.print();
assert!(text.contains("[OK]"));
}
#[test]
fn test_pipeline_cache() {
let mut cache = PipelineCache::new(16);
let key = PipelineKey {
pass_kind: PassKind::GBuffer,
fill_mode: 0, cull_mode: 2, depth_test: true, depth_write: true,
blend_enabled: false, sample_count: 1, output_format_hash: 42,
};
assert!(cache.get(&key).is_none());
cache.insert(key.clone(), 9999);
assert_eq!(cache.get(&key), Some(9999));
assert!(cache.hit_rate() > 0.0);
}
#[test]
fn test_ggx_brdf() {
let n = Vec3::Y;
let v = Vec3::new(0.0, 1.0, 0.0);
let l = Vec3::new(0.5, 0.5, 0.0).normalize();
let albedo = Vec3::new(0.8, 0.2, 0.1);
let result = cook_torrance_brdf(n, v, l, albedo, 0.0, 0.5);
assert!(result.length() > 0.0);
assert!(result.x <= 10.0 && result.y <= 10.0 && result.z <= 10.0);
}
#[test]
fn test_brdf_lut_integration() {
let lut = integrate_brdf(0.5, 0.5, 64);
assert!(lut.x >= 0.0 && lut.x <= 1.0);
assert!(lut.y >= 0.0 && lut.y <= 1.0);
}
#[test]
fn test_aabb_frustum_cull() {
let vp = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 100.0);
let planes = frustum_planes_from_view_proj(vp);
let inside = aabb_in_frustum(&planes, Vec3::new(-0.5, -0.5, -10.0), Vec3::new(0.5, 0.5, -9.0));
assert!(inside || !inside); }
#[test]
fn test_tbr_bandwidth_savings() {
let fmts = vec![TextureFormat::RGBA8Unorm, TextureFormat::RG16Float, TextureFormat::Depth24UnormStencil8];
let savings = TBRDetector::bandwidth_savings_mb(1920, 1080, &fmts);
assert!(savings > 0.0);
}
#[test]
fn test_async_compute_scheduling() {
let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let candidates: HashSet<PassId> = AsyncComputeScheduler::identify_async_candidates(&editor.passes).into_iter().collect();
assert!(!candidates.is_empty());
}
#[test]
fn test_forward_plus_compile() {
let mut editor = build_forward_plus_pipeline(1920, 1080);
let result = editor.compile();
assert!(result.is_ok(), "{:?}", result);
}
#[test]
fn test_mobile_deferred_compile() {
let mut editor = build_mobile_deferred_pipeline(1920, 1080);
let result = editor.compile();
assert!(result.is_ok(), "{:?}", result);
}
#[test]
fn test_gbuffer_lighting_renderpass() {
let gbuf = GBufferPassDesc::default(1920, 1080);
let light = LightingPassDesc::default(1920, 1080);
let rp = RenderPassDescription::build_gbuffer_lighting_renderpass(&gbuf, &light);
assert_eq!(rp.subpasses.len(), 2);
assert!(rp.detect_tbr_optimization());
let bw = rp.total_load_store_bandwidth_bytes(1920, 1080);
assert!(bw > 0);
}
#[test]
fn test_resource_aliasing() {
let td_a = TextureDesc::render_target(1920, 1080, TextureFormat::RGBA16Float);
let td_b = TextureDesc::render_target(1920, 1080, TextureFormat::RGBA16Float);
let mut ra = RenderGraphResource::new_transient_texture(ResourceId(0), "a", td_a);
let mut rb = RenderGraphResource::new_transient_texture(ResourceId(1), "b", td_b);
ra.first_use = 0; ra.last_use = 2;
rb.first_use = 5; rb.last_use = 8;
assert!(!ra.lifetime_overlaps(&rb));
assert!(ra.can_alias_with(&rb));
}
#[test]
fn test_bloom_mip_sizes() {
let bloom = BloomPassDesc::default(1920, 1080);
let (w0, h0) = bloom.mip_size(0);
let (w1, h1) = bloom.mip_size(1);
assert_eq!(w0, 1920);
assert_eq!(w1, 960);
assert_eq!(h1, 540);
}
#[test]
fn test_dof_coc() {
let dof = DepthOfFieldPassDesc::default(1920, 1080);
let coc = dof.coc_from_depth(10.0, 0.05, 0.1); assert!(coc.abs() < 0.01);
}
#[test]
fn test_motion_blur_soft_depth() {
let soft = MotionBlurPassDesc::soft_depth_compare(1.0, 0.5, 1.0);
assert!(soft > 0.0 && soft <= 1.0);
}
}
#[derive(Debug, Clone)]
pub struct VXGIPassDesc {
pub voxel_grid_size: u32, pub voxel_world_size: f32, pub output_radiance_grid: ResourceId,
pub output_normal_grid: ResourceId,
pub output_opacity_grid: ResourceId,
pub inject_light: bool,
pub num_cones: u32,
pub cone_aperture_deg: f32,
pub max_cone_distance: f32,
pub indirect_diffuse_enabled: bool,
pub indirect_specular_enabled: bool,
pub mip_generation: bool,
pub temporal_accumulation: f32,
}
impl VXGIPassDesc {
pub fn default() -> Self {
VXGIPassDesc {
voxel_grid_size: 256,
voxel_world_size: 50.0,
output_radiance_grid: ResourceId(300),
output_normal_grid: ResourceId(301),
output_opacity_grid: ResourceId(302),
inject_light: true,
num_cones: 6,
cone_aperture_deg: 60.0,
max_cone_distance: 10.0,
indirect_diffuse_enabled: true,
indirect_specular_enabled: true,
mip_generation: true,
temporal_accumulation: 0.05,
}
}
pub fn voxel_size(&self) -> f32 {
self.voxel_world_size / self.voxel_grid_size as f32
}
pub fn grid_memory_bytes(&self) -> u64 {
let n = self.voxel_grid_size as u64;
n * n * n * (8 + 8 + 4)
}
pub fn mip_levels(&self) -> u32 {
compute_mip_count(self.voxel_grid_size, self.voxel_grid_size)
}
pub fn cone_trace(
&self,
start: Vec3,
direction: Vec3,
aperture: f32,
max_distance: f32,
step_multiplier: f32,
) -> (Vec3, f32) {
let voxel_size = self.voxel_size();
let mut accum_color = Vec3::ZERO;
let mut accum_alpha = 0.0f32;
let mut dist = voxel_size; while dist < max_distance && accum_alpha < 0.95 {
let diameter = 2.0 * aperture * dist;
let mip = (diameter / voxel_size).log2().max(0.0);
let sample_pos = start + direction * dist;
let alpha = 0.1 * (1.0 - accum_alpha); let color = Vec3::new(0.1, 0.08, 0.06) * alpha; accum_color += color * (1.0 - accum_alpha);
accum_alpha += alpha * (1.0 - accum_alpha);
dist += diameter.max(voxel_size) * step_multiplier;
}
(accum_color, accum_alpha)
}
pub fn diffuse_cone_directions(num_cones: u32) -> Vec<Vec3> {
let mut dirs = Vec::with_capacity(num_cones as usize);
let sq3 = (1.0f32/3.0).sqrt();
dirs.push(Vec3::new( 0.0, 1.0, 0.0));
dirs.push(Vec3::new( sq3 * 2.0, sq3, 0.0).normalize());
dirs.push(Vec3::new(-sq3, sq3, sq3 * std::f32::consts::SQRT_2).normalize());
dirs.push(Vec3::new(-sq3, sq3, -sq3 * std::f32::consts::SQRT_2).normalize());
dirs.push(Vec3::new( sq3, sq3, sq3 * std::f32::consts::SQRT_2).normalize());
dirs.push(Vec3::new( sq3, sq3, -sq3 * std::f32::consts::SQRT_2).normalize());
while dirs.len() < num_cones as usize {
let i = dirs.len() as f32;
let phi = i * std::f32::consts::TAU * 0.6180339887;
let theta = (1.0 - 2.0 * i / num_cones as f32).acos();
dirs.push(Vec3::new(theta.sin() * phi.cos(), theta.cos(), theta.sin() * phi.sin()));
}
dirs
}
}
pub struct HiZTracer;
impl HiZTracer {
pub fn trace(
ray_origin_ss: Vec2,
ray_dir_ss: Vec2,
ray_start_depth: f32,
max_steps: u32,
max_mip: u32,
) -> Option<(Vec2, f32)> {
let mut pos = ray_origin_ss;
let mut mip = 0u32;
let mut depth = ray_start_depth;
let step = ray_dir_ss * 0.001; for i in 0..max_steps {
pos += step * (1 << mip) as f32;
if pos.x < 0.0 || pos.x > 1.0 || pos.y < 0.0 || pos.y > 1.0 { return None; }
let sample_depth = depth - 0.01 * i as f32;
if depth > sample_depth + 0.001 {
if mip == 0 {
return Some((pos, depth));
}
mip = mip.saturating_sub(1);
} else {
mip = (mip + 1).min(max_mip);
}
depth += step.length() * 0.1;
}
None
}
pub fn cell_bounds(pos: Vec2, mip: u32, texture_size: Vec2) -> (Vec2, Vec2) {
let cell_size = Vec2::splat((1 << mip) as f32) / texture_size;
let cell = (pos / cell_size).floor();
(cell * cell_size, (cell + Vec2::ONE) * cell_size)
}
pub fn intersect_cell_boundary(pos: Vec2, dir: Vec2, cell_min: Vec2, cell_max: Vec2) -> f32 {
let t_max_x = if dir.x > 0.0 { (cell_max.x - pos.x) / (dir.x + 1e-7) }
else if dir.x < 0.0 { (cell_min.x - pos.x) / (dir.x - 1e-7) }
else { f32::MAX };
let t_max_y = if dir.y > 0.0 { (cell_max.y - pos.y) / (dir.y + 1e-7) }
else if dir.y < 0.0 { (cell_min.y - pos.y) / (dir.y - 1e-7) }
else { f32::MAX };
t_max_x.min(t_max_y)
}
}
#[derive(Debug, Clone)]
pub struct SSSPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_sss: ResourceId,
pub input_irradiance: ResourceId,
pub input_depth: ResourceId,
pub input_albedo: ResourceId,
pub algorithm: SSSAlgorithm,
pub falloff: Vec3,
pub strength: Vec3,
pub max_radius_px: f32,
pub sample_count: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SSSAlgorithm { BurleyDiffusion, SeparableSSS, PreintegratedSSS }
impl SSSPassDesc {
pub fn default(width: u32, height: u32) -> Self {
SSSPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_sss: ResourceId(400),
input_irradiance: ResourceId(10),
input_depth: ResourceId(4),
input_albedo: ResourceId(0),
algorithm: SSSAlgorithm::SeparableSSS,
falloff: Vec3::new(1.0, 0.37, 0.3),
strength: Vec3::new(0.48, 0.41, 0.28),
max_radius_px: 25.0,
sample_count: 25,
}
}
pub fn burley_diffusion_profile(r: f32, s: f32) -> f32 {
((-s * r).exp() + (-s * r / 3.0).exp()) / (8.0 * std::f32::consts::PI * r)
}
pub fn separable_kernel(&self) -> Vec<Vec4> {
let mut kernel = Vec::with_capacity(self.sample_count as usize);
let n = self.sample_count as f32;
for i in 0..self.sample_count {
let r = ((i as f32 + 0.5) / n) * self.max_radius_px;
let sigma = self.max_radius_px * 0.25;
let w = (-0.5 * (r / sigma) * (r / sigma)).exp();
let offset = r;
kernel.push(Vec4::new(offset, w * self.strength.x, w * self.strength.y, w * self.strength.z));
}
let sum_w: f32 = kernel.iter().map(|k| k.y).sum();
if sum_w > 1e-6 {
for k in &mut kernel { k.y /= sum_w; k.z /= sum_w; k.w /= sum_w; }
}
kernel
}
pub fn preintegrated_lut_value(n_dot_l: f32, curvature: f32) -> Vec3 {
let wrap = (n_dot_l + curvature * 0.5).clamp(0.0, 1.0);
let redness = (curvature * 5.0).clamp(0.0, 1.0);
Vec3::new(
lerp(smoothstep(-0.2, 0.8, n_dot_l), wrap, redness),
smoothstep(-0.1, 0.7, n_dot_l),
smoothstep(0.0, 0.6, n_dot_l),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AOAlgorithm { SSAO, HBAO, GTAO, RTAO }
#[derive(Debug, Clone)]
pub struct GTAOPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_ao: ResourceId,
pub input_depth: ResourceId,
pub input_normal: ResourceId,
pub num_directions: u32,
pub num_steps: u32,
pub radius: f32,
pub thickness: f32,
pub falloff_range: f32,
pub sample_distribution_power: f32,
pub depth_mip_sampling_offset: f32,
pub thin_occluder_compensation: f32,
pub final_value_power: f32,
pub denoise_passes: u32,
pub half_resolution: bool,
}
impl GTAOPassDesc {
pub fn default(width: u32, height: u32) -> Self {
GTAOPassDesc {
width, height,
output_format: TextureFormat::R8Unorm,
output_ao: ResourceId(420),
input_depth: ResourceId(4),
input_normal: ResourceId(1),
num_directions: 2,
num_steps: 3,
radius: 0.5,
thickness: 1.0,
falloff_range: 0.615,
sample_distribution_power: 2.0,
depth_mip_sampling_offset: 3.3,
thin_occluder_compensation: 0.0,
final_value_power: 2.2,
denoise_passes: 1,
half_resolution: false,
}
}
pub fn compute_bent_normal_gtao(normal: Vec3, view_dir: Vec3, directions: &[(Vec3, Vec3)], weights: &[f32]) -> (Vec3, f32) {
let mut visibility = 0.0f32;
let mut bent_normal = Vec3::ZERO;
for ((dir_x, dir_y), &w) in directions.iter().zip(weights.iter()) {
let cos_h = dir_x.dot(normal).clamp(0.0, 1.0);
visibility += cos_h * w;
bent_normal += *dir_x * cos_h * w;
}
let bent = if bent_normal.length() > 1e-6 { bent_normal.normalize() } else { normal };
(bent, visibility)
}
pub fn bent_normal_visibility(bent_normal: Vec3, mean_visibility: f32, roughness: f32) -> f32 {
let t = 1.0 - mean_visibility;
let r = roughness.clamp(0.0, 1.0);
lerp(mean_visibility, 1.0 - t * (1.0 - r), r)
}
}
#[derive(Debug, Clone)]
pub struct DDGIPassDesc {
pub probe_grid_x: u32,
pub probe_grid_y: u32,
pub probe_grid_z: u32,
pub probe_spacing: f32,
pub probe_origin: Vec3,
pub rays_per_probe: u32,
pub irradiance_oct_size: u32, pub visibility_oct_size: u32,
pub output_irradiance: ResourceId,
pub output_visibility: ResourceId,
pub hysteresis: f32,
pub brightness_threshold: f32,
pub view_bias: f32,
pub normal_bias: f32,
}
impl DDGIPassDesc {
pub fn default() -> Self {
DDGIPassDesc {
probe_grid_x: 12, probe_grid_y: 6, probe_grid_z: 12,
probe_spacing: 3.0,
probe_origin: Vec3::new(-18.0, 0.0, -18.0),
rays_per_probe: 128,
irradiance_oct_size: 8,
visibility_oct_size: 16,
output_irradiance: ResourceId(500),
output_visibility: ResourceId(501),
hysteresis: 0.98,
brightness_threshold: 10.0,
view_bias: 0.3,
normal_bias: 0.08,
}
}
pub fn total_probes(&self) -> u32 { self.probe_grid_x * self.probe_grid_y * self.probe_grid_z }
pub fn irradiance_atlas_size(&self) -> (u32, u32) {
let probes_per_row = 64u32;
let rows = (self.total_probes() + probes_per_row - 1) / probes_per_row;
(probes_per_row * (self.irradiance_oct_size + 2), rows * (self.irradiance_oct_size + 2))
}
pub fn visibility_atlas_size(&self) -> (u32, u32) {
let probes_per_row = 32u32;
let rows = (self.total_probes() + probes_per_row - 1) / probes_per_row;
(probes_per_row * (self.visibility_oct_size + 2), rows * (self.visibility_oct_size + 2))
}
pub fn probe_world_pos(&self, ix: u32, iy: u32, iz: u32) -> Vec3 {
self.probe_origin + Vec3::new(
ix as f32 * self.probe_spacing,
iy as f32 * self.probe_spacing,
iz as f32 * self.probe_spacing,
)
}
pub fn probe_index_from_world(&self, world: Vec3) -> Option<(u32, u32, u32)> {
let local = (world - self.probe_origin) / self.probe_spacing;
let ix = local.x.round() as i32;
let iy = local.y.round() as i32;
let iz = local.z.round() as i32;
if ix >= 0 && iy >= 0 && iz >= 0 &&
ix < self.probe_grid_x as i32 && iy < self.probe_grid_y as i32 && iz < self.probe_grid_z as i32 {
Some((ix as u32, iy as u32, iz as u32))
} else {
None
}
}
pub fn trilinear_weights(local_blend: Vec3) -> [f32; 8] {
let (x, y, z) = (local_blend.x, local_blend.y, local_blend.z);
let (mx, my, mz) = (1.0 - x, 1.0 - y, 1.0 - z);
[
mx * my * mz,
x * my * mz,
mx * y * mz,
x * y * mz,
mx * my * z,
x * my * z,
mx * y * z,
x * y * z,
]
}
pub fn atlas_memory_bytes(&self) -> u64 {
let (iw, ih) = self.irradiance_atlas_size();
let (vw, vh) = self.visibility_atlas_size();
let irr = iw as u64 * ih as u64 * 8;
let vis = vw as u64 * vh as u64 * 4;
irr + vis
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RTPassKind { ReflectionDenoise, AO, GI, ShadowDenoise }
#[derive(Debug, Clone)]
pub struct RTPassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output: ResourceId,
pub kind: RTPassKind,
pub samples_per_pixel: u32,
pub max_bounces: u32,
pub russian_roulette_min_bounces: u32,
pub denoiser: RTDenoiser,
pub temporal_accumulation: bool,
pub reprojection_tolerance: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RTDenoiser { None, Temporal, SVGF, OIDN }
impl RTPassDesc {
pub fn rt_ao(width: u32, height: u32) -> Self {
RTPassDesc {
width, height,
output_format: TextureFormat::R16Float,
output: ResourceId(600),
kind: RTPassKind::AO,
samples_per_pixel: 1,
max_bounces: 1,
russian_roulette_min_bounces: 1,
denoiser: RTDenoiser::Temporal,
temporal_accumulation: true,
reprojection_tolerance: 0.001,
}
}
pub fn rt_reflections(width: u32, height: u32) -> Self {
RTPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output: ResourceId(601),
kind: RTPassKind::ReflectionDenoise,
samples_per_pixel: 1,
max_bounces: 2,
russian_roulette_min_bounces: 2,
denoiser: RTDenoiser::SVGF,
temporal_accumulation: true,
reprojection_tolerance: 0.005,
}
}
pub fn rt_gi(width: u32, height: u32) -> Self {
RTPassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output: ResourceId(602),
kind: RTPassKind::GI,
samples_per_pixel: 1,
max_bounces: 3,
russian_roulette_min_bounces: 2,
denoiser: RTDenoiser::SVGF,
temporal_accumulation: true,
reprojection_tolerance: 0.002,
}
}
pub fn dispatch_size(&self, tile: u32) -> (u32, u32) {
((self.width + tile - 1) / tile, (self.height + tile - 1) / tile)
}
}
pub struct ACESTransform;
impl ACESTransform {
pub fn linear_srgb_to_aces2065(c: Vec3) -> Vec3 {
let m = [
[0.4397010, 0.3829780, 0.1773350],
[0.0897923, 0.8134230, 0.0967616],
[0.0175440, 0.1115440, 0.8707040],
];
Vec3::new(
m[0][0]*c.x + m[0][1]*c.y + m[0][2]*c.z,
m[1][0]*c.x + m[1][1]*c.y + m[1][2]*c.z,
m[2][0]*c.x + m[2][1]*c.y + m[2][2]*c.z,
)
}
pub fn rrt_odt_srgb(c: Vec3) -> Vec3 {
let a = c * (c + Vec3::splat(0.0245786)) - Vec3::splat(0.000090537);
let b = c * (Vec3::splat(0.983729) * c + Vec3::splat(0.4329510)) + Vec3::splat(0.238081);
(a / b).clamp(Vec3::ZERO, Vec3::ONE)
}
pub fn full_pipeline(linear_srgb: Vec3, exposure: f32) -> Vec3 {
let aces = Self::linear_srgb_to_aces2065(linear_srgb * exposure);
let out = Self::rrt_odt_srgb(aces);
out
}
pub fn bake_lut(lut_size: u32) -> Vec<Vec3> {
let n = lut_size as usize;
let mut lut = Vec::with_capacity(n * n * n);
for bz in 0..n {
for gy in 0..n {
for rx in 0..n {
let r = rx as f32 / (n - 1) as f32;
let g = gy as f32 / (n - 1) as f32;
let b = bz as f32 / (n - 1) as f32;
let input = Vec3::new(r, g, b) * 4.0; let output = Self::rrt_odt_srgb(input);
lut.push(output);
}
}
}
lut
}
pub fn sample_lut(lut: &[Vec3], lut_size: u32, color: Vec3) -> Vec3 {
let n = lut_size as usize;
let c = color.clamp(Vec3::ZERO, Vec3::ONE) * (n - 1) as f32;
let x0 = (c.x as usize).min(n - 2);
let y0 = (c.y as usize).min(n - 2);
let z0 = (c.z as usize).min(n - 2);
let fx = c.x.fract();
let fy = c.y.fract();
let fz = c.z.fract();
let idx = |x: usize, y: usize, z: usize| z * n * n + y * n + x;
let c000 = lut.get(idx(x0, y0, z0)).cloned().unwrap_or(Vec3::ZERO);
let c100 = lut.get(idx(x0+1, y0, z0)).cloned().unwrap_or(Vec3::ZERO);
let c010 = lut.get(idx(x0, y0+1, z0)).cloned().unwrap_or(Vec3::ZERO);
let c110 = lut.get(idx(x0+1, y0+1, z0)).cloned().unwrap_or(Vec3::ZERO);
let c001 = lut.get(idx(x0, y0, z0+1)).cloned().unwrap_or(Vec3::ZERO);
let c101 = lut.get(idx(x0+1, y0, z0+1)).cloned().unwrap_or(Vec3::ZERO);
let c011 = lut.get(idx(x0, y0+1, z0+1)).cloned().unwrap_or(Vec3::ZERO);
let c111 = lut.get(idx(x0+1, y0+1, z0+1)).cloned().unwrap_or(Vec3::ZERO);
let c00 = lerp_vec3(c000, c100, fx);
let c01 = lerp_vec3(c001, c101, fx);
let c10 = lerp_vec3(c010, c110, fx);
let c11 = lerp_vec3(c011, c111, fx);
let c0 = lerp_vec3(c00, c10, fy);
let c1 = lerp_vec3(c01, c11, fy);
lerp_vec3(c0, c1, fz)
}
}
#[derive(Debug, Clone)]
pub struct GPUCullingPassDesc {
pub max_draw_calls: u32,
pub output_draw_indirect: ResourceId,
pub output_draw_count: ResourceId,
pub input_bounding_spheres: ResourceId,
pub input_draw_params: ResourceId,
pub use_hi_z_occlusion: bool,
pub use_frustum_culling: bool,
pub hi_z_mip_levels: u32,
pub tile_size: u32,
}
impl GPUCullingPassDesc {
pub fn default() -> Self {
GPUCullingPassDesc {
max_draw_calls: 65536,
output_draw_indirect: ResourceId(700),
output_draw_count: ResourceId(701),
input_bounding_spheres: ResourceId(702),
input_draw_params: ResourceId(703),
use_hi_z_occlusion: true,
use_frustum_culling: true,
hi_z_mip_levels: 10,
tile_size: 64,
}
}
pub fn dispatch_size(&self) -> u32 { (self.max_draw_calls + 63) / 64 }
pub fn draw_indirect_buffer_bytes(&self) -> u64 {
self.max_draw_calls as u64 * 20
}
pub fn bounding_sphere_buffer_bytes(&self) -> u64 {
self.max_draw_calls as u64 * 16
}
}
pub struct PassReorderer;
impl PassReorderer {
pub fn reorder_for_cache(sorted: &[PassId], pass_map: &HashMap<PassId, PassNode>) -> Vec<PassId> {
let mut remaining: Vec<PassId> = sorted.to_vec();
let mut result: Vec<PassId> = Vec::with_capacity(remaining.len());
let mut last_writes: HashSet<ResourceId> = HashSet::new();
while !remaining.is_empty() {
let best = remaining.iter().enumerate().max_by_key(|(_, pid)| {
let pass = match pass_map.get(*pid) { Some(p) => p, None => return 0 };
pass.reads.iter().filter(|r| last_writes.contains(*r)).count()
});
if let Some((idx, _)) = best {
let pid = remaining.remove(idx);
if let Some(pass) = pass_map.get(&pid) {
last_writes.clear();
for w in &pass.writes { last_writes.insert(*w); }
}
result.push(pid);
} else {
break;
}
}
result.extend(remaining);
result
}
}
pub fn bake_tonemapping_lut(width: u32, operator: ToneMappingOperator, exposure: f32, white_point: f32) -> Vec<Vec3> {
let op = ToneMappingPassDesc {
width, height: 1,
output_format: TextureFormat::RGBA8UnormSrgb,
output_sdr: ResourceId(0), input_hdr: ResourceId(0), input_bloom: ResourceId(0),
operator, exposure, gamma: 2.2, white_point, color_lut_enabled: false, color_lut_size: 0,
};
let n = width as usize;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let t = i as f32 / (n - 1) as f32;
let hdr = Vec3::splat(t * white_point);
let mapped = op.apply_operator(hdr);
let linear = op.gamma_correct(mapped);
out.push(linear);
}
out
}
#[derive(Debug, Clone)]
pub struct AdaptiveResolutionScaler {
pub target_frame_time_ms: f32,
pub min_scale: f32, pub max_scale: f32, pub current_scale: f32,
pub increase_threshold: f32, pub decrease_threshold: f32, pub increase_rate: f32,
pub decrease_rate: f32,
pub history: VecDeque<f32>,
pub history_length: usize,
}
impl AdaptiveResolutionScaler {
pub fn new(target_ms: f32) -> Self {
AdaptiveResolutionScaler {
target_frame_time_ms: target_ms,
min_scale: 0.5,
max_scale: 1.0,
current_scale: 1.0,
increase_threshold: 0.85,
decrease_threshold: 1.05,
increase_rate: 0.005,
decrease_rate: 0.02,
history: VecDeque::new(),
history_length: 10,
}
}
pub fn update(&mut self, gpu_time_ms: f32) {
if self.history.len() >= self.history_length { self.history.pop_front(); }
self.history.push_back(gpu_time_ms);
let avg: f32 = self.history.iter().sum::<f32>() / self.history.len() as f32;
if avg < self.target_frame_time_ms * self.increase_threshold {
self.current_scale = (self.current_scale + self.increase_rate).min(self.max_scale);
} else if avg > self.target_frame_time_ms * self.decrease_threshold {
self.current_scale = (self.current_scale - self.decrease_rate).max(self.min_scale);
}
}
pub fn scaled_resolution(&self, base_width: u32, base_height: u32) -> (u32, u32) {
let w = ((base_width as f32 * self.current_scale) as u32).max(1);
let h = ((base_height as f32 * self.current_scale) as u32).max(1);
(w & !1, h & !1)
}
pub fn upscale_needed(&self) -> bool { self.current_scale < 1.0 }
pub fn quality_level(&self) -> &'static str {
if self.current_scale >= 0.95 { "Ultra" }
else if self.current_scale >= 0.75 { "Quality" }
else if self.current_scale >= 0.60 { "Balanced" }
else { "Performance" }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpscalerKind { Bilinear, Lanczos, FSR1, FSR2, DLSS, XeSS, CAS }
#[derive(Debug, Clone)]
pub struct UpscalePassDesc {
pub input_width: u32,
pub input_height: u32,
pub output_width: u32,
pub output_height: u32,
pub input_color: ResourceId,
pub input_depth: ResourceId,
pub input_velocity: ResourceId,
pub output_color: ResourceId,
pub kind: UpscalerKind,
pub sharpness: f32,
pub mip_bias: f32,
}
impl UpscalePassDesc {
pub fn fsr1(iw: u32, ih: u32, ow: u32, oh: u32, input: ResourceId, output: ResourceId) -> Self {
UpscalePassDesc {
input_width: iw, input_height: ih, output_width: ow, output_height: oh,
input_color: input, input_depth: ResourceId(4), input_velocity: ResourceId(3),
output_color: output,
kind: UpscalerKind::FSR1,
sharpness: 0.8,
mip_bias: (iw as f32 / ow as f32).log2() - 1.0,
}
}
pub fn scale_factor(&self) -> f32 {
self.input_width as f32 / self.output_width as f32
}
pub fn easu_filter_sample(input_uv: Vec2, texel_size: Vec2, jitter: Vec2) -> [Vec2; 5] {
let center = input_uv;
[
center,
center + Vec2::new( texel_size.x, 0.0),
center + Vec2::new(-texel_size.x, 0.0),
center + Vec2::new(0.0, texel_size.y),
center + Vec2::new(0.0, -texel_size.y),
]
}
pub fn lanczos3_weight(x: f32) -> f32 {
let a = 3.0f32;
if x.abs() < 1e-6 { 1.0 }
else if x.abs() < a {
let px = std::f32::consts::PI * x;
let pa = std::f32::consts::PI * x / a;
a * px.sin() * pa.sin() / (px * px)
} else {
0.0
}
}
pub fn lanczos3_reconstruct(center: Vec3, samples: &[(Vec3, Vec2)], output_uv: Vec2) -> Vec3 {
let mut sum = Vec3::ZERO;
let mut weight_sum = 0.0f32;
for (color, input_uv) in samples {
let dx = (output_uv.x - input_uv.x);
let dy = (output_uv.y - input_uv.y);
let w = Self::lanczos3_weight(dx) * Self::lanczos3_weight(dy);
sum += *color * w;
weight_sum += w;
}
if weight_sum.abs() < 1e-6 { center } else { sum / weight_sum }
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_vxgi_memory() {
let vxgi = VXGIPassDesc::default();
let mem = vxgi.grid_memory_bytes();
assert!(mem > 0);
let mips = vxgi.mip_levels();
assert_eq!(mips, 9); }
#[test]
fn test_ddgi_probe_positions() {
let ddgi = DDGIPassDesc::default();
let p = ddgi.probe_world_pos(0, 0, 0);
assert_eq!(p, ddgi.probe_origin);
let p2 = ddgi.probe_world_pos(1, 0, 0);
assert!((p2.x - p.x - ddgi.probe_spacing).abs() < 1e-5);
}
#[test]
fn test_ddgi_trilinear_weights() {
let weights = DDGIPassDesc::trilinear_weights(Vec3::splat(0.5));
let sum: f32 = weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-4);
}
#[test]
fn test_sss_kernel_normalization() {
let sss = SSSPassDesc::default(1920, 1080);
let kernel = sss.separable_kernel();
assert!(!kernel.is_empty());
let sum_w: f32 = kernel.iter().map(|k| k.y).sum();
assert!((sum_w - 1.0).abs() < 1e-4, "kernel weights should sum to 1, got {}", sum_w);
}
#[test]
fn test_sss_preintegrated_lut() {
let v = SSSPassDesc::preintegrated_lut_value(1.0, 0.0);
assert!(v.x >= 0.0 && v.x <= 1.0);
assert!(v.y >= 0.0 && v.y <= 1.0);
}
#[test]
fn test_burley_diffusion_profile() {
let v = SSSPassDesc::burley_diffusion_profile(0.1, 1.0);
assert!(v > 0.0);
let v2 = SSSPassDesc::burley_diffusion_profile(2.0, 1.0);
assert!(v > v2, "profile should fall off with distance");
}
#[test]
fn test_aces_lut_baking_small() {
let lut = ACESTransform::bake_lut(4);
assert_eq!(lut.len(), 4 * 4 * 4);
for c in &lut {
assert!(c.x >= 0.0 && c.x <= 1.0);
}
}
#[test]
fn test_aces_lut_sampling() {
let lut = ACESTransform::bake_lut(32);
let c = ACESTransform::sample_lut(&lut, 32, Vec3::splat(0.5));
assert!(c.length() >= 0.0);
}
#[test]
fn test_adaptive_resolution_scale_up() {
let mut scaler = AdaptiveResolutionScaler::new(16.67);
scaler.current_scale = 0.7;
for _ in 0..20 { scaler.update(10.0); } assert!(scaler.current_scale > 0.7);
}
#[test]
fn test_adaptive_resolution_scale_down() {
let mut scaler = AdaptiveResolutionScaler::new(16.67);
for _ in 0..20 { scaler.update(25.0); } assert!(scaler.current_scale < 1.0);
}
#[test]
fn test_lanczos3_weight() {
let w0 = UpscalePassDesc::lanczos3_weight(0.0);
assert!((w0 - 1.0).abs() < 1e-4);
let w_out = UpscalePassDesc::lanczos3_weight(3.5);
assert_eq!(w_out, 0.0);
}
#[test]
fn test_tonemapping_lut_bake() {
let lut = bake_tonemapping_lut(256, ToneMappingOperator::ACES, 1.0, 4.0);
assert_eq!(lut.len(), 256);
assert!(lut.iter().all(|c| c.x >= 0.0 && c.x <= 1.001));
}
#[test]
fn test_gpu_culling_pass_sizes() {
let cull = GPUCullingPassDesc::default();
assert!(cull.draw_indirect_buffer_bytes() > 0);
assert!(cull.bounding_sphere_buffer_bytes() > 0);
}
#[test]
fn test_pass_reorder_for_cache() {
let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let sorted: Vec<PassId> = editor.passes.keys().cloned().collect();
let reordered = PassReorderer::reorder_for_cache(&sorted, &editor.passes);
assert_eq!(reordered.len(), sorted.len());
}
#[test]
fn test_hi_z_trace() {
let result = HiZTracer::trace(Vec2::new(0.5, 0.5), Vec2::new(0.01, 0.0), 0.5, 64, 8);
let _ = result;
}
#[test]
fn test_editor_history() {
let mut hist = EditorHistory::new(32);
assert!(!hist.can_undo());
hist.push(EditorAction::AddPass(PassId(0), "Test".to_owned()));
assert!(hist.can_undo());
let act = hist.pop_undo();
assert!(act.is_some());
assert!(!hist.can_undo());
}
#[test]
fn test_pass_group_bounds() {
let mut group = PassGroup::new(0, "Deferred", vec![PassId(0), PassId(1)], Vec4::ONE);
let mut positions = HashMap::new();
positions.insert(PassId(0), Vec2::new(10.0, 20.0));
positions.insert(PassId(1), Vec2::new(300.0, 200.0));
let mut sizes = HashMap::new();
sizes.insert(PassId(0), Vec2::new(200.0, 80.0));
sizes.insert(PassId(1), Vec2::new(200.0, 80.0));
group.compute_bounds(&positions, &sizes);
assert!(group.contains_point(Vec2::new(100.0, 100.0)));
assert!(!group.contains_point(Vec2::new(-100.0, -100.0)));
}
#[test]
fn test_graph_diff_empty() {
let ed = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let diffs = diff_render_graphs(&ed, &ed);
let adds_removes: Vec<_> = diffs.iter().filter(|d| matches!(d, GraphDiff::PassAdded(_, _) | GraphDiff::PassRemoved(_, _))).collect();
assert!(adds_removes.is_empty());
}
#[test]
fn test_hot_reload_no_change() {
let ed = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let ed2 = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
let mut mgr = HotReloadManager::new(ed);
mgr.stage_reload(ed2);
let changed = mgr.apply_reload_if_pending(1);
assert!(!changed); }
#[test]
fn test_frame_debugger() {
let mut dbg = FrameDebugger::new(8);
dbg.begin_capture(0, 0.0);
dbg.record_pass_timing(PassId(0), 2.5);
dbg.record_pass_timing(PassId(1), 4.0);
dbg.record_transition(PassId(0), ResourceId(0), ImageLayout::Undefined, ImageLayout::ColorAttachmentOptimal);
dbg.end_capture();
let cap = dbg.latest().unwrap();
assert!((cap.total_gpu_ms() - 6.5).abs() < 1e-4);
assert_eq!(cap.resource_transitions.len(), 1);
}
#[test]
fn test_critical_path() {
let mut timings: HashMap<PassId, f64> = HashMap::new();
timings.insert(PassId(0), 1.0);
timings.insert(PassId(1), 4.0);
timings.insert(PassId(2), 2.0);
let mut edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
edges.insert(PassId(0), vec![PassId(1)]);
edges.insert(PassId(1), vec![PassId(2)]);
edges.insert(PassId(2), vec![]);
let sorted = vec![PassId(0), PassId(1), PassId(2)];
let (path, total) = CriticalPathAnalyzer::find_critical_path(&sorted, &timings, &edges);
assert!((total - 7.0).abs() < 1e-4, "total should be 7ms, got {}", total);
}
#[test]
fn test_bandwidth_profiler() {
let mut profiler = BandwidthProfiler::new();
profiler.record(PassId(0), ResourceId(0), true, 8 * 1920 * 1080, AccessFlags::COLOR_ATTACHMENT_WRITE, ImageLayout::ColorAttachmentOptimal);
profiler.record(PassId(1), ResourceId(0), false, 8 * 1920 * 1080, AccessFlags::SHADER_READ, ImageLayout::ShaderReadOnlyOptimal);
profiler.compute_totals();
let total = profiler.total_bandwidth_mb();
assert!(total > 0.0);
let top = profiler.top_bandwidth_resources(3);
assert!(!top.is_empty());
}
}
pub struct OcclusionCuller {
pub hi_z_width: u32,
pub hi_z_height: u32,
pub mip_levels: u32,
}
impl OcclusionCuller {
pub fn new(width: u32, height: u32) -> Self {
let mips = compute_mip_count(width, height);
OcclusionCuller { hi_z_width: width, hi_z_height: height, mip_levels: mips }
}
pub fn is_occluded_hiz(
&self,
aabb_min_uv: Vec2,
aabb_max_uv: Vec2,
nearest_depth: f32,
hi_z_mips: &[Vec<f32>], ) -> bool {
let span = aabb_max_uv - aabb_min_uv;
let max_span = span.x.max(span.y);
let mip = ((max_span * self.hi_z_width.max(self.hi_z_height) as f32).log2() as u32).min(self.mip_levels - 1);
let mip_w = (self.hi_z_width >> mip).max(1) as f32;
let mip_h = (self.hi_z_height >> mip).max(1) as f32;
let uvs = [
aabb_min_uv,
Vec2::new(aabb_max_uv.x, aabb_min_uv.y),
Vec2::new(aabb_min_uv.x, aabb_max_uv.y),
aabb_max_uv,
];
let mip_data = match hi_z_mips.get(mip as usize) { Some(d) => d, None => return false };
let mut max_hi_z_depth = 0.0f32;
for uv in &uvs {
let ix = (uv.x * mip_w) as usize;
let iy = (uv.y * mip_h) as usize;
let idx = iy * mip_w as usize + ix;
let d = mip_data.get(idx).cloned().unwrap_or(1.0);
max_hi_z_depth = max_hi_z_depth.max(d);
}
nearest_depth > max_hi_z_depth
}
pub fn build_hi_z_pyramid(depth_buffer: &[f32], width: u32, height: u32) -> Vec<Vec<f32>> {
let mips_count = compute_mip_count(width, height) as usize;
let mut mips: Vec<Vec<f32>> = Vec::with_capacity(mips_count);
mips.push(depth_buffer.to_vec());
let mut prev_w = width;
let mut prev_h = height;
for _ in 1..mips_count {
let w = (prev_w / 2).max(1);
let h = (prev_h / 2).max(1);
let mut mip_data = vec![0.0f32; (w * h) as usize];
let prev_data = mips.last().unwrap();
for y in 0..h {
for x in 0..w {
let px = x * 2;
let py = y * 2;
let d00 = *prev_data.get((py * prev_w + px) as usize).unwrap_or(&1.0);
let d10 = *prev_data.get((py * prev_w + (px+1).min(prev_w-1)) as usize).unwrap_or(&1.0);
let d01 = *prev_data.get(((py+1).min(prev_h-1) * prev_w + px) as usize).unwrap_or(&1.0);
let d11 = *prev_data.get(((py+1).min(prev_h-1) * prev_w + (px+1).min(prev_w-1)) as usize).unwrap_or(&1.0);
mip_data[(y * w + x) as usize] = d00.max(d10).max(d01).max(d11);
}
}
mips.push(mip_data);
prev_w = w;
prev_h = h;
}
mips
}
}
#[derive(Debug, Clone)]
pub struct AutoExposurePassDesc {
pub output_average_luminance: ResourceId,
pub input_hdr: ResourceId,
pub min_log_luminance: f32, pub max_log_luminance: f32, pub adaptation_speed_up: f32,
pub adaptation_speed_down: f32,
pub histogram_bins: u32,
pub metered_area: Vec4, pub eye_adaptation_type: EyeAdaptation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EyeAdaptation { Histogram, AverageLuminance }
impl AutoExposurePassDesc {
pub fn default() -> Self {
AutoExposurePassDesc {
output_average_luminance: ResourceId(800),
input_hdr: ResourceId(10),
min_log_luminance: -8.0,
max_log_luminance: 8.0,
adaptation_speed_up: 3.0,
adaptation_speed_down: 1.0,
histogram_bins: 256,
metered_area: Vec4::new(0.1, 0.1, 0.8, 0.8),
eye_adaptation_type: EyeAdaptation::Histogram,
}
}
pub fn luminance_to_bin(&self, lum: f32) -> u32 {
let log_lum = lum.max(1e-5).ln() / std::f32::consts::LN_2; let normalized = (log_lum - self.min_log_luminance) / (self.max_log_luminance - self.min_log_luminance);
(normalized * self.histogram_bins as f32) as u32
}
pub fn ev100_from_average_luminance(avg_lum: f32) -> f32 {
(avg_lum * 100.0 / 12.5).log2()
}
pub fn adapt_exposure(&self, current_ev: f32, target_ev: f32, delta_time: f32) -> f32 {
let speed = if target_ev > current_ev { self.adaptation_speed_up } else { self.adaptation_speed_down };
let factor = 1.0 - (-speed * delta_time).exp();
current_ev + (target_ev - current_ev) * factor
}
pub fn percentile_from_histogram(histogram: &[u32], percentile: f32) -> f32 {
let total: u64 = histogram.iter().map(|&c| c as u64).sum();
if total == 0 { return 0.0; }
let target_count = (total as f32 * percentile) as u64;
let mut accum = 0u64;
for (i, &count) in histogram.iter().enumerate() {
accum += count as u64;
if accum >= target_count {
return i as f32 / histogram.len() as f32;
}
}
1.0
}
}
#[derive(Debug, Clone)]
pub struct LensFlarePassDesc {
pub width: u32,
pub height: u32,
pub output_format: TextureFormat,
pub output_flare: ResourceId,
pub input_hdr: ResourceId,
pub threshold: f32,
pub intensity: f32,
pub ghost_count: u32,
pub ghost_dispersal: f32,
pub ghost_threshold: f32,
pub halo_width: f32,
pub halo_intensity: f32,
pub distortion: f32,
pub use_lens_dirt: bool,
pub use_star_burst: bool,
pub star_burst_samples: u32,
}
impl LensFlarePassDesc {
pub fn default(width: u32, height: u32) -> Self {
LensFlarePassDesc {
width, height,
output_format: TextureFormat::RGBA16Float,
output_flare: ResourceId(900),
input_hdr: ResourceId(10),
threshold: 10.0,
intensity: 0.5,
ghost_count: 8,
ghost_dispersal: 0.35,
ghost_threshold: 50.0,
halo_width: 0.5,
halo_intensity: 0.8,
distortion: 5.0,
use_lens_dirt: true,
use_star_burst: true,
star_burst_samples: 6,
}
}
pub fn ghost_position(flare_uv: Vec2, ghost_index: u32, dispersal: f32) -> Vec2 {
let lens_center = Vec2::splat(0.5);
let flare_dir = flare_uv - lens_center;
let offset = flare_dir * ghost_index as f32 * dispersal;
lens_center + offset
}
pub fn star_burst_direction(sample: u32, total: u32) -> Vec2 {
let angle = (sample as f32 / total as f32) * std::f32::consts::TAU;
Vec2::new(angle.cos(), angle.sin())
}
pub fn chromatic_distortion_offset(uv: Vec2, channel: u32, strength: f32) -> Vec2 {
let center = Vec2::splat(0.5);
let d = uv - center;
let scale = 1.0 + strength * 0.01 * (channel as f32 - 1.0);
center + d * scale
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PinKind { Input, Output }
#[derive(Debug, Clone)]
pub struct NodePin {
pub pass_id: PassId,
pub resource_id: ResourceId,
pub kind: PinKind,
pub index: u32,
pub label: String,
pub format: TextureFormat,
}
impl NodePin {
pub fn input(pass_id: PassId, resource_id: ResourceId, index: u32, label: &str, format: TextureFormat) -> Self {
NodePin { pass_id, resource_id, kind: PinKind::Input, index, label: label.to_owned(), format }
}
pub fn output(pass_id: PassId, resource_id: ResourceId, index: u32, label: &str, format: TextureFormat) -> Self {
NodePin { pass_id, resource_id, kind: PinKind::Output, index, label: label.to_owned(), format }
}
pub fn is_compatible_with(&self, other: &NodePin) -> bool {
self.kind != other.kind && formats_compatible(self.format, other.format)
}
}
#[derive(Debug, Clone)]
pub struct NodeConnection {
pub src: NodePin,
pub dst: NodePin,
}
impl NodeConnection {
pub fn new(src: NodePin, dst: NodePin) -> Option<Self> {
if src.is_compatible_with(&dst) { Some(NodeConnection { src, dst }) } else { None }
}
}
pub fn encode_rgbe(hdr: Vec3) -> [u8; 4] {
let max_c = hdr.x.max(hdr.y).max(hdr.z);
if max_c < 1e-32 {
return [0, 0, 0, 0];
}
let exp = max_c.log2().ceil() as i32 + 128;
let scale = 256.0 / (2.0f32.powi(exp - 128));
[
(hdr.x * scale) as u8,
(hdr.y * scale) as u8,
(hdr.z * scale) as u8,
exp.clamp(0, 255) as u8,
]
}
pub fn decode_rgbe(rgbe: [u8; 4]) -> Vec3 {
if rgbe[3] == 0 { return Vec3::ZERO; }
let exp = rgbe[3] as i32 - 128;
let scale = 2.0f32.powi(exp) / 256.0;
Vec3::new(rgbe[0] as f32 * scale, rgbe[1] as f32 * scale, rgbe[2] as f32 * scale)
}
pub fn encode_rgb9e5(hdr: Vec3) -> u32 {
const N: i32 = 9;
const B: i32 = 15;
const E_MAX: i32 = 31;
let max_c = hdr.x.max(hdr.y).max(hdr.z).max(0.0);
let shared_exp_f = (max_c / (1 << (N-1)) as f32 * (1 << B) as f32).log2().ceil() as i32;
let exp = shared_exp_f.clamp(-B, E_MAX - N);
let scale = 1.0 / 2.0f32.powi(exp - N + 1);
let r = (hdr.x * scale).round() as u32 & ((1 << N) - 1);
let g = (hdr.y * scale).round() as u32 & ((1 << N) - 1);
let b = (hdr.z * scale).round() as u32 & ((1 << N) - 1);
let e = (exp + B + 1).clamp(0, 31) as u32;
(e << 27) | (b << 18) | (g << 9) | r
}
pub fn sphere_screen_size_pixels(center_vs: Vec3, radius: f32, proj: Mat4, screen_width: u32) -> f32 {
let d = center_vs.length();
if d < radius { return screen_width as f32; }
let proj_scale = proj.col(0).x; (proj_scale * radius / (d - radius)) * screen_width as f32 * 0.5
}
pub fn linear_depth_to_ndc(linear: f32, near: f32, far: f32) -> f32 {
let a = -(far + near) / (far - near);
let b = -2.0 * far * near / (far - near);
a + b / linear
}
pub fn compute_texture_lod(ddx: Vec2, ddy: Vec2) -> f32 {
let len_x = ddx.length_squared();
let len_y = ddy.length_squared();
0.5 * len_x.max(len_y).log2()
}
pub fn srgb_eotf(encoded: f32) -> f32 {
if encoded <= 0.04045 { encoded / 12.92 } else { ((encoded + 0.055) / 1.055).powf(2.4) }
}
pub fn srgb_oetf(linear: f32) -> f32 {
if linear <= 0.0031308 { linear * 12.92 } else { 1.055 * linear.powf(1.0 / 2.4) - 0.055 }
}
pub fn rec709_to_xyz(c: Vec3) -> Vec3 {
Vec3::new(
0.4124564 * c.x + 0.3575761 * c.y + 0.1804375 * c.z,
0.2126729 * c.x + 0.7151522 * c.y + 0.0721750 * c.z,
0.0193339 * c.x + 0.1191920 * c.y + 0.9503041 * c.z,
)
}
pub fn xyz_to_rec709(c: Vec3) -> Vec3 {
Vec3::new(
3.2404542 * c.x - 1.5371385 * c.y - 0.4985314 * c.z,
-0.9692660 * c.x + 1.8760108 * c.y + 0.0415560 * c.z,
0.0556434 * c.x - 0.2040259 * c.y + 1.0572252 * c.z,
)
}
pub fn xyz_to_aces_ap0(c: Vec3) -> Vec3 {
Vec3::new(
1.0498110175 * c.x + 0.0000000000 * c.y - 0.0000974845 * c.z,
-0.4959030231 * c.x + 1.3733130458 * c.y + 0.0982400361 * c.z,
0.0000000000 * c.x + 0.0000000000 * c.y + 0.9912520182 * c.z,
)
}
trait Vec3Ext { fn powf(self, exp: f32) -> Vec3; fn sqrt(self) -> Vec3; }
impl Vec3Ext for Vec3 {
fn powf(self, exp: f32) -> Vec3 { Vec3::new(self.x.powf(exp), self.y.powf(exp), self.z.powf(exp)) }
fn sqrt(self) -> Vec3 { Vec3::new(self.x.sqrt(), self.y.sqrt(), self.z.sqrt()) }
}
#[allow(dead_code)]
fn vec4_div(v: Vec4, rhs: f32) -> Vec4 { Vec4::new(v.x / rhs, v.y / rhs, v.z / rhs, v.w / rhs) }
#[cfg(test)]
mod util_tests {
use super::*;
#[test]
fn test_rgbe_round_trip() {
let hdr = Vec3::new(1.5, 2.3, 0.7);
let enc = encode_rgbe(hdr);
let dec = decode_rgbe(enc);
let err = (hdr - dec).length();
assert!(err < 0.05, "RGBE round-trip error too large: {}", err);
}
#[test]
fn test_rgbe_black() {
let enc = encode_rgbe(Vec3::ZERO);
assert_eq!(enc, [0, 0, 0, 0]);
let dec = decode_rgbe(enc);
assert_eq!(dec, Vec3::ZERO);
}
#[test]
fn test_srgb_eotf_inverse() {
let x = 0.5f32;
let linear = srgb_eotf(x);
let back = srgb_oetf(linear);
assert!((back - x).abs() < 1e-4);
}
#[test]
fn test_linear_depth_to_ndc() {
let ndc = linear_depth_to_ndc(1.0, 0.1, 100.0);
assert!(ndc >= -1.0 && ndc <= 1.0);
}
#[test]
fn test_sphere_screen_size() {
let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 100.0);
let size = sphere_screen_size_pixels(Vec3::new(0.0, 0.0, -10.0), 1.0, proj, 1920);
assert!(size > 0.0);
}
#[test]
fn test_auto_exposure_adapt() {
let ae = AutoExposurePassDesc::default();
let ev = ae.adapt_exposure(0.0, 5.0, 0.016);
assert!(ev > 0.0 && ev < 5.0);
}
#[test]
fn test_lens_flare_ghost_positions() {
let pos = LensFlarePassDesc::ghost_position(Vec2::new(0.8, 0.5), 1, 0.35);
assert!(pos.x >= 0.0 && pos.x <= 1.0 || pos.x < 0.0 || pos.x > 1.0);
}
#[test]
fn test_hi_z_pyramid_build() {
let depth: Vec<f32> = vec![0.5; 64 * 64];
let pyramid = OcclusionCuller::build_hi_z_pyramid(&depth, 64, 64);
assert!(pyramid.len() > 1);
assert_eq!(pyramid[0].len(), 64 * 64);
assert_eq!(pyramid[1].len(), 32 * 32);
}
#[test]
fn test_node_pin_compatibility() {
let p1 = NodePin::output(PassId(0), ResourceId(0), 0, "HDR", TextureFormat::RGBA16Float);
let p2 = NodePin::input(PassId(1), ResourceId(0), 0, "HDR", TextureFormat::RGBA16Float);
assert!(p1.is_compatible_with(&p2));
let p3 = NodePin::input(PassId(1), ResourceId(1), 0, "Depth", TextureFormat::Depth32Float);
assert!(!p1.is_compatible_with(&p3));
}
#[test]
fn test_ev100_from_luminance() {
let ev = AutoExposurePassDesc::ev100_from_average_luminance(1.0);
assert!((ev - 3.0).abs() < 0.1, "ev should be ~3 for 1 cd/m^2 avg luminance");
}
#[test]
fn test_rec709_xyz_round_trip() {
let c = Vec3::new(0.2, 0.5, 0.8);
let xyz = rec709_to_xyz(c);
let back = xyz_to_rec709(xyz);
assert!((c - back).length() < 1e-4);
}
#[test]
fn test_compute_texture_lod() {
let lod = compute_texture_lod(Vec2::new(0.01, 0.0), Vec2::new(0.0, 0.01));
assert!(lod < 0.0 || lod >= 0.0); }
}