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 CacheStats, Cost, Expected, Payload, PayloadKind, Recording, Slots, 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}
43
44impl RenderConfig {
45 pub fn seconds(rate: u32, secs: f64) -> RenderConfig {
46 RenderConfig {
47 rate,
48 horizon: Horizon::secs(0.0, secs),
49 profile: PSYCHOACOUSTIC_V1,
50 asks: Vec::new(),
51 flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
52 volatile: Vec::new(),
53 }
54 }
55
56 pub fn asking(mut self, asks: Vec<Ask>) -> RenderConfig {
57 self.asks = asks;
58 self
59 }
60}
61
62pub use answer::{answer, answer_buffer, ledger_over, sketch_atom};
63pub use silent::{Silent, render_until_silent};
64pub use stream::{Block, Checkpoint, STREAMED, Stream, StreamConfig};
65
66pub struct Render {
67 pub root: NodeId,
68 pub tys: Typing,
69 pub buffers: BTreeMap<NodeId, Buffer>,
70 pub frames: BTreeMap<NodeId, Frames>,
71 pub symbolic: BTreeMap<NodeId, SpectralSum>,
72 pub labels: BTreeMap<NodeId, Label>,
73 pub traces: Vec<FilterTrace>,
74 pub config: RenderConfig,
75 pub schedule: Schedule,
76 pub bindings: BTreeMap<NodeId, Vec<Binding>>,
77 pub cache_stats: Option<CacheStats>,
78 pub proofs: u64,
80}
81
82impl Render {
83 pub fn work(&self) -> crate::flops::Work {
85 let samples = self.config.horizon.len(self.config.rate).unwrap_or(0);
86 crate::flops::Work {
87 samples: samples as u64,
88 proofs: self.proofs,
89 priced_flops: crate::flops::total(self),
90 waves: None,
91 }
92 }
93
94 pub fn buffer(&self, node: NodeId) -> Option<&Buffer> {
95 self.buffers.get(&node)
96 }
97
98 pub fn id(&self, path: &str) -> Option<NodeId> {
99 self.tys.id(path)
100 }
101
102 pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
104 self.tys.resolve(path)
105 }
106
107 pub fn alias_oversample(&self, node: NodeId) -> Option<u32> {
109 self.config
110 .asks
111 .iter()
112 .find_map(|ask| match ask.representation {
113 crate::query::Representation::Alias { oversample }
114 if self.node(&ask.node).is_ok_and(|asked| asked == node) =>
115 {
116 Some(oversample)
117 }
118 _ => None,
119 })
120 }
121
122 pub fn alias_score(&self, node: NodeId) -> AliasScore {
123 match self.alias_oversample(node) {
124 Some(_) => AliasScore::Asked,
125 None => AliasScore::NotAsked,
126 }
127 }
128}
129
130pub fn render(
131 graph: &Graph,
132 target: &str,
133 config: RenderConfig,
134 cache: Option<&dyn crate::cache::Cache>,
135) -> Result<Render, EngineError> {
136 render_with_slots(graph, target, config, cache, None)
137}
138
139pub fn render_with_slots(
142 graph: &Graph,
143 target: &str,
144 config: RenderConfig,
145 cache: Option<&dyn crate::cache::Cache>,
146 slots: Option<&Slots>,
147) -> Result<Render, EngineError> {
148 run(prepared(graph, target)?, config, cache, slots, None)
149}
150
151pub(crate) struct Prepared<'g> {
153 pub(crate) instances: instantiate::Instances<'g>,
154 pub(crate) order: schedule::Order,
155 pub(crate) tys: Typing,
156 pub(crate) root: NodeId,
157 pub(crate) target: String,
158}
159
160impl Prepared<'_> {
161 pub(crate) fn identity(&self, asks: &[Ask]) -> Result<(sva_formula::Hash, bool), EngineError> {
163 let scored = asks.iter().any(|ask| {
164 matches!(
165 ask.representation,
166 crate::query::Representation::Alias { .. }
167 ) && self.tys.resolve(&ask.node).is_ok_and(|id| id == self.root)
168 });
169 Ok((refs::identity(&self.tys, self.root)?, scored))
170 }
171}
172
173pub(crate) fn prepared<'g>(graph: &'g Graph, target: &str) -> Result<Prepared<'g>, EngineError> {
174 let instances = instantiate::instantiate(graph, target)?;
175 let held = instances.instance_of(target)?;
176 let order = schedule::schedule_from(&instances, std::slice::from_ref(&held))?;
177 let tys = typing::infer_all(&instances, &order)?;
178 let root = tys
179 .id(&held)
180 .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
181 Ok(Prepared {
182 instances,
183 order,
184 tys,
185 root,
186 target: target.to_string(),
187 })
188}
189
190pub(crate) fn run(
193 prepared: Prepared,
194 config: RenderConfig,
195 cache: Option<&dyn crate::cache::Cache>,
196 slots: Option<&Slots>,
197 known: Option<(Buffer, Label)>,
198) -> Result<Render, EngineError> {
199 let Prepared {
200 instances,
201 order,
202 tys,
203 root,
204 target,
205 } = prepared;
206 let mut schedule = schedule::plan(&tys, &order, root, &config.asks);
207 if known.is_some() {
208 match only_the_root(&tys, root, &config.asks) {
209 true => schedule.materialize.clear(),
210 false => schedule.materialize.retain(|id| *id != root),
211 }
212 }
213
214 let bindings = tys
215 .paths()
216 .filter_map(|(path, id)| Some((id, resolved(&instances, path)?)))
217 .collect();
218 let mut held = Render {
219 root,
220 tys,
221 buffers: BTreeMap::new(),
222 frames: BTreeMap::new(),
223 symbolic: BTreeMap::new(),
224 labels: BTreeMap::new(),
225 traces: Vec::new(),
226 config,
227 schedule,
228 bindings,
229 cache_stats: None,
230 proofs: 0,
231 };
232 if let Some((buffer, label)) = known {
233 held.buffers.insert(root, buffer);
234 held.labels.insert(root, label);
235 }
236 let volatile = volatile::mark(&instances, &held.tys, &held.config, &target)?;
237 affordable(&held)?;
238 let recording = cache.map(|c| Recording::over(c, slots));
239 for id in held.schedule.materialize.clone() {
240 let lens = recording.as_ref().map(|r| r.at(volatile.slot(id)));
241 materialize(
242 &mut held,
243 id,
244 lens.as_ref().map(|l| l as &dyn crate::cache::Cache),
245 )?;
246 }
247 held.cache_stats = recording.map(Recording::finish);
248 compose_read(&mut held);
249 stamp(&mut held);
250 Ok(held)
251}
252
253fn only_the_root(tys: &Typing, root: NodeId, asks: &[Ask]) -> bool {
255 asks.iter().all(|ask| {
256 tys.resolve(&ask.node).is_ok_and(|id| id == root)
257 && !matches!(
258 ask.representation,
259 crate::query::Representation::Ledger { .. }
260 )
261 })
262}
263
264fn affordable(held: &Render) -> Result<(), EngineError> {
268 if held.schedule.materialize.is_empty() {
269 return Ok(());
270 }
271 let total = crate::flops::total(held);
272 if total <= held.config.flop_budget {
273 return Ok(());
274 }
275 let counted = crate::flops::tree(held);
276 let over = crate::flops::dominating(&counted).expect("a counted tree holds its root");
277 Err(EngineError::refused(Diagnostic {
278 code: "collapse.over_budget".to_string(),
279 message: format!(
280 "this render counts {} operations, over the budget of {}; `{}` dominates it at \
281 {} by {}",
282 counted.total, counted.budget, over.node, over.subtree, over.route
283 ),
284 location: Located::at(held.tys.name(held.root), None),
285 help: format!("pass --flop-budget {} to render it anyway", counted.total),
286 }))
287}
288
289fn stamp(held: &mut Render) {
291 let root = held.root;
292 let Some(label) = held.labels.remove(&root) else {
293 return;
294 };
295 let counted = crate::flops::total(held);
296 held.labels
297 .insert(root, label.costing(counted, held.config.flop_budget));
298}
299
300fn compose_read(held: &mut Render) {
304 for id in held.schedule.compose.clone() {
305 if held.symbolic.contains_key(&id) {
306 continue;
307 }
308 if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
309 held.symbolic.insert(id, sum);
310 }
311 }
312}
313
314fn materialize(
315 held: &mut Render,
316 id: NodeId,
317 cache: Option<&dyn crate::cache::Cache>,
318) -> Result<(), EngineError> {
319 match held.tys.ty(id).held {
320 Held::Frames => frames_of(held, id, cache),
321 Held::Sampled => sampled::run(held, id, cache),
322 _ => collapse_closed_form(held, id, cache),
323 }
324}
325
326fn collapse_closed_form(
328 held: &mut Render,
329 id: NodeId,
330 cache: Option<&dyn crate::cache::Cache>,
331) -> Result<(), EngineError> {
332 let var = held.tys.var(id);
333 let written = match refs::resolve(&held.tys, id, 0, held.tys.ty(id).held) {
334 Ok(refs::Read::Substitute(form)) => Some(*form),
335 _ => None,
336 };
337 let symbolic = written.as_ref().map(|t| symbolic_key(hash_closed_form(t)));
338 let sum = remembered(held, id, symbolic, cache)
339 .map(Ok)
340 .unwrap_or_else(|| {
341 let began = Cost::begun();
342 let found = refs::spectral_sum_of(&held.tys, id, var);
343 if let (Ok(sum), Some(key), Some(cache)) = (&found, symbolic, cache) {
344 let payload = Payload::Symbolic(Box::new(sum.clone()));
345 if cache.worth_storing(began.elapsed(), payload.bytes(), PayloadKind::Symbolic) {
346 cache.store(key, &payload, &[], None);
347 }
348 }
349 found
350 });
351 let identity = match refs::closed_form_identity(&sum, written.as_ref()) {
352 Ok(identity) => identity,
353 Err(_) if var == sva_formula::Var::T => refs::identity(&held.tys, id)?,
354 Err(e) => return Err(e),
355 };
356 let score = held.alias_score(id);
357 let samples = length(held, id)?;
358 let key = crate::cache::buffer_key(
359 identity,
360 held.config.rate,
361 held.config.horizon.start_secs,
362 samples,
363 held.tys.ty(id).width as usize,
364 score,
365 );
366 if let Ok(sum) = &sum {
367 held.symbolic.insert(id, sum.clone());
368 }
369 if let Some((hit, label)) = warm(held, id, key, samples, cache) {
371 held.buffers.insert(id, hit);
372 held.labels.insert(id, label);
373 return Ok(());
374 }
375 let began = Cost::begun();
376 let (buffer, label) = match (&sum, &written) {
377 (Err(_), None) => pointwise::point_sample(held, id, score)?,
378 _ => sampled_form(held, &sum, written.as_ref(), score)
379 .map_err(|e| collapse_refused(held, id, &e))?,
380 };
381 store(key, &buffer, &label, began.elapsed(), cache);
382 held.buffers.insert(id, buffer);
383 held.labels.insert(id, label);
384 Ok(())
385}
386
387fn collapse_refused(held: &Render, id: NodeId, e: &sva_samples::CollapseError) -> EngineError {
388 EngineError::refused(Diagnostic {
389 code: e.code().to_string(),
390 message: e.to_string(),
391 location: Located::at(held.tys.name(id), None),
392 help: e.help().to_string(),
393 })
394}
395
396fn length(held: &Render, id: NodeId) -> Result<usize, EngineError> {
397 held.config
398 .horizon
399 .len(held.config.rate)
400 .map_err(|e| collapse_refused(held, id, &e))
401}
402
403fn sampled_form(
406 held: &Render,
407 sum: &Result<SpectralSum, EngineError>,
408 written: Option<&sva_formula::ClosedForm>,
409 score: AliasScore,
410) -> Result<(Buffer, Label), sva_samples::CollapseError> {
411 let (rate, horizon, profile) = (held.config.rate, held.config.horizon, &held.config.profile);
412 match (sum, written) {
413 (Ok(sum), written) => {
414 sva_samples::of_spectral_sum_or_point(sum, written, rate, horizon, profile, score)
415 }
416 (Err(_), Some(form)) => sva_samples::render(form, rate, horizon, profile, score),
417 (Err(_), None) => unreachable!("a closed form with neither view is point-sampled above"),
418 }
419}
420
421fn remembered(
423 held: &Render,
424 id: NodeId,
425 key: Option<sva_formula::Hash>,
426 cache: Option<&dyn crate::cache::Cache>,
427) -> Option<SpectralSum> {
428 let entry = cache?.load(key?, held.tys.name(id), Expected::Symbolic)?;
429 entry.payload.symbolic().cloned()
430}
431
432fn warm(
435 held: &Render,
436 id: NodeId,
437 key: sva_formula::Hash,
438 samples: usize,
439 cache: Option<&dyn crate::cache::Cache>,
440) -> Option<(Buffer, Label)> {
441 let expected = Expected::Samples {
442 rate: held.config.rate,
443 width: held.tys.ty(id).width as usize,
444 samples,
445 };
446 let entry = cache?.load(key, held.tys.name(id), expected)?;
447 Some((entry.payload.samples().cloned()?, entry.label?))
448}
449
450fn store(
451 key: sva_formula::Hash,
452 buffer: &Buffer,
453 label: &Label,
454 cost: std::time::Duration,
455 cache: Option<&dyn crate::cache::Cache>,
456) {
457 let Some(cache) = cache else { return };
458 let payload = Payload::Samples(Box::new(buffer.clone()));
459 if cache.worth_storing(cost, payload.bytes(), PayloadKind::Samples) {
460 cache.store(key, &payload, &[], Some(label));
461 }
462}
463
464fn frames_of(
465 held: &mut Render,
466 id: NodeId,
467 cache: Option<&dyn crate::cache::Cache>,
468) -> Result<(), EngineError> {
469 let Value::Cast(Cast::Stft { window, hop }, source) = *held.tys.value(id) else {
470 return Err(not_frames(held, id));
471 };
472 let buffer = held
473 .buffers
474 .get(&source)
475 .ok_or_else(|| not_frames(held, id))?;
476 let key = frames_key(
477 crate::cache::buffer_key(
478 refs::identity(&held.tys, source)?,
479 buffer.rate,
480 buffer.origin_secs,
481 buffer.len(),
482 buffer.width,
483 AliasScore::NotAsked,
484 ),
485 window,
486 hop,
487 );
488 if let Some(entry) = cache.and_then(|c| c.load(key, held.tys.name(id), Expected::Frames))
489 && let Payload::Frames(frames) = entry.payload
490 {
491 held.frames.insert(id, *frames);
492 return Ok(());
493 }
494 let began = Cost::begun();
495 let frames = stft::forward(buffer, window, hop).map_err(|e| sampled::refused(held, id, &e))?;
496 if let Some(cache) = cache {
497 let payload = Payload::Frames(Box::new(frames.clone()));
498 if cache.worth_storing(began.elapsed(), payload.bytes(), PayloadKind::Frames) {
499 cache.store(key, &payload, &[], None);
500 }
501 }
502 held.frames.insert(id, frames);
503 Ok(())
504}
505
506fn not_frames(held: &Render, id: NodeId) -> EngineError {
507 EngineError::refused(Diagnostic {
508 code: "cast.stft_needs_samples".to_string(),
509 message: format!("`{}` holds no frames to read", held.tys.name(id)),
510 location: Located::at(held.tys.name(id), None),
511 help: "write stft(sample(x), window=, hop=)".to_string(),
512 })
513}
514
515fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
517 Some(
518 instances
519 .bindings(path)?
520 .into_iter()
521 .map(|(name, expr, cx)| Binding {
522 name: name.to_string(),
523 source: instances.render(expr, cx),
524 })
525 .collect(),
526 )
527}