Skip to main content

proof_engine/editor/
render_pipeline.rs

1
2//! Render pipeline integration — pass graph, resource management, draw call batching.
3
4use glam::{Vec2, Vec3, Vec4, Mat4};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Resource types
9// ---------------------------------------------------------------------------
10
11#[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// ---------------------------------------------------------------------------
107// Buffer descriptors
108// ---------------------------------------------------------------------------
109
110#[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// ---------------------------------------------------------------------------
124// Render pass descriptor
125// ---------------------------------------------------------------------------
126
127#[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// ---------------------------------------------------------------------------
154// Pipeline state
155// ---------------------------------------------------------------------------
156
157#[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, // RGBA bits
203}
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// ---------------------------------------------------------------------------
266// Render pass node in graph
267// ---------------------------------------------------------------------------
268
269#[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]>,  // x, y, w, h
297    pub scissor: Option<[i32; 4]>,   // x, y, w, h
298    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// ---------------------------------------------------------------------------
359// Render graph
360// ---------------------------------------------------------------------------
361
362#[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        // Simple ordering by dependency: if pass A writes a resource that pass B reads, A comes first
408        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                // Check all dependencies are already added
415                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
448// ---------------------------------------------------------------------------
449// Standard deferred pipeline
450// ---------------------------------------------------------------------------
451
452pub fn build_deferred_pipeline(width: u32, height: u32) -> RenderGraph {
453    let mut g = RenderGraph::new("Deferred PBR", width, height);
454    // GBuffer textures
455    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    // Shadow pass
469    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    // GBuffer pass
481    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    // SSAO pass (compute)
500    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    // Lighting pass (compute)
506    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    // Sky / transparent pass
515    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    // Bloom compute
525    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    // TAA
531    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    // Post process
537    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// ---------------------------------------------------------------------------
546// Draw call batching
547// ---------------------------------------------------------------------------
548
549#[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// ---------------------------------------------------------------------------
635// Pipeline editor
636// ---------------------------------------------------------------------------
637
638#[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// ---------------------------------------------------------------------------
695// Tests
696// ---------------------------------------------------------------------------
697#[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}