Skip to main content

ezu_graph/
graph.rs

1//! The DAG itself: building, type-checking, topology, pad propagation.
2
3use std::collections::VecDeque;
4
5use indexmap::IndexMap;
6
7use crate::node::Node;
8use crate::port::PortKind;
9
10/// Identifier for a node in the [`Graph`]. Matches the key used in the
11/// style JSON's `nodes` object (e.g. `"water_paint"`).
12pub type NodeId = String;
13
14/// Compact internal index assigned during graph build. Stable for the
15/// lifetime of the graph; used by topo-sorted operations.
16pub type NodeIx = usize;
17
18#[derive(Debug, thiserror::Error)]
19pub enum BuildError {
20    #[error("unknown node reference `{from}` -> `{to}`")]
21    UnknownRef { from: NodeId, to: NodeId },
22
23    #[error("node `{node}` has no input port named `{port}`")]
24    UnknownPort { node: NodeId, port: String },
25
26    #[error("port `{node}.{port}` already connected")]
27    DuplicateEdge { node: NodeId, port: String },
28
29    #[error(
30        "type mismatch on `{node}.{port}`: expected one of [{}], source `{src}` produces {got}",
31        accepts.iter().map(|k| k.to_string()).collect::<Vec<_>>().join(", ")
32    )]
33    TypeMismatch {
34        node: NodeId,
35        port: String,
36        src: NodeId,
37        accepts: Vec<PortKind>,
38        got: PortKind,
39    },
40
41    #[error("required port `{node}.{port}` is not connected")]
42    MissingInput { node: NodeId, port: String },
43
44    #[error("cycle detected involving node `{0}`")]
45    Cycle(NodeId),
46
47    #[error("output node `{0}` is not in the graph")]
48    UnknownOutput(NodeId),
49
50    #[error("graph has no output node")]
51    NoOutput,
52
53    #[error(
54        "output node `{node}` produces `{got}`, but the document output must produce `raster` (canvas-padded). Pipe a sprite through `place`, `tiling`, or `stamp` first."
55    )]
56    OutputKindMismatch { node: NodeId, got: PortKind },
57
58    #[error("required pad ({required}) on node `{node}` exceeds limit ({limit})")]
59    PadExceeded {
60        node: NodeId,
61        required: u32,
62        limit: u32,
63    },
64}
65
66/// An edge in the DAG: `src.output` flows into `dst.inputs()[port_ix]`.
67#[derive(Debug, Clone, Copy)]
68pub struct Edge {
69    pub src: NodeIx,
70    pub dst: NodeIx,
71    pub dst_port: usize,
72}
73
74/// A built, type-checked DAG. Tile-independent; build once per style
75/// and evaluate many times.
76pub struct Graph {
77    nodes: IndexMap<NodeId, Box<dyn Node>>,
78    /// Per-node, per-input-port edge source (None if unconnected and
79    /// the port was optional).
80    incoming: Vec<Vec<Option<NodeIx>>>,
81    /// Adjacency for downstream walks: outgoing[src] -> list of dst.
82    outgoing: Vec<Vec<NodeIx>>,
83    /// Deduplicated downstream adjacency: outgoing_unique[src] -> each
84    /// distinct dst once, even when several of dst's ports read `src`.
85    /// Drives the readiness scheduler's per-dependency decrements.
86    outgoing_unique: Vec<Vec<NodeIx>>,
87    /// Count of distinct upstream nodes feeding each node.
88    indegree: Vec<usize>,
89    /// Output node index.
90    output: NodeIx,
91    /// Topological order, output last.
92    topo: Vec<NodeIx>,
93    /// Resolved output [`PortKind`] for every node, indexed by [`NodeIx`].
94    /// Polymorphic nodes (e.g. `blur` accepting both `Raster` and
95    /// `Sprite`) have their actual output kind decided here at build
96    /// time based on their connected inputs.
97    output_kinds: Vec<PortKind>,
98}
99
100/// Maximum allowed pad propagated to any node, in pixels. Prevents
101/// runaway blurs from demanding multi-tile buffers.
102pub const MAX_PAD: u32 = 256;
103
104/// Builder for constructing a [`Graph`] programmatically. The style
105/// parser will drive this from JSON later; tests use it directly.
106pub struct GraphBuilder {
107    nodes: IndexMap<NodeId, Box<dyn Node>>,
108    /// Pending edges, recorded by name; resolved at `build()` time.
109    edges: Vec<EdgeSpec>,
110    output: Option<NodeId>,
111}
112
113struct EdgeSpec {
114    src: NodeId,
115    dst: NodeId,
116    dst_port: String,
117}
118
119impl GraphBuilder {
120    pub fn new() -> Self {
121        Self {
122            nodes: IndexMap::new(),
123            edges: Vec::new(),
124            output: None,
125        }
126    }
127
128    pub fn add_node(&mut self, id: impl Into<NodeId>, node: Box<dyn Node>) -> &mut Self {
129        self.nodes.insert(id.into(), node);
130        self
131    }
132
133    pub fn connect(
134        &mut self,
135        src: impl Into<NodeId>,
136        dst: impl Into<NodeId>,
137        dst_port: impl Into<String>,
138    ) -> &mut Self {
139        self.edges.push(EdgeSpec {
140            src: src.into(),
141            dst: dst.into(),
142            dst_port: dst_port.into(),
143        });
144        self
145    }
146
147    pub fn set_output(&mut self, id: impl Into<NodeId>) -> &mut Self {
148        self.output = Some(id.into());
149        self
150    }
151
152    pub fn build(self) -> Result<Graph, BuildError> {
153        let n = self.nodes.len();
154        let mut incoming: Vec<Vec<Option<NodeIx>>> = self
155            .nodes
156            .values()
157            .map(|node| vec![None; node.inputs().len()])
158            .collect();
159        let mut outgoing: Vec<Vec<NodeIx>> = vec![Vec::new(); n];
160
161        let ix_of = |id: &str| -> Option<NodeIx> { self.nodes.get_index_of(id) };
162
163        // Pass 1: wire edges (no type check yet — output kinds may be
164        // polymorphic and only resolvable in topo order).
165        for edge in &self.edges {
166            let src_ix = ix_of(&edge.src).ok_or_else(|| BuildError::UnknownRef {
167                from: edge.src.clone(),
168                to: edge.dst.clone(),
169            })?;
170            let dst_ix = ix_of(&edge.dst).ok_or_else(|| BuildError::UnknownRef {
171                from: edge.src.clone(),
172                to: edge.dst.clone(),
173            })?;
174
175            let (_, dst_node) = self
176                .nodes
177                .get_index(dst_ix)
178                .expect("dst_ix came from ix_of and is in range");
179            let port_ix = dst_node
180                .inputs()
181                .iter()
182                .position(|p| p.name == edge.dst_port)
183                .ok_or_else(|| BuildError::UnknownPort {
184                    node: edge.dst.clone(),
185                    port: edge.dst_port.clone(),
186                })?;
187
188            if incoming[dst_ix][port_ix].is_some() {
189                return Err(BuildError::DuplicateEdge {
190                    node: edge.dst.clone(),
191                    port: edge.dst_port.clone(),
192                });
193            }
194
195            incoming[dst_ix][port_ix] = Some(src_ix);
196            outgoing[src_ix].push(dst_ix);
197        }
198
199        // Required-port check.
200        for (ix, (id, node)) in self.nodes.iter().enumerate() {
201            for (port_ix, port) in node.inputs().iter().enumerate() {
202                if !port.optional && incoming[ix][port_ix].is_none() {
203                    return Err(BuildError::MissingInput {
204                        node: id.clone(),
205                        port: port.name.to_string(),
206                    });
207                }
208            }
209        }
210
211        let output_id = self.output.ok_or(BuildError::NoOutput)?;
212        let output_ix = ix_of(&output_id).ok_or(BuildError::UnknownOutput(output_id.clone()))?;
213
214        let topo = topo_sort(n, &incoming, &self.nodes, output_ix)?;
215
216        // Pass 2: walk topo order, resolve each node's output kind from
217        // its (already-resolved) upstream kinds, and check the upstream
218        // kind against each input port's `accepts` list.
219        let mut output_kinds: Vec<PortKind> = vec![PortKind::Raster; n];
220        for &ix in &topo {
221            let (id, node) = self.nodes.get_index(ix).expect("ix from topo is in range");
222            let specs = node.inputs();
223            let mut input_kinds: Vec<Option<PortKind>> = Vec::with_capacity(specs.len());
224            for (port_ix, spec) in specs.iter().enumerate() {
225                match incoming[ix][port_ix] {
226                    Some(src_ix) => {
227                        let src_kind = output_kinds[src_ix];
228                        if !spec.accepts_kind(src_kind) {
229                            let (src_id, _) = self
230                                .nodes
231                                .get_index(src_ix)
232                                .expect("src_ix from incoming is in range");
233                            return Err(BuildError::TypeMismatch {
234                                node: id.clone(),
235                                port: spec.name.to_string(),
236                                src: src_id.clone(),
237                                accepts: spec.accepts.to_vec(),
238                                got: src_kind,
239                            });
240                        }
241                        input_kinds.push(Some(src_kind));
242                    }
243                    None => input_kinds.push(None),
244                }
245            }
246            output_kinds[ix] = node.output(&input_kinds);
247        }
248
249        // Document output must be a canvas-padded raster — anything
250        // smaller (e.g. a raw `Sprite`) will alias badly through the
251        // host's `raster_to_png` crop. Catch this at build time.
252        let output_kind = output_kinds[output_ix];
253        if output_kind != PortKind::Raster {
254            return Err(BuildError::OutputKindMismatch {
255                node: output_id.clone(),
256                got: output_kind,
257            });
258        }
259
260        // Deduplicated views used by the readiness scheduler: each node
261        // has one decrement per distinct upstream, and reaching zero
262        // triggers exactly one spawn per distinct downstream.
263        let mut outgoing_unique = outgoing.clone();
264        for dsts in &mut outgoing_unique {
265            dsts.sort_unstable();
266            dsts.dedup();
267        }
268        let indegree: Vec<usize> = incoming
269            .iter()
270            .map(|ports| {
271                let mut srcs: Vec<NodeIx> = ports.iter().filter_map(|p| *p).collect();
272                srcs.sort_unstable();
273                srcs.dedup();
274                srcs.len()
275            })
276            .collect();
277
278        Ok(Graph {
279            nodes: self.nodes,
280            incoming,
281            outgoing,
282            outgoing_unique,
283            indegree,
284            output: output_ix,
285            topo,
286            output_kinds,
287        })
288    }
289}
290
291impl Default for GraphBuilder {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297/// Evaluation order for the graph: a valid topological order, chosen to
298/// keep few intermediates alive at once.
299///
300/// Any topological order is correct, but they differ enormously in peak
301/// memory. Breadth-first (plain Kahn) evaluates every source, then every
302/// node above them, so a wide graph holds a full canvas-sized buffer for
303/// each parallel branch before the node that consumes them ever runs.
304/// Depth-first from the output instead finishes one input subtree before
305/// starting its sibling, so a branch's buffers are released as soon as
306/// their consumer has run.
307///
308/// Kahn still runs first, to detect cycles and to order any nodes the
309/// output does not depend on (which are appended, after everything the
310/// output needs).
311fn topo_sort(
312    n: usize,
313    incoming: &[Vec<Option<NodeIx>>],
314    nodes: &IndexMap<NodeId, Box<dyn Node>>,
315    output: NodeIx,
316) -> Result<Vec<NodeIx>, BuildError> {
317    let kahn = kahn_order(n, incoming, nodes)?;
318
319    // Depth-first post-order from the output: a node is emitted only
320    // after every node it reads. Iterative, so a deep chain cannot blow
321    // the stack.
322    let mut order = Vec::with_capacity(n);
323    let mut seen = vec![false; n];
324    let mut stack: Vec<(NodeIx, usize)> = vec![(output, 0)];
325    seen[output] = true;
326    while let Some((ix, port)) = stack.pop() {
327        // Ports are visited in declaration order, which for the
328        // accumulator-style nodes (`stack`, `blend`) means the running
329        // base is resolved before the layers composited onto it.
330        match incoming[ix].get(port) {
331            Some(&edge) => {
332                stack.push((ix, port + 1));
333                if let Some(src) = edge {
334                    if !seen[src] {
335                        seen[src] = true;
336                        stack.push((src, 0));
337                    }
338                }
339            }
340            None => order.push(ix),
341        }
342    }
343    // Nodes the output does not depend on: keep them, in Kahn order, so
344    // a document is still free to evaluate a node for its own sake.
345    order.extend(kahn.into_iter().filter(|&ix| !seen[ix]));
346    Ok(order)
347}
348
349fn kahn_order(
350    n: usize,
351    incoming: &[Vec<Option<NodeIx>>],
352    nodes: &IndexMap<NodeId, Box<dyn Node>>,
353) -> Result<Vec<NodeIx>, BuildError> {
354    // Kahn's algorithm over the unique upstream set per node.
355    let mut indegree: Vec<usize> = incoming
356        .iter()
357        .map(|ports| {
358            let mut srcs: Vec<NodeIx> = ports.iter().filter_map(|p| *p).collect();
359            srcs.sort_unstable();
360            srcs.dedup();
361            srcs.len()
362        })
363        .collect();
364
365    // Reverse adjacency: for each src, the unique dsts that depend on it.
366    let mut rev: Vec<Vec<NodeIx>> = vec![Vec::new(); n];
367    for (dst, ports) in incoming.iter().enumerate() {
368        let mut srcs: Vec<NodeIx> = ports.iter().filter_map(|p| *p).collect();
369        srcs.sort_unstable();
370        srcs.dedup();
371        for src in srcs {
372            rev[src].push(dst);
373        }
374    }
375
376    let mut queue: VecDeque<NodeIx> = (0..n).filter(|&i| indegree[i] == 0).collect();
377    let mut order = Vec::with_capacity(n);
378    while let Some(ix) = queue.pop_front() {
379        order.push(ix);
380        for &dst in &rev[ix] {
381            indegree[dst] -= 1;
382            if indegree[dst] == 0 {
383                queue.push_back(dst);
384            }
385        }
386    }
387
388    if order.len() != n {
389        // `order.len() != n` means at least one node still has incoming
390        // edges (otherwise topo would have queued it). Find one such
391        // node to name in the error.
392        let bad = (0..n)
393            .find(|&i| indegree[i] != 0)
394            .expect("order.len() != n implies some indegree is non-zero");
395        let (id, _) = nodes.get_index(bad).expect("bad < n is within nodes range");
396        return Err(BuildError::Cycle(id.clone()));
397    }
398    Ok(order)
399}
400
401impl std::fmt::Debug for Graph {
402    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403        let ids: Vec<&str> = self.nodes.keys().map(String::as_str).collect();
404        f.debug_struct("Graph")
405            .field("nodes", &ids)
406            .field("output", &self.node_id(self.output))
407            .field(
408                "topo",
409                &self
410                    .topo
411                    .iter()
412                    .map(|&i| self.node_id(i))
413                    .collect::<Vec<_>>(),
414            )
415            .finish()
416    }
417}
418
419impl Graph {
420    /// Number of nodes.
421    pub fn len(&self) -> usize {
422        self.nodes.len()
423    }
424
425    pub fn is_empty(&self) -> bool {
426        self.nodes.is_empty()
427    }
428
429    pub fn output(&self) -> NodeIx {
430        self.output
431    }
432
433    /// Topological order; output node is last.
434    pub fn topo_order(&self) -> &[NodeIx] {
435        &self.topo
436    }
437
438    /// The union of every node's [`Node::asset_inputs`], deduplicated and
439    /// ordered. A host consults this to learn which named bindings the
440    /// document's graph samples — including the `@<dx>,<dy>` neighbour
441    /// names (see [`crate::neighbor`]) a cross-tile node requests, so it
442    /// can fetch and bind exactly the neighbour tiles the graph needs
443    /// rather than the whole 3×3 window unconditionally.
444    pub fn asset_inputs(&self) -> std::collections::BTreeSet<String> {
445        self.nodes.values().flat_map(|n| n.asset_inputs()).collect()
446    }
447
448    pub fn node(&self, ix: NodeIx) -> &dyn Node {
449        self.nodes
450            .get_index(ix)
451            .expect("NodeIx is always within self.nodes range")
452            .1
453            .as_ref()
454    }
455
456    pub fn node_id(&self, ix: NodeIx) -> &str {
457        self.nodes
458            .get_index(ix)
459            .expect("NodeIx is always within self.nodes range")
460            .0
461    }
462
463    /// Look up a node's index by id.
464    pub fn index_of(&self, id: &str) -> Option<NodeIx> {
465        self.nodes.get_index_of(id)
466    }
467
468    /// Upstream nodes feeding `ix`, deduplicated.
469    pub fn upstream(&self, ix: NodeIx) -> impl Iterator<Item = NodeIx> + '_ {
470        let mut srcs: Vec<NodeIx> = self.incoming[ix].iter().filter_map(|p| *p).collect();
471        srcs.sort_unstable();
472        srcs.dedup();
473        srcs.into_iter()
474    }
475
476    /// Downstream nodes consuming `ix`'s output (may contain duplicates
477    /// if the same node connects multiple of its input ports to `ix`).
478    pub fn downstream(&self, ix: NodeIx) -> &[NodeIx] {
479        &self.outgoing[ix]
480    }
481
482    /// Distinct downstream nodes consuming `ix`'s output, each listed once.
483    pub fn downstream_unique(&self, ix: NodeIx) -> &[NodeIx] {
484        &self.outgoing_unique[ix]
485    }
486
487    /// Number of distinct upstream nodes feeding `ix` (0 for sources).
488    pub fn indegree(&self, ix: NodeIx) -> usize {
489        self.indegree[ix]
490    }
491
492    /// The source feeding `node.inputs()[port_ix]`, if connected.
493    pub fn incoming(&self, ix: NodeIx, port_ix: usize) -> Option<NodeIx> {
494        self.incoming[ix][port_ix]
495    }
496
497    /// Resolved output [`PortKind`] for `ix`. Decided at build time;
498    /// polymorphic nodes' kind is fixed once the graph is built.
499    pub fn output_kind(&self, ix: NodeIx) -> PortKind {
500        self.output_kinds[ix]
501    }
502
503    /// Compute the canvas padding each node must supply, given the
504    /// document-level `pad` requested at the output.
505    pub fn compute_pad(&self, doc_pad: u32) -> Result<Vec<u32>, BuildError> {
506        let mut required = vec![0u32; self.len()];
507        required[self.output] = doc_pad;
508        for &ix in self.topo.iter().rev() {
509            let down = required[ix];
510            let up = self.node(ix).required_pad(down);
511            if up > MAX_PAD {
512                return Err(BuildError::PadExceeded {
513                    node: self.node_id(ix).to_string(),
514                    required: up,
515                    limit: MAX_PAD,
516                });
517            }
518            for src in self.upstream(ix) {
519                required[src] = required[src].max(up);
520            }
521        }
522        Ok(required)
523    }
524}