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 };
54 if crate::mem::enabled() {
55 crate::mem::reset();
56 }
57 let n = self.graph.len();
58 let mut hashes: Vec<Hash128> = vec![0; n];
59 let mut values: Vec<Option<PortValue>> = vec![None; n];
60
61 let mut consumers: Vec<usize> = (0..n)
65 .map(|ix| self.graph.downstream_unique(ix).len())
66 .collect();
67
68 for &ix in self.graph.topo_order() {
69 let (value, hash) = {
70 let upstream = |src: NodeIx| -> (Hash128, PortValue) {
71 (
72 hashes[src],
73 values[src]
74 .clone()
75 .expect("upstream evaluated earlier in topo order"),
76 )
77 };
78 self.eval_one(ix, &ctx, &upstream)?
79 };
80 hashes[ix] = hash;
81 values[ix] = Some(value);
82
83 for src in self.graph.upstream(ix) {
86 consumers[src] -= 1;
87 if consumers[src] == 0 && src != self.graph.output() {
88 if let Some(v) = values[src].take() {
89 if crate::mem::enabled() {
90 crate::mem::released(v.approx_bytes());
91 }
92 }
93 }
94 }
95 }
96 let out = values[self.graph.output()].clone().expect("output unset");
97 if crate::mem::enabled() {
98 eprintln!("{}", crate::mem::report());
99 }
100 Ok(out)
101 }
102
103 pub fn render_parallel(
112 &self,
113 tile: TileId,
114 canvas: CanvasInfo,
115 params: &ParamValues,
116 rng_seed: u64,
117 ) -> Result<PortValue, RenderError> {
118 #[cfg(not(feature = "parallel"))]
119 {
120 self.render(tile, canvas, params, rng_seed)
121 }
122 #[cfg(feature = "parallel")]
123 {
124 use std::sync::atomic::AtomicUsize;
125 use std::sync::{Mutex, OnceLock};
126
127 let ctx = EvalCtx {
128 tile,
129 canvas,
130 assets: self.assets,
131 params,
132 rng_seed,
133 };
134 if crate::mem::enabled() {
135 crate::mem::reset();
136 }
137 let n = self.graph.len();
138 let state = ParState {
139 slots: (0..n).map(|_| Mutex::new(None)).collect(),
140 hashes: (0..n).map(|_| OnceLock::new()).collect(),
141 pending: (0..n)
142 .map(|ix| AtomicUsize::new(self.graph.indegree(ix)))
143 .collect(),
144 consumers: (0..n)
145 .map(|ix| AtomicUsize::new(self.graph.downstream_unique(ix).len()))
146 .collect(),
147 first_err: Mutex::new(None),
148 ctx,
149 };
150
151 let state = &state;
154 rayon::scope(|scope| {
155 for ix in 0..n {
156 if self.graph.indegree(ix) == 0 {
157 scope.spawn(move |s| self.schedule(s, state, ix));
158 }
159 }
160 });
161
162 if let Some(e) = state
163 .first_err
164 .lock()
165 .unwrap_or_else(|p| p.into_inner())
166 .take()
167 {
168 return Err(e);
169 }
170 let out = state.slots[self.graph.output()]
171 .lock()
172 .unwrap_or_else(|p| p.into_inner())
173 .take()
174 .expect("output unset");
175 if crate::mem::enabled() {
176 eprintln!("{}", crate::mem::report());
177 }
178 Ok(out)
179 }
180 }
181
182 #[cfg(feature = "parallel")]
188 fn schedule<'scope>(
189 &'scope self,
190 scope: &rayon::Scope<'scope>,
191 state: &'scope ParState<'scope>,
192 ix: NodeIx,
193 ) {
194 use std::sync::atomic::Ordering;
195
196 if state
197 .first_err
198 .lock()
199 .unwrap_or_else(|p| p.into_inner())
200 .is_some()
201 {
202 return;
203 }
204
205 let upstream = |src: NodeIx| -> (Hash128, PortValue) {
206 let v = state.slots[src]
207 .lock()
208 .unwrap_or_else(|p| p.into_inner())
209 .clone()
210 .expect("upstream still held while a consumer is running");
211 let h = *state.hashes[src]
212 .get()
213 .expect("upstream resolved before dependent is scheduled");
214 (h, v)
215 };
216
217 match self.eval_one(ix, &state.ctx, &upstream) {
218 Ok((v, h)) => {
219 let _ = state.hashes[ix].set(h);
220 *state.slots[ix].lock().unwrap_or_else(|p| p.into_inner()) = Some(v);
221 for src in self.graph.upstream(ix) {
225 if state.consumers[src].fetch_sub(1, Ordering::AcqRel) == 1
226 && src != self.graph.output()
227 {
228 let dropped = state.slots[src]
229 .lock()
230 .unwrap_or_else(|p| p.into_inner())
231 .take();
232 if crate::mem::enabled() {
233 if let Some(v) = dropped {
234 crate::mem::released(v.approx_bytes());
235 }
236 }
237 }
238 }
239 }
240 Err(e) => {
241 let mut slot = state.first_err.lock().unwrap_or_else(|p| p.into_inner());
242 if slot.is_none() {
243 *slot = Some(e);
244 }
245 return;
246 }
247 }
248
249 for &dst in self.graph.downstream_unique(ix) {
250 if state.pending[dst].fetch_sub(1, Ordering::AcqRel) == 1 {
251 scope.spawn(move |s| self.schedule(s, state, dst));
252 }
253 }
254 }
255
256 fn eval_one(
261 &self,
262 ix: NodeIx,
263 ctx: &EvalCtx<'_>,
264 upstream: &dyn Fn(NodeIx) -> (Hash128, PortValue),
265 ) -> Result<(PortValue, Hash128), RenderError> {
266 let node = self.graph.node(ix);
267
268 let mut h = Xxh3::new();
270 node.param_hash(&mut h);
271 for name in node.asset_inputs() {
272 h.update(name.as_bytes());
273 h.update(&ctx.assets.hash(&name).to_le_bytes());
274 }
275 for name in node.param_refs() {
278 h.update(name.as_bytes());
279 match ctx.params.get(&name) {
280 Some(v) => v.hash_into(&mut h),
281 None => h.update(b"\0default"),
282 }
283 }
284 let params_hash: Hash128 = h.digest128();
285
286 let input_specs = node.inputs();
288 let mut input_hashes: Vec<Hash128> = Vec::with_capacity(input_specs.len());
289 let mut input_vals: Vec<Option<PortValue>> = Vec::with_capacity(input_specs.len());
290 for port_ix in 0..input_specs.len() {
291 match self.graph.incoming(ix, port_ix) {
292 Some(src) => {
293 let (h, v) = upstream(src);
294 input_hashes.push(h);
295 input_vals.push(Some(v));
296 }
297 None => {
298 input_hashes.push(0);
299 input_vals.push(None);
300 }
301 }
302 }
303
304 let tile_for_key = match node.coord_space() {
307 CoordSpace::World => None,
308 _ => Some(ctx.tile),
309 };
310 let key = CacheKey::build(ctx.canvas, tile_for_key, params_hash, &input_hashes);
311
312 if let Some(v) = self.cache.get(key) {
313 tracing::debug!(
314 target: "ezu_graph::eval",
315 node = self.graph.node_id(ix),
316 op = node.op_name(),
317 cache = "hit",
318 output = %describe_value(&v),
319 tile = %format!("{}/{}/{}", ctx.tile.z, ctx.tile.x, ctx.tile.y),
320 "cache hit",
321 );
322 return Ok((v, key.0));
323 }
324 #[cfg(not(target_arch = "wasm32"))]
328 let t0 = Instant::now();
329 let (value, blank) = intern_blank(node.eval(ctx, &input_vals)?);
330 #[cfg(not(target_arch = "wasm32"))]
331 let elapsed_us = t0.elapsed().as_micros();
332 #[cfg(target_arch = "wasm32")]
333 let elapsed_us = 0u128;
334 tracing::debug!(
335 target: "ezu_graph::eval",
336 node = self.graph.node_id(ix),
337 op = node.op_name(),
338 cache = "miss",
339 output = %describe_value(&value),
340 tile = %format!("{}/{}/{}", ctx.tile.z, ctx.tile.x, ctx.tile.y),
341 elapsed_us,
342 "evaluated",
343 );
344 if crate::mem::enabled() && !blank {
345 crate::mem::acquired(node.op_name(), value.approx_bytes());
346 }
347 self.cache.insert(key, value.clone());
348 Ok((value, key.0))
349 }
350}
351
352#[cfg(feature = "parallel")]
357struct ParState<'a> {
358 slots: Vec<std::sync::Mutex<Option<PortValue>>>,
360 hashes: Vec<std::sync::OnceLock<Hash128>>,
362 pending: Vec<std::sync::atomic::AtomicUsize>,
363 consumers: Vec<std::sync::atomic::AtomicUsize>,
365 first_err: std::sync::Mutex<Option<RenderError>>,
366 ctx: EvalCtx<'a>,
367}
368
369fn intern_blank(value: PortValue) -> (PortValue, bool) {
381 match &value {
382 PortValue::Raster(r) if r.is_blank() => (
383 PortValue::Raster(RasterBuf::blank_shared(r.width, r.height)),
384 true,
385 ),
386 PortValue::Sprite(s) if s.is_blank() => (
387 PortValue::Sprite(RasterBuf::blank_shared(s.width, s.height)),
388 true,
389 ),
390 _ => (value, false),
391 }
392}
393
394fn describe_value(v: &PortValue) -> String {
397 match v {
398 PortValue::Raster(r) => format!("raster {}x{}", r.width, r.height),
399 PortValue::Sprite(s) => format!("sprite {}x{}", s.width, s.height),
400 PortValue::ScalarField(f) => format!(
401 "scalar-field {}x{} (mpp~{:.2})",
402 f.width,
403 f.height,
404 f.metres_per_pixel_x(),
405 ),
406 PortValue::Features(_) => "features".to_string(),
407 PortValue::Brush(_) => "brush".to_string(),
408 PortValue::Labels(_) => "labels".to_string(),
409 PortValue::Scalar(s) => format!("scalar {}({:?})", s.kind_name(), s),
410 }
411}