Files
opera-libretro/opera_lr_dsp_threaded.ic
libretroadmin 85e45d084f Lock-free DSP thread on libretro-common; trim libretro-common
The threaded DSP takes no mutex or condition variable. The emulation
thread publishes a monotonic tick count with a release store and wakes
the DSP thread through a retro_eventcount once per 32 ticks and at
frame end; the DSP thread runs every posted tick and writes samples
into a retro_spsc ring that upload hands to the frontend in place.
Mode switches keep samples in order, and the thread runs out every
posted tick before it exits. THREADED_DSP defines HAVE_THREADS and
builds the eventcount and SPSC sources.

libretro-common is synced with RetroArch master and holds only the
sources the core builds and the headers they and the core include;
rstrtod.c is built since file_stream needs it.
2026-10-08 07:42:18 +02:00

275 lines
6.1 KiB
Plaintext

#include "libopera/opera_dsp.h"
#include "opera_lr_callbacks.h"
#include "opera_lr_dsp.h"
#include "boolean.h"
#include <retro_atomic.h>
#include <retro_spsc.h>
#include <rthreads/rthreads.h>
#include <rthreads/retro_eventcount.h>
#include <stdint.h>
#include <string.h>
/* FORWARD DECLARATIONS */
static void dsp_process(void);
static void dsp_upload(void);
/* MACROS */
#define DSP_BUF_SIZE (1 << 12)
#define DSP_BUF_SIZE_MASK (DSP_BUF_SIZE - 1)
#define DSP_FRAME_BYTES (sizeof(int32_t))
#ifndef DSP_WAKE_BATCH
#define DSP_WAKE_BATCH 32
#endif
/* GLOBAL VARIABLES */
static bool g_DSP_THREADED = false;
static uint32_t g_DSP_BUF_IDX = 0;
static int32_t g_DSP_BUF[DSP_BUF_SIZE];
/* Threaded mode, all lock-free:
* - g_DSP_TICKS: monotonic count of DSP ticks posted. Written only by
* the emulation thread, read by the DSP thread.
* - g_DSP_RING: SPSC sample ring, DSP thread -> emulation thread.
* - g_DSP_EC: eventcount the DSP thread parks on when idle. */
static sthread_t *g_DSP_THREAD = NULL;
static retro_spsc_t g_DSP_RING;
static retro_eventcount_t g_DSP_EC;
static retro_atomic_size_t g_DSP_TICKS;
static retro_atomic_int_t g_DSP_RUNNING;
static size_t g_DSP_TICKS_POSTED = 0;
static size_t g_DSP_TICKS_WOKEN = 0;
static void (*g_DSP_UPLOAD)(void) = dsp_upload;
static void (*g_DSP_PROCESS)(void) = dsp_process;
/* PRIVATE FUNCTIONS */
/* Runs 'count_' DSP ticks, publishing samples to the ring. Samples
* that do not fit are dropped; the ticks still run so the DSP keeps
* time with the CPU. */
static
void
dsp_thread_run_ticks(size_t count_)
{
size_t i;
size_t n;
size_t span;
int32_t *dst;
void *ptr;
while(count_ > 0)
{
span = retro_spsc_write_begin(&g_DSP_RING,&ptr) / DSP_FRAME_BYTES;
if(span == 0)
{
opera_dsp_loop();
count_--;
continue;
}
n = ((span < count_) ? span : count_);
dst = (int32_t*)ptr;
for(i = 0; i < n; i++)
dst[i] = opera_dsp_loop();
retro_spsc_write_end(&g_DSP_RING,n * DSP_FRAME_BYTES);
count_ -= n;
}
}
static
void
dsp_thread_loop(void *handle_)
{
int key;
size_t posted;
size_t done = 0;
(void)handle_;
for(;;)
{
posted = retro_atomic_load_acquire_size(&g_DSP_TICKS);
if(posted != done)
{
dsp_thread_run_ticks(posted - done);
done = posted;
continue;
}
/* Stop only once every posted tick has run. */
if(!retro_atomic_load_acquire_int(&g_DSP_RUNNING))
break;
key = retro_eventcount_prepare_wait(&g_DSP_EC);
if((retro_atomic_load_acquire_size(&g_DSP_TICKS) != done) ||
!retro_atomic_load_acquire_int(&g_DSP_RUNNING))
{
retro_eventcount_cancel_wait(&g_DSP_EC);
continue;
}
retro_eventcount_commit_wait(&g_DSP_EC,key);
}
}
static
void
dsp_upload(void)
{
retro_audio_sample_batch_cb((int16_t*)g_DSP_BUF,g_DSP_BUF_IDX);
g_DSP_BUF_IDX = 0;
}
/* Hands the ring's contents straight to the frontend, no copy. */
static
void
dsp_upload_threaded(void)
{
size_t span;
const void *ptr;
/* Wake the DSP thread for this frame's last partial batch. */
if(g_DSP_TICKS_WOKEN != g_DSP_TICKS_POSTED)
{
g_DSP_TICKS_WOKEN = g_DSP_TICKS_POSTED;
retro_eventcount_notify(&g_DSP_EC);
}
/* Samples made before switching to threaded mode go out first. */
if(g_DSP_BUF_IDX)
dsp_upload();
while((span = retro_spsc_read_begin(&g_DSP_RING,&ptr)) != 0)
{
retro_audio_sample_batch_cb((const int16_t*)ptr,span / DSP_FRAME_BYTES);
retro_spsc_read_end(&g_DSP_RING,span);
}
}
static
void
dsp_process(void)
{
g_DSP_BUF[g_DSP_BUF_IDX++] = opera_dsp_loop();
g_DSP_BUF_IDX &= DSP_BUF_SIZE_MASK;
}
static
void
dsp_process_threaded(void)
{
retro_atomic_store_release_size(&g_DSP_TICKS,++g_DSP_TICKS_POSTED);
/* Wake per batch, not per tick: a tick is far cheaper than a wake. */
if((g_DSP_TICKS_POSTED - g_DSP_TICKS_WOKEN) >= DSP_WAKE_BATCH)
{
g_DSP_TICKS_WOKEN = g_DSP_TICKS_POSTED;
retro_eventcount_notify(&g_DSP_EC);
}
}
static
bool
dsp_thread_start(void)
{
if(!retro_spsc_init(&g_DSP_RING,DSP_BUF_SIZE * DSP_FRAME_BYTES))
return false;
if(!retro_eventcount_init(&g_DSP_EC))
{
retro_spsc_free(&g_DSP_RING);
return false;
}
g_DSP_TICKS_POSTED = 0;
g_DSP_TICKS_WOKEN = 0;
retro_atomic_size_init(&g_DSP_TICKS,0);
retro_atomic_int_init(&g_DSP_RUNNING,1);
g_DSP_THREAD = sthread_create(dsp_thread_loop,NULL);
if(!g_DSP_THREAD)
{
retro_eventcount_free(&g_DSP_EC);
retro_spsc_free(&g_DSP_RING);
return false;
}
return true;
}
/* PUBLIC FUNCTIONS */
void
opera_lr_dsp_process(void)
{
(*g_DSP_PROCESS)();
}
void
opera_lr_dsp_upload(void)
{
(*g_DSP_UPLOAD)();
}
void
opera_lr_dsp_destroy(void)
{
size_t room;
size_t span;
const void *ptr;
if(!g_DSP_THREADED)
return;
retro_atomic_store_release_int(&g_DSP_RUNNING,0);
retro_eventcount_notify(&g_DSP_EC);
sthread_join(g_DSP_THREAD);
g_DSP_THREAD = NULL;
/* Carry samples not yet uploaded over to the unthreaded buffer,
* leaving it room for the next frame. */
while((span = retro_spsc_read_begin(&g_DSP_RING,&ptr)) != 0)
{
room = ((g_DSP_BUF_IDX < (DSP_BUF_SIZE / 2)) ?
((DSP_BUF_SIZE / 2) - g_DSP_BUF_IDX) : 0);
span /= DSP_FRAME_BYTES;
if(span > room)
span = room;
if(span == 0)
break;
memcpy(&g_DSP_BUF[g_DSP_BUF_IDX],ptr,span * DSP_FRAME_BYTES);
g_DSP_BUF_IDX += (uint32_t)span;
retro_spsc_read_end(&g_DSP_RING,span * DSP_FRAME_BYTES);
}
retro_eventcount_free(&g_DSP_EC);
retro_spsc_free(&g_DSP_RING);
g_DSP_UPLOAD = dsp_upload;
g_DSP_PROCESS = dsp_process;
g_DSP_THREADED = false;
}
void
opera_lr_dsp_init(const bool threaded_)
{
if(g_DSP_THREADED == threaded_)
return;
opera_lr_dsp_destroy();
if(threaded_ && dsp_thread_start())
{
g_DSP_THREADED = true;
g_DSP_UPLOAD = dsp_upload_threaded;
g_DSP_PROCESS = dsp_process_threaded;
}
}