oss: RetroFW writes whole 16-byte DMA units

RetroFW's jz4760 OSS driver moves a node to the codec in 16-byte DMA
units: it drops a write's remainder, and a write under one unit starts
a transfer of none. The threaded pipeline writes any number of frames,
so most writes lost one to three frames - static at its rate of writes -
and a short one could leave the device playing nothing it was given.
On RetroFW a write now goes out in whole units, its remainder held to
lead the next one.

samples/audio/oss_report models that DMA; built with RETROFW=1, which
CI now does, it checks every node is whole units and plays in order.

Fixes #20006
This commit is contained in:
libretroadmin committed 2026-11-07 19:06:01 +01:00
1 parent 391dbc91b1
commit 7e92aa944e
4 files changed
+136 -2

No files matched your search

@@ -633,6 +633,10 @@ jobs:
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./oss_report_test
echo "[pass] oss_report_test (ASan)"
# RetroFW's jz4760 driver plays whole 16-byte DMA units only.
make clean && make -j$(nproc) all RETROFW=1 SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./oss_report_test
echo "[pass] oss_report_test (RetroFW, ASan)"
- name: Build and run live_drivers_test (a live PulseAudio server, a null ALSA PCM)
shell: bash
+71
View File
@@ -76,6 +76,15 @@ typedef struct oss_audio
* wait for room is the blocking write's, in the driver, on its own
* fragment-done interrupt. */
bool select_blind;
#ifdef RETROFW
/* jz4760's DMA moves 16-byte units: it drops a write's remainder,
* and a write under one unit starts a transfer of none. Writes go
* out in whole units; the remainder leads the next one. */
uint8_t *units;
size_t units_bytes;
uint8_t carry[16];
size_t carry_bytes;
#endif
} oss_audio_t;
#define OSS_FRAME_BYTES (ossaudio->channels * sizeof(int16_t))
@@ -84,6 +93,59 @@ typedef struct oss_audio
* space, so one call costs at most this many bounded waits. */
#define OSS_WAIT_WRITABLE_LAPS 8
#ifdef RETROFW
#define OSS_DMA_UNIT 16
/* @len bytes behind the carry, whole units to the device. Returns the
* bytes of @s taken, the remainder held among them. */
static ssize_t oss_write_units(oss_audio_t *ossaudio,
const uint8_t *s, size_t len)
{
ssize_t w;
size_t k = ossaudio->carry_bytes;
size_t total = k + len;
size_t whole = total & ~(size_t)(OSS_DMA_UNIT - 1);
const uint8_t *out = s;
if (!whole)
{
memcpy(ossaudio->carry + k, s, len);
ossaudio->carry_bytes = total;
return (ssize_t)len;
}
if (k)
{
if (ossaudio->units_bytes < whole)
{
uint8_t *n = (uint8_t*)realloc(ossaudio->units, whole);
if (!n)
return -1;
ossaudio->units = n;
ossaudio->units_bytes = whole;
}
memcpy(ossaudio->units, ossaudio->carry, k);
memcpy(ossaudio->units + k, s, whole - k);
out = ossaudio->units;
}
if ((w = write(ossaudio->fd, out, whole)) < 0)
{
if (errno == EAGAIN && (fcntl(ossaudio->fd, F_GETFL) & O_NONBLOCK))
return 0;
return -1;
}
if (!w)
return 0;
ossaudio->frames_written += (uint64_t)w / OSS_FRAME_BYTES;
ossaudio->carry_bytes = 0;
/* Short, the rest stays with the caller; whole units always. */
if ((size_t)w < whole)
return w - (ssize_t)k;
memcpy(ossaudio->carry, s + (whole - k), total - whole);
ossaudio->carry_bytes = total - whole;
return (ssize_t)len;
}
#endif
static void *oss_init(const char *device,
unsigned rate, unsigned latency,
@@ -212,6 +274,9 @@ static ssize_t oss_write(void *data, const void *s, size_t len)
s = ossaudio->stage;
len = frames * OSS_FRAME_BYTES;
}
#ifdef RETROFW
return oss_write_units(ossaudio, (const uint8_t*)s, len);
#endif
if ((_len = write(ossaudio->fd, s, len)) < 0)
{
if (errno == EAGAIN && (fcntl(ossaudio->fd, F_GETFL) & O_NONBLOCK))
@@ -327,6 +392,9 @@ static bool oss_stop(void *data)
#endif
ossaudio->is_paused = true;
#ifdef RETROFW
ossaudio->carry_bytes = 0;
#endif
return true;
}
@@ -377,6 +445,9 @@ static void oss_free(void *data)
"its blocking write did.\n");
close(ossaudio->fd);
free(ossaudio->stage);
#ifdef RETROFW
free(ossaudio->units);
#endif
free(data);
}
+4
View File
@@ -12,6 +12,10 @@ LDFLAGS += -lm
# ioctl, select, write and close on the modelled device's descriptor
# are the test's; every other descriptor goes to the real call.
LDFLAGS += -Wl,--wrap=ioctl,--wrap=select,--wrap=write,--wrap=close
# RetroFW's driver, whose DMA moves whole 16-byte units
ifneq ($(RETROFW),)
CFLAGS += -DRETROFW=1
endif
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer -g1 $(CFLAGS)
+57 -2
View File
@@ -21,6 +21,11 @@
* free() on a device whose SNDCTL_DSP_RESET fails returned before
* closing it, leaking the descriptor, the stage and the driver.
*
* Built with RETROFW=1: that driver's DMA moves 16-byte units, drops a
* node's remainder and starts no transfer for a node under one unit.
* The threaded pipeline writes any number of frames, and every
* remainder dropped was a click - static, at its rate of writes.
*
* The driver is this translation unit; a closed descriptor makes the
* real ioctl fail, and ioctl, select, write and close are wrapped
* (--wrap) so one descriptor is the modelled device. */
@@ -64,6 +69,10 @@ static unsigned dev_write_sleeps;
* that waits for a fragment and times out on a full device (0) */
static int dev_select_blind;
static unsigned dev_closes;
/* What the DMA played, and the nodes it could not play whole */
static uint8_t dev_played[1 << 16];
static size_t dev_played_bytes;
static unsigned dev_cut_nodes;
int __real_ioctl(int fd, unsigned long req, ...);
int __real_select(int n, fd_set *r, fd_set *w, fd_set *e,
@@ -131,14 +140,29 @@ ssize_t __wrap_write(int fd, const void *buf, size_t len)
dev_free_nodes++;
}
dev_free_nodes--;
done += n;
if (n % 16)
dev_cut_nodes++;
n -= n % 16;
if (dev_played_bytes + n <= sizeof(dev_played))
memcpy(dev_played + dev_played_bytes,
(const uint8_t*)buf + done, n);
dev_played_bytes += n;
done += len - done > DEV_FRAGSIZE ? DEV_FRAGSIZE : len - done;
}
return (ssize_t)done;
}
static void dev_close(oss_audio_t *oss)
{
#ifdef RETROFW
free(oss->units);
#endif
memset(oss, 0, sizeof(*oss));
}
static void dev_open(oss_audio_t *oss, int blind)
{
memset(oss, 0, sizeof(*oss));
dev_close(oss);
oss->fd = DEV_FD;
oss->rate = 32000;
oss->channels = 2;
@@ -146,6 +170,8 @@ static void dev_open(oss_audio_t *oss, int blind)
dev_selects = 0;
dev_write_sleeps = 0;
dev_select_blind = blind;
dev_played_bytes = 0;
dev_cut_nodes = 0;
}
int main(void)
@@ -214,6 +240,35 @@ int main(void)
CHECK(dev_selects == 1, "after one bounded wait", dev_selects, 1);
CHECK(!oss.select_blind, "and is not latched as poll-less", 1, 0);
#ifdef RETROFW
/* Writes of any number of frames - one, three, a publish - play
* whole and in order, the last few frames held for the next. */
{
static uint8_t in[16384];
static const size_t frames[] = { 533, 1, 3, 2, 534, 7, 1066, 5 };
size_t i, off = 0;
for (i = 0; i < sizeof(in); i++)
in[i] = (uint8_t)(i * 7 + 3);
dev_open(&oss, 1);
dev_free_nodes = DEV_FRAGS;
for (i = 0; i < sizeof(frames) / sizeof(frames[0]); i++)
{
size_t bytes = frames[i] * 4;
CHECK(oss_write(&oss, in + off, bytes) == (ssize_t)bytes,
"a write is taken whole", i, bytes);
off += bytes;
}
CHECK(dev_cut_nodes == 0, "every node is whole DMA units",
dev_cut_nodes, 0);
CHECK(dev_played_bytes <= off && off - dev_played_bytes < 16,
"all but a remainder under a unit is played",
dev_played_bytes, off);
CHECK(!memcmp(dev_played, in, dev_played_bytes),
"what plays is what was written, in order", 0, 0);
}
#endif
dev_close(&oss);
/* SNDCTL_DSP_RESET fails on the modelled device: free() still closes
* it, and LeakSanitizer sees the stage and the driver freed. */
{