Skip to content

Commit 2a4e607

Browse files
committed
Render smoothness: avoid audio/render contention and GC stalls\n- Native: make clonable, keep last snapshot, use try_lock in render; skip pulse update if locked; shorter sleep\n- Web: reuse analyser buffer to avoid per-frame allocations\n- All: ci passes
1 parent 5d93cf0 commit 2a4e607

File tree

2 files changed

+64
-28
lines changed

2 files changed

+64
-28
lines changed

crates/app-native/src/main.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct InstanceData {
2626
pulse: f32,
2727
}
2828

29-
#[derive(Default)]
29+
#[derive(Default, Clone)]
3030
struct VisState {
3131
positions: [Vec3; 3],
3232
colors: [Vec4; 3],
@@ -48,6 +48,8 @@ struct GpuState<'w> {
4848
height: u32,
4949
last_frame: Instant,
5050
shared: Arc<Mutex<VisState>>,
51+
// Local snapshot to render when shared state is locked by audio thread
52+
last_vis_snapshot: VisState,
5153
}
5254

5355
impl<'w> GpuState<'w> {
@@ -212,6 +214,9 @@ impl<'w> GpuState<'w> {
212214
multiview: None,
213215
});
214216

217+
// Take an initial snapshot of visual state (non-blocking best-effort)
218+
let initial_snapshot = shared.lock().map(|v| v.clone()).unwrap_or_default();
219+
215220
Ok(Self {
216221
window,
217222
surface,
@@ -227,6 +232,7 @@ impl<'w> GpuState<'w> {
227232
height: size.height,
228233
last_frame: Instant::now(),
229234
shared,
235+
last_vis_snapshot: initial_snapshot,
230236
})
231237
}
232238

@@ -267,35 +273,46 @@ impl<'w> GpuState<'w> {
267273
}),
268274
);
269275

270-
// Build instance data from shared state
271-
let mut vis = self.shared.lock().unwrap();
272-
// decay pulses
276+
// Build instance data from shared state without blocking the render thread.
277+
// If the mutex is held by the audio scheduler, render using the last snapshot.
273278
let dt_sec = dt.as_secs_f32();
274-
for p in vis.pulses.iter_mut() {
275-
*p = (*p - dt_sec * 1.5).max(0.0);
276-
}
279+
let vis_local: VisState = if let Ok(mut vis) = self.shared.try_lock() {
280+
// Decay pulses in shared state and copy snapshot
281+
for p in vis.pulses.iter_mut() {
282+
*p = (*p - dt_sec * 1.5).max(0.0);
283+
}
284+
let snapshot = vis.clone();
285+
self.last_vis_snapshot = snapshot.clone();
286+
snapshot
287+
} else {
288+
// Decay locally; avoid writing back to shared state
289+
for p in self.last_vis_snapshot.pulses.iter_mut() {
290+
*p = (*p - dt_sec * 1.5).max(0.0);
291+
}
292+
self.last_vis_snapshot.clone()
293+
};
294+
277295
let z_offset = app_core::z_offset_vec3();
278296
let spread = SPREAD;
279297
let positions = [
280-
vis.positions[0] * spread + z_offset,
281-
vis.positions[1] * spread + z_offset,
282-
vis.positions[2] * spread + z_offset,
298+
vis_local.positions[0] * spread + z_offset,
299+
vis_local.positions[1] * spread + z_offset,
300+
vis_local.positions[2] * spread + z_offset,
283301
];
284302
let scales = [
285-
BASE_SCALE + vis.pulses[0] * app_core::SCALE_PULSE_MULTIPLIER,
286-
BASE_SCALE + vis.pulses[1] * app_core::SCALE_PULSE_MULTIPLIER,
287-
BASE_SCALE + vis.pulses[2] * app_core::SCALE_PULSE_MULTIPLIER,
303+
BASE_SCALE + vis_local.pulses[0] * app_core::SCALE_PULSE_MULTIPLIER,
304+
BASE_SCALE + vis_local.pulses[1] * app_core::SCALE_PULSE_MULTIPLIER,
305+
BASE_SCALE + vis_local.pulses[2] * app_core::SCALE_PULSE_MULTIPLIER,
288306
];
289307
let mut instances: Vec<InstanceData> = Vec::with_capacity(3);
290308
for i in 0..3 {
291309
instances.push(InstanceData {
292310
pos: positions[i].to_array(),
293311
scale: scales[i],
294-
color: vis.colors[i].to_array(),
295-
pulse: vis.pulses[i],
312+
color: vis_local.colors[i].to_array(),
313+
pulse: vis_local.pulses[i],
296314
});
297315
}
298-
drop(vis);
299316
self.queue
300317
.write_buffer(&self.instance_vb, 0, bytemuck::cast_slice(&instances));
301318

@@ -628,13 +645,16 @@ fn start_audio_engine(
628645
}
629646
drop(guard);
630647
// Kick visual pulses
631-
let mut vis = vis_clone.lock().unwrap();
632-
for ev in &events {
633-
let i = ev.voice_index.min(2);
634-
vis.pulses[i] = (vis.pulses[i] + ev.velocity).min(1.5);
648+
// Try to update visual pulses without blocking; if busy, skip this tick
649+
if let Ok(mut vis) = vis_clone.try_lock() {
650+
for ev in &events {
651+
let i = ev.voice_index.min(2);
652+
vis.pulses[i] = (vis.pulses[i] + ev.velocity).min(1.5);
653+
}
635654
}
636655
}
637-
std::thread::sleep(Duration::from_millis(15));
656+
// Small sleep to limit CPU without inducing long stalls
657+
std::thread::sleep(Duration::from_millis(8));
638658
}
639659
})
640660
.ok()?;

crates/app-web/src/lib.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,12 @@ async fn init() -> anyhow::Result<()> {
402402
if let Some(a) = &analyser {
403403
a.set_fft_size(256);
404404
}
405+
// Reusable buffer for analyser to avoid per-frame allocations/GC pauses
406+
let analyser_buf: Rc<RefCell<Vec<f32>>> = Rc::new(RefCell::new(Vec::new()));
407+
if let Some(a) = &analyser {
408+
let bins = a.frequency_bin_count() as usize;
409+
analyser_buf.borrow_mut().resize(bins, 0.0);
410+
}
405411

406412
// Pause state (stops scheduling new notes but keeps rendering)
407413
let paused = Rc::new(RefCell::new(false));
@@ -924,14 +930,19 @@ async fn init() -> anyhow::Result<()> {
924930
}
925931
// Optional analyser-driven mild ambient pulse
926932
if let Some(a) = &analyser {
927-
let bins = a.frequency_bin_count();
928-
let mut freq = vec![0.0_f32; bins as usize];
929-
a.get_float_frequency_data(&mut freq);
933+
let bins = a.frequency_bin_count() as usize;
934+
{
935+
let mut buf = analyser_buf.borrow_mut();
936+
if buf.len() != bins {
937+
buf.resize(bins, 0.0);
938+
}
939+
a.get_float_frequency_data(&mut buf);
940+
}
930941
// Use low-frequency bin energy to adjust background subtly
931942
let mut sum = 0.0f32;
932943
let take = (bins.min(16)) as u32;
933944
for i in 0..take {
934-
let v = freq[i as usize]; // in dBFS (-inf..0)
945+
let v = analyser_buf.borrow()[i as usize]; // in dBFS (-inf..0)
935946
// map dB to 0..1 roughly
936947
let lin = ((v + 100.0) / 100.0).clamp(0.0, 1.0);
937948
sum += lin;
@@ -1005,15 +1016,20 @@ async fn init() -> anyhow::Result<()> {
10051016
let bins = a.frequency_bin_count() as usize;
10061017
let dots = bins.min(16);
10071018
if dots > 0 {
1008-
let mut freq = vec![0.0_f32; bins];
1009-
a.get_float_frequency_data(&mut freq);
1019+
{
1020+
let mut buf = analyser_buf.borrow_mut();
1021+
if buf.len() != bins {
1022+
buf.resize(bins, 0.0);
1023+
}
1024+
a.get_float_frequency_data(&mut buf);
1025+
}
10101026
let _w = canvas_for_tick.width().max(1) as f32;
10111027
let _h = canvas_for_tick.height().max(1) as f32;
10121028
// place dots near bottom of view in world space
10131029
// map x from -2.8..2.8 and y slightly below origin
10141030
let z = z_offset.z;
10151031
for i in 0..dots {
1016-
let v_db = freq[i];
1032+
let v_db = analyser_buf.borrow()[i];
10171033
let lin = ((v_db + 100.0) / 100.0).clamp(0.0, 1.0);
10181034
let x = -2.8 + (i as f32) * (5.6 / (dots as f32 - 1.0));
10191035
let y = -1.8;

0 commit comments

Comments
 (0)