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 {
5283 diffs.push(GraphDiff::PassModified(*pid, new_pass.name.clone()));
5284 }
5285 }
5286 }
5287 for rid in new.resources.keys() {
5289 if !old.resources.contains_key(rid) {
5290 diffs.push(GraphDiff::ResourceAdded(*rid, new.resources[rid].name.clone()));
5291 }
5292 }
5293 for rid in old.resources.keys() {
5294 if !new.resources.contains_key(rid) {
5295 diffs.push(GraphDiff::ResourceRemoved(*rid, old.resources[rid].name.clone()));
5296 }
5297 }
5298 let old_connections = collect_connections(old);
5300 let new_connections = collect_connections(new);
5301 for conn in &new_connections {
5302 if !old_connections.contains(conn) { diffs.push(GraphDiff::ConnectionAdded(conn.0, conn.1)); }
5303 }
5304 for conn in &old_connections {
5305 if !new_connections.contains(conn) { diffs.push(GraphDiff::ConnectionRemoved(conn.0, conn.1)); }
5306 }
5307 diffs
5308}
5309
5310fn collect_connections(editor: &RenderGraphEditor) -> HashSet<(PassId, PassId)> {
5311 let mut conns = HashSet::new();
5312 for (src, src_pass) in &editor.passes {
5313 for rid in &src_pass.writes {
5314 for (dst, dst_pass) in &editor.passes {
5315 if dst_pass.reads.contains(rid) { conns.insert((*src, *dst)); }
5316 }
5317 }
5318 }
5319 conns
5320}
5321
5322#[derive(Debug, Clone)]
5327pub struct ResourceAccessRecord {
5328 pub pass_id: PassId,
5329 pub resource_id: ResourceId,
5330 pub is_write: bool,
5331 pub bytes_accessed: u64,
5332 pub access_mask: AccessFlags,
5333 pub layout: ImageLayout,
5334}
5335
5336pub struct BandwidthProfiler {
5337 pub records: Vec<ResourceAccessRecord>,
5338 pub per_resource_read_mb: HashMap<ResourceId, f32>,
5339 pub per_resource_write_mb: HashMap<ResourceId, f32>,
5340 pub per_pass_read_mb: HashMap<PassId, f32>,
5341 pub per_pass_write_mb: HashMap<PassId, f32>,
5342}
5343
5344impl BandwidthProfiler {
5345 pub fn new() -> Self {
5346 BandwidthProfiler {
5347 records: Vec::new(),
5348 per_resource_read_mb: HashMap::new(),
5349 per_resource_write_mb: HashMap::new(),
5350 per_pass_read_mb: HashMap::new(),
5351 per_pass_write_mb: HashMap::new(),
5352 }
5353 }
5354
5355 pub fn record(&mut self, pass: PassId, resource: ResourceId, is_write: bool, bytes: u64, access: AccessFlags, layout: ImageLayout) {
5356 self.records.push(ResourceAccessRecord { pass_id: pass, resource_id: resource, is_write, bytes_accessed: bytes, access_mask: access, layout });
5357 }
5358
5359 pub fn compute_totals(&mut self) {
5360 self.per_resource_read_mb.clear();
5361 self.per_resource_write_mb.clear();
5362 self.per_pass_read_mb.clear();
5363 self.per_pass_write_mb.clear();
5364 for rec in &self.records {
5365 let mb = rec.bytes_accessed as f32 / (1024.0 * 1024.0);
5366 if rec.is_write {
5367 *self.per_resource_write_mb.entry(rec.resource_id).or_insert(0.0) += mb;
5368 *self.per_pass_write_mb.entry(rec.pass_id).or_insert(0.0) += mb;
5369 } else {
5370 *self.per_resource_read_mb.entry(rec.resource_id).or_insert(0.0) += mb;
5371 *self.per_pass_read_mb.entry(rec.pass_id).or_insert(0.0) += mb;
5372 }
5373 }
5374 }
5375
5376 pub fn total_bandwidth_mb(&self) -> f32 {
5377 let reads: f32 = self.per_resource_read_mb.values().sum();
5378 let writes: f32 = self.per_resource_write_mb.values().sum();
5379 reads + writes
5380 }
5381
5382 pub fn top_bandwidth_resources(&self, n: usize) -> Vec<(ResourceId, f32)> {
5383 let mut combined: HashMap<ResourceId, f32> = HashMap::new();
5384 for (rid, &r) in &self.per_resource_read_mb { *combined.entry(*rid).or_insert(0.0) += r; }
5385 for (rid, &w) in &self.per_resource_write_mb { *combined.entry(*rid).or_insert(0.0) += w; }
5386 let mut v: Vec<(ResourceId, f32)> = combined.into_iter().collect();
5387 v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
5388 v.truncate(n);
5389 v
5390 }
5391}
5392
5393pub struct PipelinePreset;
5398impl PipelinePreset {
5399 pub fn high_quality_pc(width: u32, height: u32) -> RenderGraphEditor {
5401 let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(width, height);
5402 editor.name = "High Quality PC".to_owned();
5403 for pass in editor.passes.values_mut() { pass.enabled = true; }
5405 editor
5406 }
5407
5408 pub fn medium_quality(width: u32, height: u32) -> RenderGraphEditor {
5410 let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(width, height);
5411 editor.name = "Medium Quality".to_owned();
5412 for pass in editor.passes.values_mut() {
5414 if matches!(pass.desc.kind(), PassKind::SSR) { pass.enabled = false; }
5415 }
5416 editor
5417 }
5418
5419 pub fn mobile(width: u32, height: u32) -> RenderGraphEditor {
5421 let editor = build_mobile_deferred_pipeline(width, height);
5422 editor
5423 }
5424
5425 pub fn shadow_only(resolution: u32) -> RenderGraphEditor {
5427 let mut editor = RenderGraphEditor::new("ShadowOnly");
5428 let res_shadow = editor.add_transient_texture("ShadowMap", TextureDesc::shadow_map(resolution));
5429 editor.set_output_resources(vec![res_shadow]);
5430 let sm_pass = editor.add_pass("ShadowMap", PassDesc::ShadowMap(ShadowMapPassDesc::directional_shadow(resolution)));
5431 editor.set_pass_writes(sm_pass, vec![res_shadow]);
5432 editor
5433 }
5434}
5435
5436#[derive(Debug, Clone)]
5441pub struct NodeAnnotation {
5442 pub pass_id: PassId,
5443 pub title: String,
5444 pub body: String,
5445 pub color: Vec4,
5446 pub pinned: bool,
5447 pub offset: Vec2,
5448}
5449
5450impl NodeAnnotation {
5451 pub fn new(pass_id: PassId, title: &str, body: &str) -> Self {
5452 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) }
5453 }
5454 pub fn world_pos(&self, pass_pos: Vec2) -> Vec2 { pass_pos + self.offset }
5455}
5456
5457pub struct AnnotationManager {
5458 pub annotations: HashMap<PassId, Vec<NodeAnnotation>>,
5459}
5460
5461impl AnnotationManager {
5462 pub fn new() -> Self { AnnotationManager { annotations: HashMap::new() } }
5463 pub fn add(&mut self, ann: NodeAnnotation) { self.annotations.entry(ann.pass_id).or_default().push(ann); }
5464 pub fn get(&self, pass_id: PassId) -> &[NodeAnnotation] { self.annotations.get(&pass_id).map(|v| v.as_slice()).unwrap_or(&[]) }
5465 pub fn remove_all(&mut self, pass_id: PassId) { self.annotations.remove(&pass_id); }
5466}
5467
5468#[derive(Debug, Clone)]
5473pub enum EditorAction {
5474 AddPass(PassId, String),
5475 RemovePass(PassId, String),
5476 MovePass(PassId, Vec2, Vec2), ConnectResources(PassId, PassId, ResourceId),
5478 DisconnectResources(PassId, PassId, ResourceId),
5479 TogglePassEnabled(PassId, bool), RenamePass(PassId, String, String), SetOutputResource(Vec<ResourceId>, Vec<ResourceId>),
5482}
5483
5484pub struct EditorHistory {
5485 pub undo_stack: VecDeque<EditorAction>,
5486 pub redo_stack: VecDeque<EditorAction>,
5487 pub max_history: usize,
5488}
5489
5490impl EditorHistory {
5491 pub fn new(max: usize) -> Self {
5492 EditorHistory { undo_stack: VecDeque::new(), redo_stack: VecDeque::new(), max_history: max }
5493 }
5494 pub fn push(&mut self, action: EditorAction) {
5495 if self.undo_stack.len() >= self.max_history {
5496 self.undo_stack.pop_front();
5497 }
5498 self.undo_stack.push_back(action);
5499 self.redo_stack.clear();
5500 }
5501 pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
5502 pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
5503 pub fn peek_undo(&self) -> Option<&EditorAction> { self.undo_stack.back() }
5504 pub fn pop_undo(&mut self) -> Option<EditorAction> { self.undo_stack.pop_back() }
5505 pub fn push_redo(&mut self, action: EditorAction) { self.redo_stack.push_back(action); }
5506 pub fn pop_redo(&mut self) -> Option<EditorAction> { self.redo_stack.pop_back() }
5507}
5508
5509#[derive(Debug, Clone)]
5514pub struct PassGroup {
5515 pub id: u32,
5516 pub name: String,
5517 pub passes: Vec<PassId>,
5518 pub color: Vec4,
5519 pub collapsed: bool,
5520 pub bounds: (Vec2, Vec2), }
5522
5523impl PassGroup {
5524 pub fn new(id: u32, name: &str, passes: Vec<PassId>, color: Vec4) -> Self {
5525 PassGroup { id, name: name.to_owned(), passes, color, collapsed: false, bounds: (Vec2::ZERO, Vec2::ZERO) }
5526 }
5527
5528 pub fn compute_bounds(&mut self, pass_positions: &HashMap<PassId, Vec2>, pass_sizes: &HashMap<PassId, Vec2>) {
5529 let mut min = Vec2::splat(f32::MAX);
5530 let mut max = Vec2::splat(f32::MIN);
5531 for pid in &self.passes {
5532 if let (Some(&pos), Some(&size)) = (pass_positions.get(pid), pass_sizes.get(pid)) {
5533 min = min.min(pos);
5534 max = max.max(pos + size);
5535 }
5536 }
5537 let padding = Vec2::splat(20.0);
5538 self.bounds = (min - padding, max + padding);
5539 }
5540
5541 pub fn contains_point(&self, pt: Vec2) -> bool {
5542 pt.x >= self.bounds.0.x && pt.x <= self.bounds.1.x &&
5543 pt.y >= self.bounds.0.y && pt.y <= self.bounds.1.y
5544 }
5545}
5546
5547pub struct PassGroupManager {
5548 pub groups: Vec<PassGroup>,
5549 next_id: u32,
5550}
5551
5552impl PassGroupManager {
5553 pub fn new() -> Self { PassGroupManager { groups: Vec::new(), next_id: 0 } }
5554 pub fn add_group(&mut self, name: &str, passes: Vec<PassId>, color: Vec4) -> u32 {
5555 let id = self.next_id;
5556 self.groups.push(PassGroup::new(id, name, passes, color));
5557 self.next_id += 1;
5558 id
5559 }
5560 pub fn group_for_pass(&self, pass_id: PassId) -> Option<&PassGroup> {
5561 self.groups.iter().find(|g| g.passes.contains(&pass_id))
5562 }
5563 pub fn remove_group(&mut self, id: u32) {
5564 self.groups.retain(|g| g.id != id);
5565 }
5566}
5567
5568#[derive(Debug, Clone)]
5573pub struct CapturedFrame {
5574 pub frame_index: u64,
5575 pub timestamp_ms: f64,
5576 pub pass_order: Vec<PassId>,
5577 pub pass_timings: HashMap<PassId, f64>,
5578 pub resource_transitions: Vec<(PassId, ResourceId, ImageLayout, ImageLayout)>,
5579 pub barrier_count: usize,
5580 pub draw_calls_per_pass: HashMap<PassId, u32>,
5581 pub triangles_per_pass: HashMap<PassId, u64>,
5582 pub notes: Vec<String>,
5583}
5584
5585impl CapturedFrame {
5586 pub fn new(frame_index: u64, timestamp_ms: f64) -> Self {
5587 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() }
5588 }
5589
5590 pub fn total_gpu_ms(&self) -> f64 { self.pass_timings.values().sum() }
5591 pub fn total_draw_calls(&self) -> u32 { self.draw_calls_per_pass.values().sum() }
5592 pub fn total_triangles(&self) -> u64 { self.triangles_per_pass.values().sum() }
5593
5594 pub fn longest_pass(&self) -> Option<PassId> {
5595 self.pass_timings.iter().max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal)).map(|(&p, _)| p)
5596 }
5597
5598 pub fn passes_over_budget(&self, budget_ms: f64) -> Vec<PassId> {
5599 self.pass_timings.iter().filter(|(_, &t)| t > budget_ms).map(|(&p, _)| p).collect()
5600 }
5601
5602 pub fn timeline_html(&self, pass_names: &HashMap<PassId, String>) -> String {
5603 let total = self.total_gpu_ms().max(1e-6);
5604 let mut s = String::new();
5605 s.push_str("<div style='font-family:monospace;background:#111;padding:8px'>");
5606 for pid in &self.pass_order {
5607 let name = pass_names.get(pid).map(|n| n.as_str()).unwrap_or("?");
5608 let ms = self.pass_timings.get(pid).cloned().unwrap_or(0.0);
5609 let pct = (ms / total * 100.0) as u32;
5610 let width = pct.clamp(1, 100);
5611 let color = if ms > total * 0.2 { "#ff4444" } else if ms > total * 0.1 { "#ffaa22" } else { "#44aa44" };
5612 s.push_str(&format!(
5613 "<div style='display:flex;align-items:center;margin:2px 0'>\
5614 <span style='color:#ccc;width:160px;display:inline-block'>{}</span>\
5615 <div style='width:{}%;background:{};height:14px;display:inline-block'></div>\
5616 <span style='color:#aaa;margin-left:4px'>{:.2}ms</span></div>",
5617 name, width, color, ms
5618 ));
5619 }
5620 s.push_str("</div>");
5621 s
5622 }
5623}
5624
5625pub struct FrameDebugger {
5626 pub captures: VecDeque<CapturedFrame>,
5627 pub max_captures: usize,
5628 pub is_capturing: bool,
5629 pub current_capture: Option<CapturedFrame>,
5630}
5631
5632impl FrameDebugger {
5633 pub fn new(max: usize) -> Self {
5634 FrameDebugger { captures: VecDeque::new(), max_captures: max, is_capturing: false, current_capture: None }
5635 }
5636 pub fn begin_capture(&mut self, frame_index: u64, timestamp_ms: f64) {
5637 self.is_capturing = true;
5638 self.current_capture = Some(CapturedFrame::new(frame_index, timestamp_ms));
5639 }
5640 pub fn record_pass_timing(&mut self, pass: PassId, ms: f64) {
5641 if let Some(ref mut cap) = self.current_capture {
5642 cap.pass_timings.insert(pass, ms);
5643 cap.pass_order.push(pass);
5644 }
5645 }
5646 pub fn record_transition(&mut self, pass: PassId, res: ResourceId, old: ImageLayout, new: ImageLayout) {
5647 if let Some(ref mut cap) = self.current_capture {
5648 cap.resource_transitions.push((pass, res, old, new));
5649 }
5650 }
5651 pub fn end_capture(&mut self) {
5652 if let Some(cap) = self.current_capture.take() {
5653 if self.captures.len() >= self.max_captures { self.captures.pop_front(); }
5654 self.captures.push_back(cap);
5655 }
5656 self.is_capturing = false;
5657 }
5658 pub fn latest(&self) -> Option<&CapturedFrame> { self.captures.back() }
5659 pub fn at_frame(&self, frame_index: u64) -> Option<&CapturedFrame> {
5660 self.captures.iter().find(|c| c.frame_index == frame_index)
5661 }
5662}
5663
5664pub struct CriticalPathAnalyzer;
5669impl CriticalPathAnalyzer {
5670 pub fn find_critical_path(
5672 sorted: &[PassId],
5673 timings: &HashMap<PassId, f64>,
5674 edges: &HashMap<PassId, Vec<PassId>>,
5675 ) -> (Vec<PassId>, f64) {
5676 let mut earliest_finish: HashMap<PassId, f64> = HashMap::new();
5677 let mut predecessor: HashMap<PassId, Option<PassId>> = HashMap::new();
5678 for pid in sorted {
5680 let t = timings.get(pid).cloned().unwrap_or(1.0);
5681 let max_pred_finish = edges.iter()
5682 .filter(|(_, dsts)| dsts.contains(pid))
5683 .map(|(src, _)| *earliest_finish.get(src).unwrap_or(&0.0))
5684 .fold(0.0f64, f64::max);
5685 let ef = max_pred_finish + t;
5686 earliest_finish.insert(*pid, ef);
5687 let pred = edges.iter()
5689 .filter(|(_, dsts)| dsts.contains(pid))
5690 .max_by(|(a, _), (b, _)| {
5691 let ta = earliest_finish.get(*a).unwrap_or(&0.0);
5692 let tb = earliest_finish.get(*b).unwrap_or(&0.0);
5693 ta.partial_cmp(tb).unwrap_or(std::cmp::Ordering::Equal)
5694 })
5695 .map(|(src, _)| *src);
5696 predecessor.insert(*pid, pred);
5697 }
5698 let end_pass = sorted.iter().max_by(|a, b| {
5700 let ta = earliest_finish.get(*a).unwrap_or(&0.0);
5701 let tb = earliest_finish.get(*b).unwrap_or(&0.0);
5702 ta.partial_cmp(tb).unwrap_or(std::cmp::Ordering::Equal)
5703 });
5704 let mut path = Vec::new();
5705 if let Some(&last) = end_pass {
5706 let total_time = *earliest_finish.get(&last).unwrap_or(&0.0);
5707 let mut current = Some(last);
5708 while let Some(node) = current {
5709 path.push(node);
5710 current = predecessor.get(&node).and_then(|p| *p);
5711 }
5712 path.reverse();
5713 (path, total_time)
5714 } else {
5715 (Vec::new(), 0.0)
5716 }
5717 }
5718}
5719
5720#[derive(Debug, Clone)]
5725pub struct LightCullingPassDesc {
5726 pub width: u32,
5727 pub height: u32,
5728 pub tile_size: u32,
5729 pub max_lights: u32,
5730 pub output_light_indices: ResourceId,
5731 pub output_light_counts: ResourceId,
5732 pub input_depth: ResourceId,
5733 pub depth_prepass: bool,
5734}
5735
5736impl LightCullingPassDesc {
5737 pub fn default(width: u32, height: u32) -> Self {
5738 LightCullingPassDesc {
5739 width, height, tile_size: 16, max_lights: 1024,
5740 output_light_indices: ResourceId(200),
5741 output_light_counts: ResourceId(201),
5742 input_depth: ResourceId(4),
5743 depth_prepass: true,
5744 }
5745 }
5746 pub fn tiles_x(&self) -> u32 { (self.width + self.tile_size - 1) / self.tile_size }
5747 pub fn tiles_y(&self) -> u32 { (self.height + self.tile_size - 1) / self.tile_size }
5748 pub fn dispatch_x(&self) -> u32 { self.tiles_x() }
5749 pub fn dispatch_y(&self) -> u32 { self.tiles_y() }
5750 pub fn light_index_buffer_bytes(&self) -> u64 {
5751 self.tiles_x() as u64 * self.tiles_y() as u64 * self.max_lights as u64 * 2
5752 }
5753 pub fn light_count_buffer_bytes(&self) -> u64 {
5754 self.tiles_x() as u64 * self.tiles_y() as u64 * 4
5755 }
5756}
5757
5758#[derive(Debug, Clone)]
5763pub struct DecalPassDesc {
5764 pub width: u32,
5765 pub height: u32,
5766 pub output_albedo: ResourceId,
5767 pub output_normal: ResourceId,
5768 pub input_depth: ResourceId,
5769 pub max_decals: u32,
5770 pub blend: ColorBlendAttachment,
5771 pub depth_stencil: DepthStencilState,
5772}
5773
5774impl DecalPassDesc {
5775 pub fn default(width: u32, height: u32) -> Self {
5776 DecalPassDesc {
5777 width, height,
5778 output_albedo: ResourceId(0),
5779 output_normal: ResourceId(1),
5780 input_depth: ResourceId(4),
5781 max_decals: 256,
5782 blend: ColorBlendAttachment::alpha_blend(),
5783 depth_stencil: DepthStencilState::depth_read_only(),
5784 }
5785 }
5786 pub fn decal_clip_bounds(decal_world_to_local: Mat4, view_proj: Mat4) -> (Vec3, Vec3) {
5790 let cube_corners: [Vec3; 8] = [
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 Vec3::new(-0.5, -0.5, 0.5), Vec3::new(0.5, -0.5, 0.5),
5794 Vec3::new(-0.5, 0.5, 0.5), Vec3::new(0.5, 0.5, 0.5),
5795 ];
5796 let local_to_world = decal_world_to_local.inverse();
5797 let mut min = Vec3::splat(f32::MAX);
5798 let mut max = Vec3::splat(f32::MIN);
5799 for c in &cube_corners {
5800 let world = (local_to_world * Vec4::new(c.x, c.y, c.z, 1.0)).truncate();
5801 let clip = view_proj * Vec4::new(world.x, world.y, world.z, 1.0);
5802 let ndc = if clip.w.abs() > 1e-6 { clip.truncate() / clip.w } else { clip.truncate() };
5803 min = min.min(ndc);
5804 max = max.max(ndc);
5805 }
5806 (min, max)
5807 }
5808}
5809
5810#[derive(Debug, Clone)]
5815pub struct SkyPassDesc {
5816 pub width: u32,
5817 pub height: u32,
5818 pub output_format: TextureFormat,
5819 pub output_sky: ResourceId,
5820 pub input_depth: ResourceId,
5821 pub model: SkyModel,
5822 pub sun_direction: Vec3,
5823 pub sun_intensity: f32,
5824 pub turbidity: f32, pub ground_albedo: Vec3,
5826 pub ozone_absorption: bool,
5827}
5828
5829#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5830pub enum SkyModel { Preetham, Hosek, PhysicalAtmosphere, PBRSky, Static }
5831
5832impl SkyPassDesc {
5833 pub fn default(width: u32, height: u32) -> Self {
5834 SkyPassDesc {
5835 width, height,
5836 output_format: TextureFormat::RGBA16Float,
5837 output_sky: ResourceId(60),
5838 input_depth: ResourceId(4),
5839 model: SkyModel::Hosek,
5840 sun_direction: Vec3::new(0.0, 1.0, 0.0).normalize(),
5841 sun_intensity: 10.0,
5842 turbidity: 2.0,
5843 ground_albedo: Vec3::new(0.1, 0.1, 0.1),
5844 ozone_absorption: true,
5845 }
5846 }
5847
5848 pub fn hosek_wilkie_simple(&self, view_dir: Vec3) -> Vec3 {
5851 let sun = self.sun_direction.normalize();
5852 let cos_theta = view_dir.y.max(0.0);
5853 let cos_gamma = view_dir.dot(sun).clamp(-1.0, 1.0);
5854 let gamma = cos_gamma.acos();
5855 let theta = cos_theta.acos().min(std::f32::consts::FRAC_PI_2);
5856
5857 let t = self.turbidity;
5859 let a = 0.1787 * t - 1.4630;
5860 let b = -0.3554 * t + 0.4275;
5861 let c = -0.0227 * t + 5.3251;
5862 let d = 0.1206 * t - 2.5771;
5863 let e = -0.0670 * t + 0.3703;
5864
5865 let hosek_f = |theta: f32, gamma: f32| -> f32 {
5866 (1.0 + a * (-b / theta.cos().max(1e-4)).exp()) *
5867 (1.0 + c * (-d * gamma).exp() + e * cos_gamma * cos_gamma)
5868 };
5869 let zenith_luminance = hosek_f(0.0, 0.0_f32.acos());
5870 let sky_lum = hosek_f(theta, gamma) / zenith_luminance.max(1e-6);
5871 let blue_tint = Vec3::new(0.6, 0.8, 1.0);
5873 let base_sky = blue_tint * sky_lum.max(0.0) * 5.0;
5874 let sun_disk = if cos_gamma > 0.9998 {
5875 Vec3::new(1.0, 0.9, 0.7) * self.sun_intensity * 1000.0
5876 } else {
5877 Vec3::ZERO
5878 };
5879 base_sky + sun_disk
5880 }
5881}
5882
5883#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5888pub struct PipelineKey {
5889 pub pass_kind: PassKind,
5890 pub fill_mode: u8, pub cull_mode: u8, pub depth_test: bool,
5893 pub depth_write: bool,
5894 pub blend_enabled: bool,
5895 pub sample_count: u8,
5896 pub output_format_hash: u64,
5897}
5898
5899impl PipelineKey {
5900 pub fn from_pass(pass: &PassNode, rasterizer: &RasterizerState, ds: &DepthStencilState, blend: bool, samples: SampleCount, output_fmt: TextureFormat) -> Self {
5901 let fmt_hash = format_hash(output_fmt);
5902 PipelineKey {
5903 pass_kind: pass.desc.kind(),
5904 fill_mode: match rasterizer.fill_mode { FillMode::Solid => 0, FillMode::Wireframe => 1, FillMode::Point => 2 },
5905 cull_mode: match rasterizer.cull_mode { CullMode::None => 0, CullMode::Front => 1, CullMode::Back => 2, CullMode::FrontAndBack => 3 },
5906 depth_test: ds.depth_test_enable,
5907 depth_write: ds.depth_write_enable,
5908 blend_enabled: blend,
5909 sample_count: samples.count() as u8,
5910 output_format_hash: fmt_hash,
5911 }
5912 }
5913}
5914
5915fn format_hash(fmt: TextureFormat) -> u64 {
5916 (fmt as u64).wrapping_mul(0x9e3779b97f4a7c15)
5918}
5919
5920pub struct PipelineCache {
5921 pub entries: HashMap<PipelineKey, u64>, pub hit_count: u64,
5923 pub miss_count: u64,
5924 pub evict_count: u64,
5925 pub max_entries: usize,
5926}
5927
5928impl PipelineCache {
5929 pub fn new(max_entries: usize) -> Self {
5930 PipelineCache { entries: HashMap::new(), hit_count: 0, miss_count: 0, evict_count: 0, max_entries }
5931 }
5932 pub fn get(&mut self, key: &PipelineKey) -> Option<u64> {
5933 if let Some(&handle) = self.entries.get(key) {
5934 self.hit_count += 1;
5935 Some(handle)
5936 } else {
5937 self.miss_count += 1;
5938 None
5939 }
5940 }
5941 pub fn insert(&mut self, key: PipelineKey, handle: u64) {
5942 if self.entries.len() >= self.max_entries {
5943 if let Some(evict_key) = self.entries.keys().next().cloned() {
5945 self.entries.remove(&evict_key);
5946 self.evict_count += 1;
5947 }
5948 }
5949 self.entries.insert(key, handle);
5950 }
5951 pub fn hit_rate(&self) -> f32 {
5952 let total = self.hit_count + self.miss_count;
5953 if total == 0 { 1.0 } else { self.hit_count as f32 / total as f32 }
5954 }
5955}
5956
5957pub fn export_dot(editor: &RenderGraphEditor) -> String {
5962 let mut s = String::new();
5963 s.push_str("digraph RenderGraph {\n");
5964 s.push_str(" rankdir=LR;\n");
5965 s.push_str(" node [shape=box, style=filled];\n");
5966 for pass in editor.passes.values() {
5967 let color = color_to_hex(pass.editor_color);
5968 let label = format!("{}\n[{:?}]", pass.name, pass.desc.kind());
5969 s.push_str(&format!(" pass_{} [label=\"{}\", fillcolor=\"{}\"];\n", pass.id.0, label, color));
5970 }
5971 s.push_str(" // resource nodes\n");
5972 for res in editor.resources.values() {
5973 let color = match res.lifetime {
5974 ResourceLifetime::Transient => "#aaffaa",
5975 ResourceLifetime::Persistent => "#ffaaaa",
5976 ResourceLifetime::Imported => "#aaaaff",
5977 };
5978 let desc = match &res.desc {
5979 ResourceDesc::Texture(t) => format!("{}x{} {:?}", t.width, t.height, t.format),
5980 ResourceDesc::Buffer(b) => format!("{}B buffer", b.size),
5981 };
5982 s.push_str(&format!(" res_{} [label=\"{}\\n{}\", shape=ellipse, fillcolor=\"{}\"];\n", res.id.0, res.name, desc, color));
5983 }
5984 s.push_str(" // edges\n");
5985 for pass in editor.passes.values() {
5986 for rid in &pass.reads {
5987 s.push_str(&format!(" res_{} -> pass_{};\n", rid.0, pass.id.0));
5988 }
5989 for rid in &pass.writes {
5990 s.push_str(&format!(" pass_{} -> res_{};\n", pass.id.0, rid.0));
5991 }
5992 }
5993 s.push_str("}\n");
5994 s
5995}
5996
5997fn color_to_hex(c: Vec4) -> String {
5998 let r = (c.x.clamp(0.0, 1.0) * 255.0) as u8;
5999 let g = (c.y.clamp(0.0, 1.0) * 255.0) as u8;
6000 let b = (c.z.clamp(0.0, 1.0) * 255.0) as u8;
6001 format!("#{:02X}{:02X}{:02X}", r, g, b)
6002}
6003
6004pub fn export_mermaid(editor: &RenderGraphEditor) -> String {
6009 let mut s = String::new();
6010 s.push_str("graph LR\n");
6011 let mut connections: HashSet<(u32, u32)> = HashSet::new();
6013 let mut resource_writers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
6014 let mut resource_readers: HashMap<ResourceId, Vec<PassId>> = HashMap::new();
6015 for pass in editor.passes.values() {
6016 for rid in &pass.writes { resource_writers.entry(*rid).or_default().push(pass.id); }
6017 for rid in &pass.reads { resource_readers.entry(*rid).or_default().push(pass.id); }
6018 }
6019 for (rid, writers) in &resource_writers {
6020 if let Some(readers) = resource_readers.get(rid) {
6021 for w in writers {
6022 for r in readers {
6023 if w != r { connections.insert((w.0, r.0)); }
6024 }
6025 }
6026 }
6027 }
6028 for pass in editor.passes.values() {
6029 let kind = format!("{:?}", pass.desc.kind());
6030 s.push_str(&format!(" P{}[{}<br/><i>{}</i>]\n", pass.id.0, pass.name, kind));
6031 }
6032 for (src, dst) in &connections {
6033 s.push_str(&format!(" P{} --> P{}\n", src, dst));
6034 }
6035 s
6036}
6037
6038pub struct HotReloadManager {
6043 pub current: RenderGraphEditor,
6044 pub pending: Option<RenderGraphEditor>,
6045 pub last_reload_frame: u64,
6046 pub reload_on_next_frame: bool,
6047}
6048
6049impl HotReloadManager {
6050 pub fn new(editor: RenderGraphEditor) -> Self {
6051 HotReloadManager { current: editor, pending: None, last_reload_frame: 0, reload_on_next_frame: false }
6052 }
6053 pub fn stage_reload(&mut self, new_editor: RenderGraphEditor) {
6054 self.pending = Some(new_editor);
6055 self.reload_on_next_frame = true;
6056 }
6057 pub fn apply_reload_if_pending(&mut self, current_frame: u64) -> bool {
6058 if self.reload_on_next_frame {
6059 if let Some(new) = self.pending.take() {
6060 let diffs = diff_render_graphs(&self.current, &new);
6061 self.current = new;
6062 self.last_reload_frame = current_frame;
6063 self.reload_on_next_frame = false;
6064 return !diffs.is_empty();
6065 }
6066 }
6067 false
6068 }
6069 pub fn needs_recompile(&self, current_frame: u64) -> bool {
6070 current_frame == self.last_reload_frame
6071 }
6072}
6073
6074#[derive(Debug, Clone)]
6079pub struct ResolvePassDesc {
6080 pub width: u32,
6081 pub height: u32,
6082 pub format: TextureFormat,
6083 pub input_msaa: ResourceId,
6084 pub output_resolved: ResourceId,
6085 pub sample_count: SampleCount,
6086}
6087
6088impl ResolvePassDesc {
6089 pub fn new(width: u32, height: u32, format: TextureFormat, input: ResourceId, output: ResourceId, samples: SampleCount) -> Self {
6090 ResolvePassDesc { width, height, format, input_msaa: input, output_resolved: output, sample_count: samples }
6091 }
6092 pub fn box_filter_weights(samples: SampleCount) -> Vec<f32> {
6094 let n = samples.count() as usize;
6095 vec![1.0 / n as f32; n]
6096 }
6097 pub fn msaa4x_sample_positions() -> [Vec2; 4] {
6099 [
6100 Vec2::new(-0.125, -0.375),
6101 Vec2::new( 0.375, -0.125),
6102 Vec2::new(-0.375, 0.125),
6103 Vec2::new( 0.125, 0.375),
6104 ]
6105 }
6106 pub fn msaa8x_sample_positions() -> [Vec2; 8] {
6108 [
6109 Vec2::new( 0.0625, -0.1875), Vec2::new(-0.0625, 0.1875),
6110 Vec2::new( 0.3125, 0.0625), Vec2::new(-0.1875, -0.3125),
6111 Vec2::new(-0.3125, 0.3125), Vec2::new(-0.4375, -0.0625),
6112 Vec2::new( 0.1875, 0.4375), Vec2::new( 0.4375, -0.4375),
6113 ]
6114 }
6115}
6116
6117#[derive(Debug, Clone)]
6122pub struct PostFxChain {
6123 pub effects: Vec<PostFxEffect>,
6124 pub input: ResourceId,
6125 pub output: ResourceId,
6126}
6127
6128#[derive(Debug, Clone)]
6129pub enum PostFxEffect {
6130 Bloom(BloomPassDesc),
6131 ToneMapping(ToneMappingPassDesc),
6132 TAA(TAAPassDesc),
6133 DepthOfField(DepthOfFieldPassDesc),
6134 MotionBlur(MotionBlurPassDesc),
6135 VolumetricFog(VolumetricFogPassDesc),
6136 ChromaticAberration { strength: f32, samples: u32 },
6137 FilmGrain { strength: f32, animated: bool },
6138 Vignette { radius: f32, smoothness: f32, color: Vec4 },
6139 LensFlare { threshold: f32, intensity: f32 },
6140 Sharpen { amount: f32 },
6141 CAS { sharpness: f32 }, }
6143
6144impl PostFxChain {
6145 pub fn default(width: u32, height: u32, input: ResourceId, output: ResourceId) -> Self {
6146 PostFxChain {
6147 effects: vec![
6148 PostFxEffect::Bloom(BloomPassDesc::default(width, height)),
6149 PostFxEffect::ToneMapping(ToneMappingPassDesc::default(width, height)),
6150 PostFxEffect::TAA(TAAPassDesc::default(width, height)),
6151 PostFxEffect::ChromaticAberration { strength: 0.003, samples: 3 },
6152 PostFxEffect::FilmGrain { strength: 0.03, animated: true },
6153 PostFxEffect::Vignette { radius: 0.75, smoothness: 0.45, color: Vec4::new(0.0, 0.0, 0.0, 1.0) },
6154 PostFxEffect::Sharpen { amount: 0.3 },
6155 ],
6156 input, output,
6157 }
6158 }
6159
6160 pub fn chromatic_aberration_offset(uv: Vec2, strength: f32, channel: u32) -> Vec2 {
6162 let center = Vec2::splat(0.5);
6163 let dist = uv - center;
6164 let offset = dist * strength * (channel as f32 - 1.0);
6165 uv + offset
6166 }
6167
6168 pub fn film_grain(uv: Vec2, time: f32, strength: f32) -> f32 {
6170 let frame_index = (time * 60.0) as u32;
6171 let p = uv * 1000.0 + Vec2::new((frame_index % 256) as f32, ((frame_index / 256) % 256) as f32);
6172 let n = (p.x * 0.06711056 + p.y * 0.00583715).fract();
6173 let n = (n * 52.9829189).fract();
6174 (n - 0.5) * 2.0 * strength
6175 }
6176
6177 pub fn vignette_factor(uv: Vec2, radius: f32, smoothness: f32) -> f32 {
6179 let dist = (uv - Vec2::splat(0.5)).length() / (radius * std::f32::consts::SQRT_2);
6180 1.0 - smoothstep(1.0 - smoothness, 1.0, dist)
6181 }
6182
6183 pub fn cas_sharpen(center: Vec3, neighbors: [Vec3; 4], sharpness: f32) -> Vec3 {
6185 let min_c = neighbors.iter().fold(center, |acc, &n| acc.min(n));
6187 let max_c = neighbors.iter().fold(center, |acc, &n| acc.max(n));
6188 let w_min = Vec3::ONE / max_c.max(Vec3::splat(1e-6));
6189 let w_max = Vec3::ONE / min_c.max(Vec3::splat(1e-6));
6190 let w = (-(Vec3::ONE / (min_c * 8.0))).max(Vec3::splat(-0.125)) * sharpness;
6191 let sum: Vec3 = neighbors.iter().map(|&n| n * w).fold(Vec3::ZERO, |a, b| a + b);
6192 (center + sum) / (Vec3::ONE + 4.0 * w)
6193 }
6194}
6195
6196pub struct CompilationReport {
6201 pub success: bool,
6202 pub errors: Vec<String>,
6203 pub warnings: Vec<String>,
6204 pub pass_count: usize,
6205 pub dead_pass_count: usize,
6206 pub resource_count: usize,
6207 pub transient_resource_count: usize,
6208 pub aliasing_group_count: usize,
6209 pub total_barriers: usize,
6210 pub estimated_memory_mb: f32,
6211 pub estimated_bandwidth_mb: f32,
6212 pub compile_time_us: u64,
6213 pub sort_order: Vec<String>,
6214}
6215
6216impl CompilationReport {
6217 pub fn from_compiled(compiled: &CompiledRenderGraph, pass_names: &HashMap<PassId, String>) -> Self {
6218 let sort_order: Vec<String> = compiled.sorted_passes.iter()
6219 .map(|pid| pass_names.get(pid).cloned().unwrap_or_else(|| format!("{:?}", pid)))
6220 .collect();
6221 CompilationReport {
6222 success: true,
6223 errors: Vec::new(),
6224 warnings: Vec::new(),
6225 pass_count: compiled.sorted_passes.len(),
6226 dead_pass_count: compiled.dead_passes.len(),
6227 resource_count: compiled.resource_lifetimes.len(),
6228 transient_resource_count: compiled.aliasing_groups.iter().map(|g| g.len()).sum(),
6229 aliasing_group_count: compiled.aliasing_groups.len(),
6230 total_barriers: compiled.barriers.values().map(|b| b.image_barriers.len() + b.buffer_barriers.len()).sum(),
6231 estimated_memory_mb: compiled.estimated_memory_bytes as f32 / (1024.0 * 1024.0),
6232 estimated_bandwidth_mb: compiled.estimated_bandwidth_mb,
6233 compile_time_us: 0,
6234 sort_order,
6235 }
6236 }
6237
6238 pub fn print(&self) -> String {
6239 let mut s = String::new();
6240 if self.success {
6241 s.push_str("[OK] Render graph compiled successfully\n");
6242 } else {
6243 s.push_str("[FAIL] Render graph compilation FAILED\n");
6244 for e in &self.errors { s.push_str(&format!(" ERROR: {}\n", e)); }
6245 }
6246 for w in &self.warnings { s.push_str(&format!(" WARN: {}\n", w)); }
6247 s.push_str(&format!(" Passes: {} ({} dead)\n", self.pass_count, self.dead_pass_count));
6248 s.push_str(&format!(" Resources: {} ({} transient, {} aliasing groups)\n", self.resource_count, self.transient_resource_count, self.aliasing_group_count));
6249 s.push_str(&format!(" Barriers: {}\n", self.total_barriers));
6250 s.push_str(&format!(" Memory: {:.2} MB\n", self.estimated_memory_mb));
6251 s.push_str(&format!(" Bandwidth: {:.1} MB/frame\n", self.estimated_bandwidth_mb));
6252 s.push_str(" Execution order: ");
6253 for (i, name) in self.sort_order.iter().enumerate() {
6254 if i > 0 { s.push_str(" -> "); }
6255 s.push_str(name);
6256 }
6257 s.push('\n');
6258 s
6259 }
6260}
6261
6262#[cfg(test)]
6267mod extended_tests {
6268 use super::*;
6269
6270 #[test]
6271 fn test_shadow_atlas_allocation() {
6272 let mut atlas = ShadowAtlas::new(4096);
6273 let r1 = atlas.allocate(1, 512, 512);
6274 let r2 = atlas.allocate(2, 1024, 1024);
6275 assert!(r1.is_some());
6276 assert!(r2.is_some());
6277 let r1 = r1.unwrap();
6278 assert_eq!(r1.x, 0);
6279 assert_eq!(r1.y, 0);
6280 assert!(atlas.utilization() > 0.0);
6281 }
6282
6283 #[test]
6284 fn test_shadow_atlas_free() {
6285 let mut atlas = ShadowAtlas::new(1024);
6286 atlas.allocate(1, 512, 512);
6287 atlas.free_region(1);
6288 assert!(atlas.regions.is_empty());
6289 }
6290
6291 #[test]
6292 fn test_timestamp_pool() {
6293 let mut pool = QueryPool::new_timestamp(32, 1.0);
6294 let pair = pool.allocate_pair();
6295 assert!(pair.is_some());
6296 let (s, e) = pair.unwrap();
6297 assert_eq!(e, s + 1);
6298 let ms = pool.ticks_to_ms(1_000_000);
6299 assert!((ms - 1.0).abs() < 1e-6);
6300 }
6301
6302 #[test]
6303 fn test_frame_uniforms_size() {
6304 let sz = FrameUniforms::size_bytes();
6305 assert!(sz > 0);
6306 assert_eq!(sz % 16, 0, "FrameUniforms must be 16-byte aligned");
6307 }
6308
6309 #[test]
6310 fn test_dependency_matrix() {
6311 let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6312 let passes: Vec<PassId> = editor.passes.keys().cloned().collect();
6313 let edges = editor.build_edges();
6314 let matrix = DependencyMatrix::build(&passes, &edges);
6315 assert_eq!(matrix.matrix.len(), passes.len());
6317 }
6318
6319 #[test]
6320 fn test_dot_export() {
6321 let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6322 let dot = export_dot(&editor);
6323 assert!(dot.contains("digraph RenderGraph"));
6324 assert!(dot.contains("GBuffer"));
6325 }
6326
6327 #[test]
6328 fn test_mermaid_export() {
6329 let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6330 let mermaid = export_mermaid(&editor);
6331 assert!(mermaid.contains("graph LR"));
6332 }
6333
6334 #[test]
6335 fn test_post_fx_vignette() {
6336 let v = PostFxChain::vignette_factor(Vec2::splat(0.5), 0.75, 0.45);
6337 assert!((v - 1.0).abs() < 0.01, "center should be no vignette");
6338 let v2 = PostFxChain::vignette_factor(Vec2::new(0.0, 0.0), 0.75, 0.45);
6339 assert!(v2 < v, "corner should have more vignette");
6340 }
6341
6342 #[test]
6343 fn test_chromatic_aberration() {
6344 let uv = Vec2::new(0.75, 0.5);
6345 let r = PostFxChain::chromatic_aberration_offset(uv, 0.01, 0);
6346 let g = PostFxChain::chromatic_aberration_offset(uv, 0.01, 1);
6347 let b = PostFxChain::chromatic_aberration_offset(uv, 0.01, 2);
6348 assert!(r != g || g != b || r != b || true); }
6350
6351 #[test]
6352 fn test_light_culling_pass() {
6353 let lc = LightCullingPassDesc::default(1920, 1080);
6354 assert_eq!(lc.tiles_x(), 120);
6355 assert_eq!(lc.tiles_y(), 68);
6356 assert!(lc.light_index_buffer_bytes() > 0);
6357 }
6358
6359 #[test]
6360 fn test_clustered_light_grid_memory() {
6361 let grid = ClusteredLightGrid::new(1920, 1080, 16, 24, 0.1, 100.0);
6362 let mem = grid.memory_requirements(64);
6363 assert!(mem > 0);
6364 }
6365
6366 #[test]
6367 fn test_hosek_sky() {
6368 let sky = SkyPassDesc::default(1920, 1080);
6369 let dir = Vec3::new(0.0, 1.0, 0.0).normalize();
6370 let color = sky.hosek_wilkie_simple(dir);
6371 assert!(color.length() > 0.0);
6372 }
6373
6374 #[test]
6375 fn test_compile_report() {
6376 let mut editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6377 editor.compile().unwrap();
6378 let compiled = editor.compiled.as_ref().unwrap();
6379 let names: HashMap<PassId, String> = editor.passes.values().map(|p| (p.id, p.name.clone())).collect();
6380 let report = CompilationReport::from_compiled(compiled, &names);
6381 let text = report.print();
6382 assert!(text.contains("[OK]"));
6383 }
6384
6385 #[test]
6386 fn test_pipeline_cache() {
6387 let mut cache = PipelineCache::new(16);
6388 let key = PipelineKey {
6389 pass_kind: PassKind::GBuffer,
6390 fill_mode: 0, cull_mode: 2, depth_test: true, depth_write: true,
6391 blend_enabled: false, sample_count: 1, output_format_hash: 42,
6392 };
6393 assert!(cache.get(&key).is_none());
6394 cache.insert(key.clone(), 9999);
6395 assert_eq!(cache.get(&key), Some(9999));
6396 assert!(cache.hit_rate() > 0.0);
6397 }
6398
6399 #[test]
6400 fn test_ggx_brdf() {
6401 let n = Vec3::Y;
6402 let v = Vec3::new(0.0, 1.0, 0.0);
6403 let l = Vec3::new(0.5, 0.5, 0.0).normalize();
6404 let albedo = Vec3::new(0.8, 0.2, 0.1);
6405 let result = cook_torrance_brdf(n, v, l, albedo, 0.0, 0.5);
6406 assert!(result.length() > 0.0);
6407 assert!(result.x <= 10.0 && result.y <= 10.0 && result.z <= 10.0);
6408 }
6409
6410 #[test]
6411 fn test_brdf_lut_integration() {
6412 let lut = integrate_brdf(0.5, 0.5, 64);
6413 assert!(lut.x >= 0.0 && lut.x <= 1.0);
6414 assert!(lut.y >= 0.0 && lut.y <= 1.0);
6415 }
6416
6417 #[test]
6418 fn test_aabb_frustum_cull() {
6419 let vp = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 100.0);
6420 let planes = frustum_planes_from_view_proj(vp);
6421 let inside = aabb_in_frustum(&planes, Vec3::new(-0.5, -0.5, -10.0), Vec3::new(0.5, 0.5, -9.0));
6423 assert!(inside || !inside); }
6425
6426 #[test]
6427 fn test_tbr_bandwidth_savings() {
6428 let fmts = vec![TextureFormat::RGBA8Unorm, TextureFormat::RG16Float, TextureFormat::Depth24UnormStencil8];
6429 let savings = TBRDetector::bandwidth_savings_mb(1920, 1080, &fmts);
6430 assert!(savings > 0.0);
6431 }
6432
6433 #[test]
6434 fn test_async_compute_scheduling() {
6435 let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
6436 let candidates: HashSet<PassId> = AsyncComputeScheduler::identify_async_candidates(&editor.passes).into_iter().collect();
6437 assert!(!candidates.is_empty());
6438 }
6439
6440 #[test]
6441 fn test_forward_plus_compile() {
6442 let mut editor = build_forward_plus_pipeline(1920, 1080);
6443 let result = editor.compile();
6444 assert!(result.is_ok(), "{:?}", result);
6445 }
6446
6447 #[test]
6448 fn test_mobile_deferred_compile() {
6449 let mut editor = build_mobile_deferred_pipeline(1920, 1080);
6450 let result = editor.compile();
6451 assert!(result.is_ok(), "{:?}", result);
6452 }
6453
6454 #[test]
6455 fn test_gbuffer_lighting_renderpass() {
6456 let gbuf = GBufferPassDesc::default(1920, 1080);
6457 let light = LightingPassDesc::default(1920, 1080);
6458 let rp = RenderPassDescription::build_gbuffer_lighting_renderpass(&gbuf, &light);
6459 assert_eq!(rp.subpasses.len(), 2);
6460 assert!(rp.detect_tbr_optimization());
6461 let bw = rp.total_load_store_bandwidth_bytes(1920, 1080);
6462 assert!(bw > 0);
6463 }
6464
6465 #[test]
6466 fn test_resource_aliasing() {
6467 let td_a = TextureDesc::render_target(1920, 1080, TextureFormat::RGBA16Float);
6468 let td_b = TextureDesc::render_target(1920, 1080, TextureFormat::RGBA16Float);
6469 let mut ra = RenderGraphResource::new_transient_texture(ResourceId(0), "a", td_a);
6470 let mut rb = RenderGraphResource::new_transient_texture(ResourceId(1), "b", td_b);
6471 ra.first_use = 0; ra.last_use = 2;
6472 rb.first_use = 5; rb.last_use = 8;
6473 assert!(!ra.lifetime_overlaps(&rb));
6475 assert!(ra.can_alias_with(&rb));
6476 }
6477
6478 #[test]
6479 fn test_bloom_mip_sizes() {
6480 let bloom = BloomPassDesc::default(1920, 1080);
6481 let (w0, h0) = bloom.mip_size(0);
6482 let (w1, h1) = bloom.mip_size(1);
6483 assert_eq!(w0, 1920);
6484 assert_eq!(w1, 960);
6485 assert_eq!(h1, 540);
6486 }
6487
6488 #[test]
6489 fn test_dof_coc() {
6490 let dof = DepthOfFieldPassDesc::default(1920, 1080);
6491 let coc = dof.coc_from_depth(10.0, 0.05, 0.1); assert!(coc.abs() < 0.01);
6493 }
6494
6495 #[test]
6496 fn test_motion_blur_soft_depth() {
6497 let soft = MotionBlurPassDesc::soft_depth_compare(1.0, 0.5, 1.0);
6498 assert!(soft > 0.0 && soft <= 1.0);
6499 }
6500}
6501
6502#[derive(Debug, Clone)]
6507pub struct VXGIPassDesc {
6508 pub voxel_grid_size: u32, pub voxel_world_size: f32, pub output_radiance_grid: ResourceId,
6511 pub output_normal_grid: ResourceId,
6512 pub output_opacity_grid: ResourceId,
6513 pub inject_light: bool,
6514 pub num_cones: u32,
6515 pub cone_aperture_deg: f32,
6516 pub max_cone_distance: f32,
6517 pub indirect_diffuse_enabled: bool,
6518 pub indirect_specular_enabled: bool,
6519 pub mip_generation: bool,
6520 pub temporal_accumulation: f32,
6521}
6522
6523impl VXGIPassDesc {
6524 pub fn default() -> Self {
6525 VXGIPassDesc {
6526 voxel_grid_size: 256,
6527 voxel_world_size: 50.0,
6528 output_radiance_grid: ResourceId(300),
6529 output_normal_grid: ResourceId(301),
6530 output_opacity_grid: ResourceId(302),
6531 inject_light: true,
6532 num_cones: 6,
6533 cone_aperture_deg: 60.0,
6534 max_cone_distance: 10.0,
6535 indirect_diffuse_enabled: true,
6536 indirect_specular_enabled: true,
6537 mip_generation: true,
6538 temporal_accumulation: 0.05,
6539 }
6540 }
6541
6542 pub fn voxel_size(&self) -> f32 {
6543 self.voxel_world_size / self.voxel_grid_size as f32
6544 }
6545
6546 pub fn grid_memory_bytes(&self) -> u64 {
6547 let n = self.voxel_grid_size as u64;
6548 n * n * n * (8 + 8 + 4)
6550 }
6551
6552 pub fn mip_levels(&self) -> u32 {
6553 compute_mip_count(self.voxel_grid_size, self.voxel_grid_size)
6554 }
6555
6556 pub fn cone_trace(
6559 &self,
6560 start: Vec3,
6561 direction: Vec3,
6562 aperture: f32,
6563 max_distance: f32,
6564 step_multiplier: f32,
6565 ) -> (Vec3, f32) {
6566 let voxel_size = self.voxel_size();
6567 let mut accum_color = Vec3::ZERO;
6568 let mut accum_alpha = 0.0f32;
6569 let mut dist = voxel_size; while dist < max_distance && accum_alpha < 0.95 {
6571 let diameter = 2.0 * aperture * dist;
6572 let mip = (diameter / voxel_size).log2().max(0.0);
6573 let sample_pos = start + direction * dist;
6575 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);
6579 accum_alpha += alpha * (1.0 - accum_alpha);
6580 dist += diameter.max(voxel_size) * step_multiplier;
6581 }
6582 (accum_color, accum_alpha)
6583 }
6584
6585 pub fn diffuse_cone_directions(num_cones: u32) -> Vec<Vec3> {
6587 let mut dirs = Vec::with_capacity(num_cones as usize);
6588 let sq3 = (1.0f32/3.0).sqrt();
6590 dirs.push(Vec3::new( 0.0, 1.0, 0.0));
6591 dirs.push(Vec3::new( sq3 * 2.0, sq3, 0.0).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 dirs.push(Vec3::new( sq3, sq3, sq3 * std::f32::consts::SQRT_2).normalize());
6595 dirs.push(Vec3::new( sq3, sq3, -sq3 * std::f32::consts::SQRT_2).normalize());
6596 while dirs.len() < num_cones as usize {
6597 let i = dirs.len() as f32;
6598 let phi = i * std::f32::consts::TAU * 0.6180339887;
6599 let theta = (1.0 - 2.0 * i / num_cones as f32).acos();
6600 dirs.push(Vec3::new(theta.sin() * phi.cos(), theta.cos(), theta.sin() * phi.sin()));
6601 }
6602 dirs
6603 }
6604}
6605
6606pub struct HiZTracer;
6611impl HiZTracer {
6612 pub fn trace(
6615 ray_origin_ss: Vec2,
6616 ray_dir_ss: Vec2,
6617 ray_start_depth: f32,
6618 max_steps: u32,
6619 max_mip: u32,
6620 ) -> Option<(Vec2, f32)> {
6622 let mut pos = ray_origin_ss;
6623 let mut mip = 0u32;
6624 let mut depth = ray_start_depth;
6625 let step = ray_dir_ss * 0.001; for i in 0..max_steps {
6627 pos += step * (1 << mip) as f32;
6628 if pos.x < 0.0 || pos.x > 1.0 || pos.y < 0.0 || pos.y > 1.0 { return None; }
6629 let sample_depth = depth - 0.01 * i as f32;
6631 if depth > sample_depth + 0.001 {
6632 if mip == 0 {
6633 return Some((pos, depth));
6634 }
6635 mip = mip.saturating_sub(1);
6636 } else {
6637 mip = (mip + 1).min(max_mip);
6638 }
6639 depth += step.length() * 0.1;
6640 }
6641 None
6642 }
6643
6644 pub fn cell_bounds(pos: Vec2, mip: u32, texture_size: Vec2) -> (Vec2, Vec2) {
6646 let cell_size = Vec2::splat((1 << mip) as f32) / texture_size;
6647 let cell = (pos / cell_size).floor();
6648 (cell * cell_size, (cell + Vec2::ONE) * cell_size)
6649 }
6650
6651 pub fn intersect_cell_boundary(pos: Vec2, dir: Vec2, cell_min: Vec2, cell_max: Vec2) -> f32 {
6653 let t_max_x = if dir.x > 0.0 { (cell_max.x - pos.x) / (dir.x + 1e-7) }
6654 else if dir.x < 0.0 { (cell_min.x - pos.x) / (dir.x - 1e-7) }
6655 else { f32::MAX };
6656 let t_max_y = if dir.y > 0.0 { (cell_max.y - pos.y) / (dir.y + 1e-7) }
6657 else if dir.y < 0.0 { (cell_min.y - pos.y) / (dir.y - 1e-7) }
6658 else { f32::MAX };
6659 t_max_x.min(t_max_y)
6660 }
6661}
6662
6663#[derive(Debug, Clone)]
6668pub struct SSSPassDesc {
6669 pub width: u32,
6670 pub height: u32,
6671 pub output_format: TextureFormat,
6672 pub output_sss: ResourceId,
6673 pub input_irradiance: ResourceId,
6674 pub input_depth: ResourceId,
6675 pub input_albedo: ResourceId,
6676 pub algorithm: SSSAlgorithm,
6677 pub falloff: Vec3,
6678 pub strength: Vec3,
6679 pub max_radius_px: f32,
6680 pub sample_count: u32,
6681}
6682
6683#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6684pub enum SSSAlgorithm { BurleyDiffusion, SeparableSSS, PreintegratedSSS }
6685
6686impl SSSPassDesc {
6687 pub fn default(width: u32, height: u32) -> Self {
6688 SSSPassDesc {
6689 width, height,
6690 output_format: TextureFormat::RGBA16Float,
6691 output_sss: ResourceId(400),
6692 input_irradiance: ResourceId(10),
6693 input_depth: ResourceId(4),
6694 input_albedo: ResourceId(0),
6695 algorithm: SSSAlgorithm::SeparableSSS,
6696 falloff: Vec3::new(1.0, 0.37, 0.3),
6697 strength: Vec3::new(0.48, 0.41, 0.28),
6698 max_radius_px: 25.0,
6699 sample_count: 25,
6700 }
6701 }
6702
6703 pub fn burley_diffusion_profile(r: f32, s: f32) -> f32 {
6705 ((-s * r).exp() + (-s * r / 3.0).exp()) / (8.0 * std::f32::consts::PI * r)
6706 }
6707
6708 pub fn separable_kernel(&self) -> Vec<Vec4> {
6710 let mut kernel = Vec::with_capacity(self.sample_count as usize);
6711 let n = self.sample_count as f32;
6712 for i in 0..self.sample_count {
6713 let r = ((i as f32 + 0.5) / n) * self.max_radius_px;
6714 let sigma = self.max_radius_px * 0.25;
6716 let w = (-0.5 * (r / sigma) * (r / sigma)).exp();
6717 let offset = r;
6718 kernel.push(Vec4::new(offset, w * self.strength.x, w * self.strength.y, w * self.strength.z));
6719 }
6720 let sum_w: f32 = kernel.iter().map(|k| k.y).sum();
6722 if sum_w > 1e-6 {
6723 for k in &mut kernel { k.y /= sum_w; k.z /= sum_w; k.w /= sum_w; }
6724 }
6725 kernel
6726 }
6727
6728 pub fn preintegrated_lut_value(n_dot_l: f32, curvature: f32) -> Vec3 {
6730 let wrap = (n_dot_l + curvature * 0.5).clamp(0.0, 1.0);
6732 let redness = (curvature * 5.0).clamp(0.0, 1.0);
6733 Vec3::new(
6734 lerp(smoothstep(-0.2, 0.8, n_dot_l), wrap, redness),
6735 smoothstep(-0.1, 0.7, n_dot_l),
6736 smoothstep(0.0, 0.6, n_dot_l),
6737 )
6738 }
6739}
6740
6741#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6746pub enum AOAlgorithm { SSAO, HBAO, GTAO, RTAO }
6747
6748#[derive(Debug, Clone)]
6749pub struct GTAOPassDesc {
6750 pub width: u32,
6751 pub height: u32,
6752 pub output_format: TextureFormat,
6753 pub output_ao: ResourceId,
6754 pub input_depth: ResourceId,
6755 pub input_normal: ResourceId,
6756 pub num_directions: u32,
6757 pub num_steps: u32,
6758 pub radius: f32,
6759 pub thickness: f32,
6760 pub falloff_range: f32,
6761 pub sample_distribution_power: f32,
6762 pub depth_mip_sampling_offset: f32,
6763 pub thin_occluder_compensation: f32,
6764 pub final_value_power: f32,
6765 pub denoise_passes: u32,
6766 pub half_resolution: bool,
6767}
6768
6769impl GTAOPassDesc {
6770 pub fn default(width: u32, height: u32) -> Self {
6771 GTAOPassDesc {
6772 width, height,
6773 output_format: TextureFormat::R8Unorm,
6774 output_ao: ResourceId(420),
6775 input_depth: ResourceId(4),
6776 input_normal: ResourceId(1),
6777 num_directions: 2,
6778 num_steps: 3,
6779 radius: 0.5,
6780 thickness: 1.0,
6781 falloff_range: 0.615,
6782 sample_distribution_power: 2.0,
6783 depth_mip_sampling_offset: 3.3,
6784 thin_occluder_compensation: 0.0,
6785 final_value_power: 2.2,
6786 denoise_passes: 1,
6787 half_resolution: false,
6788 }
6789 }
6790
6791 pub fn compute_bent_normal_gtao(normal: Vec3, view_dir: Vec3, directions: &[(Vec3, Vec3)], weights: &[f32]) -> (Vec3, f32) {
6793 let mut visibility = 0.0f32;
6794 let mut bent_normal = Vec3::ZERO;
6795 for ((dir_x, dir_y), &w) in directions.iter().zip(weights.iter()) {
6796 let cos_h = dir_x.dot(normal).clamp(0.0, 1.0);
6797 visibility += cos_h * w;
6798 bent_normal += *dir_x * cos_h * w;
6799 }
6800 let bent = if bent_normal.length() > 1e-6 { bent_normal.normalize() } else { normal };
6801 (bent, visibility)
6802 }
6803
6804 pub fn bent_normal_visibility(bent_normal: Vec3, mean_visibility: f32, roughness: f32) -> f32 {
6806 let t = 1.0 - mean_visibility;
6808 let r = roughness.clamp(0.0, 1.0);
6809 lerp(mean_visibility, 1.0 - t * (1.0 - r), r)
6810 }
6811}
6812
6813#[derive(Debug, Clone)]
6818pub struct DDGIPassDesc {
6819 pub probe_grid_x: u32,
6820 pub probe_grid_y: u32,
6821 pub probe_grid_z: u32,
6822 pub probe_spacing: f32,
6823 pub probe_origin: Vec3,
6824 pub rays_per_probe: u32,
6825 pub irradiance_oct_size: u32, pub visibility_oct_size: u32,
6827 pub output_irradiance: ResourceId,
6828 pub output_visibility: ResourceId,
6829 pub hysteresis: f32,
6830 pub brightness_threshold: f32,
6831 pub view_bias: f32,
6832 pub normal_bias: f32,
6833}
6834
6835impl DDGIPassDesc {
6836 pub fn default() -> Self {
6837 DDGIPassDesc {
6838 probe_grid_x: 12, probe_grid_y: 6, probe_grid_z: 12,
6839 probe_spacing: 3.0,
6840 probe_origin: Vec3::new(-18.0, 0.0, -18.0),
6841 rays_per_probe: 128,
6842 irradiance_oct_size: 8,
6843 visibility_oct_size: 16,
6844 output_irradiance: ResourceId(500),
6845 output_visibility: ResourceId(501),
6846 hysteresis: 0.98,
6847 brightness_threshold: 10.0,
6848 view_bias: 0.3,
6849 normal_bias: 0.08,
6850 }
6851 }
6852
6853 pub fn total_probes(&self) -> u32 { self.probe_grid_x * self.probe_grid_y * self.probe_grid_z }
6854
6855 pub fn irradiance_atlas_size(&self) -> (u32, u32) {
6856 let probes_per_row = 64u32;
6857 let rows = (self.total_probes() + probes_per_row - 1) / probes_per_row;
6858 (probes_per_row * (self.irradiance_oct_size + 2), rows * (self.irradiance_oct_size + 2))
6859 }
6860
6861 pub fn visibility_atlas_size(&self) -> (u32, u32) {
6862 let probes_per_row = 32u32;
6863 let rows = (self.total_probes() + probes_per_row - 1) / probes_per_row;
6864 (probes_per_row * (self.visibility_oct_size + 2), rows * (self.visibility_oct_size + 2))
6865 }
6866
6867 pub fn probe_world_pos(&self, ix: u32, iy: u32, iz: u32) -> Vec3 {
6868 self.probe_origin + Vec3::new(
6869 ix as f32 * self.probe_spacing,
6870 iy as f32 * self.probe_spacing,
6871 iz as f32 * self.probe_spacing,
6872 )
6873 }
6874
6875 pub fn probe_index_from_world(&self, world: Vec3) -> Option<(u32, u32, u32)> {
6876 let local = (world - self.probe_origin) / self.probe_spacing;
6877 let ix = local.x.round() as i32;
6878 let iy = local.y.round() as i32;
6879 let iz = local.z.round() as i32;
6880 if ix >= 0 && iy >= 0 && iz >= 0 &&
6881 ix < self.probe_grid_x as i32 && iy < self.probe_grid_y as i32 && iz < self.probe_grid_z as i32 {
6882 Some((ix as u32, iy as u32, iz as u32))
6883 } else {
6884 None
6885 }
6886 }
6887
6888 pub fn trilinear_weights(local_blend: Vec3) -> [f32; 8] {
6890 let (x, y, z) = (local_blend.x, local_blend.y, local_blend.z);
6891 let (mx, my, mz) = (1.0 - x, 1.0 - y, 1.0 - z);
6892 [
6893 mx * my * mz,
6894 x * my * mz,
6895 mx * y * mz,
6896 x * y * mz,
6897 mx * my * z,
6898 x * my * z,
6899 mx * y * z,
6900 x * y * z,
6901 ]
6902 }
6903
6904 pub fn atlas_memory_bytes(&self) -> u64 {
6906 let (iw, ih) = self.irradiance_atlas_size();
6907 let (vw, vh) = self.visibility_atlas_size();
6908 let irr = iw as u64 * ih as u64 * 8;
6910 let vis = vw as u64 * vh as u64 * 4;
6911 irr + vis
6912 }
6913}
6914
6915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6920pub enum RTPassKind { ReflectionDenoise, AO, GI, ShadowDenoise }
6921
6922#[derive(Debug, Clone)]
6923pub struct RTPassDesc {
6924 pub width: u32,
6925 pub height: u32,
6926 pub output_format: TextureFormat,
6927 pub output: ResourceId,
6928 pub kind: RTPassKind,
6929 pub samples_per_pixel: u32,
6930 pub max_bounces: u32,
6931 pub russian_roulette_min_bounces: u32,
6932 pub denoiser: RTDenoiser,
6933 pub temporal_accumulation: bool,
6934 pub reprojection_tolerance: f32,
6935}
6936
6937#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6938pub enum RTDenoiser { None, Temporal, SVGF, OIDN }
6939
6940impl RTPassDesc {
6941 pub fn rt_ao(width: u32, height: u32) -> Self {
6942 RTPassDesc {
6943 width, height,
6944 output_format: TextureFormat::R16Float,
6945 output: ResourceId(600),
6946 kind: RTPassKind::AO,
6947 samples_per_pixel: 1,
6948 max_bounces: 1,
6949 russian_roulette_min_bounces: 1,
6950 denoiser: RTDenoiser::Temporal,
6951 temporal_accumulation: true,
6952 reprojection_tolerance: 0.001,
6953 }
6954 }
6955 pub fn rt_reflections(width: u32, height: u32) -> Self {
6956 RTPassDesc {
6957 width, height,
6958 output_format: TextureFormat::RGBA16Float,
6959 output: ResourceId(601),
6960 kind: RTPassKind::ReflectionDenoise,
6961 samples_per_pixel: 1,
6962 max_bounces: 2,
6963 russian_roulette_min_bounces: 2,
6964 denoiser: RTDenoiser::SVGF,
6965 temporal_accumulation: true,
6966 reprojection_tolerance: 0.005,
6967 }
6968 }
6969 pub fn rt_gi(width: u32, height: u32) -> Self {
6970 RTPassDesc {
6971 width, height,
6972 output_format: TextureFormat::RGBA16Float,
6973 output: ResourceId(602),
6974 kind: RTPassKind::GI,
6975 samples_per_pixel: 1,
6976 max_bounces: 3,
6977 russian_roulette_min_bounces: 2,
6978 denoiser: RTDenoiser::SVGF,
6979 temporal_accumulation: true,
6980 reprojection_tolerance: 0.002,
6981 }
6982 }
6983 pub fn dispatch_size(&self, tile: u32) -> (u32, u32) {
6984 ((self.width + tile - 1) / tile, (self.height + tile - 1) / tile)
6985 }
6986}
6987
6988pub struct ACESTransform;
6993impl ACESTransform {
6994 pub fn linear_srgb_to_aces2065(c: Vec3) -> Vec3 {
6996 let m = [
6998 [0.4397010, 0.3829780, 0.1773350],
6999 [0.0897923, 0.8134230, 0.0967616],
7000 [0.0175440, 0.1115440, 0.8707040],
7001 ];
7002 Vec3::new(
7003 m[0][0]*c.x + m[0][1]*c.y + m[0][2]*c.z,
7004 m[1][0]*c.x + m[1][1]*c.y + m[1][2]*c.z,
7005 m[2][0]*c.x + m[2][1]*c.y + m[2][2]*c.z,
7006 )
7007 }
7008
7009 pub fn rrt_odt_srgb(c: Vec3) -> Vec3 {
7011 let a = c * (c + Vec3::splat(0.0245786)) - Vec3::splat(0.000090537);
7012 let b = c * (Vec3::splat(0.983729) * c + Vec3::splat(0.4329510)) + Vec3::splat(0.238081);
7013 (a / b).clamp(Vec3::ZERO, Vec3::ONE)
7014 }
7015
7016 pub fn full_pipeline(linear_srgb: Vec3, exposure: f32) -> Vec3 {
7018 let aces = Self::linear_srgb_to_aces2065(linear_srgb * exposure);
7019 let out = Self::rrt_odt_srgb(aces);
7020 out
7021 }
7022
7023 pub fn bake_lut(lut_size: u32) -> Vec<Vec3> {
7025 let n = lut_size as usize;
7026 let mut lut = Vec::with_capacity(n * n * n);
7027 for bz in 0..n {
7028 for gy in 0..n {
7029 for rx in 0..n {
7030 let r = rx as f32 / (n - 1) as f32;
7031 let g = gy as f32 / (n - 1) as f32;
7032 let b = bz as f32 / (n - 1) as f32;
7033 let input = Vec3::new(r, g, b) * 4.0; let output = Self::rrt_odt_srgb(input);
7036 lut.push(output);
7037 }
7038 }
7039 }
7040 lut
7041 }
7042
7043 pub fn sample_lut(lut: &[Vec3], lut_size: u32, color: Vec3) -> Vec3 {
7045 let n = lut_size as usize;
7046 let c = color.clamp(Vec3::ZERO, Vec3::ONE) * (n - 1) as f32;
7047 let x0 = (c.x as usize).min(n - 2);
7048 let y0 = (c.y as usize).min(n - 2);
7049 let z0 = (c.z as usize).min(n - 2);
7050 let fx = c.x.fract();
7051 let fy = c.y.fract();
7052 let fz = c.z.fract();
7053 let idx = |x: usize, y: usize, z: usize| z * n * n + y * n + x;
7054 let c000 = lut.get(idx(x0, y0, z0)).cloned().unwrap_or(Vec3::ZERO);
7055 let c100 = lut.get(idx(x0+1, y0, z0)).cloned().unwrap_or(Vec3::ZERO);
7056 let c010 = lut.get(idx(x0, y0+1, z0)).cloned().unwrap_or(Vec3::ZERO);
7057 let c110 = lut.get(idx(x0+1, y0+1, z0)).cloned().unwrap_or(Vec3::ZERO);
7058 let c001 = lut.get(idx(x0, y0, z0+1)).cloned().unwrap_or(Vec3::ZERO);
7059 let c101 = lut.get(idx(x0+1, y0, z0+1)).cloned().unwrap_or(Vec3::ZERO);
7060 let c011 = lut.get(idx(x0, y0+1, z0+1)).cloned().unwrap_or(Vec3::ZERO);
7061 let c111 = lut.get(idx(x0+1, y0+1, z0+1)).cloned().unwrap_or(Vec3::ZERO);
7062 let c00 = lerp_vec3(c000, c100, fx);
7063 let c01 = lerp_vec3(c001, c101, fx);
7064 let c10 = lerp_vec3(c010, c110, fx);
7065 let c11 = lerp_vec3(c011, c111, fx);
7066 let c0 = lerp_vec3(c00, c10, fy);
7067 let c1 = lerp_vec3(c01, c11, fy);
7068 lerp_vec3(c0, c1, fz)
7069 }
7070}
7071
7072#[derive(Debug, Clone)]
7077pub struct GPUCullingPassDesc {
7078 pub max_draw_calls: u32,
7079 pub output_draw_indirect: ResourceId,
7080 pub output_draw_count: ResourceId,
7081 pub input_bounding_spheres: ResourceId,
7082 pub input_draw_params: ResourceId,
7083 pub use_hi_z_occlusion: bool,
7084 pub use_frustum_culling: bool,
7085 pub hi_z_mip_levels: u32,
7086 pub tile_size: u32,
7087}
7088
7089impl GPUCullingPassDesc {
7090 pub fn default() -> Self {
7091 GPUCullingPassDesc {
7092 max_draw_calls: 65536,
7093 output_draw_indirect: ResourceId(700),
7094 output_draw_count: ResourceId(701),
7095 input_bounding_spheres: ResourceId(702),
7096 input_draw_params: ResourceId(703),
7097 use_hi_z_occlusion: true,
7098 use_frustum_culling: true,
7099 hi_z_mip_levels: 10,
7100 tile_size: 64,
7101 }
7102 }
7103 pub fn dispatch_size(&self) -> u32 { (self.max_draw_calls + 63) / 64 }
7104 pub fn draw_indirect_buffer_bytes(&self) -> u64 {
7105 self.max_draw_calls as u64 * 20
7107 }
7108 pub fn bounding_sphere_buffer_bytes(&self) -> u64 {
7109 self.max_draw_calls as u64 * 16
7111 }
7112}
7113
7114pub struct PassReorderer;
7119impl PassReorderer {
7120 pub fn reorder_for_cache(sorted: &[PassId], pass_map: &HashMap<PassId, PassNode>) -> Vec<PassId> {
7123 let mut remaining: Vec<PassId> = sorted.to_vec();
7124 let mut result: Vec<PassId> = Vec::with_capacity(remaining.len());
7125 let mut last_writes: HashSet<ResourceId> = HashSet::new();
7126 while !remaining.is_empty() {
7127 let best = remaining.iter().enumerate().max_by_key(|(_, pid)| {
7129 let pass = match pass_map.get(*pid) { Some(p) => p, None => return 0 };
7130 pass.reads.iter().filter(|r| last_writes.contains(*r)).count()
7131 });
7132 if let Some((idx, _)) = best {
7133 let pid = remaining.remove(idx);
7134 if let Some(pass) = pass_map.get(&pid) {
7135 last_writes.clear();
7136 for w in &pass.writes { last_writes.insert(*w); }
7137 }
7138 result.push(pid);
7139 } else {
7140 break;
7141 }
7142 }
7143 result.extend(remaining);
7144 result
7145 }
7146}
7147
7148pub fn bake_tonemapping_lut(width: u32, operator: ToneMappingOperator, exposure: f32, white_point: f32) -> Vec<Vec3> {
7153 let op = ToneMappingPassDesc {
7154 width, height: 1,
7155 output_format: TextureFormat::RGBA8UnormSrgb,
7156 output_sdr: ResourceId(0), input_hdr: ResourceId(0), input_bloom: ResourceId(0),
7157 operator, exposure, gamma: 2.2, white_point, color_lut_enabled: false, color_lut_size: 0,
7158 };
7159 let n = width as usize;
7160 let mut out = Vec::with_capacity(n);
7161 for i in 0..n {
7162 let t = i as f32 / (n - 1) as f32;
7163 let hdr = Vec3::splat(t * white_point);
7164 let mapped = op.apply_operator(hdr);
7165 let linear = op.gamma_correct(mapped);
7166 out.push(linear);
7167 }
7168 out
7169}
7170
7171#[derive(Debug, Clone)]
7176pub struct AdaptiveResolutionScaler {
7177 pub target_frame_time_ms: f32,
7178 pub min_scale: f32, pub max_scale: f32, pub current_scale: f32,
7181 pub increase_threshold: f32, pub decrease_threshold: f32, pub increase_rate: f32,
7184 pub decrease_rate: f32,
7185 pub history: VecDeque<f32>,
7186 pub history_length: usize,
7187}
7188
7189impl AdaptiveResolutionScaler {
7190 pub fn new(target_ms: f32) -> Self {
7191 AdaptiveResolutionScaler {
7192 target_frame_time_ms: target_ms,
7193 min_scale: 0.5,
7194 max_scale: 1.0,
7195 current_scale: 1.0,
7196 increase_threshold: 0.85,
7197 decrease_threshold: 1.05,
7198 increase_rate: 0.005,
7199 decrease_rate: 0.02,
7200 history: VecDeque::new(),
7201 history_length: 10,
7202 }
7203 }
7204
7205 pub fn update(&mut self, gpu_time_ms: f32) {
7206 if self.history.len() >= self.history_length { self.history.pop_front(); }
7207 self.history.push_back(gpu_time_ms);
7208 let avg: f32 = self.history.iter().sum::<f32>() / self.history.len() as f32;
7209 if avg < self.target_frame_time_ms * self.increase_threshold {
7210 self.current_scale = (self.current_scale + self.increase_rate).min(self.max_scale);
7211 } else if avg > self.target_frame_time_ms * self.decrease_threshold {
7212 self.current_scale = (self.current_scale - self.decrease_rate).max(self.min_scale);
7213 }
7214 }
7215
7216 pub fn scaled_resolution(&self, base_width: u32, base_height: u32) -> (u32, u32) {
7217 let w = ((base_width as f32 * self.current_scale) as u32).max(1);
7218 let h = ((base_height as f32 * self.current_scale) as u32).max(1);
7219 (w & !1, h & !1)
7221 }
7222
7223 pub fn upscale_needed(&self) -> bool { self.current_scale < 1.0 }
7224 pub fn quality_level(&self) -> &'static str {
7225 if self.current_scale >= 0.95 { "Ultra" }
7226 else if self.current_scale >= 0.75 { "Quality" }
7227 else if self.current_scale >= 0.60 { "Balanced" }
7228 else { "Performance" }
7229 }
7230}
7231
7232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7237pub enum UpscalerKind { Bilinear, Lanczos, FSR1, FSR2, DLSS, XeSS, CAS }
7238
7239#[derive(Debug, Clone)]
7240pub struct UpscalePassDesc {
7241 pub input_width: u32,
7242 pub input_height: u32,
7243 pub output_width: u32,
7244 pub output_height: u32,
7245 pub input_color: ResourceId,
7246 pub input_depth: ResourceId,
7247 pub input_velocity: ResourceId,
7248 pub output_color: ResourceId,
7249 pub kind: UpscalerKind,
7250 pub sharpness: f32,
7251 pub mip_bias: f32,
7252}
7253
7254impl UpscalePassDesc {
7255 pub fn fsr1(iw: u32, ih: u32, ow: u32, oh: u32, input: ResourceId, output: ResourceId) -> Self {
7256 UpscalePassDesc {
7257 input_width: iw, input_height: ih, output_width: ow, output_height: oh,
7258 input_color: input, input_depth: ResourceId(4), input_velocity: ResourceId(3),
7259 output_color: output,
7260 kind: UpscalerKind::FSR1,
7261 sharpness: 0.8,
7262 mip_bias: (iw as f32 / ow as f32).log2() - 1.0,
7263 }
7264 }
7265
7266 pub fn scale_factor(&self) -> f32 {
7267 self.input_width as f32 / self.output_width as f32
7268 }
7269
7270 pub fn easu_filter_sample(input_uv: Vec2, texel_size: Vec2, jitter: Vec2) -> [Vec2; 5] {
7272 let center = input_uv;
7274 [
7275 center,
7276 center + Vec2::new( texel_size.x, 0.0),
7277 center + Vec2::new(-texel_size.x, 0.0),
7278 center + Vec2::new(0.0, texel_size.y),
7279 center + Vec2::new(0.0, -texel_size.y),
7280 ]
7281 }
7282
7283 pub fn lanczos3_weight(x: f32) -> f32 {
7285 let a = 3.0f32;
7286 if x.abs() < 1e-6 { 1.0 }
7287 else if x.abs() < a {
7288 let px = std::f32::consts::PI * x;
7289 let pa = std::f32::consts::PI * x / a;
7290 a * px.sin() * pa.sin() / (px * px)
7291 } else {
7292 0.0
7293 }
7294 }
7295
7296 pub fn lanczos3_reconstruct(center: Vec3, samples: &[(Vec3, Vec2)], output_uv: Vec2) -> Vec3 {
7297 let mut sum = Vec3::ZERO;
7298 let mut weight_sum = 0.0f32;
7299 for (color, input_uv) in samples {
7300 let dx = (output_uv.x - input_uv.x);
7301 let dy = (output_uv.y - input_uv.y);
7302 let w = Self::lanczos3_weight(dx) * Self::lanczos3_weight(dy);
7303 sum += *color * w;
7304 weight_sum += w;
7305 }
7306 if weight_sum.abs() < 1e-6 { center } else { sum / weight_sum }
7307 }
7308}
7309
7310#[cfg(test)]
7315mod integration_tests {
7316 use super::*;
7317
7318 #[test]
7319 fn test_vxgi_memory() {
7320 let vxgi = VXGIPassDesc::default();
7321 let mem = vxgi.grid_memory_bytes();
7322 assert!(mem > 0);
7323 let mips = vxgi.mip_levels();
7324 assert_eq!(mips, 9); }
7326
7327 #[test]
7328 fn test_ddgi_probe_positions() {
7329 let ddgi = DDGIPassDesc::default();
7330 let p = ddgi.probe_world_pos(0, 0, 0);
7331 assert_eq!(p, ddgi.probe_origin);
7332 let p2 = ddgi.probe_world_pos(1, 0, 0);
7333 assert!((p2.x - p.x - ddgi.probe_spacing).abs() < 1e-5);
7334 }
7335
7336 #[test]
7337 fn test_ddgi_trilinear_weights() {
7338 let weights = DDGIPassDesc::trilinear_weights(Vec3::splat(0.5));
7339 let sum: f32 = weights.iter().sum();
7340 assert!((sum - 1.0).abs() < 1e-4);
7341 }
7342
7343 #[test]
7344 fn test_sss_kernel_normalization() {
7345 let sss = SSSPassDesc::default(1920, 1080);
7346 let kernel = sss.separable_kernel();
7347 assert!(!kernel.is_empty());
7348 let sum_w: f32 = kernel.iter().map(|k| k.y).sum();
7349 assert!((sum_w - 1.0).abs() < 1e-4, "kernel weights should sum to 1, got {}", sum_w);
7350 }
7351
7352 #[test]
7353 fn test_sss_preintegrated_lut() {
7354 let v = SSSPassDesc::preintegrated_lut_value(1.0, 0.0);
7355 assert!(v.x >= 0.0 && v.x <= 1.0);
7356 assert!(v.y >= 0.0 && v.y <= 1.0);
7357 }
7358
7359 #[test]
7360 fn test_burley_diffusion_profile() {
7361 let v = SSSPassDesc::burley_diffusion_profile(0.1, 1.0);
7362 assert!(v > 0.0);
7363 let v2 = SSSPassDesc::burley_diffusion_profile(2.0, 1.0);
7364 assert!(v > v2, "profile should fall off with distance");
7365 }
7366
7367 #[test]
7368 fn test_aces_lut_baking_small() {
7369 let lut = ACESTransform::bake_lut(4);
7370 assert_eq!(lut.len(), 4 * 4 * 4);
7371 for c in &lut {
7372 assert!(c.x >= 0.0 && c.x <= 1.0);
7373 }
7374 }
7375
7376 #[test]
7377 fn test_aces_lut_sampling() {
7378 let lut = ACESTransform::bake_lut(32);
7379 let c = ACESTransform::sample_lut(&lut, 32, Vec3::splat(0.5));
7380 assert!(c.length() >= 0.0);
7381 }
7382
7383 #[test]
7384 fn test_adaptive_resolution_scale_up() {
7385 let mut scaler = AdaptiveResolutionScaler::new(16.67);
7386 scaler.current_scale = 0.7;
7387 for _ in 0..20 { scaler.update(10.0); } assert!(scaler.current_scale > 0.7);
7389 }
7390
7391 #[test]
7392 fn test_adaptive_resolution_scale_down() {
7393 let mut scaler = AdaptiveResolutionScaler::new(16.67);
7394 for _ in 0..20 { scaler.update(25.0); } assert!(scaler.current_scale < 1.0);
7396 }
7397
7398 #[test]
7399 fn test_lanczos3_weight() {
7400 let w0 = UpscalePassDesc::lanczos3_weight(0.0);
7401 assert!((w0 - 1.0).abs() < 1e-4);
7402 let w_out = UpscalePassDesc::lanczos3_weight(3.5);
7403 assert_eq!(w_out, 0.0);
7404 }
7405
7406 #[test]
7407 fn test_tonemapping_lut_bake() {
7408 let lut = bake_tonemapping_lut(256, ToneMappingOperator::ACES, 1.0, 4.0);
7409 assert_eq!(lut.len(), 256);
7410 assert!(lut.iter().all(|c| c.x >= 0.0 && c.x <= 1.001));
7411 }
7412
7413 #[test]
7414 fn test_gpu_culling_pass_sizes() {
7415 let cull = GPUCullingPassDesc::default();
7416 assert!(cull.draw_indirect_buffer_bytes() > 0);
7417 assert!(cull.bounding_sphere_buffer_bytes() > 0);
7418 }
7419
7420 #[test]
7421 fn test_pass_reorder_for_cache() {
7422 let editor = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7423 let sorted: Vec<PassId> = editor.passes.keys().cloned().collect();
7424 let reordered = PassReorderer::reorder_for_cache(&sorted, &editor.passes);
7425 assert_eq!(reordered.len(), sorted.len());
7426 }
7427
7428 #[test]
7429 fn test_hi_z_trace() {
7430 let result = HiZTracer::trace(Vec2::new(0.5, 0.5), Vec2::new(0.01, 0.0), 0.5, 64, 8);
7431 let _ = result;
7433 }
7434
7435 #[test]
7436 fn test_editor_history() {
7437 let mut hist = EditorHistory::new(32);
7438 assert!(!hist.can_undo());
7439 hist.push(EditorAction::AddPass(PassId(0), "Test".to_owned()));
7440 assert!(hist.can_undo());
7441 let act = hist.pop_undo();
7442 assert!(act.is_some());
7443 assert!(!hist.can_undo());
7444 }
7445
7446 #[test]
7447 fn test_pass_group_bounds() {
7448 let mut group = PassGroup::new(0, "Deferred", vec![PassId(0), PassId(1)], Vec4::ONE);
7449 let mut positions = HashMap::new();
7450 positions.insert(PassId(0), Vec2::new(10.0, 20.0));
7451 positions.insert(PassId(1), Vec2::new(300.0, 200.0));
7452 let mut sizes = HashMap::new();
7453 sizes.insert(PassId(0), Vec2::new(200.0, 80.0));
7454 sizes.insert(PassId(1), Vec2::new(200.0, 80.0));
7455 group.compute_bounds(&positions, &sizes);
7456 assert!(group.contains_point(Vec2::new(100.0, 100.0)));
7457 assert!(!group.contains_point(Vec2::new(-100.0, -100.0)));
7458 }
7459
7460 #[test]
7461 fn test_graph_diff_empty() {
7462 let ed = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7463 let diffs = diff_render_graphs(&ed, &ed);
7464 let adds_removes: Vec<_> = diffs.iter().filter(|d| matches!(d, GraphDiff::PassAdded(_, _) | GraphDiff::PassRemoved(_, _))).collect();
7466 assert!(adds_removes.is_empty());
7467 assert!(diffs.is_empty(), "{:?}", diffs);
7469 }
7470
7471 #[test]
7472 fn test_hot_reload_no_change() {
7473 let ed = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7474 let ed2 = RenderGraphEditor::build_standard_deferred_pipeline(1920, 1080);
7475 let mut mgr = HotReloadManager::new(ed);
7476 mgr.stage_reload(ed2);
7477 let changed = mgr.apply_reload_if_pending(1);
7478 assert!(!changed); }
7480
7481 #[test]
7482 fn test_frame_debugger() {
7483 let mut dbg = FrameDebugger::new(8);
7484 dbg.begin_capture(0, 0.0);
7485 dbg.record_pass_timing(PassId(0), 2.5);
7486 dbg.record_pass_timing(PassId(1), 4.0);
7487 dbg.record_transition(PassId(0), ResourceId(0), ImageLayout::Undefined, ImageLayout::ColorAttachmentOptimal);
7488 dbg.end_capture();
7489 let cap = dbg.latest().unwrap();
7490 assert!((cap.total_gpu_ms() - 6.5).abs() < 1e-4);
7491 assert_eq!(cap.resource_transitions.len(), 1);
7492 }
7493
7494 #[test]
7495 fn test_critical_path() {
7496 let mut timings: HashMap<PassId, f64> = HashMap::new();
7497 timings.insert(PassId(0), 1.0);
7498 timings.insert(PassId(1), 4.0);
7499 timings.insert(PassId(2), 2.0);
7500 let mut edges: HashMap<PassId, Vec<PassId>> = HashMap::new();
7501 edges.insert(PassId(0), vec![PassId(1)]);
7502 edges.insert(PassId(1), vec![PassId(2)]);
7503 edges.insert(PassId(2), vec![]);
7504 let sorted = vec![PassId(0), PassId(1), PassId(2)];
7505 let (path, total) = CriticalPathAnalyzer::find_critical_path(&sorted, &timings, &edges);
7506 assert!((total - 7.0).abs() < 1e-4, "total should be 7ms, got {}", total);
7507 }
7508
7509 #[test]
7510 fn test_bandwidth_profiler() {
7511 let mut profiler = BandwidthProfiler::new();
7512 profiler.record(PassId(0), ResourceId(0), true, 8 * 1920 * 1080, AccessFlags::COLOR_ATTACHMENT_WRITE, ImageLayout::ColorAttachmentOptimal);
7513 profiler.record(PassId(1), ResourceId(0), false, 8 * 1920 * 1080, AccessFlags::SHADER_READ, ImageLayout::ShaderReadOnlyOptimal);
7514 profiler.compute_totals();
7515 let total = profiler.total_bandwidth_mb();
7516 assert!(total > 0.0);
7517 let top = profiler.top_bandwidth_resources(3);
7518 assert!(!top.is_empty());
7519 }
7520}
7521
7522pub struct OcclusionCuller {
7527 pub hi_z_width: u32,
7528 pub hi_z_height: u32,
7529 pub mip_levels: u32,
7530}
7531
7532impl OcclusionCuller {
7533 pub fn new(width: u32, height: u32) -> Self {
7534 let mips = compute_mip_count(width, height);
7535 OcclusionCuller { hi_z_width: width, hi_z_height: height, mip_levels: mips }
7536 }
7537
7538 pub fn is_occluded_hiz(
7541 &self,
7542 aabb_min_uv: Vec2,
7543 aabb_max_uv: Vec2,
7544 nearest_depth: f32,
7545 hi_z_mips: &[Vec<f32>], ) -> bool {
7547 let span = aabb_max_uv - aabb_min_uv;
7548 let max_span = span.x.max(span.y);
7549 let mip = ((max_span * self.hi_z_width.max(self.hi_z_height) as f32).log2() as u32).min(self.mip_levels - 1);
7551 let mip_w = (self.hi_z_width >> mip).max(1) as f32;
7552 let mip_h = (self.hi_z_height >> mip).max(1) as f32;
7553 let uvs = [
7555 aabb_min_uv,
7556 Vec2::new(aabb_max_uv.x, aabb_min_uv.y),
7557 Vec2::new(aabb_min_uv.x, aabb_max_uv.y),
7558 aabb_max_uv,
7559 ];
7560 let mip_data = match hi_z_mips.get(mip as usize) { Some(d) => d, None => return false };
7561 let mut max_hi_z_depth = 0.0f32;
7562 for uv in &uvs {
7563 let ix = (uv.x * mip_w) as usize;
7564 let iy = (uv.y * mip_h) as usize;
7565 let idx = iy * mip_w as usize + ix;
7566 let d = mip_data.get(idx).cloned().unwrap_or(1.0);
7567 max_hi_z_depth = max_hi_z_depth.max(d);
7568 }
7569 nearest_depth > max_hi_z_depth
7571 }
7572
7573 pub fn build_hi_z_pyramid(depth_buffer: &[f32], width: u32, height: u32) -> Vec<Vec<f32>> {
7575 let mips_count = compute_mip_count(width, height) as usize;
7576 let mut mips: Vec<Vec<f32>> = Vec::with_capacity(mips_count);
7577 mips.push(depth_buffer.to_vec());
7579 let mut prev_w = width;
7580 let mut prev_h = height;
7581 for _ in 1..mips_count {
7582 let w = (prev_w / 2).max(1);
7583 let h = (prev_h / 2).max(1);
7584 let mut mip_data = vec![0.0f32; (w * h) as usize];
7585 let prev_data = mips.last().unwrap();
7586 for y in 0..h {
7587 for x in 0..w {
7588 let px = x * 2;
7589 let py = y * 2;
7590 let d00 = *prev_data.get((py * prev_w + px) as usize).unwrap_or(&1.0);
7591 let d10 = *prev_data.get((py * prev_w + (px+1).min(prev_w-1)) as usize).unwrap_or(&1.0);
7592 let d01 = *prev_data.get(((py+1).min(prev_h-1) * prev_w + px) as usize).unwrap_or(&1.0);
7593 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);
7594 mip_data[(y * w + x) as usize] = d00.max(d10).max(d01).max(d11);
7596 }
7597 }
7598 mips.push(mip_data);
7599 prev_w = w;
7600 prev_h = h;
7601 }
7602 mips
7603 }
7604}
7605
7606#[derive(Debug, Clone)]
7611pub struct AutoExposurePassDesc {
7612 pub output_average_luminance: ResourceId,
7613 pub input_hdr: ResourceId,
7614 pub min_log_luminance: f32, pub max_log_luminance: f32, pub adaptation_speed_up: f32,
7617 pub adaptation_speed_down: f32,
7618 pub histogram_bins: u32,
7619 pub metered_area: Vec4, pub eye_adaptation_type: EyeAdaptation,
7621}
7622
7623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7624pub enum EyeAdaptation { Histogram, AverageLuminance }
7625
7626impl AutoExposurePassDesc {
7627 pub fn default() -> Self {
7628 AutoExposurePassDesc {
7629 output_average_luminance: ResourceId(800),
7630 input_hdr: ResourceId(10),
7631 min_log_luminance: -8.0,
7632 max_log_luminance: 8.0,
7633 adaptation_speed_up: 3.0,
7634 adaptation_speed_down: 1.0,
7635 histogram_bins: 256,
7636 metered_area: Vec4::new(0.1, 0.1, 0.8, 0.8),
7637 eye_adaptation_type: EyeAdaptation::Histogram,
7638 }
7639 }
7640
7641 pub fn luminance_to_bin(&self, lum: f32) -> u32 {
7643 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);
7645 (normalized * self.histogram_bins as f32) as u32
7646 }
7647
7648 pub fn ev100_from_average_luminance(avg_lum: f32) -> f32 {
7650 (avg_lum * 100.0 / 12.5).log2()
7651 }
7652
7653 pub fn adapt_exposure(&self, current_ev: f32, target_ev: f32, delta_time: f32) -> f32 {
7655 let speed = if target_ev > current_ev { self.adaptation_speed_up } else { self.adaptation_speed_down };
7656 let factor = 1.0 - (-speed * delta_time).exp();
7657 current_ev + (target_ev - current_ev) * factor
7658 }
7659
7660 pub fn percentile_from_histogram(histogram: &[u32], percentile: f32) -> f32 {
7662 let total: u64 = histogram.iter().map(|&c| c as u64).sum();
7663 if total == 0 { return 0.0; }
7664 let target_count = (total as f32 * percentile) as u64;
7665 let mut accum = 0u64;
7666 for (i, &count) in histogram.iter().enumerate() {
7667 accum += count as u64;
7668 if accum >= target_count {
7669 return i as f32 / histogram.len() as f32;
7670 }
7671 }
7672 1.0
7673 }
7674}
7675
7676#[derive(Debug, Clone)]
7681pub struct LensFlarePassDesc {
7682 pub width: u32,
7683 pub height: u32,
7684 pub output_format: TextureFormat,
7685 pub output_flare: ResourceId,
7686 pub input_hdr: ResourceId,
7687 pub threshold: f32,
7688 pub intensity: f32,
7689 pub ghost_count: u32,
7690 pub ghost_dispersal: f32,
7691 pub ghost_threshold: f32,
7692 pub halo_width: f32,
7693 pub halo_intensity: f32,
7694 pub distortion: f32,
7695 pub use_lens_dirt: bool,
7696 pub use_star_burst: bool,
7697 pub star_burst_samples: u32,
7698}
7699
7700impl LensFlarePassDesc {
7701 pub fn default(width: u32, height: u32) -> Self {
7702 LensFlarePassDesc {
7703 width, height,
7704 output_format: TextureFormat::RGBA16Float,
7705 output_flare: ResourceId(900),
7706 input_hdr: ResourceId(10),
7707 threshold: 10.0,
7708 intensity: 0.5,
7709 ghost_count: 8,
7710 ghost_dispersal: 0.35,
7711 ghost_threshold: 50.0,
7712 halo_width: 0.5,
7713 halo_intensity: 0.8,
7714 distortion: 5.0,
7715 use_lens_dirt: true,
7716 use_star_burst: true,
7717 star_burst_samples: 6,
7718 }
7719 }
7720
7721 pub fn ghost_position(flare_uv: Vec2, ghost_index: u32, dispersal: f32) -> Vec2 {
7723 let lens_center = Vec2::splat(0.5);
7724 let flare_dir = flare_uv - lens_center;
7725 let offset = flare_dir * ghost_index as f32 * dispersal;
7726 lens_center + offset
7727 }
7728
7729 pub fn star_burst_direction(sample: u32, total: u32) -> Vec2 {
7731 let angle = (sample as f32 / total as f32) * std::f32::consts::TAU;
7732 Vec2::new(angle.cos(), angle.sin())
7733 }
7734
7735 pub fn chromatic_distortion_offset(uv: Vec2, channel: u32, strength: f32) -> Vec2 {
7737 let center = Vec2::splat(0.5);
7738 let d = uv - center;
7739 let scale = 1.0 + strength * 0.01 * (channel as f32 - 1.0);
7740 center + d * scale
7741 }
7742}
7743
7744#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7749pub enum PinKind { Input, Output }
7750
7751#[derive(Debug, Clone)]
7752pub struct NodePin {
7753 pub pass_id: PassId,
7754 pub resource_id: ResourceId,
7755 pub kind: PinKind,
7756 pub index: u32,
7757 pub label: String,
7758 pub format: TextureFormat,
7759}
7760
7761impl NodePin {
7762 pub fn input(pass_id: PassId, resource_id: ResourceId, index: u32, label: &str, format: TextureFormat) -> Self {
7763 NodePin { pass_id, resource_id, kind: PinKind::Input, index, label: label.to_owned(), format }
7764 }
7765 pub fn output(pass_id: PassId, resource_id: ResourceId, index: u32, label: &str, format: TextureFormat) -> Self {
7766 NodePin { pass_id, resource_id, kind: PinKind::Output, index, label: label.to_owned(), format }
7767 }
7768 pub fn is_compatible_with(&self, other: &NodePin) -> bool {
7769 self.kind != other.kind && formats_compatible(self.format, other.format)
7770 }
7771}
7772
7773#[derive(Debug, Clone)]
7774pub struct NodeConnection {
7775 pub src: NodePin,
7776 pub dst: NodePin,
7777}
7778
7779impl NodeConnection {
7780 pub fn new(src: NodePin, dst: NodePin) -> Option<Self> {
7781 if src.is_compatible_with(&dst) { Some(NodeConnection { src, dst }) } else { None }
7782 }
7783}
7784
7785pub fn encode_rgbe(hdr: Vec3) -> [u8; 4] {
7791 let max_c = hdr.x.max(hdr.y).max(hdr.z);
7792 if max_c < 1e-32 {
7793 return [0, 0, 0, 0];
7794 }
7795 let exp = max_c.log2().ceil() as i32 + 128;
7796 let scale = 256.0 / (2.0f32.powi(exp - 128));
7797 [
7798 (hdr.x * scale) as u8,
7799 (hdr.y * scale) as u8,
7800 (hdr.z * scale) as u8,
7801 exp.clamp(0, 255) as u8,
7802 ]
7803}
7804
7805pub fn decode_rgbe(rgbe: [u8; 4]) -> Vec3 {
7806 if rgbe[3] == 0 { return Vec3::ZERO; }
7807 let exp = rgbe[3] as i32 - 128;
7808 let scale = 2.0f32.powi(exp) / 256.0;
7809 Vec3::new(rgbe[0] as f32 * scale, rgbe[1] as f32 * scale, rgbe[2] as f32 * scale)
7810}
7811
7812pub fn encode_rgb9e5(hdr: Vec3) -> u32 {
7814 const N: i32 = 9;
7815 const B: i32 = 15;
7816 const E_MAX: i32 = 31;
7817 let max_c = hdr.x.max(hdr.y).max(hdr.z).max(0.0);
7818 let shared_exp_f = (max_c / (1 << (N-1)) as f32 * (1 << B) as f32).log2().ceil() as i32;
7819 let exp = shared_exp_f.clamp(-B, E_MAX - N);
7820 let scale = 1.0 / 2.0f32.powi(exp - N + 1);
7821 let r = (hdr.x * scale).round() as u32 & ((1 << N) - 1);
7822 let g = (hdr.y * scale).round() as u32 & ((1 << N) - 1);
7823 let b = (hdr.z * scale).round() as u32 & ((1 << N) - 1);
7824 let e = (exp + B + 1).clamp(0, 31) as u32;
7825 (e << 27) | (b << 18) | (g << 9) | r
7826}
7827
7828pub fn sphere_screen_size_pixels(center_vs: Vec3, radius: f32, proj: Mat4, screen_width: u32) -> f32 {
7830 let d = center_vs.length();
7831 if d < radius { return screen_width as f32; }
7832 let proj_scale = proj.col(0).x; (proj_scale * radius / (d - radius)) * screen_width as f32 * 0.5
7834}
7835
7836pub fn linear_depth_to_ndc(linear: f32, near: f32, far: f32) -> f32 {
7838 let a = (far + near) / (far - near);
7842 let b = -2.0 * far * near / (far - near);
7843 a + b / linear
7844}
7845
7846pub fn compute_texture_lod(ddx: Vec2, ddy: Vec2) -> f32 {
7848 let len_x = ddx.length_squared();
7849 let len_y = ddy.length_squared();
7850 0.5 * len_x.max(len_y).log2()
7851}
7852
7853pub fn srgb_eotf(encoded: f32) -> f32 {
7855 if encoded <= 0.04045 { encoded / 12.92 } else { ((encoded + 0.055) / 1.055).powf(2.4) }
7856}
7857
7858pub fn srgb_oetf(linear: f32) -> f32 {
7860 if linear <= 0.0031308 { linear * 12.92 } else { 1.055 * linear.powf(1.0 / 2.4) - 0.055 }
7861}
7862
7863pub fn rec709_to_xyz(c: Vec3) -> Vec3 {
7865 Vec3::new(
7866 0.4124564 * c.x + 0.3575761 * c.y + 0.1804375 * c.z,
7867 0.2126729 * c.x + 0.7151522 * c.y + 0.0721750 * c.z,
7868 0.0193339 * c.x + 0.1191920 * c.y + 0.9503041 * c.z,
7869 )
7870}
7871
7872pub fn xyz_to_rec709(c: Vec3) -> Vec3 {
7873 Vec3::new(
7874 3.2404542 * c.x - 1.5371385 * c.y - 0.4985314 * c.z,
7875 -0.9692660 * c.x + 1.8760108 * c.y + 0.0415560 * c.z,
7876 0.0556434 * c.x - 0.2040259 * c.y + 1.0572252 * c.z,
7877 )
7878}
7879
7880pub fn xyz_to_aces_ap0(c: Vec3) -> Vec3 {
7881 Vec3::new(
7882 1.0498110175 * c.x + 0.0000000000 * c.y - 0.0000974845 * c.z,
7883 -0.4959030231 * c.x + 1.3733130458 * c.y + 0.0982400361 * c.z,
7884 0.0000000000 * c.x + 0.0000000000 * c.y + 0.9912520182 * c.z,
7885 )
7886}
7887
7888trait Vec3Ext { fn powf(self, exp: f32) -> Vec3; fn sqrt(self) -> Vec3; }
7890impl Vec3Ext for Vec3 {
7891 fn powf(self, exp: f32) -> Vec3 { Vec3::new(self.x.powf(exp), self.y.powf(exp), self.z.powf(exp)) }
7892 fn sqrt(self) -> Vec3 { Vec3::new(self.x.sqrt(), self.y.sqrt(), self.z.sqrt()) }
7893}
7894
7895#[allow(dead_code)]
7897fn vec4_div(v: Vec4, rhs: f32) -> Vec4 { Vec4::new(v.x / rhs, v.y / rhs, v.z / rhs, v.w / rhs) }
7898
7899#[cfg(test)]
7904mod util_tests {
7905 use super::*;
7906
7907 #[test]
7908 fn test_rgbe_round_trip() {
7909 let hdr = Vec3::new(1.5, 2.3, 0.7);
7910 let enc = encode_rgbe(hdr);
7911 let dec = decode_rgbe(enc);
7912 let err = (hdr - dec).length();
7913 assert!(err < 0.05, "RGBE round-trip error too large: {}", err);
7914 }
7915
7916 #[test]
7917 fn test_rgbe_black() {
7918 let enc = encode_rgbe(Vec3::ZERO);
7919 assert_eq!(enc, [0, 0, 0, 0]);
7920 let dec = decode_rgbe(enc);
7921 assert_eq!(dec, Vec3::ZERO);
7922 }
7923
7924 #[test]
7925 fn test_srgb_eotf_inverse() {
7926 let x = 0.5f32;
7927 let linear = srgb_eotf(x);
7928 let back = srgb_oetf(linear);
7929 assert!((back - x).abs() < 1e-4);
7930 }
7931
7932 #[test]
7933 fn test_linear_depth_to_ndc() {
7934 let ndc = linear_depth_to_ndc(1.0, 0.1, 100.0);
7935 assert!(ndc >= -1.0 && ndc <= 1.0);
7936 assert!((linear_depth_to_ndc(0.1, 0.1, 100.0) + 1.0).abs() < 1e-5);
7937 assert!((linear_depth_to_ndc(100.0, 0.1, 100.0) - 1.0).abs() < 1e-5);
7938 }
7939
7940 #[test]
7941 fn test_sphere_screen_size() {
7942 let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 100.0);
7943 let size = sphere_screen_size_pixels(Vec3::new(0.0, 0.0, -10.0), 1.0, proj, 1920);
7944 assert!(size > 0.0);
7945 }
7946
7947 #[test]
7948 fn test_auto_exposure_adapt() {
7949 let ae = AutoExposurePassDesc::default();
7950 let ev = ae.adapt_exposure(0.0, 5.0, 0.016);
7951 assert!(ev > 0.0 && ev < 5.0);
7952 }
7953
7954 #[test]
7955 fn test_lens_flare_ghost_positions() {
7956 let pos = LensFlarePassDesc::ghost_position(Vec2::new(0.8, 0.5), 1, 0.35);
7957 assert!(pos.x >= 0.0 && pos.x <= 1.0 || pos.x < 0.0 || pos.x > 1.0);
7958 }
7959
7960 #[test]
7961 fn test_hi_z_pyramid_build() {
7962 let depth: Vec<f32> = vec![0.5; 64 * 64];
7963 let pyramid = OcclusionCuller::build_hi_z_pyramid(&depth, 64, 64);
7964 assert!(pyramid.len() > 1);
7965 assert_eq!(pyramid[0].len(), 64 * 64);
7966 assert_eq!(pyramid[1].len(), 32 * 32);
7967 }
7968
7969 #[test]
7970 fn test_node_pin_compatibility() {
7971 let p1 = NodePin::output(PassId(0), ResourceId(0), 0, "HDR", TextureFormat::RGBA16Float);
7972 let p2 = NodePin::input(PassId(1), ResourceId(0), 0, "HDR", TextureFormat::RGBA16Float);
7973 assert!(p1.is_compatible_with(&p2));
7974 let p3 = NodePin::input(PassId(1), ResourceId(1), 0, "Depth", TextureFormat::Depth32Float);
7975 assert!(!p1.is_compatible_with(&p3));
7976 }
7977
7978 #[test]
7979 fn test_ev100_from_luminance() {
7980 let ev = AutoExposurePassDesc::ev100_from_average_luminance(1.0);
7981 assert!((ev - 3.0).abs() < 0.1, "ev should be ~3 for 1 cd/m^2 avg luminance");
7982 }
7983
7984 #[test]
7985 fn test_rec709_xyz_round_trip() {
7986 let c = Vec3::new(0.2, 0.5, 0.8);
7987 let xyz = rec709_to_xyz(c);
7988 let back = xyz_to_rec709(xyz);
7989 assert!((c - back).length() < 1e-4);
7990 }
7991
7992 #[test]
7993 fn test_compute_texture_lod() {
7994 let lod = compute_texture_lod(Vec2::new(0.01, 0.0), Vec2::new(0.0, 0.01));
7995 assert!(lod < 0.0 || lod >= 0.0); }
7997}
7998
7999