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