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