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