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