Skip to main content

proof_engine/editor/
render_graph_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7//  TEXTURE FORMAT ENUM — 50+ formats with metadata
8// ============================================================
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum TextureFormat {
12    // 8-bit unorm
13    R8Unorm,
14    RG8Unorm,
15    RGBA8Unorm,
16    RGBA8UnormSrgb,
17    BGRA8Unorm,
18    BGRA8UnormSrgb,
19    // 8-bit snorm
20    R8Snorm,
21    RG8Snorm,
22    RGBA8Snorm,
23    // 8-bit uint/sint
24    R8Uint,
25    RG8Uint,
26    RGBA8Uint,
27    R8Sint,
28    RG8Sint,
29    RGBA8Sint,
30    // 16-bit unorm
31    R16Unorm,
32    RG16Unorm,
33    RGBA16Unorm,
34    // 16-bit float
35    R16Float,
36    RG16Float,
37    RGBA16Float,
38    // 16-bit uint/sint
39    R16Uint,
40    RG16Uint,
41    RGBA16Uint,
42    R16Sint,
43    // 32-bit float
44    R32Float,
45    RG32Float,
46    RGB32Float,
47    RGBA32Float,
48    // 32-bit uint/sint
49    R32Uint,
50    RG32Uint,
51    RGBA32Uint,
52    R32Sint,
53    // 10-bit packed
54    RGB10A2Unorm,
55    RG11B10Float,
56    RGB9E5Float,
57    // Depth/Stencil
58    Depth16Unorm,
59    Depth24Unorm,
60    Depth32Float,
61    Depth24UnormStencil8,
62    Depth32FloatStencil8,
63    Stencil8,
64    // BC compressed
65    BC1RgbUnorm,
66    BC1RgbSrgb,
67    BC1RgbaUnorm,
68    BC1RgbaSrgb,
69    BC2Unorm,
70    BC2Srgb,
71    BC3Unorm,
72    BC3Srgb,
73    BC4Unorm,
74    BC4Snorm,
75    BC5Unorm,
76    BC5Snorm,
77    BC6HUfloat,
78    BC6HSfloat,
79    BC7Unorm,
80    BC7Srgb,
81    // ETC2/EAC
82    Etc2Rgb8Unorm,
83    Etc2Rgb8Srgb,
84    Etc2Rgb8A1Unorm,
85    Etc2Rgba8Unorm,
86    EacR11Unorm,
87    EacRG11Unorm,
88    // ASTC
89    Astc4x4Unorm,
90    Astc4x4Srgb,
91    Astc8x8Unorm,
92    Astc8x8Srgb,
93    Astc12x12Unorm,
94}
95
96#[derive(Debug, Clone, Copy)]
97pub struct FormatInfo {
98    pub bytes_per_block: u32,
99    pub block_width: u32,
100    pub block_height: u32,
101    pub components: u32,
102    pub is_depth: bool,
103    pub is_stencil: bool,
104    pub is_compressed: bool,
105    pub is_srgb: bool,
106    pub is_float: bool,
107    pub is_uint: bool,
108    pub is_sint: bool,
109}
110
111impl FormatInfo {
112    pub fn bytes_per_pixel(&self) -> f32 {
113        (self.bytes_per_block as f32) / (self.block_width * self.block_height) as f32
114    }
115}
116
117pub fn format_info(fmt: TextureFormat) -> FormatInfo {
118    match fmt {
119        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 },
120        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 },
121        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 },
122        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 },
123        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 },
124        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 },
125        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 },
126        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 },
127        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 },
128        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 },
129        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 },
130        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 },
131        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  },
132        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  },
133        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  },
134        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 },
135        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 },
136        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 },
137        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 },
138        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 },
139        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 },
140        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 },
141        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 },
142        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 },
143        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  },
144        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 },
145        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 },
146        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 },
147        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 },
148        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 },
149        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 },
150        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 },
151        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  },
152        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 },
153        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 },
154        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 },
155        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 },
156        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 },
157        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 },
158        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 },
159        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 },
160        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 },
161        // BC compressed formats (4x4 blocks)
162        TextureFormat::BC1RgbUnorm  | TextureFormat::BC1RgbSrgb  |
163        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 },
164        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 },
165        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 },
166        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 },
167        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 },
168        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 },
169        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 },
170        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 },
171        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 },
172        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 },
173        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 },
174        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 },
175        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 },
176        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 },
177        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 },
178    }
179}
180
181pub fn texture_size_bytes(fmt: TextureFormat, width: u32, height: u32, mip_levels: u32) -> u64 {
182    let info = format_info(fmt);
183    let mut total: u64 = 0;
184    let mut w = width;
185    let mut h = height;
186    for _ in 0..mip_levels {
187        let bw = (w + info.block_width - 1) / info.block_width;
188        let bh = (h + info.block_height - 1) / info.block_height;
189        total += (bw * bh * info.bytes_per_block) as u64;
190        w = (w / 2).max(1);
191        h = (h / 2).max(1);
192    }
193    total
194}
195
196// ============================================================
197//  VULKAN-STYLE ENUMS
198// ============================================================
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
201pub enum ImageLayout {
202    Undefined,
203    General,
204    ColorAttachmentOptimal,
205    DepthStencilAttachmentOptimal,
206    DepthStencilReadOnlyOptimal,
207    ShaderReadOnlyOptimal,
208    TransferSrcOptimal,
209    TransferDstOptimal,
210    Preinitialized,
211    DepthReadOnlyStencilAttachmentOptimal,
212    DepthAttachmentStencilReadOnlyOptimal,
213    DepthAttachmentOptimal,
214    DepthReadOnlyOptimal,
215    StencilAttachmentOptimal,
216    StencilReadOnlyOptimal,
217    PresentSrc,
218    SharedPresent,
219    ShadingRateOptimal,
220    FragmentDensityMapOptimal,
221    VideoDecodeSrc,
222    VideoDecodeDst,
223    AttachmentOptimal,
224    ReadOnlyOptimal,
225}
226
227// Manual bitflags macro since we cannot use the bitflags crate
228macro_rules! bitflags_manual {
229    (
230        #[derive($($derive:ident),*)]
231        pub struct $name:ident: $ty:ty {
232            $(const $flag:ident = $val:expr;)*
233        }
234    ) => {
235        #[derive($($derive),*)]
236        pub struct $name(pub $ty);
237        impl $name {
238            $(pub const $flag: $name = $name($val);)*
239            pub fn contains(self, other: $name) -> bool {
240                (self.0 & other.0) == other.0
241            }
242            pub fn intersects(self, other: $name) -> bool {
243                (self.0 & other.0) != 0
244            }
245            pub fn is_empty(self) -> bool { self.0 == 0 }
246            pub fn bits(self) -> $ty { self.0 }
247        }
248        impl std::ops::BitOr for $name {
249            type Output = $name;
250            fn bitor(self, rhs: $name) -> $name { $name(self.0 | rhs.0) }
251        }
252        impl std::ops::BitAnd for $name {
253            type Output = $name;
254            fn bitand(self, rhs: $name) -> $name { $name(self.0 & rhs.0) }
255        }
256        impl std::ops::BitOrAssign for $name {
257            fn bitor_assign(&mut self, rhs: $name) { self.0 |= rhs.0; }
258        }
259        impl std::ops::Not for $name {
260            type Output = $name;
261            fn not(self) -> $name { $name(!self.0) }
262        }
263    }
264}
265
266bitflags_manual! {
267    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
268    pub struct AccessFlags: u64 {
269        const NONE                              = 0;
270        const INDIRECT_COMMAND_READ             = 1 << 0;
271        const INDEX_READ                        = 1 << 1;
272        const VERTEX_ATTRIBUTE_READ             = 1 << 2;
273        const UNIFORM_READ                      = 1 << 3;
274        const INPUT_ATTACHMENT_READ             = 1 << 4;
275        const SHADER_READ                       = 1 << 5;
276        const SHADER_WRITE                      = 1 << 6;
277        const COLOR_ATTACHMENT_READ             = 1 << 7;
278        const COLOR_ATTACHMENT_WRITE            = 1 << 8;
279        const DEPTH_STENCIL_ATTACHMENT_READ     = 1 << 9;
280        const DEPTH_STENCIL_ATTACHMENT_WRITE    = 1 << 10;
281        const TRANSFER_READ                     = 1 << 11;
282        const TRANSFER_WRITE                    = 1 << 12;
283        const HOST_READ                         = 1 << 13;
284        const HOST_WRITE                        = 1 << 14;
285        const MEMORY_READ                       = 1 << 15;
286        const MEMORY_WRITE                      = 1 << 16;
287        const ACCELERATION_STRUCTURE_READ       = 1 << 17;
288        const ACCELERATION_STRUCTURE_WRITE      = 1 << 18;
289    }
290}
291
292bitflags_manual! {
293    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
294    pub struct PipelineStageFlags: u64 {
295        const NONE                              = 0;
296        const TOP_OF_PIPE                       = 1 << 0;
297        const DRAW_INDIRECT                     = 1 << 1;
298        const VERTEX_INPUT                      = 1 << 2;
299        const VERTEX_SHADER                     = 1 << 3;
300        const TESSELLATION_CONTROL_SHADER       = 1 << 4;
301        const TESSELLATION_EVALUATION_SHADER    = 1 << 5;
302        const GEOMETRY_SHADER                   = 1 << 6;
303        const FRAGMENT_SHADER                   = 1 << 7;
304        const EARLY_FRAGMENT_TESTS              = 1 << 8;
305        const LATE_FRAGMENT_TESTS               = 1 << 9;
306        const COLOR_ATTACHMENT_OUTPUT           = 1 << 10;
307        const COMPUTE_SHADER                    = 1 << 11;
308        const TRANSFER                          = 1 << 12;
309        const BOTTOM_OF_PIPE                    = 1 << 13;
310        const HOST                              = 1 << 14;
311        const ALL_GRAPHICS                      = 1 << 15;
312        const ALL_COMMANDS                      = 1 << 16;
313        const TASK_SHADER_NV                    = 1 << 17;
314        const MESH_SHADER_NV                    = 1 << 18;
315        const RAY_TRACING_SHADER                = 1 << 19;
316        const ACCELERATION_STRUCTURE_BUILD      = 1 << 20;
317    }
318}
319
320// ============================================================
321//  ATTACHMENT DESCRIPTIONS
322// ============================================================
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum LoadOp {
326    Load,
327    Clear,
328    DontCare,
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum StoreOp {
333    Store,
334    DontCare,
335    None,
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum SampleCount {
340    S1  = 1,
341    S2  = 2,
342    S4  = 4,
343    S8  = 8,
344    S16 = 16,
345    S32 = 32,
346    S64 = 64,
347}
348
349impl SampleCount {
350    pub fn count(self) -> u32 { self as u32 }
351}
352
353#[derive(Debug, Clone)]
354pub struct AttachmentDescription {
355    pub format: TextureFormat,
356    pub samples: SampleCount,
357    pub load_op: LoadOp,
358    pub store_op: StoreOp,
359    pub stencil_load_op: LoadOp,
360    pub stencil_store_op: StoreOp,
361    pub initial_layout: ImageLayout,
362    pub final_layout: ImageLayout,
363}
364
365impl AttachmentDescription {
366    pub fn color(format: TextureFormat) -> Self {
367        AttachmentDescription {
368            format,
369            samples: SampleCount::S1,
370            load_op: LoadOp::Clear,
371            store_op: StoreOp::Store,
372            stencil_load_op: LoadOp::DontCare,
373            stencil_store_op: StoreOp::DontCare,
374            initial_layout: ImageLayout::Undefined,
375            final_layout: ImageLayout::ColorAttachmentOptimal,
376        }
377    }
378    pub fn depth(format: TextureFormat) -> Self {
379        AttachmentDescription {
380            format,
381            samples: SampleCount::S1,
382            load_op: LoadOp::Clear,
383            store_op: StoreOp::Store,
384            stencil_load_op: LoadOp::Clear,
385            stencil_store_op: StoreOp::DontCare,
386            initial_layout: ImageLayout::Undefined,
387            final_layout: ImageLayout::DepthStencilAttachmentOptimal,
388        }
389    }
390    pub fn transient_color(format: TextureFormat, samples: SampleCount) -> Self {
391        AttachmentDescription {
392            format,
393            samples,
394            load_op: LoadOp::Clear,
395            store_op: StoreOp::DontCare,
396            stencil_load_op: LoadOp::DontCare,
397            stencil_store_op: StoreOp::DontCare,
398            initial_layout: ImageLayout::Undefined,
399            final_layout: ImageLayout::ColorAttachmentOptimal,
400        }
401    }
402}
403
404// ============================================================
405//  RENDER GRAPH RESOURCE SYSTEM
406// ============================================================
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
409pub struct ResourceId(pub u32);
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
412pub struct PassId(pub u32);
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum ResourceKind {
416    Texture2D,
417    Texture2DArray,
418    TextureCube,
419    Texture3D,
420    Buffer,
421}
422
423#[derive(Debug, Clone)]
424pub struct TextureDesc {
425    pub width: u32,
426    pub height: u32,
427    pub depth_or_layers: u32,
428    pub mip_levels: u32,
429    pub format: TextureFormat,
430    pub samples: SampleCount,
431    pub kind: ResourceKind,
432}
433
434impl TextureDesc {
435    pub fn render_target(width: u32, height: u32, format: TextureFormat) -> Self {
436        TextureDesc { width, height, depth_or_layers: 1, mip_levels: 1, format, samples: SampleCount::S1, kind: ResourceKind::Texture2D }
437    }
438    pub fn depth_target(width: u32, height: u32) -> Self {
439        TextureDesc { width, height, depth_or_layers: 1, mip_levels: 1, format: TextureFormat::Depth24UnormStencil8, samples: SampleCount::S1, kind: ResourceKind::Texture2D }
440    }
441    pub fn shadow_map(size: u32) -> Self {
442        TextureDesc { width: size, height: size, depth_or_layers: 1, mip_levels: 1, format: TextureFormat::Depth32Float, samples: SampleCount::S1, kind: ResourceKind::Texture2D }
443    }
444    pub fn size_bytes(&self) -> u64 {
445        texture_size_bytes(self.format, self.width, self.height, self.mip_levels)
446    }
447}
448
449#[derive(Debug, Clone)]
450pub struct BufferDesc {
451    pub size: u64,
452    pub stride: u32,
453    pub is_structured: bool,
454}
455
456#[derive(Debug, Clone)]
457pub enum ResourceDesc {
458    Texture(TextureDesc),
459    Buffer(BufferDesc),
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub enum ResourceLifetime {
464    Transient,    // only lives within the frame
465    Persistent,   // survives across frames
466    Imported,     // created externally, imported into graph
467}
468
469#[derive(Debug, Clone)]
470pub struct RenderGraphResource {
471    pub id: ResourceId,
472    pub name: String,
473    pub desc: ResourceDesc,
474    pub lifetime: ResourceLifetime,
475    /// The pass range [first_write_pass, last_read_pass] (index into sorted pass list)
476    pub first_use: usize,
477    pub last_use: usize,
478    /// Whether this resource can share physical memory with another
479    pub can_alias: bool,
480    /// If aliased, the physical resource id it is assigned to
481    pub alias_target: Option<ResourceId>,
482    /// Current layout (updated during barrier analysis)
483    pub current_layout: ImageLayout,
484}
485
486impl RenderGraphResource {
487    pub fn new_transient_texture(id: ResourceId, name: &str, desc: TextureDesc) -> Self {
488        RenderGraphResource {
489            id,
490            name: name.to_owned(),
491            desc: ResourceDesc::Texture(desc),
492            lifetime: ResourceLifetime::Transient,
493            first_use: usize::MAX,
494            last_use: 0,
495            can_alias: true,
496            alias_target: None,
497            current_layout: ImageLayout::Undefined,
498        }
499    }
500
501    pub fn is_texture(&self) -> bool {
502        matches!(self.desc, ResourceDesc::Texture(_))
503    }
504
505    pub fn texture_desc(&self) -> Option<&TextureDesc> {
506        match &self.desc {
507            ResourceDesc::Texture(t) => Some(t),
508            _ => None,
509        }
510    }
511
512    /// Two transient resources can alias if their lifetimes don't overlap
513    pub fn can_alias_with(&self, other: &RenderGraphResource) -> bool {
514        if !self.can_alias || !other.can_alias { return false; }
515        if self.lifetime != ResourceLifetime::Transient || other.lifetime != ResourceLifetime::Transient { return false; }
516        // Check memory-compatibility (same size/format requirements)
517        match (&self.desc, &other.desc) {
518            (ResourceDesc::Texture(a), ResourceDesc::Texture(b)) => {
519                a.size_bytes() == b.size_bytes() && a.samples.count() == b.samples.count()
520            }
521            (ResourceDesc::Buffer(a), ResourceDesc::Buffer(b)) => {
522                a.size == b.size
523            }
524            _ => false,
525        }
526    }
527
528    /// Lifetimes overlap if [first_use, last_use] intervals intersect
529    pub fn lifetime_overlaps(&self, other: &RenderGraphResource) -> bool {
530        !(self.last_use < other.first_use || other.last_use < self.first_use)
531    }
532}
533
534// ============================================================
535//  BARRIER MANAGEMENT
536// ============================================================
537
538#[derive(Debug, Clone)]
539pub struct ImageBarrier {
540    pub resource_id: ResourceId,
541    pub src_stage: PipelineStageFlags,
542    pub dst_stage: PipelineStageFlags,
543    pub src_access: AccessFlags,
544    pub dst_access: AccessFlags,
545    pub old_layout: ImageLayout,
546    pub new_layout: ImageLayout,
547    pub src_queue_family: u32,
548    pub dst_queue_family: u32,
549}
550
551impl ImageBarrier {
552    pub const QUEUE_FAMILY_IGNORED: u32 = u32::MAX;
553
554    pub fn layout_transition(res: ResourceId, old: ImageLayout, new: ImageLayout) -> Self {
555        let (src_stage, src_access) = layout_to_src_info(old);
556        let (dst_stage, dst_access) = layout_to_dst_info(new);
557        ImageBarrier {
558            resource_id: res,
559            src_stage,
560            dst_stage,
561            src_access,
562            dst_access,
563            old_layout: old,
564            new_layout: new,
565            src_queue_family: Self::QUEUE_FAMILY_IGNORED,
566            dst_queue_family: Self::QUEUE_FAMILY_IGNORED,
567        }
568    }
569}
570
571#[derive(Debug, Clone)]
572pub struct BufferBarrier {
573    pub resource_id: ResourceId,
574    pub src_stage: PipelineStageFlags,
575    pub dst_stage: PipelineStageFlags,
576    pub src_access: AccessFlags,
577    pub dst_access: AccessFlags,
578    pub offset: u64,
579    pub size: u64,
580}
581
582#[derive(Debug, Clone)]
583pub struct PipelineBarrier {
584    pub image_barriers: Vec<ImageBarrier>,
585    pub buffer_barriers: Vec<BufferBarrier>,
586    pub memory_barriers: Vec<(AccessFlags, AccessFlags, PipelineStageFlags, PipelineStageFlags)>,
587}
588
589impl PipelineBarrier {
590    pub fn new() -> Self {
591        PipelineBarrier { image_barriers: Vec::new(), buffer_barriers: Vec::new(), memory_barriers: Vec::new() }
592    }
593    pub fn is_empty(&self) -> bool {
594        self.image_barriers.is_empty() && self.buffer_barriers.is_empty() && self.memory_barriers.is_empty()
595    }
596}
597
598/// Map an image layout to the typical pipeline stage/access for a source (after that usage)
599pub fn layout_to_src_info(layout: ImageLayout) -> (PipelineStageFlags, AccessFlags) {
600    match layout {
601        ImageLayout::Undefined | ImageLayout::Preinitialized => {
602            (PipelineStageFlags::TOP_OF_PIPE, AccessFlags::NONE)
603        }
604        ImageLayout::ColorAttachmentOptimal => {
605            (PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
606             AccessFlags::COLOR_ATTACHMENT_WRITE | AccessFlags::COLOR_ATTACHMENT_READ)
607        }
608        ImageLayout::DepthStencilAttachmentOptimal => {
609            (PipelineStageFlags::LATE_FRAGMENT_TESTS | PipelineStageFlags::EARLY_FRAGMENT_TESTS,
610             AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE | AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ)
611        }
612        ImageLayout::DepthStencilReadOnlyOptimal => {
613            (PipelineStageFlags::EARLY_FRAGMENT_TESTS | PipelineStageFlags::FRAGMENT_SHADER,
614             AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ | AccessFlags::SHADER_READ)
615        }
616        ImageLayout::ShaderReadOnlyOptimal => {
617            (PipelineStageFlags::FRAGMENT_SHADER | PipelineStageFlags::COMPUTE_SHADER,
618             AccessFlags::SHADER_READ)
619        }
620        ImageLayout::TransferSrcOptimal => {
621            (PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_READ)
622        }
623        ImageLayout::TransferDstOptimal => {
624            (PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_WRITE)
625        }
626        ImageLayout::PresentSrc => {
627            (PipelineStageFlags::BOTTOM_OF_PIPE, AccessFlags::NONE)
628        }
629        ImageLayout::General => {
630            (PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
631        }
632        _ => {
633            (PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
634        }
635    }
636}
637
638/// Map an image layout to the typical pipeline stage/access for a destination (before that usage)
639pub fn layout_to_dst_info(layout: ImageLayout) -> (PipelineStageFlags, AccessFlags) {
640    match layout {
641        ImageLayout::Undefined => {
642            (PipelineStageFlags::TOP_OF_PIPE, AccessFlags::NONE)
643        }
644        ImageLayout::ColorAttachmentOptimal => {
645            (PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
646             AccessFlags::COLOR_ATTACHMENT_WRITE | AccessFlags::COLOR_ATTACHMENT_READ)
647        }
648        ImageLayout::DepthStencilAttachmentOptimal => {
649            (PipelineStageFlags::EARLY_FRAGMENT_TESTS,
650             AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE | AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ)
651        }
652        ImageLayout::DepthStencilReadOnlyOptimal => {
653            (PipelineStageFlags::EARLY_FRAGMENT_TESTS | PipelineStageFlags::FRAGMENT_SHADER,
654             AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ | AccessFlags::SHADER_READ)
655        }
656        ImageLayout::ShaderReadOnlyOptimal => {
657            (PipelineStageFlags::VERTEX_SHADER | PipelineStageFlags::FRAGMENT_SHADER | PipelineStageFlags::COMPUTE_SHADER,
658             AccessFlags::SHADER_READ)
659        }
660        ImageLayout::TransferSrcOptimal => {
661            (PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_READ)
662        }
663        ImageLayout::TransferDstOptimal => {
664            (PipelineStageFlags::TRANSFER, AccessFlags::TRANSFER_WRITE)
665        }
666        ImageLayout::PresentSrc => {
667            (PipelineStageFlags::BOTTOM_OF_PIPE, AccessFlags::NONE)
668        }
669        ImageLayout::General => {
670            (PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
671        }
672        _ => {
673            (PipelineStageFlags::ALL_COMMANDS, AccessFlags::MEMORY_READ | AccessFlags::MEMORY_WRITE)
674        }
675    }
676}
677
678// ============================================================
679//  PIPELINE STATE OBJECTS
680// ============================================================
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq)]
683pub enum FillMode { Solid, Wireframe, Point }
684
685#[derive(Debug, Clone, Copy, PartialEq, Eq)]
686pub enum CullMode { None, Front, Back, FrontAndBack }
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq)]
689pub enum FrontFace { CounterClockwise, Clockwise }
690
691#[derive(Debug, Clone, Copy, PartialEq, Eq)]
692pub enum CompareOp {
693    Never, Less, Equal, LessOrEqual, Greater, NotEqual, GreaterOrEqual, Always
694}
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697pub enum StencilOp {
698    Keep, Zero, Replace, IncrementAndClamp, DecrementAndClamp,
699    Invert, IncrementAndWrap, DecrementAndWrap
700}
701
702#[derive(Debug, Clone, Copy, PartialEq, Eq)]
703pub enum BlendFactor {
704    Zero, One,
705    SrcColor, OneMinusSrcColor, DstColor, OneMinusDstColor,
706    SrcAlpha, OneMinusSrcAlpha, DstAlpha, OneMinusDstAlpha,
707    ConstantColor, OneMinusConstantColor, ConstantAlpha, OneMinusConstantAlpha,
708    SrcAlphaSaturate,
709    Src1Color, OneMinusSrc1Color, Src1Alpha, OneMinusSrc1Alpha,
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713pub enum BlendOp {
714    Add, Subtract, ReverseSubtract, Min, Max,
715}
716
717#[derive(Debug, Clone, Copy, PartialEq, Eq)]
718pub enum LogicOp {
719    Clear, And, AndReverse, Copy, AndInverted, NoOp, Xor, Or,
720    Nor, Equivalent, Invert, OrReverse, CopyInverted, OrInverted, Nand, Set,
721}
722
723#[derive(Debug, Clone, Copy)]
724pub struct RasterizerState {
725    pub fill_mode: FillMode,
726    pub cull_mode: CullMode,
727    pub front_face: FrontFace,
728    pub depth_clamp_enable: bool,
729    pub rasterizer_discard_enable: bool,
730    pub depth_bias_enable: bool,
731    pub depth_bias_constant_factor: f32,
732    pub depth_bias_clamp: f32,
733    pub depth_bias_slope_factor: f32,
734    pub line_width: f32,
735    pub conservative_rasterization: bool,
736}
737
738impl RasterizerState {
739    pub fn default_opaque() -> Self {
740        RasterizerState {
741            fill_mode: FillMode::Solid,
742            cull_mode: CullMode::Back,
743            front_face: FrontFace::CounterClockwise,
744            depth_clamp_enable: false,
745            rasterizer_discard_enable: false,
746            depth_bias_enable: false,
747            depth_bias_constant_factor: 0.0,
748            depth_bias_clamp: 0.0,
749            depth_bias_slope_factor: 0.0,
750            line_width: 1.0,
751            conservative_rasterization: false,
752        }
753    }
754    pub fn shadow_map() -> Self {
755        RasterizerState {
756            fill_mode: FillMode::Solid,
757            cull_mode: CullMode::Front, // front-face culling for shadow maps avoids peter-panning
758            front_face: FrontFace::CounterClockwise,
759            depth_clamp_enable: true,  // clamp depth to avoid near-plane clip artifacts
760            rasterizer_discard_enable: false,
761            depth_bias_enable: true,
762            depth_bias_constant_factor: 1.25,
763            depth_bias_clamp: 0.0,
764            depth_bias_slope_factor: 1.75,
765            line_width: 1.0,
766            conservative_rasterization: false,
767        }
768    }
769    pub fn wireframe() -> Self {
770        RasterizerState {
771            fill_mode: FillMode::Wireframe,
772            cull_mode: CullMode::None,
773            front_face: FrontFace::CounterClockwise,
774            depth_clamp_enable: false,
775            rasterizer_discard_enable: false,
776            depth_bias_enable: false,
777            depth_bias_constant_factor: 0.0,
778            depth_bias_clamp: 0.0,
779            depth_bias_slope_factor: 0.0,
780            line_width: 1.0,
781            conservative_rasterization: false,
782        }
783    }
784}
785
786#[derive(Debug, Clone, Copy)]
787pub struct StencilOpState {
788    pub fail_op: StencilOp,
789    pub pass_op: StencilOp,
790    pub depth_fail_op: StencilOp,
791    pub compare_op: CompareOp,
792    pub compare_mask: u32,
793    pub write_mask: u32,
794    pub reference: u32,
795}
796
797impl StencilOpState {
798    pub fn disabled() -> Self {
799        StencilOpState {
800            fail_op: StencilOp::Keep,
801            pass_op: StencilOp::Keep,
802            depth_fail_op: StencilOp::Keep,
803            compare_op: CompareOp::Always,
804            compare_mask: 0xFF,
805            write_mask: 0xFF,
806            reference: 0,
807        }
808    }
809    pub fn write_on_pass(ref_val: u32) -> Self {
810        StencilOpState {
811            fail_op: StencilOp::Keep,
812            pass_op: StencilOp::Replace,
813            depth_fail_op: StencilOp::Keep,
814            compare_op: CompareOp::Always,
815            compare_mask: 0xFF,
816            write_mask: 0xFF,
817            reference: ref_val,
818        }
819    }
820    pub fn test_equal(ref_val: u32) -> Self {
821        StencilOpState {
822            fail_op: StencilOp::Keep,
823            pass_op: StencilOp::Keep,
824            depth_fail_op: StencilOp::Keep,
825            compare_op: CompareOp::Equal,
826            compare_mask: 0xFF,
827            write_mask: 0,
828            reference: ref_val,
829        }
830    }
831}
832
833#[derive(Debug, Clone, Copy)]
834pub struct DepthStencilState {
835    pub depth_test_enable: bool,
836    pub depth_write_enable: bool,
837    pub depth_compare_op: CompareOp,
838    pub depth_bounds_test_enable: bool,
839    pub min_depth_bounds: f32,
840    pub max_depth_bounds: f32,
841    pub stencil_test_enable: bool,
842    pub front: StencilOpState,
843    pub back: StencilOpState,
844}
845
846impl DepthStencilState {
847    pub fn depth_read_write() -> Self {
848        DepthStencilState {
849            depth_test_enable: true,
850            depth_write_enable: true,
851            depth_compare_op: CompareOp::Less,
852            depth_bounds_test_enable: false,
853            min_depth_bounds: 0.0,
854            max_depth_bounds: 1.0,
855            stencil_test_enable: false,
856            front: StencilOpState::disabled(),
857            back: StencilOpState::disabled(),
858        }
859    }
860    pub fn depth_read_only() -> Self {
861        DepthStencilState {
862            depth_test_enable: true,
863            depth_write_enable: false,
864            depth_compare_op: CompareOp::LessOrEqual,
865            depth_bounds_test_enable: false,
866            min_depth_bounds: 0.0,
867            max_depth_bounds: 1.0,
868            stencil_test_enable: false,
869            front: StencilOpState::disabled(),
870            back: StencilOpState::disabled(),
871        }
872    }
873    pub fn no_depth() -> Self {
874        DepthStencilState {
875            depth_test_enable: false,
876            depth_write_enable: false,
877            depth_compare_op: CompareOp::Always,
878            depth_bounds_test_enable: false,
879            min_depth_bounds: 0.0,
880            max_depth_bounds: 1.0,
881            stencil_test_enable: false,
882            front: StencilOpState::disabled(),
883            back: StencilOpState::disabled(),
884        }
885    }
886    pub fn reverse_z() -> Self {
887        DepthStencilState {
888            depth_test_enable: true,
889            depth_write_enable: true,
890            depth_compare_op: CompareOp::Greater,
891            depth_bounds_test_enable: false,
892            min_depth_bounds: 0.0,
893            max_depth_bounds: 1.0,
894            stencil_test_enable: false,
895            front: StencilOpState::disabled(),
896            back: StencilOpState::disabled(),
897        }
898    }
899}
900
901#[derive(Debug, Clone, Copy)]
902pub struct ColorBlendAttachment {
903    pub blend_enable: bool,
904    pub src_color_blend_factor: BlendFactor,
905    pub dst_color_blend_factor: BlendFactor,
906    pub color_blend_op: BlendOp,
907    pub src_alpha_blend_factor: BlendFactor,
908    pub dst_alpha_blend_factor: BlendFactor,
909    pub alpha_blend_op: BlendOp,
910    pub color_write_mask: u8, // RGBA bits
911}
912
913impl ColorBlendAttachment {
914    pub const COLOR_WRITE_RGBA: u8 = 0b1111;
915    pub const COLOR_WRITE_RGB: u8  = 0b0111;
916    pub const COLOR_WRITE_A: u8    = 0b1000;
917
918    pub fn opaque() -> Self {
919        ColorBlendAttachment {
920            blend_enable: false,
921            src_color_blend_factor: BlendFactor::One,
922            dst_color_blend_factor: BlendFactor::Zero,
923            color_blend_op: BlendOp::Add,
924            src_alpha_blend_factor: BlendFactor::One,
925            dst_alpha_blend_factor: BlendFactor::Zero,
926            alpha_blend_op: BlendOp::Add,
927            color_write_mask: Self::COLOR_WRITE_RGBA,
928        }
929    }
930    pub fn alpha_blend() -> Self {
931        ColorBlendAttachment {
932            blend_enable: true,
933            src_color_blend_factor: BlendFactor::SrcAlpha,
934            dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
935            color_blend_op: BlendOp::Add,
936            src_alpha_blend_factor: BlendFactor::One,
937            dst_alpha_blend_factor: BlendFactor::OneMinusSrcAlpha,
938            alpha_blend_op: BlendOp::Add,
939            color_write_mask: Self::COLOR_WRITE_RGBA,
940        }
941    }
942    pub fn premultiplied_alpha() -> Self {
943        ColorBlendAttachment {
944            blend_enable: true,
945            src_color_blend_factor: BlendFactor::One,
946            dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
947            color_blend_op: BlendOp::Add,
948            src_alpha_blend_factor: BlendFactor::One,
949            dst_alpha_blend_factor: BlendFactor::OneMinusSrcAlpha,
950            alpha_blend_op: BlendOp::Add,
951            color_write_mask: Self::COLOR_WRITE_RGBA,
952        }
953    }
954    pub fn additive() -> Self {
955        ColorBlendAttachment {
956            blend_enable: true,
957            src_color_blend_factor: BlendFactor::One,
958            dst_color_blend_factor: BlendFactor::One,
959            color_blend_op: BlendOp::Add,
960            src_alpha_blend_factor: BlendFactor::One,
961            dst_alpha_blend_factor: BlendFactor::One,
962            alpha_blend_op: BlendOp::Add,
963            color_write_mask: Self::COLOR_WRITE_RGBA,
964        }
965    }
966}
967
968#[derive(Debug, Clone)]
969pub struct ColorBlendState {
970    pub logic_op_enable: bool,
971    pub logic_op: LogicOp,
972    pub attachments: Vec<ColorBlendAttachment>,
973    pub blend_constants: [f32; 4],
974}
975
976impl ColorBlendState {
977    pub fn all_opaque(count: usize) -> Self {
978        ColorBlendState {
979            logic_op_enable: false,
980            logic_op: LogicOp::Copy,
981            attachments: vec![ColorBlendAttachment::opaque(); count],
982            blend_constants: [0.0; 4],
983        }
984    }
985}
986
987#[derive(Debug, Clone, Copy)]
988pub struct MultisampleState {
989    pub sample_count: SampleCount,
990    pub sample_shading_enable: bool,
991    pub min_sample_shading: f32,
992    pub alpha_to_coverage: bool,
993    pub alpha_to_one: bool,
994}
995
996impl MultisampleState {
997    pub fn disabled() -> Self {
998        MultisampleState { sample_count: SampleCount::S1, sample_shading_enable: false, min_sample_shading: 0.0, alpha_to_coverage: false, alpha_to_one: false }
999    }
1000    pub fn msaa4x() -> Self {
1001        MultisampleState { sample_count: SampleCount::S4, sample_shading_enable: false, min_sample_shading: 0.0, alpha_to_coverage: false, alpha_to_one: false }
1002    }
1003}
1004
1005#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1006pub enum VertexFormat {
1007    Float1, Float2, Float3, Float4,
1008    Half2, Half4,
1009    Uint1, Uint2, Uint4,
1010    Int1, Int2, Int4,
1011    Unorm8x4, Snorm8x4,
1012    Unorm16x2, Unorm16x4,
1013}
1014
1015impl VertexFormat {
1016    pub fn size_bytes(self) -> u32 {
1017        match self {
1018            VertexFormat::Float1 => 4,
1019            VertexFormat::Float2 => 8,
1020            VertexFormat::Float3 => 12,
1021            VertexFormat::Float4 => 16,
1022            VertexFormat::Half2  => 4,
1023            VertexFormat::Half4  => 8,
1024            VertexFormat::Uint1  => 4,
1025            VertexFormat::Uint2  => 8,
1026            VertexFormat::Uint4  => 16,
1027            VertexFormat::Int1   => 4,
1028            VertexFormat::Int2   => 8,
1029            VertexFormat::Int4   => 16,
1030            VertexFormat::Unorm8x4  => 4,
1031            VertexFormat::Snorm8x4  => 4,
1032            VertexFormat::Unorm16x2 => 4,
1033            VertexFormat::Unorm16x4 => 8,
1034        }
1035    }
1036}
1037
1038#[derive(Debug, Clone)]
1039pub struct VertexAttribute {
1040    pub location: u32,
1041    pub binding: u32,
1042    pub format: VertexFormat,
1043    pub offset: u32,
1044}
1045
1046#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1047pub enum VertexInputRate { Vertex, Instance }
1048
1049#[derive(Debug, Clone)]
1050pub struct VertexBinding {
1051    pub binding: u32,
1052    pub stride: u32,
1053    pub input_rate: VertexInputRate,
1054}
1055
1056#[derive(Debug, Clone)]
1057pub struct VertexInputLayout {
1058    pub bindings: Vec<VertexBinding>,
1059    pub attributes: Vec<VertexAttribute>,
1060}
1061
1062impl VertexInputLayout {
1063    pub fn empty() -> Self { VertexInputLayout { bindings: vec![], attributes: vec![] } }
1064
1065    pub fn standard_mesh() -> Self {
1066        // binding 0: position (vec3), normal (vec3), tangent (vec4), uv (vec2) = 12+12+16+8 = 48 bytes
1067        let bindings = vec![
1068            VertexBinding { binding: 0, stride: 48, input_rate: VertexInputRate::Vertex },
1069        ];
1070        let attributes = vec![
1071            VertexAttribute { location: 0, binding: 0, format: VertexFormat::Float3, offset: 0  }, // position
1072            VertexAttribute { location: 1, binding: 0, format: VertexFormat::Float3, offset: 12 }, // normal
1073            VertexAttribute { location: 2, binding: 0, format: VertexFormat::Float4, offset: 24 }, // tangent
1074            VertexAttribute { location: 3, binding: 0, format: VertexFormat::Float2, offset: 40 }, // uv
1075        ];
1076        VertexInputLayout { bindings, attributes }
1077    }
1078
1079    pub fn skinned_mesh() -> Self {
1080        // binding 0: pos+normal+tangent+uv, binding 1: bone indices (uvec4) + bone weights (vec4)
1081        let bindings = vec![
1082            VertexBinding { binding: 0, stride: 48, input_rate: VertexInputRate::Vertex },
1083            VertexBinding { binding: 1, stride: 32, input_rate: VertexInputRate::Vertex },
1084        ];
1085        let attributes = vec![
1086            VertexAttribute { location: 0, binding: 0, format: VertexFormat::Float3, offset: 0  },
1087            VertexAttribute { location: 1, binding: 0, format: VertexFormat::Float3, offset: 12 },
1088            VertexAttribute { location: 2, binding: 0, format: VertexFormat::Float4, offset: 24 },
1089            VertexAttribute { location: 3, binding: 0, format: VertexFormat::Float2, offset: 40 },
1090            VertexAttribute { location: 4, binding: 1, format: VertexFormat::Uint4,  offset: 0  }, // bone indices
1091            VertexAttribute { location: 5, binding: 1, format: VertexFormat::Float4, offset: 16 }, // bone weights
1092        ];
1093        VertexInputLayout { bindings, attributes }
1094    }
1095
1096    pub fn total_stride(&self, binding: u32) -> u32 {
1097        self.bindings.iter().find(|b| b.binding == binding).map(|b| b.stride).unwrap_or(0)
1098    }
1099}
1100
1101// ============================================================
1102//  RENDER PASS NODE DEFINITIONS
1103// ============================================================
1104
1105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1106pub enum PassKind {
1107    GBuffer,
1108    ShadowMap,
1109    Lighting,
1110    SSAO,
1111    SSR,
1112    Bloom,
1113    ToneMapping,
1114    TAA,
1115    DepthOfField,
1116    MotionBlur,
1117    VolumetricFog,
1118    Particle,
1119    UI,
1120    Debug,
1121    Custom,
1122}
1123
1124// ---- GBuffer Pass ----
1125
1126#[derive(Debug, Clone)]
1127pub struct GBufferPassDesc {
1128    pub width: u32,
1129    pub height: u32,
1130    pub albedo_format: TextureFormat,       // GBuffer A: albedo + roughness
1131    pub normal_format: TextureFormat,       // GBuffer B: world-space normals
1132    pub material_format: TextureFormat,     // GBuffer C: metallic + AO + emissive
1133    pub velocity_format: TextureFormat,     // GBuffer D: motion vectors
1134    pub depth_format: TextureFormat,
1135    pub samples: SampleCount,
1136    pub output_albedo: ResourceId,
1137    pub output_normal: ResourceId,
1138    pub output_material: ResourceId,
1139    pub output_velocity: ResourceId,
1140    pub output_depth: ResourceId,
1141    pub rasterizer: RasterizerState,
1142    pub depth_stencil: DepthStencilState,
1143    pub vertex_layout: VertexInputLayout,
1144}
1145
1146impl GBufferPassDesc {
1147    pub fn default(width: u32, height: u32) -> Self {
1148        GBufferPassDesc {
1149            width, height,
1150            albedo_format: TextureFormat::RGBA8Unorm,
1151            normal_format: TextureFormat::RG16Float,     // octahedral encoded normals
1152            material_format: TextureFormat::RGBA8Unorm,
1153            velocity_format: TextureFormat::RG16Float,
1154            depth_format: TextureFormat::Depth24UnormStencil8,
1155            samples: SampleCount::S1,
1156            output_albedo:   ResourceId(0),
1157            output_normal:   ResourceId(1),
1158            output_material: ResourceId(2),
1159            output_velocity: ResourceId(3),
1160            output_depth:    ResourceId(4),
1161            rasterizer: RasterizerState::default_opaque(),
1162            depth_stencil: DepthStencilState::depth_read_write(),
1163            vertex_layout: VertexInputLayout::standard_mesh(),
1164        }
1165    }
1166    pub fn attachment_descriptions(&self) -> Vec<AttachmentDescription> {
1167        vec![
1168            AttachmentDescription::color(self.albedo_format),
1169            AttachmentDescription::color(self.normal_format),
1170            AttachmentDescription::color(self.material_format),
1171            AttachmentDescription::color(self.velocity_format),
1172            AttachmentDescription::depth(self.depth_format),
1173        ]
1174    }
1175    /// Total bandwidth per pixel for writing the full GBuffer
1176    pub fn bandwidth_bytes_per_pixel(&self) -> f32 {
1177        let fi_a = format_info(self.albedo_format).bytes_per_pixel();
1178        let fi_b = format_info(self.normal_format).bytes_per_pixel();
1179        let fi_c = format_info(self.material_format).bytes_per_pixel();
1180        let fi_d = format_info(self.velocity_format).bytes_per_pixel();
1181        let fi_z = format_info(self.depth_format).bytes_per_pixel();
1182        fi_a + fi_b + fi_c + fi_d + fi_z
1183    }
1184    /// Estimate total GBuffer write bandwidth in MB for one frame
1185    pub fn estimate_write_bandwidth_mb(&self) -> f32 {
1186        let bpp = self.bandwidth_bytes_per_pixel();
1187        let pixels = (self.width * self.height) as f32;
1188        (bpp * pixels) / (1024.0 * 1024.0)
1189    }
1190}
1191
1192// ---- Shadow Map Pass ----
1193
1194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1195pub enum ShadowMapKind { Directional, Spot, Point, Cascaded }
1196
1197#[derive(Debug, Clone)]
1198pub struct ShadowMapPassDesc {
1199    pub kind: ShadowMapKind,
1200    pub resolution: u32,
1201    pub cascade_count: u32,          // for cascaded shadow maps
1202    pub depth_format: TextureFormat,
1203    pub output_shadow_map: ResourceId,
1204    pub rasterizer: RasterizerState,
1205    pub depth_stencil: DepthStencilState,
1206    pub near_plane: f32,
1207    pub far_plane: f32,
1208    pub light_view_proj: [Mat4; 4],  // up to 4 cascades
1209}
1210
1211impl ShadowMapPassDesc {
1212    pub fn directional_shadow(resolution: u32) -> Self {
1213        ShadowMapPassDesc {
1214            kind: ShadowMapKind::Cascaded,
1215            resolution,
1216            cascade_count: 4,
1217            depth_format: TextureFormat::Depth32Float,
1218            output_shadow_map: ResourceId(100),
1219            rasterizer: RasterizerState::shadow_map(),
1220            depth_stencil: DepthStencilState::depth_read_write(),
1221            near_plane: 0.1,
1222            far_plane: 200.0,
1223            light_view_proj: [Mat4::IDENTITY; 4],
1224        }
1225    }
1226    /// Compute cascade split distances using the practical split scheme
1227    pub fn compute_cascade_splits(&self, lambda: f32, near: f32, far: f32) -> Vec<f32> {
1228        let n = self.cascade_count as usize;
1229        let mut splits = vec![0.0f32; n];
1230        let ratio = far / near;
1231        for i in 0..n {
1232            let p = (i + 1) as f32 / n as f32;
1233            let log = near * ratio.powf(p);
1234            let uniform = near + (far - near) * p;
1235            let d = lambda * (log - uniform) + uniform;
1236            splits[i] = d;
1237        }
1238        splits
1239    }
1240    /// Compute a tight light-space projection for a cascade slice
1241    pub fn compute_cascade_view_proj(&self, camera_view: Mat4, inv_cam_proj: Mat4, near_split: f32, far_split: f32, light_dir: Vec3) -> Mat4 {
1242        // Compute frustum corners in world space
1243        let ndc_corners = [
1244            Vec4::new(-1.0, -1.0, 0.0, 1.0),
1245            Vec4::new( 1.0, -1.0, 0.0, 1.0),
1246            Vec4::new(-1.0,  1.0, 0.0, 1.0),
1247            Vec4::new( 1.0,  1.0, 0.0, 1.0),
1248            Vec4::new(-1.0, -1.0, 1.0, 1.0),
1249            Vec4::new( 1.0, -1.0, 1.0, 1.0),
1250            Vec4::new(-1.0,  1.0, 1.0, 1.0),
1251            Vec4::new( 1.0,  1.0, 1.0, 1.0),
1252        ];
1253        let inv_view_proj = (camera_view).inverse();
1254        let mut world_corners = [Vec3::ZERO; 8];
1255        for (i, ndc) in ndc_corners.iter().enumerate() {
1256            let view_h = inv_cam_proj * *ndc;
1257            let view = view_h / view_h.w;
1258            // scale near/far
1259            let z_frac = if i < 4 { near_split } else { far_split };
1260            let view_scaled = Vec4::new(view.x * z_frac, view.y * z_frac, view.z * z_frac, 1.0);
1261            let world_h = inv_view_proj * view_scaled;
1262            world_corners[i] = world_h.truncate() / world_h.w;
1263        }
1264        // Compute centroid
1265        let mut centroid = Vec3::ZERO;
1266        for c in &world_corners { centroid += *c; }
1267        centroid /= 8.0;
1268        // Build light view matrix
1269        let up = if light_dir.dot(Vec3::Y).abs() < 0.999 { Vec3::Y } else { Vec3::Z };
1270        let light_view = Mat4::look_at_rh(centroid - light_dir * 50.0, centroid, up);
1271        // Transform corners to light space, compute AABB
1272        let mut min_ls = Vec3::splat(f32::MAX);
1273        let mut max_ls = Vec3::splat(f32::MIN);
1274        for c in &world_corners {
1275            let ls = (light_view * Vec4::new(c.x, c.y, c.z, 1.0)).truncate();
1276            min_ls = min_ls.min(ls);
1277            max_ls = max_ls.max(ls);
1278        }
1279        // Snap to texel grid to reduce shadow shimmering
1280        let world_units_per_texel = (max_ls.x - min_ls.x) / self.resolution as f32;
1281        min_ls.x = (min_ls.x / world_units_per_texel).floor() * world_units_per_texel;
1282        max_ls.x = (max_ls.x / world_units_per_texel).ceil()  * world_units_per_texel;
1283        min_ls.y = (min_ls.y / world_units_per_texel).floor() * world_units_per_texel;
1284        max_ls.y = (max_ls.y / world_units_per_texel).ceil()  * world_units_per_texel;
1285        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);
1286        light_proj * light_view
1287    }
1288}
1289
1290// ---- Lighting Pass ----
1291
1292#[derive(Debug, Clone)]
1293pub struct LightingPassDesc {
1294    pub width: u32,
1295    pub height: u32,
1296    pub output_format: TextureFormat,
1297    pub output_hdr: ResourceId,
1298    // Inputs
1299    pub input_albedo: ResourceId,
1300    pub input_normal: ResourceId,
1301    pub input_material: ResourceId,
1302    pub input_depth: ResourceId,
1303    pub input_shadow_map: ResourceId,
1304    pub input_ssao: ResourceId,
1305    // IBL
1306    pub ibl_enabled: bool,
1307    pub ibl_diffuse_irradiance_res: u32,
1308    pub ibl_specular_prefiltered_res: u32,
1309    pub ibl_brdf_lut_res: u32,
1310    // Tiled / clustered
1311    pub tile_size: u32,
1312    pub max_lights_per_tile: u32,
1313}
1314
1315impl LightingPassDesc {
1316    pub fn default(width: u32, height: u32) -> Self {
1317        LightingPassDesc {
1318            width, height,
1319            output_format: TextureFormat::RGBA16Float,
1320            output_hdr: ResourceId(10),
1321            input_albedo: ResourceId(0),
1322            input_normal: ResourceId(1),
1323            input_material: ResourceId(2),
1324            input_depth: ResourceId(4),
1325            input_shadow_map: ResourceId(100),
1326            input_ssao: ResourceId(20),
1327            ibl_enabled: true,
1328            ibl_diffuse_irradiance_res: 32,
1329            ibl_specular_prefiltered_res: 256,
1330            ibl_brdf_lut_res: 512,
1331            tile_size: 16,
1332            max_lights_per_tile: 1024,
1333        }
1334    }
1335    pub fn tile_count_x(&self) -> u32 { (self.width + self.tile_size - 1) / self.tile_size }
1336    pub fn tile_count_y(&self) -> u32 { (self.height + self.tile_size - 1) / self.tile_size }
1337    pub fn total_tiles(&self) -> u32 { self.tile_count_x() * self.tile_count_y() }
1338    pub fn light_list_buffer_size_bytes(&self) -> u64 {
1339        // Each tile stores up to max_lights_per_tile 16-bit indices + a count
1340        (self.total_tiles() as u64) * (self.max_lights_per_tile as u64 + 1) * 2
1341    }
1342}
1343
1344// ---- SSAO Pass ----
1345
1346#[derive(Debug, Clone)]
1347pub struct SSAOPassDesc {
1348    pub width: u32,
1349    pub height: u32,
1350    pub output_format: TextureFormat,
1351    pub output_ao: ResourceId,
1352    pub input_depth: ResourceId,
1353    pub input_normal: ResourceId,
1354    pub kernel_size: u32,
1355    pub radius: f32,
1356    pub bias: f32,
1357    pub power: f32,
1358    pub noise_tex_size: u32,
1359    pub blur_passes: u32,
1360    pub half_resolution: bool,
1361}
1362
1363impl SSAOPassDesc {
1364    pub fn default(width: u32, height: u32) -> Self {
1365        SSAOPassDesc {
1366            width, height,
1367            output_format: TextureFormat::R8Unorm,
1368            output_ao: ResourceId(20),
1369            input_depth: ResourceId(4),
1370            input_normal: ResourceId(1),
1371            kernel_size: 64,
1372            radius: 0.5,
1373            bias: 0.025,
1374            power: 2.2,
1375            noise_tex_size: 4,
1376            blur_passes: 2,
1377            half_resolution: true,
1378        }
1379    }
1380
1381    /// Generate SSAO hemisphere kernel samples
1382    pub fn generate_kernel(&self) -> Vec<Vec3> {
1383        let mut kernel = Vec::with_capacity(self.kernel_size as usize);
1384        // Use a deterministic LCG for reproducible kernel
1385        let mut lcg: u64 = 0x123456789ABCDEF0;
1386        let lcg_next = |state: &mut u64| -> f32 {
1387            *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1388            ((*state >> 33) as f32) / (u32::MAX as f32)
1389        };
1390        for i in 0..self.kernel_size {
1391            let x = lcg_next(&mut lcg) * 2.0 - 1.0;
1392            let y = lcg_next(&mut lcg) * 2.0 - 1.0;
1393            let z = lcg_next(&mut lcg); // only positive z hemisphere
1394            let mut sample = Vec3::new(x, y, z).normalize();
1395            sample *= lcg_next(&mut lcg);
1396            // Accelerating interpolation (more samples near origin)
1397            let scale = (i as f32) / (self.kernel_size as f32);
1398            let scale = lerp(0.1, 1.0, scale * scale);
1399            sample *= scale;
1400            kernel.push(sample);
1401        }
1402        kernel
1403    }
1404
1405    /// Generate noise texture for SSAO rotation
1406    pub fn generate_noise(&self) -> Vec<Vec3> {
1407        let n = (self.noise_tex_size * self.noise_tex_size) as usize;
1408        let mut noise = Vec::with_capacity(n);
1409        let mut lcg: u64 = 0xDEADBEEFCAFEBABE;
1410        let lcg_next = |state: &mut u64| -> f32 {
1411            *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1412            ((*state >> 33) as f32) / (u32::MAX as f32)
1413        };
1414        for _ in 0..n {
1415            let x = lcg_next(&mut lcg) * 2.0 - 1.0;
1416            let y = lcg_next(&mut lcg) * 2.0 - 1.0;
1417            noise.push(Vec3::new(x, y, 0.0)); // rotation around z-axis
1418        }
1419        noise
1420    }
1421
1422    pub fn effective_width(&self) -> u32 { if self.half_resolution { self.width / 2 } else { self.width } }
1423    pub fn effective_height(&self) -> u32 { if self.half_resolution { self.height / 2 } else { self.height } }
1424}
1425
1426// ---- SSR Pass ----
1427
1428#[derive(Debug, Clone)]
1429pub struct SSRPassDesc {
1430    pub width: u32,
1431    pub height: u32,
1432    pub output_format: TextureFormat,
1433    pub output_ssr: ResourceId,
1434    pub input_depth: ResourceId,
1435    pub input_normal: ResourceId,
1436    pub input_material: ResourceId,
1437    pub input_hdr: ResourceId,
1438    pub max_steps: u32,
1439    pub step_size: f32,
1440    pub max_distance: f32,
1441    pub thickness: f32,
1442    pub binary_search_steps: u32,
1443    pub jitter: bool,
1444    pub half_resolution: bool,
1445    pub reprojection_enabled: bool,
1446}
1447
1448impl SSRPassDesc {
1449    pub fn default(width: u32, height: u32) -> Self {
1450        SSRPassDesc {
1451            width, height,
1452            output_format: TextureFormat::RGBA16Float,
1453            output_ssr: ResourceId(21),
1454            input_depth: ResourceId(4),
1455            input_normal: ResourceId(1),
1456            input_material: ResourceId(2),
1457            input_hdr: ResourceId(10),
1458            max_steps: 64,
1459            step_size: 0.1,
1460            max_distance: 10.0,
1461            thickness: 0.1,
1462            binary_search_steps: 8,
1463            jitter: true,
1464            half_resolution: true,
1465            reprojection_enabled: true,
1466        }
1467    }
1468    /// Compute hi-z mip level for a given screen-space distance
1469    pub fn compute_hiz_mip_level(&self, screen_distance: f32) -> u32 {
1470        let mip = (screen_distance / self.step_size).log2() as u32;
1471        mip.clamp(0, 8)
1472    }
1473    pub fn screen_fade(&self, uv: Vec2) -> f32 {
1474        let edge = 0.1f32;
1475        let fade_x = smoothstep(0.0, edge, uv.x) * smoothstep(1.0, 1.0 - edge, uv.x);
1476        let fade_y = smoothstep(0.0, edge, uv.y) * smoothstep(1.0, 1.0 - edge, uv.y);
1477        fade_x * fade_y
1478    }
1479}
1480
1481// ---- Bloom Pass ----
1482
1483#[derive(Debug, Clone)]
1484pub struct BloomPassDesc {
1485    pub width: u32,
1486    pub height: u32,
1487    pub output_format: TextureFormat,
1488    pub output_bloom: ResourceId,
1489    pub input_hdr: ResourceId,
1490    pub threshold: f32,
1491    pub knee: f32,
1492    pub intensity: f32,
1493    pub scatter: f32,
1494    pub mip_levels: u32,
1495    pub use_lens_dirt: bool,
1496    pub lens_dirt_intensity: f32,
1497}
1498
1499impl BloomPassDesc {
1500    pub fn default(width: u32, height: u32) -> Self {
1501        BloomPassDesc {
1502            width, height,
1503            output_format: TextureFormat::RGBA16Float,
1504            output_bloom: ResourceId(22),
1505            input_hdr: ResourceId(10),
1506            threshold: 1.0,
1507            knee: 0.5,
1508            intensity: 0.05,
1509            scatter: 0.7,
1510            mip_levels: 6,
1511            use_lens_dirt: false,
1512            lens_dirt_intensity: 0.3,
1513        }
1514    }
1515    /// Quadratic threshold curve: bright pass filter to extract bright regions
1516    pub fn quadratic_threshold(&self, lum: f32) -> f32 {
1517        let t = self.threshold;
1518        let k = self.knee;
1519        // Quadratic curve: smoothly remap luminance above threshold
1520        let rq = (lum - t + k * 0.5).clamp(0.0, k);
1521        let threshold_result = (rq * rq) / (4.0 * k + 0.00001);
1522        let linear_result = (lum - t).max(0.0);
1523        // Combine curves
1524        threshold_result.max(linear_result)
1525    }
1526    /// Kawase blur kernel weights for downsampling
1527    pub fn kawase_weights(iter: u32) -> [f32; 4] {
1528        let offset = iter as f32 + 0.5;
1529        [offset, offset, offset, offset]
1530    }
1531    /// Dual Kawase upsample offsets
1532    pub fn dual_kawase_upsample_offsets(iter: u32) -> [Vec2; 8] {
1533        let s = (iter as f32) + 0.5;
1534        [
1535            Vec2::new(-s, -s), Vec2::new(0.0, -s), Vec2::new(s, -s),
1536            Vec2::new(-s,  0.0),                    Vec2::new(s,  0.0),
1537            Vec2::new(-s,  s), Vec2::new(0.0,  s), Vec2::new(s,  s),
1538        ]
1539    }
1540    pub fn mip_size(&self, mip: u32) -> (u32, u32) {
1541        let w = (self.width >> mip).max(1);
1542        let h = (self.height >> mip).max(1);
1543        (w, h)
1544    }
1545}
1546
1547// ---- Tone Mapping Pass ----
1548
1549#[derive(Debug, Clone, Copy, PartialEq)]
1550pub enum ToneMappingOperator {
1551    Linear,
1552    Reinhard,
1553    ReinhardExtended,
1554    Filmic,        // Hable
1555    ACES,          // ACES fitted
1556    Uncharted2,
1557    Lottes,
1558    Uchimura,
1559}
1560
1561#[derive(Debug, Clone)]
1562pub struct ToneMappingPassDesc {
1563    pub width: u32,
1564    pub height: u32,
1565    pub output_format: TextureFormat,
1566    pub output_sdr: ResourceId,
1567    pub input_hdr: ResourceId,
1568    pub input_bloom: ResourceId,
1569    pub operator: ToneMappingOperator,
1570    pub exposure: f32,
1571    pub gamma: f32,
1572    pub white_point: f32,
1573    pub color_lut_enabled: bool,
1574    pub color_lut_size: u32,
1575}
1576
1577impl ToneMappingPassDesc {
1578    pub fn default(width: u32, height: u32) -> Self {
1579        ToneMappingPassDesc {
1580            width, height,
1581            output_format: TextureFormat::RGBA8UnormSrgb,
1582            output_sdr: ResourceId(30),
1583            input_hdr: ResourceId(10),
1584            input_bloom: ResourceId(22),
1585            operator: ToneMappingOperator::ACES,
1586            exposure: 1.0,
1587            gamma: 2.2,
1588            white_point: 4.0,
1589            color_lut_enabled: false,
1590            color_lut_size: 32,
1591        }
1592    }
1593
1594    pub fn apply_aces(&self, color: Vec3) -> Vec3 {
1595        // ACES fitted by Stephen Hill
1596        let m1 = Mat3F32([
1597            [0.59719, 0.35458, 0.04823],
1598            [0.07600, 0.90834, 0.01566],
1599            [0.02840, 0.13383, 0.83777],
1600        ]);
1601        let m2 = Mat3F32([
1602            [ 1.60475, -0.53108, -0.07367],
1603            [-0.10208,  1.10813, -0.00605],
1604            [-0.00327, -0.07276,  1.07602],
1605        ]);
1606        let v = m1.mul_vec3(color);
1607        let a = v * (v + Vec3::splat(0.0245786)) - Vec3::splat(0.000090537);
1608        let b = v * (Vec3::splat(0.983729) * v + Vec3::splat(0.4329510)) + Vec3::splat(0.238081);
1609        let rrt_odt = a / b;
1610        let mapped = m2.mul_vec3(rrt_odt);
1611        mapped.clamp(Vec3::ZERO, Vec3::ONE)
1612    }
1613
1614    pub fn apply_hable_filmic(&self, color: Vec3) -> Vec3 {
1615        let hable = |x: Vec3| -> Vec3 {
1616            let a = Vec3::splat(0.15);
1617            let b = Vec3::splat(0.50);
1618            let c = Vec3::splat(0.10);
1619            let d = Vec3::splat(0.20);
1620            let e = Vec3::splat(0.02);
1621            let f = Vec3::splat(0.30);
1622            (x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f) - e / f
1623        };
1624        let white = Vec3::splat(self.white_point);
1625        hable(color * self.exposure) / hable(white)
1626    }
1627
1628    pub fn apply_reinhard(&self, color: Vec3) -> Vec3 {
1629        color / (color + Vec3::ONE)
1630    }
1631
1632    pub fn apply_reinhard_extended(&self, color: Vec3) -> Vec3 {
1633        let w2 = Vec3::splat(self.white_point * self.white_point);
1634        (color * (Vec3::ONE + color / w2)) / (color + Vec3::ONE)
1635    }
1636
1637    pub fn apply_operator(&self, color: Vec3) -> Vec3 {
1638        let c = color * self.exposure;
1639        match self.operator {
1640            ToneMappingOperator::Linear           => c.clamp(Vec3::ZERO, Vec3::ONE),
1641            ToneMappingOperator::Reinhard         => self.apply_reinhard(c),
1642            ToneMappingOperator::ReinhardExtended => self.apply_reinhard_extended(c),
1643            ToneMappingOperator::Filmic           => self.apply_hable_filmic(color),
1644            ToneMappingOperator::ACES             => self.apply_aces(c),
1645            ToneMappingOperator::Uncharted2       => self.apply_hable_filmic(color), // alias
1646            ToneMappingOperator::Lottes           => self.apply_lottes(c),
1647            ToneMappingOperator::Uchimura         => self.apply_uchimura(c),
1648        }
1649    }
1650
1651    fn apply_lottes(&self, color: Vec3) -> Vec3 {
1652        let a = Vec3::splat(1.6);
1653        let d = Vec3::splat(0.977);
1654        let hdr_max = Vec3::splat(8.0);
1655        let mid_in  = Vec3::splat(0.18);
1656        let mid_out = Vec3::splat(0.267);
1657        let b = (-mid_out + mid_in.powf(a.x) * hdr_max.powf(d.x)) /
1658                ((hdr_max.powf(a.x) - mid_in.powf(a.x)) * mid_out);
1659        let c = (mid_in.powf(a.x) * hdr_max.powf(d.x) - hdr_max.powf(a.x) * mid_out) /
1660                ((hdr_max.powf(a.x) - mid_in.powf(a.x)) * mid_out);
1661        color.powf(a.x) / (color.powf(a.x * d.x) * b + c)
1662    }
1663
1664    fn apply_uchimura(&self, color: Vec3) -> Vec3 {
1665        let p = 1.0f32;  // max brightness
1666        let a = 1.0f32;  // contrast
1667        let m = 0.22f32; // linear section start
1668        let l = 0.4f32;  // linear section length
1669        let c = 1.33f32; // black tightness
1670        let b = 0.0f32;  // pedestal
1671
1672        let map_channel = |x: f32| -> f32 {
1673            let l0 = (p - m) * l / a;
1674            let s0 = m + l0;
1675            let s1 = m + a * l0;
1676            let c2 = a * p / (p - s1);
1677            let cp = -c2 / p;
1678            if x < m {
1679                let d = m / (c * m + 1.0 - c);
1680                d * x
1681            } else if x < s1 {
1682                let d = m + a * (x - m);
1683                d
1684            } else {
1685                p - (p - s1) * (-c2 * (x - s0) / p).exp()
1686            }
1687        };
1688        Vec3::new(map_channel(color.x), map_channel(color.y), map_channel(color.z))
1689    }
1690
1691    pub fn gamma_correct(&self, linear: Vec3) -> Vec3 {
1692        let inv_gamma = 1.0 / self.gamma;
1693        Vec3::new(linear.x.powf(inv_gamma), linear.y.powf(inv_gamma), linear.z.powf(inv_gamma))
1694    }
1695}
1696
1697// Helper 3x3 matrix (glam Mat4 is 4x4, we need a small 3x3 for ACES)
1698struct Mat3F32([[f32; 3]; 3]);
1699impl Mat3F32 {
1700    fn mul_vec3(&self, v: Vec3) -> Vec3 {
1701        Vec3::new(
1702            self.0[0][0]*v.x + self.0[0][1]*v.y + self.0[0][2]*v.z,
1703            self.0[1][0]*v.x + self.0[1][1]*v.y + self.0[1][2]*v.z,
1704            self.0[2][0]*v.x + self.0[2][1]*v.y + self.0[2][2]*v.z,
1705        )
1706    }
1707}
1708
1709// ---- TAA Pass ----
1710
1711#[derive(Debug, Clone)]
1712pub struct TAAPassDesc {
1713    pub width: u32,
1714    pub height: u32,
1715    pub output_format: TextureFormat,
1716    pub output_resolved: ResourceId,
1717    pub input_current: ResourceId,
1718    pub input_history: ResourceId,
1719    pub input_depth: ResourceId,
1720    pub input_velocity: ResourceId,
1721    pub blend_factor: f32,
1722    pub variance_clip_gamma: f32,
1723    pub velocity_weight_scale: f32,
1724    pub jitter_sequence_len: u32,
1725    pub use_catmull_rom: bool,
1726    pub anti_flicker: bool,
1727}
1728
1729impl TAAPassDesc {
1730    pub fn default(width: u32, height: u32) -> Self {
1731        TAAPassDesc {
1732            width, height,
1733            output_format: TextureFormat::RGBA16Float,
1734            output_resolved: ResourceId(31),
1735            input_current: ResourceId(10),
1736            input_history: ResourceId(32),
1737            input_depth: ResourceId(4),
1738            input_velocity: ResourceId(3),
1739            blend_factor: 0.1,
1740            variance_clip_gamma: 1.0,
1741            velocity_weight_scale: 500.0,
1742            jitter_sequence_len: 16,
1743            use_catmull_rom: true,
1744            anti_flicker: true,
1745        }
1746    }
1747
1748    /// Halton sequence jitter offsets for TAA sub-pixel sampling
1749    pub fn halton_jitter(&self, frame: u32) -> Vec2 {
1750        let idx = (frame % self.jitter_sequence_len) + 1;
1751        let hx = halton_sequence(idx, 2);
1752        let hy = halton_sequence(idx, 3);
1753        Vec2::new(hx - 0.5, hy - 0.5)
1754    }
1755
1756    /// Catmull-Rom 5-tap filter for history sampling to reduce blurriness
1757    pub fn catmull_rom_weights(frac: Vec2) -> [f32; 5] {
1758        let f = frac;
1759        // Simplified 1D Catmull-Rom weights applied separably
1760        let w0 = |t: f32| { -0.5*t*t*t + t*t - 0.5*t };
1761        let w1 = |t: f32| { 1.5*t*t*t - 2.5*t*t + 1.0 };
1762        let w2 = |t: f32| { -1.5*t*t*t + 2.0*t*t + 0.5*t };
1763        let w3 = |t: f32| { 0.5*t*t*t - 0.5*t*t };
1764        [w0(f.x), w1(f.x), w2(f.x), w3(f.x), 0.0] // simplified
1765    }
1766
1767    /// Variance-based color clipping for ghosting prevention
1768    pub fn clip_color_to_aabb(history: Vec3, min_c: Vec3, max_c: Vec3) -> Vec3 {
1769        let center = (min_c + max_c) * 0.5;
1770        let extents = (max_c - min_c) * 0.5;
1771        let ray = history - center;
1772        let abs_ray = Vec3::new(ray.x.abs(), ray.y.abs(), ray.z.abs());
1773        let r_extents = Vec3::new(
1774            if abs_ray.x > 0.0 { extents.x / abs_ray.x } else { 1.0 },
1775            if abs_ray.y > 0.0 { extents.y / abs_ray.y } else { 1.0 },
1776            if abs_ray.z > 0.0 { extents.z / abs_ray.z } else { 1.0 },
1777        );
1778        let factor = r_extents.x.min(r_extents.y).min(r_extents.z).min(1.0);
1779        center + ray * factor
1780    }
1781
1782    /// Variance clipping with a 3x3 neighborhood sample
1783    pub fn variance_clip(history: Vec3, neighborhood: &[Vec3], gamma: f32) -> Vec3 {
1784        let n = neighborhood.len() as f32;
1785        let mut mu = Vec3::ZERO;
1786        let mut sq = Vec3::ZERO;
1787        for s in neighborhood {
1788            mu += *s;
1789            sq += *s * *s;
1790        }
1791        mu /= n;
1792        sq /= n;
1793        let sigma = (sq - mu * mu).max(Vec3::ZERO).sqrt() * gamma;
1794        let min_c = mu - sigma;
1795        let max_c = mu + sigma;
1796        Self::clip_color_to_aabb(history, min_c, max_c)
1797    }
1798}
1799
1800// ---- Depth of Field Pass ----
1801
1802#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1803pub enum DoFAlgorithm { CircleOfConfusion, BokehHexagonal, BokehOctagonal, TileMax, Scatter }
1804
1805#[derive(Debug, Clone)]
1806pub struct DepthOfFieldPassDesc {
1807    pub width: u32,
1808    pub height: u32,
1809    pub output_format: TextureFormat,
1810    pub output_dof: ResourceId,
1811    pub input_hdr: ResourceId,
1812    pub input_depth: ResourceId,
1813    pub algorithm: DoFAlgorithm,
1814    pub focus_distance: f32,
1815    pub focus_range: f32,
1816    pub bokeh_radius: f32,
1817    pub far_blur_amount: f32,
1818    pub near_blur_amount: f32,
1819    pub sample_count: u32,
1820    pub bokeh_rotation: f32,
1821}
1822
1823impl DepthOfFieldPassDesc {
1824    pub fn default(width: u32, height: u32) -> Self {
1825        DepthOfFieldPassDesc {
1826            width, height,
1827            output_format: TextureFormat::RGBA16Float,
1828            output_dof: ResourceId(33),
1829            input_hdr: ResourceId(10),
1830            input_depth: ResourceId(4),
1831            algorithm: DoFAlgorithm::CircleOfConfusion,
1832            focus_distance: 10.0,
1833            focus_range: 5.0,
1834            bokeh_radius: 8.0,
1835            far_blur_amount: 1.0,
1836            near_blur_amount: 0.5,
1837            sample_count: 16,
1838            bokeh_rotation: 0.0,
1839        }
1840    }
1841    /// Compute circle of confusion radius from depth and camera params
1842    /// f = focal length, a = aperture diameter, fd = focus distance, d = sample depth
1843    pub fn coc_from_depth(&self, depth: f32, focal_length: f32, aperture: f32) -> f32 {
1844        let fd = self.focus_distance;
1845        let numerator = aperture * focal_length * (depth - fd);
1846        let denominator = depth * (fd - focal_length);
1847        if denominator.abs() < 1e-6 { 0.0 } else { (numerator / denominator).abs() }
1848    }
1849    /// Bokeh hexagonal kernel positions for N samples
1850    pub fn hexagonal_bokeh_samples(&self) -> Vec<Vec2> {
1851        let n = self.sample_count as usize;
1852        let mut samples = Vec::with_capacity(n);
1853        let rings = ((n as f32).sqrt().ceil() as u32).max(1);
1854        let mut idx = 0;
1855        'outer: for ring in 0..=rings {
1856            if ring == 0 {
1857                samples.push(Vec2::ZERO);
1858                idx += 1;
1859                if idx >= n { break; }
1860            } else {
1861                let steps = ring * 6;
1862                for step in 0..steps {
1863                    let angle = (step as f32 / steps as f32) * std::f32::consts::TAU;
1864                    let r = ring as f32 / rings as f32;
1865                    // Hexagonal clipping: use hex distance
1866                    let x = r * angle.cos();
1867                    let y = r * angle.sin();
1868                    let hex_d = hex_distance(Vec2::new(x, y));
1869                    if hex_d <= 1.0 {
1870                        samples.push(Vec2::new(x, y) * self.bokeh_radius);
1871                        idx += 1;
1872                        if idx >= n { break 'outer; }
1873                    }
1874                }
1875            }
1876        }
1877        samples
1878    }
1879}
1880
1881// ---- Motion Blur Pass ----
1882
1883#[derive(Debug, Clone)]
1884pub struct MotionBlurPassDesc {
1885    pub width: u32,
1886    pub height: u32,
1887    pub output_format: TextureFormat,
1888    pub output_mb: ResourceId,
1889    pub input_hdr: ResourceId,
1890    pub input_velocity: ResourceId,
1891    pub input_depth: ResourceId,
1892    pub sample_count: u32,
1893    pub shutter_angle: f32,  // degrees, 180 = half-frame exposure
1894    pub max_velocity_pixels: f32,
1895    pub tile_size: u32,
1896    pub reconstruction_filter: bool,
1897}
1898
1899impl MotionBlurPassDesc {
1900    pub fn default(width: u32, height: u32) -> Self {
1901        MotionBlurPassDesc {
1902            width, height,
1903            output_format: TextureFormat::RGBA16Float,
1904            output_mb: ResourceId(34),
1905            input_hdr: ResourceId(10),
1906            input_velocity: ResourceId(3),
1907            input_depth: ResourceId(4),
1908            sample_count: 8,
1909            shutter_angle: 180.0,
1910            max_velocity_pixels: 32.0,
1911            tile_size: 16,
1912            reconstruction_filter: true,
1913        }
1914    }
1915    pub fn shutter_fraction(&self) -> f32 { self.shutter_angle / 360.0 }
1916    pub fn tile_count_x(&self) -> u32 { (self.width + self.tile_size - 1) / self.tile_size }
1917    pub fn tile_count_y(&self) -> u32 { (self.height + self.tile_size - 1) / self.tile_size }
1918    /// Sample positions along motion vector using jittered stratification
1919    pub fn sample_positions(velocity: Vec2, n: u32) -> Vec<Vec2> {
1920        let mut positions = Vec::with_capacity(n as usize);
1921        for i in 0..n {
1922            let t = (i as f32 + 0.5) / n as f32 - 0.5; // range [-0.5, 0.5]
1923            positions.push(velocity * t);
1924        }
1925        positions
1926    }
1927    /// Soft depth comparison to reduce silhouette artifacts
1928    pub fn soft_depth_compare(za: f32, zb: f32, extent: f32) -> f32 {
1929        clamp01(1.0 - (za - zb) / extent.max(1e-6))
1930    }
1931}
1932
1933// ---- Volumetric Fog Pass ----
1934
1935#[derive(Debug, Clone)]
1936pub struct VolumetricFogPassDesc {
1937    pub width: u32,
1938    pub height: u32,
1939    pub depth_slices: u32,   // number of frustum slices for 3D LUT
1940    pub output_format: TextureFormat,
1941    pub output_fog: ResourceId,
1942    pub input_depth: ResourceId,
1943    pub input_shadow_map: ResourceId,
1944    pub scattering: f32,
1945    pub absorption: f32,
1946    pub density: f32,
1947    pub phase_g: f32,   // Henyey-Greenstein anisotropy [-1, 1]
1948    pub ambient_intensity: f32,
1949    pub max_distance: f32,
1950    pub use_temporal_reprojection: bool,
1951    pub noise_scale: Vec3,
1952    pub wind_speed: Vec3,
1953}
1954
1955impl VolumetricFogPassDesc {
1956    pub fn default(width: u32, height: u32) -> Self {
1957        VolumetricFogPassDesc {
1958            width, height,
1959            depth_slices: 128,
1960            output_format: TextureFormat::RGBA16Float,
1961            output_fog: ResourceId(40),
1962            input_depth: ResourceId(4),
1963            input_shadow_map: ResourceId(100),
1964            scattering: 0.1,
1965            absorption: 0.01,
1966            density: 0.05,
1967            phase_g: 0.2,
1968            ambient_intensity: 0.1,
1969            max_distance: 100.0,
1970            use_temporal_reprojection: true,
1971            noise_scale: Vec3::new(0.1, 0.1, 0.1),
1972            wind_speed: Vec3::new(0.5, 0.0, 0.3),
1973        }
1974    }
1975    /// Henyey-Greenstein phase function
1976    pub fn henyey_greenstein(&self, cos_theta: f32) -> f32 {
1977        let g = self.phase_g;
1978        let g2 = g * g;
1979        let denom = (1.0 + g2 - 2.0 * g * cos_theta).abs().powf(1.5);
1980        (1.0 - g2) / (4.0 * std::f32::consts::PI * denom)
1981    }
1982    /// Beer-Lambert extinction
1983    pub fn extinction(&self, distance: f32) -> f32 {
1984        let sigma_t = self.scattering + self.absorption;
1985        (-sigma_t * self.density * distance).exp()
1986    }
1987    /// Cornette-Shanks phase function (more accurate than HG)
1988    pub fn cornette_shanks(&self, cos_theta: f32) -> f32 {
1989        let g = self.phase_g;
1990        let g2 = g * g;
1991        let num = 3.0 * (1.0 - g2) * (1.0 + cos_theta * cos_theta);
1992        let den = 2.0 * (2.0 + g2) * (1.0 + g2 - 2.0 * g * cos_theta).abs().powf(1.5);
1993        num / den
1994    }
1995    /// Compute froxel (frustum voxel) z-slice position using log distribution
1996    pub fn froxel_depth_from_slice(&self, slice: u32, near: f32, far: f32) -> f32 {
1997        let s = slice as f32 / self.depth_slices as f32;
1998        near * (far / near).powf(s)
1999    }
2000    pub fn froxel_volume_size(&self) -> (u32, u32, u32) {
2001        let w = (self.width + 7) / 8;
2002        let h = (self.height + 7) / 8;
2003        (w, h, self.depth_slices)
2004    }
2005}
2006
2007// ---- Particle Pass ----
2008
2009#[derive(Debug, Clone)]
2010pub struct ParticlePassDesc {
2011    pub width: u32,
2012    pub height: u32,
2013    pub output_format: TextureFormat,
2014    pub output_particles: ResourceId,
2015    pub input_depth: ResourceId,
2016    pub input_hdr: ResourceId,
2017    pub max_particles: u32,
2018    pub sort_enabled: bool,
2019    pub soft_particle_enabled: bool,
2020    pub soft_particle_extent: f32,
2021    pub use_gpu_simulation: bool,
2022    pub blend: ColorBlendAttachment,
2023}
2024
2025impl ParticlePassDesc {
2026    pub fn default(width: u32, height: u32) -> Self {
2027        ParticlePassDesc {
2028            width, height,
2029            output_format: TextureFormat::RGBA16Float,
2030            output_particles: ResourceId(41),
2031            input_depth: ResourceId(4),
2032            input_hdr: ResourceId(10),
2033            max_particles: 1_000_000,
2034            sort_enabled: true,
2035            soft_particle_enabled: true,
2036            soft_particle_extent: 1.0,
2037            use_gpu_simulation: true,
2038            blend: ColorBlendAttachment::additive(),
2039        }
2040    }
2041    pub fn particle_buffer_size_bytes(&self) -> u64 {
2042        // Each particle: pos(12) + vel(12) + color(16) + lifetime(4) + size(4) + rot(4) = 52 bytes
2043        self.max_particles as u64 * 52
2044    }
2045    pub fn sort_key_buffer_size_bytes(&self) -> u64 {
2046        // 64-bit sort key (upper 32: depth, lower 32: index)
2047        self.max_particles as u64 * 8
2048    }
2049    /// Soft particle factor based on depth difference
2050    pub fn soft_particle_factor(scene_depth: f32, particle_depth: f32, extent: f32) -> f32 {
2051        let diff = scene_depth - particle_depth;
2052        clamp01(diff / extent.max(1e-6))
2053    }
2054}
2055
2056// ---- UI Pass ----
2057
2058#[derive(Debug, Clone)]
2059pub struct UIPassDesc {
2060    pub width: u32,
2061    pub height: u32,
2062    pub output_format: TextureFormat,
2063    pub output_ui: ResourceId,
2064    pub input_scene: ResourceId,
2065    pub blend: ColorBlendAttachment,
2066    pub scissor_test_enabled: bool,
2067    pub max_draw_calls: u32,
2068    pub vertex_buffer_size: u64,
2069    pub index_buffer_size: u64,
2070    pub text_atlas_size: u32,
2071    pub max_textures: u32,
2072}
2073
2074impl UIPassDesc {
2075    pub fn default(width: u32, height: u32) -> Self {
2076        UIPassDesc {
2077            width, height,
2078            output_format: TextureFormat::RGBA8UnormSrgb,
2079            output_ui: ResourceId(50),
2080            input_scene: ResourceId(31),
2081            blend: ColorBlendAttachment::alpha_blend(),
2082            scissor_test_enabled: true,
2083            max_draw_calls: 4096,
2084            vertex_buffer_size: 4 * 1024 * 1024,
2085            index_buffer_size: 2 * 1024 * 1024,
2086            text_atlas_size: 2048,
2087            max_textures: 64,
2088        }
2089    }
2090}
2091
2092// ---- Debug Pass ----
2093
2094#[derive(Debug, Clone)]
2095pub struct DebugPassDesc {
2096    pub width: u32,
2097    pub height: u32,
2098    pub output_format: TextureFormat,
2099    pub output_debug: ResourceId,
2100    pub input_depth: ResourceId,
2101    pub draw_wireframe: bool,
2102    pub draw_normals: bool,
2103    pub draw_bounding_boxes: bool,
2104    pub draw_light_volumes: bool,
2105    pub draw_nav_mesh: bool,
2106    pub draw_physics_shapes: bool,
2107    pub draw_frustums: bool,
2108    pub line_color: Vec4,
2109    pub max_lines: u32,
2110    pub max_debug_primitives: u32,
2111}
2112
2113impl DebugPassDesc {
2114    pub fn default(width: u32, height: u32) -> Self {
2115        DebugPassDesc {
2116            width, height,
2117            output_format: TextureFormat::RGBA8Unorm,
2118            output_debug: ResourceId(51),
2119            input_depth: ResourceId(4),
2120            draw_wireframe: false,
2121            draw_normals: false,
2122            draw_bounding_boxes: true,
2123            draw_light_volumes: false,
2124            draw_nav_mesh: false,
2125            draw_physics_shapes: false,
2126            draw_frustums: false,
2127            line_color: Vec4::new(0.0, 1.0, 0.0, 1.0),
2128            max_lines: 65536,
2129            max_debug_primitives: 8192,
2130        }
2131    }
2132    pub fn line_buffer_size_bytes(&self) -> u64 {
2133        // Each line: 2 vertices * (pos: 12 + color: 16) bytes = 56 bytes
2134        self.max_lines as u64 * 56
2135    }
2136}
2137
2138// ============================================================
2139//  PASS NODE (unified)
2140// ============================================================
2141
2142#[derive(Debug, Clone)]
2143pub enum PassDesc {
2144    GBuffer(GBufferPassDesc),
2145    ShadowMap(ShadowMapPassDesc),
2146    Lighting(LightingPassDesc),
2147    SSAO(SSAOPassDesc),
2148    SSR(SSRPassDesc),
2149    Bloom(BloomPassDesc),
2150    ToneMapping(ToneMappingPassDesc),
2151    TAA(TAAPassDesc),
2152    DepthOfField(DepthOfFieldPassDesc),
2153    MotionBlur(MotionBlurPassDesc),
2154    VolumetricFog(VolumetricFogPassDesc),
2155    Particle(ParticlePassDesc),
2156    UI(UIPassDesc),
2157    Debug(DebugPassDesc),
2158}
2159
2160impl PassDesc {
2161    pub fn kind(&self) -> PassKind {
2162        match self {
2163            PassDesc::GBuffer(_) => PassKind::GBuffer,
2164            PassDesc::ShadowMap(_) => PassKind::ShadowMap,
2165            PassDesc::Lighting(_) => PassKind::Lighting,
2166            PassDesc::SSAO(_) => PassKind::SSAO,
2167            PassDesc::SSR(_) => PassKind::SSR,
2168            PassDesc::Bloom(_) => PassKind::Bloom,
2169            PassDesc::ToneMapping(_) => PassKind::ToneMapping,
2170            PassDesc::TAA(_) => PassKind::TAA,
2171            PassDesc::DepthOfField(_) => PassKind::DepthOfField,
2172            PassDesc::MotionBlur(_) => PassKind::MotionBlur,
2173            PassDesc::VolumetricFog(_) => PassKind::VolumetricFog,
2174            PassDesc::Particle(_) => PassKind::Particle,
2175            PassDesc::UI(_) => PassKind::UI,
2176            PassDesc::Debug(_) => PassKind::Debug,
2177        }
2178    }
2179}
2180
2181#[derive(Debug, Clone)]
2182pub struct PassNode {
2183    pub id: PassId,
2184    pub name: String,
2185    pub desc: PassDesc,
2186    pub reads: Vec<ResourceId>,
2187    pub writes: Vec<ResourceId>,
2188    pub barriers_before: Vec<ImageBarrier>,
2189    pub barriers_after: Vec<ImageBarrier>,
2190    pub enabled: bool,
2191    pub async_compute: bool,
2192    pub execute_order: usize,
2193    /// Visual position in the editor (layered graph layout)
2194    pub editor_pos: Vec2,
2195    pub editor_size: Vec2,
2196    pub editor_layer: i32,
2197    pub editor_color: Vec4,
2198}
2199
2200impl PassNode {
2201    pub fn new(id: PassId, name: &str, desc: PassDesc) -> Self {
2202        let color = pass_kind_color(desc.kind());
2203        PassNode {
2204            id, name: name.to_owned(), desc,
2205            reads: vec![], writes: vec![],
2206            barriers_before: vec![], barriers_after: vec![],
2207            enabled: true, async_compute: false,
2208            execute_order: 0,
2209            editor_pos: Vec2::ZERO, editor_size: Vec2::new(200.0, 80.0),
2210            editor_layer: 0, editor_color: color,
2211        }
2212    }
2213    pub fn add_read(&mut self, res: ResourceId) { if !self.reads.contains(&res) { self.reads.push(res); } }
2214    pub fn add_write(&mut self, res: ResourceId) { if !self.writes.contains(&res) { self.writes.push(res); } }
2215}
2216
2217fn pass_kind_color(kind: PassKind) -> Vec4 {
2218    match kind {
2219        PassKind::GBuffer      => Vec4::new(0.20, 0.40, 0.80, 1.0),
2220        PassKind::ShadowMap    => Vec4::new(0.10, 0.10, 0.30, 1.0),
2221        PassKind::Lighting     => Vec4::new(0.90, 0.75, 0.10, 1.0),
2222        PassKind::SSAO         => Vec4::new(0.30, 0.30, 0.30, 1.0),
2223        PassKind::SSR          => Vec4::new(0.10, 0.60, 0.90, 1.0),
2224        PassKind::Bloom        => Vec4::new(0.90, 0.50, 0.10, 1.0),
2225        PassKind::ToneMapping  => Vec4::new(0.50, 0.80, 0.50, 1.0),
2226        PassKind::TAA          => Vec4::new(0.60, 0.20, 0.80, 1.0),
2227        PassKind::DepthOfField => Vec4::new(0.80, 0.20, 0.50, 1.0),
2228        PassKind::MotionBlur   => Vec4::new(0.50, 0.50, 0.80, 1.0),
2229        PassKind::VolumetricFog=> Vec4::new(0.50, 0.70, 0.90, 1.0),
2230        PassKind::Particle     => Vec4::new(0.90, 0.60, 0.30, 1.0),
2231        PassKind::UI           => Vec4::new(0.30, 0.80, 0.30, 1.0),
2232        PassKind::Debug        => Vec4::new(0.80, 0.20, 0.20, 1.0),
2233        PassKind::Custom       => Vec4::new(0.50, 0.50, 0.50, 1.0),
2234    }
2235}
2236
2237// ============================================================
2238//  RENDER GRAPH COMPILATION — Topological Sort
2239// ============================================================
2240
2241#[derive(Debug, Clone)]
2242pub struct CompiledRenderGraph {
2243    pub sorted_passes: Vec<PassId>,
2244    pub dead_passes: Vec<PassId>,
2245    pub barriers: HashMap<PassId, PipelineBarrier>,
2246    pub resource_lifetimes: HashMap<ResourceId, (usize, usize)>,
2247    pub aliasing_groups: Vec<Vec<ResourceId>>,
2248    pub estimated_memory_bytes: u64,
2249    pub estimated_bandwidth_mb: f32,
2250}
2251
2252pub struct RenderGraphCompiler {
2253    pass_map: HashMap<PassId, PassNode>,
2254    resource_map: HashMap<ResourceId, RenderGraphResource>,
2255}
2256
2257impl RenderGraphCompiler {
2258    pub fn new() -> Self {
2259        RenderGraphCompiler { pass_map: HashMap::new(), resource_map: HashMap::new() }
2260    }
2261
2262    pub fn add_pass(&mut self, pass: PassNode) {
2263        self.pass_map.insert(pass.id, pass);
2264    }
2265
2266    pub fn add_resource(&mut self, res: RenderGraphResource) {
2267        self.resource_map.insert(res.id, res);
2268    }
2269
2270    /// Full compilation pipeline
2271    pub fn compile(&mut self, output_resources: &[ResourceId]) -> Result<CompiledRenderGraph, String> {
2272        // 1. Build dependency graph edges: pass A -> pass B if A writes something B reads
2273        let edges = self.build_dependency_edges();
2274        // 2. Detect cycles using DFS; break by removing back edges (greedy)
2275        let (acyclic_edges, removed_edges) = self.remove_cycles(&edges);
2276        if !removed_edges.is_empty() {
2277            // Log cycle removals (in a real system, would return error or warn)
2278        }
2279        // 3. Topological sort (Kahn's algorithm)
2280        let sorted = self.kahn_topological_sort(&acyclic_edges)?;
2281        // 4. Dead-pass elimination: any pass not contributing to output_resources
2282        let live_passes = self.mark_live_passes(&sorted, output_resources, &acyclic_edges);
2283        let dead_passes: Vec<PassId> = sorted.iter().filter(|p| !live_passes.contains(p)).cloned().collect();
2284        let sorted_live: Vec<PassId> = sorted.iter().filter(|p| live_passes.contains(p)).cloned().collect();
2285        // 5. Assign execution order
2286        let mut pass_index: HashMap<PassId, usize> = HashMap::new();
2287        for (i, pid) in sorted_live.iter().enumerate() { pass_index.insert(*pid, i); }
2288        // 6. Resource lifetime analysis
2289        let resource_lifetimes = self.compute_resource_lifetimes(&sorted_live, &pass_index);
2290        // 7. Update resource first/last use
2291        let mut rm = self.resource_map.clone();
2292        for (rid, (first, last)) in &resource_lifetimes {
2293            if let Some(res) = rm.get_mut(rid) {
2294                res.first_use = *first;
2295                res.last_use = *last;
2296            }
2297        }
2298        // 8. Aliasing analysis: find resources whose lifetimes don't overlap
2299        let aliasing_groups = self.compute_aliasing_groups(&rm, &resource_lifetimes);
2300        // 9. Barrier insertion: for each resource, insert image layout transitions
2301        let barriers = self.insert_barriers(&sorted_live, &rm);
2302        // 10. Estimate memory usage
2303        let estimated_memory_bytes = self.estimate_memory_usage(&rm, &aliasing_groups);
2304        // 11. Estimate bandwidth
2305        let estimated_bandwidth_mb = self.estimate_bandwidth_mb(&sorted_live);
2306
2307        Ok(CompiledRenderGraph {
2308            sorted_passes: sorted_live,
2309            dead_passes,
2310            barriers,
2311            resource_lifetimes,
2312            aliasing_groups,
2313            estimated_memory_bytes,
2314            estimated_bandwidth_mb,
2315        })
2316    }
2317
2318    fn build_dependency_edges(&self) -> HashMap<PassId, Vec<PassId>> {
2319        // For each resource, find: which passes write it (producers) and which passes read it (consumers)
2320        let mut resource_writers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
2321        let mut resource_readers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
2322        for (pid, pass) in &self.pass_map {
2323            for rid in &pass.writes { resource_writers.entry(*rid).or_default().push(*pid); }
2324            for rid in &pass.reads  { resource_readers.entry(*rid).or_default().push(*pid); }
2325        }
2326        // Build edges: writer -> reader
2327        let mut edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
2328        for pid in self.pass_map.keys() { edges.insert(*pid, vec![]); }
2329        for (rid, writers) in &resource_writers {
2330            if let Some(readers) = resource_readers.get(rid) {
2331                for w in writers {
2332                    for r in readers {
2333                        if w != r {
2334                            edges.entry(*w).or_default().push(*r);
2335                        }
2336                    }
2337                }
2338            }
2339        }
2340        // Deduplicate edges
2341        for v in edges.values_mut() { v.sort_unstable_by_key(|p| p.0); v.dedup(); }
2342        edges
2343    }
2344
2345    /// Cycle removal using DFS-based back-edge detection; removes back edges
2346    fn remove_cycles(&self, edges: &HashMap<PassId, Vec<PassId>>) -> (HashMap<PassId, Vec<PassId>>, Vec<(PassId, PassId)>) {
2347        let mut visited: HashSet<PassId> = HashSet::new();
2348        let mut in_stack: HashSet<PassId> = HashSet::new();
2349        let mut removed: Vec<(PassId, PassId)> = Vec::new();
2350        let mut result = edges.clone();
2351        let keys: Vec<PassId> = edges.keys().cloned().collect();
2352        fn dfs(node: PassId, edges: &mut HashMap<PassId, Vec<PassId>>, visited: &mut HashSet<PassId>, in_stack: &mut HashSet<PassId>, removed: &mut Vec<(PassId, PassId)>) {
2353            visited.insert(node);
2354            in_stack.insert(node);
2355            let neighbors: Vec<PassId> = edges.get(&node).cloned().unwrap_or_default();
2356            for nbr in neighbors {
2357                if in_stack.contains(&nbr) {
2358                    // back edge: remove it
2359                    if let Some(v) = edges.get_mut(&node) { v.retain(|x| *x != nbr); }
2360                    removed.push((node, nbr));
2361                } else if !visited.contains(&nbr) {
2362                    dfs(nbr, edges, visited, in_stack, removed);
2363                }
2364            }
2365            in_stack.remove(&node);
2366        }
2367        for key in keys {
2368            if !visited.contains(&key) {
2369                dfs(key, &mut result, &mut visited, &mut in_stack, &mut removed);
2370            }
2371        }
2372        (result, removed)
2373    }
2374
2375    /// Kahn's algorithm for topological sort
2376    fn kahn_topological_sort(&self, edges: &HashMap<PassId, Vec<PassId>>) -> Result<Vec<PassId>, String> {
2377        let mut in_degree: HashMap<PassId, usize> = HashMap::new();
2378        for pid in edges.keys() { in_degree.insert(*pid, 0); }
2379        for succs in edges.values() {
2380            for s in succs {
2381                *in_degree.entry(*s).or_insert(0) += 1;
2382            }
2383        }
2384        let mut queue: VecDeque<PassId> = in_degree.iter().filter(|(_, &d)| d == 0).map(|(&p, _)| p).collect();
2385        // Deterministic order: sort by PassId
2386        let mut queue_vec: Vec<PassId> = queue.drain(..).collect();
2387        queue_vec.sort_unstable_by_key(|p| p.0);
2388        queue.extend(queue_vec);
2389        let mut sorted: Vec<PassId> = Vec::new();
2390        while let Some(node) = queue.pop_front() {
2391            sorted.push(node);
2392            if let Some(succs) = edges.get(&node) {
2393                let mut new_zeros: Vec<PassId> = Vec::new();
2394                for s in succs {
2395                    let deg = in_degree.entry(*s).or_insert(0);
2396                    *deg = deg.saturating_sub(1);
2397                    if *deg == 0 { new_zeros.push(*s); }
2398                }
2399                new_zeros.sort_unstable_by_key(|p| p.0);
2400                for z in new_zeros { queue.push_back(z); }
2401            }
2402        }
2403        if sorted.len() != self.pass_map.len() {
2404            Err(format!("Topological sort failed: cycle detected ({} of {} passes sorted)", sorted.len(), self.pass_map.len()))
2405        } else {
2406            Ok(sorted)
2407        }
2408    }
2409
2410    /// Mark all passes that contribute to the given output resources (reverse BFS)
2411    fn mark_live_passes(&self, sorted: &[PassId], outputs: &[ResourceId], edges: &HashMap<PassId, Vec<PassId>>) -> HashSet<PassId> {
2412        // Build reverse edges (successor -> predecessor)
2413        let mut rev_edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
2414        for (src, dsts) in edges {
2415            for dst in dsts {
2416                rev_edges.entry(*dst).or_default().push(*src);
2417            }
2418        }
2419        // Find passes that write any output resource
2420        let mut live: HashSet<PassId> = HashSet::new();
2421        let mut queue: VecDeque<PassId> = VecDeque::new();
2422        for pid in sorted {
2423            if let Some(pass) = self.pass_map.get(pid) {
2424                for out in outputs {
2425                    if pass.writes.contains(out) {
2426                        if live.insert(*pid) { queue.push_back(*pid); }
2427                    }
2428                }
2429            }
2430        }
2431        while let Some(pid) = queue.pop_front() {
2432            if let Some(preds) = rev_edges.get(&pid) {
2433                for pred in preds {
2434                    if live.insert(*pred) { queue.push_back(*pred); }
2435                }
2436            }
2437        }
2438        live
2439    }
2440
2441    fn compute_resource_lifetimes(&self, sorted: &[PassId], pass_index: &HashMap<PassId, usize>) -> HashMap<ResourceId, (usize, usize)> {
2442        let mut lifetimes: HashMap<ResourceId, (usize, usize)> = HashMap::new();
2443        for (pid, pass) in &self.pass_map {
2444            let idx = match pass_index.get(pid) { Some(&i) => i, None => continue };
2445            for rid in pass.reads.iter().chain(pass.writes.iter()) {
2446                let entry = lifetimes.entry(*rid).or_insert((usize::MAX, 0));
2447                if idx < entry.0 { entry.0 = idx; }
2448                if idx > entry.1 { entry.1 = idx; }
2449            }
2450        }
2451        lifetimes
2452    }
2453
2454    fn compute_aliasing_groups(&self, rm: &HashMap<ResourceId, RenderGraphResource>, lifetimes: &HashMap<ResourceId, (usize, usize)>) -> Vec<Vec<ResourceId>> {
2455        // Greedy interval-graph coloring: assign resources to the same physical slot
2456        // if they don't overlap in lifetime. Sort by first_use for greedy ordering.
2457        let mut transient_res: Vec<ResourceId> = rm.values()
2458            .filter(|r| r.lifetime == ResourceLifetime::Transient)
2459            .map(|r| r.id)
2460            .collect();
2461        transient_res.sort_by_key(|id| lifetimes.get(id).map(|l| l.0).unwrap_or(usize::MAX));
2462
2463        let mut groups: Vec<(Vec<ResourceId>, usize)> = Vec::new(); // (ids, max_end)
2464        for rid in &transient_res {
2465            let (start, end) = lifetimes.get(rid).copied().unwrap_or((0, 0));
2466            let res = rm.get(rid).unwrap();
2467            // Find an existing group whose last resource doesn't overlap
2468            let mut placed = false;
2469            for (group_ids, group_end) in &mut groups {
2470                if *group_end < start {
2471                    // Check memory compatibility with the first member
2472                    let first = group_ids.first().and_then(|id| rm.get(id)).unwrap();
2473                    if res.can_alias_with(first) {
2474                        group_ids.push(*rid);
2475                        if end > *group_end { *group_end = end; }
2476                        placed = true;
2477                        break;
2478                    }
2479                }
2480            }
2481            if !placed {
2482                groups.push((vec![*rid], end));
2483            }
2484        }
2485        groups.into_iter().map(|(ids, _)| ids).collect()
2486    }
2487
2488    fn insert_barriers(&self, sorted: &[PassId], rm: &HashMap<ResourceId, RenderGraphResource>) -> HashMap<PassId, PipelineBarrier> {
2489        let mut result: HashMap<PassId, PipelineBarrier> = HashMap::new();
2490        // Track the current layout of each resource as we walk the sorted pass list
2491        let mut current_layouts: HashMap<ResourceId, ImageLayout> = HashMap::new();
2492        for res in rm.values() {
2493            current_layouts.insert(res.id, res.current_layout);
2494        }
2495        for pid in sorted {
2496            let pass = match self.pass_map.get(pid) { Some(p) => p, None => continue };
2497            let mut barrier = PipelineBarrier::new();
2498            // Resources read by this pass: ensure they're in ShaderReadOnlyOptimal
2499            for rid in &pass.reads {
2500                let res = match rm.get(rid) { Some(r) => r, None => continue };
2501                if !res.is_texture() { continue; }
2502                let desc = match res.texture_desc() { Some(d) => d, None => continue };
2503                let fi = format_info(desc.format);
2504                let required_layout = if fi.is_depth || fi.is_stencil {
2505                    ImageLayout::DepthStencilReadOnlyOptimal
2506                } else {
2507                    ImageLayout::ShaderReadOnlyOptimal
2508                };
2509                let old_layout = *current_layouts.get(rid).unwrap_or(&ImageLayout::Undefined);
2510                if old_layout != required_layout {
2511                    barrier.image_barriers.push(ImageBarrier::layout_transition(*rid, old_layout, required_layout));
2512                    current_layouts.insert(*rid, required_layout);
2513                }
2514            }
2515            // Resources written: ensure correct attachment layouts
2516            for rid in &pass.writes {
2517                let res = match rm.get(rid) { Some(r) => r, None => continue };
2518                if !res.is_texture() { continue; }
2519                let desc = match res.texture_desc() { Some(d) => d, None => continue };
2520                let fi = format_info(desc.format);
2521                let required_layout = if fi.is_depth || fi.is_stencil {
2522                    ImageLayout::DepthStencilAttachmentOptimal
2523                } else {
2524                    ImageLayout::ColorAttachmentOptimal
2525                };
2526                let old_layout = *current_layouts.get(rid).unwrap_or(&ImageLayout::Undefined);
2527                if old_layout != required_layout {
2528                    barrier.image_barriers.push(ImageBarrier::layout_transition(*rid, old_layout, required_layout));
2529                    current_layouts.insert(*rid, required_layout);
2530                }
2531            }
2532            result.insert(*pid, barrier);
2533        }
2534        result
2535    }
2536
2537    fn estimate_memory_usage(&self, rm: &HashMap<ResourceId, RenderGraphResource>, aliasing_groups: &[Vec<ResourceId>]) -> u64 {
2538        let mut total: u64 = 0;
2539        // Non-transient resources must all live
2540        for res in rm.values() {
2541            if res.lifetime != ResourceLifetime::Transient {
2542                total += match &res.desc {
2543                    ResourceDesc::Texture(t) => t.size_bytes(),
2544                    ResourceDesc::Buffer(b) => b.size,
2545                };
2546            }
2547        }
2548        // Transient: only pay for the max in each aliasing group
2549        for group in aliasing_groups {
2550            let max_size = group.iter().filter_map(|id| rm.get(id)).map(|r| match &r.desc {
2551                ResourceDesc::Texture(t) => t.size_bytes(),
2552                ResourceDesc::Buffer(b) => b.size,
2553            }).max().unwrap_or(0);
2554            total += max_size;
2555        }
2556        total
2557    }
2558
2559    fn estimate_bandwidth_mb(&self, sorted: &[PassId]) -> f32 {
2560        let mut bw: f32 = 0.0;
2561        for pid in sorted {
2562            if let Some(pass) = self.pass_map.get(pid) {
2563                match &pass.desc {
2564                    PassDesc::GBuffer(d) => bw += d.estimate_write_bandwidth_mb(),
2565                    PassDesc::Lighting(d) => {
2566                        let pixels = (d.width * d.height) as f32;
2567                        let bpp = format_info(d.output_format).bytes_per_pixel();
2568                        bw += bpp * pixels / (1024.0 * 1024.0);
2569                        // reads: 5 gbuffer textures
2570                        let gbuf_bytes: f32 = (4.0 + 8.0 + 4.0 + 4.0 + 4.0) * pixels; // approx
2571                        bw += gbuf_bytes / (1024.0 * 1024.0);
2572                    }
2573                    PassDesc::SSAO(d) => {
2574                        let ew = d.effective_width() as f32;
2575                        let eh = d.effective_height() as f32;
2576                        bw += format_info(d.output_format).bytes_per_pixel() * ew * eh / (1024.0*1024.0);
2577                    }
2578                    PassDesc::Bloom(d) => {
2579                        // Bloom is bandwidth-heavy: sum over mip chain (down + up)
2580                        let mut bloom_bw = 0.0f32;
2581                        for mip in 0..d.mip_levels {
2582                            let (w, h) = d.mip_size(mip);
2583                            bloom_bw += format_info(d.output_format).bytes_per_pixel() * (w * h) as f32;
2584                        }
2585                        bw += bloom_bw * 2.0 / (1024.0 * 1024.0); // read + write each mip
2586                    }
2587                    _ => {
2588                        // Generic: assume 1 read + 1 write per pass at full resolution
2589                        if let Some(pass) = self.pass_map.get(pid) {
2590                            let r = pass.reads.len() as f32;
2591                            let w_c = pass.writes.len() as f32;
2592                            bw += (r + w_c) * 4.0 * 1920.0 * 1080.0 / (1024.0 * 1024.0);
2593                        }
2594                    }
2595                }
2596            }
2597        }
2598        bw
2599    }
2600}
2601
2602// ============================================================
2603//  RENDER GRAPH VALIDATION
2604// ============================================================
2605
2606#[derive(Debug, Clone)]
2607pub enum ValidationError {
2608    MissingResource { pass: PassId, resource: ResourceId },
2609    WrittenWithoutRead { resource: ResourceId },
2610    IncompatibleFormats { pass: PassId, resource: ResourceId, expected: TextureFormat, actual: TextureFormat },
2611    CyclicDependency { passes: Vec<PassId> },
2612    ResourceSizeMismatch { resource: ResourceId, expected: (u32, u32), actual: (u32, u32) },
2613    TooManyCascades { pass: PassId, count: u32 },
2614    InvalidBlendState { pass: PassId, attachment_index: u32 },
2615    DuplicatePassId(PassId),
2616    DuplicateResourceId(ResourceId),
2617}
2618
2619#[derive(Debug)]
2620pub struct ValidationReport {
2621    pub errors: Vec<ValidationError>,
2622    pub warnings: Vec<String>,
2623}
2624
2625impl ValidationReport {
2626    pub fn new() -> Self { ValidationReport { errors: vec![], warnings: vec![] } }
2627    pub fn is_valid(&self) -> bool { self.errors.is_empty() }
2628    pub fn error(&mut self, e: ValidationError) { self.errors.push(e); }
2629    pub fn warn(&mut self, s: &str) { self.warnings.push(s.to_owned()); }
2630}
2631
2632pub struct RenderGraphValidator<'a> {
2633    pass_map: &'a HashMap<PassId, PassNode>,
2634    resource_map: &'a HashMap<ResourceId, RenderGraphResource>,
2635}
2636
2637impl<'a> RenderGraphValidator<'a> {
2638    pub fn new(pass_map: &'a HashMap<PassId, PassNode>, resource_map: &'a HashMap<ResourceId, RenderGraphResource>) -> Self {
2639        RenderGraphValidator { pass_map, resource_map }
2640    }
2641
2642    pub fn validate(&self) -> ValidationReport {
2643        let mut report = ValidationReport::new();
2644        self.check_duplicate_ids(&mut report);
2645        self.check_missing_resources(&mut report);
2646        self.check_format_compatibility(&mut report);
2647        self.check_cascade_limits(&mut report);
2648        self.check_blend_state(&mut report);
2649        self.check_unread_writes(&mut report);
2650        report
2651    }
2652
2653    fn check_duplicate_ids(&self, report: &mut ValidationReport) {
2654        let mut seen_pass: HashSet<PassId> = HashSet::new();
2655        for pid in self.pass_map.keys() {
2656            if !seen_pass.insert(*pid) { report.error(ValidationError::DuplicatePassId(*pid)); }
2657        }
2658        let mut seen_res: HashSet<ResourceId> = HashSet::new();
2659        for rid in self.resource_map.keys() {
2660            if !seen_res.insert(*rid) { report.error(ValidationError::DuplicateResourceId(*rid)); }
2661        }
2662    }
2663
2664    fn check_missing_resources(&self, report: &mut ValidationReport) {
2665        for (pid, pass) in self.pass_map {
2666            for rid in pass.reads.iter().chain(pass.writes.iter()) {
2667                if !self.resource_map.contains_key(rid) {
2668                    report.error(ValidationError::MissingResource { pass: *pid, resource: *rid });
2669                }
2670            }
2671        }
2672    }
2673
2674    fn check_format_compatibility(&self, report: &mut ValidationReport) {
2675        for (pid, pass) in self.pass_map {
2676            match &pass.desc {
2677                PassDesc::Lighting(d) => {
2678                    if let Some(res) = self.resource_map.get(&d.input_depth) {
2679                        if let Some(t) = res.texture_desc() {
2680                            let fi = format_info(t.format);
2681                            if !fi.is_depth {
2682                                report.error(ValidationError::IncompatibleFormats {
2683                                    pass: *pid, resource: d.input_depth,
2684                                    expected: TextureFormat::Depth24UnormStencil8,
2685                                    actual: t.format,
2686                                });
2687                            }
2688                        }
2689                    }
2690                }
2691                PassDesc::SSAO(d) => {
2692                    if let Some(res) = self.resource_map.get(&d.input_depth) {
2693                        if let Some(t) = res.texture_desc() {
2694                            let fi = format_info(t.format);
2695                            if !fi.is_depth {
2696                                report.error(ValidationError::IncompatibleFormats {
2697                                    pass: *pid, resource: d.input_depth,
2698                                    expected: TextureFormat::Depth32Float,
2699                                    actual: t.format,
2700                                });
2701                            }
2702                        }
2703                    }
2704                }
2705                _ => {}
2706            }
2707        }
2708    }
2709
2710    fn check_cascade_limits(&self, report: &mut ValidationReport) {
2711        for (pid, pass) in self.pass_map {
2712            if let PassDesc::ShadowMap(d) = &pass.desc {
2713                if d.cascade_count > 4 {
2714                    report.error(ValidationError::TooManyCascades { pass: *pid, count: d.cascade_count });
2715                }
2716            }
2717        }
2718    }
2719
2720    fn check_blend_state(&self, report: &mut ValidationReport) {
2721        // Check that blend state attachment count matches the number of color outputs
2722        for (pid, pass) in self.pass_map {
2723            // Only check passes with explicit color blend states
2724            // For now, just do a simple sanity check on blend factors
2725            if let PassDesc::Particle(d) = &pass.desc {
2726                let b = &d.blend;
2727                if b.blend_enable {
2728                    let src_zero = b.src_color_blend_factor == BlendFactor::Zero && b.dst_color_blend_factor == BlendFactor::Zero;
2729                    if src_zero {
2730                        report.error(ValidationError::InvalidBlendState { pass: *pid, attachment_index: 0 });
2731                    }
2732                }
2733            }
2734        }
2735    }
2736
2737    fn check_unread_writes(&self, report: &mut ValidationReport) {
2738        let mut all_reads: HashSet<ResourceId> = HashSet::new();
2739        for pass in self.pass_map.values() {
2740            for rid in &pass.reads { all_reads.insert(*rid); }
2741        }
2742        for pass in self.pass_map.values() {
2743            for rid in &pass.writes {
2744                if !all_reads.contains(rid) {
2745                    report.warn(&format!("Resource {:?} is written but never read (pass: {:?})", rid, pass.id));
2746                }
2747            }
2748        }
2749    }
2750}
2751
2752// ============================================================
2753//  GRAPH VISUALIZATION — Sugiyama Algorithm
2754// ============================================================
2755// Steps: 1) Cycle removal (already done)
2756//         2) Layer assignment (longest path)
2757//         3) Crossing minimization (barycenter heuristic)
2758//         4) Position assignment (Brandes-Köpf)
2759
2760#[derive(Debug, Clone)]
2761pub struct GraphLayout {
2762    pub node_positions: HashMap<PassId, Vec2>,
2763    pub node_sizes: HashMap<PassId, Vec2>,
2764    pub edge_paths: HashMap<(PassId, PassId), Vec<Vec2>>,
2765    pub layer_assignment: HashMap<PassId, i32>,
2766    pub nodes_per_layer: BTreeMap<i32, Vec<PassId>>,
2767    pub total_bounds: (Vec2, Vec2), // min, max
2768}
2769
2770pub struct SugiyamaLayout {
2771    pub horizontal_gap: f32,
2772    pub vertical_gap: f32,
2773    pub node_width: f32,
2774    pub node_height: f32,
2775    pub crossing_minimization_rounds: u32,
2776}
2777
2778impl SugiyamaLayout {
2779    pub fn default() -> Self {
2780        SugiyamaLayout {
2781            horizontal_gap: 60.0,
2782            vertical_gap: 40.0,
2783            node_width: 200.0,
2784            node_height: 80.0,
2785            crossing_minimization_rounds: 24,
2786        }
2787    }
2788
2789    /// Full Sugiyama layout pipeline
2790    pub fn layout(&self, passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> GraphLayout {
2791        // Step 1: Layer assignment via longest-path algorithm
2792        let layers = self.assign_layers(passes, edges);
2793        // Step 2: Build per-layer node lists
2794        let mut nodes_per_layer: BTreeMap<i32, Vec<PassId>> = BTreeMap::new();
2795        for (pid, &layer) in &layers {
2796            nodes_per_layer.entry(layer).or_default().push(*pid);
2797        }
2798        // Sort each layer by initial ordering (pass id for determinism)
2799        for v in nodes_per_layer.values_mut() {
2800            v.sort_by_key(|p| p.0);
2801        }
2802        // Step 3: Crossing minimization using barycenter method
2803        self.minimize_crossings(edges, &layers, &mut nodes_per_layer);
2804        // Step 4: Position assignment
2805        let positions = self.assign_positions(&layers, &nodes_per_layer);
2806        // Step 5: Compute edge routing (simple splines / polylines)
2807        let paths = self.route_edges(passes, edges, &positions);
2808        // Step 6: Compute bounds
2809        let mut min_pos = Vec2::splat(f32::MAX);
2810        let mut max_pos = Vec2::splat(f32::MIN);
2811        for &pos in positions.values() {
2812            min_pos = min_pos.min(pos);
2813            max_pos = max_pos.max(pos + Vec2::new(self.node_width, self.node_height));
2814        }
2815        let mut sizes: HashMap<PassId, Vec2> = HashMap::new();
2816        for pid in passes { sizes.insert(*pid, Vec2::new(self.node_width, self.node_height)); }
2817        GraphLayout {
2818            node_positions: positions,
2819            node_sizes: sizes,
2820            edge_paths: paths,
2821            layer_assignment: layers,
2822            nodes_per_layer,
2823            total_bounds: (min_pos, max_pos),
2824        }
2825    }
2826
2827    /// Longest-path layer assignment: l(v) = max(l(u)+1 for all predecessors u of v)
2828    fn assign_layers(&self, passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> HashMap<PassId, i32> {
2829        // Build in-degree and predecessor maps
2830        let mut pred: HashMap<PassId, Vec<PassId>> = HashMap::new();
2831        for pid in passes { pred.insert(*pid, vec![]); }
2832        for (src, dsts) in edges {
2833            for dst in dsts {
2834                pred.entry(*dst).or_default().push(*src);
2835            }
2836        }
2837        let mut layers: HashMap<PassId, i32> = HashMap::new();
2838        // Process in topological order (passes array is already sorted)
2839        for pid in passes {
2840            let preds = pred.get(pid).cloned().unwrap_or_default();
2841            let layer = if preds.is_empty() {
2842                0
2843            } else {
2844                preds.iter().filter_map(|p| layers.get(p)).max().copied().unwrap_or(0) + 1
2845            };
2846            layers.insert(*pid, layer);
2847        }
2848        layers
2849    }
2850
2851    /// Barycenter crossing minimization: sweep top-down then bottom-up, repeat
2852    fn minimize_crossings(
2853        &self,
2854        edges: &HashMap<PassId, Vec<PassId>>,
2855        layers: &HashMap<PassId, i32>,
2856        nodes_per_layer: &mut BTreeMap<i32, Vec<PassId>>,
2857    ) {
2858        // Build reverse edge map
2859        let mut rev_edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
2860        for (src, dsts) in edges {
2861            for dst in dsts {
2862                rev_edges.entry(*dst).or_default().push(*src);
2863            }
2864        }
2865        let max_layer = *layers.values().max().unwrap_or(&0);
2866        for _round in 0..self.crossing_minimization_rounds {
2867            // Top-down sweep: order each layer by average position of predecessors
2868            for layer_idx in 1..=max_layer {
2869                let prev_layer_idx = layer_idx - 1;
2870                let prev_positions: HashMap<PassId, usize> = nodes_per_layer
2871                    .get(&prev_layer_idx)
2872                    .map(|v| v.iter().enumerate().map(|(i, p)| (*p, i)).collect())
2873                    .unwrap_or_default();
2874                if let Some(nodes) = nodes_per_layer.get_mut(&layer_idx) {
2875                    nodes.sort_by(|a, b| {
2876                        let ba = barycenter(*a, &rev_edges, &prev_positions);
2877                        let bb = barycenter(*b, &rev_edges, &prev_positions);
2878                        ba.partial_cmp(&bb).unwrap_or(std::cmp::Ordering::Equal)
2879                    });
2880                }
2881            }
2882            // Bottom-up sweep
2883            for layer_idx in (0..max_layer).rev() {
2884                let next_layer_idx = layer_idx + 1;
2885                let next_positions: HashMap<PassId, usize> = nodes_per_layer
2886                    .get(&next_layer_idx)
2887                    .map(|v| v.iter().enumerate().map(|(i, p)| (*p, i)).collect())
2888                    .unwrap_or_default();
2889                if let Some(nodes) = nodes_per_layer.get_mut(&layer_idx) {
2890                    nodes.sort_by(|a, b| {
2891                        let ba = barycenter(*a, edges, &next_positions);
2892                        let bb = barycenter(*b, edges, &next_positions);
2893                        ba.partial_cmp(&bb).unwrap_or(std::cmp::Ordering::Equal)
2894                    });
2895                }
2896            }
2897        }
2898    }
2899
2900    /// Assign 2D pixel positions based on layer + in-layer order
2901    fn assign_positions(&self, layers: &HashMap<PassId, i32>, nodes_per_layer: &BTreeMap<i32, Vec<PassId>>) -> HashMap<PassId, Vec2> {
2902        let mut positions: HashMap<PassId, Vec2> = HashMap::new();
2903        for (layer_idx, nodes) in nodes_per_layer {
2904            let x = *layer_idx as f32 * (self.node_width + self.horizontal_gap);
2905            let total_height = nodes.len() as f32 * (self.node_height + self.vertical_gap);
2906            let start_y = -total_height / 2.0; // center around origin
2907            for (i, pid) in nodes.iter().enumerate() {
2908                let y = start_y + i as f32 * (self.node_height + self.vertical_gap);
2909                positions.insert(*pid, Vec2::new(x, y));
2910            }
2911        }
2912        positions
2913    }
2914
2915    /// Route edges as cubic bezier polylines between node ports
2916    fn route_edges(
2917        &self,
2918        passes: &[PassId],
2919        edges: &HashMap<PassId, Vec<PassId>>,
2920        positions: &HashMap<PassId, Vec2>,
2921    ) -> HashMap<(PassId, PassId), Vec<Vec2>> {
2922        let mut paths = HashMap::new();
2923        let half_w = self.node_width * 0.5;
2924        let half_h = self.node_height * 0.5;
2925        for (src, dsts) in edges {
2926            for dst in dsts {
2927                let src_pos = match positions.get(src) { Some(p) => *p, None => continue };
2928                let dst_pos = match positions.get(dst) { Some(p) => *p, None => continue };
2929                // Source port: right-center of source node
2930                let p0 = src_pos + Vec2::new(self.node_width, half_h);
2931                // Dest port: left-center of destination node
2932                let p3 = dst_pos + Vec2::new(0.0, half_h);
2933                let ctrl_dist = (p3.x - p0.x).abs() * 0.5;
2934                let p1 = p0 + Vec2::new(ctrl_dist, 0.0);
2935                let p2 = p3 - Vec2::new(ctrl_dist, 0.0);
2936                // Tessellate cubic bezier into polyline
2937                let points = tessellate_cubic_bezier(p0, p1, p2, p3, 16);
2938                paths.insert((*src, *dst), points);
2939            }
2940        }
2941        paths
2942    }
2943}
2944
2945fn barycenter(node: PassId, edges: &HashMap<PassId, Vec<PassId>>, neighbor_positions: &HashMap<PassId, usize>) -> f32 {
2946    let neighbors: Vec<PassId> = edges.get(&node).cloned().unwrap_or_default();
2947    if neighbors.is_empty() { return 0.0; }
2948    let sum: f32 = neighbors.iter().filter_map(|n| neighbor_positions.get(n)).map(|&i| i as f32).sum();
2949    sum / neighbors.len() as f32
2950}
2951
2952fn tessellate_cubic_bezier(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, steps: u32) -> Vec<Vec2> {
2953    let mut pts = Vec::with_capacity(steps as usize + 1);
2954    for i in 0..=steps {
2955        let t = i as f32 / steps as f32;
2956        let mt = 1.0 - t;
2957        let pos = p0 * (mt*mt*mt) + p1 * (3.0*mt*mt*t) + p2 * (3.0*mt*t*t) + p3 * (t*t*t);
2958        pts.push(pos);
2959    }
2960    pts
2961}
2962
2963// ============================================================
2964//  PASS STATISTICS AND TIMING QUERIES
2965// ============================================================
2966
2967#[derive(Debug, Clone)]
2968pub struct PassStatistics {
2969    pub pass_id: PassId,
2970    pub gpu_time_ms: f32,
2971    pub cpu_time_ms: f32,
2972    pub draw_calls: u32,
2973    pub triangle_count: u64,
2974    pub primitive_overdraw_estimate: f32,    // average number of fragment shader invocations per pixel
2975    pub bandwidth_read_mb: f32,
2976    pub bandwidth_write_mb: f32,
2977    pub texture_cache_miss_rate: f32,        // 0..1 estimate
2978    pub barrier_count: u32,
2979    pub render_target_clears: u32,
2980}
2981
2982impl PassStatistics {
2983    pub fn new(pass_id: PassId) -> Self {
2984        PassStatistics {
2985            pass_id, gpu_time_ms: 0.0, cpu_time_ms: 0.0, draw_calls: 0, triangle_count: 0,
2986            primitive_overdraw_estimate: 1.0, bandwidth_read_mb: 0.0, bandwidth_write_mb: 0.0,
2987            texture_cache_miss_rate: 0.0, barrier_count: 0, render_target_clears: 0,
2988        }
2989    }
2990    pub fn total_bandwidth_mb(&self) -> f32 { self.bandwidth_read_mb + self.bandwidth_write_mb }
2991    pub fn pixels_per_ms(&self, width: u32, height: u32) -> f32 {
2992        if self.gpu_time_ms < 1e-6 { return 0.0; }
2993        (width * height) as f32 / self.gpu_time_ms
2994    }
2995}
2996
2997#[derive(Debug, Clone)]
2998pub struct FrameStatistics {
2999    pub pass_stats: HashMap<PassId, PassStatistics>,
3000    pub total_gpu_time_ms: f32,
3001    pub total_cpu_time_ms: f32,
3002    pub total_draw_calls: u32,
3003    pub total_triangles: u64,
3004    pub total_bandwidth_mb: f32,
3005    pub frame_time_ms: f32,
3006    pub fps: f32,
3007}
3008
3009impl FrameStatistics {
3010    pub fn new() -> Self {
3011        FrameStatistics {
3012            pass_stats: HashMap::new(),
3013            total_gpu_time_ms: 0.0, total_cpu_time_ms: 0.0,
3014            total_draw_calls: 0, total_triangles: 0, total_bandwidth_mb: 0.0,
3015            frame_time_ms: 0.0, fps: 0.0,
3016        }
3017    }
3018    pub fn aggregate(&mut self) {
3019        self.total_gpu_time_ms = self.pass_stats.values().map(|s| s.gpu_time_ms).sum();
3020        self.total_cpu_time_ms = self.pass_stats.values().map(|s| s.cpu_time_ms).sum();
3021        self.total_draw_calls  = self.pass_stats.values().map(|s| s.draw_calls).sum();
3022        self.total_triangles   = self.pass_stats.values().map(|s| s.triangle_count).sum();
3023        self.total_bandwidth_mb = self.pass_stats.values().map(|s| s.total_bandwidth_mb()).sum();
3024        if self.frame_time_ms > 1e-6 { self.fps = 1000.0 / self.frame_time_ms; }
3025    }
3026    pub fn bottleneck_pass(&self) -> Option<PassId> {
3027        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)
3028    }
3029    pub fn bandwidth_budget_used(&self, budget_gb_s: f32, frame_time_ms: f32) -> f32 {
3030        let available_mb = budget_gb_s * 1024.0 * frame_time_ms / 1000.0;
3031        if available_mb < 1e-6 { 0.0 } else { self.total_bandwidth_mb / available_mb }
3032    }
3033}
3034
3035// ============================================================
3036//  OVERDRAW ESTIMATION
3037// ============================================================
3038
3039pub struct OverdrawEstimator {
3040    pub tile_size: u32,
3041    pub max_depth: u32,
3042}
3043
3044impl OverdrawEstimator {
3045    pub fn new(tile_size: u32) -> Self { OverdrawEstimator { tile_size, max_depth: 32 } }
3046
3047    /// Rasterize a screen-space AABB and accumulate overdraw counts (CPU-side simulation)
3048    pub fn estimate_triangle_overdraw(triangles: &[(Vec2, Vec2, Vec2)], width: u32, height: u32, tile_size: u32) -> f32 {
3049        let tw = ((width + tile_size - 1) / tile_size) as usize;
3050        let th = ((height + tile_size - 1) / tile_size) as usize;
3051        let mut tile_counts = vec![0u32; tw * th];
3052        for (a, b, c) in triangles {
3053            // Compute screen-space bounding box in tiles
3054            let min_x = a.x.min(b.x).min(c.x).max(0.0) as u32;
3055            let min_y = a.y.min(b.y).min(c.y).max(0.0) as u32;
3056            let max_x = (a.x.max(b.x).max(c.x) as u32).min(width - 1);
3057            let max_y = (a.y.max(b.y).max(c.y) as u32).min(height - 1);
3058            let t_min_x = (min_x / tile_size) as usize;
3059            let t_min_y = (min_y / tile_size) as usize;
3060            let t_max_x = ((max_x / tile_size) as usize).min(tw - 1);
3061            let t_max_y = ((max_y / tile_size) as usize).min(th - 1);
3062            for ty in t_min_y..=t_max_y {
3063                for tx in t_min_x..=t_max_x {
3064                    tile_counts[ty * tw + tx] += 1;
3065                }
3066            }
3067        }
3068        let total_count: u64 = tile_counts.iter().map(|&c| c as u64).sum();
3069        let total_tiles = (tw * th) as f64;
3070        total_count as f32 / total_tiles as f32
3071    }
3072
3073    /// Estimate overdraw for a GBuffer pass based on draw call info
3074    pub fn estimate_gbuffer_overdraw(draw_calls: u32, avg_triangle_screen_coverage: f32, width: u32, height: u32) -> f32 {
3075        let total_pixels_shaded = draw_calls as f32 * avg_triangle_screen_coverage * (width * height) as f32;
3076        let screen_pixels = (width * height) as f32;
3077        total_pixels_shaded / screen_pixels
3078    }
3079}
3080
3081// ============================================================
3082//  SUBPASS DEPENDENCIES
3083// ============================================================
3084
3085#[derive(Debug, Clone)]
3086pub struct SubpassDependency {
3087    pub src_subpass: u32,
3088    pub dst_subpass: u32,
3089    pub src_stage: PipelineStageFlags,
3090    pub dst_stage: PipelineStageFlags,
3091    pub src_access: AccessFlags,
3092    pub dst_access: AccessFlags,
3093    pub by_region: bool,
3094}
3095
3096impl SubpassDependency {
3097    pub const SUBPASS_EXTERNAL: u32 = u32::MAX;
3098
3099    /// External -> first subpass dependency for color attachment
3100    pub fn external_to_color(dst_subpass: u32) -> Self {
3101        SubpassDependency {
3102            src_subpass: Self::SUBPASS_EXTERNAL,
3103            dst_subpass,
3104            src_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
3105            dst_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
3106            src_access: AccessFlags::NONE,
3107            dst_access: AccessFlags::COLOR_ATTACHMENT_WRITE | AccessFlags::COLOR_ATTACHMENT_READ,
3108            by_region: false,
3109        }
3110    }
3111    /// Last subpass -> external for presentation
3112    pub fn color_to_external(src_subpass: u32) -> Self {
3113        SubpassDependency {
3114            src_subpass,
3115            dst_subpass: Self::SUBPASS_EXTERNAL,
3116            src_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
3117            dst_stage: PipelineStageFlags::BOTTOM_OF_PIPE,
3118            src_access: AccessFlags::COLOR_ATTACHMENT_WRITE,
3119            dst_access: AccessFlags::NONE,
3120            by_region: false,
3121        }
3122    }
3123    /// Input attachment: produced by src_subpass, consumed by dst_subpass in same renderpass
3124    pub fn input_attachment(src_subpass: u32, dst_subpass: u32) -> Self {
3125        SubpassDependency {
3126            src_subpass,
3127            dst_subpass,
3128            src_stage: PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
3129            dst_stage: PipelineStageFlags::FRAGMENT_SHADER,
3130            src_access: AccessFlags::COLOR_ATTACHMENT_WRITE,
3131            dst_access: AccessFlags::INPUT_ATTACHMENT_READ,
3132            by_region: true, // tile-based optimization: reads only the same tile
3133        }
3134    }
3135    /// Detect if this dependency can be merged for tile-based rendering (TBR)
3136    pub fn is_tbr_friendly(&self) -> bool {
3137        self.by_region
3138    }
3139}
3140
3141#[derive(Debug, Clone)]
3142pub struct SubpassDescription {
3143    pub index: u32,
3144    pub input_attachments: Vec<u32>,   // attachment indices
3145    pub color_attachments: Vec<u32>,
3146    pub resolve_attachments: Vec<u32>,
3147    pub depth_stencil_attachment: Option<u32>,
3148    pub preserve_attachments: Vec<u32>,
3149}
3150
3151#[derive(Debug, Clone)]
3152pub struct RenderPassDescription {
3153    pub attachments: Vec<AttachmentDescription>,
3154    pub subpasses: Vec<SubpassDescription>,
3155    pub dependencies: Vec<SubpassDependency>,
3156}
3157
3158impl RenderPassDescription {
3159    /// Attempt to merge the GBuffer + Lighting passes into a single renderpass with subpasses
3160    /// This is the key TBR optimization that avoids writing GBuffer data to main memory
3161    pub fn build_gbuffer_lighting_renderpass(
3162        gbuf: &GBufferPassDesc,
3163        light: &LightingPassDesc,
3164    ) -> Self {
3165        let mut attachments = gbuf.attachment_descriptions();
3166        // Add lighting output as a new attachment
3167        attachments.push(AttachmentDescription {
3168            format: light.output_format,
3169            samples: SampleCount::S1,
3170            load_op: LoadOp::DontCare,
3171            store_op: StoreOp::Store,
3172            stencil_load_op: LoadOp::DontCare,
3173            stencil_store_op: StoreOp::DontCare,
3174            initial_layout: ImageLayout::Undefined,
3175            final_layout: ImageLayout::ColorAttachmentOptimal,
3176        });
3177        let lighting_att_idx = (attachments.len() - 1) as u32;
3178        let subpasses = vec![
3179            SubpassDescription {
3180                index: 0,
3181                input_attachments: vec![],
3182                color_attachments: vec![0, 1, 2, 3], // albedo, normal, material, velocity
3183                resolve_attachments: vec![],
3184                depth_stencil_attachment: Some(4),
3185                preserve_attachments: vec![],
3186            },
3187            SubpassDescription {
3188                index: 1,
3189                input_attachments: vec![0, 1, 2, 4], // read albedo, normal, material, depth as input attachments
3190                color_attachments: vec![lighting_att_idx],
3191                resolve_attachments: vec![],
3192                depth_stencil_attachment: None,
3193                preserve_attachments: vec![3], // preserve velocity for later TAA
3194            },
3195        ];
3196        let dependencies = vec![
3197            SubpassDependency::external_to_color(0),
3198            SubpassDependency::input_attachment(0, 1),
3199            SubpassDependency::color_to_external(1),
3200        ];
3201        RenderPassDescription { attachments, subpasses, dependencies }
3202    }
3203
3204    pub fn detect_tbr_optimization(&self) -> bool {
3205        // If all inter-subpass dependencies are by_region, the renderpass benefits from TBR
3206        self.dependencies.iter().all(|d|
3207            d.src_subpass == SubpassDependency::SUBPASS_EXTERNAL ||
3208            d.dst_subpass == SubpassDependency::SUBPASS_EXTERNAL ||
3209            d.by_region
3210        )
3211    }
3212
3213    pub fn total_load_store_bandwidth_bytes(&self, width: u32, height: u32) -> u64 {
3214        let pixels = (width * height) as u64;
3215        let mut bw: u64 = 0;
3216        for att in &self.attachments {
3217            let fi = format_info(att.format);
3218            let bpp = fi.bytes_per_block as u64;
3219            if att.load_op == LoadOp::Load   { bw += bpp * pixels; }
3220            if att.store_op == StoreOp::Store { bw += bpp * pixels; }
3221        }
3222        bw
3223    }
3224}
3225
3226// ============================================================
3227//  SERIALIZATION
3228// ============================================================
3229
3230#[derive(Debug, Clone)]
3231pub struct SerializedNode {
3232    pub id: u32,
3233    pub name: String,
3234    pub kind: String,
3235    pub pos: [f32; 2],
3236    pub size: [f32; 2],
3237    pub color: [f32; 4],
3238    pub enabled: bool,
3239    pub reads: Vec<u32>,
3240    pub writes: Vec<u32>,
3241}
3242
3243#[derive(Debug, Clone)]
3244pub struct SerializedResource {
3245    pub id: u32,
3246    pub name: String,
3247    pub kind: String,
3248    pub format: String,
3249    pub width: u32,
3250    pub height: u32,
3251    pub mip_levels: u32,
3252    pub lifetime: String,
3253}
3254
3255#[derive(Debug, Clone)]
3256pub struct SerializedRenderGraph {
3257    pub version: u32,
3258    pub name: String,
3259    pub nodes: Vec<SerializedNode>,
3260    pub resources: Vec<SerializedResource>,
3261    pub connections: Vec<[u32; 2]>, // [src_pass_id, dst_pass_id]
3262}
3263
3264impl SerializedRenderGraph {
3265    pub fn serialize(editor: &RenderGraphEditor) -> Self {
3266        let nodes: Vec<SerializedNode> = editor.passes.values().map(|p| SerializedNode {
3267            id: p.id.0,
3268            name: p.name.clone(),
3269            kind: format!("{:?}", p.desc.kind()),
3270            pos: [p.editor_pos.x, p.editor_pos.y],
3271            size: [p.editor_size.x, p.editor_size.y],
3272            color: [p.editor_color.x, p.editor_color.y, p.editor_color.z, p.editor_color.w],
3273            enabled: p.enabled,
3274            reads: p.reads.iter().map(|r| r.0).collect(),
3275            writes: p.writes.iter().map(|r| r.0).collect(),
3276        }).collect();
3277        let resources: Vec<SerializedResource> = editor.resources.values().map(|r| {
3278            let (fmt_str, w, h, mip) = match &r.desc {
3279                ResourceDesc::Texture(t) => (format!("{:?}", t.format), t.width, t.height, t.mip_levels),
3280                ResourceDesc::Buffer(_) => ("Buffer".to_owned(), 0, 0, 0),
3281            };
3282            SerializedResource {
3283                id: r.id.0, name: r.name.clone(),
3284                kind: if r.is_texture() { "Texture".to_owned() } else { "Buffer".to_owned() },
3285                format: fmt_str, width: w, height: h, mip_levels: mip,
3286                lifetime: format!("{:?}", r.lifetime),
3287            }
3288        }).collect();
3289        let mut connections: Vec<[u32; 2]> = Vec::new();
3290        for (src_id, src_pass) in &editor.passes {
3291            for rid in &src_pass.writes {
3292                for (dst_id, dst_pass) in &editor.passes {
3293                    if dst_pass.reads.contains(rid) {
3294                        connections.push([src_id.0, dst_id.0]);
3295                    }
3296                }
3297            }
3298        }
3299        connections.sort();
3300        connections.dedup();
3301        SerializedRenderGraph { version: 1, name: editor.name.clone(), nodes, resources, connections }
3302    }
3303
3304    pub fn to_json_string(&self) -> String {
3305        let mut s = String::new();
3306        s.push_str("{\n");
3307        s.push_str(&format!("  \"version\": {},\n", self.version));
3308        s.push_str(&format!("  \"name\": \"{}\",\n", self.name));
3309        s.push_str("  \"nodes\": [\n");
3310        for (i, n) in self.nodes.iter().enumerate() {
3311            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]));
3312            if i + 1 < self.nodes.len() { s.push(','); }
3313            s.push('\n');
3314        }
3315        s.push_str("  ],\n  \"resources\": [\n");
3316        for (i, r) in self.resources.iter().enumerate() {
3317            s.push_str(&format!("    {{\"id\":{},\"name\":\"{}\",\"format\":\"{}\",\"w\":{},\"h\":{}}}", r.id, r.name, r.format, r.width, r.height));
3318            if i + 1 < self.resources.len() { s.push(','); }
3319            s.push('\n');
3320        }
3321        s.push_str("  ],\n  \"connections\": [");
3322        for (i, c) in self.connections.iter().enumerate() {
3323            s.push_str(&format!("[{},{}]", c[0], c[1]));
3324            if i + 1 < self.connections.len() { s.push(','); }
3325        }
3326        s.push_str("]\n}\n");
3327        s
3328    }
3329}
3330
3331// ============================================================
3332//  MAIN RENDER GRAPH EDITOR STRUCT
3333// ============================================================
3334
3335pub struct RenderGraphEditor {
3336    pub name: String,
3337    pub passes: HashMap<PassId, PassNode>,
3338    pub resources: HashMap<ResourceId, RenderGraphResource>,
3339    pub compiled: Option<CompiledRenderGraph>,
3340    pub layout: Option<GraphLayout>,
3341    pub stats: FrameStatistics,
3342    pub validation_report: Option<ValidationReport>,
3343    // Editor UI state
3344    pub selected_pass: Option<PassId>,
3345    pub selected_resource: Option<ResourceId>,
3346    pub hover_pass: Option<PassId>,
3347    pub drag_pass: Option<PassId>,
3348    pub drag_offset: Vec2,
3349    pub camera_pos: Vec2,
3350    pub camera_zoom: f32,
3351    pub show_resources: bool,
3352    pub show_barriers: bool,
3353    pub show_stats: bool,
3354    pub show_validation: bool,
3355    pub output_resources: Vec<ResourceId>,
3356    next_pass_id: u32,
3357    next_resource_id: u32,
3358}
3359
3360impl RenderGraphEditor {
3361    pub fn new(name: &str) -> Self {
3362        RenderGraphEditor {
3363            name: name.to_owned(),
3364            passes: HashMap::new(),
3365            resources: HashMap::new(),
3366            compiled: None,
3367            layout: None,
3368            stats: FrameStatistics::new(),
3369            validation_report: None,
3370            selected_pass: None,
3371            selected_resource: None,
3372            hover_pass: None,
3373            drag_pass: None,
3374            drag_offset: Vec2::ZERO,
3375            camera_pos: Vec2::ZERO,
3376            camera_zoom: 1.0,
3377            show_resources: true,
3378            show_barriers: false,
3379            show_stats: true,
3380            show_validation: true,
3381            output_resources: vec![],
3382            next_pass_id: 0,
3383            next_resource_id: 0,
3384        }
3385    }
3386
3387    // --- Resource management ---
3388
3389    pub fn alloc_resource_id(&mut self) -> ResourceId {
3390        let id = ResourceId(self.next_resource_id);
3391        self.next_resource_id += 1;
3392        id
3393    }
3394
3395    pub fn alloc_pass_id(&mut self) -> PassId {
3396        let id = PassId(self.next_pass_id);
3397        self.next_pass_id += 1;
3398        id
3399    }
3400
3401    pub fn add_resource(&mut self, name: &str, desc: ResourceDesc, lifetime: ResourceLifetime) -> ResourceId {
3402        let id = self.alloc_resource_id();
3403        let res = RenderGraphResource {
3404            id, name: name.to_owned(), desc, lifetime,
3405            first_use: usize::MAX, last_use: 0,
3406            can_alias: lifetime == ResourceLifetime::Transient,
3407            alias_target: None,
3408            current_layout: ImageLayout::Undefined,
3409        };
3410        self.resources.insert(id, res);
3411        id
3412    }
3413
3414    pub fn add_transient_texture(&mut self, name: &str, desc: TextureDesc) -> ResourceId {
3415        self.add_resource(name, ResourceDesc::Texture(desc), ResourceLifetime::Transient)
3416    }
3417
3418    pub fn add_persistent_texture(&mut self, name: &str, desc: TextureDesc) -> ResourceId {
3419        self.add_resource(name, ResourceDesc::Texture(desc), ResourceLifetime::Persistent)
3420    }
3421
3422    pub fn add_pass(&mut self, name: &str, desc: PassDesc) -> PassId {
3423        let id = self.alloc_pass_id();
3424        let pass = PassNode::new(id, name, desc);
3425        self.passes.insert(id, pass);
3426        id
3427    }
3428
3429    pub fn set_pass_reads(&mut self, pass: PassId, reads: Vec<ResourceId>) {
3430        if let Some(p) = self.passes.get_mut(&pass) { p.reads = reads; }
3431    }
3432
3433    pub fn set_pass_writes(&mut self, pass: PassId, writes: Vec<ResourceId>) {
3434        if let Some(p) = self.passes.get_mut(&pass) { p.writes = writes; }
3435    }
3436
3437    pub fn set_output_resources(&mut self, outputs: Vec<ResourceId>) {
3438        self.output_resources = outputs;
3439    }
3440
3441    // --- Compilation ---
3442
3443    pub fn compile(&mut self) -> Result<(), String> {
3444        let mut compiler = RenderGraphCompiler::new();
3445        for pass in self.passes.values() { compiler.add_pass(pass.clone()); }
3446        for res in self.resources.values() { compiler.add_resource(res.clone()); }
3447        let compiled = compiler.compile(&self.output_resources)?;
3448        // Write back execution order into pass nodes
3449        for (i, pid) in compiled.sorted_passes.iter().enumerate() {
3450            if let Some(p) = self.passes.get_mut(pid) { p.execute_order = i; }
3451        }
3452        self.compiled = Some(compiled);
3453        Ok(())
3454    }
3455
3456    // --- Validation ---
3457
3458    pub fn validate(&mut self) -> &ValidationReport {
3459        let validator = RenderGraphValidator::new(&self.passes, &self.resources);
3460        self.validation_report = Some(validator.validate());
3461        self.validation_report.as_ref().unwrap()
3462    }
3463
3464    // --- Visualization ---
3465
3466    pub fn visualize(&mut self) {
3467        let layout_algo = SugiyamaLayout::default();
3468        let passes: Vec<PassId> = self.passes.keys().cloned().collect();
3469        let edges = self.build_edges();
3470        self.layout = Some(layout_algo.layout(&passes, &edges));
3471        // Apply positions to pass nodes
3472        if let Some(ref lay) = self.layout {
3473            for (pid, pos) in &lay.node_positions {
3474                if let Some(pass) = self.passes.get_mut(pid) {
3475                    pass.editor_pos = *pos;
3476                }
3477            }
3478        }
3479    }
3480
3481    fn build_edges(&self) -> HashMap<PassId, Vec<PassId>> {
3482        let mut resource_writers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
3483        let mut resource_readers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
3484        for (pid, pass) in &self.passes {
3485            for rid in &pass.writes { resource_writers.entry(*rid).or_default().push(*pid); }
3486            for rid in &pass.reads  { resource_readers.entry(*rid).or_default().push(*pid); }
3487        }
3488        let mut edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
3489        for pid in self.passes.keys() { edges.insert(*pid, vec![]); }
3490        for (rid, writers) in &resource_writers {
3491            if let Some(readers) = resource_readers.get(rid) {
3492                for w in writers {
3493                    for r in readers {
3494                        if w != r {
3495                            let v = edges.entry(*w).or_default();
3496                            if !v.contains(r) { v.push(*r); }
3497                        }
3498                    }
3499                }
3500            }
3501        }
3502        edges
3503    }
3504
3505    // --- Serialization ---
3506
3507    pub fn serialize(&self) -> SerializedRenderGraph {
3508        SerializedRenderGraph::serialize(self)
3509    }
3510
3511    pub fn to_json(&self) -> String {
3512        self.serialize().to_json_string()
3513    }
3514
3515    // --- High-level builder: standard deferred pipeline ---
3516
3517    pub fn build_standard_deferred_pipeline(width: u32, height: u32) -> RenderGraphEditor {
3518        let mut editor = RenderGraphEditor::new("Standard Deferred");
3519
3520        // Create resources
3521        let res_albedo   = editor.add_transient_texture("GBuffer_Albedo",   TextureDesc::render_target(width, height, TextureFormat::RGBA8Unorm));
3522        let res_normal   = editor.add_transient_texture("GBuffer_Normal",   TextureDesc::render_target(width, height, TextureFormat::RG16Float));
3523        let res_material = editor.add_transient_texture("GBuffer_Material", TextureDesc::render_target(width, height, TextureFormat::RGBA8Unorm));
3524        let res_velocity = editor.add_transient_texture("GBuffer_Velocity", TextureDesc::render_target(width, height, TextureFormat::RG16Float));
3525        let res_depth    = editor.add_transient_texture("GBuffer_Depth",    TextureDesc::depth_target(width, height));
3526        let res_shadow   = editor.add_transient_texture("ShadowMap",        TextureDesc::shadow_map(4096));
3527        let res_ao       = editor.add_transient_texture("SSAO_AO",          TextureDesc::render_target(width/2, height/2, TextureFormat::R8Unorm));
3528        let res_hdr      = editor.add_transient_texture("HDR_Color",        TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
3529        let res_bloom    = editor.add_transient_texture("Bloom",            TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
3530        let res_ssr      = editor.add_transient_texture("SSR",              TextureDesc::render_target(width/2, height/2, TextureFormat::RGBA16Float));
3531        let res_fog      = editor.add_transient_texture("VolumetricFog",    TextureDesc::render_target(width/8, height/8, TextureFormat::RGBA16Float));
3532        let res_taa      = editor.add_transient_texture("TAA_Resolved",     TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
3533        let res_sdr      = editor.add_persistent_texture("SDR_Output",      TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
3534        let res_particles= editor.add_transient_texture("Particles",        TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
3535        let res_ui       = editor.add_persistent_texture("UI_Output",       TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
3536
3537        editor.set_output_resources(vec![res_ui]);
3538
3539        // Shadow Map Pass
3540        let mut sm_desc = ShadowMapPassDesc::directional_shadow(4096);
3541        sm_desc.output_shadow_map = res_shadow;
3542        let sm_pass = editor.add_pass("ShadowMap", PassDesc::ShadowMap(sm_desc));
3543        editor.set_pass_writes(sm_pass, vec![res_shadow]);
3544
3545        // GBuffer Pass
3546        let mut gbuf_desc = GBufferPassDesc::default(width, height);
3547        gbuf_desc.output_albedo   = res_albedo;
3548        gbuf_desc.output_normal   = res_normal;
3549        gbuf_desc.output_material = res_material;
3550        gbuf_desc.output_velocity = res_velocity;
3551        gbuf_desc.output_depth    = res_depth;
3552        let gbuf_pass = editor.add_pass("GBuffer", PassDesc::GBuffer(gbuf_desc));
3553        editor.set_pass_writes(gbuf_pass, vec![res_albedo, res_normal, res_material, res_velocity, res_depth]);
3554
3555        // SSAO Pass
3556        let mut ssao_desc = SSAOPassDesc::default(width, height);
3557        ssao_desc.output_ao = res_ao;
3558        ssao_desc.input_depth = res_depth;
3559        ssao_desc.input_normal = res_normal;
3560        let ssao_pass = editor.add_pass("SSAO", PassDesc::SSAO(ssao_desc));
3561        editor.set_pass_reads(ssao_pass, vec![res_depth, res_normal]);
3562        editor.set_pass_writes(ssao_pass, vec![res_ao]);
3563
3564        // Volumetric Fog
3565        let mut fog_desc = VolumetricFogPassDesc::default(width, height);
3566        fog_desc.output_fog = res_fog;
3567        fog_desc.input_depth = res_depth;
3568        fog_desc.input_shadow_map = res_shadow;
3569        let fog_pass = editor.add_pass("VolumetricFog", PassDesc::VolumetricFog(fog_desc));
3570        editor.set_pass_reads(fog_pass, vec![res_depth, res_shadow]);
3571        editor.set_pass_writes(fog_pass, vec![res_fog]);
3572
3573        // Lighting Pass
3574        let mut light_desc = LightingPassDesc::default(width, height);
3575        light_desc.output_hdr = res_hdr;
3576        light_desc.input_albedo = res_albedo;
3577        light_desc.input_normal = res_normal;
3578        light_desc.input_material = res_material;
3579        light_desc.input_depth = res_depth;
3580        light_desc.input_shadow_map = res_shadow;
3581        light_desc.input_ssao = res_ao;
3582        let light_pass = editor.add_pass("Lighting", PassDesc::Lighting(light_desc));
3583        editor.set_pass_reads(light_pass, vec![res_albedo, res_normal, res_material, res_depth, res_shadow, res_ao, res_fog]);
3584        editor.set_pass_writes(light_pass, vec![res_hdr]);
3585
3586        // SSR Pass
3587        let mut ssr_desc = SSRPassDesc::default(width, height);
3588        ssr_desc.output_ssr = res_ssr;
3589        ssr_desc.input_depth = res_depth;
3590        ssr_desc.input_normal = res_normal;
3591        ssr_desc.input_material = res_material;
3592        ssr_desc.input_hdr = res_hdr;
3593        let ssr_pass = editor.add_pass("SSR", PassDesc::SSR(ssr_desc));
3594        editor.set_pass_reads(ssr_pass, vec![res_depth, res_normal, res_material, res_hdr]);
3595        editor.set_pass_writes(ssr_pass, vec![res_ssr]);
3596
3597        // Particle Pass
3598        let mut particle_desc = ParticlePassDesc::default(width, height);
3599        particle_desc.output_particles = res_particles;
3600        particle_desc.input_depth = res_depth;
3601        particle_desc.input_hdr = res_hdr;
3602        let particle_pass = editor.add_pass("Particles", PassDesc::Particle(particle_desc));
3603        editor.set_pass_reads(particle_pass, vec![res_hdr, res_depth]);
3604        editor.set_pass_writes(particle_pass, vec![res_particles]);
3605
3606        // Bloom Pass
3607        let mut bloom_desc = BloomPassDesc::default(width, height);
3608        bloom_desc.output_bloom = res_bloom;
3609        bloom_desc.input_hdr = res_hdr;
3610        let bloom_pass = editor.add_pass("Bloom", PassDesc::Bloom(bloom_desc));
3611        editor.set_pass_reads(bloom_pass, vec![res_hdr]);
3612        editor.set_pass_writes(bloom_pass, vec![res_bloom]);
3613
3614        // Tone Mapping Pass
3615        let mut tonemap_desc = ToneMappingPassDesc::default(width, height);
3616        tonemap_desc.output_sdr = res_sdr;
3617        tonemap_desc.input_hdr = res_hdr;
3618        tonemap_desc.input_bloom = res_bloom;
3619        let tonemap_pass = editor.add_pass("ToneMapping", PassDesc::ToneMapping(tonemap_desc));
3620        editor.set_pass_reads(tonemap_pass, vec![res_hdr, res_bloom, res_ssr, res_particles]);
3621        editor.set_pass_writes(tonemap_pass, vec![res_sdr]);
3622
3623        // TAA Pass
3624        let mut taa_desc = TAAPassDesc::default(width, height);
3625        taa_desc.output_resolved = res_taa;
3626        taa_desc.input_current = res_sdr;
3627        taa_desc.input_depth = res_depth;
3628        taa_desc.input_velocity = res_velocity;
3629        let taa_pass = editor.add_pass("TAA", PassDesc::TAA(taa_desc));
3630        editor.set_pass_reads(taa_pass, vec![res_sdr, res_depth, res_velocity]);
3631        editor.set_pass_writes(taa_pass, vec![res_taa]);
3632
3633        // UI Pass
3634        let mut ui_desc = UIPassDesc::default(width, height);
3635        ui_desc.output_ui = res_ui;
3636        ui_desc.input_scene = res_taa;
3637        let ui_pass = editor.add_pass("UI", PassDesc::UI(ui_desc));
3638        editor.set_pass_reads(ui_pass, vec![res_taa]);
3639        editor.set_pass_writes(ui_pass, vec![res_ui]);
3640
3641        editor
3642    }
3643
3644    // --- Editor camera utilities ---
3645
3646    pub fn screen_to_world(&self, screen: Vec2) -> Vec2 {
3647        (screen - self.camera_pos) / self.camera_zoom
3648    }
3649    pub fn world_to_screen(&self, world: Vec2) -> Vec2 {
3650        world * self.camera_zoom + self.camera_pos
3651    }
3652
3653    pub fn zoom_around(&mut self, center: Vec2, delta: f32) {
3654        let old_zoom = self.camera_zoom;
3655        self.camera_zoom = (self.camera_zoom * (1.0 + delta * 0.1)).clamp(0.1, 8.0);
3656        let zoom_ratio = self.camera_zoom / old_zoom;
3657        self.camera_pos = center - (center - self.camera_pos) * zoom_ratio;
3658    }
3659
3660    pub fn begin_drag_pass(&mut self, pass: PassId, mouse_pos: Vec2) {
3661        if let Some(p) = self.passes.get(&pass) {
3662            self.drag_pass = Some(pass);
3663            self.drag_offset = self.screen_to_world(mouse_pos) - p.editor_pos;
3664        }
3665    }
3666
3667    pub fn update_drag(&mut self, mouse_pos: Vec2) {
3668        if let Some(pid) = self.drag_pass {
3669            let world_pos = self.screen_to_world(mouse_pos) - self.drag_offset;
3670            if let Some(pass) = self.passes.get_mut(&pid) {
3671                pass.editor_pos = world_pos;
3672            }
3673        }
3674    }
3675
3676    pub fn end_drag(&mut self) {
3677        self.drag_pass = None;
3678    }
3679
3680    pub fn hit_test_pass(&self, mouse_pos: Vec2) -> Option<PassId> {
3681        let world = self.screen_to_world(mouse_pos);
3682        for pass in self.passes.values() {
3683            let min = pass.editor_pos;
3684            let max = pass.editor_pos + pass.editor_size;
3685            if world.x >= min.x && world.x <= max.x && world.y >= min.y && world.y <= max.y {
3686                return Some(pass.id);
3687            }
3688        }
3689        None
3690    }
3691
3692    pub fn get_pass_port_position(&self, pass: PassId, is_output: bool, port_index: u32) -> Vec2 {
3693        if let Some(p) = self.passes.get(&pass) {
3694            let x = if is_output { p.editor_pos.x + p.editor_size.x } else { p.editor_pos.x };
3695            let y = p.editor_pos.y + (port_index as f32 + 0.5) * (p.editor_size.y / (p.reads.len().max(1) as f32));
3696            return Vec2::new(x, y);
3697        }
3698        Vec2::ZERO
3699    }
3700
3701    // --- Statistics ---
3702
3703    pub fn update_pass_stats(&mut self, pass_id: PassId, stats: PassStatistics) {
3704        self.stats.pass_stats.insert(pass_id, stats);
3705        self.stats.aggregate();
3706    }
3707
3708    pub fn get_stats_summary(&self) -> String {
3709        let mut s = String::new();
3710        s.push_str(&format!("Total GPU: {:.2}ms  FPS: {:.1}\n", self.stats.total_gpu_time_ms, self.stats.fps));
3711        s.push_str(&format!("Draw Calls: {}  Triangles: {}M\n", self.stats.total_draw_calls, self.stats.total_triangles / 1_000_000));
3712        s.push_str(&format!("Bandwidth: {:.1}MB/frame\n", self.stats.total_bandwidth_mb));
3713        if let Some(bp) = self.stats.bottleneck_pass() {
3714            if let Some(ps) = self.stats.pass_stats.get(&bp) {
3715                s.push_str(&format!("Bottleneck: Pass {:?} ({:.2}ms)\n", bp, ps.gpu_time_ms));
3716            }
3717        }
3718        if let Some(ref compiled) = self.compiled {
3719            s.push_str(&format!("Memory: {:.1}MB  Bandwidth est: {:.1}MB\n",
3720                compiled.estimated_memory_bytes as f32 / (1024.0*1024.0),
3721                compiled.estimated_bandwidth_mb));
3722            s.push_str(&format!("Dead passes: {}\n", compiled.dead_passes.len()));
3723        }
3724        s
3725    }
3726
3727    // --- Barrier analysis utilities ---
3728
3729    pub fn get_barriers_for_pass(&self, pass_id: PassId) -> Vec<&ImageBarrier> {
3730        if let Some(ref compiled) = self.compiled {
3731            if let Some(barrier) = compiled.barriers.get(&pass_id) {
3732                return barrier.image_barriers.iter().collect();
3733            }
3734        }
3735        vec![]
3736    }
3737
3738    pub fn count_total_barriers(&self) -> usize {
3739        if let Some(ref compiled) = self.compiled {
3740            compiled.barriers.values().map(|b| b.image_barriers.len() + b.buffer_barriers.len()).sum()
3741        } else {
3742            0
3743        }
3744    }
3745
3746    /// For debugging: print a text description of the compiled graph
3747    pub fn describe_compiled(&self) -> String {
3748        let mut s = String::new();
3749        let compiled = match &self.compiled { Some(c) => c, None => return "Not compiled".to_owned() };
3750        s.push_str(&format!("=== {} Render Graph ===\n", self.name));
3751        s.push_str(&format!("Passes ({}): ", compiled.sorted_passes.len()));
3752        for pid in &compiled.sorted_passes {
3753            if let Some(p) = self.passes.get(pid) { s.push_str(&format!("{} ", p.name)); }
3754        }
3755        s.push('\n');
3756        if !compiled.dead_passes.is_empty() {
3757            s.push_str("Dead passes: ");
3758            for pid in &compiled.dead_passes {
3759                if let Some(p) = self.passes.get(pid) { s.push_str(&format!("{} ", p.name)); }
3760            }
3761            s.push('\n');
3762        }
3763        s.push_str(&format!("Aliasing groups: {}\n", compiled.aliasing_groups.len()));
3764        s.push_str(&format!("Estimated memory: {:.2} MB\n", compiled.estimated_memory_bytes as f64 / (1024.0*1024.0)));
3765        s.push_str(&format!("Estimated bandwidth: {:.1} MB/frame\n", compiled.estimated_bandwidth_mb));
3766        s.push_str(&format!("Total barriers: {}\n", self.count_total_barriers()));
3767        s
3768    }
3769}
3770
3771// ============================================================
3772//  UTILITY FUNCTIONS
3773// ============================================================
3774
3775pub fn lerp(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
3776pub fn clamp01(x: f32) -> f32 { x.clamp(0.0, 1.0) }
3777pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
3778    let t = clamp01((x - edge0) / (edge1 - edge0 + 1e-7));
3779    t * t * (3.0 - 2.0 * t)
3780}
3781
3782pub fn halton_sequence(index: u32, base: u32) -> f32 {
3783    let mut f = 1.0f32;
3784    let mut r = 0.0f32;
3785    let mut i = index;
3786    while i > 0 {
3787        f /= base as f32;
3788        r += f * (i % base) as f32;
3789        i /= base;
3790    }
3791    r
3792}
3793
3794pub fn hex_distance(p: Vec2) -> f32 {
3795    let q = Vec2::new(p.x.abs(), p.y.abs());
3796    let s = 0.5f32;
3797    let dot = q.x * s + q.y * (3.0f32).sqrt() * 0.5;
3798    let a = q.x.max(dot);
3799    a.max(q.y)
3800}
3801
3802pub fn compute_mip_count(width: u32, height: u32) -> u32 {
3803    (width.max(height) as f32).log2().floor() as u32 + 1
3804}
3805
3806pub fn align_up(value: u64, alignment: u64) -> u64 {
3807    (value + alignment - 1) & !(alignment - 1)
3808}
3809
3810pub fn align_up_u32(value: u32, alignment: u32) -> u32 {
3811    (value + alignment - 1) & !(alignment - 1)
3812}
3813
3814/// Convert sRGB to linear (approximate gamma 2.2)
3815pub fn srgb_to_linear(c: Vec3) -> Vec3 {
3816    Vec3::new(
3817        srgb_channel_to_linear(c.x),
3818        srgb_channel_to_linear(c.y),
3819        srgb_channel_to_linear(c.z),
3820    )
3821}
3822pub fn srgb_channel_to_linear(c: f32) -> f32 {
3823    if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
3824}
3825pub fn linear_to_srgb_channel(c: f32) -> f32 {
3826    if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
3827}
3828pub fn linear_to_srgb(c: Vec3) -> Vec3 {
3829    Vec3::new(linear_to_srgb_channel(c.x), linear_to_srgb_channel(c.y), linear_to_srgb_channel(c.z))
3830}
3831
3832/// Luminance (CIE Y)
3833pub fn luminance(c: Vec3) -> f32 { 0.2126 * c.x + 0.7152 * c.y + 0.0722 * c.z }
3834
3835/// Exposure compensation from EV100
3836pub fn ev100_to_exposure(ev100: f32) -> f32 { 1.0 / (1.2 * (2.0f32).powf(ev100)) }
3837
3838/// Reconstruct world-space position from depth buffer
3839pub fn reconstruct_world_pos(uv: Vec2, depth: f32, inv_view_proj: Mat4) -> Vec3 {
3840    let ndc = Vec4::new(uv.x * 2.0 - 1.0, uv.y * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
3841    let world_h = inv_view_proj * ndc;
3842    world_h.truncate() / world_h.w
3843}
3844
3845/// Linearize depth from non-linear depth buffer
3846pub fn linearize_depth(depth: f32, near: f32, far: f32) -> f32 {
3847    (2.0 * near * far) / (far + near - depth * (far - near))
3848}
3849
3850/// Compute screen-space UV from world position
3851pub fn world_to_screen_uv(world_pos: Vec3, view_proj: Mat4) -> Option<Vec2> {
3852    let clip = view_proj * Vec4::new(world_pos.x, world_pos.y, world_pos.z, 1.0);
3853    if clip.w < 1e-6 { return None; }
3854    let ndc = clip / clip.w;
3855    if ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 { return None; }
3856    Some(Vec2::new(ndc.x * 0.5 + 0.5, ndc.y * 0.5 + 0.5))
3857}
3858
3859/// Octahedral encode normal (for RG16F GBuffer storage)
3860pub fn octahedral_encode(n: Vec3) -> Vec2 {
3861    let l1 = n.x.abs() + n.y.abs() + n.z.abs();
3862    let p = Vec2::new(n.x / l1, n.y / l1);
3863    if n.z < 0.0 {
3864        let xp = (1.0 - p.y.abs()) * if p.x >= 0.0 { 1.0 } else { -1.0 };
3865        let yp = (1.0 - p.x.abs()) * if p.y >= 0.0 { 1.0 } else { -1.0 };
3866        Vec2::new(xp, yp)
3867    } else {
3868        p
3869    }
3870}
3871
3872/// Octahedral decode normal
3873pub fn octahedral_decode(e: Vec2) -> Vec3 {
3874    let mut n = Vec3::new(e.x, e.y, 1.0 - e.x.abs() - e.y.abs());
3875    if n.z < 0.0 {
3876        let xp = (1.0 - n.y.abs()) * if n.x >= 0.0 { 1.0 } else { -1.0 };
3877        let yp = (1.0 - n.x.abs()) * if n.y >= 0.0 { 1.0 } else { -1.0 };
3878        n.x = xp;
3879        n.y = yp;
3880    }
3881    n.normalize()
3882}
3883
3884/// Encode/decode velocity to/from RG16F
3885pub fn encode_velocity(velocity_pixels: Vec2, max_velocity: f32) -> Vec2 {
3886    velocity_pixels / max_velocity * 0.5 + Vec2::splat(0.5)
3887}
3888pub fn decode_velocity(encoded: Vec2, max_velocity: f32) -> Vec2 {
3889    (encoded - Vec2::splat(0.5)) * 2.0 * max_velocity
3890}
3891
3892/// Encode/decode RGBA8 packed normal+roughness (for material GBuffer)
3893pub fn pack_material(metallic: f32, roughness: f32, ao: f32, emissive_scale: f32) -> u32 {
3894    let m = (metallic.clamp(0.0, 1.0) * 255.0) as u32;
3895    let r = (roughness.clamp(0.0, 1.0) * 255.0) as u32;
3896    let a = (ao.clamp(0.0, 1.0) * 255.0) as u32;
3897    let e = (emissive_scale.clamp(0.0, 1.0) * 255.0) as u32;
3898    (e << 24) | (a << 16) | (r << 8) | m
3899}
3900
3901pub fn unpack_material(packed: u32) -> (f32, f32, f32, f32) {
3902    let m = (packed & 0xFF) as f32 / 255.0;
3903    let r = ((packed >> 8) & 0xFF) as f32 / 255.0;
3904    let a = ((packed >> 16) & 0xFF) as f32 / 255.0;
3905    let e = ((packed >> 24) & 0xFF) as f32 / 255.0;
3906    (m, r, a, e)
3907}
3908
3909// ============================================================
3910//  PBR UTILITY: GGX BRDF TERMS
3911// ============================================================
3912
3913pub fn ggx_distribution(n_dot_h: f32, roughness: f32) -> f32 {
3914    let a = roughness * roughness;
3915    let a2 = a * a;
3916    let denom = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
3917    a2 / (std::f32::consts::PI * denom * denom)
3918}
3919
3920pub fn schlick_fresnel(cos_theta: f32, f0: Vec3) -> Vec3 {
3921    f0 + (Vec3::ONE - f0) * (1.0 - cos_theta).powf(5.0)
3922}
3923
3924pub fn smith_g1_ggx(n_dot_v: f32, roughness: f32) -> f32 {
3925    let r = roughness + 1.0;
3926    let k = (r * r) / 8.0;
3927    n_dot_v / (n_dot_v * (1.0 - k) + k)
3928}
3929
3930pub fn smith_g_ggx(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
3931    smith_g1_ggx(n_dot_v, roughness) * smith_g1_ggx(n_dot_l, roughness)
3932}
3933
3934pub fn cook_torrance_brdf(n: Vec3, v: Vec3, l: Vec3, albedo: Vec3, metallic: f32, roughness: f32) -> Vec3 {
3935    let h = (v + l).normalize();
3936    let n_dot_l = n.dot(l).max(0.0);
3937    let n_dot_v = n.dot(v).max(0.0);
3938    let n_dot_h = n.dot(h).max(0.0);
3939    let h_dot_v = h.dot(v).max(0.0);
3940    let f0 = lerp_vec3(Vec3::splat(0.04), albedo, metallic);
3941    let d = ggx_distribution(n_dot_h, roughness);
3942    let f = schlick_fresnel(h_dot_v, f0);
3943    let g = smith_g_ggx(n_dot_v, n_dot_l, roughness);
3944    let specular = (d * f * g) / (4.0 * n_dot_v * n_dot_l + 1e-7);
3945    let k_s = f;
3946    let k_d = (Vec3::ONE - k_s) * (1.0 - metallic);
3947    let diffuse = k_d * albedo / std::f32::consts::PI;
3948    (diffuse + specular) * n_dot_l
3949}
3950
3951pub fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 { a + (b - a) * t }
3952
3953/// Pre-integrated BRDF LUT sample (GGX + Schlick)
3954pub fn integrate_brdf(n_dot_v: f32, roughness: f32, sample_count: u32) -> Vec2 {
3955    let v = Vec3::new((1.0 - n_dot_v * n_dot_v).sqrt(), 0.0, n_dot_v);
3956    let n = Vec3::Z;
3957    let mut a = 0.0f32;
3958    let mut b = 0.0f32;
3959    for i in 0..sample_count {
3960        let xi = Vec2::new(halton_sequence(i, 2), halton_sequence(i, 3));
3961        let h = importance_sample_ggx(xi, n, roughness);
3962        let l = (2.0 * v.dot(h) * h - v).normalize();
3963        let n_dot_l = l.z.max(0.0);
3964        let n_dot_h = h.z.max(0.0);
3965        let v_dot_h = v.dot(h).max(0.0);
3966        if n_dot_l > 0.0 {
3967            let g = smith_g_ggx(n_dot_v, n_dot_l, roughness);
3968            let g_vis = (g * v_dot_h) / (n_dot_h * n_dot_v + 1e-7);
3969            let fc = (1.0 - v_dot_h).powf(5.0);
3970            a += (1.0 - fc) * g_vis;
3971            b += fc * g_vis;
3972        }
3973    }
3974    Vec2::new(a / sample_count as f32, b / sample_count as f32)
3975}
3976
3977pub fn importance_sample_ggx(xi: Vec2, n: Vec3, roughness: f32) -> Vec3 {
3978    let a = roughness * roughness;
3979    let phi = 2.0 * std::f32::consts::PI * xi.x;
3980    let cos_theta = ((1.0 - xi.y) / (1.0 + (a*a - 1.0) * xi.y)).sqrt();
3981    let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
3982    let h = Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta);
3983    // From tangent to world space
3984    let up = if n.z.abs() < 0.999 { Vec3::Z } else { Vec3::X };
3985    let tangent = up.cross(n).normalize();
3986    let bitangent = n.cross(tangent);
3987    (tangent * h.x + bitangent * h.y + n * h.z).normalize()
3988}
3989
3990// ============================================================
3991//  CLUSTERED LIGHTING
3992// ============================================================
3993
3994#[derive(Debug, Clone)]
3995pub struct ClusteredLightGrid {
3996    pub tiles_x: u32,
3997    pub tiles_y: u32,
3998    pub depth_slices: u32,
3999    pub tile_size: u32,
4000    pub screen_width: u32,
4001    pub screen_height: u32,
4002    pub near: f32,
4003    pub far: f32,
4004}
4005
4006impl ClusteredLightGrid {
4007    pub fn new(screen_width: u32, screen_height: u32, tile_size: u32, depth_slices: u32, near: f32, far: f32) -> Self {
4008        ClusteredLightGrid {
4009            tiles_x: (screen_width + tile_size - 1) / tile_size,
4010            tiles_y: (screen_height + tile_size - 1) / tile_size,
4011            depth_slices,
4012            tile_size,
4013            screen_width,
4014            screen_height,
4015            near,
4016            far,
4017        }
4018    }
4019    pub fn total_clusters(&self) -> u32 { self.tiles_x * self.tiles_y * self.depth_slices }
4020    pub fn cluster_index(&self, tile_x: u32, tile_y: u32, depth_slice: u32) -> u32 {
4021        tile_x + tile_y * self.tiles_x + depth_slice * self.tiles_x * self.tiles_y
4022    }
4023    /// Convert linear depth to cluster depth slice index
4024    pub fn depth_slice_from_linear(linear_depth: f32, near: f32, far: f32, num_slices: u32) -> u32 {
4025        let s = (linear_depth / near).ln() / (far / near).ln();
4026        ((s * num_slices as f32) as u32).min(num_slices - 1)
4027    }
4028    /// Compute the AABB of a cluster in view-space
4029    pub fn cluster_view_aabb(&self, tile_x: u32, tile_y: u32, depth_slice: u32, proj: Mat4) -> (Vec3, Vec3) {
4030        let x0 = (tile_x * self.tile_size) as f32 / self.screen_width as f32 * 2.0 - 1.0;
4031        let x1 = ((tile_x + 1) * self.tile_size).min(self.screen_width) as f32 / self.screen_width as f32 * 2.0 - 1.0;
4032        let y0 = (tile_y * self.tile_size) as f32 / self.screen_height as f32 * 2.0 - 1.0;
4033        let y1 = ((tile_y + 1) * self.tile_size).min(self.screen_height) as f32 / self.screen_height as f32 * 2.0 - 1.0;
4034        let z_near = Self::depth_slice_z(depth_slice, self.near, self.far, self.depth_slices);
4035        let z_far  = Self::depth_slice_z(depth_slice + 1, self.near, self.far, self.depth_slices);
4036        let inv_proj = proj.inverse();
4037        let ndc_to_view = |ndc: Vec4| -> Vec3 {
4038            let v = inv_proj * ndc;
4039            v.truncate() / v.w
4040        };
4041        let min_v = ndc_to_view(Vec4::new(x0, y0, z_near, 1.0));
4042        let max_v = ndc_to_view(Vec4::new(x1, y1, z_far,  1.0));
4043        (min_v.min(max_v), min_v.max(max_v))
4044    }
4045    fn depth_slice_z(slice: u32, near: f32, far: f32, num_slices: u32) -> f32 {
4046        near * (far / near).powf(slice as f32 / num_slices as f32)
4047    }
4048    pub fn memory_requirements(&self, max_lights_per_cluster: u32) -> u64 {
4049        // offset buffer: total_clusters * 4 bytes (u32 offset into light index list)
4050        // count buffer: total_clusters * 4 bytes
4051        // light index buffer: worst case all lights in all clusters
4052        let offset_buf = self.total_clusters() as u64 * 4;
4053        let count_buf  = self.total_clusters() as u64 * 4;
4054        let index_buf  = self.total_clusters() as u64 * max_lights_per_cluster as u64 * 2; // u16 indices
4055        offset_buf + count_buf + index_buf
4056    }
4057}
4058
4059// ============================================================
4060//  TEMPORAL HISTORY BUFFER MANAGEMENT
4061// ============================================================
4062
4063#[derive(Debug, Clone)]
4064pub struct TemporalHistoryBuffer {
4065    pub current_frame: u32,
4066    pub history_count: u32,
4067    pub resources: Vec<ResourceId>,
4068    pub active_index: usize,
4069}
4070
4071impl TemporalHistoryBuffer {
4072    pub fn new(count: u32) -> Self {
4073        TemporalHistoryBuffer { current_frame: 0, history_count: count, resources: Vec::new(), active_index: 0 }
4074    }
4075    pub fn current(&self) -> Option<ResourceId> { self.resources.get(self.active_index).cloned() }
4076    pub fn previous(&self) -> Option<ResourceId> {
4077        let prev = (self.active_index + self.resources.len() - 1) % self.resources.len().max(1);
4078        self.resources.get(prev).cloned()
4079    }
4080    pub fn advance(&mut self) {
4081        self.active_index = (self.active_index + 1) % self.resources.len().max(1);
4082        self.current_frame += 1;
4083    }
4084}
4085
4086// ============================================================
4087//  ADDITIONAL MATH UTILITIES
4088// ============================================================
4089
4090pub fn view_direction_from_uv(uv: Vec2, inv_proj: Mat4) -> Vec3 {
4091    let ndc = Vec4::new(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, -1.0, 1.0);
4092    let view_h = inv_proj * ndc;
4093    let view = view_h.truncate() / view_h.w;
4094    view.normalize()
4095}
4096
4097pub fn sphere_intersect(ray_origin: Vec3, ray_dir: Vec3, sphere_center: Vec3, sphere_radius: f32) -> Option<f32> {
4098    let oc = ray_origin - sphere_center;
4099    let a = ray_dir.dot(ray_dir);
4100    let half_b = oc.dot(ray_dir);
4101    let c = oc.dot(oc) - sphere_radius * sphere_radius;
4102    let discriminant = half_b * half_b - a * c;
4103    if discriminant < 0.0 { None }
4104    else { Some((-half_b - discriminant.sqrt()) / a) }
4105}
4106
4107pub fn aabb_intersect(ray_origin: Vec3, inv_ray_dir: Vec3, aabb_min: Vec3, aabb_max: Vec3) -> Option<(f32, f32)> {
4108    let t1 = (aabb_min - ray_origin) * inv_ray_dir;
4109    let t2 = (aabb_max - ray_origin) * inv_ray_dir;
4110    let tmin_v = t1.min(t2);
4111    let tmax_v = t1.max(t2);
4112    let tmin = tmin_v.x.max(tmin_v.y).max(tmin_v.z);
4113    let tmax = tmax_v.x.min(tmax_v.y).min(tmax_v.z);
4114    if tmax < tmin { None } else { Some((tmin, tmax)) }
4115}
4116
4117pub fn frustum_planes_from_view_proj(vp: Mat4) -> [Vec4; 6] {
4118    let m = vp.to_cols_array_2d();
4119    // Extract planes from combined view-projection matrix (Gribb-Hartmann method)
4120    let row0 = Vec4::new(m[0][0], m[1][0], m[2][0], m[3][0]);
4121    let row1 = Vec4::new(m[0][1], m[1][1], m[2][1], m[3][1]);
4122    let row2 = Vec4::new(m[0][2], m[1][2], m[2][2], m[3][2]);
4123    let row3 = Vec4::new(m[0][3], m[1][3], m[2][3], m[3][3]);
4124    let normalize_plane = |p: Vec4| -> Vec4 {
4125        let len = Vec3::new(p.x, p.y, p.z).length();
4126        p / len
4127    };
4128    [
4129        normalize_plane(row3 + row0), // left
4130        normalize_plane(row3 - row0), // right
4131        normalize_plane(row3 + row1), // bottom
4132        normalize_plane(row3 - row1), // top
4133        normalize_plane(row3 + row2), // near
4134        normalize_plane(row3 - row2), // far
4135    ]
4136}
4137
4138/// Test sphere against frustum planes
4139pub fn sphere_in_frustum(planes: &[Vec4; 6], center: Vec3, radius: f32) -> bool {
4140    for plane in planes {
4141        let dist = plane.x * center.x + plane.y * center.y + plane.z * center.z + plane.w;
4142        if dist < -radius { return false; }
4143    }
4144    true
4145}
4146
4147/// Test AABB against frustum planes
4148pub fn aabb_in_frustum(planes: &[Vec4; 6], aabb_min: Vec3, aabb_max: Vec3) -> bool {
4149    for plane in planes {
4150        let p = Vec3::new(
4151            if plane.x > 0.0 { aabb_max.x } else { aabb_min.x },
4152            if plane.y > 0.0 { aabb_max.y } else { aabb_min.y },
4153            if plane.z > 0.0 { aabb_max.z } else { aabb_min.z },
4154        );
4155        if plane.x * p.x + plane.y * p.y + plane.z * p.z + plane.w < 0.0 { return false; }
4156    }
4157    true
4158}
4159
4160// ============================================================
4161//  RENDER GRAPH PASS — TILE-BASED DETECTION
4162// ============================================================
4163
4164pub struct TBRDetector;
4165impl TBRDetector {
4166    /// Detect if the GPU architecture is likely a tile-based renderer
4167    /// (heuristic: small VRAM, mobile GPU flags, or explicit override)
4168    pub fn is_tbr(vendor_id: u32, device_id: u32, is_mobile: bool) -> bool {
4169        if is_mobile { return true; }
4170        // Known TBR vendor patterns (heuristic)
4171        match vendor_id {
4172            0x13B5 => true, // ARM Mali
4173            0x5143 => true, // Qualcomm Adreno
4174            0x1010 => true, // Imagination PowerVR
4175            _ => false,
4176        }
4177    }
4178
4179    /// Suggest optimal renderpass merging for a TBR architecture
4180    pub fn suggest_pass_merging(passes: &[PassKind]) -> Vec<Vec<PassKind>> {
4181        let mut groups: Vec<Vec<PassKind>> = Vec::new();
4182        let mut current: Vec<PassKind> = Vec::new();
4183        for &kind in passes {
4184            match kind {
4185                PassKind::GBuffer | PassKind::Lighting | PassKind::SSAO => {
4186                    // These can be merged into a single renderpass on TBR
4187                    current.push(kind);
4188                }
4189                _ => {
4190                    if !current.is_empty() {
4191                        groups.push(current.clone());
4192                        current.clear();
4193                    }
4194                    groups.push(vec![kind]);
4195                }
4196            }
4197        }
4198        if !current.is_empty() { groups.push(current); }
4199        groups
4200    }
4201
4202    /// Estimate TBR bandwidth savings from on-chip merging
4203    pub fn bandwidth_savings_mb(width: u32, height: u32, gbuf_formats: &[TextureFormat]) -> f32 {
4204        let pixels = (width * height) as f32;
4205        let mut bpp: f32 = 0.0;
4206        for fmt in gbuf_formats {
4207            bpp += format_info(*fmt).bytes_per_pixel();
4208        }
4209        // On TBR: GBuffer doesn't need to be written to/read from main memory
4210        bpp * pixels / (1024.0 * 1024.0)
4211    }
4212}
4213
4214// ============================================================
4215//  RENDER GRAPH — ASYNC COMPUTE SCHEDULING
4216// ============================================================
4217
4218#[derive(Debug, Clone)]
4219pub struct AsyncComputeGroup {
4220    pub passes: Vec<PassId>,
4221    pub queue: ComputeQueue,
4222    pub semaphore_signals: Vec<PassId>,
4223    pub semaphore_waits: Vec<PassId>,
4224}
4225
4226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4227pub enum ComputeQueue { Graphics, AsyncCompute, Transfer }
4228
4229pub struct AsyncComputeScheduler;
4230impl AsyncComputeScheduler {
4231    /// Identify passes suitable for async compute (compute-only, no color attachments)
4232    pub fn identify_async_candidates(passes: &HashMap<PassId, PassNode>) -> Vec<PassId> {
4233        passes.values().filter(|p| {
4234            matches!(p.desc.kind(), PassKind::SSAO | PassKind::SSR | PassKind::Bloom | PassKind::VolumetricFog | PassKind::TAA)
4235        }).map(|p| p.id).collect()
4236    }
4237
4238    /// Schedule passes into overlapping async compute groups
4239    pub fn schedule(sorted: &[PassId], candidates: &HashSet<PassId>, passes: &HashMap<PassId, PassNode>) -> Vec<AsyncComputeGroup> {
4240        let mut groups: Vec<AsyncComputeGroup> = Vec::new();
4241        let mut current_async: Vec<PassId> = Vec::new();
4242        for pid in sorted {
4243            if candidates.contains(pid) {
4244                current_async.push(*pid);
4245            } else {
4246                if !current_async.is_empty() {
4247                    groups.push(AsyncComputeGroup {
4248                        passes: current_async.clone(),
4249                        queue: ComputeQueue::AsyncCompute,
4250                        semaphore_signals: vec![*current_async.last().unwrap()],
4251                        semaphore_waits: vec![*pid],
4252                    });
4253                    current_async.clear();
4254                }
4255            }
4256        }
4257        if !current_async.is_empty() {
4258            groups.push(AsyncComputeGroup {
4259                passes: current_async.clone(),
4260                queue: ComputeQueue::AsyncCompute,
4261                semaphore_signals: current_async.clone(),
4262                semaphore_waits: vec![],
4263            });
4264        }
4265        groups
4266    }
4267}
4268
4269// ============================================================
4270//  UI DRAWING HELPERS FOR THE EDITOR
4271// ============================================================
4272
4273#[derive(Debug, Clone)]
4274pub struct DrawCommand {
4275    pub kind: DrawCommandKind,
4276    pub clip_rect: Option<[f32; 4]>,
4277    pub z_order: i32,
4278}
4279
4280#[derive(Debug, Clone)]
4281pub enum DrawCommandKind {
4282    Rect { pos: Vec2, size: Vec2, color: Vec4, rounding: f32 },
4283    RectOutline { pos: Vec2, size: Vec2, color: Vec4, thickness: f32, rounding: f32 },
4284    Text { pos: Vec2, text: String, color: Vec4, font_size: f32 },
4285    Line { a: Vec2, b: Vec2, color: Vec4, thickness: f32 },
4286    BezierCubic { p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, color: Vec4, thickness: f32 },
4287    Circle { center: Vec2, radius: f32, color: Vec4, filled: bool },
4288    Triangle { p: [Vec2; 3], color: Vec4, filled: bool },
4289}
4290
4291pub struct EditorRenderer {
4292    pub commands: Vec<DrawCommand>,
4293    pub viewport_size: Vec2,
4294}
4295
4296impl EditorRenderer {
4297    pub fn new(viewport_size: Vec2) -> Self { EditorRenderer { commands: vec![], viewport_size } }
4298
4299    pub fn clear(&mut self) { self.commands.clear(); }
4300
4301    pub fn draw_rect(&mut self, pos: Vec2, size: Vec2, color: Vec4, rounding: f32) {
4302        self.commands.push(DrawCommand { kind: DrawCommandKind::Rect { pos, size, color, rounding }, clip_rect: None, z_order: 0 });
4303    }
4304    pub fn draw_rect_outline(&mut self, pos: Vec2, size: Vec2, color: Vec4, thickness: f32, rounding: f32) {
4305        self.commands.push(DrawCommand { kind: DrawCommandKind::RectOutline { pos, size, color, thickness, rounding }, clip_rect: None, z_order: 0 });
4306    }
4307    pub fn draw_text(&mut self, pos: Vec2, text: &str, color: Vec4, font_size: f32) {
4308        self.commands.push(DrawCommand { kind: DrawCommandKind::Text { pos, text: text.to_owned(), color, font_size }, clip_rect: None, z_order: 1 });
4309    }
4310    pub fn draw_line(&mut self, a: Vec2, b: Vec2, color: Vec4, thickness: f32) {
4311        self.commands.push(DrawCommand { kind: DrawCommandKind::Line { a, b, color, thickness }, clip_rect: None, z_order: 0 });
4312    }
4313    pub fn draw_bezier(&mut self, p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, color: Vec4, thickness: f32) {
4314        self.commands.push(DrawCommand { kind: DrawCommandKind::BezierCubic { p0, p1, p2, p3, color, thickness }, clip_rect: None, z_order: 0 });
4315    }
4316
4317    /// Render a pass node
4318    pub fn render_pass_node(&mut self, pass: &PassNode, camera_pos: Vec2, camera_zoom: f32, is_selected: bool, is_hovered: bool) {
4319        let pos = (pass.editor_pos + camera_pos) * camera_zoom;
4320        let size = pass.editor_size * camera_zoom;
4321        // Background
4322        let mut bg = pass.editor_color;
4323        if is_hovered  { bg = bg * 1.2; bg.w = 1.0; }
4324        if is_selected { bg = Vec4::new(1.0, 0.9, 0.2, 1.0); }
4325        self.draw_rect(pos, size, bg, 6.0 * camera_zoom);
4326        // Border
4327        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) };
4328        self.draw_rect_outline(pos, size, border_color, 2.0 * camera_zoom, 6.0 * camera_zoom);
4329        // Title
4330        let title_pos = pos + Vec2::new(8.0 * camera_zoom, 8.0 * camera_zoom);
4331        let text_color = Vec4::new(1.0, 1.0, 1.0, 1.0);
4332        self.draw_text(title_pos, &pass.name, text_color, 14.0 * camera_zoom);
4333        // Kind label
4334        let kind_str = format!("{:?}", pass.desc.kind());
4335        let kind_pos = pos + Vec2::new(8.0 * camera_zoom, 28.0 * camera_zoom);
4336        self.draw_text(kind_pos, &kind_str, Vec4::new(0.8, 0.8, 0.8, 0.9), 10.0 * camera_zoom);
4337        // Input/output ports
4338        let port_radius = 5.0 * camera_zoom;
4339        for (i, _rid) in pass.reads.iter().enumerate() {
4340            let py = pos.y + (i as f32 + 0.5) * (size.y / pass.reads.len().max(1) as f32);
4341            let port_pos = Vec2::new(pos.x, py);
4342            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 });
4343        }
4344        for (i, _rid) in pass.writes.iter().enumerate() {
4345            let py = pos.y + (i as f32 + 0.5) * (size.y / pass.writes.len().max(1) as f32);
4346            let port_pos = Vec2::new(pos.x + size.x, py);
4347            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 });
4348        }
4349    }
4350
4351    /// Render an edge connecting two passes
4352    pub fn render_edge(&mut self, src_port: Vec2, dst_port: Vec2, color: Vec4, thickness: f32) {
4353        let dx = (dst_port.x - src_port.x).abs() * 0.5;
4354        let p1 = src_port + Vec2::new(dx, 0.0);
4355        let p2 = dst_port - Vec2::new(dx, 0.0);
4356        self.draw_bezier(src_port, p1, p2, dst_port, color, thickness);
4357    }
4358
4359    /// Render a resource node (small chip)
4360    pub fn render_resource_node(&mut self, res: &RenderGraphResource, pos: Vec2, zoom: f32) {
4361        let size = Vec2::new(120.0, 40.0) * zoom;
4362        let color = match res.lifetime {
4363            ResourceLifetime::Transient  => Vec4::new(0.2, 0.4, 0.2, 0.8),
4364            ResourceLifetime::Persistent => Vec4::new(0.4, 0.2, 0.2, 0.8),
4365            ResourceLifetime::Imported   => Vec4::new(0.2, 0.2, 0.4, 0.8),
4366        };
4367        self.draw_rect(pos, size, color, 4.0 * zoom);
4368        self.draw_text(pos + Vec2::new(4.0 * zoom, 4.0 * zoom), &res.name, Vec4::ONE, 10.0 * zoom);
4369        if let Some(td) = res.texture_desc() {
4370            let info_str = format!("{}x{} {:?}", td.width, td.height, td.format);
4371            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);
4372        }
4373    }
4374
4375    /// Render barrier indicators on an edge
4376    pub fn render_barrier_indicator(&mut self, pos: Vec2, zoom: f32, old_layout: ImageLayout, new_layout: ImageLayout) {
4377        let r = 8.0 * zoom;
4378        let color = barrier_color_for_layouts(old_layout, new_layout);
4379        self.commands.push(DrawCommand {
4380            kind: DrawCommandKind::Circle { center: pos, radius: r, color, filled: true },
4381            clip_rect: None, z_order: 3,
4382        });
4383    }
4384
4385    /// Render stats overlay
4386    pub fn render_stats_overlay(&mut self, stats: &FrameStatistics, pos: Vec2) {
4387        let bg_size = Vec2::new(260.0, 120.0);
4388        self.draw_rect(pos, bg_size, Vec4::new(0.0, 0.0, 0.0, 0.8), 4.0);
4389        let mut y = pos.y + 8.0;
4390        let lh = 16.0;
4391        let tc = Vec4::new(0.9, 0.9, 0.9, 1.0);
4392        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;
4393        self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("Draw calls: {}", stats.total_draw_calls), tc, 11.0); y += lh;
4394        self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("Triangles: {}M", stats.total_triangles / 1_000_000), tc, 11.0); y += lh;
4395        self.draw_text(Vec2::new(pos.x + 8.0, y), &format!("BW: {:.1}MB/frame", stats.total_bandwidth_mb), tc, 11.0); y += lh;
4396        if let Some(bp) = stats.bottleneck_pass() {
4397            if let Some(ps) = stats.pass_stats.get(&bp) {
4398                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);
4399            }
4400        }
4401    }
4402
4403    pub fn sort_by_z(&mut self) {
4404        self.commands.sort_by_key(|c| c.z_order);
4405    }
4406}
4407
4408fn barrier_color_for_layouts(old: ImageLayout, new: ImageLayout) -> Vec4 {
4409    match (old, new) {
4410        (ImageLayout::Undefined, _) => Vec4::new(1.0, 0.2, 0.2, 1.0), // red: expensive undefined transition
4411        (ImageLayout::ColorAttachmentOptimal, ImageLayout::ShaderReadOnlyOptimal) => Vec4::new(0.2, 1.0, 0.2, 1.0), // green: common
4412        (_, ImageLayout::ShaderReadOnlyOptimal) => Vec4::new(0.4, 0.8, 0.4, 1.0),
4413        (_, ImageLayout::ColorAttachmentOptimal) => Vec4::new(0.8, 0.6, 0.2, 1.0),
4414        (_, ImageLayout::TransferSrcOptimal) | (_, ImageLayout::TransferDstOptimal) => Vec4::new(0.6, 0.2, 0.8, 1.0),
4415        _ => Vec4::new(0.8, 0.8, 0.8, 1.0),
4416    }
4417}
4418
4419// ============================================================
4420//  RENDER GRAPH TESTS / SCENARIO BUILDERS
4421// ============================================================
4422
4423pub fn build_forward_plus_pipeline(width: u32, height: u32) -> RenderGraphEditor {
4424    let mut editor = RenderGraphEditor::new("Forward+");
4425    let res_depth_prepass = editor.add_transient_texture("DepthPrepass", TextureDesc::depth_target(width, height));
4426    let res_depth_main    = editor.add_transient_texture("MainDepth",    TextureDesc::depth_target(width, height));
4427    let res_hdr           = editor.add_transient_texture("HDR",          TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
4428    let res_bloom         = editor.add_transient_texture("Bloom",        TextureDesc::render_target(width, height, TextureFormat::RGBA16Float));
4429    let res_sdr           = editor.add_persistent_texture("SDR",         TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
4430    let res_shadow        = editor.add_transient_texture("Shadow",       TextureDesc::shadow_map(2048));
4431    let res_ui            = editor.add_persistent_texture("UI",          TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
4432    editor.set_output_resources(vec![res_ui]);
4433
4434    // Depth pre-pass
4435    let depth_pp = editor.add_pass("DepthPrepass", PassDesc::GBuffer(GBufferPassDesc::default(width, height)));
4436    editor.set_pass_writes(depth_pp, vec![res_depth_prepass]);
4437
4438    // Shadow
4439    let sm = editor.add_pass("ShadowMap", PassDesc::ShadowMap(ShadowMapPassDesc::directional_shadow(2048)));
4440    editor.set_pass_writes(sm, vec![res_shadow]);
4441
4442    // Forward lighting (no GBuffer, uses cluster light list)
4443    let light = editor.add_pass("ForwardLighting", PassDesc::Lighting(LightingPassDesc::default(width, height)));
4444    editor.set_pass_reads(light, vec![res_depth_prepass, res_shadow]);
4445    editor.set_pass_writes(light, vec![res_hdr, res_depth_main]);
4446
4447    // Particles
4448    let particles = editor.add_pass("Particles", PassDesc::Particle(ParticlePassDesc::default(width, height)));
4449    editor.set_pass_reads(particles, vec![res_hdr, res_depth_main]);
4450    editor.set_pass_writes(particles, vec![res_hdr]);
4451
4452    // Bloom
4453    let bloom = editor.add_pass("Bloom", PassDesc::Bloom(BloomPassDesc::default(width, height)));
4454    editor.set_pass_reads(bloom, vec![res_hdr]);
4455    editor.set_pass_writes(bloom, vec![res_bloom]);
4456
4457    // Tonemap
4458    let tonemap = editor.add_pass("ToneMapping", PassDesc::ToneMapping(ToneMappingPassDesc::default(width, height)));
4459    editor.set_pass_reads(tonemap, vec![res_hdr, res_bloom]);
4460    editor.set_pass_writes(tonemap, vec![res_sdr]);
4461
4462    // UI
4463    let ui = editor.add_pass("UI", PassDesc::UI(UIPassDesc::default(width, height)));
4464    editor.set_pass_reads(ui, vec![res_sdr]);
4465    editor.set_pass_writes(ui, vec![res_ui]);
4466
4467    editor
4468}
4469
4470pub fn build_mobile_deferred_pipeline(width: u32, height: u32) -> RenderGraphEditor {
4471    // Simplified pipeline for mobile TBR
4472    let mut editor = RenderGraphEditor::new("MobileDeferred");
4473    let res_albedo   = editor.add_transient_texture("Albedo",   TextureDesc::render_target(width, height, TextureFormat::RGBA8Unorm));
4474    let res_normal   = editor.add_transient_texture("Normal",   TextureDesc::render_target(width, height, TextureFormat::RGBA8Snorm));
4475    let res_depth    = editor.add_transient_texture("Depth",    TextureDesc::depth_target(width, height));
4476    let res_hdr      = editor.add_transient_texture("HDR",      TextureDesc::render_target(width, height, TextureFormat::RG11B10Float));
4477    let res_sdr      = editor.add_persistent_texture("SDR",     TextureDesc::render_target(width, height, TextureFormat::RGBA8UnormSrgb));
4478    editor.set_output_resources(vec![res_sdr]);
4479
4480    let gbuf = editor.add_pass("GBuffer", PassDesc::GBuffer(GBufferPassDesc::default(width, height)));
4481    editor.set_pass_writes(gbuf, vec![res_albedo, res_normal, res_depth]);
4482
4483    let light = editor.add_pass("Lighting", PassDesc::Lighting(LightingPassDesc::default(width, height)));
4484    editor.set_pass_reads(light, vec![res_albedo, res_normal, res_depth]);
4485    editor.set_pass_writes(light, vec![res_hdr]);
4486
4487    let tonemap = editor.add_pass("ToneMapping", PassDesc::ToneMapping(ToneMappingPassDesc::default(width, height)));
4488    editor.set_pass_reads(tonemap, vec![res_hdr]);
4489    editor.set_pass_writes(tonemap, vec![res_sdr]);
4490
4491    editor
4492}
4493
4494// ============================================================
4495//  RESOURCE FORMAT COMPARISON AND COMPATIBILITY TABLE
4496// ============================================================
4497
4498/// Check if two formats are compatible for aliasing (same memory layout requirements)
4499pub fn formats_compatible(a: TextureFormat, b: TextureFormat) -> bool {
4500    let ia = format_info(a);
4501    let ib = format_info(b);
4502    ia.bytes_per_block == ib.bytes_per_block
4503        && ia.block_width == ib.block_width
4504        && ia.block_height == ib.block_height
4505}
4506
4507/// Can the format be used as a color attachment?
4508pub fn is_color_attachment_format(fmt: TextureFormat) -> bool {
4509    let fi = format_info(fmt);
4510    !fi.is_depth && !fi.is_stencil && !fi.is_compressed
4511}
4512
4513/// Can the format be used as a depth/stencil attachment?
4514pub fn is_depth_stencil_attachment_format(fmt: TextureFormat) -> bool {
4515    let fi = format_info(fmt);
4516    fi.is_depth || fi.is_stencil
4517}
4518
4519/// Get the number of bits in each channel
4520pub fn format_channel_bits(fmt: TextureFormat) -> [u8; 4] {
4521    match fmt {
4522        TextureFormat::R8Unorm | TextureFormat::R8Snorm | TextureFormat::R8Uint | TextureFormat::R8Sint => [8, 0, 0, 0],
4523        TextureFormat::RG8Unorm | TextureFormat::RG8Snorm | TextureFormat::RG8Uint | TextureFormat::RG8Sint => [8, 8, 0, 0],
4524        TextureFormat::RGBA8Unorm | TextureFormat::RGBA8UnormSrgb | TextureFormat::RGBA8Snorm | TextureFormat::RGBA8Uint | TextureFormat::RGBA8Sint => [8, 8, 8, 8],
4525        TextureFormat::BGRA8Unorm | TextureFormat::BGRA8UnormSrgb => [8, 8, 8, 8],
4526        TextureFormat::R16Unorm | TextureFormat::R16Float | TextureFormat::R16Uint | TextureFormat::R16Sint => [16, 0, 0, 0],
4527        TextureFormat::RG16Unorm | TextureFormat::RG16Float | TextureFormat::RG16Uint => [16, 16, 0, 0],
4528        TextureFormat::RGBA16Unorm | TextureFormat::RGBA16Float | TextureFormat::RGBA16Uint => [16, 16, 16, 16],
4529        TextureFormat::R32Float | TextureFormat::R32Uint | TextureFormat::R32Sint => [32, 0, 0, 0],
4530        TextureFormat::RG32Float | TextureFormat::RG32Uint => [32, 32, 0, 0],
4531        TextureFormat::RGB32Float => [32, 32, 32, 0],
4532        TextureFormat::RGBA32Float | TextureFormat::RGBA32Uint => [32, 32, 32, 32],
4533        TextureFormat::RGB10A2Unorm => [10, 10, 10, 2],
4534        TextureFormat::RG11B10Float => [11, 11, 10, 0],
4535        TextureFormat::Depth16Unorm => [16, 0, 0, 0],
4536        TextureFormat::Depth24Unorm => [24, 0, 0, 0],
4537        TextureFormat::Depth32Float => [32, 0, 0, 0],
4538        TextureFormat::Depth24UnormStencil8 => [24, 8, 0, 0],
4539        TextureFormat::Depth32FloatStencil8 => [32, 8, 0, 0],
4540        TextureFormat::Stencil8 => [0, 8, 0, 0],
4541        _ => [0, 0, 0, 0],
4542    }
4543}
4544
4545// ============================================================
4546//  END OF FILE
4547// ============================================================
4548
4549#[cfg(test)]
4550mod tests {
4551    use super::*;
4552
4553    #[test]
4554    fn test_format_info_bytes_per_pixel() {
4555        assert_eq!(format_info(TextureFormat::RGBA8Unorm).bytes_per_block, 4);
4556        assert_eq!(format_info(TextureFormat::RGBA16Float).bytes_per_block, 8);
4557        assert_eq!(format_info(TextureFormat::R32Float).bytes_per_block, 4);
4558        let bc1 = format_info(TextureFormat::BC1RgbUnorm);
4559        assert_eq!(bc1.block_width, 4);
4560        assert!((bc1.bytes_per_pixel() - 0.5).abs() < 1e-4);
4561    }
4562
4563    #[test]
4564    fn test_texture_size() {
4565        let sz = texture_size_bytes(TextureFormat::RGBA8Unorm, 1920, 1080, 1);
4566        assert_eq!(sz, 1920 * 1080 * 4);
4567    }
4568
4569    #[test]
4570    fn test_gbuffer_bandwidth() {
4571        let gbuf = GBufferPassDesc::default(1920, 1080);
4572        let bw = gbuf.estimate_write_bandwidth_mb();
4573        assert!(bw > 0.0 && bw < 200.0);
4574    }
4575
4576    #[test]
4577    fn test_halton() {
4578        let h = halton_sequence(1, 2);
4579        assert!((h - 0.5).abs() < 1e-5);
4580        let h2 = halton_sequence(2, 2);
4581        assert!((h2 - 0.25).abs() < 1e-5);
4582    }
4583
4584    #[test]
4585    fn test_ssao_kernel() {
4586        let ssao = SSAOPassDesc::default(1920, 1080);
4587        let kernel = ssao.generate_kernel();
4588        assert_eq!(kernel.len(), ssao.kernel_size as usize);
4589        for s in &kernel {
4590            assert!(s.length() <= 1.0 + 1e-4);
4591        }
4592    }
4593
4594    #[test]
4595    fn test_taa_jitter() {
4596        let taa = TAAPassDesc::default(1920, 1080);
4597        let j0 = taa.halton_jitter(0);
4598        let j1 = taa.halton_jitter(1);
4599        assert!(j0 != j1);
4600        assert!(j0.x.abs() < 1.0 && j0.y.abs() < 1.0);
4601    }
4602
4603    #[test]
4604    fn test_octahedral_encoding() {
4605        let n = Vec3::new(0.0, 1.0, 0.0).normalize();
4606        let e = octahedral_encode(n);
4607        let d = octahedral_decode(e);
4608        assert!((d - n).length() < 1e-3);
4609    }
4610
4611    #[test]
4612    fn test_aces_tone_mapping() {
4613        let op = ToneMappingPassDesc::default(1920, 1080);
4614        let color_in = Vec3::new(1.0, 0.5, 0.2);
4615        let out = op.apply_aces(color_in);
4616        assert!(out.x >= 0.0 && out.x <= 1.0);
4617        assert!(out.y >= 0.0 && out.y <= 1.0);
4618        assert!(out.z >= 0.0 && out.z <= 1.0);
4619    }
4620
4621    #[test]
4622    fn test_bloom_quadratic_threshold() {
4623        let bloom = BloomPassDesc::default(1920, 1080);
4624        assert_eq!(bloom.quadratic_threshold(0.0), 0.0);
4625        let above = bloom.quadratic_threshold(2.0);
4626        assert!(above > 0.0);
4627    }
4628
4629    #[test]
4630    fn test_compile_standard_pipeline() {
4631        let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
4632        let result = editor.compile();
4633        assert!(result.is_ok(), "Compilation failed: {:?}", result);
4634        let compiled = editor.compiled.as_ref().unwrap();
4635        assert!(!compiled.sorted_passes.is_empty());
4636    }
4637
4638    #[test]
4639    fn test_sugiyama_layout() {
4640        let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
4641        editor.visualize();
4642        assert!(editor.layout.is_some());
4643        let layout = editor.layout.as_ref().unwrap();
4644        assert!(!layout.node_positions.is_empty());
4645    }
4646
4647    #[test]
4648    fn test_serialization() {
4649        let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
4650        let json = editor.to_json();
4651        assert!(json.contains("GBuffer"));
4652        assert!(json.contains("\"version\": 1"));
4653    }
4654
4655    #[test]
4656    fn test_hg_phase() {
4657        let fog = VolumetricFogPassDesc::default(1920, 1080);
4658        let p0 = fog.henyey_greenstein(1.0);  // forward scatter
4659        let p1 = fog.henyey_greenstein(-1.0); // back scatter
4660        assert!(p0 > p1); // forward peak
4661    }
4662
4663    #[test]
4664    fn test_barrier_layout_transitions() {
4665        let b = ImageBarrier::layout_transition(
4666            ResourceId(0),
4667            ImageLayout::Undefined,
4668            ImageLayout::ColorAttachmentOptimal,
4669        );
4670        assert_eq!(b.old_layout, ImageLayout::Undefined);
4671        assert_eq!(b.new_layout, ImageLayout::ColorAttachmentOptimal);
4672    }
4673
4674    #[test]
4675    fn test_cluster_grid() {
4676        let grid = ClusteredLightGrid::new(1920, 1080, 16, 24, 0.1, 100.0);
4677        assert_eq!(grid.tiles_x, 120);
4678        assert_eq!(grid.tiles_y, 68);
4679        assert_eq!(grid.total_clusters(), 120 * 68 * 24);
4680    }
4681
4682    #[test]
4683    fn test_cascade_splits() {
4684        let sm = ShadowMapPassDesc::directional_shadow(4096);
4685        let splits = sm.compute_cascade_splits(0.75, 0.1, 200.0);
4686        assert_eq!(splits.len(), 4);
4687        for i in 1..splits.len() { assert!(splits[i] > splits[i-1]); }
4688    }
4689
4690    #[test]
4691    fn test_sphere_in_frustum() {
4692        let vp = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 16.0/9.0, 0.1, 100.0);
4693        let planes = frustum_planes_from_view_proj(vp);
4694        assert!(sphere_in_frustum(&planes, Vec3::new(0.0, 0.0, -10.0), 1.0));
4695    }
4696}
4697
4698// ============================================================
4699//  SHADOW ATLAS MANAGEMENT
4700// ============================================================
4701
4702#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4703pub struct AtlasRegion {
4704    pub x: u32,
4705    pub y: u32,
4706    pub width: u32,
4707    pub height: u32,
4708}
4709
4710impl AtlasRegion {
4711    pub fn uv_offset(&self, atlas_size: u32) -> Vec2 {
4712        Vec2::new(self.x as f32 / atlas_size as f32, self.y as f32 / atlas_size as f32)
4713    }
4714    pub fn uv_scale(&self, atlas_size: u32) -> Vec2 {
4715        Vec2::new(self.width as f32 / atlas_size as f32, self.height as f32 / atlas_size as f32)
4716    }
4717    pub fn uv_transform(&self, atlas_size: u32) -> Vec4 {
4718        let off = self.uv_offset(atlas_size);
4719        let sc  = self.uv_scale(atlas_size);
4720        Vec4::new(sc.x, sc.y, off.x, off.y)
4721    }
4722}
4723
4724pub struct ShadowAtlas {
4725    pub atlas_size: u32,
4726    pub regions: Vec<(u32, AtlasRegion)>, // (light_id, region)
4727    pub free_rects: Vec<AtlasRegion>,
4728}
4729
4730impl ShadowAtlas {
4731    pub fn new(atlas_size: u32) -> Self {
4732        ShadowAtlas {
4733            atlas_size,
4734            regions: Vec::new(),
4735            free_rects: vec![AtlasRegion { x: 0, y: 0, width: atlas_size, height: atlas_size }],
4736        }
4737    }
4738
4739    /// Guillotine rectangle packing: find the best-fit free rect for a given size
4740    pub fn allocate(&mut self, light_id: u32, width: u32, height: u32) -> Option<AtlasRegion> {
4741        // Find the smallest free rect that fits
4742        let best = self.free_rects.iter().enumerate()
4743            .filter(|(_, r)| r.width >= width && r.height >= height)
4744            .min_by_key(|(_, r)| r.width * r.height);
4745        let (idx, region) = best.map(|(i, r)| (i, *r))?;
4746        self.free_rects.remove(idx);
4747        let allocated = AtlasRegion { x: region.x, y: region.y, width, height };
4748        // Guillotine split: choose the split that leaves less waste
4749        let right = AtlasRegion { x: region.x + width, y: region.y, width: region.width - width, height };
4750        let bottom = AtlasRegion { x: region.x, y: region.y + height, width: region.width, height: region.height - height };
4751        if right.width > 0 { self.free_rects.push(right); }
4752        if bottom.height > 0 { self.free_rects.push(bottom); }
4753        self.regions.push((light_id, allocated));
4754        Some(allocated)
4755    }
4756
4757    pub fn free_region(&mut self, light_id: u32) {
4758        if let Some(pos) = self.regions.iter().position(|(id, _)| *id == light_id) {
4759            let (_, region) = self.regions.remove(pos);
4760            self.free_rects.push(region);
4761            // Merge adjacent free rects (simplified: just keep them separate)
4762        }
4763    }
4764
4765    pub fn region_for_light(&self, light_id: u32) -> Option<AtlasRegion> {
4766        self.regions.iter().find(|(id, _)| *id == light_id).map(|(_, r)| *r)
4767    }
4768
4769    pub fn utilization(&self) -> f32 {
4770        let used: u32 = self.regions.iter().map(|(_, r)| r.width * r.height).sum();
4771        let total = self.atlas_size * self.atlas_size;
4772        used as f32 / total as f32
4773    }
4774}
4775
4776// ============================================================
4777//  RENDER GRAPH PROFILING QUERIES
4778// ============================================================
4779
4780#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4781pub enum QueryType { Timestamp, Occlusion, PipelineStatistics }
4782
4783#[derive(Debug, Clone)]
4784pub struct TimestampQuery {
4785    pub pass_id: PassId,
4786    pub name: String,
4787    pub start_index: u32,
4788    pub end_index: u32,
4789}
4790
4791#[derive(Debug, Clone)]
4792pub struct QueryPool {
4793    pub query_type: QueryType,
4794    pub capacity: u32,
4795    pub next_index: u32,
4796    pub timestamp_period_ns: f64,   // nanoseconds per GPU tick
4797}
4798
4799impl QueryPool {
4800    pub fn new_timestamp(capacity: u32, timestamp_period_ns: f64) -> Self {
4801        QueryPool { query_type: QueryType::Timestamp, capacity, next_index: 0, timestamp_period_ns }
4802    }
4803
4804    pub fn allocate_pair(&mut self) -> Option<(u32, u32)> {
4805        if self.next_index + 2 <= self.capacity {
4806            let start = self.next_index;
4807            self.next_index += 2;
4808            Some((start, start + 1))
4809        } else {
4810            None
4811        }
4812    }
4813
4814    pub fn reset(&mut self) { self.next_index = 0; }
4815
4816    pub fn ticks_to_ms(&self, ticks: u64) -> f64 {
4817        (ticks as f64 * self.timestamp_period_ns) / 1_000_000.0
4818    }
4819
4820    pub fn ticks_to_us(&self, ticks: u64) -> f64 {
4821        (ticks as f64 * self.timestamp_period_ns) / 1_000.0
4822    }
4823}
4824
4825pub struct ProfilingManager {
4826    pub timestamp_pool: QueryPool,
4827    pub queries: Vec<TimestampQuery>,
4828    pub results: HashMap<PassId, f64>, // pass -> GPU time in ms
4829}
4830
4831impl ProfilingManager {
4832    pub fn new(max_passes: u32, timestamp_period_ns: f64) -> Self {
4833        ProfilingManager {
4834            timestamp_pool: QueryPool::new_timestamp(max_passes * 2, timestamp_period_ns),
4835            queries: Vec::new(),
4836            results: HashMap::new(),
4837        }
4838    }
4839
4840    pub fn begin_pass(&mut self, pass_id: PassId, name: &str) -> Option<u32> {
4841        let (start, end) = self.timestamp_pool.allocate_pair()?;
4842        self.queries.push(TimestampQuery { pass_id, name: name.to_owned(), start_index: start, end_index: end });
4843        Some(start)
4844    }
4845
4846    pub fn process_results(&mut self, raw_timestamps: &[u64]) {
4847        for q in &self.queries {
4848            let start_idx = q.start_index as usize;
4849            let end_idx = q.end_index as usize;
4850            if end_idx < raw_timestamps.len() {
4851                let ticks = raw_timestamps[end_idx].saturating_sub(raw_timestamps[start_idx]);
4852                let ms = self.timestamp_pool.ticks_to_ms(ticks);
4853                self.results.insert(q.pass_id, ms);
4854            }
4855        }
4856    }
4857
4858    pub fn reset_frame(&mut self) {
4859        self.timestamp_pool.reset();
4860        self.queries.clear();
4861    }
4862
4863    pub fn get_pass_time_ms(&self, pass_id: PassId) -> f64 {
4864        *self.results.get(&pass_id).unwrap_or(&0.0)
4865    }
4866
4867    pub fn total_gpu_time_ms(&self) -> f64 {
4868        self.results.values().sum()
4869    }
4870}
4871
4872// ============================================================
4873//  RENDER GRAPH PASS PARAMETER BINDING
4874// ============================================================
4875
4876#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4877pub enum BindingType {
4878    SampledTexture,
4879    StorageTexture,
4880    UniformBuffer,
4881    StorageBuffer,
4882    Sampler,
4883    InputAttachment,
4884    AccelerationStructure,
4885}
4886
4887#[derive(Debug, Clone)]
4888pub struct DescriptorBinding {
4889    pub binding: u32,
4890    pub binding_type: BindingType,
4891    pub resource_id: ResourceId,
4892    pub stage_flags: PipelineStageFlags,
4893    pub array_count: u32,
4894}
4895
4896#[derive(Debug, Clone)]
4897pub struct DescriptorSet {
4898    pub set_index: u32,
4899    pub bindings: Vec<DescriptorBinding>,
4900}
4901
4902impl DescriptorSet {
4903    pub fn new(set_index: u32) -> Self { DescriptorSet { set_index, bindings: Vec::new() } }
4904
4905    pub fn bind_texture(&mut self, binding: u32, resource_id: ResourceId, stage: PipelineStageFlags) {
4906        self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::SampledTexture, resource_id, stage_flags: stage, array_count: 1 });
4907    }
4908
4909    pub fn bind_storage_texture(&mut self, binding: u32, resource_id: ResourceId, stage: PipelineStageFlags) {
4910        self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::StorageTexture, resource_id, stage_flags: stage, array_count: 1 });
4911    }
4912
4913    pub fn bind_uniform_buffer(&mut self, binding: u32, resource_id: ResourceId) {
4914        self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::UniformBuffer, resource_id, stage_flags: PipelineStageFlags::VERTEX_SHADER | PipelineStageFlags::FRAGMENT_SHADER, array_count: 1 });
4915    }
4916
4917    pub fn bind_input_attachment(&mut self, binding: u32, resource_id: ResourceId) {
4918        self.bindings.push(DescriptorBinding { binding, binding_type: BindingType::InputAttachment, resource_id, stage_flags: PipelineStageFlags::FRAGMENT_SHADER, array_count: 1 });
4919    }
4920
4921    pub fn has_input_attachments(&self) -> bool {
4922        self.bindings.iter().any(|b| b.binding_type == BindingType::InputAttachment)
4923    }
4924}
4925
4926// Build descriptor sets for a standard lighting pass
4927pub fn build_lighting_descriptor_set(desc: &LightingPassDesc) -> Vec<DescriptorSet> {
4928    let mut set0 = DescriptorSet::new(0);
4929    let frag = PipelineStageFlags::FRAGMENT_SHADER;
4930    set0.bind_input_attachment(0, desc.input_albedo);
4931    set0.bind_input_attachment(1, desc.input_normal);
4932    set0.bind_input_attachment(2, desc.input_material);
4933    set0.bind_input_attachment(3, desc.input_depth);
4934    set0.bind_texture(4, desc.input_shadow_map, frag);
4935    set0.bind_texture(5, desc.input_ssao, frag);
4936    vec![set0]
4937}
4938
4939// Build descriptor sets for SSAO
4940pub fn build_ssao_descriptor_set(desc: &SSAOPassDesc) -> Vec<DescriptorSet> {
4941    let mut set0 = DescriptorSet::new(0);
4942    let frag = PipelineStageFlags::FRAGMENT_SHADER;
4943    set0.bind_texture(0, desc.input_depth,  frag);
4944    set0.bind_texture(1, desc.input_normal, frag);
4945    // binding 2 = noise texture (static, from persistent resource)
4946    // binding 3 = kernel UBO
4947    vec![set0]
4948}
4949
4950// ============================================================
4951//  PUSH CONSTANTS (per-pass frame data)
4952// ============================================================
4953
4954#[repr(C)]
4955#[derive(Debug, Clone, Copy)]
4956pub struct FrameUniforms {
4957    pub view: Mat4,
4958    pub proj: Mat4,
4959    pub view_proj: Mat4,
4960    pub inv_view: Mat4,
4961    pub inv_proj: Mat4,
4962    pub inv_view_proj: Mat4,
4963    pub prev_view_proj: Mat4,
4964    pub camera_pos: Vec4,
4965    pub camera_dir: Vec4,
4966    pub resolution: Vec4,          // (width, height, 1/width, 1/height)
4967    pub time: Vec4,                 // (time, delta_time, frame_index, -)
4968    pub near_far: Vec4,            // (near, far, 1/near, 1/far)
4969    pub exposure: Vec4,            // (exposure, ev100, -, -)
4970    pub jitter: Vec4,              // (jitter_x, jitter_y, prev_jitter_x, prev_jitter_y)
4971    pub fog_params: Vec4,          // (density, scatter, absorption, -)
4972    pub ambient: Vec4,
4973}
4974
4975impl FrameUniforms {
4976    pub fn new(view: Mat4, proj: Mat4, near: f32, far: f32, width: u32, height: u32) -> Self {
4977        let view_proj = proj * view;
4978        FrameUniforms {
4979            view,
4980            proj,
4981            view_proj,
4982            inv_view: view.inverse(),
4983            inv_proj: proj.inverse(),
4984            inv_view_proj: view_proj.inverse(),
4985            prev_view_proj: view_proj,
4986            camera_pos: Vec4::new(0.0, 0.0, 0.0, 1.0),
4987            camera_dir: Vec4::new(0.0, 0.0, -1.0, 0.0),
4988            resolution: Vec4::new(width as f32, height as f32, 1.0 / width as f32, 1.0 / height as f32),
4989            time: Vec4::new(0.0, 0.016, 0.0, 0.0),
4990            near_far: Vec4::new(near, far, 1.0 / near, 1.0 / far),
4991            exposure: Vec4::new(1.0, 0.0, 0.0, 0.0),
4992            jitter: Vec4::ZERO,
4993            fog_params: Vec4::new(0.01, 0.05, 0.005, 0.0),
4994            ambient: Vec4::new(0.03, 0.03, 0.05, 1.0),
4995        }
4996    }
4997    pub fn size_bytes() -> usize { std::mem::size_of::<FrameUniforms>() }
4998}
4999
5000#[repr(C)]
5001#[derive(Debug, Clone, Copy)]
5002pub struct ShadowUniforms {
5003    pub light_view_proj: [Mat4; 4],
5004    pub cascade_splits: Vec4,
5005    pub shadow_map_size: Vec4,
5006    pub shadow_bias: Vec4,
5007    pub pcf_radius: f32,
5008    pub pcss_light_size: f32,
5009    pub _pad: [f32; 2],
5010}
5011
5012impl ShadowUniforms {
5013    pub fn new(light_vps: [Mat4; 4], splits: [f32; 4], map_size: f32) -> Self {
5014        ShadowUniforms {
5015            light_view_proj: light_vps,
5016            cascade_splits: Vec4::from(splits),
5017            shadow_map_size: Vec4::new(map_size, 1.0 / map_size, 0.0, 0.0),
5018            shadow_bias: Vec4::new(0.0005, 0.0, 0.0, 0.0),
5019            pcf_radius: 2.0,
5020            pcss_light_size: 0.5,
5021            _pad: [0.0; 2],
5022        }
5023    }
5024}
5025
5026#[repr(C)]
5027#[derive(Debug, Clone, Copy)]
5028pub struct BloomUniforms {
5029    pub threshold: f32,
5030    pub knee: f32,
5031    pub intensity: f32,
5032    pub scatter: f32,
5033    pub mip_level: u32,
5034    pub _pad: [u32; 3],
5035    pub inv_resolution: Vec2,
5036    pub _pad2: Vec2,
5037}
5038
5039#[repr(C)]
5040#[derive(Debug, Clone, Copy)]
5041pub struct TAAUniforms {
5042    pub blend_factor: f32,
5043    pub variance_clip_gamma: f32,
5044    pub velocity_weight_scale: f32,
5045    pub _pad: f32,
5046    pub jitter: Vec4,
5047    pub resolution: Vec4,
5048}
5049
5050#[repr(C)]
5051#[derive(Debug, Clone, Copy)]
5052pub struct SSAOUniforms {
5053    pub radius: f32,
5054    pub bias: f32,
5055    pub power: f32,
5056    pub kernel_size: u32,
5057    pub noise_scale: Vec2,
5058    pub _pad: Vec2,
5059}
5060
5061// ============================================================
5062//  GPU MEMORY BUDGET TRACKER
5063// ============================================================
5064
5065pub struct GpuMemoryBudget {
5066    pub device_local_total: u64,
5067    pub device_local_used: u64,
5068    pub host_visible_total: u64,
5069    pub host_visible_used: u64,
5070    pub allocations: Vec<(String, u64, bool)>, // (name, size, is_device_local)
5071}
5072
5073impl GpuMemoryBudget {
5074    pub fn new(device_local_mb: u64, host_visible_mb: u64) -> Self {
5075        GpuMemoryBudget {
5076            device_local_total: device_local_mb * 1024 * 1024,
5077            device_local_used: 0,
5078            host_visible_total: host_visible_mb * 1024 * 1024,
5079            host_visible_used: 0,
5080            allocations: Vec::new(),
5081        }
5082    }
5083
5084    pub fn allocate(&mut self, name: &str, size: u64, device_local: bool) -> bool {
5085        if device_local {
5086            if self.device_local_used + size > self.device_local_total { return false; }
5087            self.device_local_used += size;
5088        } else {
5089            if self.host_visible_used + size > self.host_visible_total { return false; }
5090            self.host_visible_used += size;
5091        }
5092        self.allocations.push((name.to_owned(), size, device_local));
5093        true
5094    }
5095
5096    pub fn free(&mut self, name: &str) {
5097        if let Some(pos) = self.allocations.iter().position(|(n, _, _)| n == name) {
5098            let (_, size, device_local) = self.allocations.remove(pos);
5099            if device_local { self.device_local_used = self.device_local_used.saturating_sub(size); }
5100            else             { self.host_visible_used = self.host_visible_used.saturating_sub(size); }
5101        }
5102    }
5103
5104    pub fn device_local_free_mb(&self) -> f64 {
5105        (self.device_local_total - self.device_local_used) as f64 / (1024.0 * 1024.0)
5106    }
5107
5108    pub fn device_local_utilization(&self) -> f32 {
5109        if self.device_local_total == 0 { 0.0 } else { self.device_local_used as f32 / self.device_local_total as f32 }
5110    }
5111
5112    pub fn largest_allocation(&self) -> Option<(&str, u64)> {
5113        self.allocations.iter().max_by_key(|(_, s, _)| *s).map(|(n, s, _)| (n.as_str(), *s))
5114    }
5115
5116    pub fn report(&self) -> String {
5117        let mut s = String::new();
5118        s.push_str(&format!("Device-local: {:.1}MB / {:.1}MB ({:.1}%)\n",
5119            self.device_local_used as f64 / (1024.0*1024.0),
5120            self.device_local_total as f64 / (1024.0*1024.0),
5121            self.device_local_utilization() * 100.0));
5122        s.push_str(&format!("Host-visible:  {:.1}MB / {:.1}MB\n",
5123            self.host_visible_used as f64 / (1024.0*1024.0),
5124            self.host_visible_total as f64 / (1024.0*1024.0)));
5125        for (name, size, dl) in &self.allocations {
5126            s.push_str(&format!("  {:40} {:6.1}MB  {}\n", name, *size as f64 / (1024.0*1024.0), if *dl { "DEVICE" } else { "HOST" }));
5127        }
5128        s
5129    }
5130}
5131
5132// ============================================================
5133//  RENDER GRAPH RESOURCE GRAPH (VISUAL — resource nodes)
5134// ============================================================
5135
5136#[derive(Debug, Clone)]
5137pub struct ResourceNodeVisual {
5138    pub id: ResourceId,
5139    pub pos: Vec2,
5140    pub size: Vec2,
5141    pub color: Vec4,
5142    pub label: String,
5143    pub tooltip: String,
5144    pub lifetime_bar_start: f32, // 0..1 normalized position in frame timeline
5145    pub lifetime_bar_end: f32,
5146}
5147
5148impl ResourceNodeVisual {
5149    pub fn from_resource(res: &RenderGraphResource, total_passes: usize) -> Self {
5150        let total = total_passes.max(1) as f32;
5151        let color = resource_lifetime_color(res.lifetime);
5152        let tooltip = match &res.desc {
5153            ResourceDesc::Texture(t) => format!("{:?} {}x{} mip:{} {:?} {:?}", res.lifetime, t.width, t.height, t.mip_levels, t.format, t.kind),
5154            ResourceDesc::Buffer(b) => format!("{:?} {} bytes stride:{}", res.lifetime, b.size, b.stride),
5155        };
5156        ResourceNodeVisual {
5157            id: res.id, pos: Vec2::ZERO, size: Vec2::new(140.0, 36.0), color,
5158            label: res.name.clone(), tooltip,
5159            lifetime_bar_start: if res.first_use == usize::MAX { 0.0 } else { res.first_use as f32 / total },
5160            lifetime_bar_end: res.last_use as f32 / total,
5161        }
5162    }
5163}
5164
5165fn resource_lifetime_color(lt: ResourceLifetime) -> Vec4 {
5166    match lt {
5167        ResourceLifetime::Transient  => Vec4::new(0.15, 0.55, 0.25, 0.85),
5168        ResourceLifetime::Persistent => Vec4::new(0.55, 0.15, 0.15, 0.85),
5169        ResourceLifetime::Imported   => Vec4::new(0.15, 0.25, 0.55, 0.85),
5170    }
5171}
5172
5173// ============================================================
5174//  PASS DEPENDENCY MATRIX
5175// ============================================================
5176
5177pub struct DependencyMatrix {
5178    pub pass_ids: Vec<PassId>,
5179    pub matrix: Vec<Vec<bool>>, // matrix[i][j] = true means pass i depends on pass j
5180}
5181
5182impl DependencyMatrix {
5183    pub fn build(passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> Self {
5184        let n = passes.len();
5185        let pass_index: HashMap<PassId, usize> = passes.iter().enumerate().map(|(i, p)| (*p, i)).collect();
5186        let mut matrix = vec![vec![false; n]; n];
5187        // Direct dependencies
5188        for (src, dsts) in edges {
5189            if let Some(&si) = pass_index.get(src) {
5190                for dst in dsts {
5191                    if let Some(&di) = pass_index.get(dst) {
5192                        matrix[di][si] = true; // di depends on si
5193                    }
5194                }
5195            }
5196        }
5197        // Transitive closure (Floyd-Warshall)
5198        for k in 0..n {
5199            for i in 0..n {
5200                for j in 0..n {
5201                    if matrix[i][k] && matrix[k][j] {
5202                        matrix[i][j] = true;
5203                    }
5204                }
5205            }
5206        }
5207        DependencyMatrix { pass_ids: passes.to_vec(), matrix }
5208    }
5209
5210    pub fn depends_on(&self, a: PassId, b: PassId) -> bool {
5211        let ai = self.pass_ids.iter().position(|&p| p == a);
5212        let bi = self.pass_ids.iter().position(|&p| p == b);
5213        match (ai, bi) {
5214            (Some(i), Some(j)) => self.matrix[i][j],
5215            _ => false,
5216        }
5217    }
5218
5219    pub fn can_execute_in_parallel(&self, a: PassId, b: PassId) -> bool {
5220        !self.depends_on(a, b) && !self.depends_on(b, a)
5221    }
5222
5223    pub fn render_html_table(&self, pass_names: &HashMap<PassId, String>) -> String {
5224        let mut s = String::new();
5225        s.push_str("<table border='1'><tr><th></th>");
5226        for pid in &self.pass_ids {
5227            let name = pass_names.get(pid).map(|n| n.as_str()).unwrap_or("?");
5228            s.push_str(&format!("<th>{}</th>", name));
5229        }
5230        s.push_str("</tr>");
5231        for (i, row_pid) in self.pass_ids.iter().enumerate() {
5232            let row_name = pass_names.get(row_pid).map(|n| n.as_str()).unwrap_or("?");
5233            s.push_str(&format!("<tr><td>{}</td>", row_name));
5234            for j in 0..self.pass_ids.len() {
5235                let cell = if self.matrix[i][j] { "✓" } else { "" };
5236                let color = if self.matrix[i][j] { "#aaffaa" } else { "white" };
5237                s.push_str(&format!("<td style='background:{}'>{}</td>", color, cell));
5238            }
5239            s.push_str("</tr>");
5240        }
5241        s.push_str("</table>");
5242        s
5243    }
5244}
5245
5246// ============================================================
5247//  RENDER GRAPH DIFF (compare two graphs for hot-reload)
5248// ============================================================
5249
5250#[derive(Debug, Clone)]
5251pub enum GraphDiff {
5252    PassAdded(PassId, String),
5253    PassRemoved(PassId, String),
5254    PassModified(PassId, String),
5255    ResourceAdded(ResourceId, String),
5256    ResourceRemoved(ResourceId, String),
5257    ResourceModified(ResourceId, String),
5258    ConnectionAdded(PassId, PassId),
5259    ConnectionRemoved(PassId, PassId),
5260}
5261
5262pub fn diff_render_graphs(old: &RenderGraphEditor, new: &RenderGraphEditor) -> Vec<GraphDiff> {
5263    let mut diffs = Vec::new();
5264    // Check added/removed passes
5265    for pid in new.passes.keys() {
5266        if !old.passes.contains_key(pid) {
5267            let name = new.passes[pid].name.clone();
5268            diffs.push(GraphDiff::PassAdded(*pid, name));
5269        }
5270    }
5271    for pid in old.passes.keys() {
5272        if !new.passes.contains_key(pid) {
5273            let name = old.passes[pid].name.clone();
5274            diffs.push(GraphDiff::PassRemoved(*pid, name));
5275        }
5276    }
5277    // Check modified passes (simplified: check reads/writes changed)
5278    for (pid, new_pass) in &new.passes {
5279        if let Some(old_pass) = old.passes.get(pid) {
5280            if old_pass.reads != new_pass.reads || old_pass.writes != new_pass.writes || !old_pass.enabled == !new_pass.enabled {
5281                diffs.push(GraphDiff::PassModified(*pid, new_pass.name.clone()));
5282            }
5283        }
5284    }
5285    // Check added/removed resources
5286    for rid in new.resources.keys() {
5287        if !old.resources.contains_key(rid) {
5288            diffs.push(GraphDiff::ResourceAdded(*rid, new.resources[rid].name.clone()));
5289        }
5290    }
5291    for rid in old.resources.keys() {
5292        if !new.resources.contains_key(rid) {
5293            diffs.push(GraphDiff::ResourceRemoved(*rid, old.resources[rid].name.clone()));
5294        }
5295    }
5296    // Check connections
5297    let old_connections = collect_connections(old);
5298    let new_connections = collect_connections(new);
5299    for conn in &new_connections {
5300        if !old_connections.contains(conn) { diffs.push(GraphDiff::ConnectionAdded(conn.0, conn.1)); }
5301    }
5302    for conn in &old_connections {
5303        if !new_connections.contains(conn) { diffs.push(GraphDiff::ConnectionRemoved(conn.0, conn.1)); }
5304    }
5305    diffs
5306}
5307
5308fn collect_connections(editor: &RenderGraphEditor) -> HashSet<(PassId, PassId)> {
5309    let mut conns = HashSet::new();
5310    for (src, src_pass) in &editor.passes {
5311        for rid in &src_pass.writes {
5312            for (dst, dst_pass) in &editor.passes {
5313                if dst_pass.reads.contains(rid) { conns.insert((*src, *dst)); }
5314            }
5315        }
5316    }
5317    conns
5318}
5319
5320// ============================================================
5321//  BANDWIDTH PROFILER — per resource access tracking
5322// ============================================================
5323
5324#[derive(Debug, Clone)]
5325pub struct ResourceAccessRecord {
5326    pub pass_id: PassId,
5327    pub resource_id: ResourceId,
5328    pub is_write: bool,
5329    pub bytes_accessed: u64,
5330    pub access_mask: AccessFlags,
5331    pub layout: ImageLayout,
5332}
5333
5334pub struct BandwidthProfiler {
5335    pub records: Vec<ResourceAccessRecord>,
5336    pub per_resource_read_mb: HashMap<ResourceId, f32>,
5337    pub per_resource_write_mb: HashMap<ResourceId, f32>,
5338    pub per_pass_read_mb: HashMap<PassId, f32>,
5339    pub per_pass_write_mb: HashMap<PassId, f32>,
5340}
5341
5342impl BandwidthProfiler {
5343    pub fn new() -> Self {
5344        BandwidthProfiler {
5345            records: Vec::new(),
5346            per_resource_read_mb: HashMap::new(),
5347            per_resource_write_mb: HashMap::new(),
5348            per_pass_read_mb: HashMap::new(),
5349            per_pass_write_mb: HashMap::new(),
5350        }
5351    }
5352
5353    pub fn record(&mut self, pass: PassId, resource: ResourceId, is_write: bool, bytes: u64, access: AccessFlags, layout: ImageLayout) {
5354        self.records.push(ResourceAccessRecord { pass_id: pass, resource_id: resource, is_write, bytes_accessed: bytes, access_mask: access, layout });
5355    }
5356
5357    pub fn compute_totals(&mut self) {
5358        self.per_resource_read_mb.clear();
5359        self.per_resource_write_mb.clear();
5360        self.per_pass_read_mb.clear();
5361        self.per_pass_write_mb.clear();
5362        for rec in &self.records {
5363            let mb = rec.bytes_accessed as f32 / (1024.0 * 1024.0);
5364            if rec.is_write {
5365                *self.per_resource_write_mb.entry(rec.resource_id).or_insert(0.0) += mb;
5366                *self.per_pass_write_mb.entry(rec.pass_id).or_insert(0.0) += mb;
5367            } else {
5368                *self.per_resource_read_mb.entry(rec.resource_id).or_insert(0.0) += mb;
5369                *self.per_pass_read_mb.entry(rec.pass_id).or_insert(0.0) += mb;
5370            }
5371        }
5372    }
5373
5374    pub fn total_bandwidth_mb(&self) -> f32 {
5375        let reads: f32 = self.per_resource_read_mb.values().sum();
5376        let writes: f32 = self.per_resource_write_mb.values().sum();
5377        reads + writes
5378    }
5379
5380    pub fn top_bandwidth_resources(&self, n: usize) -> Vec<(ResourceId, f32)> {
5381        let mut combined: HashMap<ResourceId, f32> = HashMap::new();
5382        for (rid, &r) in &self.per_resource_read_mb { *combined.entry(*rid).or_insert(0.0) += r; }
5383        for (rid, &w) in &self.per_resource_write_mb { *combined.entry(*rid).or_insert(0.0) += w; }
5384        let mut v: Vec<(ResourceId, f32)> = combined.into_iter().collect();
5385        v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
5386        v.truncate(n);
5387        v
5388    }
5389}
5390
5391// ============================================================
5392//  FULL PIPELINE PRESETS
5393// ============================================================
5394
5395pub struct PipelinePreset;
5396impl PipelinePreset {
5397    /// High-quality PC preset
5398    pub fn high_quality_pc(width: u32, height: u32) -> RenderGraphEditor {
5399        let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(width, height);
5400        editor.name = "High Quality PC".to_owned();
5401        // Enable all passes
5402        for pass in editor.passes.values_mut() { pass.enabled = true; }
5403        editor
5404    }
5405
5406    /// Medium quality (no SSR, half-res SSAO)
5407    pub fn medium_quality(width: u32, height: u32) -> RenderGraphEditor {
5408        let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(width, height);
5409        editor.name = "Medium Quality".to_owned();
5410        // Disable SSR
5411        for pass in editor.passes.values_mut() {
5412            if matches!(pass.desc.kind(), PassKind::SSR) { pass.enabled = false; }
5413        }
5414        editor
5415    }
5416
5417    /// Mobile TBR preset
5418    pub fn mobile(width: u32, height: u32) -> RenderGraphEditor {
5419        let editor = build_mobile_deferred_pipeline(width, height);
5420        editor
5421    }
5422
5423    /// Shadow-only preset (for depth-only renders, e.g. cube shadow maps)
5424    pub fn shadow_only(resolution: u32) -> RenderGraphEditor {
5425        let mut editor = RenderGraphEditor::new("ShadowOnly");
5426        let res_shadow = editor.add_transient_texture("ShadowMap", TextureDesc::shadow_map(resolution));
5427        editor.set_output_resources(vec![res_shadow]);
5428        let sm_pass = editor.add_pass("ShadowMap", PassDesc::ShadowMap(ShadowMapPassDesc::directional_shadow(resolution)));
5429        editor.set_pass_writes(sm_pass, vec![res_shadow]);
5430        editor
5431    }
5432}
5433
5434// ============================================================
5435//  RENDER GRAPH NODE COMMENTS / ANNOTATIONS
5436// ============================================================
5437
5438#[derive(Debug, Clone)]
5439pub struct NodeAnnotation {
5440    pub pass_id: PassId,
5441    pub title: String,
5442    pub body: String,
5443    pub color: Vec4,
5444    pub pinned: bool,
5445    pub offset: Vec2,
5446}
5447
5448impl NodeAnnotation {
5449    pub fn new(pass_id: PassId, title: &str, body: &str) -> Self {
5450        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) }
5451    }
5452    pub fn world_pos(&self, pass_pos: Vec2) -> Vec2 { pass_pos + self.offset }
5453}
5454
5455pub struct AnnotationManager {
5456    pub annotations: HashMap<PassId, Vec<NodeAnnotation>>,
5457}
5458
5459impl AnnotationManager {
5460    pub fn new() -> Self { AnnotationManager { annotations: HashMap::new() } }
5461    pub fn add(&mut self, ann: NodeAnnotation) { self.annotations.entry(ann.pass_id).or_default().push(ann); }
5462    pub fn get(&self, pass_id: PassId) -> &[NodeAnnotation] { self.annotations.get(&pass_id).map(|v| v.as_slice()).unwrap_or(&[]) }
5463    pub fn remove_all(&mut self, pass_id: PassId) { self.annotations.remove(&pass_id); }
5464}
5465
5466// ============================================================
5467//  RENDER GRAPH UNDO/REDO HISTORY
5468// ============================================================
5469
5470#[derive(Debug, Clone)]
5471pub enum EditorAction {
5472    AddPass(PassId, String),
5473    RemovePass(PassId, String),
5474    MovePass(PassId, Vec2, Vec2), // pass_id, old_pos, new_pos
5475    ConnectResources(PassId, PassId, ResourceId),
5476    DisconnectResources(PassId, PassId, ResourceId),
5477    TogglePassEnabled(PassId, bool), // pass_id, was_enabled
5478    RenamePass(PassId, String, String), // pass_id, old_name, new_name
5479    SetOutputResource(Vec<ResourceId>, Vec<ResourceId>),
5480}
5481
5482pub struct EditorHistory {
5483    pub undo_stack: VecDeque<EditorAction>,
5484    pub redo_stack: VecDeque<EditorAction>,
5485    pub max_history: usize,
5486}
5487
5488impl EditorHistory {
5489    pub fn new(max: usize) -> Self {
5490        EditorHistory { undo_stack: VecDeque::new(), redo_stack: VecDeque::new(), max_history: max }
5491    }
5492    pub fn push(&mut self, action: EditorAction) {
5493        if self.undo_stack.len() >= self.max_history {
5494            self.undo_stack.pop_front();
5495        }
5496        self.undo_stack.push_back(action);
5497        self.redo_stack.clear();
5498    }
5499    pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
5500    pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
5501    pub fn peek_undo(&self) -> Option<&EditorAction> { self.undo_stack.back() }
5502    pub fn pop_undo(&mut self) -> Option<EditorAction> { self.undo_stack.pop_back() }
5503    pub fn push_redo(&mut self, action: EditorAction) { self.redo_stack.push_back(action); }
5504    pub fn pop_redo(&mut self) -> Option<EditorAction> { self.redo_stack.pop_back() }
5505}
5506
5507// ============================================================
5508//  RENDER GRAPH PASS GROUPS (named groups for organization)
5509// ============================================================
5510
5511#[derive(Debug, Clone)]
5512pub struct PassGroup {
5513    pub id: u32,
5514    pub name: String,
5515    pub passes: Vec<PassId>,
5516    pub color: Vec4,
5517    pub collapsed: bool,
5518    pub bounds: (Vec2, Vec2), // min, max in editor space
5519}
5520
5521impl PassGroup {
5522    pub fn new(id: u32, name: &str, passes: Vec<PassId>, color: Vec4) -> Self {
5523        PassGroup { id, name: name.to_owned(), passes, color, collapsed: false, bounds: (Vec2::ZERO, Vec2::ZERO) }
5524    }
5525
5526    pub fn compute_bounds(&mut self, pass_positions: &HashMap<PassId, Vec2>, pass_sizes: &HashMap<PassId, Vec2>) {
5527        let mut min = Vec2::splat(f32::MAX);
5528        let mut max = Vec2::splat(f32::MIN);
5529        for pid in &self.passes {
5530            if let (Some(&pos), Some(&size)) = (pass_positions.get(pid), pass_sizes.get(pid)) {
5531                min = min.min(pos);
5532                max = max.max(pos + size);
5533            }
5534        }
5535        let padding = Vec2::splat(20.0);
5536        self.bounds = (min - padding, max + padding);
5537    }
5538
5539    pub fn contains_point(&self, pt: Vec2) -> bool {
5540        pt.x >= self.bounds.0.x && pt.x <= self.bounds.1.x &&
5541        pt.y >= self.bounds.0.y && pt.y <= self.bounds.1.y
5542    }
5543}
5544
5545pub struct PassGroupManager {
5546    pub groups: Vec<PassGroup>,
5547    next_id: u32,
5548}
5549
5550impl PassGroupManager {
5551    pub fn new() -> Self { PassGroupManager { groups: Vec::new(), next_id: 0 } }
5552    pub fn add_group(&mut self, name: &str, passes: Vec<PassId>, color: Vec4) -> u32 {
5553        let id = self.next_id;
5554        self.groups.push(PassGroup::new(id, name, passes, color));
5555        self.next_id += 1;
5556        id
5557    }
5558    pub fn group_for_pass(&self, pass_id: PassId) -> Option<&PassGroup> {
5559        self.groups.iter().find(|g| g.passes.contains(&pass_id))
5560    }
5561    pub fn remove_group(&mut self, id: u32) {
5562        self.groups.retain(|g| g.id != id);
5563    }
5564}
5565
5566// ============================================================
5567//  RENDER GRAPH — FRAME DEBUGGER CAPTURE
5568// ============================================================
5569
5570#[derive(Debug, Clone)]
5571pub struct CapturedFrame {
5572    pub frame_index: u64,
5573    pub timestamp_ms: f64,
5574    pub pass_order: Vec<PassId>,
5575    pub pass_timings: HashMap<PassId, f64>,
5576    pub resource_transitions: Vec<(PassId, ResourceId, ImageLayout, ImageLayout)>,
5577    pub barrier_count: usize,
5578    pub draw_calls_per_pass: HashMap<PassId, u32>,
5579    pub triangles_per_pass: HashMap<PassId, u64>,
5580    pub notes: Vec<String>,
5581}
5582
5583impl CapturedFrame {
5584    pub fn new(frame_index: u64, timestamp_ms: f64) -> Self {
5585        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() }
5586    }
5587
5588    pub fn total_gpu_ms(&self) -> f64 { self.pass_timings.values().sum() }
5589    pub fn total_draw_calls(&self) -> u32 { self.draw_calls_per_pass.values().sum() }
5590    pub fn total_triangles(&self) -> u64 { self.triangles_per_pass.values().sum() }
5591
5592    pub fn longest_pass(&self) -> Option<PassId> {
5593        self.pass_timings.iter().max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal)).map(|(&p, _)| p)
5594    }
5595
5596    pub fn passes_over_budget(&self, budget_ms: f64) -> Vec<PassId> {
5597        self.pass_timings.iter().filter(|(_, &t)| t > budget_ms).map(|(&p, _)| p).collect()
5598    }
5599
5600    pub fn timeline_html(&self, pass_names: &HashMap<PassId, String>) -> String {
5601        let total = self.total_gpu_ms().max(1e-6);
5602        let mut s = String::new();
5603        s.push_str("<div style='font-family:monospace;background:#111;padding:8px'>");
5604        for pid in &self.pass_order {
5605            let name = pass_names.get(pid).map(|n| n.as_str()).unwrap_or("?");
5606            let ms = self.pass_timings.get(pid).cloned().unwrap_or(0.0);
5607            let pct = (ms / total * 100.0) as u32;
5608            let width = pct.clamp(1, 100);
5609            let color = if ms > total * 0.2 { "#ff4444" } else if ms > total * 0.1 { "#ffaa22" } else { "#44aa44" };
5610            s.push_str(&format!(
5611                "<div style='display:flex;align-items:center;margin:2px 0'>\
5612                 <span style='color:#ccc;width:160px;display:inline-block'>{}</span>\
5613                 <div style='width:{}%;background:{};height:14px;display:inline-block'></div>\
5614                 <span style='color:#aaa;margin-left:4px'>{:.2}ms</span></div>",
5615                name, width, color, ms
5616            ));
5617        }
5618        s.push_str("</div>");
5619        s
5620    }
5621}
5622
5623pub struct FrameDebugger {
5624    pub captures: VecDeque<CapturedFrame>,
5625    pub max_captures: usize,
5626    pub is_capturing: bool,
5627    pub current_capture: Option<CapturedFrame>,
5628}
5629
5630impl FrameDebugger {
5631    pub fn new(max: usize) -> Self {
5632        FrameDebugger { captures: VecDeque::new(), max_captures: max, is_capturing: false, current_capture: None }
5633    }
5634    pub fn begin_capture(&mut self, frame_index: u64, timestamp_ms: f64) {
5635        self.is_capturing = true;
5636        self.current_capture = Some(CapturedFrame::new(frame_index, timestamp_ms));
5637    }
5638    pub fn record_pass_timing(&mut self, pass: PassId, ms: f64) {
5639        if let Some(ref mut cap) = self.current_capture {
5640            cap.pass_timings.insert(pass, ms);
5641            cap.pass_order.push(pass);
5642        }
5643    }
5644    pub fn record_transition(&mut self, pass: PassId, res: ResourceId, old: ImageLayout, new: ImageLayout) {
5645        if let Some(ref mut cap) = self.current_capture {
5646            cap.resource_transitions.push((pass, res, old, new));
5647        }
5648    }
5649    pub fn end_capture(&mut self) {
5650        if let Some(cap) = self.current_capture.take() {
5651            if self.captures.len() >= self.max_captures { self.captures.pop_front(); }
5652            self.captures.push_back(cap);
5653        }
5654        self.is_capturing = false;
5655    }
5656    pub fn latest(&self) -> Option<&CapturedFrame> { self.captures.back() }
5657    pub fn at_frame(&self, frame_index: u64) -> Option<&CapturedFrame> {
5658        self.captures.iter().find(|c| c.frame_index == frame_index)
5659    }
5660}
5661
5662// ============================================================
5663//  RENDER GRAPH PASS DEPENDENCY CRITICAL PATH
5664// ============================================================
5665
5666pub struct CriticalPathAnalyzer;
5667impl CriticalPathAnalyzer {
5668    /// Find the critical path through the render graph (longest chain by estimated time)
5669    pub fn find_critical_path(
5670        sorted: &[PassId],
5671        timings: &HashMap<PassId, f64>,
5672        edges: &HashMap<PassId, Vec<PassId>>,
5673    ) -> (Vec<PassId>, f64) {
5674        let mut earliest_finish: HashMap<PassId, f64> = HashMap::new();
5675        let mut predecessor: HashMap<PassId, Option<PassId>> = HashMap::new();
5676        // Forward pass: compute earliest finish time
5677        for pid in sorted {
5678            let t = timings.get(pid).cloned().unwrap_or(1.0);
5679            let max_pred_finish = edges.iter()
5680                .filter(|(_, dsts)| dsts.contains(pid))
5681                .map(|(src, _)| *earliest_finish.get(src).unwrap_or(&0.0))
5682                .fold(0.0f64, f64::max);
5683            let ef = max_pred_finish + t;
5684            earliest_finish.insert(*pid, ef);
5685            // Track predecessor on critical path
5686            let pred = edges.iter()
5687                .filter(|(_, dsts)| dsts.contains(pid))
5688                .max_by(|(a, _), (b, _)| {
5689                    let ta = earliest_finish.get(*a).unwrap_or(&0.0);
5690                    let tb = earliest_finish.get(*b).unwrap_or(&0.0);
5691                    ta.partial_cmp(tb).unwrap_or(std::cmp::Ordering::Equal)
5692                })
5693                .map(|(src, _)| *src);
5694            predecessor.insert(*pid, pred);
5695        }
5696        // Find the pass with the maximum finish time
5697        let end_pass = sorted.iter().max_by(|a, b| {
5698            let ta = earliest_finish.get(*a).unwrap_or(&0.0);
5699            let tb = earliest_finish.get(*b).unwrap_or(&0.0);
5700            ta.partial_cmp(tb).unwrap_or(std::cmp::Ordering::Equal)
5701        });
5702        let mut path = Vec::new();
5703        if let Some(&last) = end_pass {
5704            let total_time = *earliest_finish.get(&last).unwrap_or(&0.0);
5705            let mut current = Some(last);
5706            while let Some(node) = current {
5707                path.push(node);
5708                current = predecessor.get(&node).and_then(|p| *p);
5709            }
5710            path.reverse();
5711            (path, total_time)
5712        } else {
5713            (Vec::new(), 0.0)
5714        }
5715    }
5716}
5717
5718// ============================================================
5719//  LIGHT GRID BUILDING COMPUTE PASS DESCRIPTOR
5720// ============================================================
5721
5722#[derive(Debug, Clone)]
5723pub struct LightCullingPassDesc {
5724    pub width: u32,
5725    pub height: u32,
5726    pub tile_size: u32,
5727    pub max_lights: u32,
5728    pub output_light_indices: ResourceId,
5729    pub output_light_counts: ResourceId,
5730    pub input_depth: ResourceId,
5731    pub depth_prepass: bool,
5732}
5733
5734impl LightCullingPassDesc {
5735    pub fn default(width: u32, height: u32) -> Self {
5736        LightCullingPassDesc {
5737            width, height, tile_size: 16, max_lights: 1024,
5738            output_light_indices: ResourceId(200),
5739            output_light_counts: ResourceId(201),
5740            input_depth: ResourceId(4),
5741            depth_prepass: true,
5742        }
5743    }
5744    pub fn tiles_x(&self) -> u32 { (self.width + self.tile_size - 1) / self.tile_size }
5745    pub fn tiles_y(&self) -> u32 { (self.height + self.tile_size - 1) / self.tile_size }
5746    pub fn dispatch_x(&self) -> u32 { self.tiles_x() }
5747    pub fn dispatch_y(&self) -> u32 { self.tiles_y() }
5748    pub fn light_index_buffer_bytes(&self) -> u64 {
5749        self.tiles_x() as u64 * self.tiles_y() as u64 * self.max_lights as u64 * 2
5750    }
5751    pub fn light_count_buffer_bytes(&self) -> u64 {
5752        self.tiles_x() as u64 * self.tiles_y() as u64 * 4
5753    }
5754}
5755
5756// ============================================================
5757//  DEFERRED DECAL PASS
5758// ============================================================
5759
5760#[derive(Debug, Clone)]
5761pub struct DecalPassDesc {
5762    pub width: u32,
5763    pub height: u32,
5764    pub output_albedo: ResourceId,
5765    pub output_normal: ResourceId,
5766    pub input_depth: ResourceId,
5767    pub max_decals: u32,
5768    pub blend: ColorBlendAttachment,
5769    pub depth_stencil: DepthStencilState,
5770}
5771
5772impl DecalPassDesc {
5773    pub fn default(width: u32, height: u32) -> Self {
5774        DecalPassDesc {
5775            width, height,
5776            output_albedo: ResourceId(0),
5777            output_normal: ResourceId(1),
5778            input_depth: ResourceId(4),
5779            max_decals: 256,
5780            blend: ColorBlendAttachment::alpha_blend(),
5781            depth_stencil: DepthStencilState::depth_read_only(),
5782        }
5783    }
5784    /// A decal is rendered as a unit cube in world-space; the projection back to screen-space
5785    /// uses the GBuffer depth to compute the world position of each fragment.
5786    /// This function computes the OBB (oriented bounding box) of a decal in clip space.
5787    pub fn decal_clip_bounds(decal_world_to_local: Mat4, view_proj: Mat4) -> (Vec3, Vec3) {
5788        let cube_corners: [Vec3; 8] = [
5789            Vec3::new(-0.5, -0.5, -0.5), Vec3::new(0.5, -0.5, -0.5),
5790            Vec3::new(-0.5,  0.5, -0.5), Vec3::new(0.5,  0.5, -0.5),
5791            Vec3::new(-0.5, -0.5,  0.5), Vec3::new(0.5, -0.5,  0.5),
5792            Vec3::new(-0.5,  0.5,  0.5), Vec3::new(0.5,  0.5,  0.5),
5793        ];
5794        let local_to_world = decal_world_to_local.inverse();
5795        let mut min = Vec3::splat(f32::MAX);
5796        let mut max = Vec3::splat(f32::MIN);
5797        for c in &cube_corners {
5798            let world = (local_to_world * Vec4::new(c.x, c.y, c.z, 1.0)).truncate();
5799            let clip = view_proj * Vec4::new(world.x, world.y, world.z, 1.0);
5800            let ndc = if clip.w.abs() > 1e-6 { clip.truncate() / clip.w } else { clip.truncate() };
5801            min = min.min(ndc);
5802            max = max.max(ndc);
5803        }
5804        (min, max)
5805    }
5806}
5807
5808// ============================================================
5809//  PROCEDURAL SKY PASS
5810// ============================================================
5811
5812#[derive(Debug, Clone)]
5813pub struct SkyPassDesc {
5814    pub width: u32,
5815    pub height: u32,
5816    pub output_format: TextureFormat,
5817    pub output_sky: ResourceId,
5818    pub input_depth: ResourceId,
5819    pub model: SkyModel,
5820    pub sun_direction: Vec3,
5821    pub sun_intensity: f32,
5822    pub turbidity: f32,          // atmospheric turbidity (1..10)
5823    pub ground_albedo: Vec3,
5824    pub ozone_absorption: bool,
5825}
5826
5827#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5828pub enum SkyModel { Preetham, Hosek, PhysicalAtmosphere, PBRSky, Static }
5829
5830impl SkyPassDesc {
5831    pub fn default(width: u32, height: u32) -> Self {
5832        SkyPassDesc {
5833            width, height,
5834            output_format: TextureFormat::RGBA16Float,
5835            output_sky: ResourceId(60),
5836            input_depth: ResourceId(4),
5837            model: SkyModel::Hosek,
5838            sun_direction: Vec3::new(0.0, 1.0, 0.0).normalize(),
5839            sun_intensity: 10.0,
5840            turbidity: 2.0,
5841            ground_albedo: Vec3::new(0.1, 0.1, 0.1),
5842            ozone_absorption: true,
5843        }
5844    }
5845
5846    /// Hosek-Wilkie sky model — compute sky radiance in a given direction
5847    /// This is a simplified fit; the full model uses precomputed spectral tables.
5848    pub fn hosek_wilkie_simple(&self, view_dir: Vec3) -> Vec3 {
5849        let sun = self.sun_direction.normalize();
5850        let cos_theta = view_dir.y.max(0.0);
5851        let cos_gamma = view_dir.dot(sun).clamp(-1.0, 1.0);
5852        let gamma = cos_gamma.acos();
5853        let theta = cos_theta.acos().min(std::f32::consts::FRAC_PI_2);
5854
5855        // Hosek dataset approximation (single turbidity-based fit for visible channel)
5856        let t = self.turbidity;
5857        let a = 0.1787 * t - 1.4630;
5858        let b = -0.3554 * t + 0.4275;
5859        let c = -0.0227 * t + 5.3251;
5860        let d = 0.1206 * t - 2.5771;
5861        let e = -0.0670 * t + 0.3703;
5862
5863        let hosek_f = |theta: f32, gamma: f32| -> f32 {
5864            (1.0 + a * (-b / theta.cos().max(1e-4)).exp()) *
5865            (1.0 + c * (-d * gamma).exp() + e * cos_gamma * cos_gamma)
5866        };
5867        let zenith_luminance = hosek_f(0.0, 0.0_f32.acos());
5868        let sky_lum = hosek_f(theta, gamma) / zenith_luminance.max(1e-6);
5869        // Compose RGB approximation
5870        let blue_tint = Vec3::new(0.6, 0.8, 1.0);
5871        let base_sky = blue_tint * sky_lum.max(0.0) * 5.0;
5872        let sun_disk = if cos_gamma > 0.9998 {
5873            Vec3::new(1.0, 0.9, 0.7) * self.sun_intensity * 1000.0
5874        } else {
5875            Vec3::ZERO
5876        };
5877        base_sky + sun_disk
5878    }
5879}
5880
5881// ============================================================
5882//  RENDER GRAPH — PIPELINE CACHE
5883// ============================================================
5884
5885#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5886pub struct PipelineKey {
5887    pub pass_kind: PassKind,
5888    pub fill_mode: u8,  // 0=solid, 1=wire, 2=point
5889    pub cull_mode: u8,  // 0=none, 1=front, 2=back
5890    pub depth_test: bool,
5891    pub depth_write: bool,
5892    pub blend_enabled: bool,
5893    pub sample_count: u8,
5894    pub output_format_hash: u64,
5895}
5896
5897impl PipelineKey {
5898    pub fn from_pass(pass: &PassNode, rasterizer: &RasterizerState, ds: &DepthStencilState, blend: bool, samples: SampleCount, output_fmt: TextureFormat) -> Self {
5899        let fmt_hash = format_hash(output_fmt);
5900        PipelineKey {
5901            pass_kind: pass.desc.kind(),
5902            fill_mode: match rasterizer.fill_mode { FillMode::Solid => 0, FillMode::Wireframe => 1, FillMode::Point => 2 },
5903            cull_mode: match rasterizer.cull_mode { CullMode::None => 0, CullMode::Front => 1, CullMode::Back => 2, CullMode::FrontAndBack => 3 },
5904            depth_test: ds.depth_test_enable,
5905            depth_write: ds.depth_write_enable,
5906            blend_enabled: blend,
5907            sample_count: samples.count() as u8,
5908            output_format_hash: fmt_hash,
5909        }
5910    }
5911}
5912
5913fn format_hash(fmt: TextureFormat) -> u64 {
5914    // Simple deterministic hash based on format discriminant
5915    (fmt as u64).wrapping_mul(0x9e3779b97f4a7c15)
5916}
5917
5918pub struct PipelineCache {
5919    pub entries: HashMap<PipelineKey, u64>, // key -> pipeline_handle (opaque u64 in real impl)
5920    pub hit_count: u64,
5921    pub miss_count: u64,
5922    pub evict_count: u64,
5923    pub max_entries: usize,
5924}
5925
5926impl PipelineCache {
5927    pub fn new(max_entries: usize) -> Self {
5928        PipelineCache { entries: HashMap::new(), hit_count: 0, miss_count: 0, evict_count: 0, max_entries }
5929    }
5930    pub fn get(&mut self, key: &PipelineKey) -> Option<u64> {
5931        if let Some(&handle) = self.entries.get(key) {
5932            self.hit_count += 1;
5933            Some(handle)
5934        } else {
5935            self.miss_count += 1;
5936            None
5937        }
5938    }
5939    pub fn insert(&mut self, key: PipelineKey, handle: u64) {
5940        if self.entries.len() >= self.max_entries {
5941            // Evict a random entry (LRU would require extra bookkeeping)
5942            if let Some(evict_key) = self.entries.keys().next().cloned() {
5943                self.entries.remove(&evict_key);
5944                self.evict_count += 1;
5945            }
5946        }
5947        self.entries.insert(key, handle);
5948    }
5949    pub fn hit_rate(&self) -> f32 {
5950        let total = self.hit_count + self.miss_count;
5951        if total == 0 { 1.0 } else { self.hit_count as f32 / total as f32 }
5952    }
5953}
5954
5955// ============================================================
5956//  RENDER GRAPH EXPORT — DOT (GraphViz) FORMAT
5957// ============================================================
5958
5959pub fn export_dot(editor: &RenderGraphEditor) -> String {
5960    let mut s = String::new();
5961    s.push_str("digraph RenderGraph {\n");
5962    s.push_str("  rankdir=LR;\n");
5963    s.push_str("  node [shape=box, style=filled];\n");
5964    for pass in editor.passes.values() {
5965        let color = color_to_hex(pass.editor_color);
5966        let label = format!("{}\n[{:?}]", pass.name, pass.desc.kind());
5967        s.push_str(&format!("  pass_{} [label=\"{}\", fillcolor=\"{}\"];\n", pass.id.0, label, color));
5968    }
5969    s.push_str("  // resource nodes\n");
5970    for res in editor.resources.values() {
5971        let color = match res.lifetime {
5972            ResourceLifetime::Transient => "#aaffaa",
5973            ResourceLifetime::Persistent => "#ffaaaa",
5974            ResourceLifetime::Imported => "#aaaaff",
5975        };
5976        let desc = match &res.desc {
5977            ResourceDesc::Texture(t) => format!("{}x{} {:?}", t.width, t.height, t.format),
5978            ResourceDesc::Buffer(b) => format!("{}B buffer", b.size),
5979        };
5980        s.push_str(&format!("  res_{} [label=\"{}\\n{}\", shape=ellipse, fillcolor=\"{}\"];\n", res.id.0, res.name, desc, color));
5981    }
5982    s.push_str("  // edges\n");
5983    for pass in editor.passes.values() {
5984        for rid in &pass.reads {
5985            s.push_str(&format!("  res_{} -> pass_{};\n", rid.0, pass.id.0));
5986        }
5987        for rid in &pass.writes {
5988            s.push_str(&format!("  pass_{} -> res_{};\n", pass.id.0, rid.0));
5989        }
5990    }
5991    s.push_str("}\n");
5992    s
5993}
5994
5995fn color_to_hex(c: Vec4) -> String {
5996    let r = (c.x.clamp(0.0, 1.0) * 255.0) as u8;
5997    let g = (c.y.clamp(0.0, 1.0) * 255.0) as u8;
5998    let b = (c.z.clamp(0.0, 1.0) * 255.0) as u8;
5999    format!("#{:02X}{:02X}{:02X}", r, g, b)
6000}
6001
6002// ============================================================
6003//  RENDER GRAPH EXPORT — MERMAID FORMAT
6004// ============================================================
6005
6006pub fn export_mermaid(editor: &RenderGraphEditor) -> String {
6007    let mut s = String::new();
6008    s.push_str("graph LR\n");
6009    // Collect only pass-to-pass connections (via shared resources)
6010    let mut connections: HashSet<(u32, u32)> = HashSet::new();
6011    let mut resource_writers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
6012    let mut resource_readers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
6013    for pass in editor.passes.values() {
6014        for rid in &pass.writes { resource_writers.entry(*rid).or_default().push(pass.id); }
6015        for rid in &pass.reads  { resource_readers.entry(*rid).or_default().push(pass.id); }
6016    }
6017    for (rid, writers) in &resource_writers {
6018        if let Some(readers) = resource_readers.get(rid) {
6019            for w in writers {
6020                for r in readers {
6021                    if w != r { connections.insert((w.0, r.0)); }
6022                }
6023            }
6024        }
6025    }
6026    for pass in editor.passes.values() {
6027        let kind = format!("{:?}", pass.desc.kind());
6028        s.push_str(&format!("  P{}[{}<br/><i>{}</i>]\n", pass.id.0, pass.name, kind));
6029    }
6030    for (src, dst) in &connections {
6031        s.push_str(&format!("  P{} --> P{}\n", src, dst));
6032    }
6033    s
6034}
6035
6036// ============================================================
6037//  INTEGRATION HELPERS — HOT RELOAD
6038// ============================================================
6039
6040pub struct HotReloadManager {
6041    pub current: RenderGraphEditor,
6042    pub pending: Option<RenderGraphEditor>,
6043    pub last_reload_frame: u64,
6044    pub reload_on_next_frame: bool,
6045}
6046
6047impl HotReloadManager {
6048    pub fn new(editor: RenderGraphEditor) -> Self {
6049        HotReloadManager { current: editor, pending: None, last_reload_frame: 0, reload_on_next_frame: false }
6050    }
6051    pub fn stage_reload(&mut self, new_editor: RenderGraphEditor) {
6052        self.pending = Some(new_editor);
6053        self.reload_on_next_frame = true;
6054    }
6055    pub fn apply_reload_if_pending(&mut self, current_frame: u64) -> bool {
6056        if self.reload_on_next_frame {
6057            if let Some(new) = self.pending.take() {
6058                let diffs = diff_render_graphs(&self.current, &new);
6059                self.current = new;
6060                self.last_reload_frame = current_frame;
6061                self.reload_on_next_frame = false;
6062                return !diffs.is_empty();
6063            }
6064        }
6065        false
6066    }
6067    pub fn needs_recompile(&self, current_frame: u64) -> bool {
6068        current_frame == self.last_reload_frame
6069    }
6070}
6071
6072// ============================================================
6073//  RENDER GRAPH — MULTISAMPLE RESOLVE
6074// ============================================================
6075
6076#[derive(Debug, Clone)]
6077pub struct ResolvePassDesc {
6078    pub width: u32,
6079    pub height: u32,
6080    pub format: TextureFormat,
6081    pub input_msaa: ResourceId,
6082    pub output_resolved: ResourceId,
6083    pub sample_count: SampleCount,
6084}
6085
6086impl ResolvePassDesc {
6087    pub fn new(width: u32, height: u32, format: TextureFormat, input: ResourceId, output: ResourceId, samples: SampleCount) -> Self {
6088        ResolvePassDesc { width, height, format, input_msaa: input, output_resolved: output, sample_count: samples }
6089    }
6090    /// Box filter weights for each sample count
6091    pub fn box_filter_weights(samples: SampleCount) -> Vec<f32> {
6092        let n = samples.count() as usize;
6093        vec![1.0 / n as f32; n]
6094    }
6095    /// Standard sample positions for MSAA (D3D-style for 4x)
6096    pub fn msaa4x_sample_positions() -> [Vec2; 4] {
6097        [
6098            Vec2::new(-0.125, -0.375),
6099            Vec2::new( 0.375, -0.125),
6100            Vec2::new(-0.375,  0.125),
6101            Vec2::new( 0.125,  0.375),
6102        ]
6103    }
6104    /// Standard sample positions for MSAA 8x
6105    pub fn msaa8x_sample_positions() -> [Vec2; 8] {
6106        [
6107            Vec2::new( 0.0625, -0.1875), Vec2::new(-0.0625,  0.1875),
6108            Vec2::new( 0.3125,  0.0625), Vec2::new(-0.1875, -0.3125),
6109            Vec2::new(-0.3125,  0.3125), Vec2::new(-0.4375, -0.0625),
6110            Vec2::new( 0.1875,  0.4375), Vec2::new( 0.4375, -0.4375),
6111        ]
6112    }
6113}
6114
6115// ============================================================
6116//  POST-FX CHAIN — ordered pipeline
6117// ============================================================
6118
6119#[derive(Debug, Clone)]
6120pub struct PostFxChain {
6121    pub effects: Vec<PostFxEffect>,
6122    pub input: ResourceId,
6123    pub output: ResourceId,
6124}
6125
6126#[derive(Debug, Clone)]
6127pub enum PostFxEffect {
6128    Bloom(BloomPassDesc),
6129    ToneMapping(ToneMappingPassDesc),
6130    TAA(TAAPassDesc),
6131    DepthOfField(DepthOfFieldPassDesc),
6132    MotionBlur(MotionBlurPassDesc),
6133    VolumetricFog(VolumetricFogPassDesc),
6134    ChromaticAberration { strength: f32, samples: u32 },
6135    FilmGrain { strength: f32, animated: bool },
6136    Vignette { radius: f32, smoothness: f32, color: Vec4 },
6137    LensFlare { threshold: f32, intensity: f32 },
6138    Sharpen { amount: f32 },
6139    CAS { sharpness: f32 },  // Contrast Adaptive Sharpening
6140}
6141
6142impl PostFxChain {
6143    pub fn default(width: u32, height: u32, input: ResourceId, output: ResourceId) -> Self {
6144        PostFxChain {
6145            effects: vec![
6146                PostFxEffect::Bloom(BloomPassDesc::default(width, height)),
6147                PostFxEffect::ToneMapping(ToneMappingPassDesc::default(width, height)),
6148                PostFxEffect::TAA(TAAPassDesc::default(width, height)),
6149                PostFxEffect::ChromaticAberration { strength: 0.003, samples: 3 },
6150                PostFxEffect::FilmGrain { strength: 0.03, animated: true },
6151                PostFxEffect::Vignette { radius: 0.75, smoothness: 0.45, color: Vec4::new(0.0, 0.0, 0.0, 1.0) },
6152                PostFxEffect::Sharpen { amount: 0.3 },
6153            ],
6154            input, output,
6155        }
6156    }
6157
6158    /// Apply chromatic aberration offset (screen-space UV displacement)
6159    pub fn chromatic_aberration_offset(uv: Vec2, strength: f32, channel: u32) -> Vec2 {
6160        let center = Vec2::splat(0.5);
6161        let dist = uv - center;
6162        let offset = dist * strength * (channel as f32 - 1.0);
6163        uv + offset
6164    }
6165
6166    /// Film grain value at a given pixel + time using interleaved gradient noise
6167    pub fn film_grain(uv: Vec2, time: f32, strength: f32) -> f32 {
6168        let frame_index = (time * 60.0) as u32;
6169        let p = uv * 1000.0 + Vec2::new((frame_index % 256) as f32, ((frame_index / 256) % 256) as f32);
6170        let n = (p.x * 0.06711056 + p.y * 0.00583715).fract();
6171        let n = (n * 52.9829189).fract();
6172        (n - 0.5) * 2.0 * strength
6173    }
6174
6175    /// Vignette factor at a given UV
6176    pub fn vignette_factor(uv: Vec2, radius: f32, smoothness: f32) -> f32 {
6177        let dist = (uv - Vec2::splat(0.5)).length() / (radius * std::f32::consts::SQRT_2);
6178        1.0 - smoothstep(1.0 - smoothness, 1.0, dist)
6179    }
6180
6181    /// CAS sharpening kernel (AMD Contrast Adaptive Sharpening)
6182    pub fn cas_sharpen(center: Vec3, neighbors: [Vec3; 4], sharpness: f32) -> Vec3 {
6183        // neighbors: [top, bottom, left, right]
6184        let min_c = neighbors.iter().fold(center, |acc, &n| acc.min(n));
6185        let max_c = neighbors.iter().fold(center, |acc, &n| acc.max(n));
6186        let w_min = Vec3::ONE / max_c.max(Vec3::splat(1e-6));
6187        let w_max = Vec3::ONE / min_c.max(Vec3::splat(1e-6));
6188        let w = (-(Vec3::ONE / (min_c * 8.0))).max(Vec3::splat(-0.125)) * sharpness;
6189        let sum: Vec3 = neighbors.iter().map(|&n| n * w).fold(Vec3::ZERO, |a, b| a + b);
6190        (center + sum) / (Vec3::ONE + 4.0 * w)
6191    }
6192}
6193
6194// ============================================================
6195//  RENDER GRAPH — COMPLETE COMPILATION REPORT
6196// ============================================================
6197
6198pub struct CompilationReport {
6199    pub success: bool,
6200    pub errors: Vec<String>,
6201    pub warnings: Vec<String>,
6202    pub pass_count: usize,
6203    pub dead_pass_count: usize,
6204    pub resource_count: usize,
6205    pub transient_resource_count: usize,
6206    pub aliasing_group_count: usize,
6207    pub total_barriers: usize,
6208    pub estimated_memory_mb: f32,
6209    pub estimated_bandwidth_mb: f32,
6210    pub compile_time_us: u64,
6211    pub sort_order: Vec<String>,
6212}
6213
6214impl CompilationReport {
6215    pub fn from_compiled(compiled: &CompiledRenderGraph, pass_names: &HashMap<PassId, String>) -> Self {
6216        let sort_order: Vec<String> = compiled.sorted_passes.iter()
6217            .map(|pid| pass_names.get(pid).cloned().unwrap_or_else(|| format!("{:?}", pid)))
6218            .collect();
6219        CompilationReport {
6220            success: true,
6221            errors: Vec::new(),
6222            warnings: Vec::new(),
6223            pass_count: compiled.sorted_passes.len(),
6224            dead_pass_count: compiled.dead_passes.len(),
6225            resource_count: compiled.resource_lifetimes.len(),
6226            transient_resource_count: compiled.aliasing_groups.iter().map(|g| g.len()).sum(),
6227            aliasing_group_count: compiled.aliasing_groups.len(),
6228            total_barriers: compiled.barriers.values().map(|b| b.image_barriers.len() + b.buffer_barriers.len()).sum(),
6229            estimated_memory_mb: compiled.estimated_memory_bytes as f32 / (1024.0 * 1024.0),
6230            estimated_bandwidth_mb: compiled.estimated_bandwidth_mb,
6231            compile_time_us: 0,
6232            sort_order,
6233        }
6234    }
6235
6236    pub fn print(&self) -> String {
6237        let mut s = String::new();
6238        if self.success {
6239            s.push_str("[OK] Render graph compiled successfully\n");
6240        } else {
6241            s.push_str("[FAIL] Render graph compilation FAILED\n");
6242            for e in &self.errors { s.push_str(&format!("  ERROR: {}\n", e)); }
6243        }
6244        for w in &self.warnings { s.push_str(&format!("  WARN: {}\n", w)); }
6245        s.push_str(&format!("  Passes: {} ({} dead)\n", self.pass_count, self.dead_pass_count));
6246        s.push_str(&format!("  Resources: {} ({} transient, {} aliasing groups)\n", self.resource_count, self.transient_resource_count, self.aliasing_group_count));
6247        s.push_str(&format!("  Barriers: {}\n", self.total_barriers));
6248        s.push_str(&format!("  Memory:    {:.2} MB\n", self.estimated_memory_mb));
6249        s.push_str(&format!("  Bandwidth: {:.1} MB/frame\n", self.estimated_bandwidth_mb));
6250        s.push_str("  Execution order: ");
6251        for (i, name) in self.sort_order.iter().enumerate() {
6252            if i > 0 { s.push_str(" -> "); }
6253            s.push_str(name);
6254        }
6255        s.push('\n');
6256        s
6257    }
6258}
6259
6260// ============================================================
6261//  EXTENDED TESTS
6262// ============================================================
6263
6264#[cfg(test)]
6265mod extended_tests {
6266    use super::*;
6267
6268    #[test]
6269    fn test_shadow_atlas_allocation() {
6270        let mut atlas = ShadowAtlas::new(4096);
6271        let r1 = atlas.allocate(1, 512, 512);
6272        let r2 = atlas.allocate(2, 1024, 1024);
6273        assert!(r1.is_some());
6274        assert!(r2.is_some());
6275        let r1 = r1.unwrap();
6276        assert_eq!(r1.x, 0);
6277        assert_eq!(r1.y, 0);
6278        assert!(atlas.utilization() > 0.0);
6279    }
6280
6281    #[test]
6282    fn test_shadow_atlas_free() {
6283        let mut atlas = ShadowAtlas::new(1024);
6284        atlas.allocate(1, 512, 512);
6285        atlas.free_region(1);
6286        assert!(atlas.regions.is_empty());
6287    }
6288
6289    #[test]
6290    fn test_timestamp_pool() {
6291        let mut pool = QueryPool::new_timestamp(32, 1.0);
6292        let pair = pool.allocate_pair();
6293        assert!(pair.is_some());
6294        let (s, e) = pair.unwrap();
6295        assert_eq!(e, s + 1);
6296        let ms = pool.ticks_to_ms(1_000_000);
6297        assert!((ms - 1.0).abs() < 1e-6);
6298    }
6299
6300    #[test]
6301    fn test_frame_uniforms_size() {
6302        let sz = FrameUniforms::size_bytes();
6303        assert!(sz > 0);
6304        assert_eq!(sz % 16, 0, "FrameUniforms must be 16-byte aligned");
6305    }
6306
6307    #[test]
6308    fn test_dependency_matrix() {
6309        let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6310        let passes: Vec<PassId> = editor.passes.keys().cloned().collect();
6311        let edges = editor.build_edges();
6312        let matrix = DependencyMatrix::build(&passes, &edges);
6313        // Matrix should be n x n
6314        assert_eq!(matrix.matrix.len(), passes.len());
6315    }
6316
6317    #[test]
6318    fn test_dot_export() {
6319        let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6320        let dot = export_dot(&editor);
6321        assert!(dot.contains("digraph RenderGraph"));
6322        assert!(dot.contains("GBuffer"));
6323    }
6324
6325    #[test]
6326    fn test_mermaid_export() {
6327        let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6328        let mermaid = export_mermaid(&editor);
6329        assert!(mermaid.contains("graph LR"));
6330    }
6331
6332    #[test]
6333    fn test_post_fx_vignette() {
6334        let v = PostFxChain::vignette_factor(Vec2::splat(0.5), 0.75, 0.45);
6335        assert!((v - 1.0).abs() < 0.01, "center should be no vignette");
6336        let v2 = PostFxChain::vignette_factor(Vec2::new(0.0, 0.0), 0.75, 0.45);
6337        assert!(v2 < v, "corner should have more vignette");
6338    }
6339
6340    #[test]
6341    fn test_chromatic_aberration() {
6342        let uv = Vec2::new(0.75, 0.5);
6343        let r = PostFxChain::chromatic_aberration_offset(uv, 0.01, 0);
6344        let g = PostFxChain::chromatic_aberration_offset(uv, 0.01, 1);
6345        let b = PostFxChain::chromatic_aberration_offset(uv, 0.01, 2);
6346        assert!(r != g || g != b || r != b || true); // at least compiles
6347    }
6348
6349    #[test]
6350    fn test_light_culling_pass() {
6351        let lc = LightCullingPassDesc::default(1920, 1080);
6352        assert_eq!(lc.tiles_x(), 120);
6353        assert_eq!(lc.tiles_y(), 68);
6354        assert!(lc.light_index_buffer_bytes() > 0);
6355    }
6356
6357    #[test]
6358    fn test_clustered_light_grid_memory() {
6359        let grid = ClusteredLightGrid::new(1920, 1080, 16, 24, 0.1, 100.0);
6360        let mem = grid.memory_requirements(64);
6361        assert!(mem > 0);
6362    }
6363
6364    #[test]
6365    fn test_hosek_sky() {
6366        let sky = SkyPassDesc::default(1920, 1080);
6367        let dir = Vec3::new(0.0, 1.0, 0.0).normalize();
6368        let color = sky.hosek_wilkie_simple(dir);
6369        assert!(color.length() > 0.0);
6370    }
6371
6372    #[test]
6373    fn test_compile_report() {
6374        let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6375        editor.compile().unwrap();
6376        let compiled = editor.compiled.as_ref().unwrap();
6377        let names: HashMap<PassId, String> = editor.passes.values().map(|p| (p.id, p.name.clone())).collect();
6378        let report = CompilationReport::from_compiled(compiled, &names);
6379        let text = report.print();
6380        assert!(text.contains("[OK]"));
6381    }
6382
6383    #[test]
6384    fn test_pipeline_cache() {
6385        let mut cache = PipelineCache::new(16);
6386        let key = PipelineKey {
6387            pass_kind: PassKind::GBuffer,
6388            fill_mode: 0, cull_mode: 2, depth_test: true, depth_write: true,
6389            blend_enabled: false, sample_count: 1, output_format_hash: 42,
6390        };
6391        assert!(cache.get(&key).is_none());
6392        cache.insert(key.clone(), 9999);
6393        assert_eq!(cache.get(&key), Some(9999));
6394        assert!(cache.hit_rate() > 0.0);
6395    }
6396
6397    #[test]
6398    fn test_ggx_brdf() {
6399        let n = Vec3::Y;
6400        let v = Vec3::new(0.0, 1.0, 0.0);
6401        let l = Vec3::new(0.5, 0.5, 0.0).normalize();
6402        let albedo = Vec3::new(0.8, 0.2, 0.1);
6403        let result = cook_torrance_brdf(n, v, l, albedo, 0.0, 0.5);
6404        assert!(result.length() > 0.0);
6405        assert!(result.x <= 10.0 && result.y <= 10.0 && result.z <= 10.0);
6406    }
6407
6408    #[test]
6409    fn test_brdf_lut_integration() {
6410        let lut = integrate_brdf(0.5, 0.5, 64);
6411        assert!(lut.x >= 0.0 && lut.x <= 1.0);
6412        assert!(lut.y >= 0.0 && lut.y <= 1.0);
6413    }
6414
6415    #[test]
6416    fn test_aabb_frustum_cull() {
6417        let vp = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 100.0);
6418        let planes = frustum_planes_from_view_proj(vp);
6419        // Object far behind camera should be culled
6420        let inside = aabb_in_frustum(&planes, Vec3::new(-0.5, -0.5, -10.0), Vec3::new(0.5, 0.5, -9.0));
6421        assert!(inside || !inside); // just make sure it runs without panic
6422    }
6423
6424    #[test]
6425    fn test_tbr_bandwidth_savings() {
6426        let fmts = vec![TextureFormat::RGBA8Unorm, TextureFormat::RG16Float, TextureFormat::Depth24UnormStencil8];
6427        let savings = TBRDetector::bandwidth_savings_mb(1920, 1080, &fmts);
6428        assert!(savings > 0.0);
6429    }
6430
6431    #[test]
6432    fn test_async_compute_scheduling() {
6433        let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6434        let candidates: HashSet<PassId> = AsyncComputeScheduler::identify_async_candidates(&editor.passes).into_iter().collect();
6435        assert!(!candidates.is_empty());
6436    }
6437
6438    #[test]
6439    fn test_forward_plus_compile() {
6440        let mut editor = build_forward_plus_pipeline(1920, 1080);
6441        let result = editor.compile();
6442        assert!(result.is_ok(), "{:?}", result);
6443    }
6444
6445    #[test]
6446    fn test_mobile_deferred_compile() {
6447        let mut editor = build_mobile_deferred_pipeline(1920, 1080);
6448        let result = editor.compile();
6449        assert!(result.is_ok(), "{:?}", result);
6450    }
6451
6452    #[test]
6453    fn test_gbuffer_lighting_renderpass() {
6454        let gbuf = GBufferPassDesc::default(1920, 1080);
6455        let light = LightingPassDesc::default(1920, 1080);
6456        let rp = RenderPassDescription::build_gbuffer_lighting_renderpass(&gbuf, &light);
6457        assert_eq!(rp.subpasses.len(), 2);
6458        assert!(rp.detect_tbr_optimization());
6459        let bw = rp.total_load_store_bandwidth_bytes(1920, 1080);
6460        assert!(bw > 0);
6461    }
6462
6463    #[test]
6464    fn test_resource_aliasing() {
6465        let td_a = TextureDesc::render_target(1920, 1080, TextureFormat::RGBA16Float);
6466        let td_b = TextureDesc::render_target(1920, 1080, TextureFormat::RGBA16Float);
6467        let mut ra = RenderGraphResource::new_transient_texture(ResourceId(0), "a", td_a);
6468        let mut rb = RenderGraphResource::new_transient_texture(ResourceId(1), "b", td_b);
6469        ra.first_use = 0; ra.last_use = 2;
6470        rb.first_use = 5; rb.last_use = 8;
6471        // Non-overlapping lifetimes: should be aliasable
6472        assert!(!ra.lifetime_overlaps(&rb));
6473        assert!(ra.can_alias_with(&rb));
6474    }
6475
6476    #[test]
6477    fn test_bloom_mip_sizes() {
6478        let bloom = BloomPassDesc::default(1920, 1080);
6479        let (w0, h0) = bloom.mip_size(0);
6480        let (w1, h1) = bloom.mip_size(1);
6481        assert_eq!(w0, 1920);
6482        assert_eq!(w1, 960);
6483        assert_eq!(h1, 540);
6484    }
6485
6486    #[test]
6487    fn test_dof_coc() {
6488        let dof = DepthOfFieldPassDesc::default(1920, 1080);
6489        let coc = dof.coc_from_depth(10.0, 0.05, 0.1); // at focus distance
6490        assert!(coc.abs() < 0.01);
6491    }
6492
6493    #[test]
6494    fn test_motion_blur_soft_depth() {
6495        let soft = MotionBlurPassDesc::soft_depth_compare(1.0, 0.5, 1.0);
6496        assert!(soft > 0.0 && soft <= 1.0);
6497    }
6498}
6499
6500// ============================================================
6501//  RENDER GRAPH — SPARSE VOXEL GLOBAL ILLUMINATION PASS
6502// ============================================================
6503
6504#[derive(Debug, Clone)]
6505pub struct VXGIPassDesc {
6506    pub voxel_grid_size: u32,          // e.g. 256 voxels per axis
6507    pub voxel_world_size: f32,         // world-space extent of the voxel grid
6508    pub output_radiance_grid: ResourceId,
6509    pub output_normal_grid: ResourceId,
6510    pub output_opacity_grid: ResourceId,
6511    pub inject_light: bool,
6512    pub num_cones: u32,
6513    pub cone_aperture_deg: f32,
6514    pub max_cone_distance: f32,
6515    pub indirect_diffuse_enabled: bool,
6516    pub indirect_specular_enabled: bool,
6517    pub mip_generation: bool,
6518    pub temporal_accumulation: f32,
6519}
6520
6521impl VXGIPassDesc {
6522    pub fn default() -> Self {
6523        VXGIPassDesc {
6524            voxel_grid_size: 256,
6525            voxel_world_size: 50.0,
6526            output_radiance_grid: ResourceId(300),
6527            output_normal_grid: ResourceId(301),
6528            output_opacity_grid: ResourceId(302),
6529            inject_light: true,
6530            num_cones: 6,
6531            cone_aperture_deg: 60.0,
6532            max_cone_distance: 10.0,
6533            indirect_diffuse_enabled: true,
6534            indirect_specular_enabled: true,
6535            mip_generation: true,
6536            temporal_accumulation: 0.05,
6537        }
6538    }
6539
6540    pub fn voxel_size(&self) -> f32 {
6541        self.voxel_world_size / self.voxel_grid_size as f32
6542    }
6543
6544    pub fn grid_memory_bytes(&self) -> u64 {
6545        let n = self.voxel_grid_size as u64;
6546        // RGBA16F for radiance + normal + opacity
6547        n * n * n * (8 + 8 + 4)
6548    }
6549
6550    pub fn mip_levels(&self) -> u32 {
6551        compute_mip_count(self.voxel_grid_size, self.voxel_grid_size)
6552    }
6553
6554    /// Sample radiance using a cone trace through the voxel grid
6555    /// Returns (irradiance, occlusion)
6556    pub fn cone_trace(
6557        &self,
6558        start: Vec3,
6559        direction: Vec3,
6560        aperture: f32,
6561        max_distance: f32,
6562        step_multiplier: f32,
6563    ) -> (Vec3, f32) {
6564        let voxel_size = self.voxel_size();
6565        let mut accum_color = Vec3::ZERO;
6566        let mut accum_alpha = 0.0f32;
6567        let mut dist = voxel_size; // start a bit away from surface
6568        while dist < max_distance && accum_alpha < 0.95 {
6569            let diameter = 2.0 * aperture * dist;
6570            let mip = (diameter / voxel_size).log2().max(0.0);
6571            // Sample voxel grid at 'mip' level (simulated here by linear interpolation)
6572            let sample_pos = start + direction * dist;
6573            // In practice, this would sample from a 3D texture. We simulate with a placeholder.
6574            let alpha = 0.1 * (1.0 - accum_alpha); // placeholder
6575            let color = Vec3::new(0.1, 0.08, 0.06) * alpha; // placeholder ambient
6576            accum_color += color * (1.0 - accum_alpha);
6577            accum_alpha += alpha * (1.0 - accum_alpha);
6578            dist += diameter.max(voxel_size) * step_multiplier;
6579        }
6580        (accum_color, accum_alpha)
6581    }
6582
6583    /// Generate cone directions for indirect diffuse sampling (cosine-weighted hemisphere)
6584    pub fn diffuse_cone_directions(num_cones: u32) -> Vec<Vec3> {
6585        let mut dirs = Vec::with_capacity(num_cones as usize);
6586        // Fixed 6-cone configuration (used by many VXGI implementations)
6587        let sq3 = (1.0f32/3.0).sqrt();
6588        dirs.push(Vec3::new( 0.0,  1.0,  0.0));
6589        dirs.push(Vec3::new( sq3 * 2.0,  sq3, 0.0).normalize());
6590        dirs.push(Vec3::new(-sq3,        sq3, sq3 * std::f32::consts::SQRT_2).normalize());
6591        dirs.push(Vec3::new(-sq3,        sq3, -sq3 * std::f32::consts::SQRT_2).normalize());
6592        dirs.push(Vec3::new( sq3,        sq3, sq3 * std::f32::consts::SQRT_2).normalize());
6593        dirs.push(Vec3::new( sq3,        sq3, -sq3 * std::f32::consts::SQRT_2).normalize());
6594        while dirs.len() < num_cones as usize {
6595            let i = dirs.len() as f32;
6596            let phi = i * std::f32::consts::TAU * 0.6180339887;
6597            let theta = (1.0 - 2.0 * i / num_cones as f32).acos();
6598            dirs.push(Vec3::new(theta.sin() * phi.cos(), theta.cos(), theta.sin() * phi.sin()));
6599        }
6600        dirs
6601    }
6602}
6603
6604// ============================================================
6605//  SCREEN-SPACE REFLECTIONS — HI-Z TRACE
6606// ============================================================
6607
6608pub struct HiZTracer;
6609impl HiZTracer {
6610    /// Hierarchical Z-buffer ray march (DDA on the hi-z pyramid)
6611    /// Returns the screen-space UV of the reflection hit, or None if no hit found.
6612    pub fn trace(
6613        ray_origin_ss: Vec2,
6614        ray_dir_ss: Vec2,
6615        ray_start_depth: f32,
6616        max_steps: u32,
6617        max_mip: u32,
6618        // depth_pyramid: &dyn Fn(Vec2, u32) -> f32,  // can't use trait objects without 'static
6619    ) -> Option<(Vec2, f32)> {
6620        let mut pos = ray_origin_ss;
6621        let mut mip = 0u32;
6622        let mut depth = ray_start_depth;
6623        let step = ray_dir_ss * 0.001; // initial step
6624        for i in 0..max_steps {
6625            pos += step * (1 << mip) as f32;
6626            if pos.x < 0.0 || pos.x > 1.0 || pos.y < 0.0 || pos.y > 1.0 { return None; }
6627            // Simulated depth pyramid sample (actual impl would sample GPU texture)
6628            let sample_depth = depth - 0.01 * i as f32;
6629            if depth > sample_depth + 0.001 {
6630                if mip == 0 {
6631                    return Some((pos, depth));
6632                }
6633                mip = mip.saturating_sub(1);
6634            } else {
6635                mip = (mip + 1).min(max_mip);
6636            }
6637            depth += step.length() * 0.1;
6638        }
6639        None
6640    }
6641
6642    /// Build a 2D AABB for the ray march step at a given hi-z level
6643    pub fn cell_bounds(pos: Vec2, mip: u32, texture_size: Vec2) -> (Vec2, Vec2) {
6644        let cell_size = Vec2::splat((1 << mip) as f32) / texture_size;
6645        let cell = (pos / cell_size).floor();
6646        (cell * cell_size, (cell + Vec2::ONE) * cell_size)
6647    }
6648
6649    /// Compute the t-values at which the ray crosses cell boundaries
6650    pub fn intersect_cell_boundary(pos: Vec2, dir: Vec2, cell_min: Vec2, cell_max: Vec2) -> f32 {
6651        let t_max_x = if dir.x > 0.0 { (cell_max.x - pos.x) / (dir.x + 1e-7) }
6652                      else if dir.x < 0.0 { (cell_min.x - pos.x) / (dir.x - 1e-7) }
6653                      else { f32::MAX };
6654        let t_max_y = if dir.y > 0.0 { (cell_max.y - pos.y) / (dir.y + 1e-7) }
6655                      else if dir.y < 0.0 { (cell_min.y - pos.y) / (dir.y - 1e-7) }
6656                      else { f32::MAX };
6657        t_max_x.min(t_max_y)
6658    }
6659}
6660
6661// ============================================================
6662//  RENDER GRAPH — SUBSURFACE SCATTERING PASS
6663// ============================================================
6664
6665#[derive(Debug, Clone)]
6666pub struct SSSPassDesc {
6667    pub width: u32,
6668    pub height: u32,
6669    pub output_format: TextureFormat,
6670    pub output_sss: ResourceId,
6671    pub input_irradiance: ResourceId,
6672    pub input_depth: ResourceId,
6673    pub input_albedo: ResourceId,
6674    pub algorithm: SSSAlgorithm,
6675    pub falloff: Vec3,
6676    pub strength: Vec3,
6677    pub max_radius_px: f32,
6678    pub sample_count: u32,
6679}
6680
6681#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6682pub enum SSSAlgorithm { BurleyDiffusion, SeparableSSS, PreintegratedSSS }
6683
6684impl SSSPassDesc {
6685    pub fn default(width: u32, height: u32) -> Self {
6686        SSSPassDesc {
6687            width, height,
6688            output_format: TextureFormat::RGBA16Float,
6689            output_sss: ResourceId(400),
6690            input_irradiance: ResourceId(10),
6691            input_depth: ResourceId(4),
6692            input_albedo: ResourceId(0),
6693            algorithm: SSSAlgorithm::SeparableSSS,
6694            falloff: Vec3::new(1.0, 0.37, 0.3),
6695            strength: Vec3::new(0.48, 0.41, 0.28),
6696            max_radius_px: 25.0,
6697            sample_count: 25,
6698        }
6699    }
6700
6701    /// Burley normalized diffusion profile
6702    pub fn burley_diffusion_profile(r: f32, s: f32) -> f32 {
6703        ((-s * r).exp() + (-s * r / 3.0).exp()) / (8.0 * std::f32::consts::PI * r)
6704    }
6705
6706    /// Generate separable SSS kernel samples
6707    pub fn separable_kernel(&self) -> Vec<Vec4> {
6708        let mut kernel = Vec::with_capacity(self.sample_count as usize);
6709        let n = self.sample_count as f32;
6710        for i in 0..self.sample_count {
6711            let r = ((i as f32 + 0.5) / n) * self.max_radius_px;
6712            // Gaussian profile approximation
6713            let sigma = self.max_radius_px * 0.25;
6714            let w = (-0.5 * (r / sigma) * (r / sigma)).exp();
6715            let offset = r;
6716            kernel.push(Vec4::new(offset, w * self.strength.x, w * self.strength.y, w * self.strength.z));
6717        }
6718        // Normalize weights
6719        let sum_w: f32 = kernel.iter().map(|k| k.y).sum();
6720        if sum_w > 1e-6 {
6721            for k in &mut kernel { k.y /= sum_w; k.z /= sum_w; k.w /= sum_w; }
6722        }
6723        kernel
6724    }
6725
6726    /// Pre-integrated SSS look-up table: maps (NdotL, curvature) -> diffuse response
6727    pub fn preintegrated_lut_value(n_dot_l: f32, curvature: f32) -> Vec3 {
6728        // Simplified fit to d'Eon & Luebke pre-integrated SSS
6729        let wrap = (n_dot_l + curvature * 0.5).clamp(0.0, 1.0);
6730        let redness = (curvature * 5.0).clamp(0.0, 1.0);
6731        Vec3::new(
6732            lerp(smoothstep(-0.2, 0.8, n_dot_l), wrap, redness),
6733            smoothstep(-0.1, 0.7, n_dot_l),
6734            smoothstep(0.0, 0.6, n_dot_l),
6735        )
6736    }
6737}
6738
6739// ============================================================
6740//  RENDER GRAPH — AMBIENT OCCLUSION VARIANTS
6741// ============================================================
6742
6743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6744pub enum AOAlgorithm { SSAO, HBAO, GTAO, RTAO }
6745
6746#[derive(Debug, Clone)]
6747pub struct GTAOPassDesc {
6748    pub width: u32,
6749    pub height: u32,
6750    pub output_format: TextureFormat,
6751    pub output_ao: ResourceId,
6752    pub input_depth: ResourceId,
6753    pub input_normal: ResourceId,
6754    pub num_directions: u32,
6755    pub num_steps: u32,
6756    pub radius: f32,
6757    pub thickness: f32,
6758    pub falloff_range: f32,
6759    pub sample_distribution_power: f32,
6760    pub depth_mip_sampling_offset: f32,
6761    pub thin_occluder_compensation: f32,
6762    pub final_value_power: f32,
6763    pub denoise_passes: u32,
6764    pub half_resolution: bool,
6765}
6766
6767impl GTAOPassDesc {
6768    pub fn default(width: u32, height: u32) -> Self {
6769        GTAOPassDesc {
6770            width, height,
6771            output_format: TextureFormat::R8Unorm,
6772            output_ao: ResourceId(420),
6773            input_depth: ResourceId(4),
6774            input_normal: ResourceId(1),
6775            num_directions: 2,
6776            num_steps: 3,
6777            radius: 0.5,
6778            thickness: 1.0,
6779            falloff_range: 0.615,
6780            sample_distribution_power: 2.0,
6781            depth_mip_sampling_offset: 3.3,
6782            thin_occluder_compensation: 0.0,
6783            final_value_power: 2.2,
6784            denoise_passes: 1,
6785            half_resolution: false,
6786        }
6787    }
6788
6789    /// Compute GTAO horizon angle for a single direction
6790    pub fn compute_bent_normal_gtao(normal: Vec3, view_dir: Vec3, directions: &[(Vec3, Vec3)], weights: &[f32]) -> (Vec3, f32) {
6791        let mut visibility = 0.0f32;
6792        let mut bent_normal = Vec3::ZERO;
6793        for ((dir_x, dir_y), &w) in directions.iter().zip(weights.iter()) {
6794            let cos_h = dir_x.dot(normal).clamp(0.0, 1.0);
6795            visibility += cos_h * w;
6796            bent_normal += *dir_x * cos_h * w;
6797        }
6798        let bent = if bent_normal.length() > 1e-6 { bent_normal.normalize() } else { normal };
6799        (bent, visibility)
6800    }
6801
6802    /// Approximate integration of visibility over hemisphere using bent normal
6803    pub fn bent_normal_visibility(bent_normal: Vec3, mean_visibility: f32, roughness: f32) -> f32 {
6804        // Simplified from Jimenez et al. "Practical Realtime Strategies for Accurate Indirect Occlusion"
6805        let t = 1.0 - mean_visibility;
6806        let r = roughness.clamp(0.0, 1.0);
6807        lerp(mean_visibility, 1.0 - t * (1.0 - r), r)
6808    }
6809}
6810
6811// ============================================================
6812//  RENDER GRAPH — PROBE-BASED GI (DDGI)
6813// ============================================================
6814
6815#[derive(Debug, Clone)]
6816pub struct DDGIPassDesc {
6817    pub probe_grid_x: u32,
6818    pub probe_grid_y: u32,
6819    pub probe_grid_z: u32,
6820    pub probe_spacing: f32,
6821    pub probe_origin: Vec3,
6822    pub rays_per_probe: u32,
6823    pub irradiance_oct_size: u32,   // octahedral probe atlas (e.g. 8x8 per probe)
6824    pub visibility_oct_size: u32,
6825    pub output_irradiance: ResourceId,
6826    pub output_visibility: ResourceId,
6827    pub hysteresis: f32,
6828    pub brightness_threshold: f32,
6829    pub view_bias: f32,
6830    pub normal_bias: f32,
6831}
6832
6833impl DDGIPassDesc {
6834    pub fn default() -> Self {
6835        DDGIPassDesc {
6836            probe_grid_x: 12, probe_grid_y: 6, probe_grid_z: 12,
6837            probe_spacing: 3.0,
6838            probe_origin: Vec3::new(-18.0, 0.0, -18.0),
6839            rays_per_probe: 128,
6840            irradiance_oct_size: 8,
6841            visibility_oct_size: 16,
6842            output_irradiance: ResourceId(500),
6843            output_visibility: ResourceId(501),
6844            hysteresis: 0.98,
6845            brightness_threshold: 10.0,
6846            view_bias: 0.3,
6847            normal_bias: 0.08,
6848        }
6849    }
6850
6851    pub fn total_probes(&self) -> u32 { self.probe_grid_x * self.probe_grid_y * self.probe_grid_z }
6852
6853    pub fn irradiance_atlas_size(&self) -> (u32, u32) {
6854        let probes_per_row = 64u32;
6855        let rows = (self.total_probes() + probes_per_row - 1) / probes_per_row;
6856        (probes_per_row * (self.irradiance_oct_size + 2), rows * (self.irradiance_oct_size + 2))
6857    }
6858
6859    pub fn visibility_atlas_size(&self) -> (u32, u32) {
6860        let probes_per_row = 32u32;
6861        let rows = (self.total_probes() + probes_per_row - 1) / probes_per_row;
6862        (probes_per_row * (self.visibility_oct_size + 2), rows * (self.visibility_oct_size + 2))
6863    }
6864
6865    pub fn probe_world_pos(&self, ix: u32, iy: u32, iz: u32) -> Vec3 {
6866        self.probe_origin + Vec3::new(
6867            ix as f32 * self.probe_spacing,
6868            iy as f32 * self.probe_spacing,
6869            iz as f32 * self.probe_spacing,
6870        )
6871    }
6872
6873    pub fn probe_index_from_world(&self, world: Vec3) -> Option<(u32, u32, u32)> {
6874        let local = (world - self.probe_origin) / self.probe_spacing;
6875        let ix = local.x.round() as i32;
6876        let iy = local.y.round() as i32;
6877        let iz = local.z.round() as i32;
6878        if ix >= 0 && iy >= 0 && iz >= 0 &&
6879           ix < self.probe_grid_x as i32 && iy < self.probe_grid_y as i32 && iz < self.probe_grid_z as i32 {
6880            Some((ix as u32, iy as u32, iz as u32))
6881        } else {
6882            None
6883        }
6884    }
6885
6886    /// Trilinear blend weights for sampling irradiance between 8 nearest probes
6887    pub fn trilinear_weights(local_blend: Vec3) -> [f32; 8] {
6888        let (x, y, z) = (local_blend.x, local_blend.y, local_blend.z);
6889        let (mx, my, mz) = (1.0 - x, 1.0 - y, 1.0 - z);
6890        [
6891            mx * my * mz,
6892            x  * my * mz,
6893            mx * y  * mz,
6894            x  * y  * mz,
6895            mx * my * z,
6896            x  * my * z,
6897            mx * y  * z,
6898            x  * y  * z,
6899        ]
6900    }
6901
6902    /// Memory required for DDGI atlas textures
6903    pub fn atlas_memory_bytes(&self) -> u64 {
6904        let (iw, ih) = self.irradiance_atlas_size();
6905        let (vw, vh) = self.visibility_atlas_size();
6906        // RGBA16F for irradiance, RG16F for visibility (depth+depth^2)
6907        let irr = iw as u64 * ih as u64 * 8;
6908        let vis = vw as u64 * vh as u64 * 4;
6909        irr + vis
6910    }
6911}
6912
6913// ============================================================
6914//  RENDER GRAPH — RAY TRACING PASSES
6915// ============================================================
6916
6917#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6918pub enum RTPassKind { ReflectionDenoise, AO, GI, ShadowDenoise }
6919
6920#[derive(Debug, Clone)]
6921pub struct RTPassDesc {
6922    pub width: u32,
6923    pub height: u32,
6924    pub output_format: TextureFormat,
6925    pub output: ResourceId,
6926    pub kind: RTPassKind,
6927    pub samples_per_pixel: u32,
6928    pub max_bounces: u32,
6929    pub russian_roulette_min_bounces: u32,
6930    pub denoiser: RTDenoiser,
6931    pub temporal_accumulation: bool,
6932    pub reprojection_tolerance: f32,
6933}
6934
6935#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6936pub enum RTDenoiser { None, Temporal, SVGF, OIDN }
6937
6938impl RTPassDesc {
6939    pub fn rt_ao(width: u32, height: u32) -> Self {
6940        RTPassDesc {
6941            width, height,
6942            output_format: TextureFormat::R16Float,
6943            output: ResourceId(600),
6944            kind: RTPassKind::AO,
6945            samples_per_pixel: 1,
6946            max_bounces: 1,
6947            russian_roulette_min_bounces: 1,
6948            denoiser: RTDenoiser::Temporal,
6949            temporal_accumulation: true,
6950            reprojection_tolerance: 0.001,
6951        }
6952    }
6953    pub fn rt_reflections(width: u32, height: u32) -> Self {
6954        RTPassDesc {
6955            width, height,
6956            output_format: TextureFormat::RGBA16Float,
6957            output: ResourceId(601),
6958            kind: RTPassKind::ReflectionDenoise,
6959            samples_per_pixel: 1,
6960            max_bounces: 2,
6961            russian_roulette_min_bounces: 2,
6962            denoiser: RTDenoiser::SVGF,
6963            temporal_accumulation: true,
6964            reprojection_tolerance: 0.005,
6965        }
6966    }
6967    pub fn rt_gi(width: u32, height: u32) -> Self {
6968        RTPassDesc {
6969            width, height,
6970            output_format: TextureFormat::RGBA16Float,
6971            output: ResourceId(602),
6972            kind: RTPassKind::GI,
6973            samples_per_pixel: 1,
6974            max_bounces: 3,
6975            russian_roulette_min_bounces: 2,
6976            denoiser: RTDenoiser::SVGF,
6977            temporal_accumulation: true,
6978            reprojection_tolerance: 0.002,
6979        }
6980    }
6981    pub fn dispatch_size(&self, tile: u32) -> (u32, u32) {
6982        ((self.width + tile - 1) / tile, (self.height + tile - 1) / tile)
6983    }
6984}
6985
6986// ============================================================
6987//  COLOR SCIENCE — ACES FULL TRANSFORM
6988// ============================================================
6989
6990pub struct ACESTransform;
6991impl ACESTransform {
6992    /// Input transform (IDT): Linear sRGB to AP0 (ACES 2065-1)
6993    pub fn linear_srgb_to_aces2065(c: Vec3) -> Vec3 {
6994        // Approximation of the sRGB IDT matrix
6995        let m = [
6996            [0.4397010, 0.3829780, 0.1773350],
6997            [0.0897923, 0.8134230, 0.0967616],
6998            [0.0175440, 0.1115440, 0.8707040],
6999        ];
7000        Vec3::new(
7001            m[0][0]*c.x + m[0][1]*c.y + m[0][2]*c.z,
7002            m[1][0]*c.x + m[1][1]*c.y + m[1][2]*c.z,
7003            m[2][0]*c.x + m[2][1]*c.y + m[2][2]*c.z,
7004        )
7005    }
7006
7007    /// RRT + ODT combined for sRGB display (simplified Narkowicz fit)
7008    pub fn rrt_odt_srgb(c: Vec3) -> Vec3 {
7009        let a = c * (c + Vec3::splat(0.0245786)) - Vec3::splat(0.000090537);
7010        let b = c * (Vec3::splat(0.983729) * c + Vec3::splat(0.4329510)) + Vec3::splat(0.238081);
7011        (a / b).clamp(Vec3::ZERO, Vec3::ONE)
7012    }
7013
7014    /// Full ACES pipeline
7015    pub fn full_pipeline(linear_srgb: Vec3, exposure: f32) -> Vec3 {
7016        let aces = Self::linear_srgb_to_aces2065(linear_srgb * exposure);
7017        let out = Self::rrt_odt_srgb(aces);
7018        out
7019    }
7020
7021    /// Generate a 3D LUT for ACES at a given size (e.g., 32^3)
7022    pub fn bake_lut(lut_size: u32) -> Vec<Vec3> {
7023        let n = lut_size as usize;
7024        let mut lut = Vec::with_capacity(n * n * n);
7025        for bz in 0..n {
7026            for gy in 0..n {
7027                for rx in 0..n {
7028                    let r = rx as f32 / (n - 1) as f32;
7029                    let g = gy as f32 / (n - 1) as f32;
7030                    let b = bz as f32 / (n - 1) as f32;
7031                    // Assume input is in linear light (no exposure adjust here)
7032                    let input = Vec3::new(r, g, b) * 4.0; // HDR->SDR input range
7033                    let output = Self::rrt_odt_srgb(input);
7034                    lut.push(output);
7035                }
7036            }
7037        }
7038        lut
7039    }
7040
7041    /// Sample the LUT (trilinear)
7042    pub fn sample_lut(lut: &[Vec3], lut_size: u32, color: Vec3) -> Vec3 {
7043        let n = lut_size as usize;
7044        let c = color.clamp(Vec3::ZERO, Vec3::ONE) * (n - 1) as f32;
7045        let x0 = (c.x as usize).min(n - 2);
7046        let y0 = (c.y as usize).min(n - 2);
7047        let z0 = (c.z as usize).min(n - 2);
7048        let fx = c.x.fract();
7049        let fy = c.y.fract();
7050        let fz = c.z.fract();
7051        let idx = |x: usize, y: usize, z: usize| z * n * n + y * n + x;
7052        let c000 = lut.get(idx(x0,   y0,   z0)).cloned().unwrap_or(Vec3::ZERO);
7053        let c100 = lut.get(idx(x0+1, y0,   z0)).cloned().unwrap_or(Vec3::ZERO);
7054        let c010 = lut.get(idx(x0,   y0+1, z0)).cloned().unwrap_or(Vec3::ZERO);
7055        let c110 = lut.get(idx(x0+1, y0+1, z0)).cloned().unwrap_or(Vec3::ZERO);
7056        let c001 = lut.get(idx(x0,   y0,   z0+1)).cloned().unwrap_or(Vec3::ZERO);
7057        let c101 = lut.get(idx(x0+1, y0,   z0+1)).cloned().unwrap_or(Vec3::ZERO);
7058        let c011 = lut.get(idx(x0,   y0+1, z0+1)).cloned().unwrap_or(Vec3::ZERO);
7059        let c111 = lut.get(idx(x0+1, y0+1, z0+1)).cloned().unwrap_or(Vec3::ZERO);
7060        let c00 = lerp_vec3(c000, c100, fx);
7061        let c01 = lerp_vec3(c001, c101, fx);
7062        let c10 = lerp_vec3(c010, c110, fx);
7063        let c11 = lerp_vec3(c011, c111, fx);
7064        let c0 = lerp_vec3(c00, c10, fy);
7065        let c1 = lerp_vec3(c01, c11, fy);
7066        lerp_vec3(c0, c1, fz)
7067    }
7068}
7069
7070// ============================================================
7071//  GPU CULLING PASS
7072// ============================================================
7073
7074#[derive(Debug, Clone)]
7075pub struct GPUCullingPassDesc {
7076    pub max_draw_calls: u32,
7077    pub output_draw_indirect: ResourceId,
7078    pub output_draw_count: ResourceId,
7079    pub input_bounding_spheres: ResourceId,
7080    pub input_draw_params: ResourceId,
7081    pub use_hi_z_occlusion: bool,
7082    pub use_frustum_culling: bool,
7083    pub hi_z_mip_levels: u32,
7084    pub tile_size: u32,
7085}
7086
7087impl GPUCullingPassDesc {
7088    pub fn default() -> Self {
7089        GPUCullingPassDesc {
7090            max_draw_calls: 65536,
7091            output_draw_indirect: ResourceId(700),
7092            output_draw_count: ResourceId(701),
7093            input_bounding_spheres: ResourceId(702),
7094            input_draw_params: ResourceId(703),
7095            use_hi_z_occlusion: true,
7096            use_frustum_culling: true,
7097            hi_z_mip_levels: 10,
7098            tile_size: 64,
7099        }
7100    }
7101    pub fn dispatch_size(&self) -> u32 { (self.max_draw_calls + 63) / 64 }
7102    pub fn draw_indirect_buffer_bytes(&self) -> u64 {
7103        // VkDrawIndexedIndirectCommand: 5 * 4 = 20 bytes
7104        self.max_draw_calls as u64 * 20
7105    }
7106    pub fn bounding_sphere_buffer_bytes(&self) -> u64 {
7107        // center(3 floats) + radius(1 float) = 16 bytes
7108        self.max_draw_calls as u64 * 16
7109    }
7110}
7111
7112// ============================================================
7113//  RENDER GRAPH — PASS REORDERING FOR CACHE EFFICIENCY
7114// ============================================================
7115
7116pub struct PassReorderer;
7117impl PassReorderer {
7118    /// Reorder passes to maximize render target reuse (avoid L2 cache thrashing)
7119    /// Uses a greedy approach: next pass reads from current pass's writes if possible
7120    pub fn reorder_for_cache(sorted: &[PassId], pass_map: &HashMap<PassId, PassNode>) -> Vec<PassId> {
7121        let mut remaining: Vec<PassId> = sorted.to_vec();
7122        let mut result: Vec<PassId> = Vec::with_capacity(remaining.len());
7123        let mut last_writes: HashSet<ResourceId> = HashSet::new();
7124        while !remaining.is_empty() {
7125            // Prefer passes that read from last_writes
7126            let best = remaining.iter().enumerate().max_by_key(|(_, pid)| {
7127                let pass = match pass_map.get(*pid) { Some(p) => p, None => return 0 };
7128                pass.reads.iter().filter(|r| last_writes.contains(*r)).count()
7129            });
7130            if let Some((idx, _)) = best {
7131                let pid = remaining.remove(idx);
7132                if let Some(pass) = pass_map.get(&pid) {
7133                    last_writes.clear();
7134                    for w in &pass.writes { last_writes.insert(*w); }
7135                }
7136                result.push(pid);
7137            } else {
7138                break;
7139            }
7140        }
7141        result.extend(remaining);
7142        result
7143    }
7144}
7145
7146// ============================================================
7147//  TONEMAPPING LUT BAKING — UTILITY
7148// ============================================================
7149
7150pub fn bake_tonemapping_lut(width: u32, operator: ToneMappingOperator, exposure: f32, white_point: f32) -> Vec<Vec3> {
7151    let op = ToneMappingPassDesc {
7152        width, height: 1,
7153        output_format: TextureFormat::RGBA8UnormSrgb,
7154        output_sdr: ResourceId(0), input_hdr: ResourceId(0), input_bloom: ResourceId(0),
7155        operator, exposure, gamma: 2.2, white_point, color_lut_enabled: false, color_lut_size: 0,
7156    };
7157    let n = width as usize;
7158    let mut out = Vec::with_capacity(n);
7159    for i in 0..n {
7160        let t = i as f32 / (n - 1) as f32;
7161        let hdr = Vec3::splat(t * white_point);
7162        let mapped = op.apply_operator(hdr);
7163        let linear = op.gamma_correct(mapped);
7164        out.push(linear);
7165    }
7166    out
7167}
7168
7169// ============================================================
7170//  ADAPTIVE RESOLUTION SCALING
7171// ============================================================
7172
7173#[derive(Debug, Clone)]
7174pub struct AdaptiveResolutionScaler {
7175    pub target_frame_time_ms: f32,
7176    pub min_scale: f32,           // e.g., 0.5 = half resolution
7177    pub max_scale: f32,           // e.g., 1.0 = full resolution
7178    pub current_scale: f32,
7179    pub increase_threshold: f32,  // increase if GPU time < target * this
7180    pub decrease_threshold: f32,  // decrease if GPU time > target * this
7181    pub increase_rate: f32,
7182    pub decrease_rate: f32,
7183    pub history: VecDeque<f32>,
7184    pub history_length: usize,
7185}
7186
7187impl AdaptiveResolutionScaler {
7188    pub fn new(target_ms: f32) -> Self {
7189        AdaptiveResolutionScaler {
7190            target_frame_time_ms: target_ms,
7191            min_scale: 0.5,
7192            max_scale: 1.0,
7193            current_scale: 1.0,
7194            increase_threshold: 0.85,
7195            decrease_threshold: 1.05,
7196            increase_rate: 0.005,
7197            decrease_rate: 0.02,
7198            history: VecDeque::new(),
7199            history_length: 10,
7200        }
7201    }
7202
7203    pub fn update(&mut self, gpu_time_ms: f32) {
7204        if self.history.len() >= self.history_length { self.history.pop_front(); }
7205        self.history.push_back(gpu_time_ms);
7206        let avg: f32 = self.history.iter().sum::<f32>() / self.history.len() as f32;
7207        if avg < self.target_frame_time_ms * self.increase_threshold {
7208            self.current_scale = (self.current_scale + self.increase_rate).min(self.max_scale);
7209        } else if avg > self.target_frame_time_ms * self.decrease_threshold {
7210            self.current_scale = (self.current_scale - self.decrease_rate).max(self.min_scale);
7211        }
7212    }
7213
7214    pub fn scaled_resolution(&self, base_width: u32, base_height: u32) -> (u32, u32) {
7215        let w = ((base_width as f32 * self.current_scale) as u32).max(1);
7216        let h = ((base_height as f32 * self.current_scale) as u32).max(1);
7217        // Round down to multiple of 2 for cleaner upscaling
7218        (w & !1, h & !1)
7219    }
7220
7221    pub fn upscale_needed(&self) -> bool { self.current_scale < 1.0 }
7222    pub fn quality_level(&self) -> &'static str {
7223        if self.current_scale >= 0.95 { "Ultra" }
7224        else if self.current_scale >= 0.75 { "Quality" }
7225        else if self.current_scale >= 0.60 { "Balanced" }
7226        else { "Performance" }
7227    }
7228}
7229
7230// ============================================================
7231//  UPSCALING PASS (FSR/DLSS-style)
7232// ============================================================
7233
7234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7235pub enum UpscalerKind { Bilinear, Lanczos, FSR1, FSR2, DLSS, XeSS, CAS }
7236
7237#[derive(Debug, Clone)]
7238pub struct UpscalePassDesc {
7239    pub input_width: u32,
7240    pub input_height: u32,
7241    pub output_width: u32,
7242    pub output_height: u32,
7243    pub input_color: ResourceId,
7244    pub input_depth: ResourceId,
7245    pub input_velocity: ResourceId,
7246    pub output_color: ResourceId,
7247    pub kind: UpscalerKind,
7248    pub sharpness: f32,
7249    pub mip_bias: f32,
7250}
7251
7252impl UpscalePassDesc {
7253    pub fn fsr1(iw: u32, ih: u32, ow: u32, oh: u32, input: ResourceId, output: ResourceId) -> Self {
7254        UpscalePassDesc {
7255            input_width: iw, input_height: ih, output_width: ow, output_height: oh,
7256            input_color: input, input_depth: ResourceId(4), input_velocity: ResourceId(3),
7257            output_color: output,
7258            kind: UpscalerKind::FSR1,
7259            sharpness: 0.8,
7260            mip_bias: (iw as f32 / ow as f32).log2() - 1.0,
7261        }
7262    }
7263
7264    pub fn scale_factor(&self) -> f32 {
7265        self.input_width as f32 / self.output_width as f32
7266    }
7267
7268    /// FSR1 EASU filter kernel (simplified for demonstration)
7269    pub fn easu_filter_sample(input_uv: Vec2, texel_size: Vec2, jitter: Vec2) -> [Vec2; 5] {
7270        // EASU 5-tap cross-pattern
7271        let center = input_uv;
7272        [
7273            center,
7274            center + Vec2::new( texel_size.x, 0.0),
7275            center + Vec2::new(-texel_size.x, 0.0),
7276            center + Vec2::new(0.0,  texel_size.y),
7277            center + Vec2::new(0.0, -texel_size.y),
7278        ]
7279    }
7280
7281    /// Lanczos 3 filter weight
7282    pub fn lanczos3_weight(x: f32) -> f32 {
7283        let a = 3.0f32;
7284        if x.abs() < 1e-6 { 1.0 }
7285        else if x.abs() < a {
7286            let px = std::f32::consts::PI * x;
7287            let pa = std::f32::consts::PI * x / a;
7288            a * px.sin() * pa.sin() / (px * px)
7289        } else {
7290            0.0
7291        }
7292    }
7293
7294    pub fn lanczos3_reconstruct(center: Vec3, samples: &[(Vec3, Vec2)], output_uv: Vec2) -> Vec3 {
7295        let mut sum = Vec3::ZERO;
7296        let mut weight_sum = 0.0f32;
7297        for (color, input_uv) in samples {
7298            let dx = (output_uv.x - input_uv.x);
7299            let dy = (output_uv.y - input_uv.y);
7300            let w = Self::lanczos3_weight(dx) * Self::lanczos3_weight(dy);
7301            sum += *color * w;
7302            weight_sum += w;
7303        }
7304        if weight_sum.abs() < 1e-6 { center } else { sum / weight_sum }
7305    }
7306}
7307
7308// ============================================================
7309//  FINAL INTEGRATION TESTS
7310// ============================================================
7311
7312#[cfg(test)]
7313mod integration_tests {
7314    use super::*;
7315
7316    #[test]
7317    fn test_vxgi_memory() {
7318        let vxgi = VXGIPassDesc::default();
7319        let mem = vxgi.grid_memory_bytes();
7320        assert!(mem > 0);
7321        let mips = vxgi.mip_levels();
7322        assert_eq!(mips, 9); // log2(256) + 1
7323    }
7324
7325    #[test]
7326    fn test_ddgi_probe_positions() {
7327        let ddgi = DDGIPassDesc::default();
7328        let p = ddgi.probe_world_pos(0, 0, 0);
7329        assert_eq!(p, ddgi.probe_origin);
7330        let p2 = ddgi.probe_world_pos(1, 0, 0);
7331        assert!((p2.x - p.x - ddgi.probe_spacing).abs() < 1e-5);
7332    }
7333
7334    #[test]
7335    fn test_ddgi_trilinear_weights() {
7336        let weights = DDGIPassDesc::trilinear_weights(Vec3::splat(0.5));
7337        let sum: f32 = weights.iter().sum();
7338        assert!((sum - 1.0).abs() < 1e-4);
7339    }
7340
7341    #[test]
7342    fn test_sss_kernel_normalization() {
7343        let sss = SSSPassDesc::default(1920, 1080);
7344        let kernel = sss.separable_kernel();
7345        assert!(!kernel.is_empty());
7346        let sum_w: f32 = kernel.iter().map(|k| k.y).sum();
7347        assert!((sum_w - 1.0).abs() < 1e-4, "kernel weights should sum to 1, got {}", sum_w);
7348    }
7349
7350    #[test]
7351    fn test_sss_preintegrated_lut() {
7352        let v = SSSPassDesc::preintegrated_lut_value(1.0, 0.0);
7353        assert!(v.x >= 0.0 && v.x <= 1.0);
7354        assert!(v.y >= 0.0 && v.y <= 1.0);
7355    }
7356
7357    #[test]
7358    fn test_burley_diffusion_profile() {
7359        let v = SSSPassDesc::burley_diffusion_profile(0.1, 1.0);
7360        assert!(v > 0.0);
7361        let v2 = SSSPassDesc::burley_diffusion_profile(2.0, 1.0);
7362        assert!(v > v2, "profile should fall off with distance");
7363    }
7364
7365    #[test]
7366    fn test_aces_lut_baking_small() {
7367        let lut = ACESTransform::bake_lut(4);
7368        assert_eq!(lut.len(), 4 * 4 * 4);
7369        for c in &lut {
7370            assert!(c.x >= 0.0 && c.x <= 1.0);
7371        }
7372    }
7373
7374    #[test]
7375    fn test_aces_lut_sampling() {
7376        let lut = ACESTransform::bake_lut(32);
7377        let c = ACESTransform::sample_lut(&lut, 32, Vec3::splat(0.5));
7378        assert!(c.length() >= 0.0);
7379    }
7380
7381    #[test]
7382    fn test_adaptive_resolution_scale_up() {
7383        let mut scaler = AdaptiveResolutionScaler::new(16.67);
7384        scaler.current_scale = 0.7;
7385        for _ in 0..20 { scaler.update(10.0); } // well under budget
7386        assert!(scaler.current_scale > 0.7);
7387    }
7388
7389    #[test]
7390    fn test_adaptive_resolution_scale_down() {
7391        let mut scaler = AdaptiveResolutionScaler::new(16.67);
7392        for _ in 0..20 { scaler.update(25.0); } // over budget
7393        assert!(scaler.current_scale < 1.0);
7394    }
7395
7396    #[test]
7397    fn test_lanczos3_weight() {
7398        let w0 = UpscalePassDesc::lanczos3_weight(0.0);
7399        assert!((w0 - 1.0).abs() < 1e-4);
7400        let w_out = UpscalePassDesc::lanczos3_weight(3.5);
7401        assert_eq!(w_out, 0.0);
7402    }
7403
7404    #[test]
7405    fn test_tonemapping_lut_bake() {
7406        let lut = bake_tonemapping_lut(256, ToneMappingOperator::ACES, 1.0, 4.0);
7407        assert_eq!(lut.len(), 256);
7408        assert!(lut.iter().all(|c| c.x >= 0.0 && c.x <= 1.001));
7409    }
7410
7411    #[test]
7412    fn test_gpu_culling_pass_sizes() {
7413        let cull = GPUCullingPassDesc::default();
7414        assert!(cull.draw_indirect_buffer_bytes() > 0);
7415        assert!(cull.bounding_sphere_buffer_bytes() > 0);
7416    }
7417
7418    #[test]
7419    fn test_pass_reorder_for_cache() {
7420        let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7421        let sorted: Vec<PassId> = editor.passes.keys().cloned().collect();
7422        let reordered = PassReorderer::reorder_for_cache(&sorted, &editor.passes);
7423        assert_eq!(reordered.len(), sorted.len());
7424    }
7425
7426    #[test]
7427    fn test_hi_z_trace() {
7428        let result = HiZTracer::trace(Vec2::new(0.5, 0.5), Vec2::new(0.01, 0.0), 0.5, 64, 8);
7429        // May or may not hit, just verify no panic
7430        let _ = result;
7431    }
7432
7433    #[test]
7434    fn test_editor_history() {
7435        let mut hist = EditorHistory::new(32);
7436        assert!(!hist.can_undo());
7437        hist.push(EditorAction::AddPass(PassId(0), "Test".to_owned()));
7438        assert!(hist.can_undo());
7439        let act = hist.pop_undo();
7440        assert!(act.is_some());
7441        assert!(!hist.can_undo());
7442    }
7443
7444    #[test]
7445    fn test_pass_group_bounds() {
7446        let mut group = PassGroup::new(0, "Deferred", vec![PassId(0), PassId(1)], Vec4::ONE);
7447        let mut positions = HashMap::new();
7448        positions.insert(PassId(0), Vec2::new(10.0, 20.0));
7449        positions.insert(PassId(1), Vec2::new(300.0, 200.0));
7450        let mut sizes = HashMap::new();
7451        sizes.insert(PassId(0), Vec2::new(200.0, 80.0));
7452        sizes.insert(PassId(1), Vec2::new(200.0, 80.0));
7453        group.compute_bounds(&positions, &sizes);
7454        assert!(group.contains_point(Vec2::new(100.0, 100.0)));
7455        assert!(!group.contains_point(Vec2::new(-100.0, -100.0)));
7456    }
7457
7458    #[test]
7459    fn test_graph_diff_empty() {
7460        let ed = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7461        let diffs = diff_render_graphs(&ed, &ed);
7462        // Diffing against itself: only connections may still match (no adds/removes)
7463        let adds_removes: Vec<_> = diffs.iter().filter(|d| matches!(d, GraphDiff::PassAdded(_, _) | GraphDiff::PassRemoved(_, _))).collect();
7464        assert!(adds_removes.is_empty());
7465    }
7466
7467    #[test]
7468    fn test_hot_reload_no_change() {
7469        let ed = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7470        let ed2 = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7471        let mut mgr = HotReloadManager::new(ed);
7472        mgr.stage_reload(ed2);
7473        let changed = mgr.apply_reload_if_pending(1);
7474        assert!(!changed); // identical graph → no diffs
7475    }
7476
7477    #[test]
7478    fn test_frame_debugger() {
7479        let mut dbg = FrameDebugger::new(8);
7480        dbg.begin_capture(0, 0.0);
7481        dbg.record_pass_timing(PassId(0), 2.5);
7482        dbg.record_pass_timing(PassId(1), 4.0);
7483        dbg.record_transition(PassId(0), ResourceId(0), ImageLayout::Undefined, ImageLayout::ColorAttachmentOptimal);
7484        dbg.end_capture();
7485        let cap = dbg.latest().unwrap();
7486        assert!((cap.total_gpu_ms() - 6.5).abs() < 1e-4);
7487        assert_eq!(cap.resource_transitions.len(), 1);
7488    }
7489
7490    #[test]
7491    fn test_critical_path() {
7492        let mut timings: HashMap<PassId, f64> = HashMap::new();
7493        timings.insert(PassId(0), 1.0);
7494        timings.insert(PassId(1), 4.0);
7495        timings.insert(PassId(2), 2.0);
7496        let mut edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
7497        edges.insert(PassId(0), vec![PassId(1)]);
7498        edges.insert(PassId(1), vec![PassId(2)]);
7499        edges.insert(PassId(2), vec![]);
7500        let sorted = vec![PassId(0), PassId(1), PassId(2)];
7501        let (path, total) = CriticalPathAnalyzer::find_critical_path(&sorted, &timings, &edges);
7502        assert!((total - 7.0).abs() < 1e-4, "total should be 7ms, got {}", total);
7503    }
7504
7505    #[test]
7506    fn test_bandwidth_profiler() {
7507        let mut profiler = BandwidthProfiler::new();
7508        profiler.record(PassId(0), ResourceId(0), true, 8 * 1920 * 1080, AccessFlags::COLOR_ATTACHMENT_WRITE, ImageLayout::ColorAttachmentOptimal);
7509        profiler.record(PassId(1), ResourceId(0), false, 8 * 1920 * 1080, AccessFlags::SHADER_READ, ImageLayout::ShaderReadOnlyOptimal);
7510        profiler.compute_totals();
7511        let total = profiler.total_bandwidth_mb();
7512        assert!(total > 0.0);
7513        let top = profiler.top_bandwidth_resources(3);
7514        assert!(!top.is_empty());
7515    }
7516}
7517
7518// ============================================================
7519//  RENDER GRAPH — SPARSE OCCLUSION CULLING
7520// ============================================================
7521
7522pub struct OcclusionCuller {
7523    pub hi_z_width: u32,
7524    pub hi_z_height: u32,
7525    pub mip_levels: u32,
7526}
7527
7528impl OcclusionCuller {
7529    pub fn new(width: u32, height: u32) -> Self {
7530        let mips = compute_mip_count(width, height);
7531        OcclusionCuller { hi_z_width: width, hi_z_height: height, mip_levels: mips }
7532    }
7533
7534    /// Test whether a screen-space AABB (in UV space) is occluded given
7535    /// a conservative min-depth estimate for the bounding box.
7536    pub fn is_occluded_hiz(
7537        &self,
7538        aabb_min_uv: Vec2,
7539        aabb_max_uv: Vec2,
7540        nearest_depth: f32,
7541        hi_z_mips: &[Vec<f32>], // mips[level] is a flat Vec<f32> of size (w>>level)*(h>>level)
7542    ) -> bool {
7543        let span = aabb_max_uv - aabb_min_uv;
7544        let max_span = span.x.max(span.y);
7545        // Select mip level that covers the AABB with ~2x2 texels
7546        let mip = ((max_span * self.hi_z_width.max(self.hi_z_height) as f32).log2() as u32).min(self.mip_levels - 1);
7547        let mip_w = (self.hi_z_width >> mip).max(1) as f32;
7548        let mip_h = (self.hi_z_height >> mip).max(1) as f32;
7549        // Sample the 4 corners of the AABB
7550        let uvs = [
7551            aabb_min_uv,
7552            Vec2::new(aabb_max_uv.x, aabb_min_uv.y),
7553            Vec2::new(aabb_min_uv.x, aabb_max_uv.y),
7554            aabb_max_uv,
7555        ];
7556        let mip_data = match hi_z_mips.get(mip as usize) { Some(d) => d, None => return false };
7557        let mut max_hi_z_depth = 0.0f32;
7558        for uv in &uvs {
7559            let ix = (uv.x * mip_w) as usize;
7560            let iy = (uv.y * mip_h) as usize;
7561            let idx = iy * mip_w as usize + ix;
7562            let d = mip_data.get(idx).cloned().unwrap_or(1.0);
7563            max_hi_z_depth = max_hi_z_depth.max(d);
7564        }
7565        // Object is occluded if its nearest surface is behind the max hi-z depth
7566        nearest_depth > max_hi_z_depth
7567    }
7568
7569    /// Build hi-z pyramid from a full-res depth buffer (conservative: max depth per 2x2 block)
7570    pub fn build_hi_z_pyramid(depth_buffer: &[f32], width: u32, height: u32) -> Vec<Vec<f32>> {
7571        let mips_count = compute_mip_count(width, height) as usize;
7572        let mut mips: Vec<Vec<f32>> = Vec::with_capacity(mips_count);
7573        // Mip 0: original depth buffer
7574        mips.push(depth_buffer.to_vec());
7575        let mut prev_w = width;
7576        let mut prev_h = height;
7577        for _ in 1..mips_count {
7578            let w = (prev_w / 2).max(1);
7579            let h = (prev_h / 2).max(1);
7580            let mut mip_data = vec![0.0f32; (w * h) as usize];
7581            let prev_data = mips.last().unwrap();
7582            for y in 0..h {
7583                for x in 0..w {
7584                    let px = x * 2;
7585                    let py = y * 2;
7586                    let d00 = *prev_data.get((py * prev_w + px) as usize).unwrap_or(&1.0);
7587                    let d10 = *prev_data.get((py * prev_w + (px+1).min(prev_w-1)) as usize).unwrap_or(&1.0);
7588                    let d01 = *prev_data.get(((py+1).min(prev_h-1) * prev_w + px) as usize).unwrap_or(&1.0);
7589                    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);
7590                    // Conservative: take max (farthest depth = least depth coverage)
7591                    mip_data[(y * w + x) as usize] = d00.max(d10).max(d01).max(d11);
7592                }
7593            }
7594            mips.push(mip_data);
7595            prev_w = w;
7596            prev_h = h;
7597        }
7598        mips
7599    }
7600}
7601
7602// ============================================================
7603//  RENDER GRAPH — EXPOSURE / AUTO-EXPOSURE PASS
7604// ============================================================
7605
7606#[derive(Debug, Clone)]
7607pub struct AutoExposurePassDesc {
7608    pub output_average_luminance: ResourceId,
7609    pub input_hdr: ResourceId,
7610    pub min_log_luminance: f32,   // e.g., -10 EV
7611    pub max_log_luminance: f32,   // e.g., +10 EV
7612    pub adaptation_speed_up: f32,
7613    pub adaptation_speed_down: f32,
7614    pub histogram_bins: u32,
7615    pub metered_area: Vec4,       // (x, y, w, h) in UV space; Vec4::new(0, 0, 1, 1) = full frame
7616    pub eye_adaptation_type: EyeAdaptation,
7617}
7618
7619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7620pub enum EyeAdaptation { Histogram, AverageLuminance }
7621
7622impl AutoExposurePassDesc {
7623    pub fn default() -> Self {
7624        AutoExposurePassDesc {
7625            output_average_luminance: ResourceId(800),
7626            input_hdr: ResourceId(10),
7627            min_log_luminance: -8.0,
7628            max_log_luminance: 8.0,
7629            adaptation_speed_up: 3.0,
7630            adaptation_speed_down: 1.0,
7631            histogram_bins: 256,
7632            metered_area: Vec4::new(0.1, 0.1, 0.8, 0.8),
7633            eye_adaptation_type: EyeAdaptation::Histogram,
7634        }
7635    }
7636
7637    /// Compute the log luminance bin index for a given luminance value
7638    pub fn luminance_to_bin(&self, lum: f32) -> u32 {
7639        let log_lum = lum.max(1e-5).ln() / std::f32::consts::LN_2; // log2
7640        let normalized = (log_lum - self.min_log_luminance) / (self.max_log_luminance - self.min_log_luminance);
7641        (normalized * self.histogram_bins as f32) as u32
7642    }
7643
7644    /// Compute exposure from average luminance (EV100)
7645    pub fn ev100_from_average_luminance(avg_lum: f32) -> f32 {
7646        (avg_lum * 100.0 / 12.5).log2()
7647    }
7648
7649    /// Adapt exposure over time (smooth interpolation)
7650    pub fn adapt_exposure(&self, current_ev: f32, target_ev: f32, delta_time: f32) -> f32 {
7651        let speed = if target_ev > current_ev { self.adaptation_speed_up } else { self.adaptation_speed_down };
7652        let factor = 1.0 - (-speed * delta_time).exp();
7653        current_ev + (target_ev - current_ev) * factor
7654    }
7655
7656    /// Weighted histogram percentile exposure (e.g., 50th percentile)
7657    pub fn percentile_from_histogram(histogram: &[u32], percentile: f32) -> f32 {
7658        let total: u64 = histogram.iter().map(|&c| c as u64).sum();
7659        if total == 0 { return 0.0; }
7660        let target_count = (total as f32 * percentile) as u64;
7661        let mut accum = 0u64;
7662        for (i, &count) in histogram.iter().enumerate() {
7663            accum += count as u64;
7664            if accum >= target_count {
7665                return i as f32 / histogram.len() as f32;
7666            }
7667        }
7668        1.0
7669    }
7670}
7671
7672// ============================================================
7673//  RENDER GRAPH — LENS FLARE PASS
7674// ============================================================
7675
7676#[derive(Debug, Clone)]
7677pub struct LensFlarePassDesc {
7678    pub width: u32,
7679    pub height: u32,
7680    pub output_format: TextureFormat,
7681    pub output_flare: ResourceId,
7682    pub input_hdr: ResourceId,
7683    pub threshold: f32,
7684    pub intensity: f32,
7685    pub ghost_count: u32,
7686    pub ghost_dispersal: f32,
7687    pub ghost_threshold: f32,
7688    pub halo_width: f32,
7689    pub halo_intensity: f32,
7690    pub distortion: f32,
7691    pub use_lens_dirt: bool,
7692    pub use_star_burst: bool,
7693    pub star_burst_samples: u32,
7694}
7695
7696impl LensFlarePassDesc {
7697    pub fn default(width: u32, height: u32) -> Self {
7698        LensFlarePassDesc {
7699            width, height,
7700            output_format: TextureFormat::RGBA16Float,
7701            output_flare: ResourceId(900),
7702            input_hdr: ResourceId(10),
7703            threshold: 10.0,
7704            intensity: 0.5,
7705            ghost_count: 8,
7706            ghost_dispersal: 0.35,
7707            ghost_threshold: 50.0,
7708            halo_width: 0.5,
7709            halo_intensity: 0.8,
7710            distortion: 5.0,
7711            use_lens_dirt: true,
7712            use_star_burst: true,
7713            star_burst_samples: 6,
7714        }
7715    }
7716
7717    /// Compute ghost position in screen space given flare direction and ghost index
7718    pub fn ghost_position(flare_uv: Vec2, ghost_index: u32, dispersal: f32) -> Vec2 {
7719        let lens_center = Vec2::splat(0.5);
7720        let flare_dir = flare_uv - lens_center;
7721        let offset = flare_dir * ghost_index as f32 * dispersal;
7722        lens_center + offset
7723    }
7724
7725    /// Star burst kernel direction for a given sample
7726    pub fn star_burst_direction(sample: u32, total: u32) -> Vec2 {
7727        let angle = (sample as f32 / total as f32) * std::f32::consts::TAU;
7728        Vec2::new(angle.cos(), angle.sin())
7729    }
7730
7731    /// Chromatic distortion offset for a given color channel and distortion strength
7732    pub fn chromatic_distortion_offset(uv: Vec2, channel: u32, strength: f32) -> Vec2 {
7733        let center = Vec2::splat(0.5);
7734        let d = uv - center;
7735        let scale = 1.0 + strength * 0.01 * (channel as f32 - 1.0);
7736        center + d * scale
7737    }
7738}
7739
7740// ============================================================
7741//  RENDER GRAPH NODE PIN / CONNECTION TYPES
7742// ============================================================
7743
7744#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7745pub enum PinKind { Input, Output }
7746
7747#[derive(Debug, Clone)]
7748pub struct NodePin {
7749    pub pass_id: PassId,
7750    pub resource_id: ResourceId,
7751    pub kind: PinKind,
7752    pub index: u32,
7753    pub label: String,
7754    pub format: TextureFormat,
7755}
7756
7757impl NodePin {
7758    pub fn input(pass_id: PassId, resource_id: ResourceId, index: u32, label: &str, format: TextureFormat) -> Self {
7759        NodePin { pass_id, resource_id, kind: PinKind::Input, index, label: label.to_owned(), format }
7760    }
7761    pub fn output(pass_id: PassId, resource_id: ResourceId, index: u32, label: &str, format: TextureFormat) -> Self {
7762        NodePin { pass_id, resource_id, kind: PinKind::Output, index, label: label.to_owned(), format }
7763    }
7764    pub fn is_compatible_with(&self, other: &NodePin) -> bool {
7765        self.kind != other.kind && formats_compatible(self.format, other.format)
7766    }
7767}
7768
7769#[derive(Debug, Clone)]
7770pub struct NodeConnection {
7771    pub src: NodePin,
7772    pub dst: NodePin,
7773}
7774
7775impl NodeConnection {
7776    pub fn new(src: NodePin, dst: NodePin) -> Option<Self> {
7777        if src.is_compatible_with(&dst) { Some(NodeConnection { src, dst }) } else { None }
7778    }
7779}
7780
7781// ============================================================
7782//  FINAL UTILITIES
7783// ============================================================
7784
7785/// RGBE encoding (Radiance HDR format) for efficient HDR storage
7786pub fn encode_rgbe(hdr: Vec3) -> [u8; 4] {
7787    let max_c = hdr.x.max(hdr.y).max(hdr.z);
7788    if max_c < 1e-32 {
7789        return [0, 0, 0, 0];
7790    }
7791    let exp = max_c.log2().ceil() as i32 + 128;
7792    let scale = 256.0 / (2.0f32.powi(exp - 128));
7793    [
7794        (hdr.x * scale) as u8,
7795        (hdr.y * scale) as u8,
7796        (hdr.z * scale) as u8,
7797        exp.clamp(0, 255) as u8,
7798    ]
7799}
7800
7801pub fn decode_rgbe(rgbe: [u8; 4]) -> Vec3 {
7802    if rgbe[3] == 0 { return Vec3::ZERO; }
7803    let exp = rgbe[3] as i32 - 128;
7804    let scale = 2.0f32.powi(exp) / 256.0;
7805    Vec3::new(rgbe[0] as f32 * scale, rgbe[1] as f32 * scale, rgbe[2] as f32 * scale)
7806}
7807
7808/// Shared exponent (GL_RGB9_E5) encoding
7809pub fn encode_rgb9e5(hdr: Vec3) -> u32 {
7810    const N: i32 = 9;
7811    const B: i32 = 15;
7812    const E_MAX: i32 = 31;
7813    let max_c = hdr.x.max(hdr.y).max(hdr.z).max(0.0);
7814    let shared_exp_f = (max_c / (1 << (N-1)) as f32 * (1 << B) as f32).log2().ceil() as i32;
7815    let exp = shared_exp_f.clamp(-B, E_MAX - N);
7816    let scale = 1.0 / 2.0f32.powi(exp - N + 1);
7817    let r = (hdr.x * scale).round() as u32 & ((1 << N) - 1);
7818    let g = (hdr.y * scale).round() as u32 & ((1 << N) - 1);
7819    let b = (hdr.z * scale).round() as u32 & ((1 << N) - 1);
7820    let e = (exp + B + 1).clamp(0, 31) as u32;
7821    (e << 27) | (b << 18) | (g << 9) | r
7822}
7823
7824/// Compute the screen-space size of a sphere in pixels
7825pub fn sphere_screen_size_pixels(center_vs: Vec3, radius: f32, proj: Mat4, screen_width: u32) -> f32 {
7826    let d = center_vs.length();
7827    if d < radius { return screen_width as f32; }
7828    let proj_scale = proj.col(0).x; // proj[0][0] = 1/tan(fov_x/2)
7829    (proj_scale * radius / (d - radius)) * screen_width as f32 * 0.5
7830}
7831
7832/// Convert a linear depth value to a NDC z for a given near/far
7833pub fn linear_depth_to_ndc(linear: f32, near: f32, far: f32) -> f32 {
7834    let a = -(far + near) / (far - near);
7835    let b = -2.0 * far * near / (far - near);
7836    a + b / linear
7837}
7838
7839/// Compute mip level for texture sampling based on UV footprint (OpenGL-style)
7840pub fn compute_texture_lod(ddx: Vec2, ddy: Vec2) -> f32 {
7841    let len_x = ddx.length_squared();
7842    let len_y = ddy.length_squared();
7843    0.5 * len_x.max(len_y).log2()
7844}
7845
7846/// Linearize a gamma-encoded value using the sRGB piecewise transfer function
7847pub fn srgb_eotf(encoded: f32) -> f32 {
7848    if encoded <= 0.04045 { encoded / 12.92 } else { ((encoded + 0.055) / 1.055).powf(2.4) }
7849}
7850
7851/// sRGB inverse EOTF (linear to display-encoded)
7852pub fn srgb_oetf(linear: f32) -> f32 {
7853    if linear <= 0.0031308 { linear * 12.92 } else { 1.055 * linear.powf(1.0 / 2.4) - 0.055 }
7854}
7855
7856// Convert rec709 to XYZ color space
7857pub fn rec709_to_xyz(c: Vec3) -> Vec3 {
7858    Vec3::new(
7859        0.4124564 * c.x + 0.3575761 * c.y + 0.1804375 * c.z,
7860        0.2126729 * c.x + 0.7151522 * c.y + 0.0721750 * c.z,
7861        0.0193339 * c.x + 0.1191920 * c.y + 0.9503041 * c.z,
7862    )
7863}
7864
7865pub fn xyz_to_rec709(c: Vec3) -> Vec3 {
7866    Vec3::new(
7867         3.2404542 * c.x - 1.5371385 * c.y - 0.4985314 * c.z,
7868        -0.9692660 * c.x + 1.8760108 * c.y + 0.0415560 * c.z,
7869         0.0556434 * c.x - 0.2040259 * c.y + 1.0572252 * c.z,
7870    )
7871}
7872
7873pub fn xyz_to_aces_ap0(c: Vec3) -> Vec3 {
7874    Vec3::new(
7875        1.0498110175 * c.x +  0.0000000000 * c.y - 0.0000974845 * c.z,
7876       -0.4959030231 * c.x +  1.3733130458 * c.y +  0.0982400361 * c.z,
7877        0.0000000000 * c.x +  0.0000000000 * c.y +  0.9912520182 * c.z,
7878    )
7879}
7880
7881// Vec3 powf helper
7882trait Vec3Ext { fn powf(self, exp: f32) -> Vec3; fn sqrt(self) -> Vec3; }
7883impl Vec3Ext for Vec3 {
7884    fn powf(self, exp: f32) -> Vec3 { Vec3::new(self.x.powf(exp), self.y.powf(exp), self.z.powf(exp)) }
7885    fn sqrt(self) -> Vec3 { Vec3::new(self.x.sqrt(), self.y.sqrt(), self.z.sqrt()) }
7886}
7887
7888// Vec4 div helper (free function to avoid orphan rule)
7889#[allow(dead_code)]
7890fn vec4_div(v: Vec4, rhs: f32) -> Vec4 { Vec4::new(v.x / rhs, v.y / rhs, v.z / rhs, v.w / rhs) }
7891
7892// ============================================================
7893//  FINAL UTILITY TESTS
7894// ============================================================
7895
7896#[cfg(test)]
7897mod util_tests {
7898    use super::*;
7899
7900    #[test]
7901    fn test_rgbe_round_trip() {
7902        let hdr = Vec3::new(1.5, 2.3, 0.7);
7903        let enc = encode_rgbe(hdr);
7904        let dec = decode_rgbe(enc);
7905        let err = (hdr - dec).length();
7906        assert!(err < 0.05, "RGBE round-trip error too large: {}", err);
7907    }
7908
7909    #[test]
7910    fn test_rgbe_black() {
7911        let enc = encode_rgbe(Vec3::ZERO);
7912        assert_eq!(enc, [0, 0, 0, 0]);
7913        let dec = decode_rgbe(enc);
7914        assert_eq!(dec, Vec3::ZERO);
7915    }
7916
7917    #[test]
7918    fn test_srgb_eotf_inverse() {
7919        let x = 0.5f32;
7920        let linear = srgb_eotf(x);
7921        let back = srgb_oetf(linear);
7922        assert!((back - x).abs() < 1e-4);
7923    }
7924
7925    #[test]
7926    fn test_linear_depth_to_ndc() {
7927        let ndc = linear_depth_to_ndc(1.0, 0.1, 100.0);
7928        assert!(ndc >= -1.0 && ndc <= 1.0);
7929    }
7930
7931    #[test]
7932    fn test_sphere_screen_size() {
7933        let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 100.0);
7934        let size = sphere_screen_size_pixels(Vec3::new(0.0, 0.0, -10.0), 1.0, proj, 1920);
7935        assert!(size > 0.0);
7936    }
7937
7938    #[test]
7939    fn test_auto_exposure_adapt() {
7940        let ae = AutoExposurePassDesc::default();
7941        let ev = ae.adapt_exposure(0.0, 5.0, 0.016);
7942        assert!(ev > 0.0 && ev < 5.0);
7943    }
7944
7945    #[test]
7946    fn test_lens_flare_ghost_positions() {
7947        let pos = LensFlarePassDesc::ghost_position(Vec2::new(0.8, 0.5), 1, 0.35);
7948        assert!(pos.x >= 0.0 && pos.x <= 1.0 || pos.x < 0.0 || pos.x > 1.0);
7949    }
7950
7951    #[test]
7952    fn test_hi_z_pyramid_build() {
7953        let depth: Vec<f32> = vec![0.5; 64 * 64];
7954        let pyramid = OcclusionCuller::build_hi_z_pyramid(&depth, 64, 64);
7955        assert!(pyramid.len() > 1);
7956        assert_eq!(pyramid[0].len(), 64 * 64);
7957        assert_eq!(pyramid[1].len(), 32 * 32);
7958    }
7959
7960    #[test]
7961    fn test_node_pin_compatibility() {
7962        let p1 = NodePin::output(PassId(0), ResourceId(0), 0, "HDR", TextureFormat::RGBA16Float);
7963        let p2 = NodePin::input(PassId(1), ResourceId(0), 0, "HDR", TextureFormat::RGBA16Float);
7964        assert!(p1.is_compatible_with(&p2));
7965        let p3 = NodePin::input(PassId(1), ResourceId(1), 0, "Depth", TextureFormat::Depth32Float);
7966        assert!(!p1.is_compatible_with(&p3));
7967    }
7968
7969    #[test]
7970    fn test_ev100_from_luminance() {
7971        let ev = AutoExposurePassDesc::ev100_from_average_luminance(1.0);
7972        assert!((ev - 3.0).abs() < 0.1, "ev should be ~3 for 1 cd/m^2 avg luminance");
7973    }
7974
7975    #[test]
7976    fn test_rec709_xyz_round_trip() {
7977        let c = Vec3::new(0.2, 0.5, 0.8);
7978        let xyz = rec709_to_xyz(c);
7979        let back = xyz_to_rec709(xyz);
7980        assert!((c - back).length() < 1e-4);
7981    }
7982
7983    #[test]
7984    fn test_compute_texture_lod() {
7985        let lod = compute_texture_lod(Vec2::new(0.01, 0.0), Vec2::new(0.0, 0.01));
7986        assert!(lod < 0.0 || lod >= 0.0); // just check no panic
7987    }
7988}
7989
7990// ============================================================
7991//  MODULE SUMMARY
7992//  render_graph_editor.rs — Proof Engine Render Graph Editor
7993//
7994//  Implemented components:
7995//   - 14 render pass descriptors (GBuffer, Shadow, Lighting, SSAO, SSR, Bloom,
7996//     ToneMapping, TAA, DoF, MotionBlur, VolumetricFog, Particles, UI, Debug)
7997//   - 50+ texture format enum with per-format metadata (bytes, compression, channels)
7998//   - Vulkan-style image layouts, access masks, pipeline stage flags (bitflags)
7999//   - Full barrier insertion algorithm (layout tracking across pass list)
8000//   - Pipeline state objects: rasterizer, depth-stencil, blend, multisample, vertex input
8001//   - RenderGraphResource with aliasing analysis (greedy interval-graph coloring)
8002//   - RenderGraphCompiler: cycle removal → Kahn sort → dead pass elimination →
8003//     resource lifetimes → aliasing → barrier insertion → bandwidth estimation
8004//   - RenderGraphValidator: format checks, cascade limits, blend state, duplicates
8005//   - Sugiyama graph layout: longest-path layering, barycenter crossing minimization,
8006//     position assignment, cubic bezier edge routing
8007//   - Pass statistics, frame statistics, bandwidth profiler, profiling query pool
8008//   - Subpass dependencies, TBR detection, GBuffer+Lighting merged renderpass
8009//   - Serialization to JSON and DOT/Mermaid graph export formats
8010//   - RenderGraphEditor: full editor struct with compile/validate/visualize/serialize
8011//   - Standard deferred, Forward+, and mobile deferred pipeline builders
8012//   - Advanced passes: VXGI, DDGI, RT passes, SSS, GTAO, decals, sky, GPU culling
8013//   - ACES color transform + 3D LUT baking and sampling
8014//   - Adaptive resolution scaling, upscaling (FSR1/Lanczos), CAS sharpening
8015//   - Post-FX chain: vignette, film grain, chromatic aberration, lens flare
8016//   - Editor UI: draw commands, node rendering, stat overlay, camera controls
8017//   - Undo/redo history, pass groups, annotations, hot-reload manager
8018//   - Frame debugger, critical path analyzer, dependency matrix
8019//   - Hi-Z pyramid builder + occlusion culler
8020//   - Auto-exposure with histogram and eye adaptation
8021//   - ~200 unit/integration tests covering all major subsystems
8022// ============================================================