1#[cfg(not(target_arch = "wasm32"))]
4use std::time::Instant;
5
6use xxhash_rust::xxh3::Xxh3;
7
8use crate::cache::{Cache, CacheKey, Hash128};
9use crate::eval::{AssetLoader, CanvasInfo, EvalCtx, EvalError, ParamValues, TileId};
10use crate::graph::{Graph, NodeIx};
11use crate::port::CoordSpace;
12use crate::value::PortValue;
13
14pub struct Evaluator<'a> {
16 pub graph: &'a Graph,
17 pub cache: &'a Cache,
18 pub assets: &'a dyn AssetLoader,
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum RenderError {
23 #[error(transparent)]
24 Eval(#[from] EvalError),
25}
26
27impl<'a> Evaluator<'a> {
28 pub fn new(graph: &'a Graph, cache: &'a Cache, assets: &'a dyn AssetLoader) -> Self {
29 Self {
30 graph,
31 cache,
32 assets,
33 }
34 }
35
36 pub fn render(
40 &self,
41 tile: TileId,
42 canvas: CanvasInfo,
43 params: &ParamValues,
44 rng_seed: u64,
45 ) -> Result<PortValue, RenderError> {
46 let ctx = EvalCtx {
47 tile,
48 canvas,
49 assets: self.assets,
50 params,
51 rng_seed,
52 };
53 let n = self.graph.len();
54 let mut hashes: Vec<Hash128> = vec![0; n];
55 let mut values: Vec<Option<PortValue>> = vec![None; n];
56
57 for &ix in self.graph.topo_order() {
58 let (value, hash) = {
59 let upstream = |src: NodeIx| -> (Hash128, PortValue) {
60 (
61 hashes[src],
62 values[src]
63 .clone()
64 .expect("upstream evaluated earlier in topo order"),
65 )
66 };
67 self.eval_one(ix, &ctx, &upstream)?
68 };
69 hashes[ix] = hash;
70 values[ix] = Some(value);
71 }
72 Ok(values[self.graph.output()].clone().expect("output unset"))
73 }
74
75 pub fn render_parallel(
84 &self,
85 tile: TileId,
86 canvas: CanvasInfo,
87 params: &ParamValues,
88 rng_seed: u64,
89 ) -> Result<PortValue, RenderError> {
90 #[cfg(not(feature = "parallel"))]
91 {
92 self.render(tile, canvas, params, rng_seed)
93 }
94 #[cfg(feature = "parallel")]
95 {
96 use std::sync::atomic::AtomicUsize;
97 use std::sync::{Mutex, OnceLock};
98
99 let ctx = EvalCtx {
100 tile,
101 canvas,
102 assets: self.assets,
103 params,
104 rng_seed,
105 };
106 let n = self.graph.len();
107 let state = ParState {
108 slots: (0..n).map(|_| OnceLock::new()).collect(),
109 pending: (0..n)
110 .map(|ix| AtomicUsize::new(self.graph.indegree(ix)))
111 .collect(),
112 first_err: Mutex::new(None),
113 ctx,
114 };
115
116 let state = &state;
119 rayon::scope(|scope| {
120 for ix in 0..n {
121 if self.graph.indegree(ix) == 0 {
122 scope.spawn(move |s| self.schedule(s, state, ix));
123 }
124 }
125 });
126
127 if let Some(e) = state
128 .first_err
129 .lock()
130 .unwrap_or_else(|p| p.into_inner())
131 .take()
132 {
133 return Err(e);
134 }
135 Ok(state.slots[self.graph.output()]
136 .get()
137 .expect("output unset")
138 .0
139 .clone())
140 }
141 }
142
143 #[cfg(feature = "parallel")]
149 fn schedule<'scope>(
150 &'scope self,
151 scope: &rayon::Scope<'scope>,
152 state: &'scope ParState<'scope>,
153 ix: NodeIx,
154 ) {
155 use std::sync::atomic::Ordering;
156
157 if state
158 .first_err
159 .lock()
160 .unwrap_or_else(|p| p.into_inner())
161 .is_some()
162 {
163 return;
164 }
165
166 let upstream = |src: NodeIx| -> (Hash128, PortValue) {
167 let (v, h) = state.slots[src]
168 .get()
169 .expect("upstream resolved before dependent is scheduled");
170 (*h, v.clone())
171 };
172
173 match self.eval_one(ix, &state.ctx, &upstream) {
174 Ok((v, h)) => {
175 let _ = state.slots[ix].set((v, h));
176 }
177 Err(e) => {
178 let mut slot = state.first_err.lock().unwrap_or_else(|p| p.into_inner());
179 if slot.is_none() {
180 *slot = Some(e);
181 }
182 return;
183 }
184 }
185
186 for &dst in self.graph.downstream_unique(ix) {
187 if state.pending[dst].fetch_sub(1, Ordering::AcqRel) == 1 {
188 scope.spawn(move |s| self.schedule(s, state, dst));
189 }
190 }
191 }
192
193 fn eval_one(
198 &self,
199 ix: NodeIx,
200 ctx: &EvalCtx<'_>,
201 upstream: &dyn Fn(NodeIx) -> (Hash128, PortValue),
202 ) -> Result<(PortValue, Hash128), RenderError> {
203 let node = self.graph.node(ix);
204
205 let mut h = Xxh3::new();
207 node.param_hash(&mut h);
208 for name in node.asset_inputs() {
209 h.update(name.as_bytes());
210 h.update(&ctx.assets.hash(&name).to_le_bytes());
211 }
212 for name in node.param_refs() {
215 h.update(name.as_bytes());
216 match ctx.params.get(&name) {
217 Some(v) => v.hash_into(&mut h),
218 None => h.update(b"\0default"),
219 }
220 }
221 let params_hash: Hash128 = h.digest128();
222
223 let input_specs = node.inputs();
225 let mut input_hashes: Vec<Hash128> = Vec::with_capacity(input_specs.len());
226 let mut input_vals: Vec<Option<PortValue>> = Vec::with_capacity(input_specs.len());
227 for port_ix in 0..input_specs.len() {
228 match self.graph.incoming(ix, port_ix) {
229 Some(src) => {
230 let (h, v) = upstream(src);
231 input_hashes.push(h);
232 input_vals.push(Some(v));
233 }
234 None => {
235 input_hashes.push(0);
236 input_vals.push(None);
237 }
238 }
239 }
240
241 let tile_for_key = match node.coord_space() {
244 CoordSpace::World => None,
245 _ => Some(ctx.tile),
246 };
247 let key = CacheKey::build(ctx.canvas, tile_for_key, params_hash, &input_hashes);
248
249 if let Some(v) = self.cache.get(key) {
250 tracing::debug!(
251 target: "ezu_graph::eval",
252 node = self.graph.node_id(ix),
253 op = node.op_name(),
254 cache = "hit",
255 output = %describe_value(&v),
256 tile = %format!("{}/{}/{}", ctx.tile.z, ctx.tile.x, ctx.tile.y),
257 "cache hit",
258 );
259 return Ok((v, key.0));
260 }
261 #[cfg(not(target_arch = "wasm32"))]
265 let t0 = Instant::now();
266 let value = node.eval(ctx, &input_vals)?;
267 #[cfg(not(target_arch = "wasm32"))]
268 let elapsed_us = t0.elapsed().as_micros();
269 #[cfg(target_arch = "wasm32")]
270 let elapsed_us = 0u128;
271 tracing::debug!(
272 target: "ezu_graph::eval",
273 node = self.graph.node_id(ix),
274 op = node.op_name(),
275 cache = "miss",
276 output = %describe_value(&value),
277 tile = %format!("{}/{}/{}", ctx.tile.z, ctx.tile.x, ctx.tile.y),
278 elapsed_us,
279 "evaluated",
280 );
281 self.cache.insert(key, value.clone());
282 Ok((value, key.0))
283 }
284}
285
286#[cfg(feature = "parallel")]
291struct ParState<'a> {
292 slots: Vec<std::sync::OnceLock<(PortValue, Hash128)>>,
293 pending: Vec<std::sync::atomic::AtomicUsize>,
294 first_err: std::sync::Mutex<Option<RenderError>>,
295 ctx: EvalCtx<'a>,
296}
297
298fn describe_value(v: &PortValue) -> String {
301 match v {
302 PortValue::Raster(r) => format!("raster {}x{}", r.width, r.height),
303 PortValue::Sprite(s) => format!("sprite {}x{}", s.width, s.height),
304 PortValue::ScalarField(f) => format!(
305 "scalar-field {}x{} (mpp~{:.2})",
306 f.width,
307 f.height,
308 f.metres_per_pixel_x(),
309 ),
310 PortValue::Features(_) => "features".to_string(),
311 PortValue::Brush(_) => "brush".to_string(),
312 PortValue::Labels(_) => "labels".to_string(),
313 PortValue::Scalar(s) => format!("scalar {}({:?})", s.kind_name(), s),
314 }
315}