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::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 = (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): (Option<end::End>, &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.
334pub(crate) fn ended(
335    tys: &Typing,
336    root: NodeId,
337    mut config: RenderConfig,
338) -> (RenderConfig, Option<end::End>) {
339    if config.range.end.is_some() {
340        return (config, None);
341    }
342    let supports = Supports::new(tys);
343    let end = Ending::new(tys, &config.profile, &supports).of(root);
344    config.range.end = default_end(end.support);
345    (config, Some(end))
346}
347
348/// A reading of samples needs the range; lines and structure never do.
349/// `decided`: where an open render's root ends, found before it was planned.
350fn ranged(
351    held: &mut Render,
352    (decided, hits): (Option<end::End>, &BTreeMap<NodeId, Arc<Stored>>),
353) -> Result<(), EngineError> {
354    let asked = |of: fn(&Representation) -> bool| {
355        held.config.asks.iter().any(|ask| of(&ask.representation))
356    };
357    let envelope = asked(|r| {
358        matches!(
359            r,
360            Representation::Envelope { .. } | Representation::Harmony { .. }
361        )
362    });
363    let pitch = asked(|r| matches!(r, Representation::Pitch { .. }));
364    let tabled = materializes(held);
365    if !tabled && !envelope && !pitch {
366        return Ok(());
367    }
368    let supports = Supports::new(&held.tys);
369    let end = match (held.config.range.end, decided) {
370        (_, Some(end)) => end,
371        (Some(_), None) => Ending::new(&held.tys, &held.config.profile, &supports).exact(held.root),
372        (None, None) => Ending::new(&held.tys, &held.config.profile, &supports).of(held.root),
373    };
374    let support = end.support;
375    held.read_from = Some(start_over(&held.config, support));
376    if !tabled && !envelope {
377        return Ok(());
378    }
379    held.treated_as_silent_from_sample = end.treated_as_silent_from_sample;
380    let range = range_over(
381        (&held.config, held.tys.name(held.root)),
382        support,
383        Ends::Refused,
384    );
385    if !tabled {
386        match range {
387            Ok(range) => held.range = Some(range),
388            Err(refused) => held.unranged = Some(refused),
389        }
390        return Ok(());
391    }
392    held.range = Some(range?);
393    let found = supports.into_memo();
394    let wanted: Vec<NodeId> = held.schedule.wanted.clone();
395    let root = (held.root, wanted.as_slice());
396    let value_graph = ValueGraph::bounded(&held.tys, root, &held.config.profile, (found, hits))?;
397    value_graph.refuse_endless(held.range.expect("a range was decided"))?;
398    held.value_graph = Some(value_graph);
399    Ok(())
400}
401
402fn materializes(held: &Render) -> bool {
403    !held.schedule.wanted.is_empty()
404}
405
406pub(crate) enum Ends {
407    Refused,
408    Pulled,
409}
410
411/// The range a root of `support` is read over.
412pub(crate) fn range_over(
413    (config, name): (&RenderConfig, &str),
414    support: Extent,
415    ends: Ends,
416) -> Result<Extent, EngineError> {
417    let start = start_over(config, support);
418    let end = match config.range.end.or(default_end(support)) {
419        Some(end) => end,
420        None => match ends {
421            Ends::Pulled => i64::MAX,
422            Ends::Refused => return Err(endless(name)),
423        },
424    };
425    Ok(Extent::new(start, end.max(start)))
426}
427
428/// The sample a root of `support` is read from: the stated start, else where its support begins.
429pub(crate) fn start_over(config: &RenderConfig, support: Extent) -> i64 {
430    config.range.start.unwrap_or_else(|| default_start(support))
431}
432
433fn default_start(support: Extent) -> i64 {
434    match support.is_empty() || support.start == i64::MIN || support.start > 0 {
435        true => 0,
436        false => support.start,
437    }
438}
439
440fn default_end(support: Extent) -> Option<i64> {
441    (support.end != i64::MAX).then_some(support.end)
442}
443
444fn endless(name: &str) -> EngineError {
445    EngineError::refused(Diagnostic {
446        code: "render.no_end".to_string(),
447        message: format!(
448            "`{name}` is read over an interval with no end, and its support never ends"
449        ),
450        location: Located::at(name, None),
451        help: "give the interval an end, as `[0, 2s]`, or crop it".to_string(),
452    })
453}
454
455/// Every wanted value pulled over the range, block by block, until `until` stops it.
456#[cfg(test)]
457fn pulled(held: &mut Render, memo: (&Memory, Recording)) -> Result<(), EngineError> {
458    if let Some(mut driver) = driving(held, memo)? {
459        while driver.pull()? {}
460        drove(held, driver);
461    }
462    Ok(())
463}
464
465/// What pulls a render's value graph, where it materializes one at all.
466fn driving(
467    held: &mut Render,
468    (memory, recording): (&Memory, Recording),
469) -> Result<Option<drive::Driver>, EngineError> {
470    let (Some(value_graph), Some(range)) = (held.value_graph.take(), held.range) else {
471        return Ok(None);
472    };
473    if !materializes(held) {
474        held.value_graph = Some(value_graph);
475        return Ok(None);
476    }
477    Ok(Some(drive::Driver::new(
478        value_graph,
479        range,
480        (sva_samples::BLOCK as usize).saturating_mul(held.config.threads.get()),
481        &held.config,
482        (memory.clone(), recording),
483    )))
484}
485
486pub(crate) fn dropped(config: &RenderConfig) -> bool {
487    config.out == Out::Dropped && config.asks.is_empty()
488}
489
490fn drove(held: &mut Render, driver: drive::Driver) {
491    let keep = !dropped(&held.config);
492    let range = held.range.expect("a pulled render has a range");
493    held.held_bytes = driver.most_bytes();
494    held.computed_samples = driver.work.computed_samples;
495    held.cache_stats = Some(driver.recording.stats(&driver.memory));
496    if let Some(stop) = driver.stop().filter(|stop| *stop < range.end) {
497        held.range = Some(Extent::new(range.start, stop));
498    }
499    let range = held.range.expect("a pulled render has a range");
500    let value_graph = driver.value_graph;
501    let wanted = held.schedule.wanted.iter().filter(|_| keep);
502    for (id, at) in wanted.map(|id| (*id, value_graph.of(*id))) {
503        let Some(at) = at else {
504            continue;
505        };
506        match &value_graph.values[at].holding {
507            value_graph::Holding::Frames(Some(frames)) => {
508                held.frames.insert(id, (**frames).clone());
509            }
510            _ => {
511                held.buffers.insert(id, value_graph.samples(at, range));
512                held.labels.insert(id, value_graph.label(at));
513            }
514        }
515    }
516    held.value_graph = Some(value_graph);
517}
518
519/// A closed form's value `fine` times finer than the render's step: what an alias score
520/// reads against.
521pub(crate) fn finer(
522    render: &Render,
523    node: NodeId,
524    fine: u32,
525    over: Extent,
526) -> Result<Buffer, EngineError> {
527    let profile = &render.config.profile;
528    let mut value_graph = ValueGraph::finer(&render.tys, node, &[node], profile, i128::from(fine))?;
529    let at = value_graph.root;
530    let memory = &render.memory;
531    let threads = render.config.threads;
532    value_graph.pull(over, (memory, &mut Recording::over(memory)), threads)?;
533    let mut held = value_graph.samples(at, over);
534    held.rate = render.config.rate * fine;
535    Ok(held)
536}
537
538/// A point sampling a reading asks the score of says in its label what the grid lost.
539fn scored(held: &mut Render) -> Result<(), EngineError> {
540    let asked: Vec<NodeId> = held
541        .labels
542        .keys()
543        .copied()
544        .filter(|id| held.alias_oversample(*id).is_some())
545        .collect();
546    for id in asked {
547        let sva_samples::Detail::Point {
548            rule,
549            alias_db: None,
550            tail_db,
551        } = held.labels[&id].detail
552        else {
553            continue;
554        };
555        let buffer = held.output(id)?;
556        let alias_db = Some(answer::alias_db(held, id, &buffer)?);
557        let label = held.labels.get_mut(&id).expect("an asked label");
558        label.detail = sva_samples::Detail::Point {
559            rule,
560            alias_db,
561            tail_db,
562        };
563    }
564    Ok(())
565}
566
567/// `render` with every read its own value, over a memory keeping none.
568#[cfg(test)]
569pub(crate) fn render_apart(
570    graph: &Graph,
571    target: &str,
572    config: RenderConfig,
573) -> Result<Render, EngineError> {
574    let mut held = planned(
575        (graph, target),
576        prepared(graph, target, config.rate)?,
577        (config, Memory::holding(0)),
578    )?;
579    if let (Some(range), Some(_)) = (held.range, &held.value_graph) {
580        let wanted = held.schedule.wanted.clone();
581        let value_graph = ValueGraph::apart(&held.tys, held.root, &wanted, &held.config.profile)?;
582        value_graph.refuse_endless(range)?;
583        held.value_graph = Some(value_graph);
584    }
585    let memory = held.memory.clone();
586    pulled(&mut held, (&memory, Recording::over(&memory)))?;
587    Ok(held)
588}
589
590pub(crate) fn sampled(render: &Render, node: NodeId, over: Extent) -> Result<Buffer, EngineError> {
591    let mut value_graph = ValueGraph::build(&render.tys, node, &[node], &render.config.profile)?;
592    let at = value_graph.root;
593    let memory = &render.memory;
594    let threads = render.config.threads;
595    value_graph.pull(over, (memory, &mut Recording::over(memory)), threads)?;
596    Ok(value_graph.samples(at, over))
597}
598
599/// The render's own label says where it cut below the silence threshold.
600fn stamp(held: &mut Render) {
601    let root = held.root;
602    let Some(label) = held.labels.remove(&root) else {
603        return;
604    };
605    let treated_as_silent_from_sample = held
606        .treated_as_silent_from_sample
607        .map(|at| (held.tys.name(root).to_string(), at));
608    let cutting = sva_samples::CuttingBelowSilenceThreshold {
609        silence_threshold_dbfs: held.config.profile.silence_threshold_dbfs,
610        treated_as_silent_from_sample: treated_as_silent_from_sample.into_iter().collect(),
611    };
612    let label = sva_samples::Label {
613        rate: held.config.rate,
614        moved: held.value_graph.as_ref().map(ValueGraph::moved),
615        cutting_below_silence_threshold: Some(cutting),
616        ..label
617    };
618    held.labels.insert(root, label);
619}
620
621/// One render, every reading: a closed form a reading asks for is composed once here; one
622/// that refuses is left for the reading to raise.
623fn compose_read(held: &mut Render) {
624    for id in held.schedule.compose.clone() {
625        if held.symbolic.contains_key(&id) {
626            continue;
627        }
628        if let Ok(sum) = refs::spectral_sum_of(&held.tys, id, held.tys.var(id)) {
629            held.symbolic.insert(id, sum);
630        }
631    }
632}
633
634fn resolved(instances: &instantiate::Instances, path: &str) -> Option<Vec<Binding>> {
635    Some(
636        instances
637            .bindings(path)?
638            .into_iter()
639            .map(|(name, expr, cx)| Binding {
640                name: name.to_string(),
641                source: instances.render(expr, cx),
642            })
643            .collect(),
644    )
645}