Make one audio device change event per kind instead of one per request, and close the event of a lost race.

IAudioDevice's Acquire...Notification (15, 17) and the virtual device manager's state change event (500) made a new
kernel event at every call and kept its writable handle for good, so a game asking repeatedly took a handle slot each
time (the table holds 4096 and is fatal when full), and past 16 events audio_channels_live no longer signalled the
new ones. The race loser of QueryAudioDeviceSystemEvent (4, 11, 12) also leaked its event.

change_event_of makes one event per kind (the device's, the input and output notifications', the virtual device
manager's) at the kind's first request; each request still gets its own readable handle. The event is listed for
audio_channels_live only by the request that wins the compare-exchange; the loser closes its own event, which was
never listed, so the list never holds a closed handle (closing a listed one would have ended the process at the next
channel change, or signalled whatever event took the number).

Not reproduced before the change: the fresh 0.10.0 session's guest handles stayed at 116-122. audio_mixer_check gains
change_events_once: 60 requests over the four kinds make four events and 60 readable handles, a simulated lost race
closes the loser's event and lists only the winner's, and a channel change signals each listed event once and never a
closed one. The compile check adds no warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:20:45 -05:00
parent 811024e69b
commit d3a5c3c353
2 changed files with 92 additions and 28 deletions

View File

@@ -1352,19 +1352,33 @@ void audio_mixer_render(AudioRenderer *r, double seconds, int drop) {
// ---- IAudioDevice: one output device, stereo or 5.1 (output_channels)
static const char device_name[] = "AudioTvOutput";
// The events the game took to hear of a change of the output device (IAudioDevice's, its notifications' and the
// The events the game took to hear of a change of the output device (IAudioDevice's, its two notifications' and the
// virtual device manager's). audio_channels_live signals them all: the game then asks for the channel count again, as
// it does on a console whose output moves from the TV to headphones.
static uint32_t change_events[16];
static _Atomic int n_change_events;
static uint32_t change_event_new(void) {
uint32_t ev = kern_event_new();
static void change_event_list(uint32_t ev) {
int i = atomic_fetch_add(&n_change_events, 1);
if (i < 16) change_events[i] = ev;
return ev;
}
// One event per kind, made at the kind's first request; every request gets its own readable handle to it. Made per
// request, each call took a kernel handle for good, and past 16 the events were no longer signalled. Two first
// requests at once each make one: the loser closes its own, which was never listed, so nothing signals a closed handle.
static uint32_t change_event_of(volatile uint32_t *kind) {
uint32_t ev = __atomic_load_n(kind, __ATOMIC_ACQUIRE);
if (ev) return ev;
uint32_t mine = kern_event_new(), expected = 0;
if (__atomic_compare_exchange_n(kind, &expected, mine, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
change_event_list(mine);
return mine;
}
kern_handle_close(mine);
return expected;
}
static volatile uint32_t device_event, input_notify_event, output_notify_event, vdm_event;
// Present path, after a settings change (gfx_present.c window_settings_live): a changed NX2NAVI_AUDIO_CHANNELS is
// decided again, and when it gives another channel count the game is told its output device changed. The game asks
// GetActiveChannelCount again and mixes for the new count; the mixer moves its device to that count at its next
@@ -1402,7 +1416,6 @@ static IpcBuffer out_buffer(const IpcRequest *rq, int *pointer) {
}
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
static uint32_t change_event; // the device change event: signalled when the output's channels change (audio_channels_live)
int pointer;
switch (rq->cmd) {
case 0: case 6: case 14: { // ListAudioDeviceName(Auto), ListAudioOutputDeviceName [13.0.0+]
@@ -1424,27 +1437,17 @@ void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs)
return;
}
case 5: { int32_t n = output_channels(); IPC_OUT(rs, n); return; } // GetActiveChannelCount
case 4: case 11: case 12: { // QueryAudioDeviceSystemEvent, ...InputEvent, ...OutputEvent
uint32_t ev = __atomic_load_n(&change_event, __ATOMIC_ACQUIRE);
if (!ev) {
uint32_t mine = change_event_new(), expected = 0;
if (__atomic_compare_exchange_n(&change_event, &expected, mine, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE))
ev = mine;
else
ev = expected; // the loser's event stays unused (no host-side close yet)
}
rs->copy[rs->ncopy++] = kern_event_readable(ev);
case 4: case 11: case 12: // QueryAudioDeviceSystemEvent, ...InputEvent, ...OutputEvent
rs->copy[rs->ncopy++] = kern_event_readable(change_event_of(&device_event));
return;
}
// 17.0.0+: Acquire/Release Audio{Input,Output}DeviceNotification (15-18), 18.0.0+: SetAudioDeviceOutputVolume-
// AutoTuneEnabled (19) / IsAudioDeviceOutputVolumeAutoTuneEnabled (20), 21.0.0+:
// IsActiveOutputDeviceEstimatedLowLatency (21). Command ids as the system answers them. The
// host applies no auto-tuned volume, so the answers are "off" / "no".
case 15: case 17: { // Acquire...Notification -> a readable event, signalled with the others
uint32_t ev = change_event_new();
rs->copy[rs->ncopy++] = kern_event_readable(ev);
case 15: case 17: // Acquire...Notification -> a readable event, signalled with the others
rs->copy[rs->ncopy++] = kern_event_readable(change_event_of(rq->cmd == 15 ? &input_notify_event
: &output_notify_event));
return;
}
case 16: case 18: case 19: return;
case 20: case 21: { uint8_t no = 0; IPC_OUT(rs, no); return; }
default: ipc_unimplemented(s, rq);
@@ -1510,7 +1513,7 @@ static void virtual_device_manager_handler(IpcSession *s, const IpcRequest *rq,
put_block(rq, rs, block, sizeof block);
return;
}
case 500: rs->copy[rs->ncopy++] = kern_event_readable(change_event_new()); return; // state change event
case 500: rs->copy[rs->ncopy++] = kern_event_readable(change_event_of(&vdm_event)); return; // state change event
case 700: case 800: return; // AttachStream / DetachStream
default: {
static volatile int32_t seen[64];

View File

@@ -1,5 +1,6 @@
/* The mixer's volume ramps (runtime/audio_mixer.c accumulate_ramp, audio_mixer_render) and what it owes the device
after a stall (ring_pull, audio_mixer_owed), with the rest of the runtime stubbed. No game data and no audio device:
/* The mixer's volume ramps (runtime/audio_mixer.c accumulate_ramp, audio_mixer_render), what it owes the device
after a stall (ring_pull, audio_mixer_owed) and the device change events (change_event_of, audio_channels_live),
with the rest of the runtime stubbed. No game data and no audio device:
NX2NAVI_AUDIO=none mixes without opening one, and the device's requests are made by hand. Each voice plays a wave
buffer of one constant sample, so a mix buffer holds the gain the voice went in with, sample by sample. */
#include "../runtime/audio_mixer.c"
@@ -22,9 +23,25 @@ void rt_fatal(const char *fmt, ...) {
exit(2);
}
uint64_t kern_now_ns(void) { return 1; }
uint32_t kern_event_new(void) { return 1; }
uint32_t kern_event_readable(uint32_t writable) { return writable; }
void kern_event_signal(uint32_t writable) { (void)writable; }
/* kernel events: numbered from 1; which were closed, signalled, and how many readable ends were handed out */
static int events_made, readables, closed[64], signalled[64], signal_closed;
static volatile uint32_t *race_kind; /* set: the next kern_event_new lets "another thread" win that kind first */
uint32_t kern_event_new(void) {
if (race_kind) {
volatile uint32_t *k = race_kind;
race_kind = NULL;
uint32_t other = kern_event_new();
change_event_list(other);
*k = other;
}
return (uint32_t)++events_made;
}
uint32_t kern_event_readable(uint32_t writable) { readables++; return writable; }
void kern_event_signal(uint32_t writable) {
if (writable < 64 && closed[writable]) signal_closed++;
if (writable < 64) signalled[writable]++;
}
void kern_handle_close(uint32_t h) { if (h < 64) closed[h] = 1; }
void *ipc_guest(uint64_t addr, uint64_t size) { (void)size; return (void *)(uintptr_t)addr; }
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)) {
(void)name, (void)handler, (void)state, (void)close;
@@ -34,8 +51,9 @@ void ipc_unimplemented(const IpcSession *s, const IpcRequest *rq) { (void)s, (vo
void ipc_out(IpcResponse *rs, const void *p, uint32_t n) { (void)rs, (void)p, (void)n; }
const void *ipc_arg(const IpcRequest *rq, uint32_t off, uint32_t n) { (void)rq, (void)off, (void)n; exit(2); }
void menu_sound_mix(float *out, uint32_t frames, int channels, int rate) { (void)out, (void)frames, (void)channels, (void)rate; }
const char *cfg_get(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO") ? "none" : NULL; }
const char *cfg_value(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO_CHANNELS") ? "stereo" : "default"; }
static const char *audio_mode = "none", *channels_setting = "stereo";
const char *cfg_get(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO") ? audio_mode : NULL; }
const char *cfg_value(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO_CHANNELS") ? channels_setting : "default"; }
int cfg_bool(const char *name) {
if (!strcmp(name, "NX2NAVI_AUDIO_RAMP")) return ramp_setting;
printf("unexpected setting %s\n", name);
@@ -356,12 +374,55 @@ static void catch_up(void) {
renderer_close(r);
}
/* The device's, the two notifications' and the virtual device manager's change events: one event per kind however
often the game asks, a readable end per request, and a channel change signals each once. Two first requests at once:
the loser's event is closed and never signalled. */
static void change_events_once(void) {
int made0 = events_made, read0 = readables;
static const unsigned device_cmds[] = {4, 11, 12, 15, 17};
for (int round = 0; round < 10; round++) {
for (unsigned i = 0; i < sizeof device_cmds / sizeof *device_cmds; i++) {
IpcRequest rq = {.cmd = device_cmds[i]};
IpcResponse rs = {0};
audio_device_handler(NULL, &rq, &rs);
CHECK(rs.ncopy == 1 && rs.copy[0], "command %u hands out a readable event", device_cmds[i]);
}
IpcRequest rq = {.cmd = 500};
IpcResponse rs = {0};
virtual_device_manager_handler(NULL, &rq, &rs);
CHECK(rs.ncopy == 1 && rs.copy[0], "the virtual device manager hands out a readable event");
}
CHECK(events_made - made0 == 4, "one event per kind: %d made for 60 requests", events_made - made0);
CHECK(readables - read0 == 60, "a readable end per request (%d)", readables - read0);
CHECK(atomic_load(&n_change_events) == 4, "four events listed (%d)", atomic_load(&n_change_events));
/* a lost race: the loser closes its own event, which is not listed */
int listed = atomic_load(&n_change_events);
static volatile uint32_t raced;
race_kind = &raced;
uint32_t got = change_event_of(&raced);
CHECK(got == raced && !closed[got], "the loser takes the winner's event");
CHECK(closed[events_made], "the loser closes its own event");
CHECK(atomic_load(&n_change_events) == listed + 1, "only the winner's event is listed");
/* a channel change signals every listed event once, never a closed one */
channels_setting = "stereo";
audio_channels_live();
CHECK(output_channels() == 2, "stereo decided");
audio_mode = "sdl", channels_setting = "5.1";
audio_channels_live();
int n = atomic_load(&n_change_events), once = 0;
for (int i = 0; i < n && i < 16; i++) once += signalled[change_events[i]] == 1;
CHECK(once == n, "a channel change signals each listed event once (%d of %d)", once, n);
CHECK(!signal_closed, "no closed event is signalled");
audio_mode = "none", channels_setting = "stereo";
}
int main(void) {
voice_into_mix();
voice_into_splitter();
mix_into_splitter();
ramps_off();
catch_up();
change_events_once();
printf("audio_mixer_check: %s\n", fails ? "FAIL" : "ok");
return fails ? 1 : 0;
}