mirror of
https://github.com/libretro/opera-libretro.git
synced 2026-11-07 04:25:58 -10:00
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.
275 lines
6.1 KiB
Plaintext
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;
|
|
}
|
|
}
|