Skip to main content

proof_engine/render/
render_graph.rs

1//! Render Graph — declarative, dependency-ordered GPU pass scheduling.
2//!
3//! The render graph describes the full frame as a directed acyclic graph (DAG)
4//! of render passes, each consuming and producing named resources (textures,
5//! render targets, buffers). The graph compiler topologically sorts passes,
6//! deduces resource lifetimes, inserts memory barriers, and drives execution.
7//!
8//! ## Key Types
9//!
10//! - [`RenderGraph`]      — builder: declare passes and resources
11//! - [`CompiledGraph`]    — sorted, barrier-annotated execution plan
12//! - [`PassBuilder`]      — fluent API for declaring a single pass
13//! - [`ResourceDesc`]     — describes a texture or buffer resource
14//! - [`RenderPass`]       — one node in the graph
15//! - [`PassKind`]         — Graphics, Compute, or Transfer
16//! - [`ResourceAccess`]   — read / write access mode
17//! - [`Barrier`]          — memory / layout transition between passes
18
19use std::collections::{HashMap, HashSet, VecDeque};
20use crate::render::compute::ResourceHandle;
21
22// ── ResourceDesc ──────────────────────────────────────────────────────────────
23
24/// Format of a texture resource.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum TextureFormat {
27    Rgba8Unorm,
28    Rgba16Float,
29    Rgba32Float,
30    R32Float,
31    Rg16Float,
32    Depth24Stencil8,
33    Depth32Float,
34    Rgb10A2Unorm,
35    Bgra8Unorm,
36}
37
38impl TextureFormat {
39    pub fn is_depth(self) -> bool {
40        matches!(self, Self::Depth24Stencil8 | Self::Depth32Float)
41    }
42
43    pub fn bytes_per_pixel(self) -> u32 {
44        match self {
45            Self::Rgba8Unorm | Self::Bgra8Unorm | Self::Rgb10A2Unorm | Self::Depth24Stencil8 => 4,
46            Self::R32Float => 4,
47            Self::Rg16Float => 4,
48            Self::Rgba16Float | Self::Depth32Float => 8,
49            Self::Rgba32Float => 16,
50        }
51    }
52}
53
54/// Size of a render graph resource, possibly relative to the output framebuffer.
55#[derive(Debug, Clone, Copy)]
56pub enum ResourceSize {
57    /// Absolute pixel dimensions.
58    Fixed(u32, u32),
59    /// Scale of the output framebuffer (e.g. 0.5 = half res).
60    Relative(f32),
61    /// Same as the output framebuffer.
62    Backbuffer,
63}
64
65/// Description of a render graph texture resource.
66#[derive(Debug, Clone)]
67pub struct ResourceDesc {
68    pub name:    String,
69    pub format:  TextureFormat,
70    pub size:    ResourceSize,
71    pub samples: u32,
72    /// Mip levels (1 = no mipmapping).
73    pub mips:    u32,
74    /// Number of array layers.
75    pub layers:  u32,
76    /// Persistent: survives across frames (e.g. temporal AA history).
77    pub persistent: bool,
78}
79
80impl ResourceDesc {
81    pub fn color(name: impl Into<String>, format: TextureFormat) -> Self {
82        Self {
83            name:    name.into(),
84            format,
85            size:    ResourceSize::Backbuffer,
86            samples: 1,
87            mips:    1,
88            layers:  1,
89            persistent: false,
90        }
91    }
92
93    pub fn depth(name: impl Into<String>) -> Self {
94        Self::color(name, TextureFormat::Depth24Stencil8)
95    }
96
97    pub fn half_res(mut self) -> Self {
98        self.size = ResourceSize::Relative(0.5);
99        self
100    }
101
102    pub fn fixed_size(mut self, w: u32, h: u32) -> Self {
103        self.size = ResourceSize::Fixed(w, h);
104        self
105    }
106
107    pub fn with_mips(mut self, mips: u32) -> Self {
108        self.mips = mips;
109        self
110    }
111
112    pub fn persistent(mut self) -> Self {
113        self.persistent = true;
114        self
115    }
116
117    pub fn msaa(mut self, samples: u32) -> Self {
118        self.samples = samples;
119        self
120    }
121}
122
123// ── ResourceAccess ────────────────────────────────────────────────────────────
124
125/// How a pass accesses a resource.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum ResourceAccess {
128    /// Read as a sampler / shader resource.
129    ShaderRead,
130    /// Written as a render target color attachment.
131    RenderTarget,
132    /// Written as depth/stencil attachment.
133    DepthWrite,
134    /// Read as depth attachment (no writes).
135    DepthRead,
136    /// Read/write via image load/store (compute).
137    ImageReadWrite,
138    /// Written by compute shader.
139    ComputeWrite,
140    /// Read by compute shader.
141    ComputeRead,
142    /// Source of a transfer/blit operation.
143    TransferSrc,
144    /// Destination of a transfer/blit operation.
145    TransferDst,
146    /// Presented to the display.
147    Present,
148}
149
150impl ResourceAccess {
151    pub fn is_write(self) -> bool {
152        matches!(self,
153            Self::RenderTarget
154            | Self::DepthWrite
155            | Self::ImageReadWrite
156            | Self::ComputeWrite
157            | Self::TransferDst
158        )
159    }
160
161    pub fn is_read(self) -> bool { !self.is_write() }
162}
163
164// ── Barrier ───────────────────────────────────────────────────────────────────
165
166/// A memory / image-layout barrier between two passes.
167#[derive(Debug, Clone)]
168pub struct Barrier {
169    pub resource:   String,
170    pub src_access: ResourceAccess,
171    pub dst_access: ResourceAccess,
172}
173
174impl Barrier {
175    pub fn new(resource: impl Into<String>, src: ResourceAccess, dst: ResourceAccess) -> Self {
176        Self { resource: resource.into(), src_access: src, dst_access: dst }
177    }
178}
179
180// ── PassKind ──────────────────────────────────────────────────────────────────
181
182/// What kind of work a render pass performs.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum PassKind {
185    /// Rasterization pass (vertex + fragment shaders).
186    Graphics,
187    /// GPU compute dispatch.
188    Compute,
189    /// CPU→GPU or GPU→GPU data transfer / blit.
190    Transfer,
191    /// Presentation to the display.
192    Present,
193}
194
195// ── RenderPass ────────────────────────────────────────────────────────────────
196
197/// A single node in the render graph.
198#[derive(Debug, Clone)]
199pub struct RenderPass {
200    pub name:     String,
201    pub kind:     PassKind,
202    /// Resources read by this pass and the access type.
203    pub reads:    Vec<(String, ResourceAccess)>,
204    /// Resources written by this pass and the access type.
205    pub writes:   Vec<(String, ResourceAccess)>,
206    /// User-assigned render priority (lower = earlier among peers).
207    pub priority: i32,
208    /// Whether this pass can be skipped if its outputs are unused.
209    pub optional: bool,
210    /// Explicit extra dependencies not implied by resource usage.
211    pub depends:  Vec<String>,
212}
213
214impl RenderPass {
215    pub fn new(name: impl Into<String>, kind: PassKind) -> Self {
216        Self {
217            name:     name.into(),
218            kind,
219            reads:    Vec::new(),
220            writes:   Vec::new(),
221            priority: 0,
222            optional: false,
223            depends:  Vec::new(),
224        }
225    }
226
227    pub fn reads(&mut self, res: impl Into<String>, access: ResourceAccess) {
228        self.reads.push((res.into(), access));
229    }
230
231    pub fn writes(&mut self, res: impl Into<String>, access: ResourceAccess) {
232        self.writes.push((res.into(), access));
233    }
234}
235
236// ── PassBuilder ───────────────────────────────────────────────────────────────
237
238/// Fluent builder for a single render pass.
239pub struct PassBuilder<'g> {
240    graph: &'g mut RenderGraph,
241    pass:  RenderPass,
242}
243
244impl<'g> PassBuilder<'g> {
245    fn new(graph: &'g mut RenderGraph, name: impl Into<String>, kind: PassKind) -> Self {
246        Self { graph, pass: RenderPass::new(name, kind) }
247    }
248
249    pub fn read(mut self, resource: impl Into<String>) -> Self {
250        self.pass.reads(resource, ResourceAccess::ShaderRead);
251        self
252    }
253
254    pub fn read_depth(mut self, resource: impl Into<String>) -> Self {
255        self.pass.reads(resource, ResourceAccess::DepthRead);
256        self
257    }
258
259    pub fn write(mut self, resource: impl Into<String>) -> Self {
260        self.pass.writes(resource, ResourceAccess::RenderTarget);
261        self
262    }
263
264    pub fn write_depth(mut self, resource: impl Into<String>) -> Self {
265        self.pass.writes(resource, ResourceAccess::DepthWrite);
266        self
267    }
268
269    pub fn compute_read(mut self, resource: impl Into<String>) -> Self {
270        self.pass.reads(resource, ResourceAccess::ComputeRead);
271        self
272    }
273
274    pub fn compute_write(mut self, resource: impl Into<String>) -> Self {
275        self.pass.writes(resource, ResourceAccess::ComputeWrite);
276        self
277    }
278
279    pub fn transfer_src(mut self, resource: impl Into<String>) -> Self {
280        self.pass.reads(resource, ResourceAccess::TransferSrc);
281        self
282    }
283
284    pub fn transfer_dst(mut self, resource: impl Into<String>) -> Self {
285        self.pass.writes(resource, ResourceAccess::TransferDst);
286        self
287    }
288
289    pub fn priority(mut self, p: i32) -> Self {
290        self.pass.priority = p;
291        self
292    }
293
294    pub fn optional(mut self) -> Self {
295        self.pass.optional = true;
296        self
297    }
298
299    pub fn after(mut self, pass_name: impl Into<String>) -> Self {
300        self.pass.depends.push(pass_name.into());
301        self
302    }
303
304    /// Finalize and register the pass with the graph.
305    pub fn build(self) {
306        self.graph.add_pass(self.pass);
307    }
308}
309
310// ── RenderGraph ───────────────────────────────────────────────────────────────
311
312/// Builder for a frame's render graph.
313pub struct RenderGraph {
314    passes:    Vec<RenderPass>,
315    resources: HashMap<String, ResourceDesc>,
316    /// The final output resource (usually the backbuffer).
317    output:    Option<String>,
318}
319
320impl RenderGraph {
321    pub fn new() -> Self {
322        Self {
323            passes:    Vec::new(),
324            resources: HashMap::new(),
325            output:    None,
326        }
327    }
328
329    /// Declare a transient resource (created and destroyed within this frame).
330    pub fn declare_resource(&mut self, desc: ResourceDesc) -> ResourceHandle {
331        let id = self.resources.len() as u32 + 1;
332        self.resources.insert(desc.name.clone(), desc);
333        ResourceHandle(id)
334    }
335
336    /// Declare the final output resource.
337    pub fn set_output(&mut self, resource: impl Into<String>) {
338        self.output = Some(resource.into());
339    }
340
341    /// Add a pre-built pass.
342    pub fn add_pass(&mut self, pass: RenderPass) {
343        self.passes.push(pass);
344    }
345
346    /// Start building a graphics pass.
347    pub fn graphics_pass<'g>(&'g mut self, name: impl Into<String>) -> PassBuilder<'g> {
348        PassBuilder::new(self, name, PassKind::Graphics)
349    }
350
351    /// Start building a compute pass.
352    pub fn compute_pass<'g>(&'g mut self, name: impl Into<String>) -> PassBuilder<'g> {
353        PassBuilder::new(self, name, PassKind::Compute)
354    }
355
356    /// Start building a transfer pass.
357    pub fn transfer_pass<'g>(&'g mut self, name: impl Into<String>) -> PassBuilder<'g> {
358        PassBuilder::new(self, name, PassKind::Transfer)
359    }
360
361    /// Compile the graph: topological sort + barrier insertion.
362    pub fn compile(self) -> Result<CompiledGraph, GraphError> {
363        let compiler = GraphCompiler::new(self);
364        compiler.compile()
365    }
366}
367
368impl Default for RenderGraph {
369    fn default() -> Self { Self::new() }
370}
371
372// ── GraphError ────────────────────────────────────────────────────────────────
373
374#[derive(Debug, Clone)]
375pub enum GraphError {
376    /// Cycle detected between named passes.
377    CycleDetected(Vec<String>),
378    /// A pass reads a resource that no pass writes (and it's not declared).
379    UnresolvedResource { pass: String, resource: String },
380    /// A pass named in `depends` does not exist.
381    UnknownDependency { pass: String, dep: String },
382}
383
384impl std::fmt::Display for GraphError {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        match self {
387            Self::CycleDetected(cycle) =>
388                write!(f, "render graph cycle: {}", cycle.join(" → ")),
389            Self::UnresolvedResource { pass, resource } =>
390                write!(f, "pass '{}' reads undeclared resource '{}'", pass, resource),
391            Self::UnknownDependency { pass, dep } =>
392                write!(f, "pass '{}' depends on unknown pass '{}'", pass, dep),
393        }
394    }
395}
396
397// ── CompiledPass ──────────────────────────────────────────────────────────────
398
399/// A pass in the compiled execution order, with pre-computed barriers.
400#[derive(Debug, Clone)]
401pub struct CompiledPass {
402    pub pass:         RenderPass,
403    /// Barriers to insert before this pass executes.
404    pub pre_barriers: Vec<Barrier>,
405}
406
407// ── CompiledGraph ─────────────────────────────────────────────────────────────
408
409/// The output of graph compilation: passes in execution order with barriers.
410pub struct CompiledGraph {
411    /// Passes sorted in dependency order (topological sort, priority-stable).
412    pub passes:    Vec<CompiledPass>,
413    pub resources: HashMap<String, ResourceDesc>,
414    pub output:    Option<String>,
415    /// Stats from the last compilation.
416    pub stats:     CompileStats,
417}
418
419#[derive(Debug, Default, Clone)]
420pub struct CompileStats {
421    pub pass_count:    usize,
422    pub barrier_count: usize,
423    pub culled_passes: usize,
424}
425
426impl CompiledGraph {
427    /// Iterate over passes in execution order.
428    pub fn iter(&self) -> impl Iterator<Item = &CompiledPass> {
429        self.passes.iter()
430    }
431
432    pub fn pass_count(&self) -> usize { self.passes.len() }
433
434    /// Look up a resource descriptor by name.
435    pub fn resource(&self, name: &str) -> Option<&ResourceDesc> {
436        self.resources.get(name)
437    }
438
439    /// Compute the concrete pixel size of a resource given the backbuffer dimensions.
440    pub fn resolve_size(&self, name: &str, bb_w: u32, bb_h: u32) -> Option<(u32, u32)> {
441        let desc = self.resources.get(name)?;
442        Some(match desc.size {
443            ResourceSize::Fixed(w, h)   => (w, h),
444            ResourceSize::Backbuffer    => (bb_w, bb_h),
445            ResourceSize::Relative(s)   => (
446                ((bb_w as f32 * s) as u32).max(1),
447                ((bb_h as f32 * s) as u32).max(1),
448            ),
449        })
450    }
451}
452
453// ── GraphCompiler ─────────────────────────────────────────────────────────────
454
455struct GraphCompiler {
456    graph: RenderGraph,
457}
458
459impl GraphCompiler {
460    fn new(graph: RenderGraph) -> Self { Self { graph } }
461
462    fn compile(mut self) -> Result<CompiledGraph, GraphError> {
463        // ── 1. Validate explicit dependencies ──────────────────────────────
464        let pass_names: HashSet<String> = self.graph.passes.iter()
465            .map(|p| p.name.clone())
466            .collect();
467
468        for pass in &self.graph.passes {
469            for dep in &pass.depends {
470                if !pass_names.contains(dep) {
471                    return Err(GraphError::UnknownDependency {
472                        pass: pass.name.clone(),
473                        dep:  dep.clone(),
474                    });
475                }
476            }
477        }
478
479        // ── 2. Build writer map: resource → list of pass names that write it ─
480        let mut writers: HashMap<String, Vec<String>> = HashMap::new();
481        for pass in &self.graph.passes {
482            for (res, _) in &pass.writes {
483                writers.entry(res.clone()).or_default().push(pass.name.clone());
484            }
485        }
486
487        // ── 3. Cull optional passes not reachable from output ──────────────
488        let live_passes = self.compute_live_passes(&writers);
489        let original_count = self.graph.passes.len();
490        let culled = original_count - live_passes.len();
491        self.graph.passes.retain(|p| live_passes.contains(&p.name));
492
493        // ── 4. Build dependency edges for topological sort ─────────────────
494        // Edge: pass A must run before pass B if B reads something A writes.
495        let mut adj: HashMap<String, HashSet<String>> = HashMap::new(); // A -> set of B that depend on A
496        let mut in_deg: HashMap<String, usize> = HashMap::new();
497        for pass in &self.graph.passes {
498            adj.entry(pass.name.clone()).or_default();
499            in_deg.entry(pass.name.clone()).or_insert(0);
500        }
501
502        for pass_b in &self.graph.passes {
503            for (res, _) in &pass_b.reads {
504                if let Some(ws) = writers.get(res) {
505                    for pass_a in ws {
506                        if pass_a != &pass_b.name && live_passes.contains(pass_a) {
507                            if adj.get(pass_a).map_or(true, |s| !s.contains(&pass_b.name)) {
508                                adj.entry(pass_a.clone()).or_default().insert(pass_b.name.clone());
509                                *in_deg.entry(pass_b.name.clone()).or_insert(0) += 1;
510                            }
511                        }
512                    }
513                }
514            }
515            // Explicit depends_on edges
516            for dep in &pass_b.depends {
517                if live_passes.contains(dep) {
518                    if adj.get(dep).map_or(true, |s| !s.contains(&pass_b.name)) {
519                        adj.entry(dep.clone()).or_default().insert(pass_b.name.clone());
520                        *in_deg.entry(pass_b.name.clone()).or_insert(0) += 1;
521                    }
522                }
523            }
524        }
525
526        // ── 5. Kahn's algorithm (priority-stable) ─────────────────────────
527        let pass_map: HashMap<String, RenderPass> = self.graph.passes.drain(..)
528            .map(|p| (p.name.clone(), p))
529            .collect();
530
531        let mut queue: VecDeque<String> = in_deg.iter()
532            .filter(|(_, &d)| d == 0)
533            .map(|(n, _)| n.clone())
534            .collect();
535
536        // Sort by priority to make the initial order deterministic.
537        let mut sorted: Vec<String> = Vec::new();
538        let mut cycle_check = 0usize;
539
540        while !queue.is_empty() {
541            // Pick the lowest-priority node from the ready queue.
542            let best_idx = queue.iter().enumerate()
543                .min_by_key(|(_, n)| pass_map.get(*n).map_or(0, |p| p.priority))
544                .map(|(i, _)| i)
545                .unwrap_or(0);
546            let node = queue.remove(best_idx).unwrap();
547            sorted.push(node.clone());
548            cycle_check += 1;
549
550            if let Some(successors) = adj.get(&node) {
551                for succ in successors {
552                    let deg = in_deg.get_mut(succ).unwrap();
553                    *deg -= 1;
554                    if *deg == 0 {
555                        queue.push_back(succ.clone());
556                    }
557                }
558            }
559        }
560
561        if cycle_check != pass_map.len() {
562            // Collect the cycle nodes (those with in_deg > 0 still)
563            let cycle_nodes: Vec<String> = in_deg.iter()
564                .filter(|(_, &d)| d > 0)
565                .map(|(n, _)| n.clone())
566                .collect();
567            return Err(GraphError::CycleDetected(cycle_nodes));
568        }
569
570        // ── 6. Insert barriers ────────────────────────────────────────────
571        // Track the last access mode for each resource.
572        let mut last_access: HashMap<String, ResourceAccess> = HashMap::new();
573        let mut compiled: Vec<CompiledPass> = Vec::new();
574        let mut total_barriers = 0usize;
575
576        for pass_name in &sorted {
577            let pass = pass_map.get(pass_name).unwrap().clone();
578            let mut pre_barriers = Vec::new();
579
580            // Emit barriers for resources this pass reads.
581            for (res, access) in &pass.reads {
582                if let Some(&prev) = last_access.get(res) {
583                    if needs_barrier(prev, *access) {
584                        pre_barriers.push(Barrier::new(res, prev, *access));
585                        total_barriers += 1;
586                    }
587                }
588                last_access.insert(res.clone(), *access);
589            }
590
591            // Emit barriers for resources this pass writes.
592            for (res, access) in &pass.writes {
593                if let Some(&prev) = last_access.get(res) {
594                    if needs_barrier(prev, *access) {
595                        pre_barriers.push(Barrier::new(res, prev, *access));
596                        total_barriers += 1;
597                    }
598                }
599                last_access.insert(res.clone(), *access);
600            }
601
602            compiled.push(CompiledPass { pass, pre_barriers });
603        }
604
605        let stats = CompileStats {
606            pass_count:    compiled.len(),
607            barrier_count: total_barriers,
608            culled_passes: culled,
609        };
610
611        Ok(CompiledGraph {
612            passes:    compiled,
613            resources: self.graph.resources,
614            output:    self.graph.output,
615            stats,
616        })
617    }
618
619    /// Determine which passes are "live" (reachable backward from the output).
620    fn compute_live_passes(&self, writers: &HashMap<String, Vec<String>>) -> HashSet<String> {
621        // All non-optional passes are always live.
622        let mut live: HashSet<String> = self.graph.passes.iter()
623            .filter(|p| !p.optional)
624            .map(|p| p.name.clone())
625            .collect();
626
627        // If there's an output, trace backward from it.
628        if let Some(output) = &self.graph.output {
629            let mut stack: Vec<String> = Vec::new();
630            if let Some(ws) = writers.get(output) {
631                stack.extend(ws.clone());
632            }
633            while let Some(pass_name) = stack.pop() {
634                if live.insert(pass_name.clone()) {
635                    // Trace its inputs too.
636                    if let Some(pass) = self.graph.passes.iter().find(|p| p.name == pass_name) {
637                        for (res, _) in &pass.reads {
638                            if let Some(ws) = writers.get(res) {
639                                for w in ws {
640                                    if !live.contains(w) {
641                                        stack.push(w.clone());
642                                    }
643                                }
644                            }
645                        }
646                    }
647                }
648            }
649        }
650
651        live
652    }
653}
654
655/// Returns true if a pipeline barrier is needed between `src` and `dst` access.
656fn needs_barrier(src: ResourceAccess, dst: ResourceAccess) -> bool {
657    use ResourceAccess::*;
658    // Write followed by anything → barrier.
659    // Anything followed by write → barrier.
660    // Read followed by read (same) → no barrier.
661    if src == dst && src.is_read() { return false; }
662    src.is_write() || dst.is_write()
663}
664
665// ── Standard Frame Graph ──────────────────────────────────────────────────────
666
667/// Build the standard proof-engine frame render graph.
668///
669/// Passes (in dependency order):
670/// 1. `depth_prepass`    — writes `depth`
671/// 2. `particle_update`  — compute, reads/writes `particle_buf`
672/// 3. `gbuffer`          — writes `gbuffer_albedo`, `gbuffer_normal`, `gbuffer_emissive`, reads `depth`
673/// 4. `ssao`             — compute, reads `depth` + `gbuffer_normal`, writes `ssao`
674/// 5. `lighting`         — reads gbuffer + ssao, writes `hdr`
675/// 6. `particle_draw`    — reads `particle_buf` + `depth`, writes `hdr`
676/// 7. `bloom_down`       — reads `hdr`, writes `bloom_half`
677/// 8. `bloom_up`         — reads `bloom_half`, writes `bloom`
678/// 9. `tonemap`          — reads `hdr` + `bloom`, writes `ldr`
679/// 10. `fxaa`            — reads `ldr`, writes `backbuffer`
680pub fn standard_frame_graph() -> RenderGraph {
681    let mut g = RenderGraph::new();
682
683    // ── Resource declarations ─────────────────────────────────────────────
684    g.declare_resource(ResourceDesc::depth("depth").persistent());
685    g.declare_resource(ResourceDesc::color("gbuffer_albedo",   TextureFormat::Rgba8Unorm));
686    g.declare_resource(ResourceDesc::color("gbuffer_normal",   TextureFormat::Rgba16Float));
687    g.declare_resource(ResourceDesc::color("gbuffer_emissive", TextureFormat::Rgba16Float));
688    g.declare_resource(ResourceDesc::color("ssao",             TextureFormat::R32Float).half_res());
689    g.declare_resource(ResourceDesc::color("hdr",              TextureFormat::Rgba16Float));
690    g.declare_resource(ResourceDesc::color("bloom_half",       TextureFormat::Rgba16Float).half_res());
691    g.declare_resource(ResourceDesc::color("bloom",            TextureFormat::Rgba16Float).half_res());
692    g.declare_resource(ResourceDesc::color("ldr",              TextureFormat::Rgba8Unorm));
693    g.declare_resource(ResourceDesc::color("particle_buf",     TextureFormat::Rgba32Float).persistent());
694
695    g.set_output("backbuffer");
696
697    // ── Passes ────────────────────────────────────────────────────────────
698    g.graphics_pass("depth_prepass")
699        .write_depth("depth")
700        .priority(-100)
701        .build();
702
703    g.compute_pass("particle_update")
704        .compute_read("particle_buf")
705        .compute_write("particle_buf")
706        .priority(-90)
707        .build();
708
709    g.graphics_pass("gbuffer")
710        .write("gbuffer_albedo")
711        .write("gbuffer_normal")
712        .write("gbuffer_emissive")
713        .read_depth("depth")
714        .priority(-80)
715        .build();
716
717    g.compute_pass("ssao")
718        .read("gbuffer_normal")
719        .read_depth("depth")
720        .compute_write("ssao")
721        .priority(-70)
722        .build();
723
724    g.graphics_pass("lighting")
725        .read("gbuffer_albedo")
726        .read("gbuffer_normal")
727        .read("gbuffer_emissive")
728        .read("ssao")
729        .write("hdr")
730        .priority(-60)
731        .build();
732
733    g.graphics_pass("particle_draw")
734        .compute_read("particle_buf")
735        .read_depth("depth")
736        .write("hdr")
737        .priority(-50)
738        .build();
739
740    g.graphics_pass("bloom_down")
741        .read("hdr")
742        .write("bloom_half")
743        .priority(-40)
744        .build();
745
746    g.graphics_pass("bloom_up")
747        .read("bloom_half")
748        .write("bloom")
749        .priority(-30)
750        .build();
751
752    g.graphics_pass("tonemap")
753        .read("hdr")
754        .read("bloom")
755        .write("ldr")
756        .priority(-20)
757        .build();
758
759    g.graphics_pass("fxaa")
760        .read("ldr")
761        .write("backbuffer")
762        .priority(-10)
763        .build();
764
765    g
766}
767
768// ── Tests ─────────────────────────────────────────────────────────────────────
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773
774    #[test]
775    fn test_standard_graph_compiles() {
776        let g = standard_frame_graph();
777        let compiled = g.compile().expect("standard graph should compile");
778        assert!(compiled.pass_count() >= 9);
779    }
780
781    #[test]
782    fn test_barrier_inserted_between_write_and_read() {
783        let mut g = RenderGraph::new();
784        g.declare_resource(ResourceDesc::color("tex", TextureFormat::Rgba8Unorm));
785        g.graphics_pass("writer").write("tex").build();
786        g.graphics_pass("reader").read("tex").build();
787        let compiled = g.compile().unwrap();
788        let reader = compiled.passes.iter().find(|p| p.pass.name == "reader").unwrap();
789        assert!(!reader.pre_barriers.is_empty(), "barrier expected before reader");
790    }
791
792    #[test]
793    fn test_cycle_detection() {
794        let mut g = RenderGraph::new();
795        g.declare_resource(ResourceDesc::color("a", TextureFormat::Rgba8Unorm));
796        g.declare_resource(ResourceDesc::color("b", TextureFormat::Rgba8Unorm));
797        // A writes 'a', reads 'b'; B writes 'b', reads 'a' → cycle
798        g.graphics_pass("pass_a").write("a").read("b").build();
799        g.graphics_pass("pass_b").write("b").read("a").build();
800        assert!(matches!(g.compile(), Err(GraphError::CycleDetected(_))));
801    }
802
803    #[test]
804    fn test_no_barrier_between_two_reads() {
805        let mut g = RenderGraph::new();
806        g.declare_resource(ResourceDesc::color("tex", TextureFormat::Rgba8Unorm));
807        g.declare_resource(ResourceDesc::color("out1", TextureFormat::Rgba8Unorm));
808        g.declare_resource(ResourceDesc::color("out2", TextureFormat::Rgba8Unorm));
809        // Write tex first
810        g.graphics_pass("init").write("tex").build();
811        // Two independent readers
812        g.graphics_pass("r1").read("tex").write("out1").after("init").build();
813        g.graphics_pass("r2").read("tex").write("out2").after("init").build();
814        let compiled = g.compile().unwrap();
815        // The second reader of 'tex' (whichever comes second) should not need a barrier for tex
816        // (read→read same mode = no barrier)
817        let barriers: Vec<_> = compiled.passes.iter()
818            .flat_map(|p| p.pre_barriers.iter())
819            .filter(|b| b.resource == "tex"
820                && b.src_access == ResourceAccess::ShaderRead
821                && b.dst_access == ResourceAccess::ShaderRead)
822            .collect();
823        assert!(barriers.is_empty(), "read→read should not emit a barrier");
824    }
825
826    #[test]
827    fn test_resolve_size() {
828        let g = standard_frame_graph();
829        let compiled = g.compile().unwrap();
830        let (w, h) = compiled.resolve_size("ssao", 1920, 1080).unwrap();
831        assert_eq!(w, 960);
832        assert_eq!(h, 540);
833    }
834}