Skip to main content

concinnity_render/render_graph/
compile.rs

1// src/render_graph/compile.rs
2//
3// Frozen `CompiledGraph` produced by `GraphBuilder::compile`. The compile
4// pass:
5//
6//   1. Validates exactly one pass declares `presents()`.
7//   2. Validates every declared read has a producer (write) in the graph.
8//   3. Topologically sorts passes by read-after-write + write-after-write
9//      edges. Cycles are an error.
10//   4. Derives a `barriers_before` list per pass from the per-resource
11//      state machine (Undefined → Read → Write transitions).
12//   5. Computes a `[first, last]` pass-index lifetime per resource, which
13//      [`super::alias`] uses to overlap non-overlapping lifetimes in
14//      physical memory.
15//
16// The graph allocates no GPU resources and interprets no barriers per
17// backend. This module only produces the data the backend executor consumes.
18
19use alloc::collections::BinaryHeap;
20use alloc::vec;
21use alloc::vec::Vec;
22use core::cmp::Reverse;
23use hashbrown::HashMap;
24
25use super::builder::{GraphBuilder, ResourceVersion};
26use super::passes::PassId;
27use super::types::{
28    BarrierOp, BufferDesc, GraphResourceClass, PassKind, PassRange, ReadStages, ResourceId,
29    ResourceOrigin, ResourceState, TextureDesc,
30};
31
32// Compiler error. Returned by [`GraphBuilder::compile`]; the call site
33// typically panics (these are programmer errors, not runtime conditions).
34#[derive(Debug, Clone, PartialEq)]
35pub enum GraphError {
36    // No pass declared `presents()`. Every graph must terminate at the
37    // swapchain write.
38    MissingPresenter,
39    // More than one pass declared `presents()`. The graph compiler does
40    // not pick one for you.
41    MultiplePresenters(usize),
42    // A pass declared `read_*(handle)` but no pass writes that exact
43    // `(resource, version)` pair. Most commonly this happens when a
44    // caller forgets to thread the post-write handle from a prior pass
45    // (used the pre-write `h` instead of the `h1` returned by
46    // `write_texture`).
47    MissingProducer {
48        pass: PassId,
49        resource_label: &'static str,
50        version: u32,
51    },
52    // The read / write graph has a cycle: toposort visited fewer
53    // passes than declared.
54    Cycle,
55}
56
57impl core::fmt::Display for GraphError {
58    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59        match self {
60            GraphError::MissingPresenter => {
61                write!(
62                    f,
63                    "no pass declared presents(); the graph has no terminal node"
64                )
65            }
66            GraphError::MultiplePresenters(n) => {
67                write!(f, "{} passes declared presents(); only one is allowed", n)
68            }
69            GraphError::MissingProducer {
70                pass,
71                resource_label,
72                version,
73            } => write!(
74                f,
75                "pass {:?} reads {} v{} but no pass writes that version",
76                pass, resource_label, version
77            ),
78            GraphError::Cycle => write!(f, "cycle in render-graph read/write edges"),
79        }
80    }
81}
82
83impl core::error::Error for GraphError {}
84
85/// One pass in execution order. Carries the declared reads / writes, the
86/// barriers the executor must emit before running this pass, and the
87/// `PassKind` + `PassId` the backend dispatches on.
88#[derive(Debug, Clone)]
89pub struct CompiledPass {
90    /// The pass's stable identity.
91    pub id: PassId,
92    /// Whether the executor encodes a render or a compute pass.
93    pub kind: PassKind,
94    /// Resource versions this pass reads.
95    pub reads: Vec<ResourceVersion>,
96    /// Resource versions this pass writes.
97    pub writes: Vec<ResourceVersion>,
98    /// Whether this pass writes the swapchain image.
99    pub presents: bool,
100    /// Barriers the executor emits before the pass.
101    pub barriers_before: Vec<BarrierOp>,
102}
103
104/// One resource in the compiled graph. Carries the lifetime interval
105/// (in compiled-pass-index space) and the origin distinction the
106/// aliaser will care about.
107#[derive(Debug, Clone)]
108pub struct CompiledResource {
109    /// The resource's stable label, the executor's join key.
110    pub label: &'static str,
111    /// Whether the resource is imported or graph-declared.
112    pub origin: ResourceOrigin,
113    /// The pass range over which the resource must stay live.
114    pub lifetime: PassRange,
115    /// Texture shape (format / size / sample count / layers), `None` for a
116    /// buffer. The aliasing planner uses it to size each transient resource;
117    /// the backend will use it to allocate the realised resource.
118    pub tex_desc: Option<TextureDesc>,
119    // Buffer shape (size / usage), `None` for a texture. Carried so a backend can
120    // derive the resource's barrier class from its declared usage.
121    pub(crate) buf_desc: Option<BufferDesc>,
122}
123
124impl CompiledResource {
125    /// The barrier class this resource's declared usage puts it in. The graph is
126    /// the single source of truth for it: a backend executor resolves a label to
127    /// its GPU object but never restates what kind of resource it is, so the
128    /// executors cannot disagree. `None` for a resource carrying neither desc.
129    pub fn class(&self) -> Option<GraphResourceClass> {
130        if let Some(desc) = self.tex_desc {
131            return Some(GraphResourceClass::for_texture_usage(desc.usage));
132        }
133        self.buf_desc
134            .map(|desc| GraphResourceClass::for_buffer_usage(desc.usage))
135    }
136}
137
138/// Frozen graph the per-backend executor consumes. Passes are in
139/// execution order; barriers are pre-derived; resource lifetimes are
140/// ready for a future aliaser.
141#[derive(Debug, Clone)]
142pub struct CompiledGraph {
143    /// Passes in execution order.
144    pub passes: Vec<CompiledPass>,
145    /// Resources, indexed by [`ResourceId`].
146    pub resources: Vec<CompiledResource>,
147}
148
149impl CompiledGraph {
150    // Restrict a pass's `barriers_before` to the resources whose label is in
151    // `allow`, pairing each kept barrier with that label. A backend executor
152    // uses this to drive native transitions for the subset of resources that
153    // have moved off hand-written inline barriers, while every other resource
154    // keeps its existing inline / render-pass-driven path. The label is the
155    // stable join key the backend resolves to its GPU object.
156    #[cfg(test)]
157    pub(crate) fn pass_barriers_for(
158        &self,
159        pass: &CompiledPass,
160        allow: &[&str],
161    ) -> Vec<(&'static str, BarrierOp)> {
162        pass.barriers_before
163            .iter()
164            .filter_map(|op| {
165                let label = self.resources[op.resource_index()].label;
166                allow.contains(&label).then_some((label, *op))
167            })
168            .collect()
169    }
170}
171
172impl GraphBuilder {
173    // Topologically sort, validate, and derive per-pass barriers.
174    // Returns the frozen graph the executor consumes. The graph must
175    // declare exactly one `presents()` pass: the terminal swapchain
176    // write that ends the frame.
177    pub(crate) fn compile(self) -> Result<CompiledGraph, GraphError> {
178        let GraphBuilder {
179            resources,
180            mut passes,
181        } = self;
182        let n_passes = passes.len();
183        let n_resources = resources.len();
184
185        // Step 1: presenter validation
186        let presenters: Vec<usize> = passes
187            .iter()
188            .enumerate()
189            .filter(|(_, p)| p.presents)
190            .map(|(i, _)| i)
191            .collect();
192        match presenters.len() {
193            0 => return Err(GraphError::MissingPresenter),
194            1 => {}
195            n => return Err(GraphError::MultiplePresenters(n)),
196        }
197
198        // Step 2: build (resource, version) -> writer-pass lookup
199        let mut writer_of: HashMap<(ResourceId, u32), usize> = HashMap::new();
200        for (i, pass) in passes.iter().enumerate() {
201            for w in &pass.writes {
202                // Each (resource, version) pair has exactly one writer
203                // by construction (write_texture bumps the version, so
204                // two passes can't claim the same one).
205                writer_of.insert((w.resource, w.version), i);
206            }
207        }
208
209        // Step 3: validate every read has a producer
210        // An imported resource at version 0 has an *implicit* producer
211        // (the engine that owns the GPU object). Reads of (imported, v0)
212        // are therefore always legal even though no graph pass writes
213        // them. Reads of (transient, v0) and reads at any version > 0
214        // still need a real producer in the same graph.
215        for pass in passes.iter() {
216            for r in &pass.reads {
217                if writer_of.contains_key(&(r.resource, r.version)) {
218                    continue;
219                }
220                let decl = &resources[r.resource.index()];
221                let implicit_producer = r.version == 0 && decl.origin() == ResourceOrigin::Imported;
222                if !implicit_producer {
223                    return Err(GraphError::MissingProducer {
224                        pass: pass.id,
225                        resource_label: decl.label(),
226                        version: r.version,
227                    });
228                }
229            }
230        }
231
232        // Step 4: build dependency edges and run Kahn's toposort
233        // edges[i] = list of passes that depend on i (must run after i).
234        let mut edges: Vec<Vec<usize>> = vec![Vec::new(); n_passes];
235        let mut in_degree: Vec<usize> = vec![0; n_passes];
236        let add_edge =
237            |from: usize, to: usize, edges: &mut [Vec<usize>], in_degree: &mut [usize]| {
238                if from != to {
239                    edges[from].push(to);
240                    in_degree[to] += 1;
241                }
242            };
243
244        // Build a (resource, version) → [reader pass_idxs] lookup so the
245        // WAR step below can find prior readers without rescanning every
246        // pass per write.
247        let mut readers_of: HashMap<(ResourceId, u32), Vec<usize>> = HashMap::new();
248        for (i, pass) in passes.iter().enumerate() {
249            for r in &pass.reads {
250                readers_of
251                    .entry((r.resource, r.version))
252                    .or_default()
253                    .push(i);
254            }
255        }
256
257        for (pass_idx, pass) in passes.iter().enumerate() {
258            // Read-after-write: each read of (resource, version) requires
259            // the pass that wrote (resource, version) to precede us.
260            for r in &pass.reads {
261                if let Some(&w) = writer_of.get(&(r.resource, r.version)) {
262                    add_edge(w, pass_idx, &mut edges, &mut in_degree);
263                }
264            }
265            // Write-after-write: a write that produced version V depends
266            // on the pass that produced version V-1 (the prior content
267            // matters for blend-style writes; for plain overwrites it
268            // doesn't but the ordering is still correct).
269            for w in &pass.writes {
270                if w.version > 1
271                    && let Some(&prev_writer) = writer_of.get(&(w.resource, w.version - 1))
272                {
273                    add_edge(prev_writer, pass_idx, &mut edges, &mut in_degree);
274                }
275            }
276            // Write-after-read: a write that produces version V requires
277            // every reader of version V-1 to precede us. Without this,
278            // a reader of the pre-write version could otherwise be
279            // re-ordered after the writer, sampling stale-from-the-other-
280            // direction data. In the Metal frame this fires on
281            // AutoExposure (reads `hdr_resolve_v1` produced by Main)
282            // pinning before Decals / Fog / ParticlesDraw (which bump
283            // hdr_resolve to v2+). Self-edges are filtered by add_edge.
284            for w in &pass.writes {
285                if w.version > 0
286                    && let Some(readers) = readers_of.get(&(w.resource, w.version - 1))
287                {
288                    for &reader in readers {
289                        add_edge(reader, pass_idx, &mut edges, &mut in_degree);
290                    }
291                }
292            }
293        }
294
295        // Stable order: when two passes are both ready, pick the one
296        // declared first. A min-heap on the original pass index achieves
297        // this without rescanning the ready set.
298        let mut ready: BinaryHeap<Reverse<usize>> = (0..n_passes)
299            .filter(|&i| in_degree[i] == 0)
300            .map(Reverse)
301            .collect();
302        let mut order: Vec<usize> = Vec::with_capacity(n_passes);
303        while let Some(Reverse(idx)) = ready.pop() {
304            order.push(idx);
305            for &neighbor in &edges[idx] {
306                in_degree[neighbor] -= 1;
307                if in_degree[neighbor] == 0 {
308                    ready.push(Reverse(neighbor));
309                }
310            }
311        }
312        if order.len() != n_passes {
313            return Err(GraphError::Cycle);
314        }
315
316        // Step 5: realise compiled passes in execution order
317        let mut compiled_passes: Vec<CompiledPass> = order
318            .iter()
319            .map(|&orig_idx| {
320                // `order` is a permutation, so each pass is realised exactly
321                // once and nothing reads the declaration after this.
322                let decl = &mut passes[orig_idx];
323                CompiledPass {
324                    id: decl.id,
325                    kind: decl.kind,
326                    reads: core::mem::take(&mut decl.reads),
327                    writes: core::mem::take(&mut decl.writes),
328                    presents: decl.presents,
329                    barriers_before: Vec::new(),
330                }
331            })
332            .collect();
333
334        // Step 6: derive per-pass barriers
335        derive_barriers(&mut compiled_passes, n_resources);
336
337        // Step 7: compute resource lifetimes
338        let mut lifetimes: Vec<Option<PassRange>> = vec![None; n_resources];
339        for (sorted_idx, pass) in compiled_passes.iter().enumerate() {
340            for v in pass.writes.iter().chain(pass.reads.iter()) {
341                let i = v.resource.index();
342                let merged = match lifetimes[i] {
343                    None => PassRange {
344                        first: sorted_idx,
345                        last: sorted_idx,
346                    },
347                    Some(PassRange { first, .. }) => PassRange {
348                        first,
349                        last: sorted_idx,
350                    },
351                };
352                lifetimes[i] = Some(merged);
353            }
354        }
355
356        let compiled_resources: Vec<CompiledResource> = resources
357            .into_iter()
358            .enumerate()
359            .map(|(i, decl)| {
360                // A resource that's declared but never touched gets a
361                // degenerate `[0, 0]` lifetime; the executor can treat
362                // it as a leak warning later.
363                let lifetime = lifetimes[i].unwrap_or(PassRange { first: 0, last: 0 });
364                CompiledResource {
365                    label: decl.label(),
366                    origin: decl.origin(),
367                    lifetime,
368                    tex_desc: decl.texture_desc(),
369                    buf_desc: decl.buffer_desc(),
370                }
371            })
372            .collect();
373
374        Ok(CompiledGraph {
375            passes: compiled_passes,
376            resources: compiled_resources,
377        })
378    }
379}
380
381// Derive each pass's `barriers_before` from the per-resource state machine
382// (Undefined -> Read -> Write). Emit a `BarrierOp` whenever a resource's
383// effective access (Write if the pass writes it, else Read) differs from its
384// prior state.
385//
386// Effective-access rule: a pass that writes a resource leaves it in `Write`
387// state regardless of whether it also reads it (blend-style read-modify-write
388// is handled by the backend's render-pass setup, not by an intra-pass
389// barrier).
390//
391// Read-stage union: a `* -> Read` barrier carries the stage union of the WHOLE
392// following read-run (every pass that reads this version before the next
393// writer), and a `Read -> Write` barrier carries the prior run's union. This is
394// why the work is done per resource rather than per pass: a write read by a
395// compute consumer and a fragment consumer needs ONE producer barrier that
396// makes the write visible to both stages, so the deriver must see the whole run
397// before emitting that first barrier. A per-consumer read-to-read barrier would
398// not carry the producing write and so would not synchronise the second stage.
399fn derive_barriers(passes: &mut [CompiledPass], n_resources: usize) {
400    // Per-pass effective access to one resource: a write (which wins over any
401    // read the same pass declares) or a read in the pass's shader stage.
402    #[derive(Copy, Clone)]
403    enum Eff {
404        Write,
405        Read(ReadStages),
406    }
407
408    // Build each resource's timeline: its (pass index, effective access) pairs
409    // in pass order. The outer loop is ascending pass index and each pass
410    // contributes at most one entry per resource, so every timeline stays
411    // sorted by pass index without an explicit sort.
412    let mut timeline: Vec<Vec<(usize, Eff)>> = (0..n_resources).map(|_| Vec::new()).collect();
413    for (i, pass) in passes.iter().enumerate() {
414        let stage = ReadStages::for_pass_kind(pass.kind);
415        let mut access: HashMap<ResourceId, Eff> = HashMap::new();
416        for r in &pass.reads {
417            access.entry(r.resource).or_insert(Eff::Read(stage));
418        }
419        for w in &pass.writes {
420            access.insert(w.resource, Eff::Write);
421        }
422        // Push in resource-index order so each pass's `barriers_before` ends up
423        // sorted by resource index (the outer resource loop below is ascending),
424        // matching the deterministic order the executor expects.
425        let mut touched: Vec<(ResourceId, Eff)> = access.into_iter().collect();
426        touched.sort_by_key(|(r, _)| r.0);
427        for (res, eff) in touched {
428            timeline[res.index()].push((i, eff));
429        }
430    }
431
432    // Walk each resource's timeline, tracking its running state and the current
433    // read-run's stage union, emitting a barrier on each state change.
434    for (r_idx, entries) in timeline.iter().enumerate() {
435        let resource = ResourceId(r_idx as u32);
436        let mut state = ResourceState::Undefined;
437        let mut run_stages = ReadStages::empty();
438
439        for (k, &(pass_idx, eff)) in entries.iter().enumerate() {
440            match eff {
441                Eff::Write => {
442                    // Emitted for every write, including a write that follows a
443                    // write. Each pass contributes at most one timeline entry per
444                    // resource, so back-to-back `Write` entries are always
445                    // different passes, and their ordering is not implied by
446                    // anything else: on the read-modify-write chains (hdr_resolve
447                    // through the decoration passes) each pass records into its own
448                    // command buffer, and command buffers in one submission may
449                    // overlap in execution. The resulting `Write -> Write` op is a
450                    // pure execution + memory dependency with no state change,
451                    // which each backend translates by its own rules.
452                    let read_stages = if state == ResourceState::Read {
453                        // A `Read -> Write` carries the prior run's union: the
454                        // write must wait on every reader.
455                        run_stages
456                    } else {
457                        // `Undefined -> Write` and `Write -> Write` have no Read
458                        // side.
459                        ReadStages::empty()
460                    };
461                    passes[pass_idx].barriers_before.push(BarrierOp {
462                        resource,
463                        from: state,
464                        to: ResourceState::Write,
465                        read_stages,
466                    });
467                    state = ResourceState::Write;
468                    run_stages = ReadStages::empty();
469                }
470                Eff::Read(_) => {
471                    if state != ResourceState::Read {
472                        // First read of a new run: union the contiguous run's
473                        // stages so the one producer barrier covers them all.
474                        let mut run = ReadStages::empty();
475                        for &(_, e) in entries[k..].iter() {
476                            match e {
477                                Eff::Read(s) => run = run.union(s),
478                                Eff::Write => break,
479                            }
480                        }
481                        passes[pass_idx].barriers_before.push(BarrierOp {
482                            resource,
483                            from: state,
484                            to: ResourceState::Read,
485                            read_stages: run,
486                        });
487                        state = ResourceState::Read;
488                        run_stages = run;
489                    }
490                    // Continuation read: the run's barrier already carries this
491                    // stage (unioned above), so emit nothing.
492                }
493            }
494        }
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::super::types::{BufferUsage, PixelFormat, TextureDesc, TextureSize, TextureUsage};
501    use super::*;
502    use crate::render_graph::builder::GraphBuilder;
503    use crate::render_graph::passes::PassId;
504    use crate::render_graph::types::{BufferDesc, PassKind};
505
506    fn tex() -> TextureDesc {
507        TextureDesc::texture_2d(
508            TextureSize::Drawable,
509            TextureSize::Drawable,
510            PixelFormat::Rgba16Float,
511            TextureUsage::SHADER_READ | TextureUsage::RENDER_TARGET,
512        )
513    }
514
515    fn buf() -> BufferDesc {
516        BufferDesc {
517            size_bytes: None,
518            usage: BufferUsage::STORAGE,
519        }
520    }
521
522    #[test]
523    fn linear_chain_toposorts_in_declared_order() {
524        // A writes T -> B reads T writes U -> C reads U presents.
525        let mut b = GraphBuilder::new();
526        let t = b.create_texture("t", tex());
527        let u = b.create_texture("u", tex());
528
529        let t1 = b
530            .add_pass(PassId::Shadow, PassKind::Render)
531            .write_texture(t);
532        let u1 = {
533            let mut p = b.add_pass(PassId::Main, PassKind::Render);
534            p.read_texture(t1);
535            p.write_texture(u)
536        };
537        b.add_pass(PassId::Composite, PassKind::Render)
538            .read_texture(u1)
539            .presents();
540
541        let g = b.compile().expect("graph compiles");
542        let order: Vec<PassId> = g.passes.iter().map(|p| p.id).collect();
543        assert_eq!(order, vec![PassId::Shadow, PassId::Main, PassId::Composite]);
544    }
545
546    #[test]
547    fn diamond_toposorts_with_stable_tiebreak() {
548        // A writes T; B reads T writes U; C reads T writes V; D reads U+V presents.
549        // B and C are both ready after A; declared first wins, so the
550        // order is [A, B, C, D].
551        let mut b = GraphBuilder::new();
552        let t = b.create_texture("t", tex());
553        let u = b.create_texture("u", tex());
554        let v = b.create_texture("v", tex());
555
556        let t1 = b
557            .add_pass(PassId::Shadow, PassKind::Render)
558            .write_texture(t);
559        let u1 = {
560            let mut p = b.add_pass(PassId::Main, PassKind::Render);
561            p.read_texture(t1);
562            p.write_texture(u)
563        };
564        let v1 = {
565            let mut p = b.add_pass(PassId::SsaoKernel, PassKind::Render);
566            p.read_texture(t1);
567            p.write_texture(v)
568        };
569        b.add_pass(PassId::Composite, PassKind::Render)
570            .read_texture(u1)
571            .read_texture(v1)
572            .presents();
573
574        let g = b.compile().expect("graph compiles");
575        let order: Vec<PassId> = g.passes.iter().map(|p| p.id).collect();
576        // Shadow must precede both Main and SsaoKernel (they read its
577        // output); Composite must come last (it reads both their
578        // outputs). The stable tie-break picks Main over SsaoKernel
579        // because Main was declared first.
580        assert_eq!(
581            order,
582            vec![
583                PassId::Shadow,
584                PassId::Main,
585                PassId::SsaoKernel,
586                PassId::Composite,
587            ]
588        );
589    }
590
591    #[test]
592    fn read_modify_write_chain_orders_correctly() {
593        // A writes hdr v1; B reads v1 writes v2 (decals); C reads v2 writes v3
594        // (fog); D reads v3 presents. The version-bump-implies-edge rule
595        // forces strict serial order.
596        let mut b = GraphBuilder::new();
597        let hdr = b.create_texture("hdr", tex());
598
599        let v1 = b
600            .add_pass(PassId::Main, PassKind::Render)
601            .write_texture(hdr);
602        let v2 = {
603            let mut p = b.add_pass(PassId::Decals, PassKind::Render);
604            p.read_texture(v1);
605            p.write_texture(v1)
606        };
607        let v3 = {
608            let mut p = b.add_pass(PassId::Fog, PassKind::Render);
609            p.read_texture(v2);
610            p.write_texture(v2)
611        };
612        b.add_pass(PassId::Composite, PassKind::Render)
613            .read_texture(v3)
614            .presents();
615
616        let g = b.compile().expect("graph compiles");
617        let order: Vec<PassId> = g.passes.iter().map(|p| p.id).collect();
618        assert_eq!(
619            order,
620            vec![PassId::Main, PassId::Decals, PassId::Fog, PassId::Composite,]
621        );
622    }
623
624    #[test]
625    fn war_exposes_cross_rmw_cycle() {
626        // Two passes RMW different resources, each reading the other's
627        // pre-write version: Decals reads y_v1 + writes x_v1 → x_v2;
628        // SsaoBlur reads x_v1 + writes y_v1 → y_v2. With WAR enforced,
629        // each write needs the *other* pass to have completed its read
630        // first, giving SsaoBlur → Decals (Decals's write of x_v2
631        // depends on SsaoBlur reading x_v1) AND Decals → SsaoBlur
632        // (SsaoBlur's write of y_v2 depends on Decals reading y_v1).
633        // That's a cycle, and it's a real one: there's no valid serial
634        // order for this pattern.
635        let mut b = GraphBuilder::new();
636        let x = b.create_texture("x", tex());
637        let y = b.create_texture("y", tex());
638
639        let x1 = b.add_pass(PassId::Main, PassKind::Render).write_texture(x);
640        let y1 = b.add_pass(PassId::Fog, PassKind::Render).write_texture(y);
641
642        let _x2 = {
643            let mut p = b.add_pass(PassId::Decals, PassKind::Render);
644            p.read_texture(y1);
645            p.write_texture(x1)
646        };
647        let _y2 = {
648            let mut p = b.add_pass(PassId::SsaoBlur, PassKind::Render);
649            p.read_texture(x1);
650            p.write_texture(y1)
651        };
652        b.add_pass(PassId::Composite, PassKind::Render).presents();
653
654        match b.compile() {
655            Err(GraphError::Cycle) => {}
656            other => panic!("expected Cycle, got {:?}", other),
657        }
658    }
659
660    #[test]
661    fn mutual_write_cycle_errors() {
662        // A writes X v1 reads Y v2; B writes Y v1 reads X v2; C writes X v2;
663        // D writes Y v2. C and D depend on A and B respectively (WAW on the
664        // version bump). A reads Y v2 → depends on D. B reads X v2 → depends
665        // on C. So: A -> C, B -> D, A -> D, B -> C, D -> A, C -> B. Cycle.
666        let mut b = GraphBuilder::new();
667        let x = b.create_texture("x", tex());
668        let y = b.create_texture("y", tex());
669
670        // First writes establish v1 for both.
671        let x1 = b.add_pass(PassId::Main, PassKind::Render).write_texture(x);
672        let y1 = b.add_pass(PassId::Fog, PassKind::Render).write_texture(y);
673
674        // Second writes claim v2 for both, declaring the cross-dependency.
675        // Decals reads y v2 (will exist), writes x v2.
676        // SsaoBlur reads x v2 (will exist), writes y v2.
677        // Each "reads v2" requires the other pass to have written v2 first.
678        // Mutual.
679        {
680            let mut p = b.add_pass(PassId::Decals, PassKind::Render);
681            // Read of v2 of y; producer is SsaoBlur (pass index 3).
682            p.read_texture(super::super::types::TextureHandle {
683                resource: y1.resource,
684                version: 2,
685            });
686            p.write_texture(x1);
687        }
688        {
689            let mut p = b.add_pass(PassId::SsaoBlur, PassKind::Render);
690            p.read_texture(super::super::types::TextureHandle {
691                resource: x1.resource,
692                version: 2,
693            });
694            p.write_texture(y1);
695        }
696        b.add_pass(PassId::Composite, PassKind::Render).presents();
697
698        match b.compile() {
699            Err(GraphError::Cycle) => {}
700            other => panic!("expected Cycle, got {:?}", other),
701        }
702    }
703
704    #[test]
705    fn war_edges_pin_reader_before_writer() {
706        // Main writes hdr_resolve v1. AutoExposure reads v1. Decals
707        // writes v1 → v2. Without WAR edges, toposort could place
708        // Decals before AutoExposure (Decals → Main → AutoExposure has
709        // no edge constraint either way). WAR forces AutoExposure to
710        // precede Decals because Decals's write of v2 depends on all
711        // readers of v1 completing first.
712        //
713        // Declaration order in this test deliberately puts Decals
714        // BEFORE AutoExposure so the toposort can't rely on it. Only
715        // the WAR edge gets us the right order.
716        let mut b = GraphBuilder::new();
717        let hdr = b.create_texture("hdr_resolve", tex());
718
719        let hdr_v1 = b
720            .add_pass(PassId::Main, PassKind::Render)
721            .write_texture(hdr);
722
723        // Decals declared before AutoExposure on purpose; the WAR edge
724        // from AutoExposure to Decals must override declaration order.
725        let _hdr_v2 = b
726            .add_pass(PassId::Decals, PassKind::Render)
727            .write_texture(hdr_v1);
728
729        b.add_pass(PassId::AutoExposure, PassKind::Compute)
730            .read_texture(hdr_v1);
731
732        b.add_pass(PassId::Composite, PassKind::Render).presents();
733
734        let g = b.compile().expect("compiles");
735        let order: Vec<PassId> = g.passes.iter().map(|p| p.id).collect();
736        // Main first, then AutoExposure (WAR-pinned before Decals),
737        // then Decals, then Composite.
738        assert_eq!(
739            order,
740            vec![
741                PassId::Main,
742                PassId::AutoExposure,
743                PassId::Decals,
744                PassId::Composite,
745            ]
746        );
747    }
748
749    #[test]
750    fn imported_v0_read_does_not_error() {
751        // The engine owns imported resources, so reading them at version
752        // 0 (their initial / engine-produced version) is always legal,
753        // even when no graph pass writes them. Mirrors how the Main pass
754        // reads env_irradiance / env_prefilter cubemaps the engine
755        // uploaded at init.
756        let mut b = GraphBuilder::new();
757        let env = b.import_texture("env", tex());
758        let scene = b.create_texture("scene", tex());
759        {
760            let mut p = b.add_pass(PassId::Main, PassKind::Render);
761            p.read_texture(env);
762            p.write_texture(scene);
763        }
764        b.add_pass(PassId::Composite, PassKind::Render)
765            .read_texture(super::super::types::TextureHandle {
766                resource: scene.resource,
767                version: 1,
768            })
769            .presents();
770
771        let g = b.compile().expect("imported v0 read should compile");
772        let order: Vec<PassId> = g.passes.iter().map(|p| p.id).collect();
773        assert_eq!(order, vec![PassId::Main, PassId::Composite]);
774    }
775
776    #[test]
777    fn transient_v0_read_still_errors() {
778        // The imported-v0 escape hatch only covers imported resources.
779        // A transient declared via create_texture and then read without
780        // a write is a real bug: the engine has no implicit producer.
781        let mut b = GraphBuilder::new();
782        let t = b.create_texture("scratch", tex());
783        b.add_pass(PassId::Composite, PassKind::Render)
784            .read_texture(t)
785            .presents();
786        match b.compile() {
787            Err(GraphError::MissingProducer {
788                pass: PassId::Composite,
789                resource_label: "scratch",
790                version: 0,
791            }) => {}
792            other => panic!("expected MissingProducer for transient v0, got {:?}", other),
793        }
794    }
795
796    #[test]
797    fn missing_producer_errors() {
798        // Read of a resource version that was never written.
799        let mut b = GraphBuilder::new();
800        let t = b.create_texture("t", tex());
801
802        // Skip the write; directly synthesise a handle at version 1
803        // (which write_texture would have returned).
804        let phantom = super::super::types::TextureHandle {
805            resource: t.resource,
806            version: 1,
807        };
808        b.add_pass(PassId::Composite, PassKind::Render)
809            .read_texture(phantom)
810            .presents();
811
812        match b.compile() {
813            Err(GraphError::MissingProducer {
814                pass: PassId::Composite,
815                resource_label: "t",
816                version: 1,
817            }) => {}
818            other => panic!("expected MissingProducer, got {:?}", other),
819        }
820    }
821
822    #[test]
823    fn no_presenter_errors() {
824        let mut b = GraphBuilder::new();
825        let t = b.create_texture("t", tex());
826        b.add_pass(PassId::Main, PassKind::Render).write_texture(t);
827        // No presents()!
828        match b.compile() {
829            Err(GraphError::MissingPresenter) => {}
830            other => panic!("expected MissingPresenter, got {:?}", other),
831        }
832    }
833
834    #[test]
835    fn multiple_presenters_errors() {
836        let mut b = GraphBuilder::new();
837        let t = b.create_texture("t", tex());
838        {
839            let mut p = b.add_pass(PassId::Main, PassKind::Render);
840            p.presents();
841            let _ = p.write_texture(t);
842        }
843        b.add_pass(PassId::Composite, PassKind::Render).presents();
844        match b.compile() {
845            Err(GraphError::MultiplePresenters(2)) => {}
846            other => panic!("expected MultiplePresenters(2), got {:?}", other),
847        }
848    }
849
850    #[test]
851    fn barriers_emit_on_state_transitions() {
852        // Main writes T -> Composite reads T. The barrier on Composite
853        // should be Undefined→...→Write (Main's effective state is
854        // Write because it writes) and then Write→Read (Composite reads).
855        let mut b = GraphBuilder::new();
856        let t = b.create_texture("t", tex());
857        let t1 = b.add_pass(PassId::Main, PassKind::Render).write_texture(t);
858        b.add_pass(PassId::Composite, PassKind::Render)
859            .read_texture(t1)
860            .presents();
861
862        let g = b.compile().expect("compiles");
863        // Pass 0 (Main): T transitions Undefined -> Write, one barrier.
864        assert_eq!(g.passes[0].barriers_before.len(), 1);
865        assert_eq!(
866            g.passes[0].barriers_before[0].from,
867            ResourceState::Undefined
868        );
869        assert_eq!(g.passes[0].barriers_before[0].to, ResourceState::Write);
870        // Pass 1 (Composite): T transitions Write -> Read, one barrier.
871        assert_eq!(g.passes[1].barriers_before.len(), 1);
872        assert_eq!(g.passes[1].barriers_before[0].from, ResourceState::Write);
873        assert_eq!(g.passes[1].barriers_before[0].to, ResourceState::Read);
874    }
875
876    #[test]
877    fn pass_barriers_for_filters_by_label() {
878        // Main writes "keep" + "skip"; Composite reads both. The allowlist
879        // ["keep"] must select only "keep"'s barrier on each pass, dropping
880        // "skip"'s entirely.
881        let mut b = GraphBuilder::new();
882        let keep = b.create_texture("keep", tex());
883        let skip = b.create_texture("skip", tex());
884        let (keep1, skip1) = {
885            let mut p = b.add_pass(PassId::Main, PassKind::Render);
886            (p.write_texture(keep), p.write_texture(skip))
887        };
888        b.add_pass(PassId::Composite, PassKind::Render)
889            .read_texture(keep1)
890            .read_texture(skip1)
891            .presents();
892
893        let g = b.compile().expect("compiles");
894        // Main writes both: two barriers total, one kept by the allowlist.
895        let main = &g.passes[0];
896        let kept = g.pass_barriers_for(main, &["keep"]);
897        assert_eq!(kept.len(), 1);
898        assert_eq!(kept[0].0, "keep");
899        assert_eq!(kept[0].1.source_state(), ResourceState::Undefined);
900        assert_eq!(kept[0].1.to_state(), ResourceState::Write);
901        // Composite reads both: "keep" transitions Write -> Read.
902        let composite = &g.passes[1];
903        let kept = g.pass_barriers_for(composite, &["keep"]);
904        assert_eq!(kept.len(), 1);
905        assert_eq!(kept[0].1.source_state(), ResourceState::Write);
906        assert_eq!(kept[0].1.to_state(), ResourceState::Read);
907        // An empty allowlist keeps nothing; an unknown label keeps nothing.
908        assert!(g.pass_barriers_for(main, &[]).is_empty());
909        assert!(g.pass_barriers_for(main, &["nope"]).is_empty());
910    }
911
912    #[test]
913    fn consecutive_reads_coalesce_no_barriers() {
914        // A writes T. B reads T. C reads T. D reads T presents.
915        // Barriers: A=W, B=W->R, C=R->R (none), D=R->R (none).
916        let mut b = GraphBuilder::new();
917        let t = b.create_texture("t", tex());
918        let t1 = b.add_pass(PassId::Main, PassKind::Render).write_texture(t);
919        b.add_pass(PassId::Decals, PassKind::Render)
920            .read_texture(t1);
921        b.add_pass(PassId::Fog, PassKind::Render).read_texture(t1);
922        b.add_pass(PassId::Composite, PassKind::Render)
923            .read_texture(t1)
924            .presents();
925
926        let g = b.compile().expect("compiles");
927        // pass[0] = Main: 1 barrier (Undefined -> Write)
928        assert_eq!(g.passes[0].barriers_before.len(), 1);
929        // pass[1] = Decals: 1 barrier (Write -> Read)
930        assert_eq!(g.passes[1].barriers_before.len(), 1);
931        // pass[2] = Fog: 0 barriers (already Read)
932        assert_eq!(g.passes[2].barriers_before.len(), 0);
933        // pass[3] = Composite: 0 barriers
934        assert_eq!(g.passes[3].barriers_before.len(), 0);
935    }
936
937    #[test]
938    fn lifetime_intervals_span_first_write_to_last_read() {
939        // Main(0) writes T; Decals(1) reads T; Fog(2) ignores T; Composite(3) reads T.
940        // T lifetime: [0, 3].
941        let mut b = GraphBuilder::new();
942        let t = b.create_texture("t", tex());
943        let unrelated = b.create_texture("u", tex());
944
945        let t1 = b.add_pass(PassId::Main, PassKind::Render).write_texture(t);
946        b.add_pass(PassId::Decals, PassKind::Render)
947            .read_texture(t1);
948        b.add_pass(PassId::Fog, PassKind::Render)
949            .write_texture(unrelated);
950        b.add_pass(PassId::Composite, PassKind::Render)
951            .read_texture(t1)
952            .presents();
953
954        let g = b.compile().expect("compiles");
955        let t_idx = t.resource.index();
956        let u_idx = unrelated.resource.index();
957        assert_eq!(g.resources[t_idx].lifetime.first, 0);
958        assert_eq!(g.resources[t_idx].lifetime.last, 3);
959        assert_eq!(g.resources[u_idx].lifetime.first, 2);
960        assert_eq!(g.resources[u_idx].lifetime.last, 2);
961    }
962
963    #[test]
964    fn buffer_dep_edges_work_too() {
965        // GPU-cull-style: cull writes draw_args (buffer), main reads it.
966        let mut b = GraphBuilder::new();
967        let draw_args = b.create_buffer("draw_args", buf());
968        let scene = b.create_texture("scene", tex());
969
970        let args1 = b
971            .add_pass(PassId::Cull, PassKind::Compute)
972            .write_buffer(draw_args);
973        let scene1 = {
974            let mut p = b.add_pass(PassId::Main, PassKind::Render);
975            p.read_buffer(args1);
976            p.write_texture(scene)
977        };
978        b.add_pass(PassId::Composite, PassKind::Render)
979            .read_texture(scene1)
980            .presents();
981
982        let g = b.compile().expect("compiles");
983        let order: Vec<PassId> = g.passes.iter().map(|p| p.id).collect();
984        assert_eq!(order, vec![PassId::Cull, PassId::Main, PassId::Composite]);
985    }
986
987    fn find(g: &CompiledGraph, id: PassId) -> &CompiledPass {
988        g.passes.iter().find(|p| p.id == id).expect("pass present")
989    }
990
991    #[test]
992    fn mixed_stage_read_run_unions_consumer_stages() {
993        // Main writes hdr; AutoExposure (compute) and Composite (render) read
994        // it. The single producer barrier on the first reader must carry BOTH
995        // stages so the write is made visible to the compute and the fragment
996        // consumer; the second reader coalesces (no barrier).
997        let mut b = GraphBuilder::new();
998        let hdr = b.create_texture("hdr", tex());
999        let hdr_v1 = b
1000            .add_pass(PassId::Main, PassKind::Render)
1001            .write_texture(hdr);
1002        b.add_pass(PassId::AutoExposure, PassKind::Compute)
1003            .read_texture(hdr_v1);
1004        b.add_pass(PassId::Composite, PassKind::Render)
1005            .read_texture(hdr_v1)
1006            .presents();
1007
1008        let g = b.compile().expect("compiles");
1009        // AutoExposure is the first reader: one Write -> Read barrier carrying
1010        // the whole run's stage union.
1011        let ae = find(&g, PassId::AutoExposure);
1012        assert_eq!(ae.barriers_before.len(), 1);
1013        assert_eq!(ae.barriers_before[0].source_state(), ResourceState::Write);
1014        assert_eq!(ae.barriers_before[0].to_state(), ResourceState::Read);
1015        let rs = ae.barriers_before[0].read_stages();
1016        assert!(rs.contains(ReadStages::COMPUTE));
1017        assert!(rs.contains(ReadStages::FRAGMENT));
1018        // Composite coalesces into the run: no hdr barrier.
1019        assert_eq!(find(&g, PassId::Composite).barriers_before.len(), 0);
1020    }
1021
1022    #[test]
1023    fn fragment_read_carries_only_fragment_stage() {
1024        // A lone render-pass consumer carries FRAGMENT only (the common case;
1025        // the existing migrated resources all look like this).
1026        let mut b = GraphBuilder::new();
1027        let t = b.create_texture("t", tex());
1028        let t1 = b.add_pass(PassId::Main, PassKind::Render).write_texture(t);
1029        b.add_pass(PassId::Composite, PassKind::Render)
1030            .read_texture(t1)
1031            .presents();
1032
1033        let g = b.compile().expect("compiles");
1034        let comp = find(&g, PassId::Composite);
1035        assert_eq!(comp.barriers_before.len(), 1);
1036        let rs = comp.barriers_before[0].read_stages();
1037        assert!(rs.contains(ReadStages::FRAGMENT));
1038        assert!(!rs.contains(ReadStages::COMPUTE));
1039    }
1040
1041    #[test]
1042    fn compute_read_carries_only_compute_stage() {
1043        // Cull (compute) writes draw_args; AutoExposure (compute) reads it.
1044        // The reader's barrier carries COMPUTE only. A render presenter writes
1045        // an unrelated target so the graph is well-formed.
1046        let mut b = GraphBuilder::new();
1047        let args = b.create_buffer("draw_args", buf());
1048        let scene = b.create_texture("scene", tex());
1049        let args1 = b
1050            .add_pass(PassId::Cull, PassKind::Compute)
1051            .write_buffer(args);
1052        b.add_pass(PassId::AutoExposure, PassKind::Compute)
1053            .read_buffer(args1);
1054        {
1055            let mut p = b.add_pass(PassId::Composite, PassKind::Render);
1056            let _ = p.write_texture(scene);
1057            p.presents();
1058        }
1059
1060        let g = b.compile().expect("compiles");
1061        let ae = find(&g, PassId::AutoExposure);
1062        assert_eq!(ae.barriers_before.len(), 1);
1063        let rs = ae.barriers_before[0].read_stages();
1064        assert!(rs.contains(ReadStages::COMPUTE));
1065        assert!(!rs.contains(ReadStages::FRAGMENT));
1066    }
1067
1068    #[test]
1069    fn war_barrier_carries_prior_read_run_stage_union() {
1070        // Main writes hdr v1; AutoExposure (compute) and Fog (render) read v1;
1071        // SsaoBlur writes v1 -> v2 (write only). SsaoBlur's WAR barrier must
1072        // wait on BOTH readers, so it carries the prior run's union.
1073        let mut b = GraphBuilder::new();
1074        let hdr = b.create_texture("hdr", tex());
1075        let v1 = b
1076            .add_pass(PassId::Main, PassKind::Render)
1077            .write_texture(hdr);
1078        b.add_pass(PassId::AutoExposure, PassKind::Compute)
1079            .read_texture(v1);
1080        b.add_pass(PassId::Fog, PassKind::Render).read_texture(v1);
1081        let v2 = b
1082            .add_pass(PassId::SsaoBlur, PassKind::Render)
1083            .write_texture(v1);
1084        b.add_pass(PassId::Composite, PassKind::Render)
1085            .read_texture(v2)
1086            .presents();
1087
1088        let g = b.compile().expect("compiles");
1089        let blur = find(&g, PassId::SsaoBlur);
1090        assert_eq!(blur.barriers_before.len(), 1);
1091        assert_eq!(blur.barriers_before[0].source_state(), ResourceState::Read);
1092        assert_eq!(blur.barriers_before[0].to_state(), ResourceState::Write);
1093        let rs = blur.barriers_before[0].read_stages();
1094        assert!(rs.contains(ReadStages::COMPUTE));
1095        assert!(rs.contains(ReadStages::FRAGMENT));
1096    }
1097
1098    #[test]
1099    fn producer_write_barrier_has_empty_read_stages() {
1100        // A write-only producer transition (Undefined -> Write) has no Read
1101        // side, so its stage mask is empty.
1102        let mut b = GraphBuilder::new();
1103        let t = b.create_texture("t", tex());
1104        let t1 = b.add_pass(PassId::Main, PassKind::Render).write_texture(t);
1105        b.add_pass(PassId::Composite, PassKind::Render)
1106            .read_texture(t1)
1107            .presents();
1108
1109        let g = b.compile().expect("compiles");
1110        let main = find(&g, PassId::Main);
1111        assert_eq!(main.barriers_before.len(), 1);
1112        assert_eq!(main.barriers_before[0].to_state(), ResourceState::Write);
1113        assert!(main.barriers_before[0].read_stages().is_empty());
1114    }
1115}