1
2use glam::{Vec2, Vec3, Vec4, Mat4};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum PixelFormat {
13 R8Unorm, R8Snorm, R8Uint, R8Sint,
14 Rg8Unorm, Rg8Snorm,
15 Rgba8Unorm, Rgba8Srgb, Rgba8Snorm,
16 R16Float, Rg16Float, Rgba16Float,
17 R32Float, Rg32Float, Rgba32Float,
18 R11G11B10Float,
19 Rgb9E5Float,
20 Bgra8Unorm, Bgra8Srgb,
21 Depth16, Depth24, Depth32Float,
22 Depth24Stencil8, Depth32FloatStencil8,
23 Bc1Unorm, Bc1Srgb, Bc3Unorm, Bc3Srgb, Bc5Unorm, Bc6HUfloat, Bc7Unorm, Bc7Srgb,
24}
25
26impl PixelFormat {
27 pub fn is_depth(self) -> bool {
28 matches!(self, PixelFormat::Depth16 | PixelFormat::Depth24 | PixelFormat::Depth32Float | PixelFormat::Depth24Stencil8 | PixelFormat::Depth32FloatStencil8)
29 }
30 pub fn is_compressed(self) -> bool {
31 matches!(self, PixelFormat::Bc1Unorm | PixelFormat::Bc1Srgb | PixelFormat::Bc3Unorm | PixelFormat::Bc3Srgb | PixelFormat::Bc5Unorm | PixelFormat::Bc6HUfloat | PixelFormat::Bc7Unorm | PixelFormat::Bc7Srgb)
32 }
33 pub fn bytes_per_pixel(self) -> f32 {
34 match self {
35 PixelFormat::R8Unorm | PixelFormat::R8Snorm | PixelFormat::R8Uint | PixelFormat::R8Sint => 1.0,
36 PixelFormat::Rg8Unorm | PixelFormat::Rg8Snorm => 2.0,
37 PixelFormat::Rgba8Unorm | PixelFormat::Rgba8Srgb | PixelFormat::Rgba8Snorm
38 | PixelFormat::Bgra8Unorm | PixelFormat::Bgra8Srgb => 4.0,
39 PixelFormat::R16Float => 2.0,
40 PixelFormat::Rg16Float => 4.0,
41 PixelFormat::Rgba16Float => 8.0,
42 PixelFormat::R32Float => 4.0,
43 PixelFormat::Rg32Float => 8.0,
44 PixelFormat::Rgba32Float => 16.0,
45 PixelFormat::R11G11B10Float | PixelFormat::Rgb9E5Float => 4.0,
46 PixelFormat::Depth16 => 2.0,
47 PixelFormat::Depth24 | PixelFormat::Depth32Float => 4.0,
48 PixelFormat::Depth24Stencil8 | PixelFormat::Depth32FloatStencil8 => 8.0,
49 PixelFormat::Bc1Unorm | PixelFormat::Bc1Srgb => 0.5,
50 PixelFormat::Bc3Unorm | PixelFormat::Bc3Srgb | PixelFormat::Bc5Unorm | PixelFormat::Bc6HUfloat | PixelFormat::Bc7Unorm | PixelFormat::Bc7Srgb => 1.0,
51 }
52 }
53 pub fn label(self) -> &'static str {
54 match self {
55 PixelFormat::Rgba8Unorm => "RGBA8 Unorm",
56 PixelFormat::Rgba16Float => "RGBA16F",
57 PixelFormat::Rgba32Float => "RGBA32F",
58 PixelFormat::R11G11B10Float => "R11G11B10F",
59 PixelFormat::Depth32Float => "Depth32F",
60 PixelFormat::Depth24Stencil8 => "D24S8",
61 _ => "Other",
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum TextureDim { D1, D2, D3, Cube, Array2D, ArrayCube }
68
69#[derive(Debug, Clone)]
70pub struct TextureDesc {
71 pub name: String,
72 pub width: u32,
73 pub height: u32,
74 pub depth: u32,
75 pub array_layers: u32,
76 pub mip_levels: u32,
77 pub sample_count: u32,
78 pub format: PixelFormat,
79 pub dim: TextureDim,
80 pub is_render_target: bool,
81 pub is_depth_stencil: bool,
82 pub is_uav: bool,
83 pub persistent: bool,
84}
85
86impl TextureDesc {
87 pub fn render_target_2d(name: impl Into<String>, w: u32, h: u32, fmt: PixelFormat) -> Self {
88 Self {
89 name: name.into(), width: w, height: h, depth: 1, array_layers: 1,
90 mip_levels: 1, sample_count: 1, format: fmt, dim: TextureDim::D2,
91 is_render_target: true, is_depth_stencil: false, is_uav: false, persistent: false,
92 }
93 }
94 pub fn depth_target(name: impl Into<String>, w: u32, h: u32) -> Self {
95 let mut d = Self::render_target_2d(name, w, h, PixelFormat::Depth32Float);
96 d.is_depth_stencil = true;
97 d.is_render_target = false;
98 d
99 }
100 pub fn memory_bytes(&self) -> u64 {
101 (self.width as f32 * self.height as f32 * self.depth as f32 *
102 self.array_layers as f32 * self.format.bytes_per_pixel()) as u64
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq)]
111pub enum BufferUsage { Vertex, Index, Uniform, Storage, Indirect, Staging }
112
113#[derive(Debug, Clone)]
114pub struct BufferDesc {
115 pub name: String,
116 pub size_bytes: u64,
117 pub usage: BufferUsage,
118 pub cpu_writable: bool,
119 pub cpu_readable: bool,
120 pub stride: u32,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq)]
128pub enum LoadOp { Clear, Load, DontCare }
129#[derive(Debug, Clone, Copy, PartialEq)]
130pub enum StoreOp { Store, DontCare }
131
132#[derive(Debug, Clone)]
133pub struct ColorAttachment {
134 pub texture_name: String,
135 pub mip_level: u32,
136 pub array_layer: u32,
137 pub load: LoadOp,
138 pub store: StoreOp,
139 pub clear_value: Vec4,
140}
141
142#[derive(Debug, Clone)]
143pub struct DepthAttachment {
144 pub texture_name: String,
145 pub depth_load: LoadOp,
146 pub depth_store: StoreOp,
147 pub stencil_load: LoadOp,
148 pub stencil_store: StoreOp,
149 pub clear_depth: f32,
150 pub clear_stencil: u32,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq)]
158pub enum CompareFunc { Never, Less, Equal, LessEqual, Greater, NotEqual, GreaterEqual, Always }
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum BlendFactor { Zero, One, SrcColor, SrcAlpha, DstColor, DstAlpha, OneMinusSrcAlpha, OneMinusSrcColor }
161#[derive(Debug, Clone, Copy, PartialEq)]
162pub enum BlendOp { Add, Sub, RevSub, Min, Max }
163#[derive(Debug, Clone, Copy, PartialEq)]
164pub enum CullMode { None, Front, Back }
165#[derive(Debug, Clone, Copy, PartialEq)]
166pub enum FillMode { Solid, Wireframe, Point }
167#[derive(Debug, Clone, Copy, PartialEq)]
168pub enum PrimitiveTopology { TriangleList, TriangleStrip, LineList, LineStrip, PointList, TriangleFan }
169
170#[derive(Debug, Clone)]
171pub struct DepthStencilState {
172 pub depth_test: bool,
173 pub depth_write: bool,
174 pub depth_func: CompareFunc,
175 pub stencil_test: bool,
176 pub stencil_read_mask: u8,
177 pub stencil_write_mask: u8,
178}
179
180impl Default for DepthStencilState {
181 fn default() -> Self {
182 Self {
183 depth_test: true,
184 depth_write: true,
185 depth_func: CompareFunc::Less,
186 stencil_test: false,
187 stencil_read_mask: 0xFF,
188 stencil_write_mask: 0xFF,
189 }
190 }
191}
192
193#[derive(Debug, Clone)]
194pub struct BlendState {
195 pub enabled: bool,
196 pub src_color: BlendFactor,
197 pub dst_color: BlendFactor,
198 pub color_op: BlendOp,
199 pub src_alpha: BlendFactor,
200 pub dst_alpha: BlendFactor,
201 pub alpha_op: BlendOp,
202 pub write_mask: u8, }
204
205impl BlendState {
206 pub fn opaque() -> Self {
207 Self {
208 enabled: false,
209 src_color: BlendFactor::One, dst_color: BlendFactor::Zero, color_op: BlendOp::Add,
210 src_alpha: BlendFactor::One, dst_alpha: BlendFactor::Zero, alpha_op: BlendOp::Add,
211 write_mask: 0xF,
212 }
213 }
214
215 pub fn alpha_blend() -> Self {
216 Self {
217 enabled: true,
218 src_color: BlendFactor::SrcAlpha, dst_color: BlendFactor::OneMinusSrcAlpha, color_op: BlendOp::Add,
219 src_alpha: BlendFactor::One, dst_alpha: BlendFactor::OneMinusSrcAlpha, alpha_op: BlendOp::Add,
220 write_mask: 0xF,
221 }
222 }
223
224 pub fn additive() -> Self {
225 Self {
226 enabled: true,
227 src_color: BlendFactor::One, dst_color: BlendFactor::One, color_op: BlendOp::Add,
228 src_alpha: BlendFactor::One, dst_alpha: BlendFactor::One, alpha_op: BlendOp::Add,
229 write_mask: 0xF,
230 }
231 }
232}
233
234#[derive(Debug, Clone)]
235pub struct RasterState {
236 pub cull_mode: CullMode,
237 pub fill_mode: FillMode,
238 pub front_face_ccw: bool,
239 pub depth_bias: i32,
240 pub depth_bias_clamp: f32,
241 pub slope_scaled_depth_bias: f32,
242 pub scissor_test: bool,
243 pub conservative_raster: bool,
244 pub multisample: bool,
245 pub alpha_to_coverage: bool,
246}
247
248impl Default for RasterState {
249 fn default() -> Self {
250 Self {
251 cull_mode: CullMode::Back,
252 fill_mode: FillMode::Solid,
253 front_face_ccw: true,
254 depth_bias: 0,
255 depth_bias_clamp: 0.0,
256 slope_scaled_depth_bias: 0.0,
257 scissor_test: false,
258 conservative_raster: false,
259 multisample: true,
260 alpha_to_coverage: false,
261 }
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq)]
270pub enum PassKind {
271 Graphics,
272 Compute,
273 RayTracing,
274 Copy,
275 Present,
276}
277
278#[derive(Debug, Clone)]
279pub struct RenderPassNode {
280 pub id: u32,
281 pub name: String,
282 pub kind: PassKind,
283 pub enabled: bool,
284 pub color_attachments: Vec<ColorAttachment>,
285 pub depth_attachment: Option<DepthAttachment>,
286 pub input_textures: Vec<String>,
287 pub input_buffers: Vec<String>,
288 pub output_textures: Vec<String>,
289 pub shader_vert: Option<String>,
290 pub shader_frag: Option<String>,
291 pub shader_comp: Option<String>,
292 pub depth_stencil: DepthStencilState,
293 pub blend_states: Vec<BlendState>,
294 pub raster: RasterState,
295 pub topology: PrimitiveTopology,
296 pub viewport: Option<[f32; 4]>, pub scissor: Option<[i32; 4]>, pub dispatch_x: u32,
299 pub dispatch_y: u32,
300 pub dispatch_z: u32,
301 pub indirect_dispatch: bool,
302 pub push_constants_size: u32,
303 pub profiling_color: Vec4,
304 pub estimated_draw_calls: u32,
305 pub estimated_triangles: u64,
306}
307
308impl RenderPassNode {
309 pub fn graphics(id: u32, name: impl Into<String>) -> Self {
310 Self {
311 id,
312 name: name.into(),
313 kind: PassKind::Graphics,
314 enabled: true,
315 color_attachments: Vec::new(),
316 depth_attachment: None,
317 input_textures: Vec::new(),
318 input_buffers: Vec::new(),
319 output_textures: Vec::new(),
320 shader_vert: None,
321 shader_frag: None,
322 shader_comp: None,
323 depth_stencil: DepthStencilState::default(),
324 blend_states: vec![BlendState::opaque()],
325 raster: RasterState::default(),
326 topology: PrimitiveTopology::TriangleList,
327 viewport: None,
328 scissor: None,
329 dispatch_x: 0, dispatch_y: 0, dispatch_z: 0,
330 indirect_dispatch: false,
331 push_constants_size: 0,
332 profiling_color: Vec4::new(0.2, 0.6, 1.0, 1.0),
333 estimated_draw_calls: 0,
334 estimated_triangles: 0,
335 }
336 }
337
338 pub fn compute(id: u32, name: impl Into<String>, gx: u32, gy: u32, gz: u32) -> Self {
339 let mut p = Self::graphics(id, name);
340 p.kind = PassKind::Compute;
341 p.dispatch_x = gx; p.dispatch_y = gy; p.dispatch_z = gz;
342 p.profiling_color = Vec4::new(0.9, 0.4, 0.1, 1.0);
343 p
344 }
345
346 pub fn reads(&self, resource: &str) -> bool {
347 self.input_textures.iter().any(|r| r == resource) ||
348 self.input_buffers.iter().any(|r| r == resource)
349 }
350
351 pub fn writes(&self, resource: &str) -> bool {
352 self.color_attachments.iter().any(|a| a.texture_name == resource) ||
353 self.output_textures.iter().any(|r| r == resource) ||
354 self.depth_attachment.as_ref().map(|d| d.texture_name == resource).unwrap_or(false)
355 }
356}
357
358#[derive(Debug, Clone)]
363pub struct RenderGraph {
364 pub name: String,
365 pub passes: Vec<RenderPassNode>,
366 pub textures: Vec<TextureDesc>,
367 pub buffers: Vec<BufferDesc>,
368 pub backbuffer_name: String,
369 pub width: u32,
370 pub height: u32,
371 pub hdr_enabled: bool,
372 pub msaa_samples: u32,
373 pub next_pass_id: u32,
374}
375
376impl RenderGraph {
377 pub fn new(name: impl Into<String>, width: u32, height: u32) -> Self {
378 Self {
379 name: name.into(),
380 passes: Vec::new(),
381 textures: Vec::new(),
382 buffers: Vec::new(),
383 backbuffer_name: "Backbuffer".into(),
384 width,
385 height,
386 hdr_enabled: true,
387 msaa_samples: 1,
388 next_pass_id: 1,
389 }
390 }
391
392 pub fn add_pass(&mut self, pass: RenderPassNode) -> u32 {
393 let id = pass.id;
394 self.passes.push(pass);
395 id
396 }
397
398 pub fn add_texture(&mut self, desc: TextureDesc) {
399 self.textures.push(desc);
400 }
401
402 pub fn get_pass_mut(&mut self, id: u32) -> Option<&mut RenderPassNode> {
403 self.passes.iter_mut().find(|p| p.id == id)
404 }
405
406 pub fn topological_order(&self) -> Vec<u32> {
407 let n = self.passes.len();
409 let mut order = Vec::with_capacity(n);
410 let mut added = vec![false; n];
411 for _ in 0..n {
412 'outer: for (i, p) in self.passes.iter().enumerate() {
413 if added[i] { continue; }
414 for input in &p.input_textures {
416 let dep = self.passes.iter().enumerate().find(|(j, q)| !added[*j] && q.writes(input));
417 if dep.is_some() { continue 'outer; }
418 }
419 order.push(p.id);
420 added[i] = true;
421 }
422 }
423 order
424 }
425
426 pub fn memory_estimate_bytes(&self) -> u64 {
427 self.textures.iter().map(|t| t.memory_bytes()).sum()
428 }
429
430 pub fn total_draw_calls(&self) -> u32 {
431 self.passes.iter().filter(|p| p.enabled).map(|p| p.estimated_draw_calls).sum()
432 }
433
434 pub fn validate(&self) -> Vec<String> {
435 let mut errors = Vec::new();
436 for pass in &self.passes {
437 for input in &pass.input_textures {
438 if !self.textures.iter().any(|t| &t.name == input) &&
439 !self.passes.iter().any(|p| p.writes(input)) {
440 errors.push(format!("Pass '{}': input '{}' has no producer", pass.name, input));
441 }
442 }
443 }
444 errors
445 }
446}
447
448pub fn build_deferred_pipeline(width: u32, height: u32) -> RenderGraph {
453 let mut g = RenderGraph::new("Deferred PBR", width, height);
454 g.add_texture(TextureDesc::render_target_2d("GBuffer_Albedo", width, height, PixelFormat::Rgba8Srgb));
456 g.add_texture(TextureDesc::render_target_2d("GBuffer_Normal", width, height, PixelFormat::Rgba16Float));
457 g.add_texture(TextureDesc::render_target_2d("GBuffer_ORM", width, height, PixelFormat::Rgba8Unorm));
458 g.add_texture(TextureDesc::render_target_2d("GBuffer_Emissive", width, height, PixelFormat::R11G11B10Float));
459 g.add_texture(TextureDesc::render_target_2d("GBuffer_Velocity", width, height, PixelFormat::Rg16Float));
460 g.add_texture(TextureDesc::depth_target("GBuffer_Depth", width, height));
461 g.add_texture(TextureDesc::render_target_2d("ShadowMap", 4096, 4096, PixelFormat::Depth32Float));
462 g.add_texture(TextureDesc::render_target_2d("SSAO", width / 2, height / 2, PixelFormat::R8Unorm));
463 g.add_texture(TextureDesc::render_target_2d("LightingBuffer", width, height, PixelFormat::Rgba16Float));
464 g.add_texture(TextureDesc::render_target_2d("HDRBuffer", width, height, PixelFormat::R11G11B10Float));
465 g.add_texture(TextureDesc::render_target_2d("Bloom", width / 2, height / 2, PixelFormat::R11G11B10Float));
466 g.add_texture(TextureDesc::render_target_2d("TAA_History", width, height, PixelFormat::Rgba16Float));
467 g.add_texture(TextureDesc::render_target_2d("PostProcess", width, height, PixelFormat::Rgba8Srgb));
468 let mut shadow = RenderPassNode::graphics(g.next_pass_id, "ShadowPass");
470 g.next_pass_id += 1;
471 shadow.depth_attachment = Some(DepthAttachment {
472 texture_name: "ShadowMap".into(),
473 depth_load: LoadOp::Clear, depth_store: StoreOp::Store,
474 stencil_load: LoadOp::DontCare, stencil_store: StoreOp::DontCare,
475 clear_depth: 1.0, clear_stencil: 0,
476 });
477 shadow.profiling_color = Vec4::new(0.5, 0.3, 0.0, 1.0);
478 shadow.estimated_draw_calls = 200;
479 g.add_pass(shadow);
480 let mut gbuf = RenderPassNode::graphics(g.next_pass_id, "GBufferPass");
482 g.next_pass_id += 1;
483 gbuf.color_attachments = vec![
484 ColorAttachment { texture_name: "GBuffer_Albedo".into(), mip_level: 0, array_layer: 0, load: LoadOp::Clear, store: StoreOp::Store, clear_value: Vec4::ZERO },
485 ColorAttachment { texture_name: "GBuffer_Normal".into(), mip_level: 0, array_layer: 0, load: LoadOp::Clear, store: StoreOp::Store, clear_value: Vec4::ZERO },
486 ColorAttachment { texture_name: "GBuffer_ORM".into(), mip_level: 0, array_layer: 0, load: LoadOp::Clear, store: StoreOp::Store, clear_value: Vec4::ZERO },
487 ColorAttachment { texture_name: "GBuffer_Emissive".into(), mip_level: 0, array_layer: 0, load: LoadOp::Clear, store: StoreOp::Store, clear_value: Vec4::ZERO },
488 ColorAttachment { texture_name: "GBuffer_Velocity".into(), mip_level: 0, array_layer: 0, load: LoadOp::Clear, store: StoreOp::Store, clear_value: Vec4::ZERO },
489 ];
490 gbuf.depth_attachment = Some(DepthAttachment {
491 texture_name: "GBuffer_Depth".into(),
492 depth_load: LoadOp::Clear, depth_store: StoreOp::Store,
493 stencil_load: LoadOp::Clear, stencil_store: StoreOp::Store,
494 clear_depth: 1.0, clear_stencil: 0,
495 });
496 gbuf.estimated_draw_calls = 500;
497 gbuf.estimated_triangles = 1_000_000;
498 g.add_pass(gbuf);
499 let mut ssao = RenderPassNode::compute(g.next_pass_id, "SSAO", (width / 2 + 7) / 8, (height / 2 + 7) / 8, 1);
501 g.next_pass_id += 1;
502 ssao.input_textures = vec!["GBuffer_Normal".into(), "GBuffer_Depth".into()];
503 ssao.output_textures = vec!["SSAO".into()];
504 g.add_pass(ssao);
505 let mut lighting = RenderPassNode::compute(g.next_pass_id, "DeferredLighting", (width + 7) / 8, (height + 7) / 8, 1);
507 g.next_pass_id += 1;
508 lighting.input_textures = vec![
509 "GBuffer_Albedo".into(), "GBuffer_Normal".into(), "GBuffer_ORM".into(),
510 "GBuffer_Emissive".into(), "GBuffer_Depth".into(), "ShadowMap".into(), "SSAO".into(),
511 ];
512 lighting.output_textures = vec!["LightingBuffer".into()];
513 g.add_pass(lighting);
514 let mut sky = RenderPassNode::graphics(g.next_pass_id, "SkyTransparentPass");
516 g.next_pass_id += 1;
517 sky.color_attachments = vec![
518 ColorAttachment { texture_name: "LightingBuffer".into(), mip_level: 0, array_layer: 0, load: LoadOp::Load, store: StoreOp::Store, clear_value: Vec4::ZERO },
519 ];
520 sky.blend_states = vec![BlendState::alpha_blend()];
521 sky.depth_stencil.depth_write = false;
522 sky.estimated_draw_calls = 50;
523 g.add_pass(sky);
524 let mut bloom = RenderPassNode::compute(g.next_pass_id, "BloomPass", (width / 2 + 7) / 8, (height / 2 + 7) / 8, 1);
526 g.next_pass_id += 1;
527 bloom.input_textures = vec!["LightingBuffer".into()];
528 bloom.output_textures = vec!["Bloom".into()];
529 g.add_pass(bloom);
530 let mut taa = RenderPassNode::compute(g.next_pass_id, "TAAPass", (width + 7) / 8, (height + 7) / 8, 1);
532 g.next_pass_id += 1;
533 taa.input_textures = vec!["LightingBuffer".into(), "TAA_History".into(), "GBuffer_Velocity".into(), "GBuffer_Depth".into()];
534 taa.output_textures = vec!["HDRBuffer".into()];
535 g.add_pass(taa);
536 let mut post = RenderPassNode::compute(g.next_pass_id, "PostProcess", (width + 7) / 8, (height + 7) / 8, 1);
538 g.next_pass_id += 1;
539 post.input_textures = vec!["HDRBuffer".into(), "Bloom".into()];
540 post.output_textures = vec!["PostProcess".into()];
541 g.add_pass(post);
542 g
543}
544
545#[derive(Debug, Clone, Copy, PartialEq)]
550pub enum DrawCallKind { Indexed, NonIndexed, Instanced, Indirect, Compute }
551
552#[derive(Debug, Clone)]
553pub struct DrawCall {
554 pub kind: DrawCallKind,
555 pub vertex_buffer: u64,
556 pub index_buffer: Option<u64>,
557 pub pipeline_id: u64,
558 pub first_index: u32,
559 pub index_count: u32,
560 pub first_vertex: i32,
561 pub instance_count: u32,
562 pub first_instance: u32,
563 pub material_id: u64,
564 pub transform: Mat4,
565 pub bounding_sphere: (Vec3, f32),
566 pub sort_key: u64,
567 pub pass_mask: u32,
568}
569
570impl DrawCall {
571 pub fn compute_sort_key(material_id: u64, depth: f32, layer: u8) -> u64 {
572 let depth_bits = (depth.to_bits() as u64) & 0xFFFF_FFFF;
573 let mat_bits = material_id & 0xFFFF;
574 ((layer as u64) << 48) | (mat_bits << 32) | depth_bits
575 }
576}
577
578#[derive(Debug, Clone, Default)]
579pub struct DrawCallBatch {
580 pub opaque: Vec<DrawCall>,
581 pub alpha_test: Vec<DrawCall>,
582 pub transparent: Vec<DrawCall>,
583 pub shadow_casters: Vec<DrawCall>,
584 pub ui: Vec<DrawCall>,
585}
586
587impl DrawCallBatch {
588 pub fn sort_opaque_front_to_back(&mut self) {
589 self.opaque.sort_by_key(|d| d.sort_key);
590 }
591
592 pub fn sort_transparent_back_to_front(&mut self) {
593 self.transparent.sort_by(|a, b| b.sort_key.cmp(&a.sort_key));
594 }
595
596 pub fn total_draw_calls(&self) -> usize {
597 self.opaque.len() + self.alpha_test.len() + self.transparent.len() + self.shadow_casters.len() + self.ui.len()
598 }
599
600 pub fn total_triangles(&self) -> u64 {
601 let sum_fn = |v: &Vec<DrawCall>| v.iter().map(|d| d.index_count as u64 / 3 * d.instance_count as u64).sum::<u64>();
602 sum_fn(&self.opaque) + sum_fn(&self.alpha_test) + sum_fn(&self.transparent) + sum_fn(&self.shadow_casters)
603 }
604
605 pub fn cull_frustum(&mut self, view_proj: Mat4) {
606 let frustum_planes = extract_frustum_planes(view_proj);
607 let cull = |calls: &mut Vec<DrawCall>| {
608 calls.retain(|d| {
609 let (center, radius) = d.bounding_sphere;
610 frustum_planes.iter().all(|p| p.dot(center.extend(1.0)) > -radius)
611 });
612 };
613 cull(&mut self.opaque);
614 cull(&mut self.alpha_test);
615 cull(&mut self.transparent);
616 }
617}
618
619fn extract_frustum_planes(m: Mat4) -> [Vec4; 6] {
620 let r = m.row(0);
621 let up = m.row(1);
622 let fwd = m.row(2);
623 let near = m.row(3);
624 [
625 (near + r).normalize(),
626 (near - r).normalize(),
627 (near + up).normalize(),
628 (near - up).normalize(),
629 (near + fwd).normalize(),
630 (near - fwd).normalize(),
631 ]
632}
633
634#[derive(Debug, Clone)]
639pub struct RenderPipelineEditor {
640 pub graphs: Vec<RenderGraph>,
641 pub active_graph: usize,
642 pub selected_pass: Option<u32>,
643 pub show_resource_graph: bool,
644 pub show_memory_stats: bool,
645 pub show_timing: bool,
646 pub batch: DrawCallBatch,
647 pub frame_time_ms: f32,
648 pub pass_timings: HashMap<u32, f32>,
649}
650
651impl RenderPipelineEditor {
652 pub fn new() -> Self {
653 let deferred = build_deferred_pipeline(1920, 1080);
654 Self {
655 graphs: vec![deferred],
656 active_graph: 0,
657 selected_pass: None,
658 show_resource_graph: true,
659 show_memory_stats: true,
660 show_timing: true,
661 batch: DrawCallBatch::default(),
662 frame_time_ms: 16.6,
663 pass_timings: HashMap::new(),
664 }
665 }
666
667 pub fn active_graph(&self) -> &RenderGraph {
668 &self.graphs[self.active_graph]
669 }
670
671 pub fn active_graph_mut(&mut self) -> &mut RenderGraph {
672 &mut self.graphs[self.active_graph]
673 }
674
675 pub fn validate_active(&self) -> Vec<String> {
676 self.active_graph().validate()
677 }
678
679 pub fn memory_report(&self) -> String {
680 let g = self.active_graph();
681 let bytes = g.memory_estimate_bytes();
682 format!("RT memory: {:.1} MB ({} textures, {} passes)",
683 bytes as f64 / 1_048_576.0,
684 g.textures.len(),
685 g.passes.len())
686 }
687
688 pub fn update_timing(&mut self, pass_id: u32, ms: f32) {
689 self.pass_timings.insert(pass_id, ms);
690 self.frame_time_ms = self.pass_timings.values().sum();
691 }
692}
693
694#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn test_deferred_pipeline() {
703 let g = build_deferred_pipeline(1920, 1080);
704 assert!(!g.passes.is_empty());
705 assert!(!g.textures.is_empty());
706 }
707
708 #[test]
709 fn test_topo_sort() {
710 let g = build_deferred_pipeline(1920, 1080);
711 let order = g.topological_order();
712 assert_eq!(order.len(), g.passes.len());
713 }
714
715 #[test]
716 fn test_draw_call_batch() {
717 let mut batch = DrawCallBatch::default();
718 batch.opaque.push(DrawCall {
719 kind: DrawCallKind::Indexed,
720 vertex_buffer: 1, index_buffer: Some(2), pipeline_id: 10,
721 first_index: 0, index_count: 300, first_vertex: 0,
722 instance_count: 1, first_instance: 0,
723 material_id: 5, transform: Mat4::IDENTITY,
724 bounding_sphere: (Vec3::ZERO, 1.0),
725 sort_key: 100, pass_mask: 0xFFFF,
726 });
727 assert_eq!(batch.total_draw_calls(), 1);
728 assert_eq!(batch.total_triangles(), 100);
729 }
730
731 #[test]
732 fn test_pixel_format() {
733 assert!(PixelFormat::Depth32Float.is_depth());
734 assert!(!PixelFormat::Rgba16Float.is_depth());
735 assert_eq!(PixelFormat::Rgba32Float.bytes_per_pixel(), 16.0);
736 }
737}