diff --git a/.github/workflows/Linux-samples-audio.yml b/.github/workflows/Linux-samples-audio.yml index ee09ea0d62..75ea6b7e8b 100644 --- a/.github/workflows/Linux-samples-audio.yml +++ b/.github/workflows/Linux-samples-audio.yml @@ -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 diff --git a/audio/drivers/oss.c b/audio/drivers/oss.c index ac3e2372b3..88a9cea3de 100644 --- a/audio/drivers/oss.c +++ b/audio/drivers/oss.c @@ -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); } diff --git a/samples/audio/oss_report/Makefile b/samples/audio/oss_report/Makefile index f790da2eb0..1581cda4e7 100644 --- a/samples/audio/oss_report/Makefile +++ b/samples/audio/oss_report/Makefile @@ -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) diff --git a/samples/audio/oss_report/oss_report_test.c b/samples/audio/oss_report/oss_report_test.c index 7baac07f1f..fdb1de8de8 100644 --- a/samples/audio/oss_report/oss_report_test.c +++ b/samples/audio/oss_report/oss_report_test.c @@ -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. */ {