Skip to main content

sva_engine/render/
mod.rs

1// Concern: holds a composition's decided types, the buffers a reading needed and each label | Non-concern: ordering the work (schedule.rs), a sampled shader (sampled.rs) | IO: (&Graph, target) -> Render
2
3mod 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    /// What the caller means to read. An empty list is audio out, which collapses the root.
37    pub asks: Vec<Ask>,
38    /// The operation count this render may pay; the profile's own until a caller raises it.
39    pub flop_budget: u128,
40    /// What reads one of these is kept in a slot, never in a store.
41    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    /// Silence proofs run over the whole grid.
79    pub proofs: u64,
80}
81
82impl Render {
83    /// Priced by its schedule, whether run or answered from a store.
84    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    /// The node a reading names, refusing by name where a file expanded into several.
103    pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
104        self.tys.resolve(path)
105    }
106
107    /// The multiple a reading asks this node's score against, where one asks for a score at all.
108    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
139/// Nothing is materialized that no reading asked for: a closed form answered off its spectral sum
140/// allocates no buffer at all.
141pub 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
151/// A composition resolved and typed under one target, before any horizon is chosen.
152pub(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    /// The root's content address, and whether a reading asks it for an alias score.
162    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
190/// The root's value already held, where `known` carries it: only what another reading
191/// names is materialized beside it.
192pub(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
253/// Whether every reading is one of the root's own samples, so a held root answers them all.
254fn 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
264/// A reading that materializes nothing is never refused for cost: asking what a render
265/// would cost is not paying for it. Anything that runs is counted first, and a count past
266/// the budget refuses before a sample is computed.
267fn 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
289/// FORMAT 9.3: the render's own label says what it cost and what it was allowed.
290fn 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
300/// One render, every reading: a closed form a reading asks for is composed once here, and each
301/// `--as` answers off that one form rather than walking the graph again. A form that
302/// refuses is left for the reading itself to raise, in its own words.
303fn 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
326/// The one place a closed form becomes samples: `sample(...)` and the render root, and nothing else.
327fn 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    // FORMAT 9.3: the label belongs to the value, so an entry without one is no value.
370    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
403/// A closed form with a spectral sum takes the six rows over it; one without takes the point-sampled
404/// row over the written closed form itself, which is what FORMAT 9.1's no-dual row is.
405fn 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
421/// A spectral sum is rate-free, so one entry answers every rate the same closed form is read at.
422fn 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
432/// A warm entry is the same bytes the cold run would write, so a hit is returned as it
433/// stands rather than checked against a second collapse.
434fn 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
515/// One instance's resolved parameters, as the composer wrote them at the call site.
516fn 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}