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