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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum TextureFormat {
12 R8Unorm,
14 RG8Unorm,
15 RGBA8Unorm,
16 RGBA8UnormSrgb,
17 BGRA8Unorm,
18 BGRA8UnormSrgb,
19 R8Snorm,
21 RG8Snorm,
22 RGBA8Snorm,
23 R8Uint,
25 RG8Uint,
26 RGBA8Uint,
27 R8Sint,
28 RG8Sint,
29 RGBA8Sint,
30 R16Unorm,
32 RG16Unorm,
33 RGBA16Unorm,
34 R16Float,
36 RG16Float,
37 RGBA16Float,
38 R16Uint,
40 RG16Uint,
41 RGBA16Uint,
42 R16Sint,
43 R32Float,
45 RG32Float,
46 RGB32Float,
47 RGBA32Float,
48 R32Uint,
50 RG32Uint,
51 RGBA32Uint,
52 R32Sint,
53 RGB10A2Unorm,
55 RG11B10Float,
56 RGB9E5Float,
57 Depth16Unorm,
59 Depth24Unorm,
60 Depth32Float,
61 Depth24UnormStencil8,
62 Depth32FloatStencil8,
63 Stencil8,
64 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 Etc2Rgb8Unorm,
83 Etc2Rgb8Srgb,
84 Etc2Rgb8A1Unorm,
85 Etc2Rgba8Unorm,
86 EacR11Unorm,
87 EacRG11Unorm,
88 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 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#[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
227macro_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#[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#[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, Persistent, Imported, }
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 pub first_use: usize,
477 pub last_use: usize,
478 pub can_alias: bool,
480 pub alias_target: Option<ResourceId>,
482 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 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 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 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#[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
598pub 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
638pub 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#[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: FrontFace::CounterClockwise,
759 depth_clamp_enable: true, 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, }
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 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 }, VertexAttribute { location: 1, binding: 0, format: VertexFormat::Float3, offset: 12 }, VertexAttribute { location: 2, binding: 0, format: VertexFormat::Float4, offset: 24 }, VertexAttribute { location: 3, binding: 0, format: VertexFormat::Float2, offset: 40 }, ];
1076 VertexInputLayout { bindings, attributes }
1077 }
1078
1079 pub fn skinned_mesh() -> Self {
1080 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 }, VertexAttribute { location: 5, binding: 1, format: VertexFormat::Float4, offset: 16 }, ];
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#[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#[derive(Debug, Clone)]
1127pub struct GBufferPassDesc {
1128 pub width: u32,
1129 pub height: u32,
1130 pub albedo_format: TextureFormat, pub normal_format: TextureFormat, pub material_format: TextureFormat, pub velocity_format: TextureFormat, 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, 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 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 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#[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, 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], }
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 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 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 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 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 let mut centroid = Vec3::ZERO;
1266 for c in &world_corners { centroid += *c; }
1267 centroid /= 8.0;
1268 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 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 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#[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 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 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 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 (self.total_tiles() as u64) * (self.max_lights_per_tile as u64 + 1) * 2
1341 }
1342}
1343
1344#[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 pub fn generate_kernel(&self) -> Vec<Vec3> {
1383 let mut kernel = Vec::with_capacity(self.kernel_size as usize);
1384 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); let mut sample = Vec3::new(x, y, z).normalize();
1395 sample *= lcg_next(&mut lcg);
1396 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 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)); }
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#[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 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#[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 pub fn quadratic_threshold(&self, lum: f32) -> f32 {
1517 let t = self.threshold;
1518 let k = self.knee;
1519 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 threshold_result.max(linear_result)
1525 }
1526 pub fn kawase_weights(iter: u32) -> [f32; 4] {
1528 let offset = iter as f32 + 0.5;
1529 [offset, offset, offset, offset]
1530 }
1531 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#[derive(Debug, Clone, Copy, PartialEq)]
1550pub enum ToneMappingOperator {
1551 Linear,
1552 Reinhard,
1553 ReinhardExtended,
1554 Filmic, ACES, 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 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), 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; let a = 1.0f32; let m = 0.22f32; let l = 0.4f32; let c = 1.33f32; let b = 0.0f32; let map_channel = |x: f32| -> f32 {
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
1697struct 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#[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 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 pub fn catmull_rom_weights(frac: Vec2) -> [f32; 5] {
1758 let f = frac;
1759 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] }
1766
1767 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 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#[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 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 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 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#[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, 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 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; positions.push(velocity * t);
1924 }
1925 positions
1926 }
1927 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#[derive(Debug, Clone)]
1936pub struct VolumetricFogPassDesc {
1937 pub width: u32,
1938 pub height: u32,
1939 pub depth_slices: u32, 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, 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 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 pub fn extinction(&self, distance: f32) -> f32 {
1984 let sigma_t = self.scattering + self.absorption;
1985 (-sigma_t * self.density * distance).exp()
1986 }
1987 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 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#[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 self.max_particles as u64 * 52
2044 }
2045 pub fn sort_key_buffer_size_bytes(&self) -> u64 {
2046 self.max_particles as u64 * 8
2048 }
2049 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#[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#[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 self.max_lines as u64 * 56
2135 }
2136}
2137
2138#[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 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#[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 pub fn compile(&mut self, output_resources: &[ResourceId]) -> Result<CompiledRenderGraph, String> {
2272 let edges = self.build_dependency_edges();
2274 let (acyclic_edges, removed_edges) = self.remove_cycles(&edges);
2276 if !removed_edges.is_empty() {
2277 }
2279 let sorted = self.kahn_topological_sort(&acyclic_edges)?;
2281 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 let mut pass_index: HashMap<PassId, usize> = HashMap::new();
2287 for (i, pid) in sorted_live.iter().enumerate() { pass_index.insert(*pid, i); }
2288 let resource_lifetimes = self.compute_resource_lifetimes(&sorted_live, &pass_index);
2290 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 let aliasing_groups = self.compute_aliasing_groups(&rm, &resource_lifetimes);
2300 let barriers = self.insert_barriers(&sorted_live, &rm);
2302 let estimated_memory_bytes = self.estimate_memory_usage(&rm, &aliasing_groups);
2304 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 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 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 for v in edges.values_mut() { v.sort_unstable_by_key(|p| p.0); v.dedup(); }
2342 edges
2343 }
2344
2345 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 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 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 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 fn mark_live_passes(&self, sorted: &[PassId], outputs: &[ResourceId], edges: &HashMap<PassId, Vec<PassId>>) -> HashSet<PassId> {
2412 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 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 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(); 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 let mut placed = false;
2469 for (group_ids, group_end) in &mut groups {
2470 if *group_end < start {
2471 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 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 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 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 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 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 let gbuf_bytes: f32 = (4.0 + 8.0 + 4.0 + 4.0 + 4.0) * pixels; 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 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); }
2587 _ => {
2588 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#[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 for (pid, pass) in self.pass_map {
2723 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#[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), }
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 pub fn layout(&self, passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> GraphLayout {
2791 let layers = self.assign_layers(passes, edges);
2793 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 for v in nodes_per_layer.values_mut() {
2800 v.sort_by_key(|p| p.0);
2801 }
2802 self.minimize_crossings(edges, &layers, &mut nodes_per_layer);
2804 let positions = self.assign_positions(&layers, &nodes_per_layer);
2806 let paths = self.route_edges(passes, edges, &positions);
2808 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 fn assign_layers(&self, passes: &[PassId], edges: &HashMap<PassId, Vec<PassId>>) -> HashMap<PassId, i32> {
2829 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 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 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 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 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 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 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; 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 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 let p0 = src_pos + Vec2::new(self.node_width, half_h);
2931 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 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#[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, pub bandwidth_read_mb: f32,
2976 pub bandwidth_write_mb: f32,
2977 pub texture_cache_miss_rate: f32, 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
3035pub 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 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 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 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#[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 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 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 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, }
3134 }
3135 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>, 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 pub fn build_gbuffer_lighting_renderpass(
3162 gbuf: &GBufferPassDesc,
3163 light: &LightingPassDesc,
3164 ) -> Self {
3165 let mut attachments = gbuf.attachment_descriptions();
3166 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], 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], color_attachments: vec![lighting_att_idx],
3191 resolve_attachments: vec![],
3192 depth_stencil_attachment: None,
3193 preserve_attachments: vec![3], },
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 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#[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]>, }
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
3331pub 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 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 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 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 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 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 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 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 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 pub fn build_standard_deferred_pipeline(width: u32, height: u32) -> RenderGraphEditor {
3518 let mut editor = RenderGraphEditor::new("Standard Deferred");
3519
3520 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
3771pub 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
3814pub 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
3832pub fn luminance(c: Vec3) -> f32 { 0.2126 * c.x + 0.7152 * c.y + 0.0722 * c.z }
3834
3835pub fn ev100_to_exposure(ev100: f32) -> f32 { 1.0 / (1.2 * (2.0f32).powf(ev100)) }
3837
3838pub 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
3845pub fn linearize_depth(depth: f32, near: f32, far: f32) -> f32 {
3847 (2.0 * near * far) / (far + near - depth * (far - near))
3848}
3849
3850pub 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
3859pub 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
3872pub 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
3884pub 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
3892pub 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
3909pub 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
3953pub 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 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#[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 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 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 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; offset_buf + count_buf + index_buf
4056 }
4057}
4058
4059#[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
4086pub 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 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), normalize_plane(row3 - row0), normalize_plane(row3 + row1), normalize_plane(row3 - row1), normalize_plane(row3 + row2), normalize_plane(row3 - row2), ]
4136}
4137
4138pub 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
4147pub 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
4160pub struct TBRDetector;
4165impl TBRDetector {
4166 pub fn is_tbr(vendor_id: u32, device_id: u32, is_mobile: bool) -> bool {
4169 if is_mobile { return true; }
4170 match vendor_id {
4172 0x13B5 => true, 0x5143 => true, 0x1010 => true, _ => false,
4176 }
4177 }
4178
4179 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 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 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 bpp * pixels / (1024.0 * 1024.0)
4211 }
4212}
4213
4214#[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 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 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#[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 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 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 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 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 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 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 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 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 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 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), (ImageLayout::ColorAttachmentOptimal, ImageLayout::ShaderReadOnlyOptimal) => Vec4::new(0.2, 1.0, 0.2, 1.0), (_, ImageLayout::ShaderReadOnlyOptimal) => Vec4::new(0.4, 0.8, 0.4, 1.0),
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
4419pub 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 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 let sm = editor.add_pass("ShadowMap", PassDesc::ShadowMap(ShadowMapPassDesc::directional_shadow(2048)));
4440 editor.set_pass_writes(sm, vec![res_shadow]);
4441
4442 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 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 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 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 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 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
4494pub 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
4507pub 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
4513pub fn is_depth_stencil_attachment_format(fmt: TextureFormat) -> bool {
4515 let fi = format_info(fmt);
4516 fi.is_depth || fi.is_stencil
4517}
4518
4519pub 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#[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); let p1 = fog.henyey_greenstein(-1.0); assert!(p0 > p1); }
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#[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)>, 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 pub fn allocate(&mut self, light_id: u32, width: u32, height: u32) -> Option<AtlasRegion> {
4741 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 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 }
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#[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, }
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>, }
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#[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
4926pub 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
4939pub 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 vec![set0]
4948}
4949
4950#[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, pub time: Vec4, pub near_far: Vec4, pub exposure: Vec4, pub jitter: Vec4, pub fog_params: Vec4, 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
5061pub 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)>, }
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#[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, 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
5173pub struct DependencyMatrix {
5178 pub pass_ids: Vec<PassId>,
5179 pub matrix: Vec<Vec<bool>>, }
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 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; }
5194 }
5195 }
5196 }
5197 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#[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 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 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 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 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#[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
5391pub struct PipelinePreset;
5396impl PipelinePreset {
5397 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 for pass in editor.passes.values_mut() { pass.enabled = true; }
5403 editor
5404 }
5405
5406 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 for pass in editor.passes.values_mut() {
5412 if matches!(pass.desc.kind(), PassKind::SSR) { pass.enabled = false; }
5413 }
5414 editor
5415 }
5416
5417 pub fn mobile(width: u32, height: u32) -> RenderGraphEditor {
5419 let editor = build_mobile_deferred_pipeline(width, height);
5420 editor
5421 }
5422
5423 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#[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#[derive(Debug, Clone)]
5471pub enum EditorAction {
5472 AddPass(PassId, String),
5473 RemovePass(PassId, String),
5474 MovePass(PassId, Vec2, Vec2), ConnectResources(PassId, PassId, ResourceId),
5476 DisconnectResources(PassId, PassId, ResourceId),
5477 TogglePassEnabled(PassId, bool), RenamePass(PassId, String, String), 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#[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), }
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#[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
5662pub struct CriticalPathAnalyzer;
5667impl CriticalPathAnalyzer {
5668 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 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 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 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#[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#[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 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#[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, 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 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5886pub struct PipelineKey {
5887 pub pass_kind: PassKind,
5888 pub fill_mode: u8, pub cull_mode: u8, 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 (fmt as u64).wrapping_mul(0x9e3779b97f4a7c15)
5916}
5917
5918pub struct PipelineCache {
5919 pub entries: HashMap<PipelineKey, u64>, 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 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
5955pub 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
6002pub fn export_mermaid(editor: &RenderGraphEditor) -> String {
6007 let mut s = String::new();
6008 s.push_str("graph LR\n");
6009 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
6036pub 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#[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 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 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 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#[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 }, }
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 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 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 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 pub fn cas_sharpen(center: Vec3, neighbors: [Vec3; 4], sharpness: f32) -> Vec3 {
6183 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
6194pub 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#[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 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); }
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 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); }
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 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); 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#[derive(Debug, Clone)]
6505pub struct VXGIPassDesc {
6506 pub voxel_grid_size: u32, pub voxel_world_size: f32, 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 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 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; 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 let sample_pos = start + direction * dist;
6573 let alpha = 0.1 * (1.0 - accum_alpha); let color = Vec3::new(0.1, 0.08, 0.06) * alpha; accum_color += color * (1.0 - accum_alpha);
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 pub fn diffuse_cone_directions(num_cones: u32) -> Vec<Vec3> {
6585 let mut dirs = Vec::with_capacity(num_cones as usize);
6586 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
6604pub struct HiZTracer;
6609impl HiZTracer {
6610 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 ) -> 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; 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 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 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 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#[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 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 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 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 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 pub fn preintegrated_lut_value(n_dot_l: f32, curvature: f32) -> Vec3 {
6728 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#[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 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 pub fn bent_normal_visibility(bent_normal: Vec3, mean_visibility: f32, roughness: f32) -> f32 {
6804 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#[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, 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 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 pub fn atlas_memory_bytes(&self) -> u64 {
6904 let (iw, ih) = self.irradiance_atlas_size();
6905 let (vw, vh) = self.visibility_atlas_size();
6906 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#[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
6986pub struct ACESTransform;
6991impl ACESTransform {
6992 pub fn linear_srgb_to_aces2065(c: Vec3) -> Vec3 {
6994 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 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 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 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 let input = Vec3::new(r, g, b) * 4.0; let output = Self::rrt_odt_srgb(input);
7034 lut.push(output);
7035 }
7036 }
7037 }
7038 lut
7039 }
7040
7041 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#[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 self.max_draw_calls as u64 * 20
7105 }
7106 pub fn bounding_sphere_buffer_bytes(&self) -> u64 {
7107 self.max_draw_calls as u64 * 16
7109 }
7110}
7111
7112pub struct PassReorderer;
7117impl PassReorderer {
7118 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 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
7146pub 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#[derive(Debug, Clone)]
7174pub struct AdaptiveResolutionScaler {
7175 pub target_frame_time_ms: f32,
7176 pub min_scale: f32, pub max_scale: f32, pub current_scale: f32,
7179 pub increase_threshold: f32, pub decrease_threshold: f32, 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 (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#[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 pub fn easu_filter_sample(input_uv: Vec2, texel_size: Vec2, jitter: Vec2) -> [Vec2; 5] {
7270 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 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#[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); }
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); } 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); } 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 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 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); }
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
7518pub 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 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>], ) -> bool {
7543 let span = aabb_max_uv - aabb_min_uv;
7544 let max_span = span.x.max(span.y);
7545 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 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 nearest_depth > max_hi_z_depth
7567 }
7568
7569 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 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 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#[derive(Debug, Clone)]
7607pub struct AutoExposurePassDesc {
7608 pub output_average_luminance: ResourceId,
7609 pub input_hdr: ResourceId,
7610 pub min_log_luminance: f32, pub max_log_luminance: f32, pub adaptation_speed_up: f32,
7613 pub adaptation_speed_down: f32,
7614 pub histogram_bins: u32,
7615 pub metered_area: Vec4, 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 pub fn luminance_to_bin(&self, lum: f32) -> u32 {
7639 let log_lum = lum.max(1e-5).ln() / std::f32::consts::LN_2; let normalized = (log_lum - self.min_log_luminance) / (self.max_log_luminance - self.min_log_luminance);
7641 (normalized * self.histogram_bins as f32) as u32
7642 }
7643
7644 pub fn ev100_from_average_luminance(avg_lum: f32) -> f32 {
7646 (avg_lum * 100.0 / 12.5).log2()
7647 }
7648
7649 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 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#[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 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 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 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#[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
7781pub 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
7808pub 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
7824pub 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_scale * radius / (d - radius)) * screen_width as f32 * 0.5
7830}
7831
7832pub 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
7839pub 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
7846pub 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
7851pub 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
7856pub 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
7881trait 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#[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#[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); }
7988}
7989
7990