mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 17:28:49 -10:00
audio: mixer streams arrive only inside the mixer's init..done window
First find of the discovery pass the whole-tree audit pointed at next (task retirement against subsystem teardown): audio_driver_mixer_add_stream() trusted its caller's timing. The mixer's lifecycle is the audio driver's - audio_mixer_init() runs inside audio_driver_init_internal(), audio_mixer_done() inside audio_driver_mixer_deinit() - and done() releases every voice and frees its lock. Two reachable paths deliver a stream outside that window: - shutdown tears the drivers down before the task queue, so a mixer-load task that finishes late retires its callback into a done mixer; - every failure exit of audio_driver_init_internal() precedes audio_mixer_init(), so a session whose audio driver failed to come up funnels each menu sound into the same door. In both cases add_stream claims a voice whose lock is gone - slock_lock(NULL) returns silently, so nothing crashes; the voice is written unlocked, its per-voice resources are orphaned when the next audio_mixer_init() wipes voice types, and a phantom stream entry survives in mixer_streams[]. AUDIO_FLAG_MIXER_INITED now latches the window: raised right after audio_mixer_init(), dropped with MIXER_ACTIVE before audio_mixer_done(), and checked at the top of add_stream - the one injector; the other public mixer entry points walk mixer_streams[], which teardown has already emptied. The refusal keeps the ownership contract: buf_owner is released exactly as on every other failure path, and out_slot reports -1. samples/audio/mixer_retirement links the shipping objects with only main() replaced, boots headless against ALSA's null PCM device (the "null" audio driver is a stub vtable that cannot initialize), and drives the seam: a baseline add lands in a slot with the WAV conversion releasing its owner once; an add after audio_driver_deinit() is refused with the owner released exactly once and out_slot -1; an add after CMD_EVENT_AUDIO_REINIT lands again. With the guard removed the refusal lane fails exactly as the defect predicts (the stream lands in slot 16 of the torn mixer). Noted while here, untouched: the SYNC atomic backend's load on a const-qualified state pointer trips -Wdiscarded-qualifiers at audio_driver_dev_frame_bytes() on shipping DINGUX lanes; that is pre-existing and gets its own change. Verified: thread-read audit 0 findings / 0 allowlisted pairs, the mixer_retirement harness green and red as stated, playlist_nav green, host syntax passes under __PS3__, PSP and VITA (the file's platform gates) with implicit-function-declaration promoted to an error, the full build scanned clean for implicit-declaration and static-follows warnings, hot-function and stack budgets, the 74-lane compile matrix, forced atomic backends (SYNC carries the pre-existing const warning above; the other four clean) and the C89 gate.
This commit is contained in:
1 parent
a2c94e7e94
commit
2277ad2a58
5 files changed
+345
-2
No files matched your search
+11
-1
@@ -184,7 +184,17 @@ enum audio_driver_state_flags
|
||||
* runloop's pace record so "audio is holding the loop" is a fact
|
||||
* about this iteration rather than a guess about who wrote.
|
||||
*/
|
||||
AUDIO_FLAG_WROTE = (1 << 10)
|
||||
AUDIO_FLAG_WROTE = (1 << 10),
|
||||
/* The mixer subsystem is between audio_mixer_init() and
|
||||
* audio_mixer_done(): the window in which claiming voices is
|
||||
* legal. audio_driver_init_internal() raises it right after the
|
||||
* mixer comes up; audio_driver_mixer_deinit() drops it before the
|
||||
* mixer goes down. Streams arriving outside the window - a mixer
|
||||
* load task retiring after teardown at shutdown, or a menu sound
|
||||
* during a session whose audio driver failed to initialize - are
|
||||
* refused at audio_driver_mixer_add_stream() instead of claiming
|
||||
* voices whose locks are gone. */
|
||||
AUDIO_FLAG_MIXER_INITED = (1 << 11)
|
||||
};
|
||||
|
||||
typedef struct audio_statistics
|
||||
|
||||
+16
-1
@@ -1134,7 +1134,8 @@ static void audio_driver_mixer_deinit(void)
|
||||
{
|
||||
unsigned i;
|
||||
|
||||
AUDIO_FLAGS_CLEAR(&audio_driver_st, AUDIO_FLAG_MIXER_ACTIVE);
|
||||
AUDIO_FLAGS_CLEAR(&audio_driver_st, AUDIO_FLAG_MIXER_ACTIVE
|
||||
| AUDIO_FLAG_MIXER_INITED);
|
||||
|
||||
for (i = 0; i < AUDIO_MIXER_MAX_SYSTEM_STREAMS; i++)
|
||||
{
|
||||
@@ -4530,6 +4531,7 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
|
||||
|
||||
#ifdef HAVE_AUDIOMIXER
|
||||
audio_mixer_init(settings->uints.audio_output_sample_rate);
|
||||
AUDIO_FLAGS_SET(&audio_driver_st, AUDIO_FLAG_MIXER_INITED);
|
||||
#endif
|
||||
|
||||
if (!audio_driver_transport_configure(settings))
|
||||
@@ -7374,6 +7376,19 @@ bool audio_driver_mixer_add_stream(audio_mixer_stream_params_t *params)
|
||||
* outcome: each failure return releases it. */
|
||||
if (params->out_slot)
|
||||
*params->out_slot = -1;
|
||||
/* Outside the mixer's init..done window - a mixer load task
|
||||
* retiring after audio teardown at shutdown, or any stream pushed
|
||||
* during a session whose audio driver failed to initialize -
|
||||
* claiming a voice would write into a subsystem whose voice locks
|
||||
* audio_mixer_done() already freed. Refuse; ownership is released
|
||||
* as on every other failure path. */
|
||||
if (!(AUDIO_FLAGS_GET(&audio_driver_st) & AUDIO_FLAG_MIXER_INITED))
|
||||
{
|
||||
if (params->buf_owner)
|
||||
params->buf_owner_free(params->buf_owner);
|
||||
audio_driver_state_unlock();
|
||||
return false;
|
||||
}
|
||||
if (params->stream_type == AUDIO_STREAM_TYPE_NONE)
|
||||
{
|
||||
if (params->buf_owner)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
TARGET := mixer_retirement_test
|
||||
|
||||
all:
|
||||
@sh ./build.sh
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) retroarch_nomain.o retroarch_nomain.d \
|
||||
harness_main.o harness_main.d
|
||||
|
||||
.PHONY: all clean
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
# Builds the mixer add-stream init-window harness.
|
||||
#
|
||||
# Like playlist_nav, this links the SHIPPING objects of a built
|
||||
# RetroArch with only main() replaced - nothing is stubbed. The seam
|
||||
# under test runs through rgui_frame's aspect-change detection, the staged config on the
|
||||
# rgui handle, and the application rgui_render performs on the main
|
||||
# thread before CMD_EVENT_VIDEO_SET_ASPECT_RATIO fires.
|
||||
# real frame and render code can see whether the write lands in the
|
||||
# configuration or dies with the frame's snapshot copy.
|
||||
#
|
||||
# Requires a completed NON-Qt build: with Qt enabled main() lives in
|
||||
# ui_qt.o, which drags the whole Qt UI in with it. From the repo
|
||||
# root:
|
||||
#
|
||||
# ./configure --disable-qt && make
|
||||
# samples/audio/mixer_retirement/build.sh
|
||||
#
|
||||
# retroarch.c is recompiled here with main renamed, so the harness can
|
||||
# supply its own without having to exclude the object that holds most
|
||||
# of the frontend. Both the compile and the link reuse the project's
|
||||
# own command lines, taken from make -n, so the harness cannot drift
|
||||
# away from how the program is really built.
|
||||
set -eu
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../../.." && pwd)
|
||||
|
||||
cd "$root"
|
||||
|
||||
if [ ! -f obj-unix/release/retroarch.o ]; then
|
||||
echo "build RetroArch first: ./configure --disable-qt && make" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cc_line=$(mktemp)
|
||||
ld_line=$(mktemp)
|
||||
trap 'rm -f "$cc_line" "$ld_line"' EXIT
|
||||
|
||||
# The project's own compile line for retroarch.o ...
|
||||
touch retroarch.c
|
||||
make -n 2>/dev/null | grep -E '\-o obj-unix/release/retroarch\.o' | head -1 > "$cc_line"
|
||||
if [ ! -s "$cc_line" ]; then
|
||||
echo "could not determine the compile command for retroarch.o" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ... reused twice: once for retroarch.c with main renamed out of the
|
||||
# way, once for the harness itself, so both are built exactly the way
|
||||
# the frontend is.
|
||||
sed 's#-o obj-unix/release/retroarch\.o#-Dmain=rarch_harness_unused_main -o samples/audio/mixer_retirement/retroarch_nomain.o#' \
|
||||
"$cc_line" | sh
|
||||
|
||||
sed -e 's#-o obj-unix/release/retroarch\.o#-o samples/audio/mixer_retirement/harness_main.o#' \
|
||||
-e 's# retroarch\.c# samples/audio/mixer_retirement/mixer_retirement_test.c#' \
|
||||
"$cc_line" | sh
|
||||
|
||||
# The project's own link line, with retroarch.o swapped for the pair
|
||||
# above. Removing the binary first is what makes make -n emit it.
|
||||
rm -f retroarch
|
||||
make -n 2>/dev/null | grep -E ' -o retroarch ' | tail -1 > "$ld_line"
|
||||
if [ ! -s "$ld_line" ]; then
|
||||
echo "could not determine the link command" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -e 's#obj-unix/release/retroarch\.o#samples/audio/mixer_retirement/retroarch_nomain.o samples/audio/mixer_retirement/harness_main.o#' \
|
||||
-e 's#-o retroarch #-o samples/audio/mixer_retirement/mixer_retirement_test #' \
|
||||
"$ld_line" | sh
|
||||
|
||||
echo "built samples/audio/mixer_retirement/mixer_retirement_test"
|
||||
@@ -0,0 +1,237 @@
|
||||
/* Mixer streams arrive only inside the mixer's init..done window.
|
||||
*
|
||||
* Links the shipping RetroArch objects with only main() replaced
|
||||
* (the playlist_nav pattern) and drives the seam the whole-tree
|
||||
* audit pointed at next: task retirement against subsystem
|
||||
* teardown. audio_driver_deinit() runs audio_mixer_done(), which
|
||||
* releases every voice and frees its lock; a mixer-load task that
|
||||
* retires after that - RetroArch's shutdown tears the drivers down
|
||||
* before the task queue - lands its finish callback in
|
||||
* audio_driver_mixer_add_stream(), which would claim a voice inside
|
||||
* the torn subsystem. The same door is open for every menu sound in
|
||||
* a session whose audio driver failed to initialize, since all of
|
||||
* audio_driver_init_internal()'s failure exits precede
|
||||
* audio_mixer_init().
|
||||
*
|
||||
* The claims: with audio up, a stream adds; after
|
||||
* audio_driver_deinit(), an add is refused and the ownership
|
||||
* contract still holds (buf_owner released exactly once, out_slot
|
||||
* -1); after CMD_EVENT_AUDIO_REINIT, adds work again.
|
||||
*
|
||||
* Requires a completed non-Qt build:
|
||||
*
|
||||
* ./configure --disable-qt && make
|
||||
* samples/audio/mixer_retirement/build.sh
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <time/rtime.h>
|
||||
#include <file/config_file.h>
|
||||
#include <streams/file_stream.h>
|
||||
|
||||
#include "../../../configuration.h"
|
||||
#include "../../../retroarch.h"
|
||||
#include "../../../command.h"
|
||||
#include "../../../audio/audio_driver.h"
|
||||
#include "../../../frontend/frontend_driver.h"
|
||||
|
||||
static unsigned failures = 0;
|
||||
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) \
|
||||
{ \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fprintf(stderr, "\n"); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* A minimal 16-bit stereo PCM WAV at the configured output rate:
|
||||
* 44-byte canonical header plus a short run of silence. */
|
||||
#define WAV_FRAMES 256
|
||||
#define WAV_RATE 48000
|
||||
static unsigned char wav_buf[44 + WAV_FRAMES * 4];
|
||||
|
||||
static void wav_fill(void)
|
||||
{
|
||||
unsigned char *p = wav_buf;
|
||||
unsigned data_bytes = WAV_FRAMES * 4;
|
||||
unsigned riff_bytes = 36 + data_bytes;
|
||||
|
||||
#define PUT4(s) do { memcpy(p, s, 4); p += 4; } while (0)
|
||||
#define PUT32(v) do { p[0] = (unsigned char)((v) & 0xff); \
|
||||
p[1] = (unsigned char)(((v) >> 8) & 0xff); \
|
||||
p[2] = (unsigned char)(((v) >> 16) & 0xff); \
|
||||
p[3] = (unsigned char)(((v) >> 24) & 0xff); \
|
||||
p += 4; } while (0)
|
||||
#define PUT16(v) do { p[0] = (unsigned char)((v) & 0xff); \
|
||||
p[1] = (unsigned char)(((v) >> 8) & 0xff); \
|
||||
p += 2; } while (0)
|
||||
|
||||
PUT4("RIFF"); PUT32(riff_bytes); PUT4("WAVE");
|
||||
PUT4("fmt "); PUT32(16);
|
||||
PUT16(1); /* PCM */
|
||||
PUT16(2); /* stereo */
|
||||
PUT32(WAV_RATE);
|
||||
PUT32(WAV_RATE * 4); /* byte rate */
|
||||
PUT16(4); /* block align*/
|
||||
PUT16(16); /* bits */
|
||||
PUT4("data"); PUT32(data_bytes);
|
||||
memset(p, 0, data_bytes);
|
||||
|
||||
#undef PUT4
|
||||
#undef PUT32
|
||||
#undef PUT16
|
||||
}
|
||||
|
||||
/* Counting owner: the ownership contract says buf_owner is released
|
||||
* in every outcome - on failure immediately, on success when the
|
||||
* sound dies (or straight after conversion for WAV). */
|
||||
static unsigned owner_frees = 0;
|
||||
static void counting_owner_free(void *owner)
|
||||
{
|
||||
(void)owner;
|
||||
owner_frees++;
|
||||
}
|
||||
|
||||
static bool add_one(void *owner, int *slot_out)
|
||||
{
|
||||
audio_mixer_stream_params_t params;
|
||||
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
params.buf = wav_buf;
|
||||
params.bufsize = sizeof(wav_buf);
|
||||
params.basename = NULL;
|
||||
params.cb = NULL;
|
||||
params.volume = 1.0f;
|
||||
params.slot_selection_type = AUDIO_MIXER_SLOT_SELECTION_AUTOMATIC;
|
||||
params.stream_type = AUDIO_STREAM_TYPE_SYSTEM;
|
||||
params.type = AUDIO_MIXER_TYPE_WAV;
|
||||
params.state = AUDIO_STREAM_STATE_PLAYING;
|
||||
params.buf_owner = owner;
|
||||
params.buf_owner_free = owner ? counting_owner_free : NULL;
|
||||
params.out_slot = slot_out;
|
||||
|
||||
return audio_driver_mixer_add_stream(¶ms);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
char fixture_dir[512];
|
||||
char cmd[700];
|
||||
static char cfg_path[640];
|
||||
char *rarch_argv[8];
|
||||
int rarch_argc = 0;
|
||||
int slot = -2;
|
||||
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
snprintf(fixture_dir, sizeof(fixture_dir),
|
||||
"/tmp/mixer_retire_%ld", (long)getpid());
|
||||
snprintf(cmd, sizeof(cmd), "mkdir -p %s", fixture_dir);
|
||||
if (system(cmd) != 0)
|
||||
return 1;
|
||||
|
||||
rarch_argv[rarch_argc++] = (char*)"retroarch";
|
||||
rarch_argv[rarch_argc++] = (char*)"--menu";
|
||||
rarch_argv[rarch_argc++] = (char*)"--config";
|
||||
rarch_argv[rarch_argc++] = cfg_path;
|
||||
|
||||
config_file_set_io_default(config_file_io_filestream());
|
||||
rtime_init();
|
||||
retroarch_config_init();
|
||||
retroarch_ctl(RARCH_CTL_STATE_FREE, NULL);
|
||||
frontend_driver_init_first(NULL);
|
||||
{
|
||||
FILE *cfg;
|
||||
snprintf(cfg_path, sizeof(cfg_path), "%s/harness.cfg",
|
||||
fixture_dir);
|
||||
if ((cfg = fopen(cfg_path, "wb")))
|
||||
{
|
||||
fprintf(cfg, "video_driver = \"null\"\n");
|
||||
/* The "null" audio driver is a stub vtable that cannot
|
||||
* initialize; ALSA against its null PCM plugin gives a
|
||||
* real, headless driver for the audio-up lanes. */
|
||||
fprintf(cfg, "audio_driver = \"alsa\"\n");
|
||||
fprintf(cfg, "audio_device = \"null\"\n");
|
||||
fprintf(cfg, "input_driver = \"null\"\n");
|
||||
fprintf(cfg, "input_joypad_driver = \"null\"\n");
|
||||
fprintf(cfg, "menu_driver = \"rgui\"\n");
|
||||
fprintf(cfg, "audio_enable = \"true\"\n");
|
||||
fprintf(cfg, "log_verbosity = \"true\"\n");
|
||||
fprintf(cfg, "audio_out_rate = \"%d\"\n", WAV_RATE);
|
||||
fclose(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
if (!retroarch_main_init(rarch_argc, rarch_argv))
|
||||
{
|
||||
fprintf(stderr, "FAIL: retroarch_main_init failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
wav_fill();
|
||||
|
||||
/* A menu-only boot defers audio bring-up; the reinit command runs
|
||||
* the full deinit+init pair and leaves audio initialized, which
|
||||
* is the state the lanes below need. */
|
||||
command_event(CMD_EVENT_AUDIO_REINIT, NULL);
|
||||
CHECK((AUDIO_FLAGS_GET(audio_state_get_ptr())
|
||||
& AUDIO_FLAG_MIXER_INITED) != 0,
|
||||
"fixture: mixer not inited after AUDIO_REINIT (flags 0x%x)",
|
||||
(unsigned)AUDIO_FLAGS_GET(audio_state_get_ptr()));
|
||||
|
||||
/* Lane 1: with audio up, the stream adds and lands in a slot.
|
||||
* The WAV path releases the owner right after conversion, so one
|
||||
* release is the correct count here, not zero. */
|
||||
owner_frees = 0;
|
||||
CHECK(add_one(wav_buf, &slot),
|
||||
"baseline add_stream failed with audio initialized");
|
||||
CHECK(slot >= 0, "baseline add reported no slot (%d)", slot);
|
||||
CHECK(owner_frees == 1,
|
||||
"WAV conversion owner release: %u frees, want 1",
|
||||
owner_frees);
|
||||
|
||||
/* Lane 2: after teardown, the add is refused and the ownership
|
||||
* contract still holds - this is the retiring-task shape, the
|
||||
* callback arriving after audio_driver_deinit(). */
|
||||
audio_driver_deinit();
|
||||
|
||||
owner_frees = 0;
|
||||
slot = -2;
|
||||
CHECK(!add_one(wav_buf, &slot),
|
||||
"add_stream accepted a stream into the torn-down mixer");
|
||||
CHECK(owner_frees == 1,
|
||||
"refused add released the owner %u times, want exactly 1",
|
||||
owner_frees);
|
||||
CHECK(slot == -1,
|
||||
"refused add left out_slot at %d, want -1", slot);
|
||||
|
||||
/* Lane 3: after a reinit, the window is open again. */
|
||||
command_event(CMD_EVENT_AUDIO_REINIT, NULL);
|
||||
|
||||
owner_frees = 0;
|
||||
slot = -2;
|
||||
CHECK(add_one(wav_buf, &slot),
|
||||
"add_stream still refused after CMD_EVENT_AUDIO_REINIT");
|
||||
CHECK(slot >= 0, "post-reinit add reported no slot (%d)", slot);
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", fixture_dir);
|
||||
if (system(cmd) != 0) { /* fixture dir left behind; harmless */ }
|
||||
|
||||
if (failures)
|
||||
{
|
||||
fprintf(stderr, "FAILURES (%u)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("mixer_retirement: all lanes passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user