1mod 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#[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
36pub 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 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
82fn 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
95pub(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 level *= threshold / last / 2.0;
124 }
125}
126
127pub(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
158fn 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
253fn 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
271fn 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
309fn 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
325fn 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}