1mod answer;
4pub(crate) mod bound;
5mod drive;
6mod frontier;
7mod offer;
8mod run;
9mod slots;
10mod stream;
11pub(crate) mod table;
12mod terms;
13pub mod until;
14mod volatile;
15
16use std::collections::{BTreeMap, BTreeSet};
17
18use sva_ast::Graph;
19use sva_formula::{NodeId, SpectralSum};
20use sva_samples::{
21 AliasScore, Buffer, Extent, FilterTrace, Frames, Label, PSYCHOACOUSTIC_V1, Profile,
22};
23
24use crate::bindings::Binding;
25use crate::cache::{CacheStats, Memory, Recording, Tier, now};
26use crate::error::{Diagnostic, EngineError, Located};
27use crate::instantiate;
28use crate::query::Ask;
29use crate::refs;
30use crate::schedule::{self, Schedule};
31use crate::typing::{self, Typing};
32use table::Table;
33use table::support::Supports;
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct Range {
38 pub start: Option<i64>,
39 pub end: Option<i64>,
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct RenderConfig {
44 pub rate: u32,
45 pub range: Range,
46 pub until: Option<Until>,
48 pub profile: Profile,
49 pub asks: Vec<Ask>,
51 pub flop_budget: u128,
53 pub volatile: Vec<String>,
55 pub out: Out,
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
61pub enum Out {
62 #[default]
63 Kept,
64 Dropped,
65}
66
67impl RenderConfig {
68 pub fn at(rate: u32) -> RenderConfig {
69 RenderConfig {
70 rate,
71 range: Range::default(),
72 until: None,
73 profile: PSYCHOACOUSTIC_V1,
74 asks: Vec::new(),
75 flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
76 volatile: Vec::new(),
77 out: Out::Kept,
78 }
79 }
80
81 pub fn seconds(rate: u32, secs: f64) -> RenderConfig {
82 let end = (secs * f64::from(rate)).round() as i64;
83 RenderConfig {
84 range: Range {
85 start: Some(0),
86 end: Some(end),
87 },
88 ..RenderConfig::at(rate)
89 }
90 }
91
92 pub fn asking(mut self, asks: Vec<Ask>) -> RenderConfig {
93 self.asks = asks;
94 self
95 }
96}
97
98pub use answer::{answer, answer_buffer, sketch_atom};
99pub use drive::Block;
100pub use run::render_over;
101pub use stream::{
102 Built, Change, Changed, Counts, LATEST, Placed, STREAMED, Stream, StreamConfig, change, fetch,
103};
104pub use terms::{Handle, NOTES};
105pub use until::Until;
106
107const BLOCK: usize = 1 << 12;
109
110pub struct Render {
111 pub root: NodeId,
112 pub tys: Typing,
113 pub buffers: BTreeMap<NodeId, Buffer>,
114 pub frames: BTreeMap<NodeId, Frames>,
115 pub symbolic: BTreeMap<NodeId, SpectralSum>,
116 pub labels: BTreeMap<NodeId, Label>,
117 pub traces: Vec<FilterTrace>,
118 pub config: RenderConfig,
119 pub schedule: Schedule,
120 pub bindings: BTreeMap<NodeId, Vec<Binding>>,
121 pub cache_stats: Option<CacheStats>,
122 pub held_bytes: usize,
124 pub range: Option<Extent>,
126 pub(crate) unranged: Option<EngineError>,
127 pub(crate) table: Option<Table>,
129 computed: u128,
131}
132
133impl Render {
134 pub(crate) fn shell(
135 tys: Typing,
136 root: NodeId,
137 config: RenderConfig,
138 schedule: Schedule,
139 ) -> Self {
140 Render {
141 root,
142 tys,
143 buffers: BTreeMap::new(),
144 frames: BTreeMap::new(),
145 symbolic: BTreeMap::new(),
146 labels: BTreeMap::new(),
147 traces: Vec::new(),
148 config,
149 schedule,
150 bindings: BTreeMap::new(),
151 cache_stats: None,
152 held_bytes: 0,
153 range: None,
154 unranged: None,
155 table: None,
156 computed: 0,
157 }
158 }
159
160 pub fn work(&self) -> crate::flops::Work {
162 crate::flops::Work {
163 samples: self.range.map_or(0, |range| range.len() as u64),
164 priced_flops: self.computed,
165 waves: None,
166 }
167 }
168
169 pub fn output(&self, node: NodeId) -> Result<Buffer, EngineError> {
171 match (self.buffers.get(&node), self.range) {
172 (Some(held), Some(range)) => Ok(held.over(range, held.extent())),
173 _ => Err(answer::unheld(self, node)),
174 }
175 }
176
177 pub fn buffer(&self, node: NodeId) -> Option<&Buffer> {
178 self.buffers.get(&node)
179 }
180
181 pub fn id(&self, path: &str) -> Option<NodeId> {
182 self.tys.id(path)
183 }
184
185 pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
187 self.tys.resolve(path)
188 }
189
190 pub fn evaluated(&self, node: NodeId) -> Vec<Extent> {
193 let Some(table) = &self.table else {
194 return Vec::new();
195 };
196 table
197 .of(node)
198 .map_or(Vec::new(), |at| table.values[at].evaluated.clone())
199 }
200
201 pub fn alias_oversample(&self, node: NodeId) -> Option<u32> {
203 self.config
204 .asks
205 .iter()
206 .find_map(|ask| match ask.representation {
207 crate::query::Representation::Alias { oversample }
208 if self.node(&ask.node).is_ok_and(|asked| asked == node) =>
209 {
210 Some(oversample)
211 }
212 _ => None,
213 })
214 }
215
216 pub fn alias_score(&self, node: NodeId) -> AliasScore {
217 match self.alias_oversample(node) {
218 Some(_) => AliasScore::Asked,
219 None => AliasScore::NotAsked,
220 }
221 }
222}
223
224pub fn render(
226 graph: &Graph,
227 target: &str,
228 config: RenderConfig,
229 tier: &Tier,
230) -> Result<Render, EngineError> {
231 now(render_over(graph, target, config, tier))
232}
233
234fn closed(held: &mut Render) -> Result<(), EngineError> {
235 scored(held)?;
236 compose_read(held);
237 stamp(held);
238 Ok(())
239}
240
241pub(crate) struct Prepared {
242 pub(crate) instances: instantiate::Instances,
243 pub(crate) tys: Typing,
244 pub(crate) root: NodeId,
245}
246
247pub(crate) fn prepared(graph: &Graph, target: &str, rate: u32) -> Result<Prepared, EngineError> {
248 let instances = instantiate::instantiate(graph, target, rate)?;
249 let held = instances.instance_of(target)?;
250 let order = schedule::schedule_from(&instances, std::slice::from_ref(&held))?;
251 let tys = typing::infer_all(&instances, &order)?;
252 let root = tys
253 .id(&held)
254 .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
255 Ok(Prepared {
256 instances,
257 tys,
258 root,
259 })
260}
261
262fn planned(
263 prepared: Prepared,
264 config: RenderConfig,
265 bounds: &BTreeSet<NodeId>,
266) -> Result<Render, EngineError> {
267 let Prepared {
268 instances,
269 tys,
270 root,
271 } = prepared;
272 planned_over(&instances, (tys, root), config, bounds)
273}
274
275fn planned_over(
276 instances: &instantiate::Instances,
277 (tys, root): (Typing, NodeId),
278 config: RenderConfig,
279 bounds: &BTreeSet<NodeId>,
280) -> Result<Render, EngineError> {
281 let schedule = schedule::plan(&tys, root, &config.asks);
282 let bindings = tys
283 .paths()
284 .filter_map(|(path, id)| Some((id, resolved(instances, path)?)))
285 .collect();
286 let mut held = Render::shell(tys, root, config, schedule);
287 held.bindings = bindings;
288 ranged(&mut held, bounds)?;
289 let target = held.tys.name(held.root).to_string();
290 let volatile = volatile::mark(instances, &held, &target)?;
291 if let Some(table) = &mut held.table {
292 table.slots(|id| volatile.slot(id));
293 }
294 Ok(held)
295}
296
297pub fn plan(graph: &Graph, target: &str, config: RenderConfig) -> Result<Render, EngineError> {
299 planned(
300 prepared(graph, target, config.rate)?,
301 config,
302 &BTreeSet::new(),
303 )
304}
305
306pub fn ends(
309 graph: &Graph,
310 roots: &[String],
311 config: &RenderConfig,
312) -> Result<Vec<Option<i64>>, EngineError> {
313 let (instances, named) = instantiate::from_roots(graph, roots, config.rate)?;
314 let order = schedule::schedule_from(&instances, &named)?;
315 let tys = typing::infer_all(&instances, &order)?;
316 let supports = Supports::new(&tys, &config.profile);
317 named
318 .iter()
319 .map(|held| {
320 let id = tys
321 .id(held)
322 .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
323 Ok(default_end(supports.of(id)))
324 })
325 .collect()
326}
327
328fn ranged(held: &mut Render, bounds: &BTreeSet<NodeId>) -> Result<(), EngineError> {
330 let counts = counts(&held.config.asks);
331 let envelope = held.config.asks.iter().any(|ask| {
332 matches!(
333 ask.representation,
334 crate::query::Representation::Envelope { .. }
335 )
336 });
337 if !materializes(held) && !counts {
338 if envelope && let Err(refused) = range_of(held, Ends::Refused) {
339 held.unranged = Some(refused);
340 } else if envelope {
341 held.range = Some(range_of(held, Ends::Refused)?);
342 }
343 return Ok(());
344 }
345 held.range = Some(range_of(held, Ends::Refused)?);
346 let wanted: Vec<NodeId> = held.schedule.wanted.clone();
347 let root = (held.root, wanted.as_slice());
348 let mut table = Table::bounded(&held.tys, root, &held.config.profile, bounds)?;
349 table.plan(held.range.expect("a range was decided"));
350 held.table = Some(table);
351 Ok(())
352}
353
354fn materializes(held: &Render) -> bool {
355 !held.schedule.wanted.is_empty()
356}
357
358pub(crate) enum Ends {
359 Refused,
360 Pulled,
361}
362
363pub(crate) fn range_of(held: &Render, ends: Ends) -> Result<Extent, EngineError> {
364 let support = Supports::new(&held.tys, &held.config.profile).of(held.root);
365 range_over((&held.config, held.tys.name(held.root)), support, ends)
366}
367
368pub(crate) fn range_over(
370 (config, name): (&RenderConfig, &str),
371 support: Extent,
372 ends: Ends,
373) -> Result<Extent, EngineError> {
374 let start = config.range.start.unwrap_or_else(|| default_start(support));
375 let end = match config.range.end.or(default_end(support)) {
376 Some(end) => end,
377 None => match ends {
378 Ends::Pulled => i64::MAX,
379 Ends::Refused => return Err(endless(name)),
380 },
381 };
382 Ok(Extent::new(start, end.max(start)))
383}
384
385fn default_start(support: Extent) -> i64 {
386 match support.is_empty() || support.start == i64::MIN || support.start > 0 {
387 true => 0,
388 false => support.start,
389 }
390}
391
392fn default_end(support: Extent) -> Option<i64> {
393 (support.end != i64::MAX).then_some(support.end)
394}
395
396fn endless(name: &str) -> EngineError {
397 EngineError::refused(Diagnostic {
398 code: "render.no_end".to_string(),
399 message: format!(
400 "`{name}` is read over an interval with no end, and its support never ends"
401 ),
402 location: Located::at(name, None),
403 help: "give the interval an end, as `[0, 2s]`, or crop it".to_string(),
404 })
405}
406
407fn counts(asks: &[Ask]) -> bool {
409 asks.iter()
410 .any(|ask| ask.representation == crate::query::Representation::Flops)
411}
412
413fn affordable(held: &Render) -> Result<(), EngineError> {
416 if !materializes(held) {
417 return Ok(());
418 }
419 let total = crate::flops::total(held);
420 if total <= held.config.flop_budget {
421 return Ok(());
422 }
423 let counted = crate::flops::tree(held);
424 let over = crate::flops::dominating(&counted).expect("a counted tree holds its root");
425 Err(EngineError::refused(Diagnostic {
426 code: "collapse.over_budget".to_string(),
427 message: format!(
428 "this render counts {} operations, over the budget of {}; `{}` dominates it at {} \
429 by {}",
430 counted.total, counted.budget, over.node, over.subtree, over.route
431 ),
432 location: Located::at(held.tys.name(held.root), None),
433 help: format!("pass --flop-budget {total} to render it anyway"),
434 }))
435}
436
437#[cfg(test)]
439fn pulled(held: &mut Render, recording: Recording) -> Result<(), EngineError> {
440 if let Some(mut driver) = driving(held, recording)? {
441 while driver.pull()? {}
442 drove(held, driver);
443 }
444 Ok(())
445}
446
447fn driving(held: &mut Render, recording: Recording) -> Result<Option<drive::Driver>, EngineError> {
449 affordable(held)?;
450 let (Some(table), Some(range)) = (held.table.take(), held.range) else {
451 return Ok(None);
452 };
453 if !materializes(held) {
454 held.table = Some(table);
455 return Ok(None);
456 }
457 Ok(Some(drive::Driver::new(
458 table,
459 range,
460 BLOCK,
461 &held.config,
462 recording,
463 )))
464}
465
466pub(crate) fn dropped(config: &RenderConfig) -> bool {
467 config.out == Out::Dropped && config.asks.is_empty()
468}
469
470fn drove(held: &mut Render, driver: drive::Driver) {
471 let keep = !dropped(&held.config);
472 let range = held.range.expect("a pulled render has a range");
473 held.held_bytes = driver.most_bytes();
474 held.computed = driver.work.priced_flops;
475 held.cache_stats = Some(driver.recording.stats());
476 if let Some(stop) = driver.stop().filter(|stop| *stop < range.end) {
477 held.range = Some(Extent::new(range.start, stop));
478 }
479 let range = held.range.expect("a pulled render has a range");
480 let table = driver.table;
481 let wanted = held.schedule.wanted.iter().filter(|_| keep);
482 for (id, at) in wanted.map(|id| (*id, table.of(*id))) {
483 let Some(at) = at else {
484 continue;
485 };
486 match &table.values[at].held {
487 table::Held::Frames(Some(frames)) => {
488 held.frames.insert(id, (**frames).clone());
489 }
490 _ => {
491 held.buffers.insert(id, table.samples(at, range));
492 held.labels.insert(id, table.label(at));
493 }
494 }
495 }
496 held.table = Some(table);
497}
498
499pub(crate) fn finer(
502 render: &Render,
503 node: NodeId,
504 fine: u32,
505 over: Extent,
506) -> Result<Buffer, EngineError> {
507 let profile = &render.config.profile;
508 let mut table = Table::finer(&render.tys, node, &[node], profile, i128::from(fine))?;
509 let at = table.root;
510 table.pull(over, &mut Recording::over(&Memory::holding(0)))?;
511 let mut held = table.samples(at, over);
512 held.rate = render.config.rate * fine;
513 Ok(held)
514}
515
516fn scored(held: &mut Render) -> Result<(), EngineError> {
518 let asked: Vec<NodeId> = held
519 .labels
520 .keys()
521 .copied()
522 .filter(|id| held.alias_oversample(*id).is_some())
523 .collect();
524 for id in asked {
525 let sva_samples::Detail::Point {
526 rule,
527 alias_db: None,
528 } = held.labels[&id].detail
529 else {
530 continue;
531 };
532 let buffer = held.output(id)?;
533 let alias_db = Some(answer::alias_db(held, id, &buffer)?);
534 let label = held.labels.get_mut(&id).expect("an asked label");
535 label.detail = sva_samples::Detail::Point { rule, alias_db };
536 }
537 Ok(())
538}
539
540#[cfg(test)]
542pub(crate) fn render_apart(
543 graph: &Graph,
544 target: &str,
545 config: RenderConfig,
546) -> Result<Render, EngineError> {
547 let mut held = planned(
548 prepared(graph, target, config.rate)?,
549 config,
550 &BTreeSet::new(),
551 )?;
552 if let (Some(range), Some(_)) = (held.range, &held.table) {
553 let wanted = held.schedule.wanted.clone();
554 let mut table = Table::apart(&held.tys, held.root, &wanted, &held.config.profile)?;
555 table.plan(range);
556 held.table = Some(table);
557 }
558 pulled(&mut held, Recording::over(&Memory::holding(0)))?;
559 Ok(held)
560}
561
562pub(crate) fn sampled(render: &Render, node: NodeId, over: Extent) -> Result<Buffer, EngineError> {
563 let mut table = Table::build(&render.tys, node, &[node], &render.config.profile)?;
564 let at = table.root;
565 table.pull(over, &mut Recording::over(&Memory::holding(0)))?;
566 Ok(table.samples(at, over))
567}
568
569fn stamp(held: &mut Render) {
571 let root = held.root;
572 let Some(label) = held.labels.remove(&root) else {
573 return;
574 };
575 let counted = crate::flops::total(held);
576 let label = sva_samples::Label {
577 rate: held.config.rate,
578 moved: held.table.as_ref().map(Table::moved),
579 pruned: held.table.as_ref().map(|table| table.pruned(&held.tys)),
580 ..label.costing(counted, held.config.flop_budget)
581 };
582 held.labels.insert(root, label);
583}
584
585fn compose_read(held: &mut Render) {
588 for id in held.schedule.compose.clone() {
589 if held.symbolic.contains_key(&id) {
590 continue;
591 }
592 if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
593 held.symbolic.insert(id, sum);
594 }
595 }
596}
597
598fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
599 Some(
600 instances
601 .bindings(path)?
602 .into_iter()
603 .map(|(name, expr, cx)| Binding {
604 name: name.to_string(),
605 source: instances.render(expr, cx),
606 })
607 .collect(),
608 )
609}