diff --git a/dm/host/hle_audio.c b/dm/host/hle_audio.c index 0483c146ad..5d378265f4 100644 --- a/dm/host/hle_audio.c +++ b/dm/host/hle_audio.c @@ -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) diff --git a/dm/host/tests/t_audio_stats.c b/dm/host/tests/t_audio_stats.c index 435f5e06a4..3c7824bfbb 100644 --- a/dm/host/tests/t_audio_stats.c +++ b/dm/host/tests/t_audio_stats.c @@ -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; } @@ -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; diff --git a/dm/tools/impact_map.tsv b/dm/tools/impact_map.tsv index 4961987623..96ef450bd0 100644 --- a/dm/tools/impact_map.tsv +++ b/dm/tools/impact_map.tsv @@ -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 diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 25c8ec2b3d..c162922134 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -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) diff --git a/docs/reference/audio.md b/docs/reference/audio.md index 9275ca93f0..45db5553bd 100644 --- a/docs/reference/audio.md +++ b/docs/reference/audio.md @@ -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.