1mod answer;
4mod pointwise;
5mod sampled;
6mod silent;
7mod slots;
8mod stream;
9mod volatile;
10
11use std::collections::BTreeMap;
12
13use sva_ast::Graph;
14use sva_formula::{Held, NodeId, SpectralSum, hash_closed_form};
15use sva_samples::{
16 AliasScore, Buffer, FilterTrace, Frames, Horizon, Label, PSYCHOACOUSTIC_V1, Profile, stft,
17};
18
19use crate::bindings::Binding;
20use crate::cache::{
21 Cache, CachePolicy, CacheStats, Expected, Lens, Payload, Recording, frames_key, symbolic_key,
22};
23use crate::cast::Cast;
24use crate::error::{Diagnostic, EngineError, Located};
25use crate::instantiate;
26use crate::query::Ask;
27use crate::refs;
28use crate::schedule::{self, Schedule};
29use crate::typing::{self, Typing, Value};
30
31#[derive(Clone, Debug, PartialEq)]
32pub struct RenderConfig {
33 pub rate: u32,
34 pub horizon: Horizon,
35 pub profile: Profile,
36 pub asks: Vec<Ask>,
38 pub flop_budget: u128,
40 pub volatile: Vec<String>,
42 pub cache_policy: Option<CachePolicy>,
44}
45
46impl RenderConfig {
47 pub fn seconds(rate: u32, secs: f64) -> RenderConfig {
48 RenderConfig {
49 rate,
50 horizon: Horizon::secs(0.0, secs),
51 profile: PSYCHOACOUSTIC_V1,
52 asks: Vec::new(),
53 flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
54 volatile: Vec::new(),
55 cache_policy: None,
56 }
57 }
58
59 pub fn asking(mut self, asks: Vec<Ask>) -> RenderConfig {
60 self.asks = asks;
61 self
62 }
63}
64
65pub use answer::{answer, answer_buffer, ledger_over, sketch_atom};
66pub use silent::{Silent, render_until_silent};
67pub use stream::{Block, Checkpoint, STREAMED, Stream, StreamConfig};
68
69pub struct Render {
70 pub root: NodeId,
71 pub tys: Typing,
72 pub buffers: BTreeMap<NodeId, Buffer>,
73 pub frames: BTreeMap<NodeId, Frames>,
74 pub symbolic: BTreeMap<NodeId, SpectralSum>,
75 pub labels: BTreeMap<NodeId, Label>,
76 pub traces: Vec<FilterTrace>,
77 pub config: RenderConfig,
78 pub schedule: Schedule,
79 pub bindings: BTreeMap<NodeId, Vec<Binding>>,
80 pub cache_stats: Option<CacheStats>,
81 pub proofs: u64,
83}
84
85impl Render {
86 pub fn work(&self) -> crate::flops::Work {
87 let samples = self.config.horizon.len(self.config.rate).unwrap_or(0);
88 crate::flops::Work {
89 samples: samples as u64,
90 proofs: self.proofs,
91 priced_flops: crate::flops::total(self),
92 waves: None,
93 }
94 }
95
96 pub fn buffer(&self, node: NodeId) -> Option<&Buffer> {
97 self.buffers.get(&node)
98 }
99
100 pub fn id(&self, path: &str) -> Option<NodeId> {
101 self.tys.id(path)
102 }
103
104 pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
106 self.tys.resolve(path)
107 }
108
109 pub fn alias_oversample(&self, node: NodeId) -> Option<u32> {
111 self.config
112 .asks
113 .iter()
114 .find_map(|ask| match ask.representation {
115 crate::query::Representation::Alias { oversample }
116 if self.node(&ask.node).is_ok_and(|asked| asked == node) =>
117 {
118 Some(oversample)
119 }
120 _ => None,
121 })
122 }
123
124 pub fn alias_score(&self, node: NodeId) -> AliasScore {
125 match self.alias_oversample(node) {
126 Some(_) => AliasScore::Asked,
127 None => AliasScore::NotAsked,
128 }
129 }
130}
131
132pub fn render(
135 graph: &Graph,
136 target: &str,
137 config: RenderConfig,
138 cache: Option<&Cache>,
139) -> Result<Render, EngineError> {
140 let recording = cache.map(|c| Recording::over(c, config.cache_policy));
141 let mut held = run(prepared(graph, target)?, config, recording.as_ref(), None)?;
142 held.cache_stats = recording.map(Recording::finish);
143 Ok(held)
144}
145
146pub(crate) struct Prepared<'g> {
148 pub(crate) instances: instantiate::Instances<'g>,
149 pub(crate) order: schedule::Order,
150 pub(crate) tys: Typing,
151 pub(crate) root: NodeId,
152 pub(crate) target: String,
153}
154
155impl Prepared<'_> {
156 pub(crate) fn identity(&self, asks: &[Ask]) -> Result<(sva_formula::Hash, bool), EngineError> {
158 let scored = asks.iter().any(|ask| {
159 matches!(
160 ask.representation,
161 crate::query::Representation::Alias { .. }
162 ) && self.tys.resolve(&ask.node).is_ok_and(|id| id == self.root)
163 });
164 Ok((refs::identity(&self.tys, self.root)?, scored))
165 }
166}
167
168pub(crate) fn prepared<'g>(graph: &'g Graph, target: &str) -> Result<Prepared<'g>, EngineError> {
169 let instances = instantiate::instantiate(graph, target)?;
170 let held = instances.instance_of(target)?;
171 let order = schedule::schedule_from(&instances, std::slice::from_ref(&held))?;
172 let tys = typing::infer_all(&instances, &order)?;
173 let root = tys
174 .id(&held)
175 .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
176 Ok(Prepared {
177 instances,
178 order,
179 tys,
180 root,
181 target: target.to_string(),
182 })
183}
184
185pub(crate) fn run(
188 prepared: Prepared,
189 config: RenderConfig,
190 recording: Option<&Recording>,
191 known: Option<(Buffer, Label)>,
192) -> Result<Render, EngineError> {
193 let Prepared {
194 instances,
195 order,
196 tys,
197 root,
198 target,
199 } = prepared;
200 let mut schedule = schedule::plan(&tys, &order, root, &config.asks);
201 let forks = schedule::forks(&tys, &schedule.materialize);
202 if known.is_some() {
203 match only_the_root(&tys, root, &config.asks) {
204 true => schedule.materialize.clear(),
205 false => schedule.materialize.retain(|id| *id != root),
206 }
207 }
208
209 let bindings = tys
210 .paths()
211 .filter_map(|(path, id)| Some((id, resolved(&instances, path)?)))
212 .collect();
213 let mut held = Render {
214 root,
215 tys,
216 buffers: BTreeMap::new(),
217 frames: BTreeMap::new(),
218 symbolic: BTreeMap::new(),
219 labels: BTreeMap::new(),
220 traces: Vec::new(),
221 config,
222 schedule,
223 bindings,
224 cache_stats: None,
225 proofs: 0,
226 };
227 if let Some((buffer, label)) = known {
228 held.buffers.insert(root, buffer);
229 held.labels.insert(root, label);
230 }
231 let lenses = Lenses {
232 recording,
233 volatile: volatile::mark(&instances, &held.tys, &held.config, &target)?,
234 forks,
235 root,
236 };
237 affordable(&held)?;
238 let needed = lenses.needed(&held);
239 for id in held.schedule.materialize.clone() {
240 if needed.contains(&id) {
241 materialize(&mut held, id, &lenses)?;
242 }
243 }
244 compose_read(&mut held);
245 stamp(&mut held);
246 Ok(held)
247}
248
249fn only_the_root(tys: &Typing, root: NodeId, asks: &[Ask]) -> bool {
251 asks.iter().all(|ask| {
252 tys.resolve(&ask.node).is_ok_and(|id| id == root)
253 && !matches!(
254 ask.representation,
255 crate::query::Representation::Ledger { .. }
256 )
257 })
258}
259
260fn affordable(held: &Render) -> Result<(), EngineError> {
264 if held.schedule.materialize.is_empty() {
265 return Ok(());
266 }
267 let total = crate::flops::total(held);
268 if total <= held.config.flop_budget {
269 return Ok(());
270 }
271 let counted = crate::flops::tree(held);
272 let over = crate::flops::dominating(&counted).expect("a counted tree holds its root");
273 Err(EngineError::refused(Diagnostic {
274 code: "collapse.over_budget".to_string(),
275 message: format!(
276 "this render counts {} operations, over the budget of {}; `{}` dominates it at \
277 {} by {}",
278 counted.total, counted.budget, over.node, over.subtree, over.route
279 ),
280 location: Located::at(held.tys.name(held.root), None),
281 help: format!("pass --flop-budget {} to render it anyway", counted.total),
282 }))
283}
284
285fn stamp(held: &mut Render) {
287 let root = held.root;
288 let Some(label) = held.labels.remove(&root) else {
289 return;
290 };
291 let counted = crate::flops::total(held);
292 held.labels
293 .insert(root, label.costing(counted, held.config.flop_budget));
294}
295
296fn compose_read(held: &mut Render) {
300 for id in held.schedule.compose.clone() {
301 if held.symbolic.contains_key(&id) {
302 continue;
303 }
304 if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
305 held.symbolic.insert(id, sum);
306 }
307 }
308}
309
310pub(crate) struct Lenses<'r> {
311 recording: Option<&'r Recording<'r>>,
312 volatile: volatile::Volatile,
313 forks: std::collections::BTreeSet<NodeId>,
314 root: NodeId,
315}
316
317impl Lenses<'_> {
318 pub(crate) fn none() -> Lenses<'static> {
319 Lenses {
320 recording: None,
321 volatile: volatile::Volatile::default(),
322 forks: Default::default(),
323 root: NodeId(0),
324 }
325 }
326
327 fn at(&self, id: NodeId) -> Option<Lens<'_>> {
328 let fork = self.forks.contains(&id);
329 self.recording
330 .map(|r| r.at(self.volatile.slot(id), fork, id == self.root))
331 }
332
333 fn needed(&self, held: &Render) -> std::collections::BTreeSet<NodeId> {
336 let materialize = &held.schedule.materialize;
337 let ledger = held.config.asks.iter().any(|ask| {
338 matches!(
339 ask.representation,
340 crate::query::Representation::Ledger { .. }
341 )
342 });
343 let kept = |id: &NodeId| {
344 ledger
345 || self
346 .recording
347 .is_none_or(|r| r.stores(self.forks.contains(id), *id == self.root))
348 };
349 let mut needed: std::collections::BTreeSet<NodeId> = materialize
350 .iter()
351 .copied()
352 .filter(|id| held.schedule.wanted.contains(id) || kept(id))
353 .collect();
354 for id in materialize.iter().rev() {
355 if !needed.contains(id) || self.answered(held, *id) {
356 continue;
357 }
358 needed.extend(reads(held, *id));
359 }
360 needed
361 }
362
363 fn answered(&self, held: &Render, id: NodeId) -> bool {
364 let (Some(recording), Held::Sampled) = (self.recording, held.tys.ty(id).held) else {
365 return false;
366 };
367 sampled::key(held, id).is_ok_and(|(key, _)| recording.holds(key))
368 }
369}
370
371fn reads(held: &Render, id: NodeId) -> Vec<NodeId> {
373 let mut out = schedule::materialized_operands(&held.tys, id);
374 if matches!(held.tys.ty(id).held, Held::Sampled)
375 && let Ok(program) = sampled::program(held, id)
376 {
377 out.extend(program.reads);
378 }
379 out.retain(|read| *read != id);
380 out
381}
382
383fn materialize(held: &mut Render, id: NodeId, lenses: &Lenses) -> Result<(), EngineError> {
386 if held.buffers.contains_key(&id) || held.frames.contains_key(&id) {
387 return Ok(());
388 }
389 let lens = lenses.at(id);
390 if matches!(held.tys.ty(id).held, Held::Sampled) {
391 let (key, samples) = sampled::key(held, id)?;
392 if let Some((hit, label)) = warm(held, id, key, samples, lens.as_ref()) {
393 held.buffers.insert(id, hit);
394 held.labels.insert(id, label);
395 return Ok(());
396 }
397 for read in reads(held, id) {
398 materialize(held, read, lenses)?;
399 }
400 return sampled::run(held, id, key, lens.as_ref());
401 }
402 for operand in schedule::materialized_operands(&held.tys, id) {
403 materialize(held, operand, lenses)?;
404 }
405 match held.tys.ty(id).held {
406 Held::Frames => frames_of(held, id, lens.as_ref()),
407 _ => collapse_closed_form(held, id, lens.as_ref()),
408 }
409}
410
411fn collapse_closed_form(
413 held: &mut Render,
414 id: NodeId,
415 cache: Option<&Lens>,
416) -> Result<(), EngineError> {
417 let var = held.tys.var(id);
418 let written = match refs::resolve(&held.tys, id, 0, held.tys.ty(id).held) {
419 Ok(refs::Read::Substitute(form)) => Some(*form),
420 _ => None,
421 };
422 let symbolic = written.as_ref().map(|t| symbolic_key(hash_closed_form(t)));
423 let sum = remembered(held, id, symbolic, cache)
424 .map(Ok)
425 .unwrap_or_else(|| {
426 let found = refs::spectral_sum_of(&held.tys, id, var);
427 if let (Ok(sum), Some(key), Some(cache)) = (&found, symbolic, cache) {
428 cache.store(key, &Payload::Symbolic(Box::new(sum.clone())), None);
429 }
430 found
431 });
432 let identity = match refs::closed_form_identity(&sum, written.as_ref()) {
433 Ok(identity) => identity,
434 Err(_) if var == sva_formula::Var::T => refs::identity(&held.tys, id)?,
435 Err(e) => return Err(e),
436 };
437 let score = held.alias_score(id);
438 let samples = length(held, id)?;
439 let key = crate::cache::buffer_key(
440 identity,
441 held.config.rate,
442 held.config.horizon.start_secs,
443 samples,
444 held.tys.ty(id).width as usize,
445 score,
446 );
447 if let Ok(sum) = &sum {
448 held.symbolic.insert(id, sum.clone());
449 }
450 if let Some((hit, label)) = warm(held, id, key, samples, cache) {
452 held.buffers.insert(id, hit);
453 held.labels.insert(id, label);
454 return Ok(());
455 }
456 let (buffer, label) = match (&sum, &written) {
457 (Err(_), None) => pointwise::point_sample(held, id, score)?,
458 _ => sampled_form(held, &sum, written.as_ref(), score)
459 .map_err(|e| collapse_refused(held, id, &e))?,
460 };
461 store(key, &buffer, &label, cache);
462 held.buffers.insert(id, buffer);
463 held.labels.insert(id, label);
464 Ok(())
465}
466
467fn collapse_refused(held: &Render, id: NodeId, e: &sva_samples::CollapseError) -> EngineError {
468 EngineError::refused(Diagnostic {
469 code: e.code().to_string(),
470 message: e.to_string(),
471 location: Located::at(held.tys.name(id), None),
472 help: e.help().to_string(),
473 })
474}
475
476fn length(held: &Render, id: NodeId) -> Result<usize, EngineError> {
477 held.config
478 .horizon
479 .len(held.config.rate)
480 .map_err(|e| collapse_refused(held, id, &e))
481}
482
483fn sampled_form(
486 held: &Render,
487 sum: &Result<SpectralSum, EngineError>,
488 written: Option<&sva_formula::ClosedForm>,
489 score: AliasScore,
490) -> Result<(Buffer, Label), sva_samples::CollapseError> {
491 let (rate, horizon, profile) = (held.config.rate, held.config.horizon, &held.config.profile);
492 match (sum, written) {
493 (Ok(sum), written) => {
494 sva_samples::of_spectral_sum_or_point(sum, written, rate, horizon, profile, score)
495 }
496 (Err(_), Some(form)) => sva_samples::render(form, rate, horizon, profile, score),
497 (Err(_), None) => unreachable!("a closed form with neither view is point-sampled above"),
498 }
499}
500
501fn remembered(
503 held: &Render,
504 id: NodeId,
505 key: Option<sva_formula::Hash>,
506 cache: Option<&Lens>,
507) -> Option<SpectralSum> {
508 let entry = cache?.load(key?, held.tys.name(id), Expected::Symbolic)?;
509 entry.payload.symbolic().cloned()
510}
511
512fn warm(
515 held: &Render,
516 id: NodeId,
517 key: sva_formula::Hash,
518 samples: usize,
519 cache: Option<&Lens>,
520) -> Option<(Buffer, Label)> {
521 let expected = Expected::Samples {
522 rate: held.config.rate,
523 width: held.tys.ty(id).width as usize,
524 samples,
525 };
526 let entry = cache?.load(key, held.tys.name(id), expected)?;
527 Some((entry.payload.samples().cloned()?, entry.label?))
528}
529
530fn store(key: sva_formula::Hash, buffer: &Buffer, label: &Label, cache: Option<&Lens>) {
531 if let Some(cache) = cache {
532 cache.store(
533 key,
534 &Payload::Samples(Box::new(buffer.clone())),
535 Some(label),
536 );
537 }
538}
539
540fn frames_of(held: &mut Render, id: NodeId, cache: Option<&Lens>) -> Result<(), EngineError> {
541 let Value::Cast(Cast::Stft { window, hop }, source) = *held.tys.value(id) else {
542 return Err(not_frames(held, id));
543 };
544 let buffer = held
545 .buffers
546 .get(&source)
547 .ok_or_else(|| not_frames(held, id))?;
548 let key = frames_key(
549 crate::cache::buffer_key(
550 refs::identity(&held.tys, source)?,
551 buffer.rate,
552 buffer.origin_secs,
553 buffer.len(),
554 buffer.width,
555 AliasScore::NotAsked,
556 ),
557 window,
558 hop,
559 );
560 if let Some(entry) = cache.and_then(|c| c.load(key, held.tys.name(id), Expected::Frames))
561 && let Payload::Frames(frames) = entry.payload
562 {
563 held.frames.insert(id, *frames);
564 return Ok(());
565 }
566 let frames = stft::forward(buffer, window, hop).map_err(|e| sampled::refused(held, id, &e))?;
567 if let Some(cache) = cache {
568 cache.store(key, &Payload::Frames(Box::new(frames.clone())), None);
569 }
570 held.frames.insert(id, frames);
571 Ok(())
572}
573
574fn not_frames(held: &Render, id: NodeId) -> EngineError {
575 EngineError::refused(Diagnostic {
576 code: "cast.stft_needs_samples".to_string(),
577 message: format!("`{}` holds no frames to read", held.tys.name(id)),
578 location: Located::at(held.tys.name(id), None),
579 help: "write stft(sample(x), window=, hop=)".to_string(),
580 })
581}
582
583fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
584 Some(
585 instances
586 .bindings(path)?
587 .into_iter()
588 .map(|(name, expr, cx)| Binding {
589 name: name.to_string(),
590 source: instances.render(expr, cx),
591 })
592 .collect(),
593 )
594}