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 topo = topo_sort(n, &incoming, &self.nodes)?;
212
213        // Pass 2: walk topo order, resolve each node's output kind from
214        // its (already-resolved) upstream kinds, and check the upstream
215        // kind against each input port's `accepts` list.
216        let mut output_kinds: Vec<PortKind> = vec![PortKind::Raster; n];
217        for &ix in &topo {
218            let (id, node) = self.nodes.get_index(ix).expect("ix from topo is in range");
219            let specs = node.inputs();
220            let mut input_kinds: Vec<Option<PortKind>> = Vec::with_capacity(specs.len());
221            for (port_ix, spec) in specs.iter().enumerate() {
222                match incoming[ix][port_ix] {
223                    Some(src_ix) => {
224                        let src_kind = output_kinds[src_ix];
225                        if !spec.accepts_kind(src_kind) {
226                            let (src_id, _) = self
227                                .nodes
228                                .get_index(src_ix)
229                                .expect("src_ix from incoming is in range");
230                            return Err(BuildError::TypeMismatch {
231                                node: id.clone(),
232                                port: spec.name.to_string(),
233                                src: src_id.clone(),
234                                accepts: spec.accepts.to_vec(),
235                                got: src_kind,
236                            });
237                        }
238                        input_kinds.push(Some(src_kind));
239                    }
240                    None => input_kinds.push(None),
241                }
242            }
243            output_kinds[ix] = node.output(&input_kinds);
244        }
245
246        let output_id = self.output.ok_or(BuildError::NoOutput)?;
247        let output_ix = ix_of(&output_id).ok_or(BuildError::UnknownOutput(output_id.clone()))?;
248        // Document output must be a canvas-padded raster — anything
249        // smaller (e.g. a raw `Sprite`) will alias badly through the
250        // host's `raster_to_png` crop. Catch this at build time.
251        let output_kind = output_kinds[output_ix];
252        if output_kind != PortKind::Raster {
253            return Err(BuildError::OutputKindMismatch {
254                node: output_id.clone(),
255                got: output_kind,
256            });
257        }
258
259        // Deduplicated views used by the readiness scheduler: each node
260        // has one decrement per distinct upstream, and reaching zero
261        // triggers exactly one spawn per distinct downstream.
262        let mut outgoing_unique = outgoing.clone();
263        for dsts in &mut outgoing_unique {
264            dsts.sort_unstable();
265            dsts.dedup();
266        }
267        let indegree: Vec<usize> = incoming
268            .iter()
269            .map(|ports| {
270                let mut srcs: Vec<NodeIx> = ports.iter().filter_map(|p| *p).collect();
271                srcs.sort_unstable();
272                srcs.dedup();
273                srcs.len()
274            })
275            .collect();
276
277        Ok(Graph {
278            nodes: self.nodes,
279            incoming,
280            outgoing,
281            outgoing_unique,
282            indegree,
283            output: output_ix,
284            topo,
285            output_kinds,
286        })
287    }
288}
289
290impl Default for GraphBuilder {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296fn topo_sort(
297    n: usize,
298    incoming: &[Vec<Option<NodeIx>>],
299    nodes: &IndexMap<NodeId, Box<dyn Node>>,
300) -> Result<Vec<NodeIx>, BuildError> {
301    // Kahn's algorithm over the unique upstream set per node.
302    let mut indegree: Vec<usize> = incoming
303        .iter()
304        .map(|ports| {
305            let mut srcs: Vec<NodeIx> = ports.iter().filter_map(|p| *p).collect();
306            srcs.sort_unstable();
307            srcs.dedup();
308            srcs.len()
309        })
310        .collect();
311
312    // Reverse adjacency: for each src, the unique dsts that depend on it.
313    let mut rev: Vec<Vec<NodeIx>> = vec![Vec::new(); n];
314    for (dst, ports) in incoming.iter().enumerate() {
315        let mut srcs: Vec<NodeIx> = ports.iter().filter_map(|p| *p).collect();
316        srcs.sort_unstable();
317        srcs.dedup();
318        for src in srcs {
319            rev[src].push(dst);
320        }
321    }
322
323    let mut queue: VecDeque<NodeIx> = (0..n).filter(|&i| indegree[i] == 0).collect();
324    let mut order = Vec::with_capacity(n);
325    while let Some(ix) = queue.pop_front() {
326        order.push(ix);
327        for &dst in &rev[ix] {
328            indegree[dst] -= 1;
329            if indegree[dst] == 0 {
330                queue.push_back(dst);
331            }
332        }
333    }
334
335    if order.len() != n {
336        // `order.len() != n` means at least one node still has incoming
337        // edges (otherwise topo would have queued it). Find one such
338        // node to name in the error.
339        let bad = (0..n)
340            .find(|&i| indegree[i] != 0)
341            .expect("order.len() != n implies some indegree is non-zero");
342        let (id, _) = nodes.get_index(bad).expect("bad < n is within nodes range");
343        return Err(BuildError::Cycle(id.clone()));
344    }
345    Ok(order)
346}
347
348impl std::fmt::Debug for Graph {
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350        let ids: Vec<&str> = self.nodes.keys().map(String::as_str).collect();
351        f.debug_struct("Graph")
352            .field("nodes", &ids)
353            .field("output", &self.node_id(self.output))
354            .field(
355                "topo",
356                &self
357                    .topo
358                    .iter()
359                    .map(|&i| self.node_id(i))
360                    .collect::<Vec<_>>(),
361            )
362            .finish()
363    }
364}
365
366impl Graph {
367    /// Number of nodes.
368    pub fn len(&self) -> usize {
369        self.nodes.len()
370    }
371
372    pub fn is_empty(&self) -> bool {
373        self.nodes.is_empty()
374    }
375
376    pub fn output(&self) -> NodeIx {
377        self.output
378    }
379
380    /// Topological order; output node is last.
381    pub fn topo_order(&self) -> &[NodeIx] {
382        &self.topo
383    }
384
385    /// The union of every node's [`Node::asset_inputs`], deduplicated and
386    /// ordered. A host consults this to learn which named bindings the
387    /// document's graph samples — including the `@<dx>,<dy>` neighbour
388    /// names (see [`crate::neighbor`]) a cross-tile node requests, so it
389    /// can fetch and bind exactly the neighbour tiles the graph needs
390    /// rather than the whole 3×3 window unconditionally.
391    pub fn asset_inputs(&self) -> std::collections::BTreeSet<String> {
392        self.nodes.values().flat_map(|n| n.asset_inputs()).collect()
393    }
394
395    pub fn node(&self, ix: NodeIx) -> &dyn Node {
396        self.nodes
397            .get_index(ix)
398            .expect("NodeIx is always within self.nodes range")
399            .1
400            .as_ref()
401    }
402
403    pub fn node_id(&self, ix: NodeIx) -> &str {
404        self.nodes
405            .get_index(ix)
406            .expect("NodeIx is always within self.nodes range")
407            .0
408    }
409
410    /// Look up a node's index by id.
411    pub fn index_of(&self, id: &str) -> Option<NodeIx> {
412        self.nodes.get_index_of(id)
413    }
414
415    /// Upstream nodes feeding `ix`, deduplicated.
416    pub fn upstream(&self, ix: NodeIx) -> impl Iterator<Item = NodeIx> + '_ {
417        let mut srcs: Vec<NodeIx> = self.incoming[ix].iter().filter_map(|p| *p).collect();
418        srcs.sort_unstable();
419        srcs.dedup();
420        srcs.into_iter()
421    }
422
423    /// Downstream nodes consuming `ix`'s output (may contain duplicates
424    /// if the same node connects multiple of its input ports to `ix`).
425    pub fn downstream(&self, ix: NodeIx) -> &[NodeIx] {
426        &self.outgoing[ix]
427    }
428
429    /// Distinct downstream nodes consuming `ix`'s output, each listed once.
430    pub fn downstream_unique(&self, ix: NodeIx) -> &[NodeIx] {
431        &self.outgoing_unique[ix]
432    }
433
434    /// Number of distinct upstream nodes feeding `ix` (0 for sources).
435    pub fn indegree(&self, ix: NodeIx) -> usize {
436        self.indegree[ix]
437    }
438
439    /// The source feeding `node.inputs()[port_ix]`, if connected.
440    pub fn incoming(&self, ix: NodeIx, port_ix: usize) -> Option<NodeIx> {
441        self.incoming[ix][port_ix]
442    }
443
444    /// Resolved output [`PortKind`] for `ix`. Decided at build time;
445    /// polymorphic nodes' kind is fixed once the graph is built.
446    pub fn output_kind(&self, ix: NodeIx) -> PortKind {
447        self.output_kinds[ix]
448    }
449
450    /// Compute the canvas padding each node must supply, given the
451    /// document-level `pad` requested at the output.
452    pub fn compute_pad(&self, doc_pad: u32) -> Result<Vec<u32>, BuildError> {
453        let mut required = vec![0u32; self.len()];
454        required[self.output] = doc_pad;
455        for &ix in self.topo.iter().rev() {
456            let down = required[ix];
457            let up = self.node(ix).required_pad(down);
458            if up > MAX_PAD {
459                return Err(BuildError::PadExceeded {
460                    node: self.node_id(ix).to_string(),
461                    required: up,
462                    limit: MAX_PAD,
463                });
464            }
465            for src in self.upstream(ix) {
466                required[src] = required[src].max(up);
467            }
468        }
469        Ok(required)
470    }
471}