1#[cfg(not(target_arch = "wasm32"))]
4use std::time::Instant;
5
6use xxhash_rust::xxh3::Xxh3;
7
8use crate::buf::RasterBuf;
9use crate::cache::{Cache, CacheKey, Hash128};
10use crate::eval::{AssetLoader, CanvasInfo, EvalCtx, EvalError, ParamValues, TileId};
11use crate::graph::{Graph, NodeIx};
12use crate::port::CoordSpace;
13use crate::value::PortValue;
14
15pub struct Evaluator<'a> {
17 pub graph: &'a Graph,
18 pub cache: &'a Cache,
19 pub assets: &'a dyn AssetLoader,
20}
21
22#[derive(Debug, thiserror::Error)]
23pub enum RenderError {
24 #[error(transparent)]
25 Eval(#[from] EvalError),
26}
27
28impl<'a> Evaluator<'a> {
29 pub fn new(graph: &'a Graph, cache: &'a Cache, assets: &'a dyn AssetLoader) -> Self {
30 Self {
31 graph,
32 cache,
33 assets,
34 }
35 }
36
37 pub fn render(
41 &self,
42 tile: TileId,
43 canvas: CanvasInfo,
44 params: &ParamValues,
45 rng_seed: u64,
46 ) -> Result<PortValue, RenderError> {
47 let ctx = EvalCtx {
48 tile,
49 canvas,
50 assets: self.assets,
51 params,
52 rng_seed,
53 influence_pad: u32::MAX,
56 };
57 if crate::mem::enabled() {
58 crate::mem::reset();
59 }
60 let influence = self.graph.influence_pads(self.assets);
61 let n = self.graph.len();
62 let mut hashes: Vec<Hash128> = vec![0; n];
63 let mut values: Vec<Option<PortValue>> = vec![None; n];
64
65 let mut consumers: Vec<usize> = (0..n)
69 .map(|ix| self.graph.downstream_unique(ix).len())
70 .collect();
71
72 for &ix in self.graph.topo_order() {
73 let (value, hash) = {
74 let upstream = |src: NodeIx| -> (Hash128, PortValue) {
75 (
76 hashes[src],
77 values[src]
78 .clone()
79 .expect("upstream evaluated earlier in topo order"),
80 )
81 };
82 self.eval_one(ix, &ctx, &influence, &upstream)?
83 };
84 hashes[ix] = hash;
85 values[ix] = Some(value);
86
87 for src in self.graph.upstream(ix) {
90 consumers[src] -= 1;
91 if consumers[src] == 0 && src != self.graph.output() {
92 if let Some(v) = values[src].take() {
93 if crate::mem::enabled() {
94 crate::mem::released(v.approx_bytes());
95 }
96 }
97 }
98 }
99 }
100 let out = values[self.graph.output()].clone().expect("output unset");
101 if crate::mem::enabled() {
102 eprintln!("{}", crate::mem::report());
103 }
104 Ok(out)
105 }
106
107 pub fn render_parallel(
116 &self,
117 tile: TileId,
118 canvas: CanvasInfo,
119 params: &ParamValues,
120 rng_seed: u64,
121 ) -> Result<PortValue, RenderError> {
122 #[cfg(not(feature = "parallel"))]
123 {
124 self.render(tile, canvas, params, rng_seed)
125 }
126 #[cfg(feature = "parallel")]
127 {
128 use std::sync::atomic::AtomicUsize;
129 use std::sync::{Mutex, OnceLock};
130
131 let ctx = EvalCtx {
132 tile,
133 canvas,
134 assets: self.assets,
135 params,
136 rng_seed,
137 influence_pad: u32::MAX,
140 };
141 let influence = self.graph.influence_pads(self.assets);
142 if crate::mem::enabled() {
143 crate::mem::reset();
144 }
145 let n = self.graph.len();
146 let state = ParState {
147 slots: (0..n).map(|_| Mutex::new(None)).collect(),
148 hashes: (0..n).map(|_| OnceLock::new()).collect(),
149 pending: (0..n)
150 .map(|ix| AtomicUsize::new(self.graph.indegree(ix)))
151 .collect(),
152 consumers: (0..n)
153 .map(|ix| AtomicUsize::new(self.graph.downstream_unique(ix).len()))
154 .collect(),
155 first_err: Mutex::new(None),
156 influence,
157 ctx,
158 };
159
160 let state = &state;
163 rayon::scope(|scope| {
164 for ix in 0..n {
165 if self.graph.indegree(ix) == 0 {
166 scope.spawn(move |s| self.schedule(s, state, ix));
167 }
168 }
169 });
170
171 if let Some(e) = state
172 .first_err
173 .lock()
174 .unwrap_or_else(|p| p.into_inner())
175 .take()
176 {
177 return Err(e);
178 }
179 let out = state.slots[self.graph.output()]
180 .lock()
181 .unwrap_or_else(|p| p.into_inner())
182 .take()
183 .expect("output unset");
184 if crate::mem::enabled() {
185 eprintln!("{}", crate::mem::report());
186 }
187 Ok(out)
188 }
189 }
190
191 #[cfg(feature = "parallel")]
197 fn schedule<'scope>(
198 &'scope self,
199 scope: &rayon::Scope<'scope>,
200 state: &'scope ParState<'scope>,
201 ix: NodeIx,
202 ) {
203 use std::sync::atomic::Ordering;
204
205 if state
206 .first_err
207 .lock()
208 .unwrap_or_else(|p| p.into_inner())
209 .is_some()
210 {
211 return;
212 }
213
214 let upstream = |src: NodeIx| -> (Hash128, PortValue) {
215 let v = state.slots[src]
216 .lock()
217 .unwrap_or_else(|p| p.into_inner())
218 .clone()
219 .expect("upstream still held while a consumer is running");
220 let h = *state.hashes[src]
221 .get()
222 .expect("upstream resolved before dependent is scheduled");
223 (h, v)
224 };
225
226 match self.eval_one(ix, &state.ctx, &state.influence, &upstream) {
227 Ok((v, h)) => {
228 let _ = state.hashes[ix].set(h);
229 *state.slots[ix].lock().unwrap_or_else(|p| p.into_inner()) = Some(v);
230 for src in self.graph.upstream(ix) {
234 if state.consumers[src].fetch_sub(1, Ordering::AcqRel) == 1
235 && src != self.graph.output()
236 {
237 let dropped = state.slots[src]
238 .lock()
239 .unwrap_or_else(|p| p.into_inner())
240 .take();
241 if crate::mem::enabled() {
242 if let Some(v) = dropped {
243 crate::mem::released(v.approx_bytes());
244 }
245 }
246 }
247 }
248 }
249 Err(e) => {
250 let mut slot = state.first_err.lock().unwrap_or_else(|p| p.into_inner());
251 if slot.is_none() {
252 *slot = Some(e);
253 }
254 return;
255 }
256 }
257
258 for &dst in self.graph.downstream_unique(ix) {
259 if state.pending[dst].fetch_sub(1, Ordering::AcqRel) == 1 {
260 scope.spawn(move |s| self.schedule(s, state, dst));
261 }
262 }
263 }
264
265 fn eval_one(
270 &self,
271 ix: NodeIx,
272 ctx: &EvalCtx<'_>,
273 influence: &[u32],
274 upstream: &dyn Fn(NodeIx) -> (Hash128, PortValue),
275 ) -> Result<(PortValue, Hash128), RenderError> {
276 let node = self.graph.node(ix);
277 let ctx = &EvalCtx {
280 influence_pad: influence[ix],
281 ..*ctx
282 };
283
284 let mut h = Xxh3::new();
286 node.param_hash(&mut h);
287 h.update(&ctx.influence_pad.to_le_bytes());
288 for name in node.asset_inputs() {
289 h.update(name.as_bytes());
290 h.update(&ctx.assets.hash(&name).to_le_bytes());
291 }
292 for name in node.param_refs() {
295 h.update(name.as_bytes());
296 match ctx.params.get(&name) {
297 Some(v) => v.hash_into(&mut h),
298 None => h.update(b"\0default"),
299 }
300 }
301 let params_hash: Hash128 = h.digest128();
302
303 let input_specs = node.inputs();
305 let mut input_hashes: Vec<Hash128> = Vec::with_capacity(input_specs.len());
306 let mut input_vals: Vec<Option<PortValue>> = Vec::with_capacity(input_specs.len());
307 for port_ix in 0..input_specs.len() {
308 match self.graph.incoming(ix, port_ix) {
309 Some(src) => {
310 let (h, v) = upstream(src);
311 input_hashes.push(h);
312 input_vals.push(Some(v));
313 }
314 None => {
315 input_hashes.push(0);
316 input_vals.push(None);
317 }
318 }
319 }
320
321 let tile_for_key = match node.coord_space() {
324 CoordSpace::World => None,
325 _ => Some(ctx.tile),
326 };
327 let key = CacheKey::build(ctx.canvas, tile_for_key, params_hash, &input_hashes);
328
329 if let Some(v) = self.cache.get(key) {
330 tracing::debug!(
331 target: "ezu_graph::eval",
332 node = self.graph.node_id(ix),
333 op = node.op_name(),
334 cache = "hit",
335 output = %describe_value(&v),
336 tile = %format!("{}/{}/{}", ctx.tile.z, ctx.tile.x, ctx.tile.y),
337 "cache hit",
338 );
339 return Ok((v, key.0));
340 }
341 #[cfg(not(target_arch = "wasm32"))]
345 let t0 = Instant::now();
346 let (value, blank) = intern_blank(node.eval(ctx, &input_vals)?);
347 #[cfg(not(target_arch = "wasm32"))]
348 let elapsed_us = t0.elapsed().as_micros();
349 #[cfg(target_arch = "wasm32")]
350 let elapsed_us = 0u128;
351 tracing::debug!(
352 target: "ezu_graph::eval",
353 node = self.graph.node_id(ix),
354 op = node.op_name(),
355 cache = "miss",
356 output = %describe_value(&value),
357 tile = %format!("{}/{}/{}", ctx.tile.z, ctx.tile.x, ctx.tile.y),
358 elapsed_us,
359 "evaluated",
360 );
361 if crate::mem::enabled() && !blank {
362 crate::mem::acquired(node.op_name(), value.approx_bytes());
363 }
364 self.cache.insert(key, value.clone());
365 Ok((value, key.0))
366 }
367}
368
369#[cfg(feature = "parallel")]
374struct ParState<'a> {
375 slots: Vec<std::sync::Mutex<Option<PortValue>>>,
377 hashes: Vec<std::sync::OnceLock<Hash128>>,
379 pending: Vec<std::sync::atomic::AtomicUsize>,
380 consumers: Vec<std::sync::atomic::AtomicUsize>,
382 first_err: std::sync::Mutex<Option<RenderError>>,
383 ctx: EvalCtx<'a>,
384 influence: Vec<u32>,
386}
387
388fn intern_blank(value: PortValue) -> (PortValue, bool) {
400 match &value {
401 PortValue::Raster(r) if r.is_blank() => (
402 PortValue::Raster(RasterBuf::blank_shared(r.width, r.height)),
403 true,
404 ),
405 PortValue::Sprite(s) if s.is_blank() => (
406 PortValue::Sprite(RasterBuf::blank_shared(s.width, s.height)),
407 true,
408 ),
409 _ => (value, false),
410 }
411}
412
413fn describe_value(v: &PortValue) -> String {
416 match v {
417 PortValue::Raster(r) => format!("raster {}x{}", r.width, r.height),
418 PortValue::Sprite(s) => format!("sprite {}x{}", s.width, s.height),
419 PortValue::ScalarField(f) => format!(
420 "scalar-field {}x{} (mpp~{:.2})",
421 f.width,
422 f.height,
423 f.metres_per_pixel_x(),
424 ),
425 PortValue::Features(_) => "features".to_string(),
426 PortValue::Brush(_) => "brush".to_string(),
427 PortValue::Labels(_) => "labels".to_string(),
428 PortValue::Scalar(s) => format!("scalar {}({:?})", s.kind_name(), s),
429 }
430}