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