Skip to main content

sva_engine/render/silent/
mod.rs

1// Concern: renders a target until every later sample is provably under half an LSB | Non-concern: bounding one node class (envelope.rs) | IO: (&Graph, target, bits, max) -> a Render ending at silence
2
3mod envelope;
4mod floor;
5mod range;
6mod ringing;
7
8use std::cell::RefCell;
9use std::collections::{BTreeMap, BTreeSet};
10
11use sva_ast::Graph;
12use sva_formula::{Hash, NodeId};
13use sva_samples::{Buffer, Horizon, Label};
14
15pub(crate) use envelope::Live;
16use envelope::{Bounds, Envelope, Grid, STEP, Unbounded};
17
18use super::{Prepared, Render, RenderConfig, materialize, prepared, run};
19use crate::cache::{Cache, Cost, Expected, Payload, Recording, Slots};
20use crate::error::{Diagnostic, EngineError, Located};
21use crate::schedule::{Schedule, dependencies_first};
22
23/// Silence at `bits` is every later sample under `2^-bits` of full scale, proven by `max_secs`.
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct Silent {
26    pub bits: u32,
27    pub max_secs: f64,
28}
29
30impl Silent {
31    pub fn threshold(self) -> f64 {
32        2f64.powi(-(self.bits as i32))
33    }
34}
35
36/// The horizon ends at the last sample at or over the threshold. The bound proves every sample
37/// from its grid instant on is under it, and the render between proves the rest.
38pub fn render_until_silent(
39    graph: &Graph,
40    target: &str,
41    config: RenderConfig,
42    silent: Silent,
43    cache: Option<&dyn Cache>,
44    slots: Option<&Slots>,
45) -> Result<Render, EngineError> {
46    let held = prepared(graph, target)?;
47    let rate = config.rate;
48    let width = held.tys.ty(held.root).width as usize;
49    let key = silent_key(held.identity(&config.asks)?, &config, silent);
50    // A volatile root is kept in its slot by the render itself, and nowhere else.
51    let volatile = super::volatile::mark(&held.instances, &held.tys, &config, &held.target)?;
52    let recording = cache
53        .filter(|_| volatile.slot(held.root).is_none())
54        .map(|c| Recording::over(c, slots));
55    let lens = recording.as_ref().map(|r| r.at(None));
56    let store = lens.as_ref().map(|l| l as &dyn Cache);
57    if let Some((buffer, label)) = recalled(store, key, &held, rate, width) {
58        let config = ended(config, buffer.len());
59        let render = run(held, config, cache, slots, Some((buffer, label)))?;
60        return Ok(counted(render, recording));
61    }
62    let began = Cost::begun();
63    let proven = proven_at(&held, &config, silent)?;
64    let mut render = run(held, ended(config, proven.max(1)), cache, slots, None)?;
65    let heard = render.buffers.get(&render.root).map_or(0, |b| {
66        (0..b.len())
67            .rev()
68            .find(|&n| (0..b.width).any(|c| b.plane(c)[n].abs() >= silent.threshold()))
69            .map_or(0, |n| n + 1)
70    });
71    trimmed(&mut render, heard.max(1));
72    if let (Some(store), Some(buffer), Some(label)) = (
73        store,
74        render.buffers.get(&render.root),
75        render.labels.get(&render.root),
76    ) {
77        remember(store, key, buffer, label, began.elapsed());
78    }
79    Ok(counted(render, recording))
80}
81
82/// The silent entry's own lookups, before every lookup the render made.
83fn counted(mut render: Render, recording: Option<Recording>) -> Render {
84    let Some(recording) = recording else {
85        return render;
86    };
87    let mut stats = recording.finish();
88    if let Some(made) = render.cache_stats.take() {
89        stats.lookups.extend(made.lookups);
90    }
91    render.cache_stats = Some(stats);
92    render
93}
94
95/// The first sample from which the root's bound stays under the threshold.
96pub(super) fn proven_at(
97    held: &Prepared,
98    config: &RenderConfig,
99    silent: Silent,
100) -> Result<usize, EngineError> {
101    let rate = f64::from(config.rate);
102    let start = config.horizon.start_secs;
103    let samples = ((silent.max_secs - start) * rate).ceil().max(0.0) as usize;
104    let grid = Grid {
105        start,
106        rate,
107        points: samples / STEP + 1,
108    };
109    let rendered = |id: NodeId, end: f64| heard_alone(&held.tys, id, config, end);
110    let threshold = silent.threshold();
111    let mut level = threshold;
112    loop {
113        let mut bounds = Bounds::new(&held.tys, grid.clone(), config, &rendered, level);
114        let envelope = bounded(&held.tys, held.root, &mut bounds, silent)?;
115        if let Some(j) = envelope.at.iter().position(|v| *v < threshold) {
116            return Ok(j * STEP);
117        }
118        let last = envelope.at.last().copied().unwrap_or(f64::INFINITY);
119        if !bounds.held_flat || !last.is_finite() {
120            return Err(not_by(held, &envelope, silent));
121        }
122        // Each round at least halves the level, so a held bound is stepped past, or none holds.
123        level *= threshold / last / 2.0;
124    }
125}
126
127/// A bound on every sample of `root` from `now` on, from the states `live` holds there;
128/// `heard` keeps each node rendered alone over a crop's window, which no block changes.
129pub(crate) fn bound_from(
130    tys: &crate::typing::Typing,
131    root: NodeId,
132    config: &RenderConfig,
133    silent: Silent,
134    live: &dyn Live,
135    now: usize,
136    heard: &RefCell<BTreeMap<(NodeId, u64), Buffer>>,
137) -> Result<f64, EngineError> {
138    let grid = Grid {
139        start: now as f64 / f64::from(config.rate),
140        rate: f64::from(config.rate),
141        points: 1,
142    };
143    let rendered = |id: NodeId, end: f64| {
144        if let Some(buffer) = heard.borrow().get(&(id, end.to_bits())) {
145            return Ok(buffer.clone());
146        }
147        let buffer = heard_alone(tys, id, config, end)?;
148        heard
149            .borrow_mut()
150            .insert((id, end.to_bits()), buffer.clone());
151        Ok(buffer)
152    };
153    let mut bounds = Bounds::new(tys, grid, config, &rendered, silent.threshold());
154    bounds.live = Some(live);
155    Ok(bounded(tys, root, &mut bounds, silent)?.at[0])
156}
157
158/// The root's envelope, or the refusal no bound or a level held forever makes.
159fn bounded(
160    tys: &crate::typing::Typing,
161    root: NodeId,
162    bounds: &mut Bounds,
163    silent: Silent,
164) -> Result<Envelope, EngineError> {
165    let envelope = match bounds.of(root)? {
166        Ok(envelope) => envelope,
167        Err(Unbounded { node, class }) => {
168            return Err(refusal(
169                tys,
170                root,
171                "engine.no_tail_bound",
172                format!(
173                    "`{node}` is {class}, and no bound on its tail is derived yet, so silence \
174                     is never proven for it."
175                ),
176                "render it to a stated --to instead, or crop it to a window",
177            ));
178        }
179    };
180    let threshold = silent.threshold();
181    if envelope.floor >= threshold {
182        return Err(refusal(
183            tys,
184            root,
185            "engine.never_silent",
186            format!(
187                "it returns to {} forever, at or above the {}-bit floor of {}.",
188                dbfs(envelope.floor),
189                silent.bits,
190                dbfs(threshold)
191            ),
192            "crop it, or give it a release",
193        ));
194    }
195    Ok(envelope)
196}
197
198fn not_by(held: &Prepared, envelope: &Envelope, silent: Silent) -> EngineError {
199    let last = envelope.at.last().copied().unwrap_or(f64::INFINITY);
200    not_silent_by(&held.tys, held.root, last, silent)
201}
202
203pub(crate) fn not_silent_by(
204    tys: &crate::typing::Typing,
205    root: NodeId,
206    last: f64,
207    silent: Silent,
208) -> EngineError {
209    refusal(
210        tys,
211        root,
212        "engine.not_silent_by",
213        format!(
214            "its bound at {}s is {}, not under the {}-bit floor of {}, so silence is not \
215             proven by then.",
216            silent.max_secs,
217            dbfs(last),
218            silent.bits,
219            dbfs(silent.threshold())
220        ),
221        "if it decays, raise --max or lower the bits",
222    )
223}
224
225fn dbfs(v: f64) -> String {
226    match v.is_finite() {
227        true => format!("{:.1} dBFS", 20.0 * v.log10()),
228        false => "unbounded".to_string(),
229    }
230}
231
232fn refusal(
233    tys: &crate::typing::Typing,
234    root: NodeId,
235    code: &str,
236    message: String,
237    help: &str,
238) -> EngineError {
239    EngineError::refused(Diagnostic {
240        code: code.to_string(),
241        message,
242        location: Located::at(tys.name(root), None),
243        help: help.to_string(),
244    })
245}
246
247fn ended(mut config: RenderConfig, samples: usize) -> RenderConfig {
248    let start = config.horizon.start_secs;
249    config.horizon = Horizon::secs(start, start + samples as f64 / f64::from(config.rate));
250    config
251}
252
253/// Every buffer this render holds, cut to the root's silence; each is causal from the
254/// window's start, so the cut is the value a shorter render of it would have written.
255fn trimmed(render: &mut Render, samples: usize) {
256    for buffer in render.buffers.values_mut() {
257        if buffer.len() > samples {
258            let origin = buffer.origin_secs;
259            *buffer = Buffer::of_planes(
260                buffer.rate,
261                (0..buffer.width)
262                    .map(|c| buffer.plane(c)[..samples].to_vec())
263                    .collect(),
264            );
265            buffer.origin_secs = origin;
266        }
267    }
268    render.config = ended(render.config.clone(), samples);
269}
270
271/// One node on its own, over the window a crop closes: nothing is kept past this bound.
272fn heard_alone(
273    tys: &crate::typing::Typing,
274    id: NodeId,
275    config: &RenderConfig,
276    end: f64,
277) -> Result<Buffer, EngineError> {
278    let mut held = Render {
279        root: id,
280        tys: tys.clone(),
281        buffers: BTreeMap::new(),
282        frames: BTreeMap::new(),
283        symbolic: BTreeMap::new(),
284        labels: BTreeMap::new(),
285        traces: Vec::new(),
286        config: RenderConfig {
287            horizon: Horizon::secs(config.horizon.start_secs, end),
288            asks: Vec::new(),
289            ..config.clone()
290        },
291        schedule: Schedule {
292            materialize: Vec::new(),
293            symbolic: Vec::new(),
294            compose: Vec::new(),
295        },
296        bindings: BTreeMap::new(),
297        cache_stats: None,
298    };
299    for member in dependencies_first(tys, id, &mut BTreeSet::new()) {
300        materialize(&mut held, member, None)?;
301    }
302    held.buffers
303        .remove(&id)
304        .ok_or_else(|| EngineError::UnknownNode(tys.name(id).to_string()))
305}
306
307const SILENT_TAG: u64 = 0x73_69_6c_65_6e_74_00_01;
308
309/// The root at one rate and origin, silent at `bits` by `max`, scored or not: the length is
310/// the value's own.
311fn silent_key((identity, scored): (Hash, bool), config: &RenderConfig, silent: Silent) -> Hash {
312    crate::cache::mixed(
313        identity,
314        &[
315            u64::from(config.rate),
316            config.horizon.start_secs.to_bits(),
317            u64::from(silent.bits),
318            silent.max_secs.to_bits(),
319            u64::from(scored),
320            SILENT_TAG,
321        ],
322    )
323}
324
325/// The length is stored as a one-sample record under the silent key, and the samples under
326/// that key and the length together.
327fn samples_key(key: Hash, samples: usize) -> Hash {
328    crate::cache::mixed(key, &[samples as u64, SILENT_TAG])
329}
330
331fn recalled(
332    cache: Option<&dyn Cache>,
333    key: Hash,
334    held: &Prepared,
335    rate: u32,
336    width: usize,
337) -> Option<(Buffer, Label)> {
338    let cache = cache?;
339    let name = held.tys.name(held.root);
340    let length = cache.load(
341        key,
342        name,
343        Expected::Samples {
344            rate,
345            width: 1,
346            samples: 1,
347        },
348    )?;
349    let samples = length.payload.samples()?.plane(0)[0] as usize;
350    let entry = cache.load(
351        samples_key(key, samples),
352        name,
353        Expected::Samples {
354            rate,
355            width,
356            samples,
357        },
358    )?;
359    Some((entry.payload.samples().cloned()?, entry.label?))
360}
361
362fn remember(
363    cache: &dyn Cache,
364    key: Hash,
365    buffer: &Buffer,
366    label: &Label,
367    cost: std::time::Duration,
368) {
369    let payload = Payload::Samples(Box::new(buffer.clone()));
370    if !cache.worth_storing(cost, payload.bytes(), crate::cache::PayloadKind::Samples) {
371        return;
372    }
373    cache.store(samples_key(key, buffer.len()), &payload, &[], Some(label));
374    let length = Buffer::of_planes(buffer.rate, vec![vec![buffer.len() as f64]]);
375    cache.store(key, &Payload::Samples(Box::new(length)), &[], None);
376}