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