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