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