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    Cache, CachePolicy, CacheStats, Expected, Lens, Payload, Recording, 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 keeps one value in the store, its last.
41    pub volatile: Vec<String>,
42    /// The store's own where `None`.
43    pub cache_policy: Option<CachePolicy>,
44}
45
46impl RenderConfig {
47    pub fn seconds(rate: u32, secs: f64) -> RenderConfig {
48        RenderConfig {
49            rate,
50            horizon: Horizon::secs(0.0, secs),
51            profile: PSYCHOACOUSTIC_V1,
52            asks: Vec::new(),
53            flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
54            volatile: Vec::new(),
55            cache_policy: None,
56        }
57    }
58
59    pub fn asking(mut self, asks: Vec<Ask>) -> RenderConfig {
60        self.asks = asks;
61        self
62    }
63}
64
65pub use answer::{answer, answer_buffer, ledger_over, sketch_atom};
66pub use silent::{Silent, render_until_silent};
67pub use stream::{Block, Checkpoint, STREAMED, Stream, StreamConfig};
68
69pub struct Render {
70    pub root: NodeId,
71    pub tys: Typing,
72    pub buffers: BTreeMap<NodeId, Buffer>,
73    pub frames: BTreeMap<NodeId, Frames>,
74    pub symbolic: BTreeMap<NodeId, SpectralSum>,
75    pub labels: BTreeMap<NodeId, Label>,
76    pub traces: Vec<FilterTrace>,
77    pub config: RenderConfig,
78    pub schedule: Schedule,
79    pub bindings: BTreeMap<NodeId, Vec<Binding>>,
80    pub cache_stats: Option<CacheStats>,
81    /// Silence proofs run over the whole grid.
82    pub proofs: u64,
83}
84
85impl Render {
86    pub fn work(&self) -> crate::flops::Work {
87        let samples = self.config.horizon.len(self.config.rate).unwrap_or(0);
88        crate::flops::Work {
89            samples: samples as u64,
90            proofs: self.proofs,
91            priced_flops: crate::flops::total(self),
92            waves: None,
93        }
94    }
95
96    pub fn buffer(&self, node: NodeId) -> Option<&Buffer> {
97        self.buffers.get(&node)
98    }
99
100    pub fn id(&self, path: &str) -> Option<NodeId> {
101        self.tys.id(path)
102    }
103
104    /// The node a reading names, refusing by name where a file expanded into several.
105    pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
106        self.tys.resolve(path)
107    }
108
109    /// The multiple a reading asks this node's score against, where one asks for a score at all.
110    pub fn alias_oversample(&self, node: NodeId) -> Option<u32> {
111        self.config
112            .asks
113            .iter()
114            .find_map(|ask| match ask.representation {
115                crate::query::Representation::Alias { oversample }
116                    if self.node(&ask.node).is_ok_and(|asked| asked == node) =>
117                {
118                    Some(oversample)
119                }
120                _ => None,
121            })
122    }
123
124    pub fn alias_score(&self, node: NodeId) -> AliasScore {
125        match self.alias_oversample(node) {
126            Some(_) => AliasScore::Asked,
127            None => AliasScore::NotAsked,
128        }
129    }
130}
131
132/// Nothing is materialized that no reading asked for: a closed form answered off its spectral sum
133/// allocates no buffer at all.
134pub fn render(
135    graph: &Graph,
136    target: &str,
137    config: RenderConfig,
138    cache: Option<&Cache>,
139) -> Result<Render, EngineError> {
140    let recording = cache.map(|c| Recording::over(c, config.cache_policy));
141    let mut held = run(prepared(graph, target)?, config, recording.as_ref(), None)?;
142    held.cache_stats = recording.map(Recording::finish);
143    Ok(held)
144}
145
146/// A composition resolved and typed under one target, before any horizon is chosen.
147pub(crate) struct Prepared<'g> {
148    pub(crate) instances: instantiate::Instances<'g>,
149    pub(crate) order: schedule::Order,
150    pub(crate) tys: Typing,
151    pub(crate) root: NodeId,
152    pub(crate) target: String,
153}
154
155impl Prepared<'_> {
156    /// The root's content address, and whether a reading asks it for an alias score.
157    pub(crate) fn identity(&self, asks: &[Ask]) -> Result<(sva_formula::Hash, bool), EngineError> {
158        let scored = asks.iter().any(|ask| {
159            matches!(
160                ask.representation,
161                crate::query::Representation::Alias { .. }
162            ) && self.tys.resolve(&ask.node).is_ok_and(|id| id == self.root)
163        });
164        Ok((refs::identity(&self.tys, self.root)?, scored))
165    }
166}
167
168pub(crate) fn prepared<'g>(graph: &'g Graph, target: &str) -> Result<Prepared<'g>, EngineError> {
169    let instances = instantiate::instantiate(graph, target)?;
170    let held = instances.instance_of(target)?;
171    let order = schedule::schedule_from(&instances, std::slice::from_ref(&held))?;
172    let tys = typing::infer_all(&instances, &order)?;
173    let root = tys
174        .id(&held)
175        .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
176    Ok(Prepared {
177        instances,
178        order,
179        tys,
180        root,
181        target: target.to_string(),
182    })
183}
184
185/// The root's value already held, where `known` carries it: only what another reading
186/// names is materialized beside it.
187pub(crate) fn run(
188    prepared: Prepared,
189    config: RenderConfig,
190    recording: Option<&Recording>,
191    known: Option<(Buffer, Label)>,
192) -> Result<Render, EngineError> {
193    let Prepared {
194        instances,
195        order,
196        tys,
197        root,
198        target,
199    } = prepared;
200    let mut schedule = schedule::plan(&tys, &order, root, &config.asks);
201    let forks = schedule::forks(&tys, &schedule.materialize);
202    if known.is_some() {
203        match only_the_root(&tys, root, &config.asks) {
204            true => schedule.materialize.clear(),
205            false => schedule.materialize.retain(|id| *id != root),
206        }
207    }
208
209    let bindings = tys
210        .paths()
211        .filter_map(|(path, id)| Some((id, resolved(&instances, path)?)))
212        .collect();
213    let mut held = Render {
214        root,
215        tys,
216        buffers: BTreeMap::new(),
217        frames: BTreeMap::new(),
218        symbolic: BTreeMap::new(),
219        labels: BTreeMap::new(),
220        traces: Vec::new(),
221        config,
222        schedule,
223        bindings,
224        cache_stats: None,
225        proofs: 0,
226    };
227    if let Some((buffer, label)) = known {
228        held.buffers.insert(root, buffer);
229        held.labels.insert(root, label);
230    }
231    let lenses = Lenses {
232        recording,
233        volatile: volatile::mark(&instances, &held.tys, &held.config, &target)?,
234        forks,
235        root,
236    };
237    affordable(&held)?;
238    let needed = lenses.needed(&held);
239    for id in held.schedule.materialize.clone() {
240        if needed.contains(&id) {
241            materialize(&mut held, id, &lenses)?;
242        }
243    }
244    compose_read(&mut held);
245    stamp(&mut held);
246    Ok(held)
247}
248
249/// Whether every reading is one of the root's own samples, so a held root answers them all.
250fn only_the_root(tys: &Typing, root: NodeId, asks: &[Ask]) -> bool {
251    asks.iter().all(|ask| {
252        tys.resolve(&ask.node).is_ok_and(|id| id == root)
253            && !matches!(
254                ask.representation,
255                crate::query::Representation::Ledger { .. }
256            )
257    })
258}
259
260/// A reading that materializes nothing is never refused for cost: asking what a render
261/// would cost is not paying for it. Anything that runs is counted first, and a count past
262/// the budget refuses before a sample is computed.
263fn affordable(held: &Render) -> Result<(), EngineError> {
264    if held.schedule.materialize.is_empty() {
265        return Ok(());
266    }
267    let total = crate::flops::total(held);
268    if total <= held.config.flop_budget {
269        return Ok(());
270    }
271    let counted = crate::flops::tree(held);
272    let over = crate::flops::dominating(&counted).expect("a counted tree holds its root");
273    Err(EngineError::refused(Diagnostic {
274        code: "collapse.over_budget".to_string(),
275        message: format!(
276            "this render counts {} operations, over the budget of {}; `{}` dominates it at \
277             {} by {}",
278            counted.total, counted.budget, over.node, over.subtree, over.route
279        ),
280        location: Located::at(held.tys.name(held.root), None),
281        help: format!("pass --flop-budget {} to render it anyway", counted.total),
282    }))
283}
284
285/// FORMAT 9.3: the render's own label says what it cost and what it was allowed.
286fn stamp(held: &mut Render) {
287    let root = held.root;
288    let Some(label) = held.labels.remove(&root) else {
289        return;
290    };
291    let counted = crate::flops::total(held);
292    held.labels
293        .insert(root, label.costing(counted, held.config.flop_budget));
294}
295
296/// One render, every reading: a closed form a reading asks for is composed once here, and each
297/// `--as` answers off that one form rather than walking the graph again. A form that
298/// refuses is left for the reading itself to raise, in its own words.
299fn compose_read(held: &mut Render) {
300    for id in held.schedule.compose.clone() {
301        if held.symbolic.contains_key(&id) {
302            continue;
303        }
304        if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
305            held.symbolic.insert(id, sum);
306        }
307    }
308}
309
310pub(crate) struct Lenses<'r> {
311    recording: Option<&'r Recording<'r>>,
312    volatile: volatile::Volatile,
313    forks: std::collections::BTreeSet<NodeId>,
314    root: NodeId,
315}
316
317impl Lenses<'_> {
318    pub(crate) fn none() -> Lenses<'static> {
319        Lenses {
320            recording: None,
321            volatile: volatile::Volatile::default(),
322            forks: Default::default(),
323            root: NodeId(0),
324        }
325    }
326
327    fn at(&self, id: NodeId) -> Option<Lens<'_>> {
328        let fork = self.forks.contains(&id);
329        self.recording
330            .map(|r| r.at(self.volatile.slot(id), fork, id == self.root))
331    }
332
333    /// What a reading asks for, what the policy keeps, and what those read that the store
334    /// does not answer. A ledger holds them all.
335    fn needed(&self, held: &Render) -> std::collections::BTreeSet<NodeId> {
336        let materialize = &held.schedule.materialize;
337        let ledger = held.config.asks.iter().any(|ask| {
338            matches!(
339                ask.representation,
340                crate::query::Representation::Ledger { .. }
341            )
342        });
343        let kept = |id: &NodeId| {
344            ledger
345                || self
346                    .recording
347                    .is_none_or(|r| r.stores(self.forks.contains(id), *id == self.root))
348        };
349        let mut needed: std::collections::BTreeSet<NodeId> = materialize
350            .iter()
351            .copied()
352            .filter(|id| held.schedule.wanted.contains(id) || kept(id))
353            .collect();
354        for id in materialize.iter().rev() {
355            if !needed.contains(id) || self.answered(held, *id) {
356                continue;
357            }
358            needed.extend(reads(held, *id));
359        }
360        needed
361    }
362
363    fn answered(&self, held: &Render, id: NodeId) -> bool {
364        let (Some(recording), Held::Sampled) = (self.recording, held.tys.ty(id).held) else {
365            return false;
366        };
367        sampled::key(held, id).is_ok_and(|(key, _)| recording.holds(key))
368    }
369}
370
371/// What the schedule orders before `id`, and every buffer its program reads behind those.
372fn reads(held: &Render, id: NodeId) -> Vec<NodeId> {
373    let mut out = schedule::materialized_operands(&held.tys, id);
374    if matches!(held.tys.ty(id).held, Held::Sampled)
375        && let Ok(program) = sampled::program(held, id)
376    {
377        out.extend(program.reads);
378    }
379    out.retain(|read| *read != id);
380    out
381}
382
383/// A sampled node found in the store answers for everything under it, so what it reads is
384/// held here only where it missed.
385fn materialize(held: &mut Render, id: NodeId, lenses: &Lenses) -> Result<(), EngineError> {
386    if held.buffers.contains_key(&id) || held.frames.contains_key(&id) {
387        return Ok(());
388    }
389    let lens = lenses.at(id);
390    if matches!(held.tys.ty(id).held, Held::Sampled) {
391        let (key, samples) = sampled::key(held, id)?;
392        if let Some((hit, label)) = warm(held, id, key, samples, lens.as_ref()) {
393            held.buffers.insert(id, hit);
394            held.labels.insert(id, label);
395            return Ok(());
396        }
397        for read in reads(held, id) {
398            materialize(held, read, lenses)?;
399        }
400        return sampled::run(held, id, key, lens.as_ref());
401    }
402    for operand in schedule::materialized_operands(&held.tys, id) {
403        materialize(held, operand, lenses)?;
404    }
405    match held.tys.ty(id).held {
406        Held::Frames => frames_of(held, id, lens.as_ref()),
407        _ => collapse_closed_form(held, id, lens.as_ref()),
408    }
409}
410
411/// The one place a closed form becomes samples: `sample(...)` and the render root, and nothing else.
412fn collapse_closed_form(
413    held: &mut Render,
414    id: NodeId,
415    cache: Option<&Lens>,
416) -> Result<(), EngineError> {
417    let var = held.tys.var(id);
418    let written = match refs::resolve(&held.tys, id, 0, held.tys.ty(id).held) {
419        Ok(refs::Read::Substitute(form)) => Some(*form),
420        _ => None,
421    };
422    let symbolic = written.as_ref().map(|t| symbolic_key(hash_closed_form(t)));
423    let sum = remembered(held, id, symbolic, cache)
424        .map(Ok)
425        .unwrap_or_else(|| {
426            let found = refs::spectral_sum_of(&held.tys, id, var);
427            if let (Ok(sum), Some(key), Some(cache)) = (&found, symbolic, cache) {
428                cache.store(key, &Payload::Symbolic(Box::new(sum.clone())), None);
429            }
430            found
431        });
432    let identity = match refs::closed_form_identity(&sum, written.as_ref()) {
433        Ok(identity) => identity,
434        Err(_) if var == sva_formula::Var::T => refs::identity(&held.tys, id)?,
435        Err(e) => return Err(e),
436    };
437    let score = held.alias_score(id);
438    let samples = length(held, id)?;
439    let key = crate::cache::buffer_key(
440        identity,
441        held.config.rate,
442        held.config.horizon.start_secs,
443        samples,
444        held.tys.ty(id).width as usize,
445        score,
446    );
447    if let Ok(sum) = &sum {
448        held.symbolic.insert(id, sum.clone());
449    }
450    // FORMAT 9.3: the label belongs to the value, so an entry without one is no value.
451    if let Some((hit, label)) = warm(held, id, key, samples, cache) {
452        held.buffers.insert(id, hit);
453        held.labels.insert(id, label);
454        return Ok(());
455    }
456    let (buffer, label) = match (&sum, &written) {
457        (Err(_), None) => pointwise::point_sample(held, id, score)?,
458        _ => sampled_form(held, &sum, written.as_ref(), score)
459            .map_err(|e| collapse_refused(held, id, &e))?,
460    };
461    store(key, &buffer, &label, cache);
462    held.buffers.insert(id, buffer);
463    held.labels.insert(id, label);
464    Ok(())
465}
466
467fn collapse_refused(held: &Render, id: NodeId, e: &sva_samples::CollapseError) -> EngineError {
468    EngineError::refused(Diagnostic {
469        code: e.code().to_string(),
470        message: e.to_string(),
471        location: Located::at(held.tys.name(id), None),
472        help: e.help().to_string(),
473    })
474}
475
476fn length(held: &Render, id: NodeId) -> Result<usize, EngineError> {
477    held.config
478        .horizon
479        .len(held.config.rate)
480        .map_err(|e| collapse_refused(held, id, &e))
481}
482
483/// A closed form with a spectral sum takes the six rows over it; one without takes the point-sampled
484/// row over the written closed form itself, which is what FORMAT 9.1's no-dual row is.
485fn sampled_form(
486    held: &Render,
487    sum: &Result<SpectralSum, EngineError>,
488    written: Option<&sva_formula::ClosedForm>,
489    score: AliasScore,
490) -> Result<(Buffer, Label), sva_samples::CollapseError> {
491    let (rate, horizon, profile) = (held.config.rate, held.config.horizon, &held.config.profile);
492    match (sum, written) {
493        (Ok(sum), written) => {
494            sva_samples::of_spectral_sum_or_point(sum, written, rate, horizon, profile, score)
495        }
496        (Err(_), Some(form)) => sva_samples::render(form, rate, horizon, profile, score),
497        (Err(_), None) => unreachable!("a closed form with neither view is point-sampled above"),
498    }
499}
500
501/// A spectral sum is rate-free, so one entry answers every rate the same closed form is read at.
502fn remembered(
503    held: &Render,
504    id: NodeId,
505    key: Option<sva_formula::Hash>,
506    cache: Option<&Lens>,
507) -> Option<SpectralSum> {
508    let entry = cache?.load(key?, held.tys.name(id), Expected::Symbolic)?;
509    entry.payload.symbolic().cloned()
510}
511
512/// A warm entry is the same bytes the cold run would write, so a hit is returned as it
513/// stands rather than checked against a second collapse.
514fn warm(
515    held: &Render,
516    id: NodeId,
517    key: sva_formula::Hash,
518    samples: usize,
519    cache: Option<&Lens>,
520) -> Option<(Buffer, Label)> {
521    let expected = Expected::Samples {
522        rate: held.config.rate,
523        width: held.tys.ty(id).width as usize,
524        samples,
525    };
526    let entry = cache?.load(key, held.tys.name(id), expected)?;
527    Some((entry.payload.samples().cloned()?, entry.label?))
528}
529
530fn store(key: sva_formula::Hash, buffer: &Buffer, label: &Label, cache: Option<&Lens>) {
531    if let Some(cache) = cache {
532        cache.store(
533            key,
534            &Payload::Samples(Box::new(buffer.clone())),
535            Some(label),
536        );
537    }
538}
539
540fn frames_of(held: &mut Render, id: NodeId, cache: Option<&Lens>) -> Result<(), EngineError> {
541    let Value::Cast(Cast::Stft { window, hop }, source) = *held.tys.value(id) else {
542        return Err(not_frames(held, id));
543    };
544    let buffer = held
545        .buffers
546        .get(&source)
547        .ok_or_else(|| not_frames(held, id))?;
548    let key = frames_key(
549        crate::cache::buffer_key(
550            refs::identity(&held.tys, source)?,
551            buffer.rate,
552            buffer.origin_secs,
553            buffer.len(),
554            buffer.width,
555            AliasScore::NotAsked,
556        ),
557        window,
558        hop,
559    );
560    if let Some(entry) = cache.and_then(|c| c.load(key, held.tys.name(id), Expected::Frames))
561        && let Payload::Frames(frames) = entry.payload
562    {
563        held.frames.insert(id, *frames);
564        return Ok(());
565    }
566    let frames = stft::forward(buffer, window, hop).map_err(|e| sampled::refused(held, id, &e))?;
567    if let Some(cache) = cache {
568        cache.store(key, &Payload::Frames(Box::new(frames.clone())), None);
569    }
570    held.frames.insert(id, frames);
571    Ok(())
572}
573
574fn not_frames(held: &Render, id: NodeId) -> EngineError {
575    EngineError::refused(Diagnostic {
576        code: "cast.stft_needs_samples".to_string(),
577        message: format!("`{}` holds no frames to read", held.tys.name(id)),
578        location: Located::at(held.tys.name(id), None),
579        help: "write stft(sample(x), window=, hop=)".to_string(),
580    })
581}
582
583fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
584    Some(
585        instances
586            .bindings(path)?
587            .into_iter()
588            .map(|(name, expr, cx)| Binding {
589                name: name.to_string(),
590                source: instances.render(expr, cx),
591            })
592            .collect(),
593    )
594}