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: computing a value (table/), what a reading asks (schedule.rs) | IO: (&Graph, target) -> Render
2
3mod answer;
4pub(crate) mod bound;
5mod drive;
6mod frontier;
7mod quiet;
8mod slots;
9mod stream;
10pub(crate) mod table;
11mod terms;
12mod through;
13pub mod until;
14mod volatile;
15
16use std::collections::{BTreeMap, BTreeSet};
17
18use sva_ast::Graph;
19use sva_formula::{NodeId, SpectralSum};
20use sva_samples::{
21    AliasScore, Buffer, Extent, FilterTrace, Frames, Label, PSYCHOACOUSTIC_V1, Profile,
22};
23
24use crate::bindings::Binding;
25use crate::cache::{Cache, CachePolicy, CacheStats, Recording};
26use crate::error::{Diagnostic, EngineError, Located};
27use crate::instantiate;
28use crate::query::Ask;
29use crate::refs;
30use crate::schedule::{self, Schedule};
31use crate::typing::{self, Typing};
32use table::Table;
33use table::support::Supports;
34
35/// Where a target is read, in samples from t = 0. Unstated, the root's own support.
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct Range {
38    pub start: Option<i64>,
39    pub end: Option<i64>,
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct RenderConfig {
44    pub rate: u32,
45    pub range: Range,
46    /// Where the render stops before the range ends.
47    pub until: Option<Until>,
48    pub profile: Profile,
49    /// What the caller means to read. An empty list is audio out, which collapses the root.
50    pub asks: Vec<Ask>,
51    /// The operation count this render may pay.
52    pub flop_budget: u128,
53    /// What reads one of these keeps one value in the store, its last.
54    pub volatile: Vec<String>,
55    pub cache_policy: Option<CachePolicy>,
56}
57
58impl RenderConfig {
59    pub fn at(rate: u32) -> RenderConfig {
60        RenderConfig {
61            rate,
62            range: Range::default(),
63            until: None,
64            profile: PSYCHOACOUSTIC_V1,
65            asks: Vec::new(),
66            flop_budget: PSYCHOACOUSTIC_V1.flop_budget,
67            volatile: Vec::new(),
68            cache_policy: None,
69        }
70    }
71
72    pub fn seconds(rate: u32, secs: f64) -> RenderConfig {
73        let end = (secs * f64::from(rate)).round() as i64;
74        RenderConfig {
75            range: Range {
76                start: Some(0),
77                end: Some(end),
78            },
79            ..RenderConfig::at(rate)
80        }
81    }
82
83    pub fn asking(mut self, asks: Vec<Ask>) -> RenderConfig {
84        self.asks = asks;
85        self
86    }
87}
88
89pub use answer::{answer, answer_buffer, sketch_atom};
90pub use drive::Block;
91pub use quiet::{QUIET_AFTER_SECS, QUIET_LEVEL, QuietTail, quiet_tails};
92pub use stream::{STREAMED, Stream, StreamConfig};
93pub use terms::{Handle, NOTES};
94pub use through::{render_through, warm};
95pub use until::Until;
96
97/// Samples a whole render pulls at once; any size writes the same bits.
98const BLOCK: usize = 1 << 12;
99
100pub struct Render {
101    pub root: NodeId,
102    pub tys: Typing,
103    pub buffers: BTreeMap<NodeId, Buffer>,
104    pub frames: BTreeMap<NodeId, Frames>,
105    pub symbolic: BTreeMap<NodeId, SpectralSum>,
106    pub labels: BTreeMap<NodeId, Label>,
107    pub traces: Vec<FilterTrace>,
108    pub config: RenderConfig,
109    pub schedule: Schedule,
110    pub bindings: BTreeMap<NodeId, Vec<Binding>>,
111    pub cache_stats: Option<CacheStats>,
112    /// The most bytes the table held at once, samples and state.
113    pub held_bytes: usize,
114    /// The samples the root was read over; `None` where no reading needed any.
115    pub range: Option<Extent>,
116    pub(crate) unranged: Option<EngineError>,
117    /// Every value the render read, each over the segments it computed.
118    pub(crate) table: Option<Table>,
119}
120
121impl Render {
122    pub(crate) fn shell(
123        tys: Typing,
124        root: NodeId,
125        config: RenderConfig,
126        schedule: Schedule,
127    ) -> Self {
128        Render {
129            root,
130            tys,
131            buffers: BTreeMap::new(),
132            frames: BTreeMap::new(),
133            symbolic: BTreeMap::new(),
134            labels: BTreeMap::new(),
135            traces: Vec::new(),
136            config,
137            schedule,
138            bindings: BTreeMap::new(),
139            cache_stats: None,
140            held_bytes: 0,
141            range: None,
142            unranged: None,
143            table: None,
144        }
145    }
146
147    pub(crate) fn rate(&self) -> u32 {
148        self.config.rate
149    }
150
151    pub(crate) fn grid(&self, id: NodeId) -> sva_samples::Grid {
152        self.tys.grid(id)
153    }
154
155    /// What its schedule prices: every value once over the range, whatever the store answered.
156    pub fn work(&self) -> crate::flops::Work {
157        crate::flops::Work {
158            samples: self.range.map_or(0, |range| range.len() as u64),
159            priced_flops: crate::flops::total(self),
160            waves: None,
161        }
162    }
163
164    /// A node's samples over the range, zero where it holds none.
165    pub fn output(&self, node: NodeId) -> Result<Buffer, EngineError> {
166        match (self.buffers.get(&node), self.range) {
167            (Some(held), Some(range)) => Ok(held.over(range, held.extent())),
168            _ => Err(answer::unheld(self, node)),
169        }
170    }
171
172    pub fn buffer(&self, node: NodeId) -> Option<&Buffer> {
173        self.buffers.get(&node)
174    }
175
176    pub fn id(&self, path: &str) -> Option<NodeId> {
177        self.tys.id(path)
178    }
179
180    /// The node a reading names, refusing by name where a file expanded into several.
181    pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
182        self.tys.resolve(path)
183    }
184
185    /// Each segment the render computed of `node`'s value, in order: a sample in two was
186    /// computed twice.
187    pub fn evaluated(&self, node: NodeId) -> Vec<Extent> {
188        let Some(table) = &self.table else {
189            return Vec::new();
190        };
191        table
192            .of(node)
193            .map_or(Vec::new(), |at| table.values[at].evaluated.clone())
194    }
195
196    /// The multiple a reading asks this node's score against, where one asks for a score at all.
197    pub fn alias_oversample(&self, node: NodeId) -> Option<u32> {
198        self.config
199            .asks
200            .iter()
201            .find_map(|ask| match ask.representation {
202                crate::query::Representation::Alias { oversample }
203                    if self.node(&ask.node).is_ok_and(|asked| asked == node) =>
204                {
205                    Some(oversample)
206                }
207                _ => None,
208            })
209    }
210
211    pub fn alias_score(&self, node: NodeId) -> AliasScore {
212        match self.alias_oversample(node) {
213            Some(_) => AliasScore::Asked,
214            None => AliasScore::NotAsked,
215        }
216    }
217}
218
219/// Nothing is materialized that no reading asked for.
220pub fn render(
221    graph: &Graph,
222    target: &str,
223    config: RenderConfig,
224    cache: Option<&Cache>,
225) -> Result<Render, EngineError> {
226    let recording = Recording::over(cache, config.cache_policy);
227    let mut held = planned(
228        prepared(graph, target, config.rate)?,
229        config,
230        &BTreeSet::new(),
231    )?;
232    pulled(&mut held, recording)?;
233    closed(&mut held)?;
234    Ok(held)
235}
236
237fn closed(held: &mut Render) -> Result<(), EngineError> {
238    scored(held)?;
239    compose_read(held);
240    stamp(held);
241    Ok(())
242}
243
244pub(crate) struct Prepared<'g> {
245    pub(crate) instances: instantiate::Instances<'g>,
246    pub(crate) tys: Typing,
247    pub(crate) root: NodeId,
248}
249
250pub(crate) fn prepared<'g>(
251    graph: &'g Graph,
252    target: &str,
253    rate: u32,
254) -> Result<Prepared<'g>, EngineError> {
255    let instances = instantiate::instantiate(graph, target, rate)?;
256    let held = instances.instance_of(target)?;
257    let order = schedule::schedule_from(&instances, std::slice::from_ref(&held))?;
258    let tys = typing::infer_all(&instances, &order)?;
259    let root = tys
260        .id(&held)
261        .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
262    Ok(Prepared {
263        instances,
264        tys,
265        root,
266    })
267}
268
269fn planned(
270    prepared: Prepared<'_>,
271    config: RenderConfig,
272    bounds: &BTreeSet<NodeId>,
273) -> Result<Render, EngineError> {
274    let Prepared {
275        instances,
276        tys,
277        root,
278    } = prepared;
279    planned_over(&instances, (tys, root), config, bounds)
280}
281
282fn planned_over(
283    instances: &instantiate::Instances,
284    (tys, root): (Typing, NodeId),
285    config: RenderConfig,
286    bounds: &BTreeSet<NodeId>,
287) -> Result<Render, EngineError> {
288    let schedule = schedule::plan(&tys, root, &config.asks);
289    let bindings = tys
290        .paths()
291        .filter_map(|(path, id)| Some((id, resolved(instances, path)?)))
292        .collect();
293    let mut held = Render::shell(tys, root, config, schedule);
294    held.bindings = bindings;
295    ranged(&mut held, bounds)?;
296    let target = held.tys.name(held.root).to_string();
297    let volatile = volatile::mark(instances, &held, &target)?;
298    if let Some(table) = &mut held.table {
299        table.slots(|id| volatile.slot(id));
300    }
301    Ok(held)
302}
303
304/// The range and every value a render of `target` would compute, and no sample.
305pub fn plan(graph: &Graph, target: &str, config: RenderConfig) -> Result<Render, EngineError> {
306    planned(
307        prepared(graph, target, config.rate)?,
308        config,
309        &BTreeSet::new(),
310    )
311}
312
313/// A reading of samples or of their cost needs the range; lines and structure never do.
314fn ranged(held: &mut Render, bounds: &BTreeSet<NodeId>) -> Result<(), EngineError> {
315    let counts = counts(&held.config.asks);
316    let envelope = held.config.asks.iter().any(|ask| {
317        matches!(
318            ask.representation,
319            crate::query::Representation::Envelope { .. }
320        )
321    });
322    if !materializes(held) && !counts {
323        if envelope && let Err(refused) = range_of(held, Ends::Refused) {
324            held.unranged = Some(refused);
325        } else if envelope {
326            held.range = Some(range_of(held, Ends::Refused)?);
327        }
328        return Ok(());
329    }
330    held.range = Some(range_of(held, Ends::Refused)?);
331    let wanted: Vec<NodeId> = held.schedule.wanted.clone();
332    let root = (held.root, wanted.as_slice());
333    let mut table = Table::bounded(&held.tys, root, &held.config.profile, bounds)?;
334    table.plan(held.range.expect("a range was decided"));
335    held.table = Some(table);
336    Ok(())
337}
338
339fn materializes(held: &Render) -> bool {
340    !held.schedule.wanted.is_empty()
341}
342
343pub(crate) enum Ends {
344    Refused,
345    Pulled,
346}
347
348pub(crate) fn range_of(held: &Render, ends: Ends) -> Result<Extent, EngineError> {
349    let support = Supports::new(&held.tys).of(held.root);
350    let start = held
351        .config
352        .range
353        .start
354        .unwrap_or_else(|| default_start(support));
355    let end = match held.config.range.end.or(default_end(support)) {
356        Some(end) => end,
357        None => match ends {
358            Ends::Pulled => i64::MAX,
359            Ends::Refused => return Err(endless(held)),
360        },
361    };
362    Ok(Extent::new(start, end.max(start)))
363}
364
365fn default_start(support: Extent) -> i64 {
366    match support.is_empty() || support.start == i64::MIN || support.start > 0 {
367        true => 0,
368        false => support.start,
369    }
370}
371
372fn default_end(support: Extent) -> Option<i64> {
373    (support.end != i64::MAX).then_some(support.end)
374}
375
376fn endless(held: &Render) -> EngineError {
377    let name = held.tys.name(held.root);
378    EngineError::refused(Diagnostic {
379        code: "render.no_end".to_string(),
380        message: format!(
381            "`{name}` is read over an interval with no end, and its support never ends"
382        ),
383        location: Located::at(name, None),
384        help: "give the interval an end, as `[0, 2s]`, or crop it".to_string(),
385    })
386}
387
388/// A `flops` reading counts what the audio render would run.
389fn counts(asks: &[Ask]) -> bool {
390    asks.iter()
391        .any(|ask| ask.representation == crate::query::Representation::Flops)
392}
393
394/// A reading that materializes nothing is never refused for cost; a count past the budget
395/// refuses before a sample is computed.
396fn affordable(held: &Render) -> Result<(), EngineError> {
397    if !materializes(held) {
398        return Ok(());
399    }
400    let total = crate::flops::total(held);
401    if total <= held.config.flop_budget {
402        return Ok(());
403    }
404    let counted = crate::flops::tree(held);
405    let over = crate::flops::dominating(&counted).expect("a counted tree holds its root");
406    Err(EngineError::refused(Diagnostic {
407        code: "collapse.over_budget".to_string(),
408        message: format!(
409            "this render counts {} operations, over the budget of {}; `{}` dominates it at {} \
410             by {}",
411            counted.total, counted.budget, over.node, over.subtree, over.route
412        ),
413        location: Located::at(held.tys.name(held.root), None),
414        help: format!("pass --flop-budget {total} to render it anyway"),
415    }))
416}
417
418/// Every wanted value pulled over the range, block by block, until `until` stops it.
419fn pulled(held: &mut Render, recording: Recording) -> Result<(), EngineError> {
420    if let Some(mut driver) = driving(held, recording)? {
421        while driver.pull()? {}
422        drove(held, driver, true);
423    }
424    Ok(())
425}
426
427/// What pulls a render's table, where it materializes one at all.
428fn driving(held: &mut Render, recording: Recording) -> Result<Option<drive::Driver>, EngineError> {
429    affordable(held)?;
430    let (Some(table), Some(range)) = (held.table.take(), held.range) else {
431        return Ok(None);
432    };
433    if !materializes(held) {
434        held.table = Some(table);
435        return Ok(None);
436    }
437    Ok(Some(drive::Driver::new(
438        table,
439        range,
440        BLOCK,
441        &held.config,
442        recording,
443    )))
444}
445
446/// `keep`: each wanted value's samples copied out of the table.
447fn drove(held: &mut Render, driver: drive::Driver, keep: bool) {
448    let range = held.range.expect("a pulled render has a range");
449    held.held_bytes = driver.most_bytes();
450    held.cache_stats = Some(driver.recording.stats());
451    if let Some(stop) = driver.stop().filter(|stop| *stop < range.end) {
452        held.range = Some(Extent::new(range.start, stop));
453    }
454    let range = held.range.expect("a pulled render has a range");
455    let table = driver.table;
456    let wanted = held.schedule.wanted.iter().filter(|_| keep);
457    for (id, at) in wanted.map(|id| (*id, table.of(*id))) {
458        let Some(at) = at else {
459            continue;
460        };
461        match &table.values[at].held {
462            table::Held::Frames(Some(frames)) => {
463                held.frames.insert(id, (**frames).clone());
464            }
465            _ => {
466                held.buffers.insert(id, table.samples(at, range));
467                held.labels.insert(id, table.label(at));
468            }
469        }
470    }
471    held.table = Some(table);
472}
473
474/// A closed form's value `fine` times finer than the render's step: what an alias score
475/// reads against.
476pub(crate) fn finer(
477    render: &Render,
478    node: NodeId,
479    fine: u32,
480    over: Extent,
481) -> Result<Buffer, EngineError> {
482    let profile = &render.config.profile;
483    let mut table = Table::finer(&render.tys, node, &[node], profile, i128::from(fine))?;
484    let at = table.root;
485    table.pull(over, &mut Recording::over(None, None))?;
486    let mut held = table.samples(at, over);
487    held.rate = render.config.rate * fine;
488    Ok(held)
489}
490
491/// A point sampling a reading asks the score of says in its label what the grid lost.
492fn scored(held: &mut Render) -> Result<(), EngineError> {
493    let asked: Vec<NodeId> = held
494        .labels
495        .keys()
496        .copied()
497        .filter(|id| held.alias_oversample(*id).is_some())
498        .collect();
499    for id in asked {
500        let sva_samples::Detail::Point {
501            rule,
502            alias_db: None,
503        } = held.labels[&id].detail
504        else {
505            continue;
506        };
507        let buffer = held.output(id)?;
508        let alias_db = Some(answer::alias_db(held, id, &buffer)?);
509        let label = held.labels.get_mut(&id).expect("an asked label");
510        label.detail = sva_samples::Detail::Point { rule, alias_db };
511    }
512    Ok(())
513}
514
515/// `render` with every read its own value, as if each were written out where it is read.
516#[cfg(test)]
517pub(crate) fn render_apart(
518    graph: &Graph,
519    target: &str,
520    config: RenderConfig,
521) -> Result<Render, EngineError> {
522    let mut held = planned(
523        prepared(graph, target, config.rate)?,
524        config,
525        &BTreeSet::new(),
526    )?;
527    if let (Some(range), Some(_)) = (held.range, &held.table) {
528        let wanted = held.schedule.wanted.clone();
529        let mut table = Table::apart(&held.tys, held.root, &wanted, &held.config.profile)?;
530        table.plan(range);
531        held.table = Some(table);
532    }
533    pulled(&mut held, Recording::over(None, None))?;
534    Ok(held)
535}
536
537/// A node's value pulled over `over` in a table of its own.
538pub(crate) fn sampled(render: &Render, node: NodeId, over: Extent) -> Result<Buffer, EngineError> {
539    let mut table = Table::build(&render.tys, node, &[node], &render.config.profile)?;
540    let at = table.root;
541    table.pull(over, &mut Recording::over(None, None))?;
542    Ok(table.samples(at, over))
543}
544
545/// FORMAT 9.3: the render's own label says what it cost and what it was allowed.
546fn stamp(held: &mut Render) {
547    let root = held.root;
548    let Some(label) = held.labels.remove(&root) else {
549        return;
550    };
551    let counted = crate::flops::total(held);
552    let label = sva_samples::Label {
553        rate: held.config.rate,
554        moved: held.table.as_ref().map(|table| table.moved),
555        ..label.costing(counted, held.config.flop_budget)
556    };
557    held.labels.insert(root, label);
558}
559
560/// One render, every reading: a closed form a reading asks for is composed once here; one
561/// that refuses is left for the reading to raise.
562fn compose_read(held: &mut Render) {
563    for id in held.schedule.compose.clone() {
564        if held.symbolic.contains_key(&id) {
565            continue;
566        }
567        if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
568            held.symbolic.insert(id, sum);
569        }
570    }
571}
572
573fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
574    Some(
575        instances
576            .bindings(path)?
577            .into_iter()
578            .map(|(name, expr, cx)| Binding {
579                name: name.to_string(),
580                source: instances.render(expr, cx),
581            })
582            .collect(),
583    )
584}