Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dm/host/hle_audio.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ static void *event_notify(void *p) {
dispatch_semaphore_wait(a->notify_sem, DISPATCH_TIME_FOREVER);
if (atomic_exchange_explicit(&a->notify_pending, 0, memory_order_acq_rel) && a->event)
hle_signal_system_event(a->event);
atomic_store_explicit(&a->notify_armed, 0, memory_order_release);
// Acquire a producer that saw the old armed=1, so the pending recheck
// cannot miss its release. Otherwise the producer or our re-arm posts.
// A release-only store permits pending=1/armed=0 with both sides asleep.
atomic_exchange_explicit(&a->notify_armed, 0, memory_order_acq_rel);
if (!atomic_load_explicit(&a->notify_run, memory_order_acquire)) break;
if (atomic_load_explicit(&a->notify_pending, memory_order_acquire) &&
atomic_exchange_explicit(&a->notify_armed, 1, memory_order_acq_rel) == 0)
Expand Down
21 changes: 20 additions & 1 deletion dm/host/tests/t_audio_stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,16 @@ static int render(void *out, int count, int with_pcm) {
if (with_pcm && !h_au_Append(out, &b)) return 0;
AudioBufferList bl = {.mNumberBuffers = 1, .mBuffers = {{2, frames * 4, played}}};
in_callback = 1; au_render(a, NULL, NULL, 0, frames, &bl); in_callback = 0;
if (with_pcm && ((a->event && !wait_for(released_event)) || memcmp(input, played, (size_t)frames * 4) || h_au_Released(out) != &b)) return 0;
if (with_pcm) {
int woke = !a->event || wait_for(released_event);
int same = !memcmp(input, played, (size_t)frames * 4);
gbuf *returned = woke && same ? h_au_Released(out) : NULL;
if (!woke || !same || returned != &b) {
printf("release handoff %d/%d: event=%d PCM=%d buffer=%d pending=%d armed=%d\n",
i + 1, count, woke, same, returned == &b, atomic_load(&a->notify_pending), atomic_load(&a->notify_armed));
return 0;
}
}
}
return 1;
}
Expand Down Expand Up @@ -103,6 +112,16 @@ int main(void) {
CHECK(atomic_load(&reports) == 5, "the watchdog logs its copied report after the output was freed");
CHECK(!atomic_load(&signals), "statistics wakes never signal a guest buffer-release event");
CHECK(!atomic_load(&callback_io), "all callback paths remain free of diagnostic stdio");

// One buffer per event repeatedly crosses the notifier's disarm/recheck
// boundary. Bulk PCM stress can coalesce releases and miss a stranded wake.
atomic_store(&signals, 0);
CHECK(!h_au_OpenDef(out, ev, prm), "open output for release-handoff stress");
atomic_store(&ao(out)->state, STATE_STARTED);
CHECK(render(out, 100000, 1), "every individual release handoff makes progress");
CHECK(atomic_load(&signals) == 100000, "all 100000 event-driven buffers are released");
CHECK(!atomic_load(&callback_io), "release-handoff stress keeps diagnostics off the callback");
h_au_Close(out);
dispatch_release(report_entered); dispatch_release(report_allow); dispatch_release(report_done); dispatch_release(released_event);
printf("audio callback statistics: %d checks, %d failures\n", checks, failures);
return failures != 0;
Expand Down
1 change: 1 addition & 0 deletions dm/tools/impact_map.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ dm/gfx/tools/uepak.py shader:stage:* Unreal archive input to shader scanning
dm/host/romfs_image.h hle:*fs* RomFS image index validation shared by mounts and iOS startup
dm/host/romfs_identity.h *:* RomFS identity checks at iOS startup for every game's separate deployment
dm/host/hle_fs.c hle:*fs* filesystem mounts, paths and file operations
dm/host/hle_audio.c:(notify_release|event_notify) hle:*AudioOutE* guest AudioOut buffer-release notifications; the host renderer uses rel_sem
dm/host/save_sync.* hle:*fs* cloud saves: the first save mount and every Commit (hle_fs.c)
dm/host/Makefile *:* the shared host build
dm/host/hle_thread.c svc:* threads and synchronization
Expand Down
1 change: 1 addition & 0 deletions docs/agents/symptoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
- no music and no sound effects, only the movies' sound (the log repeats `audren: first-frame commands …` every frame and never prints `DeviceSink inputs`). → [SDK 8: command IDs numbered differently](../reference/audio.md#sdk-8-command-ids-numbered-differently)
- no sound; red `audren: command 0 in the command list is malformed (first word 0x181901 size 0)`. → [SDK 21 DSP command header without a magic](../reference/audio.md#sdk-21-dsp-command-header-without-a-magic)
- RealtimeSanitizer reports stdio locking in `au_render` at its 2,000-callback diagnostic boundary. A busy log writer can block that callback even when its PCM queue is full. → [Periodic diagnostics outside the AudioUnit callback](../reference/audio.md#periodic-diagnostics-outside-the-audiounit-callback)
- PCM is consumed correctly and its buffer reaches the released queue, but the guest release event never arrives. The notifier is asleep with `notify_pending=1` and `notify_armed=0`. → [AudioOut release notification can sleep with pending work](../reference/audio.md#audioout-release-notification-can-sleep-with-pending-work)
- FMOD waits for buffer release after stopping an AudioOut, hanging the game during a scene transition. → [Stopping an AudioOut releases its pending buffers](../reference/audio.md#stopping-an-audioout-releases-its-pending-buffers)
- crackling on iOS only. → [RemoteIO's default I/O buffer duration](../reference/audio.md#remoteios-default-io-buffer-duration)
- clicks and dropouts on the Mac, heard as clipping; the log's `audio #N device callbacks` lines show hundreds of underruns per 2000 callbacks. Closing an output can also log `[RED] audio device buffer: ... (status 560947818)` (`!obj`). → [The Mac's default I/O buffer](../reference/audio.md#the-macs-default-io-buffer)
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ For a replaced DSP renderer the size query keeps the service's work-buffer contr
- **Found on:** the [synthetic real-time audio audit in issue #313](https://github.com/seedds/nxnative/issues/313#issuecomment-6523656102). No particular game's audible failure is attributed to this finding.
- **Check:** `t_audio_stats` calls the real callback with synthetic PCM and traps callback-thread stdio, including its stream lock. With the watchdog's writer deliberately blocked, each PCM buffer still triggers its guest release event and is refilled correctly; queued and merged reports preserve their fields. Close drains a second pending snapshot while the watchdog holds only a local copy, which it logs after the output was freed. Statistics alone cause no guest event. The fixture needs no sound device or game files.

### AudioOut release notification can sleep with pending work

- **Symptom:** PCM is consumed correctly and its buffer reaches the released queue, but the guest release event never arrives. The notifier is asleep with `notify_pending=1` and `notify_armed=0`.
- **Cause:** the notifier cleared `notify_armed` with a release-only store before checking `notify_pending`. A producer could publish pending work and see the old armed value, leaving the notifier to wake itself; without acquiring that producer's exchange, the notifier could read stale pending state and sleep. Apple-silicon code generated a store-release followed by acquire-PC loads, which permit this ordering.
- **Fix:** disarm with an acquire-release exchange. If the producer's armed exchange precedes it, the notifier acquires the pending publication. Otherwise the producer sees zero and posts, or finds the notifier already re-armed with its own semaphore post. No timeout, polling, callback logging or audio pacing changes are needed.
- **Found on:** shared CI's `t_audio_stats` ASan/UBSan fixture, reproduced locally without a sound device or game. This is a general AudioOut mechanism; no gameplay reproduction is claimed.
- **Check:** the existing blocked-diagnostics cases remain intact. The fixture also renders and individually reclaims 100,000 event-driven PCM buffers, so releases cannot be hidden by bulk notification coalescing. The old notifier naturally strands a release in this case; diagnostics distinguish event delivery, PCM contents and returned-buffer identity without retrying the failed wait.

### Stopping an AudioOut releases its pending buffers

- **Symptom:** FMOD waits for buffer release after stopping an AudioOut, hanging the game during a scene transition.
Expand Down