fix(audio): settle pending startup before closing contexts

This commit is contained in:
ldm0
2026-09-29 16:50:19 +08:00
parent 76bfdfd682
commit 6e2cd48fab
2 changed files with 44 additions and 4 deletions
+42 -3
View File
@@ -619,7 +619,11 @@ impl AudioContext {
// Stop AudioRenderCapacity before closing so no capacity events are queued during shutdown.
self.render_capacity.stop();
if self.state() == AudioContextState::Running {
// Startup can still be queued while the initial state is Suspended.
// Serialize Close after it before shutting down the backend.
if self.startup_pending.load(Ordering::Acquire)
|| self.state() == AudioContextState::Running
{
// First, stop rendering via a control message
let (sender, receiver) = oneshot::channel();
let notify = OneshotNotify::Async(sender);
@@ -759,8 +763,11 @@ impl AudioContext {
// Stop AudioRenderCapacity before closing so no capacity events are queued during shutdown.
self.render_capacity.stop();
// First, stop rendering via a control message
if self.state() == AudioContextState::Running {
// Startup must settle before the backend stops, even if the initial
// Suspended state has not yet transitioned to Running.
if self.startup_pending.load(Ordering::Acquire)
|| self.state() == AudioContextState::Running
{
let (sender, receiver) = crossbeam_channel::bounded(0);
let notify = OneshotNotify::Sync(sender);
self.base.send_control_msg(ControlMessage::Close { notify });
@@ -918,6 +925,38 @@ mod tests {
fn require_send_sync<T: Send + Sync>(_: T) {}
#[test]
fn test_close_sync_during_startup_is_terminal() {
let context = AudioContext::new(AudioContextOptions {
sink_id: "none".into(),
..AudioContextOptions::default()
});
context.close_sync();
assert!(
!context.startup_pending.load(Ordering::Acquire),
"close must settle the queued Startup before stopping its backend"
);
assert_eq!(context.state(), AudioContextState::Closed);
context.close_sync();
assert_eq!(context.state(), AudioContextState::Closed);
}
#[test]
fn test_close_during_startup_is_terminal() {
let context = AudioContext::new(AudioContextOptions {
sink_id: "none".into(),
..AudioContextOptions::default()
});
executor::block_on(context.close());
assert!(
!context.startup_pending.load(Ordering::Acquire),
"close must settle the queued Startup before stopping its backend"
);
assert_eq!(context.state(), AudioContextState::Closed);
context.close_sync();
assert_eq!(context.state(), AudioContextState::Closed);
}
#[test]
fn test_all_futures_thread_safe() {
let options = AudioContextOptions {
+2 -1
View File
@@ -191,10 +191,11 @@ impl RenderThread {
Startup { graph } => {
debug_assert!(self.graph.is_none());
self.graph = Some(graph);
self.set_state(AudioContextState::Running);
if let Some(startup_pending) = self.startup_pending.as_ref() {
// Observing startup completion must also observe Running.
startup_pending.store(false, Ordering::Release);
}
self.set_state(AudioContextState::Running);
}
NodeMessage { id, mut msg } => {
self.graph.as_mut().unwrap().route_message(id, msg.as_mut());