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