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 (value_graph/), what a reading asks | IO: (&Graph, target) -> Render
2
3mod answer;
4mod drive;
5mod end;
6mod run;
7mod slots;
8mod stream;
9mod terms;
10pub mod until;
11pub(crate) mod value_graph;
12mod volatile;
13mod world;
14
15use std::collections::BTreeMap;
16use std::num::NonZeroUsize;
17use std::sync::Arc;
18
19use sva_ast::Graph;
20use sva_formula::{NodeId, SpectralSum};
21use sva_samples::{Buffer, Extent, FilterTrace, Frames, Label, PSYCHOACOUSTIC_V1, Profile};
22
23use crate::bindings::Binding;
24use crate::cache::{CacheStats, Memory, Recording, Stored, Tier, now};
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 end::Ending;
32use value_graph::ValueGraph;
33use value_graph::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    /// What reads one of these keeps one value in memory, its last.
52    pub volatile: Vec<String>,
53    pub out: Out,
54    /// The most threads it computes on; one computes every value in turn on the caller's.
55    pub threads: NonZeroUsize,
56}
57
58/// Whether a render hands back its root's samples; dropped, with no reading asked, it holds
59/// none past each block.
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
61pub enum Out {
62    #[default]
63    Kept,
64    Dropped,
65}
66
67impl RenderConfig {
68    pub fn at(rate: u32) -> RenderConfig {
69        RenderConfig {
70            rate,
71            range: Range::default(),
72            until: None,
73            profile: PSYCHOACOUSTIC_V1,
74            asks: Vec::new(),
75            volatile: Vec::new(),
76            out: Out::Kept,
77            threads: crate::threads::default_threads(),
78        }
79    }
80
81    pub fn seconds(rate: u32, secs: f64) -> RenderConfig {
82        let end = (secs * f64::from(rate)).round() as i64;
83        RenderConfig {
84            range: Range {
85                start: Some(0),
86                end: Some(end),
87            },
88            ..RenderConfig::at(rate)
89        }
90    }
91
92    pub fn asking(mut self, asks: Vec<Ask>) -> RenderConfig {
93        self.asks = asks;
94        self
95    }
96}
97
98pub use answer::{answer, answer_buffer, sketch_atom};
99pub use drive::Work;
100pub use run::{Abandon, Never, Session, render_in, render_over};
101pub use stream::{
102    Built, Change, Changed, Counts, LATEST, Placed, Stream, StreamConfig, change, fetch,
103};
104pub use terms::{Handle, NOTES};
105pub use until::Until;
106pub use world::STREAMED;
107
108pub struct Render {
109    pub root: NodeId,
110    pub tys: Typing,
111    pub buffers: BTreeMap<NodeId, Buffer>,
112    pub frames: BTreeMap<NodeId, Frames>,
113    pub symbolic: BTreeMap<NodeId, SpectralSum>,
114    pub labels: BTreeMap<NodeId, Label>,
115    pub traces: Vec<FilterTrace>,
116    pub config: RenderConfig,
117    pub schedule: Schedule,
118    pub bindings: BTreeMap<NodeId, Vec<Binding>>,
119    pub cache_stats: Option<CacheStats>,
120    /// The most bytes the value graph held at once, samples and state.
121    pub held_bytes: usize,
122    /// The samples the root was read over; `None` where no reading needed any.
123    pub range: Option<Extent>,
124    pub(crate) unranged: Option<EngineError>,
125    /// Every value the render read, each over the segments it computed.
126    pub(crate) value_graph: Option<ValueGraph>,
127    /// The samples its pulls computed.
128    computed_samples: u64,
129    pub(crate) unslotted: Option<String>,
130    /// Each node typed with every volatile parameter at its stand-in.
131    pub(crate) stand_in_typed: Vec<String>,
132    pub(crate) treated_as_silent_from_sample: Option<i64>,
133    /// What its readings ask past the samples it holds.
134    pub(crate) memory: Memory,
135}
136
137impl Render {
138    pub(crate) fn shell(
139        (tys, root): (Typing, NodeId),
140        (config, schedule): (RenderConfig, Schedule),
141        memory: Memory,
142    ) -> Self {
143        Render {
144            root,
145            tys,
146            buffers: BTreeMap::new(),
147            frames: BTreeMap::new(),
148            symbolic: BTreeMap::new(),
149            labels: BTreeMap::new(),
150            traces: Vec::new(),
151            config,
152            schedule,
153            bindings: BTreeMap::new(),
154            cache_stats: None,
155            held_bytes: 0,
156            range: None,
157            unranged: None,
158            value_graph: None,
159            computed_samples: 0,
160            unslotted: None,
161            stand_in_typed: Vec::new(),
162            treated_as_silent_from_sample: None,
163            memory,
164        }
165    }
166
167    /// What it computed: nothing memory answered counts.
168    pub fn work(&self) -> Work {
169        Work {
170            samples: self.range.map_or(0, |range| range.len() as u64),
171            computed_samples: self.computed_samples,
172        }
173    }
174
175    /// A node's samples over the range, zero where it holds none.
176    pub fn output(&self, node: NodeId) -> Result<Buffer, EngineError> {
177        match (self.buffers.get(&node), self.range) {
178            (Some(held), Some(range)) => Ok(held.over(range, held.extent())),
179            _ => Err(answer::unheld(self, node)),
180        }
181    }
182
183    pub fn buffer(&self, node: NodeId) -> Option<&Buffer> {
184        self.buffers.get(&node)
185    }
186
187    pub fn id(&self, path: &str) -> Option<NodeId> {
188        self.tys.id(path)
189    }
190
191    /// The node a reading names, refusing by name where a file expanded into several.
192    pub fn node(&self, path: &str) -> Result<NodeId, EngineError> {
193        self.tys.resolve(path)
194    }
195
196    /// Each segment the render computed of `node`'s value, in order: a sample in two was
197    /// computed twice.
198    pub fn evaluated(&self, node: NodeId) -> Vec<Extent> {
199        let Some(value_graph) = &self.value_graph else {
200            return Vec::new();
201        };
202        value_graph
203            .of(node)
204            .map_or(Vec::new(), |at| value_graph.values[at].evaluated.clone())
205    }
206
207    /// The multiple a reading asks this node's score against, where one asks for a score at all.
208    pub fn alias_oversample(&self, node: NodeId) -> Option<u32> {
209        self.config
210            .asks
211            .iter()
212            .find_map(|ask| match ask.representation {
213                crate::query::Representation::Alias { oversample }
214                    if self.node(&ask.node).is_ok_and(|asked| asked == node) =>
215                {
216                    Some(oversample)
217                }
218                _ => None,
219            })
220    }
221}
222
223/// `render_over` memory alone. Nothing is materialized that no reading asked for.
224pub fn render(
225    graph: &Graph,
226    target: &str,
227    config: RenderConfig,
228    tier: &Tier,
229) -> Result<Render, EngineError> {
230    now(render_over(graph, target, config, tier))
231}
232
233fn closed(held: &mut Render) -> Result<(), EngineError> {
234    scored(held)?;
235    compose_read(held);
236    stamp(held);
237    Ok(())
238}
239
240pub(crate) struct Prepared {
241    pub(crate) instances: instantiate::Instances,
242    pub(crate) tys: Typing,
243    pub(crate) root: NodeId,
244}
245
246pub(crate) fn prepared(graph: &Graph, target: &str, rate: u32) -> Result<Prepared, EngineError> {
247    let instances = instantiate::instantiate(graph, target, rate)?;
248    let held = instances.instance_of(target)?;
249    let order = schedule::schedule_from(&instances, std::slice::from_ref(&held))?;
250    let tys = typing::infer_all(&instances, &order)?;
251    let root = tys
252        .id(&held)
253        .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
254    Ok(Prepared {
255        instances,
256        tys,
257        root,
258    })
259}
260
261fn planned(
262    (graph, target): (&Graph, &str),
263    prepared: Prepared,
264    (config, memory): (RenderConfig, Memory),
265) -> Result<Render, EngineError> {
266    let Prepared {
267        instances,
268        tys,
269        root,
270    } = prepared;
271    let config = (config, &mut None, memory);
272    let none = (None, &BTreeMap::new());
273    planned_over((graph, target, &instances), (tys, root), config, none)
274}
275
276fn planned_over(
277    (graph, target, instances): (&Graph, &str, &instantiate::Instances),
278    (tys, root): (Typing, NodeId),
279    (config, stand_in, memory): (RenderConfig, &mut Option<world::World>, Memory),
280    (decided, hits): (Option<end::End>, &BTreeMap<NodeId, Arc<Stored>>),
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), memory);
288    held.bindings = bindings;
289    ranged(&mut held, (decided, hits))?;
290    let volatile = volatile::mark((graph, instances), &held, (target, stand_in))?;
291    if let Some(value_graph) = &mut held.value_graph {
292        value_graph.slots(volatile.slots());
293    }
294    held.unslotted = volatile.unslotted.clone();
295    held.stand_in_typed = volatile.typed;
296    Ok(held)
297}
298
299/// The range and every value a render of `target` would compute, and no sample.
300pub fn plan(graph: &Graph, target: &str, config: RenderConfig) -> Result<Render, EngineError> {
301    planned(
302        (graph, target),
303        prepared(graph, target, config.rate)?,
304        (config, Memory::holding(0)),
305    )
306}
307
308/// The sample each of `roots` read bare would end at, `None` where its support never ends.
309pub fn ends(
310    graph: &Graph,
311    roots: &[String],
312    config: &RenderConfig,
313) -> Result<Vec<Option<i64>>, EngineError> {
314    let (instances, named) = instantiate::from_roots(graph, roots, config.rate)?;
315    let order = schedule::schedule_from(&instances, &named)?;
316    let tys = typing::infer_all(&instances, &order)?;
317    let supports = Supports::new(&tys);
318    let ending = Ending::new(&tys, &config.profile, &supports);
319    named
320        .iter()
321        .map(|held| {
322            let id = tys
323                .id(held)
324                .ok_or_else(|| EngineError::UnknownNode(held.clone()))?;
325            Ok(default_end(ending.of(id).support))
326        })
327        .collect()
328}
329
330/// An open render's end, found on what each node computes, never on what memory answers.
331pub(crate) fn ended(
332    tys: &Typing,
333    root: NodeId,
334    mut config: RenderConfig,
335) -> (RenderConfig, Option<end::End>) {
336    if config.range.end.is_some() {
337        return (config, None);
338    }
339    let supports = Supports::new(tys);
340    let end = Ending::new(tys, &config.profile, &supports).of(root);
341    config.range.end = default_end(end.support);
342    (config, Some(end))
343}
344
345/// A reading of samples needs the range; lines and structure never do.
346/// `decided`: where an open render's root ends, found before it was planned.
347fn ranged(
348    held: &mut Render,
349    (decided, hits): (Option<end::End>, &BTreeMap<NodeId, Arc<Stored>>),
350) -> Result<(), EngineError> {
351    let envelope = held.config.asks.iter().any(|ask| {
352        matches!(
353            ask.representation,
354            crate::query::Representation::Envelope { .. }
355        )
356    });
357    let tabled = materializes(held);
358    if !tabled && !envelope {
359        return Ok(());
360    }
361    let supports = Supports::new(&held.tys);
362    let end = match (held.config.range.end, decided) {
363        (_, Some(end)) => end,
364        (Some(_), None) => Ending::new(&held.tys, &held.config.profile, &supports).exact(held.root),
365        (None, None) => Ending::new(&held.tys, &held.config.profile, &supports).of(held.root),
366    };
367    held.treated_as_silent_from_sample = end.treated_as_silent_from_sample;
368    let support = end.support;
369    let range = range_over(
370        (&held.config, held.tys.name(held.root)),
371        support,
372        Ends::Refused,
373    );
374    if !tabled {
375        match range {
376            Ok(range) => held.range = Some(range),
377            Err(refused) => held.unranged = Some(refused),
378        }
379        return Ok(());
380    }
381    held.range = Some(range?);
382    let found = supports.into_memo();
383    let wanted: Vec<NodeId> = held.schedule.wanted.clone();
384    let root = (held.root, wanted.as_slice());
385    let value_graph = ValueGraph::bounded(&held.tys, root, &held.config.profile, (found, hits))?;
386    value_graph.refuse_endless(held.range.expect("a range was decided"))?;
387    held.value_graph = Some(value_graph);
388    Ok(())
389}
390
391fn materializes(held: &Render) -> bool {
392    !held.schedule.wanted.is_empty()
393}
394
395pub(crate) enum Ends {
396    Refused,
397    Pulled,
398}
399
400/// The range a root of `support` is read over.
401pub(crate) fn range_over(
402    (config, name): (&RenderConfig, &str),
403    support: Extent,
404    ends: Ends,
405) -> Result<Extent, EngineError> {
406    let start = config.range.start.unwrap_or_else(|| default_start(support));
407    let end = match config.range.end.or(default_end(support)) {
408        Some(end) => end,
409        None => match ends {
410            Ends::Pulled => i64::MAX,
411            Ends::Refused => return Err(endless(name)),
412        },
413    };
414    Ok(Extent::new(start, end.max(start)))
415}
416
417fn default_start(support: Extent) -> i64 {
418    match support.is_empty() || support.start == i64::MIN || support.start > 0 {
419        true => 0,
420        false => support.start,
421    }
422}
423
424fn default_end(support: Extent) -> Option<i64> {
425    (support.end != i64::MAX).then_some(support.end)
426}
427
428fn endless(name: &str) -> EngineError {
429    EngineError::refused(Diagnostic {
430        code: "render.no_end".to_string(),
431        message: format!(
432            "`{name}` is read over an interval with no end, and its support never ends"
433        ),
434        location: Located::at(name, None),
435        help: "give the interval an end, as `[0, 2s]`, or crop it".to_string(),
436    })
437}
438
439/// Every wanted value pulled over the range, block by block, until `until` stops it.
440#[cfg(test)]
441fn pulled(held: &mut Render, memo: (&Memory, Recording)) -> Result<(), EngineError> {
442    if let Some(mut driver) = driving(held, memo)? {
443        while driver.pull()? {}
444        drove(held, driver);
445    }
446    Ok(())
447}
448
449/// What pulls a render's value graph, where it materializes one at all.
450fn driving(
451    held: &mut Render,
452    (memory, recording): (&Memory, Recording),
453) -> Result<Option<drive::Driver>, EngineError> {
454    let (Some(value_graph), Some(range)) = (held.value_graph.take(), held.range) else {
455        return Ok(None);
456    };
457    if !materializes(held) {
458        held.value_graph = Some(value_graph);
459        return Ok(None);
460    }
461    Ok(Some(drive::Driver::new(
462        value_graph,
463        range,
464        (sva_samples::BLOCK as usize).saturating_mul(held.config.threads.get()),
465        &held.config,
466        (memory.clone(), recording),
467    )))
468}
469
470pub(crate) fn dropped(config: &RenderConfig) -> bool {
471    config.out == Out::Dropped && config.asks.is_empty()
472}
473
474fn drove(held: &mut Render, driver: drive::Driver) {
475    let keep = !dropped(&held.config);
476    let range = held.range.expect("a pulled render has a range");
477    held.held_bytes = driver.most_bytes();
478    held.computed_samples = driver.work.computed_samples;
479    held.cache_stats = Some(driver.recording.stats(&driver.memory));
480    if let Some(stop) = driver.stop().filter(|stop| *stop < range.end) {
481        held.range = Some(Extent::new(range.start, stop));
482    }
483    let range = held.range.expect("a pulled render has a range");
484    let value_graph = driver.value_graph;
485    let wanted = held.schedule.wanted.iter().filter(|_| keep);
486    for (id, at) in wanted.map(|id| (*id, value_graph.of(*id))) {
487        let Some(at) = at else {
488            continue;
489        };
490        match &value_graph.values[at].holding {
491            value_graph::Holding::Frames(Some(frames)) => {
492                held.frames.insert(id, (**frames).clone());
493            }
494            _ => {
495                held.buffers.insert(id, value_graph.samples(at, range));
496                held.labels.insert(id, value_graph.label(at));
497            }
498        }
499    }
500    held.value_graph = Some(value_graph);
501}
502
503/// A closed form's value `fine` times finer than the render's step: what an alias score
504/// reads against.
505pub(crate) fn finer(
506    render: &Render,
507    node: NodeId,
508    fine: u32,
509    over: Extent,
510) -> Result<Buffer, EngineError> {
511    let profile = &render.config.profile;
512    let mut value_graph = ValueGraph::finer(&render.tys, node, &[node], profile, i128::from(fine))?;
513    let at = value_graph.root;
514    let memory = &render.memory;
515    let threads = render.config.threads;
516    value_graph.pull(over, (memory, &mut Recording::over(memory)), threads)?;
517    let mut held = value_graph.samples(at, over);
518    held.rate = render.config.rate * fine;
519    Ok(held)
520}
521
522/// A point sampling a reading asks the score of says in its label what the grid lost.
523fn scored(held: &mut Render) -> Result<(), EngineError> {
524    let asked: Vec<NodeId> = held
525        .labels
526        .keys()
527        .copied()
528        .filter(|id| held.alias_oversample(*id).is_some())
529        .collect();
530    for id in asked {
531        let sva_samples::Detail::Point {
532            rule,
533            alias_db: None,
534            tail_db,
535        } = held.labels[&id].detail
536        else {
537            continue;
538        };
539        let buffer = held.output(id)?;
540        let alias_db = Some(answer::alias_db(held, id, &buffer)?);
541        let label = held.labels.get_mut(&id).expect("an asked label");
542        label.detail = sva_samples::Detail::Point {
543            rule,
544            alias_db,
545            tail_db,
546        };
547    }
548    Ok(())
549}
550
551/// `render` with every read its own value, over a memory keeping none.
552#[cfg(test)]
553pub(crate) fn render_apart(
554    graph: &Graph,
555    target: &str,
556    config: RenderConfig,
557) -> Result<Render, EngineError> {
558    let mut held = planned(
559        (graph, target),
560        prepared(graph, target, config.rate)?,
561        (config, Memory::holding(0)),
562    )?;
563    if let (Some(range), Some(_)) = (held.range, &held.value_graph) {
564        let wanted = held.schedule.wanted.clone();
565        let value_graph = ValueGraph::apart(&held.tys, held.root, &wanted, &held.config.profile)?;
566        value_graph.refuse_endless(range)?;
567        held.value_graph = Some(value_graph);
568    }
569    let memory = held.memory.clone();
570    pulled(&mut held, (&memory, Recording::over(&memory)))?;
571    Ok(held)
572}
573
574pub(crate) fn sampled(render: &Render, node: NodeId, over: Extent) -> Result<Buffer, EngineError> {
575    let mut value_graph = ValueGraph::build(&render.tys, node, &[node], &render.config.profile)?;
576    let at = value_graph.root;
577    let memory = &render.memory;
578    let threads = render.config.threads;
579    value_graph.pull(over, (memory, &mut Recording::over(memory)), threads)?;
580    Ok(value_graph.samples(at, over))
581}
582
583/// FORMAT 9.3: the render's own label says where it cut below the silence threshold.
584fn stamp(held: &mut Render) {
585    let root = held.root;
586    let Some(label) = held.labels.remove(&root) else {
587        return;
588    };
589    let treated_as_silent_from_sample = held
590        .treated_as_silent_from_sample
591        .map(|at| (held.tys.name(root).to_string(), at));
592    let cutting = sva_samples::CuttingBelowSilenceThreshold {
593        silence_threshold_dbfs: held.config.profile.silence_threshold_dbfs,
594        treated_as_silent_from_sample: treated_as_silent_from_sample.into_iter().collect(),
595    };
596    let label = sva_samples::Label {
597        rate: held.config.rate,
598        moved: held.value_graph.as_ref().map(ValueGraph::moved),
599        cutting_below_silence_threshold: Some(cutting),
600        ..label
601    };
602    held.labels.insert(root, label);
603}
604
605/// One render, every reading: a closed form a reading asks for is composed once here; one
606/// that refuses is left for the reading to raise.
607fn compose_read(held: &mut Render) {
608    for id in held.schedule.compose.clone() {
609        if held.symbolic.contains_key(&id) {
610            continue;
611        }
612        if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
613            held.symbolic.insert(id, sum);
614        }
615    }
616}
617
618fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
619    Some(
620        instances
621            .bindings(path)?
622            .into_iter()
623            .map(|(name, expr, cx)| Binding {
624                name: name.to_string(),
625                source: instances.render(expr, cx),
626            })
627            .collect(),
628    )
629}