1mod answer;
4mod pointwise;
5mod sampled;
6mod slots;
7
8use std::collections::BTreeMap;
9
10use sva_ast::Graph;
11use sva_formula::{Held, NodeId, SpectralSum, hash_closed_form};
12use sva_samples::{
13 AliasScore, Buffer, FilterTrace, Frames, Horizon, Label, PSYCHOACOUSTIC_V1, Profile, stft,
14};
15
16use crate::bindings::Binding;
17use crate::cache::{
18 CacheStats, Cost, Expected, Payload, PayloadKind, Recording, frames_key, symbolic_key,
19};
20use crate::cast::Cast;
21use crate::error::{Diagnostic, EngineError, Located};
22use crate::instantiate;
23use crate::query::Ask;
24use crate::refs;
25use crate::schedule::{self, Schedule};
26use crate::typing::{self, Typing, Value};
27
28#[derive(Clone, Debug, PartialEq)]
29pub struct RenderConfig {
30 pub rate: u32,
31 pub horizon: Horizon,
32 pub profile: Profile,
33 pub asks: Vec<Ask>,
35 pub flop_budget: u128,
37}
38
39impl RenderConfig {
40 pub fn seconds(rate: u32, secs: f64) -> RenderConfig {
41 RenderConfig {
42 rate,
43 horizon: Horizon::secs(0.0, secs),
44 profile: PSYCHOACOUSTIC_V1,
45 asks: Vec::new(),
46 flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
47 }
48 }
49
50 pub fn asking(mut self, asks: Vec<Ask>) -> RenderConfig {
51 self.asks = asks;
52 self
53 }
54}
55
56pub use answer::{answer, answer_buffer, sketch_atom};
57
58pub struct Render {
59 pub root: NodeId,
60 pub tys: Typing,
61 pub buffers: BTreeMap<NodeId, Buffer>,
62 pub frames: BTreeMap<NodeId, Frames>,
63 pub symbolic: BTreeMap<NodeId, SpectralSum>,
64 pub labels: BTreeMap<NodeId, Label>,
65 pub traces: Vec<FilterTrace>,
66 pub config: RenderConfig,
67 pub schedule: Schedule,
68 pub bindings: BTreeMap<NodeId, Vec<Binding>>,
69 pub cache_stats: Option<CacheStats>,
70}
71
72impl Render {
73 pub fn buffer(&self, node: NodeId) -> Option<&Buffer> {
74 self.buffers.get(&node)
75 }
76
77 pub fn id(&self, path: &str) -> Option<NodeId> {
78 self.tys.id(path)
79 }
80
81 pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
83 self.tys.resolve(path)
84 }
85
86 pub fn alias_oversample(&self, node: NodeId) -> Option<u32> {
88 self.config
89 .asks
90 .iter()
91 .find_map(|ask| match ask.representation {
92 crate::query::Representation::Alias { oversample }
93 if self.node(&ask.node).is_ok_and(|asked| asked == node) =>
94 {
95 Some(oversample)
96 }
97 _ => None,
98 })
99 }
100
101 pub fn alias_score(&self, node: NodeId) -> AliasScore {
102 match self.alias_oversample(node) {
103 Some(_) => AliasScore::Asked,
104 None => AliasScore::NotAsked,
105 }
106 }
107}
108
109pub fn render(
112 graph: &Graph,
113 target: &str,
114 config: RenderConfig,
115 cache: Option<&dyn crate::cache::Cache>,
116) -> Result<Render, EngineError> {
117 let instances = instantiate::instantiate(graph, target)?;
118 let held = instances.instance_of(target)?;
119 let order = schedule::schedule_from(&instances, std::slice::from_ref(&held))?;
120 let tys = typing::infer_all(&instances, &order)?;
121 let root = tys
122 .id(&held)
123 .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
124 let schedule = schedule::plan(&tys, &order, root, &config.asks);
125
126 let bindings = tys
127 .paths()
128 .filter_map(|(path, id)| Some((id, resolved(&instances, path)?)))
129 .collect();
130 let mut held = Render {
131 root,
132 tys,
133 buffers: BTreeMap::new(),
134 frames: BTreeMap::new(),
135 symbolic: BTreeMap::new(),
136 labels: BTreeMap::new(),
137 traces: Vec::new(),
138 config,
139 schedule,
140 bindings,
141 cache_stats: None,
142 };
143 affordable(&held)?;
144 let recording = cache.map(Recording::over);
145 for id in held.schedule.materialize.clone() {
146 materialize(
147 &mut held,
148 id,
149 recording.as_ref().map(|r| r as &dyn crate::cache::Cache),
150 )?;
151 }
152 held.cache_stats = recording.map(Recording::finish);
153 compose_read(&mut held);
154 stamp(&mut held);
155 Ok(held)
156}
157
158fn affordable(held: &Render) -> Result<(), EngineError> {
162 if held.schedule.materialize.is_empty() {
163 return Ok(());
164 }
165 let total = crate::flops::total(held);
166 if total <= held.config.flop_budget {
167 return Ok(());
168 }
169 let counted = crate::flops::tree(held);
170 let over = crate::flops::dominating(&counted).expect("a counted tree holds its root");
171 Err(EngineError::refused(Diagnostic {
172 code: "collapse.over_budget".to_string(),
173 message: format!(
174 "this render counts {} operations, over the budget of {}; `{}` dominates it at \
175 {} by {}",
176 counted.total, counted.budget, over.node, over.subtree, over.route
177 ),
178 location: Located::at(held.tys.name(held.root), None),
179 help: format!("pass --flop-budget {} to render it anyway", counted.total),
180 }))
181}
182
183fn stamp(held: &mut Render) {
185 let root = held.root;
186 let Some(label) = held.labels.remove(&root) else {
187 return;
188 };
189 let counted = crate::flops::total(held);
190 held.labels
191 .insert(root, label.costing(counted, held.config.flop_budget));
192}
193
194fn compose_read(held: &mut Render) {
198 for id in held.schedule.compose.clone() {
199 if held.symbolic.contains_key(&id) {
200 continue;
201 }
202 if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
203 held.symbolic.insert(id, sum);
204 }
205 }
206}
207
208fn materialize(
209 held: &mut Render,
210 id: NodeId,
211 cache: Option<&dyn crate::cache::Cache>,
212) -> Result<(), EngineError> {
213 match held.tys.ty(id).held {
214 Held::Frames => frames_of(held, id, cache),
215 Held::Sampled => sampled::run(held, id, cache),
216 _ => collapse_closed_form(held, id, cache),
217 }
218}
219
220fn collapse_closed_form(
222 held: &mut Render,
223 id: NodeId,
224 cache: Option<&dyn crate::cache::Cache>,
225) -> Result<(), EngineError> {
226 let var = held.tys.var(id);
227 let written = match refs::resolve(&held.tys, id, 0, held.tys.ty(id).held) {
228 Ok(refs::Read::Substitute(form)) => Some(*form),
229 _ => None,
230 };
231 let symbolic = written.as_ref().map(|t| symbolic_key(hash_closed_form(t)));
232 let sum = remembered(held, id, symbolic, cache)
233 .map(Ok)
234 .unwrap_or_else(|| {
235 let began = Cost::begun();
236 let found = refs::spectral_sum_of(&held.tys, id, var);
237 if let (Ok(sum), Some(key), Some(cache)) = (&found, symbolic, cache) {
238 let payload = Payload::Symbolic(Box::new(sum.clone()));
239 if cache.worth_storing(began.elapsed(), payload.bytes(), PayloadKind::Symbolic) {
240 cache.store(key, &payload, &[], None);
241 }
242 }
243 found
244 });
245 let identity = match refs::closed_form_identity(&sum, written.as_ref()) {
246 Ok(identity) => identity,
247 Err(_) if var == sva_formula::Var::T => refs::identity(&held.tys, id)?,
248 Err(e) => return Err(e),
249 };
250 let score = held.alias_score(id);
251 let key = crate::cache::buffer_key(
252 identity,
253 held.config.rate,
254 held.config.horizon.start_secs,
255 held.tys.ty(id).width as usize,
256 score,
257 );
258 if let Ok(sum) = &sum {
259 held.symbolic.insert(id, sum.clone());
260 }
261 if let Some((hit, label)) = warm(held, id, key, cache) {
263 held.buffers.insert(id, hit);
264 held.labels.insert(id, label);
265 return Ok(());
266 }
267 let began = Cost::begun();
268 let (buffer, label) = match (&sum, &written) {
269 (Err(_), None) => pointwise::point_sample(held, id, score)?,
270 _ => sampled_form(held, &sum, written.as_ref(), score).map_err(|e| {
271 EngineError::refused(Diagnostic {
272 code: e.code().to_string(),
273 message: e.to_string(),
274 location: Located::at(held.tys.name(id), None),
275 help: e.help().to_string(),
276 })
277 })?,
278 };
279 store(key, &buffer, &label, began.elapsed(), cache);
280 held.buffers.insert(id, buffer);
281 held.labels.insert(id, label);
282 Ok(())
283}
284
285fn sampled_form(
288 held: &Render,
289 sum: &Result<SpectralSum, EngineError>,
290 written: Option<&sva_formula::ClosedForm>,
291 score: AliasScore,
292) -> Result<(Buffer, Label), sva_samples::CollapseError> {
293 let (rate, horizon, profile) = (held.config.rate, held.config.horizon, &held.config.profile);
294 match (sum, written) {
295 (Ok(sum), written) => {
296 sva_samples::of_spectral_sum_or_point(sum, written, rate, horizon, profile, score)
297 }
298 (Err(_), Some(form)) => sva_samples::render(form, rate, horizon, profile, score),
299 (Err(_), None) => unreachable!("a closed form with neither view is point-sampled above"),
300 }
301}
302
303fn remembered(
305 held: &Render,
306 id: NodeId,
307 key: Option<sva_formula::Hash>,
308 cache: Option<&dyn crate::cache::Cache>,
309) -> Option<SpectralSum> {
310 let entry = cache?.load(key?, held.tys.name(id), Expected::Symbolic)?;
311 entry.payload.symbolic().cloned()
312}
313
314fn warm(
317 held: &Render,
318 id: NodeId,
319 key: sva_formula::Hash,
320 cache: Option<&dyn crate::cache::Cache>,
321) -> Option<(Buffer, Label)> {
322 let expected = Expected::Samples {
323 rate: held.config.rate,
324 width: held.tys.ty(id).width as usize,
325 samples: held.config.horizon.len(held.config.rate).ok()?,
326 };
327 let entry = cache?.load(key, held.tys.name(id), expected)?;
328 Some((entry.payload.samples().cloned()?, entry.label?))
329}
330
331fn store(
332 key: sva_formula::Hash,
333 buffer: &Buffer,
334 label: &Label,
335 cost: std::time::Duration,
336 cache: Option<&dyn crate::cache::Cache>,
337) {
338 let Some(cache) = cache else { return };
339 let payload = Payload::Samples(Box::new(buffer.clone()));
340 if cache.worth_storing(cost, payload.bytes(), PayloadKind::Samples) {
341 cache.store(key, &payload, &[], Some(label));
342 }
343}
344
345fn frames_of(
346 held: &mut Render,
347 id: NodeId,
348 cache: Option<&dyn crate::cache::Cache>,
349) -> Result<(), EngineError> {
350 let Value::Cast(Cast::Stft { window, hop }, source) = *held.tys.value(id) else {
351 return Err(not_frames(held, id));
352 };
353 let buffer = held
354 .buffers
355 .get(&source)
356 .ok_or_else(|| not_frames(held, id))?;
357 let key = frames_key(
358 crate::cache::buffer_key(
359 refs::identity(&held.tys, source)?,
360 buffer.rate,
361 buffer.origin_secs,
362 buffer.width,
363 AliasScore::NotAsked,
364 ),
365 window,
366 hop,
367 );
368 if let Some(entry) = cache.and_then(|c| c.load(key, held.tys.name(id), Expected::Frames))
369 && let Payload::Frames(frames) = entry.payload
370 {
371 held.frames.insert(id, *frames);
372 return Ok(());
373 }
374 let began = Cost::begun();
375 let frames = stft::forward(buffer, window, hop).map_err(|e| sampled::refused(held, id, &e))?;
376 if let Some(cache) = cache {
377 let payload = Payload::Frames(Box::new(frames.clone()));
378 if cache.worth_storing(began.elapsed(), payload.bytes(), PayloadKind::Frames) {
379 cache.store(key, &payload, &[], None);
380 }
381 }
382 held.frames.insert(id, frames);
383 Ok(())
384}
385
386fn not_frames(held: &Render, id: NodeId) -> EngineError {
387 EngineError::refused(Diagnostic {
388 code: "cast.stft_needs_samples".to_string(),
389 message: format!("`{}` holds no frames to read", held.tys.name(id)),
390 location: Located::at(held.tys.name(id), None),
391 help: "write stft(sample(x), window=, hop=)".to_string(),
392 })
393}
394
395fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
397 Some(
398 instances
399 .bindings(path)?
400 .into_iter()
401 .map(|(name, expr, cx)| Binding {
402 name: name.to_string(),
403 source: instances.render(expr, cx),
404 })
405 .collect(),
406 )
407}