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