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