mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 08:10:30 -10:00
Audio and frame pacing: rate control on the threaded pipeline, sink rate estimation, the gap limiter, and driver latency fixes (#19515)
Audio and frame pacing: rate control on the threaded pipeline, sink rate estimation, the gap limiter, and driver latency fixes- #19515 Read this for full description https://github.com/libretro/RetroArch/pull/19515
This commit is contained in:
1 parent
4b460947b4
commit
e0acbe2639
22 files changed
+2870
-755
No files matched your search
@@ -173,6 +173,70 @@ jobs:
|
||||
TSAN_OPTIONS=halt_on_error=1 timeout 400 ./pipeline_stall_test
|
||||
echo "[pass] pipeline_stall_test (TSan)"
|
||||
|
||||
- name: Build and run pipeline_rate_control_test (ASan + UBSan, TSan)
|
||||
shell: bash
|
||||
working-directory: samples/audio/pipeline_rate_control
|
||||
run: |
|
||||
set -eu
|
||||
# Includes audio/audio_driver.c and runs the threaded pipeline's
|
||||
# shipping producer, consumer and rate controller against a
|
||||
# device whose 8 ms buffer drains at exactly 48000 frames a
|
||||
# second of wall-clock time, fed one 800-frame video frame every
|
||||
# 1/60 s on an absolute schedule, audio sync off. The clocks
|
||||
# match, so the right ratio is 1.0: after a four-second settle
|
||||
# the mean rate adjustment over three seconds must be within
|
||||
# 500 ppm of it, and the device must have taken 99.5% of what
|
||||
# was produced. Rate control on the pipeline samples pipe and
|
||||
# device together, at the producer; reading the device alone,
|
||||
# which the consumer holds between half and full, pins the
|
||||
# ratio at a bound and the pipe overflows behind it. Real time,
|
||||
# so a loaded runner can shift the mean; the tolerance is ten
|
||||
# times the settled residual.
|
||||
make clean all SANITIZER=address,undefined
|
||||
test -x pipeline_rate_control_test
|
||||
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
|
||||
./pipeline_rate_control_test
|
||||
echo "[pass] pipeline_rate_control_test (ASan)"
|
||||
# A core late by 60 ms every 2 s, audio sync off: the silence is
|
||||
# the stalls' own and no more, the late audio is not kept, and
|
||||
# rate control is not pinned against a backlog. And with sync on:
|
||||
# the writer waits, nothing is dropped.
|
||||
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 12 4 jitter
|
||||
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 12 4 sync-jitter
|
||||
echo "[pass] pipeline_rate_control_test (late core, ASan)"
|
||||
# The 64 ms default buffer, sync off, a steady core and a late one:
|
||||
# the consumer's chunks are whole publishes and the pipe's fill
|
||||
# swings by one with the phase; nothing is dropped from a steady
|
||||
# core at any phase.
|
||||
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 12 4 big
|
||||
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 12 4 big-jitter
|
||||
echo "[pass] pipeline_rate_control_test (64 ms device, ASan)"
|
||||
make clean all SANITIZER=thread
|
||||
TSAN_OPTIONS=halt_on_error=1 timeout 240 ./pipeline_rate_control_test
|
||||
echo "[pass] pipeline_rate_control_test (TSan)"
|
||||
|
||||
- name: Build and run ff_speedup_test (ASan + UBSan, TSan)
|
||||
shell: bash
|
||||
working-directory: samples/audio/ff_speedup
|
||||
run: |
|
||||
set -eu
|
||||
# Includes audio/audio_driver.c with the clock replaced by a
|
||||
# counter. The fast-forward speedup multiplier must follow the
|
||||
# core's cadence on both pipelines: measured at the flush inline,
|
||||
# and by the producer at its publish cadence on the threaded
|
||||
# pipeline, where the consumer's own flush interval is the device
|
||||
# draining the previous chunk and would only confirm the previous
|
||||
# multiplier. Re-entry after an idle spell must start from unity
|
||||
# rather than read the idle time as one flush interval.
|
||||
make clean all SANITIZER=address,undefined
|
||||
test -x ff_speedup_test
|
||||
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
||||
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 ./ff_speedup_test
|
||||
echo "[pass] ff_speedup_test (ASan)"
|
||||
make clean all SANITIZER=thread
|
||||
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./ff_speedup_test
|
||||
echo "[pass] ff_speedup_test (TSan)"
|
||||
|
||||
- name: Build and run device_list_driver_test (ASan + UBSan)
|
||||
shell: bash
|
||||
working-directory: samples/audio/device_list_driver
|
||||
|
||||
+517
-345
File diff suppressed because it is too large.
Load diff
+96
-26
@@ -315,8 +315,31 @@ typedef struct audio_driver
|
||||
* without the estimate, as before.
|
||||
*/
|
||||
size_t (*frames_consumed)(void *data);
|
||||
|
||||
/* Optional. Periods the device played silence for want of audio
|
||||
* since init: the callback found less than one period in the
|
||||
* buffer and zero-filled it. Counted where it happens, one atomic
|
||||
* add on that path only; never logged from there. Read by the
|
||||
* frontend for the statistics overlay each frame, and once at
|
||||
* driver teardown for the log. NULL when the driver cannot tell. */
|
||||
size_t (*underruns)(void *data);
|
||||
} audio_driver_t;
|
||||
|
||||
/* A snapshot of the sink estimate's counts, taken when a window opens. */
|
||||
typedef struct
|
||||
{
|
||||
double offered; /* sink_offered */
|
||||
uint64_t consumed; /* frames_consumed() */
|
||||
} audio_sink_mark_t;
|
||||
|
||||
/* A sum of windows: their time, and what the source and the device did. */
|
||||
typedef struct
|
||||
{
|
||||
int64_t usec;
|
||||
double offered;
|
||||
double consumed;
|
||||
} audio_sink_sum_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
double src_ratio_orig;
|
||||
@@ -472,11 +495,43 @@ typedef struct
|
||||
/* Upper bound on input samples per consumer pass: one video frame's
|
||||
* worth, capped to a slice. */
|
||||
size_t pipe_pass_int16s;
|
||||
/* Rate control's fill on the threaded pipeline, as free space in
|
||||
* device bytes, sampled by the producer before each publish and
|
||||
* read by the consumer's controller; -1 before the first sample.
|
||||
*
|
||||
* The device's own fill cannot be the control variable here: the
|
||||
* consumer waits for half the device's buffer and writes half, so
|
||||
* that fill sits between half and full whatever the clocks do, and a
|
||||
* controller reading it - at any point of the pass - sees a constant
|
||||
* error and pins the ratio at a bound. What the clocks move is the
|
||||
* pipe ring in front of the device: a production surplus collects
|
||||
* there, a deficit empties it. So the fill is the two together, the
|
||||
* pipe's frames counted at the device's rate, and it is read where
|
||||
* the frame-synchronous path read it, once a frame on the core's
|
||||
* thread before the frame is published - the pipe at its low point,
|
||||
* the device wherever it is. The device's mean free space over the
|
||||
* pass is a quarter of its buffer, the controller's setpoint is
|
||||
* half, and the difference is added so a balanced pipe reads as no
|
||||
* error. */
|
||||
retro_atomic_int_t pipe_ctrl_avail;
|
||||
/* The driver's underrun count as the consumer last saw it; a change
|
||||
* means the device played silence since, and what the pipe holds
|
||||
* past its target is late audio, discarded. Consumer thread only. */
|
||||
size_t pipe_underruns_seen;
|
||||
/* Whether the consumer's next pass is its first: it waits for the
|
||||
* pipe's target then, not just a frame. Consumer thread only after
|
||||
* init. */
|
||||
bool pipe_priming;
|
||||
/* The audio thread's own copy of AUDIO_FLAG_PIPELINE_THREADED. Set
|
||||
* before the wrapper thread is released and cleared after it is
|
||||
* joined, so the thread never reads the flags word - which the main
|
||||
* thread read-modify-writes at will - just to know it is running. */
|
||||
bool pipe_threaded;
|
||||
/* The fast-forward speedup multiplier, measured by the producer on
|
||||
* the core's thread and read by the consumer, as a Q16 fixed-point
|
||||
* value. The consumer's own cadence is the device's, so it cannot
|
||||
* measure how fast the core is running; only the producer can. */
|
||||
retro_atomic_int_t pipe_ff_mult_q16;
|
||||
#ifdef HAVE_REWIND
|
||||
size_t rewind_ptr;
|
||||
size_t rewind_size;
|
||||
@@ -518,7 +573,11 @@ typedef struct
|
||||
|
||||
char resampler_ident[64];
|
||||
|
||||
bool reinit_request;
|
||||
/* A driver's request to be reinitialised - a device change or an
|
||||
* unplug, set from its notification thread. Taken by the runloop
|
||||
* once a frame on the main thread with no audio lock held; see
|
||||
* audio_driver_take_reinit_request(). */
|
||||
retro_atomic_int_t reinit_request;
|
||||
bool mute_enable;
|
||||
#ifdef HAVE_AUDIOMIXER
|
||||
bool mixer_mute_enable;
|
||||
@@ -556,36 +615,35 @@ typedef struct
|
||||
/* Sink rate estimation: see audio_driver_sink_update(). Counted
|
||||
* where the driver's write() is called, on the thread that flushes;
|
||||
* the estimate runs there too. */
|
||||
double sink_offered; /* output frames offered to the driver, each
|
||||
divided by the bias in force when it was, so
|
||||
the sum is what would have been offered
|
||||
unbiased: the source's rate on the host clock */
|
||||
uint64_t sink_accepted; /* output frames the driver took */
|
||||
double sink_offered; /* the source's count, in frames at the nominal
|
||||
ratio: what the core published into the
|
||||
pipeline's ring, or off it what was offered
|
||||
with the ratio in force divided out. Owned
|
||||
by the thread that closes the windows. */
|
||||
uint64_t sink_offered_raw; /* output frames offered, as offered */
|
||||
double sink_offered_at; /* the three at the baseline's start */
|
||||
uint64_t sink_accepted_at;
|
||||
uint64_t sink_offered_raw_at;
|
||||
uint64_t sink_consumed_at; /* frames_consumed() at the baseline's start */
|
||||
int64_t sink_baseline_start; /* usec; 0 = not started */
|
||||
int64_t sink_check_at; /* usec; the next plausibility check */
|
||||
uint64_t sink_accepted; /* output frames the driver took */
|
||||
/* The estimate: windows of a few seconds, each read as a change in
|
||||
* the counts above and in the device's consumption, kept when it
|
||||
* measures the clocks and summed; the bias is the summed ratio.
|
||||
* See audio_driver_sink_update(). */
|
||||
int64_t sink_started; /* usec; 0 = not started */
|
||||
int64_t sink_window_at; /* usec; when the open window closes */
|
||||
int64_t sink_apply_at; /* usec; the next setting of the bias */
|
||||
double sink_check_offered; /* offered and consumed at the last check */
|
||||
uint64_t sink_check_consumed;
|
||||
int64_t sink_sum_usec; /* the windows kept: time, offered, consumed */
|
||||
double sink_sum_offered;
|
||||
double sink_sum_consumed;
|
||||
audio_sink_mark_t sink_at_window; /* the counts when the open window opened */
|
||||
audio_sink_sum_t sink_kept; /* the windows summed for the bias */
|
||||
audio_sink_sum_t sink_pending; /* windows since the last kept one, for the rates shown meanwhile */
|
||||
unsigned sink_settled; /* kept windows in a row, up to 2, after which the sums stand */
|
||||
unsigned sink_applied; /* times the bias has been set */
|
||||
unsigned sink_discarded; /* windows left out in a row */
|
||||
unsigned sink_warned; /* AUDIO_SINK_WARNED_* said once each */
|
||||
double sink_bias; /* multiplied into the ratio; 1.0 = none */
|
||||
/* The bias for the thread that resamples, in hundredths of a part
|
||||
* per million: on the threaded pipeline the estimate runs on the
|
||||
* core's thread and the resampler on the audio thread, and a double
|
||||
* is not a single word. */
|
||||
retro_atomic_int_t sink_bias_q;
|
||||
double sink_rate_hz; /* the device's rate as measured; 0 = unknown */
|
||||
double sink_source_hz; /* the source's rate at the nominal ratio, as measured */
|
||||
double sink_adjust_sum; /* DRC adjusts over the baseline, for the mean */
|
||||
unsigned sink_adjust_n;
|
||||
unsigned sink_applied; /* times the bias has been set from a baseline */
|
||||
unsigned sink_discarded; /* windows discarded in a row */
|
||||
bool sink_drop_warned;
|
||||
/* Said once when a measured ratio is too far off to be a crystal. */
|
||||
bool sink_implausible_warned;
|
||||
/* Said once when the buffer empties faster than corrections arrive. */
|
||||
bool sink_too_slow_warned;
|
||||
size_t samples_since_drc; /* int16 samples submitted since last update */
|
||||
size_t drc_threshold_int16s; /* one frame's worth of stereo int16 at the current rate */
|
||||
/* Set by audio_driver_frame_end() so the next flush recomputes the
|
||||
@@ -922,6 +980,18 @@ void audio_driver_update_drc_threshold(audio_driver_state_t *audio_st);
|
||||
|
||||
const char *audio_driver_get_ident(void);
|
||||
|
||||
/* Periods the device played silence for want of audio since the driver
|
||||
* was initialised, where the driver counts them; 0 otherwise. */
|
||||
size_t audio_driver_get_underruns(void);
|
||||
|
||||
/* Whether a driver has asked to be reinitialised since the last call;
|
||||
* clears the request. The runloop calls this once a frame, on the main
|
||||
* thread, holding no audio lock, and acts on it there: the reinit
|
||||
* tears the driver down and up, which frees the state lock and, on
|
||||
* the threaded pipeline, joins the audio thread, so it can be run
|
||||
* neither under the lock nor from that thread. */
|
||||
bool audio_driver_take_reinit_request(void);
|
||||
|
||||
double audio_driver_get_buffer_latency_ms(void);
|
||||
|
||||
/* The device stage behind the buffer in ms, as set by
|
||||
|
||||
@@ -374,6 +374,14 @@ static size_t audio_thread_wait_writable(void *data, size_t len)
|
||||
/* The wrapped driver's count, for the sink rate estimate: without this
|
||||
* the frontend saw the wrapper's NULL and never measured under the
|
||||
* threaded pipeline - which is where every reporter runs. */
|
||||
static size_t audio_thread_underruns(void *data)
|
||||
{
|
||||
audio_thread_t *thr = (audio_thread_t*)data;
|
||||
if (!thr || !thr->driver->underruns || !thr->driver_data)
|
||||
return 0;
|
||||
return thr->driver->underruns(thr->driver_data);
|
||||
}
|
||||
|
||||
static size_t audio_thread_frames_consumed(void *data)
|
||||
{
|
||||
audio_thread_t *thr = (audio_thread_t*)data;
|
||||
@@ -459,7 +467,8 @@ static const audio_driver_t audio_thread = {
|
||||
audio_thread_buffer_size,
|
||||
NULL, /* write_raw */
|
||||
audio_thread_wait_writable,
|
||||
audio_thread_frames_consumed
|
||||
audio_thread_frames_consumed,
|
||||
audio_thread_underruns
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -270,7 +270,7 @@ DWORD CALLBACK mmdevice_thread(PVOID data)
|
||||
{
|
||||
case WM_AUDIO_DEVICE_STATE_CHANGED:
|
||||
case WM_AUDIO_DEFAULT_CHANGED:
|
||||
audio_st->reinit_request = true;
|
||||
retro_atomic_store_release_int(&audio_st->reinit_request, 1);
|
||||
goto done;
|
||||
case WM_QUIT:
|
||||
goto done;
|
||||
|
||||
+15
-2
@@ -684,6 +684,9 @@ typedef struct ra_asio
|
||||
/* Whether the last period ended in silence for want of audio, so
|
||||
* the next audio is faded in. Callback thread only. */
|
||||
bool last_underran;
|
||||
/* Periods that ended in silence for want of audio: one atomic add
|
||||
* on that path, read by the frontend's overlay. */
|
||||
retro_atomic_size_t underruns;
|
||||
/* Frames the device has taken: a period per callback, silence
|
||||
* included. Written by the callback thread, read by the writer; a
|
||||
* single aligned word, and a count only ever compared with itself. */
|
||||
@@ -845,6 +848,8 @@ static void asio_deinterleave_to_buffers(ra_asio_t *ad,
|
||||
asio_convert_frames(ad->sample_type, ad->scratch, have, frames,
|
||||
buf_l, buf_r, ad->last_underran);
|
||||
ad->last_underran = (have < frames);
|
||||
if (have < frames)
|
||||
retro_atomic_fetch_add_size(&ad->underruns, 1);
|
||||
ad->consumed += (size_t)frames;
|
||||
}
|
||||
|
||||
@@ -928,7 +933,7 @@ static long asio_cb_message(long selector, long value,
|
||||
selector == kAsioResetRequest ? "a reset" : "a buffer size change");
|
||||
if (g_asio)
|
||||
retro_atomic_store_release_int(&g_asio->rebuild_pending, 1);
|
||||
audio_state_get_ptr()->reinit_request = true;
|
||||
retro_atomic_store_release_int(&audio_state_get_ptr()->reinit_request, 1);
|
||||
return 1L;
|
||||
case kAsioResyncRequest:
|
||||
/* The driver lost its place - a system pause, a clock that
|
||||
@@ -1338,6 +1343,7 @@ static void *ra_asio_init(const char *device, unsigned rate,
|
||||
ad = (ra_asio_t *)calloc(1, sizeof(ra_asio_t));
|
||||
if (!ad)
|
||||
return NULL;
|
||||
retro_atomic_size_init(&ad->underruns, 0);
|
||||
|
||||
/* Register cleanup for process exit — ensures the parked
|
||||
* instance is properly torn down even if free() only parks it. */
|
||||
@@ -1756,6 +1762,12 @@ static size_t ra_asio_wait_writable(void *data, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
static size_t ra_asio_underruns(void *data)
|
||||
{
|
||||
ra_asio_t *ad = (ra_asio_t*)data;
|
||||
return ad ? retro_atomic_load_acquire_size(&ad->underruns) : 0;
|
||||
}
|
||||
|
||||
static size_t ra_asio_frames_consumed(void *data)
|
||||
{
|
||||
ra_asio_t *ad = (ra_asio_t *)data;
|
||||
@@ -1828,7 +1840,8 @@ audio_driver_t audio_asio = {
|
||||
NULL, /* write_raw — ASIO cannot dynamically adjust sample rate
|
||||
* for A/V sync rate control. Software resampler handles it. */
|
||||
ra_asio_wait_writable,
|
||||
ra_asio_frames_consumed
|
||||
ra_asio_frames_consumed,
|
||||
ra_asio_underruns
|
||||
};
|
||||
|
||||
/* Called from the menu to open the ASIO driver's control panel.
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
|
||||
#include <boolean.h>
|
||||
#include <retro_atomic.h>
|
||||
#include <features/features_cpu.h>
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
@@ -145,8 +144,6 @@ typedef struct coreaudio
|
||||
* power-of-two capacity is only the container for. Free space,
|
||||
* buffer_size() and the wait are counted against this. */
|
||||
size_t usable;
|
||||
/* The HAL IO buffer asked for, in frames; what one pull takes. */
|
||||
unsigned io_frames;
|
||||
size_t write_ptr; /* Only touched by main thread */
|
||||
size_t read_ptr; /* Only touched by audio callback */
|
||||
retro_atomic_size_t filled; /* Samples currently in buffer */
|
||||
@@ -154,12 +151,10 @@ typedef struct coreaudio
|
||||
* unit started. Written only by the callback, read by the frontend
|
||||
* through coreaudio_frames_consumed(). */
|
||||
retro_atomic_size_t consumed;
|
||||
/* Pulls the callback could not fill from the ring. Counted there,
|
||||
* reported from write_avail() on the frontend's thread - at most
|
||||
* once a second while it grows - and in full on free(). */
|
||||
/* Pulls the callback could not fill from the ring: one atomic add
|
||||
* on that path, read by the frontend's overlay, and by it once at
|
||||
* teardown for the log. Never logged from here. */
|
||||
retro_atomic_size_t underruns;
|
||||
size_t underruns_logged;
|
||||
retro_time_t underruns_logged_at;
|
||||
|
||||
/* The output unit: ComponentInstance or AudioComponentInstance,
|
||||
* both of which are this type on every SDK. */
|
||||
@@ -380,8 +375,6 @@ static bool coreaudio_update_converter(coreaudio_t *dev,
|
||||
return true;
|
||||
}
|
||||
|
||||
static void coreaudio_report_underruns(coreaudio_t *dev, bool final);
|
||||
|
||||
static void coreaudio_free(void *data)
|
||||
{
|
||||
coreaudio_t *dev = (coreaudio_t*)data;
|
||||
@@ -392,7 +385,6 @@ static void coreaudio_free(void *data)
|
||||
if (dev->dev_alive)
|
||||
{
|
||||
AudioOutputUnitStop(dev->dev);
|
||||
coreaudio_report_underruns(dev, true);
|
||||
ca_cm.close(dev->dev);
|
||||
}
|
||||
|
||||
@@ -715,7 +707,6 @@ static void *coreaudio_init(const char *device,
|
||||
period = 512;
|
||||
AudioUnitSetProperty(dev->dev, kAudioDevicePropertyBufferFrameSize,
|
||||
kAudioUnitScope_Global, 0, &period, sizeof(period));
|
||||
dev->io_frames = period;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1036,25 +1027,15 @@ static bool coreaudio_start(void *data, bool is_shutdown)
|
||||
|
||||
static bool coreaudio_use_float(void *data) { return true; }
|
||||
|
||||
static void coreaudio_report_underruns(coreaudio_t *dev, bool final)
|
||||
static size_t coreaudio_underruns(void *data)
|
||||
{
|
||||
size_t n = retro_atomic_load_acquire_size(&dev->underruns);
|
||||
retro_time_t now;
|
||||
if (n == dev->underruns_logged)
|
||||
return;
|
||||
now = cpu_features_get_time_usec();
|
||||
if (!final && now - dev->underruns_logged_at < 1000000)
|
||||
return;
|
||||
RARCH_WARN("[CoreAudio] %u underrun%s so far: the callback found less than one pull (%u frames) in the ring.\n",
|
||||
(unsigned)n, n == 1 ? "" : "s", (unsigned)dev->io_frames);
|
||||
dev->underruns_logged = n;
|
||||
dev->underruns_logged_at = now;
|
||||
coreaudio_t *dev = (coreaudio_t*)data;
|
||||
return dev ? retro_atomic_load_acquire_size(&dev->underruns) : 0;
|
||||
}
|
||||
|
||||
static size_t coreaudio_write_avail(void *data)
|
||||
{
|
||||
coreaudio_t *dev = (coreaudio_t*)data;
|
||||
coreaudio_report_underruns(dev, false);
|
||||
return rb_write_avail(dev) * sizeof(float);
|
||||
}
|
||||
|
||||
@@ -1185,7 +1166,8 @@ audio_driver_t audio_coreaudio = {
|
||||
coreaudio_buffer_size,
|
||||
coreaudio_write_raw,
|
||||
coreaudio_wait_writable,
|
||||
coreaudio_frames_consumed
|
||||
coreaudio_frames_consumed,
|
||||
coreaudio_underruns
|
||||
};
|
||||
|
||||
|
||||
|
||||
+13
-3
@@ -98,7 +98,9 @@ typedef struct
|
||||
slock_t *fifo_lock;
|
||||
scond_t *room_cond;
|
||||
retro_atomic_int_t pump_run;
|
||||
unsigned underruns;
|
||||
/* Periods the pump filled with silence for want of audio: one
|
||||
* atomic add on that path, read by the frontend's overlay. */
|
||||
retro_atomic_size_t underruns;
|
||||
/* Frames the device has consumed, kept by the pump under fifo_lock:
|
||||
* exclusive, the periods it released, each taken by the device a
|
||||
* period after; shared, the frames released less the engine's
|
||||
@@ -1390,6 +1392,7 @@ static void *wasapi_init(const char *dev_id, unsigned rate, unsigned latency,
|
||||
if (!w)
|
||||
return NULL;
|
||||
|
||||
retro_atomic_size_init(&w->underruns, 0);
|
||||
if (mmdevice_com_init())
|
||||
w->flags |= WASAPI_FLG_COM;
|
||||
w->device = (IMMDevice*)mmdevice_init_device(dev_id, 0 /* eRender */);
|
||||
@@ -1700,7 +1703,7 @@ static void wasapi_pump_thread(void *data)
|
||||
{
|
||||
memset(dest, 0, w->engine_buffer_size);
|
||||
flags = AUDCLNT_BUFFERFLAGS_SILENT;
|
||||
w->underruns++;
|
||||
retro_atomic_fetch_add_size(&w->underruns, 1);
|
||||
}
|
||||
/* Each period released is one the device takes; silence
|
||||
* counts too, the device's clock does not stop for it. */
|
||||
@@ -2155,6 +2158,12 @@ static size_t wasapi_wait_writable(void *wh, size_t len)
|
||||
return wasapi_write_avail(w);
|
||||
}
|
||||
|
||||
static size_t wasapi_underruns(void *wh)
|
||||
{
|
||||
wasapi_t *w = (wasapi_t*)wh;
|
||||
return w ? retro_atomic_load_acquire_size(&w->underruns) : 0;
|
||||
}
|
||||
|
||||
static size_t wasapi_frames_consumed(void *wh)
|
||||
{
|
||||
#ifdef HAVE_THREADS
|
||||
@@ -2188,5 +2197,6 @@ audio_driver_t audio_wasapi = {
|
||||
wasapi_buffer_size,
|
||||
NULL, /* write_raw */
|
||||
wasapi_wait_writable,
|
||||
wasapi_frames_consumed
|
||||
wasapi_frames_consumed,
|
||||
wasapi_underruns
|
||||
};
|
||||
@@ -5794,6 +5794,14 @@ void video_driver_frame(const void *data, unsigned width,
|
||||
audio_stats.close_to_blocking,
|
||||
audio_stats.samples);
|
||||
|
||||
/* Periods the device played silence for want of audio, from
|
||||
* the driver's own count where it keeps one: the number that
|
||||
* says whether a stutter was heard, against the percentages
|
||||
* above that say how near the buffer came. */
|
||||
if (audio_st->current_audio && audio_st->current_audio->underruns)
|
||||
__len += snprintf(video_info.stat_text + __len, sizeof(video_info.stat_text) - __len,
|
||||
" Dropouts: %8u\n", (unsigned)audio_driver_get_underruns());
|
||||
|
||||
__len += strlcpy_lit(video_info.stat_text + __len, "LATENCY\n",
|
||||
sizeof(video_info.stat_text) - __len);
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ void retro_sleep(unsigned msec);
|
||||
#endif
|
||||
#elif defined(_WIN32)
|
||||
#define retro_sleep(msec) (Sleep((msec)))
|
||||
#elif defined(__APPLE__) && defined(__MACH__)
|
||||
/* Darwin: the same Mach-clock deadline retro_sleep_us() takes. */
|
||||
#define retro_sleep(msec) (retro_sleep_us(1000 * (msec)))
|
||||
#elif defined(XENON)
|
||||
#define retro_sleep(msec) (udelay(1000 * (msec)))
|
||||
#elif !defined(__PSL1GHT__) && defined(__PS3__)
|
||||
@@ -155,10 +158,12 @@ static INLINE void retro_sleep(unsigned msec)
|
||||
#define retro_sleep_us(usec) (svcSleepThread(1000 * (s64)(usec)))
|
||||
#elif defined(__WINRT__) || (defined(WINAPI_FAMILY) && defined(WINAPI_FAMILY_PHONE_APP) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP)
|
||||
#define retro_sleep_us(usec) (SleepEx(((usec) + 500) / 1000, FALSE))
|
||||
#elif defined(_WIN32) && !defined(_XBOX)
|
||||
/* Desktop Windows: see the note on retro_sleep() above. Implemented in
|
||||
* libretro-common/time/rtime.c, where it is the primitive that
|
||||
* retro_sleep() itself is expressed in terms of. */
|
||||
#elif (defined(_WIN32) && !defined(_XBOX)) || (defined(__APPLE__) && defined(__MACH__))
|
||||
/* Desktop Windows: see the note on retro_sleep() above. Darwin: an
|
||||
* absolute deadline on the Mach clock through mach_wait_until(), not a
|
||||
* relative nanosleep(). Both implemented in libretro-common/time/rtime.c,
|
||||
* where on Windows it is the primitive that retro_sleep() itself is
|
||||
* expressed in terms of. */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
@@ -36,6 +36,44 @@
|
||||
slock_t *rtime_localtime_lock = NULL;
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
#include <mach/mach_time.h>
|
||||
#include <sched.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Darwin: the sleep is an absolute deadline on the Mach clock, the
|
||||
* same clock cpu_features_get_time_usec() reads there, so the time
|
||||
* spent between reading it and entering the kernel is not added to the
|
||||
* wait as it is by a relative nanosleep(). mach_wait_until() and the
|
||||
* timebase have been in libSystem since 10.0. The kernel may still
|
||||
* hold a normal-priority thread past the deadline by its timer leeway;
|
||||
* what returns is never early, and a caller that needs the deadline
|
||||
* itself sleeps short and spins the rest. */
|
||||
static mach_timebase_info_data_t rtime_mach_tb;
|
||||
|
||||
void retro_sleep_us(unsigned usec)
|
||||
{
|
||||
uint64_t ticks;
|
||||
|
||||
/* A zero duration means "yield the rest of this time slice". */
|
||||
if (!usec)
|
||||
{
|
||||
sched_yield();
|
||||
return;
|
||||
}
|
||||
|
||||
/* The timebase is a constant; a racing first read fills it with
|
||||
* the same values, so no guard is needed. */
|
||||
if (!rtime_mach_tb.denom)
|
||||
mach_timebase_info(&rtime_mach_tb);
|
||||
|
||||
/* Nanoseconds to Mach ticks; the ratio is 1:1 on Intel and
|
||||
* 125:3 on Apple silicon, so the product fits for any usec. */
|
||||
ticks = (uint64_t)usec * 1000 * rtime_mach_tb.denom / rtime_mach_tb.numer;
|
||||
mach_wait_until(mach_absolute_time() + ticks);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
|
||||
@@ -4917,13 +4917,19 @@ void runloop_set_frame_limit(
|
||||
float fastforward_ratio)
|
||||
{
|
||||
if (fastforward_ratio < 0.1f)
|
||||
runloop_state.frame_limit_minimum_time = 0;
|
||||
{
|
||||
runloop_state.frame_limit_minimum_time = 0;
|
||||
runloop_state.frame_limit_minimum_time_ns = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
float fps = av_info->timing.fps;
|
||||
runloop_state.frame_limit_minimum_time = (fps > 0.0f)
|
||||
? (retro_time_t)roundf(1000000.0f / (fps * fastforward_ratio))
|
||||
: 0;
|
||||
runloop_state.frame_limit_minimum_time_ns = (fps > 0.0f)
|
||||
? (int64_t)(1000000000.0 / ((double)fps * fastforward_ratio))
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5308,6 +5314,7 @@ bool runloop_event_init_core(
|
||||
|
||||
runloop_set_frame_limit(&video_st->av_info, fastforward_ratio);
|
||||
runloop_st->frame_limit_last_time = cpu_features_get_time_usec();
|
||||
runloop_st->frame_limit_anchor_ns = (int64_t)runloop_st->frame_limit_last_time * 1000;
|
||||
|
||||
/* Init runtime log and read current state slot */
|
||||
runloop_runtime_log_init(runloop_st);
|
||||
@@ -5372,6 +5379,10 @@ void runloop_pause_checks(void)
|
||||
((video_st->video_refresh_rate_original)
|
||||
? video_st->video_refresh_rate_original
|
||||
: video_refresh_rate));
|
||||
runloop_st->frame_limit_minimum_time_ns = runloop_content_frame_time_ns(
|
||||
(video_st->video_refresh_rate_original)
|
||||
? video_st->video_refresh_rate_original
|
||||
: video_refresh_rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -7918,6 +7929,7 @@ end:
|
||||
int runloop_iterate(void)
|
||||
{
|
||||
retro_time_t pace_limit_min;
|
||||
int64_t pace_limit_ns;
|
||||
input_driver_state_t *input_st = input_state_get_ptr();
|
||||
audio_driver_state_t *audio_st = audio_state_get_ptr();
|
||||
video_driver_state_t *video_st = video_state_get_ptr();
|
||||
@@ -8052,6 +8064,19 @@ int runloop_iterate(void)
|
||||
audio_buf_active, audio_buf_occupancy, audio_buf_underrun);
|
||||
}
|
||||
|
||||
/* A driver's request to be reinitialised - a device change or an
|
||||
* unplug - is taken here, once a frame, on the main thread with no
|
||||
* audio lock held: the reinit frees the state lock and joins the
|
||||
* audio thread, so it cannot run from a flush or from that thread. */
|
||||
if (audio_driver_take_reinit_request())
|
||||
{
|
||||
RARCH_LOG("[Audio] Driver reinit requested...\n");
|
||||
command_event(CMD_EVENT_AUDIO_REINIT, NULL);
|
||||
#ifdef HAVE_MICROPHONE
|
||||
command_event(CMD_EVENT_MICROPHONE_REINIT, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
switch ((enum runloop_state_enum)runloop_check_state(
|
||||
input_st, audio_st, video_st,
|
||||
uico_st,
|
||||
@@ -8061,6 +8086,7 @@ int runloop_iterate(void)
|
||||
{
|
||||
case RUNLOOP_STATE_QUIT:
|
||||
runloop_st->frame_limit_last_time = 0.0;
|
||||
runloop_st->frame_limit_anchor_ns = 0;
|
||||
runloop_st->flags &= ~RUNLOOP_FLAG_CORE_RUNNING;
|
||||
command_event(CMD_EVENT_QUIT, NULL);
|
||||
return -1;
|
||||
@@ -8161,20 +8187,27 @@ int runloop_iterate(void)
|
||||
/* Make sure no stale frame_limit_minimum_time from a prior
|
||||
* iteration (e.g. just before menu_pause_libretro was toggled
|
||||
* off) leaks into the sleep block below. */
|
||||
runloop_st->frame_limit_minimum_time = 0;
|
||||
runloop_st->frame_limit_minimum_time = 0;
|
||||
runloop_st->frame_limit_minimum_time_ns = 0;
|
||||
goto end;
|
||||
}
|
||||
else if (( (settings->bools.video_vsync)
|
||||
|| (settings->bools.video_scanline_sync && video_st->scanline[SCANLINE_NEXT]))
|
||||
|| (settings->bools.video_scanline_sync))
|
||||
&& (runloop_st->flags & RUNLOOP_FLAG_FOCUSED))
|
||||
goto end;
|
||||
|
||||
/* Otherwise run menu in video refresh rate speed. */
|
||||
if (menu_state_get_ptr()->flags & MENU_ST_FLAG_ALIVE)
|
||||
{
|
||||
runloop_st->frame_limit_minimum_time = (retro_time_t)roundf(1000000.0f /
|
||||
((video_st->video_refresh_rate_original)
|
||||
? video_st->video_refresh_rate_original
|
||||
: settings->floats.video_refresh_rate));
|
||||
runloop_st->frame_limit_minimum_time_ns = runloop_content_frame_time_ns(
|
||||
(video_st->video_refresh_rate_original)
|
||||
? video_st->video_refresh_rate_original
|
||||
: settings->floats.video_refresh_rate);
|
||||
}
|
||||
else
|
||||
runloop_set_frame_limit(&video_st->av_info, settings->floats.fastforward_ratio);
|
||||
#endif
|
||||
@@ -8341,6 +8374,7 @@ end:
|
||||
* fast-forward limit; replaced by the content frame time when
|
||||
* nothing else is pacing at all (see below). */
|
||||
pace_limit_min = runloop_st->frame_limit_minimum_time;
|
||||
pace_limit_ns = runloop_st->frame_limit_minimum_time_ns;
|
||||
/* The vsync bit reads the driver's blocking state, not the setting:
|
||||
* fast-forward (INP_FLAG_NONBLOCKING) and RUNLOOP_FLAG_FORCE_NONBLOCK
|
||||
* both put the driver into non-blocking presentation while the
|
||||
@@ -8420,25 +8454,25 @@ end:
|
||||
* not writing, no scanline lock, and no fast-forward limit to fall
|
||||
* back on - frame_limit_minimum_time is zero whenever the ratio is
|
||||
* "unlimited", which is the default, so the branch above cannot
|
||||
* engage however slowly the core is running. The loop then spins as
|
||||
* fast as the machine allows: a core with a light frame runs at
|
||||
* hundreds of frames a second, burning a core and drowning the
|
||||
* display in frames nobody asked for. It is not fast-forward - the
|
||||
* user did not ask for it - it is the absence of anyone saying when
|
||||
* the next frame is due.
|
||||
*
|
||||
* So the timer takes it, at the content's own rate: 1.0x, the speed
|
||||
* the core is meant to run at, which is what every other pacing
|
||||
* source would have produced. Only when the record is empty, so it
|
||||
* never competes with one that is doing the job, and never under
|
||||
* fast-forward, which is the case where running unthrottled is the
|
||||
* point. */
|
||||
* engage however slowly the core is running. The timer holds the
|
||||
* loop to the display rate, which audio rate control can follow
|
||||
* and which Scanline Sync is aiming at while it recalibrates. See
|
||||
* runloop_pace_gap_engages() for when it stays out. */
|
||||
if (runloop_pace_gap_engages(runloop_st->pace,
|
||||
(input_st->flags & INP_FLAG_NONBLOCKING) != 0,
|
||||
(runloop_st->flags & RUNLOOP_FLAG_FASTMOTION) != 0))
|
||||
(runloop_st->flags & RUNLOOP_FLAG_FASTMOTION) != 0,
|
||||
settings->bools.video_scanline_sync,
|
||||
settings->bools.audio_rate_control))
|
||||
{
|
||||
runloop_st->pace |= RUNLOOP_PACE_TIMER;
|
||||
pace_limit_min = runloop_content_frame_time_us(video_st->core_hz);
|
||||
pace_limit_min = runloop_content_frame_time_us(
|
||||
(video_st->video_refresh_rate_original)
|
||||
? video_st->video_refresh_rate_original
|
||||
: settings->floats.video_refresh_rate);
|
||||
pace_limit_ns = runloop_content_frame_time_ns(
|
||||
(video_st->video_refresh_rate_original)
|
||||
? video_st->video_refresh_rate_original
|
||||
: settings->floats.video_refresh_rate);
|
||||
}
|
||||
|
||||
/* if there's a fast forward limit, inject sleeps to keep from going too fast. */
|
||||
@@ -8446,42 +8480,57 @@ end:
|
||||
retro_time_t frame_limit_min = pace_limit_min;
|
||||
if (runloop_st->pace & RUNLOOP_PACE_TIMER)
|
||||
{
|
||||
const retro_time_t end_frame_time = cpu_features_get_time_usec();
|
||||
const retro_time_t to_sleep_us = (
|
||||
( runloop_st->frame_limit_last_time
|
||||
+ frame_limit_min)
|
||||
- end_frame_time);
|
||||
#if defined(__EMSCRIPTEN__) && !defined(EMSCRIPTEN_ASYNCIFY) && !defined(PROXY_TO_PTHREAD)
|
||||
/* Emscripten paces through a deferred main loop timeout that
|
||||
* is expressed in whole milliseconds, so it cannot act on a
|
||||
* sub-millisecond remainder. Keep the old truncation there. */
|
||||
const retro_time_t to_sleep = to_sleep_us / 1000;
|
||||
#else
|
||||
const retro_time_t to_sleep = to_sleep_us;
|
||||
#endif
|
||||
const retro_time_t end_frame_time = cpu_features_get_time_usec();
|
||||
|
||||
/* Under an external clock the sleep is the caller's; the
|
||||
* schedule is re-anchored below so it does not carry a
|
||||
* backlog into the first frame after the clock lets go. */
|
||||
if ( to_sleep > 0
|
||||
&& !(runloop_st->pace & RUNLOOP_PACE_EXTERNAL))
|
||||
* schedule is re-anchored so it does not carry a backlog into
|
||||
* the first frame after the clock lets go. */
|
||||
if (runloop_st->pace & RUNLOOP_PACE_EXTERNAL)
|
||||
{
|
||||
/* Combat jitter a bit. */
|
||||
runloop_st->frame_limit_last_time += frame_limit_min;
|
||||
|
||||
runloop_st->frame_limit_last_time = end_frame_time;
|
||||
runloop_st->frame_limit_anchor_ns = (int64_t)end_frame_time * 1000;
|
||||
}
|
||||
else
|
||||
{
|
||||
const retro_time_t to_sleep_us = runloop_pace_schedule(
|
||||
&runloop_st->frame_limit_anchor_ns,
|
||||
pace_limit_ns ? pace_limit_ns : (int64_t)frame_limit_min * 1000,
|
||||
end_frame_time);
|
||||
runloop_st->frame_limit_last_time = runloop_st->frame_limit_anchor_ns / 1000;
|
||||
if (to_sleep_us > 0)
|
||||
{
|
||||
#if defined(__EMSCRIPTEN__) && !defined(EMSCRIPTEN_ASYNCIFY) && !defined(PROXY_TO_PTHREAD)
|
||||
platform_emscripten_deferred_sleep((int)to_sleep);
|
||||
/* Emscripten paces through a deferred main loop timeout
|
||||
* that is expressed in whole milliseconds, so it cannot
|
||||
* act on a sub-millisecond remainder, nor spin. */
|
||||
platform_emscripten_deferred_sleep((int)(to_sleep_us / 1000));
|
||||
#else
|
||||
/* Sleep short by the margin the sleep has been seen to
|
||||
* overshoot, then spin the remainder to the deadline:
|
||||
* the sleep decides how much is spun, the clock decides
|
||||
* where the frame lands. */
|
||||
const retro_time_t deadline = runloop_st->frame_limit_anchor_ns / 1000;
|
||||
retro_time_t now = end_frame_time;
|
||||
#if defined(HAVE_COCOATOUCH)
|
||||
if (!(uico_state_get_ptr()->flags & UICO_ST_FLAG_IS_ON_FOREGROUND))
|
||||
/* In the background the loop is not paced at all. */
|
||||
if (uico_state_get_ptr()->flags & UICO_ST_FLAG_IS_ON_FOREGROUND)
|
||||
return 1;
|
||||
#endif
|
||||
retro_sleep_us((unsigned)to_sleep_us);
|
||||
if (to_sleep_us > runloop_st->frame_limit_margin)
|
||||
{
|
||||
const retro_time_t asked = to_sleep_us - runloop_st->frame_limit_margin;
|
||||
retro_sleep_us((unsigned)asked);
|
||||
now = cpu_features_get_time_usec();
|
||||
runloop_st->frame_limit_margin = runloop_pace_margin_update(
|
||||
runloop_st->frame_limit_margin,
|
||||
now - (end_frame_time + asked), frame_limit_min);
|
||||
}
|
||||
while (now < deadline)
|
||||
now = cpu_features_get_time_usec();
|
||||
#endif
|
||||
|
||||
return 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
runloop_st->frame_limit_last_time = end_frame_time;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,15 @@ struct runloop
|
||||
retro_time_t core_run_time;
|
||||
retro_time_t frame_limit_minimum_time;
|
||||
retro_time_t frame_limit_last_time;
|
||||
/* The same period and anchor in nanoseconds, for the gap limiter's
|
||||
* schedule: a period rounded to whole microseconds is 21 ppm off
|
||||
* at 59.94 Hz, which the schedule would carry into every frame. */
|
||||
int64_t frame_limit_minimum_time_ns;
|
||||
int64_t frame_limit_anchor_ns;
|
||||
/* How early the gap limiter's sleep is asked to return, so the
|
||||
* remainder can be spun to the deadline: the sleep's observed
|
||||
* overshoot, tracked by runloop_pace_margin_update(). */
|
||||
retro_time_t frame_limit_margin;
|
||||
/* When the previous iteration reached the pacing block, and the
|
||||
* smoothed interval between iterations. The pace bits say who
|
||||
* claims to be holding the loop; this says how fast it is actually
|
||||
@@ -415,14 +424,85 @@ static INLINE retro_time_t runloop_content_frame_time_us(float core_hz)
|
||||
return period;
|
||||
}
|
||||
|
||||
/* Whether the frame limiter should hold the loop at 1.0x because
|
||||
* nothing else is: an empty pace record means no vsync, no audio, no
|
||||
* scanline lock, and no fast-forward limit to fall back on. Never
|
||||
* under fast-forward, where running unthrottled is the point. */
|
||||
/* Whether the frame limiter should hold the loop to the display rate
|
||||
* because nothing else is: an empty pace record means no vsync, no
|
||||
* audio, no scanline lock, and no fast-forward limit to fall back on.
|
||||
* With audio rate control, whose resampler follows the loop so the
|
||||
* loop can follow the display; or with Scanline Sync enabled, whose
|
||||
* record bit clears while it recalibrates and which is aiming at the
|
||||
* display rate anyway, so the timer bridges the recalibration rather
|
||||
* than fighting it. Otherwise the content rate is what Sync to Exact
|
||||
* Content Framerate is for, and the loop runs unlimited as
|
||||
* configured. Never under fast-forward, where running unthrottled is
|
||||
* the point. */
|
||||
static INLINE bool runloop_pace_gap_engages(unsigned pace,
|
||||
bool nonblocking, bool fastmotion)
|
||||
bool nonblocking, bool fastmotion, bool scanline_sync,
|
||||
bool rate_control)
|
||||
{
|
||||
return (pace == RUNLOOP_PACE_NONE) && !nonblocking && !fastmotion;
|
||||
return (pace == RUNLOOP_PACE_NONE)
|
||||
&& !nonblocking && !fastmotion && (scanline_sync || rate_control);
|
||||
}
|
||||
|
||||
/* One frame of the content's own time, in nanoseconds, with the same
|
||||
* bounds as runloop_content_frame_time_us(). */
|
||||
static INLINE int64_t runloop_content_frame_time_ns(float core_hz)
|
||||
{
|
||||
int64_t period = (core_hz > 0.0f)
|
||||
? (int64_t)(1000000000.0 / (double)core_hz) : 16666667;
|
||||
if (period < 1000000)
|
||||
return 1000000;
|
||||
if (period > 100000000)
|
||||
return 100000000;
|
||||
return period;
|
||||
}
|
||||
|
||||
/* The gap limiter's schedule, in nanoseconds so that a period that is
|
||||
* not a whole number of microseconds - 16683.35 us at 59.94 Hz - is
|
||||
* kept exactly over any span. @anchor_ns is where the last frame was
|
||||
* due; the next is due a period after it. Returns the microseconds
|
||||
* until then, rounded up, or 0 when it has passed, and moves
|
||||
* @anchor_ns to the slot the frame is taken as filling: the due time
|
||||
* when on time or late by less than a period, so the lateness is caught
|
||||
* up on the next frame rather than kept; now when late by a period or
|
||||
* more, a stall, from which the schedule restarts rather than chases.
|
||||
* The caller spins to @anchor_ns / 1000 after the sleep. */
|
||||
static INLINE retro_time_t runloop_pace_schedule(int64_t *anchor_ns,
|
||||
int64_t period_ns, retro_time_t now_us)
|
||||
{
|
||||
int64_t now = (int64_t)now_us * 1000;
|
||||
int64_t due = *anchor_ns + period_ns;
|
||||
if (now < due)
|
||||
{
|
||||
*anchor_ns = due;
|
||||
return (retro_time_t)((due - now + 999) / 1000);
|
||||
}
|
||||
if (now - due < period_ns)
|
||||
{
|
||||
*anchor_ns = due;
|
||||
return 0;
|
||||
}
|
||||
*anchor_ns = now;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The gap limiter's sleep margin: how early the sleep is asked to
|
||||
* return, so the remainder can be spun to the deadline. It tracks the
|
||||
* sleep's observed overshoot - up at once, since one late sleep is a
|
||||
* frame late; down by a sixteenth a frame, so a single quiet sleep does
|
||||
* not unwind it - and never past a quarter of the period, so the spin
|
||||
* stays a fraction of the frame. */
|
||||
static INLINE retro_time_t runloop_pace_margin_update(retro_time_t margin,
|
||||
retro_time_t overshoot, retro_time_t period)
|
||||
{
|
||||
if (overshoot < 0)
|
||||
overshoot = 0;
|
||||
if (overshoot > margin)
|
||||
margin = overshoot;
|
||||
else
|
||||
margin -= (margin - overshoot) / 16;
|
||||
if (margin > period / 4)
|
||||
margin = period / 4;
|
||||
return margin;
|
||||
}
|
||||
|
||||
/* Whether an interval between iterations is worth averaging into the
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
compiler := gcc
|
||||
TARGET := ff_speedup_test
|
||||
|
||||
CORE_DIR = ../../..
|
||||
LIBRETRO_COMM_DIR = $(CORE_DIR)/libretro-common
|
||||
INCDIRS := -I$(LIBRETRO_COMM_DIR)/include -I$(CORE_DIR) \
|
||||
-I$(CORE_DIR)/deps -I$(CORE_DIR)/deps/stb
|
||||
|
||||
CC := $(compiler)
|
||||
flags += -std=gnu99
|
||||
|
||||
SOURCES_C := \
|
||||
ff_speedup_test.c \
|
||||
stubs_retroarch.c \
|
||||
$(LIBRETRO_COMM_DIR)/lists/string_list.c \
|
||||
$(LIBRETRO_COMM_DIR)/lists/dir_list.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/retro_dirent.c \
|
||||
$(LIBRETRO_COMM_DIR)/streams/file_stream.c \
|
||||
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \
|
||||
$(LIBRETRO_COMM_DIR)/rthreads/rthreads.c \
|
||||
$(LIBRETRO_COMM_DIR)/queues/retro_spsc.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/conversion/float_to_s16.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/conversion/s16_to_float.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/audio_resampler.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/sinc_resampler_int16.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/nearest_resampler_int16.c \
|
||||
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
|
||||
$(LIBRETRO_COMM_DIR)/memmap/memalign.c \
|
||||
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
|
||||
$(LIBRETRO_COMM_DIR)/string/rstrtod.c \
|
||||
$(LIBRETRO_COMM_DIR)/time/rtime.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_userdata.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/sinc_resampler.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/nearest_resampler.c \
|
||||
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
|
||||
$(LIBRETRO_COMM_DIR)/compat/compat_strldup.c \
|
||||
$(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c
|
||||
|
||||
CFLAGS += -Wall $(INCDIRS)
|
||||
CFLAGS += -DRARCH_INTERNAL -DHAVE_THREADS -DHAVE_AUDIOMIXER -DHAVE_MENU \
|
||||
-DHAVE_CONFIGFILE -D_GNU_SOURCE
|
||||
LDFLAGS += -lm -lpthread
|
||||
|
||||
ifneq ($(SAN),)
|
||||
SANITIZER := $(SAN)
|
||||
endif
|
||||
ifneq ($(SANITIZER),)
|
||||
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer -g $(CFLAGS)
|
||||
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
|
||||
endif
|
||||
|
||||
OBJS := $(SOURCES_C:.c=.o)
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(INCDIRS) $(CFLAGS) $(flags) -c -o$@ $<
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o$@ $(OBJS) $(LDFLAGS)
|
||||
|
||||
check: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS)
|
||||
rm -f $(TARGET)
|
||||
|
||||
.PHONY: clean all check
|
||||
@@ -0,0 +1,219 @@
|
||||
/* Fast-forward audio speedup: who measures the speed, and from what.
|
||||
*
|
||||
* audio_driver_fastforward_ratio_mult() tracks the wall-clock interval
|
||||
* between flushes against the interval a 1.0x core would produce, and
|
||||
* the resampler ratio is scaled by the result so fast-forward audio
|
||||
* pitches up with the speed instead of dropping out. The contract:
|
||||
*
|
||||
* - The measurement is of the core's cadence. On the inline pipeline
|
||||
* the flush runs on the core's thread, so the flush interval is
|
||||
* that. On the threaded pipeline the flush runs on the audio
|
||||
* thread, whose interval is the device draining the previous chunk
|
||||
* - i.e. the previous multiplier played back, which measuring only
|
||||
* confirms. There the producer measures at its publish cadence and
|
||||
* the consumer takes the figure from pipe_ff_mult_q16; nothing the
|
||||
* consumer does can move it.
|
||||
* - The first sample of a fast-forward returns 1.0 and seeds the
|
||||
* average at the 1.0x interval, and releasing fast-forward arms
|
||||
* that seed again. The idle time between two fast-forwards is not a
|
||||
* flush interval and must not enter the average: read as one, it
|
||||
* pins the multiplier at AUDIO_MAX_RATIO and the audio plays back
|
||||
* sixteen times too slow.
|
||||
*
|
||||
* Includes audio/audio_driver.c so the shipping functions run, with
|
||||
* the clock replaced by a counter the test advances. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <retro_atomic.h>
|
||||
#include <features/features_cpu.h>
|
||||
|
||||
static retro_time_t fake_now;
|
||||
static retro_time_t fake_time_usec(void) { return fake_now; }
|
||||
#define cpu_features_get_time_usec fake_time_usec
|
||||
|
||||
#include "../../../audio/audio_driver.c"
|
||||
|
||||
static int failures;
|
||||
|
||||
#define CHECK(cond, what) do { \
|
||||
if (!(cond)) { printf("FAIL: %s\n", what); failures++; } \
|
||||
else printf(" ok: %s\n", what); } while (0)
|
||||
|
||||
#define RATE 48000.0
|
||||
#define FRAMES 800 /* one 60 Hz frame of core audio */
|
||||
#define ONE_X ((retro_time_t)(FRAMES * 1000000.0 / RATE))
|
||||
|
||||
static bool near(double got, double want)
|
||||
{
|
||||
return fabs(got - want) <= want * 0.05;
|
||||
}
|
||||
|
||||
static void fresh(void)
|
||||
{
|
||||
memset(&audio_driver_st, 0, sizeof(audio_driver_st));
|
||||
audio_driver_st.input = RATE;
|
||||
fake_now = 1000000;
|
||||
}
|
||||
|
||||
/* --- the measurement at the core's cadence ------------------------- */
|
||||
|
||||
static void test_inline_measures_speed(void)
|
||||
{
|
||||
int i;
|
||||
double m;
|
||||
|
||||
fresh();
|
||||
m = audio_driver_fastforward_ratio_mult(&audio_driver_st, FRAMES);
|
||||
CHECK(m == 1.0, "first sample of a fast-forward is unity");
|
||||
|
||||
for (i = 0; i < 64; i++)
|
||||
{
|
||||
fake_now += ONE_X / 4;
|
||||
m = audio_driver_fastforward_ratio_mult(&audio_driver_st, FRAMES);
|
||||
}
|
||||
CHECK(near(m, 0.25), "64 flushes at 4x settle to a 0.25 multiplier");
|
||||
|
||||
for (i = 0; i < 64; i++)
|
||||
{
|
||||
fake_now += ONE_X * 2;
|
||||
m = audio_driver_fastforward_ratio_mult(&audio_driver_st, FRAMES);
|
||||
}
|
||||
CHECK(near(m, 2.0), "and follow a core slower than realtime to 2.0");
|
||||
}
|
||||
|
||||
/* --- release and re-entry ------------------------------------------ */
|
||||
|
||||
static void test_reentry_ignores_idle_gap(void)
|
||||
{
|
||||
int i;
|
||||
double m, peak = 0.0;
|
||||
|
||||
fresh();
|
||||
audio_driver_fastforward_ratio_mult(&audio_driver_st, FRAMES);
|
||||
for (i = 0; i < 64; i++)
|
||||
{
|
||||
fake_now += ONE_X / 4;
|
||||
audio_driver_fastforward_ratio_mult(&audio_driver_st, FRAMES);
|
||||
}
|
||||
|
||||
/* Fast-forward released; a minute at 1.0x; pressed again. */
|
||||
audio_driver_ff_mult_reset(&audio_driver_st);
|
||||
fake_now += 60 * 1000000;
|
||||
|
||||
m = audio_driver_fastforward_ratio_mult(&audio_driver_st, FRAMES);
|
||||
CHECK(m == 1.0, "re-entry after a minute idle starts at unity, not the gap");
|
||||
|
||||
for (i = 0; i < 64; i++)
|
||||
{
|
||||
fake_now += ONE_X / 4;
|
||||
m = audio_driver_fastforward_ratio_mult(&audio_driver_st, FRAMES);
|
||||
if (m > peak)
|
||||
peak = m;
|
||||
}
|
||||
CHECK(peak <= 1.0, "the multiplier never rises above unity on the way to 4x");
|
||||
CHECK(near(m, 0.25), "and settles to 0.25 again");
|
||||
}
|
||||
|
||||
/* --- the threaded handoff ------------------------------------------ */
|
||||
|
||||
static void test_threaded_consumer_takes_producer_figure(void)
|
||||
{
|
||||
int i;
|
||||
double m;
|
||||
retro_time_t before;
|
||||
|
||||
fresh();
|
||||
audio_driver_st.pipe_threaded = true;
|
||||
retro_atomic_store_release_int(&audio_driver_st.pipe_ff_mult_q16,
|
||||
(int)(0.25 * 65536.0));
|
||||
|
||||
/* The consumer flushes at the device's cadence: the previous chunk
|
||||
* at 0.25 played back is a quarter of the 1.0x interval, which is
|
||||
* what the old consumer-side measurement would have read as 4x -
|
||||
* and then, as the ratio it set drains, whatever it set last. */
|
||||
before = audio_driver_st.last_flush_time;
|
||||
for (i = 0; i < 64; i++)
|
||||
{
|
||||
fake_now += ONE_X * 4;
|
||||
m = audio_driver_ff_mult(&audio_driver_st, FRAMES);
|
||||
}
|
||||
CHECK(m == 0.25, "consumer reads 0.25 whatever its own cadence");
|
||||
CHECK(audio_driver_st.last_flush_time == before,
|
||||
"and its flushes do not enter the measurement");
|
||||
|
||||
/* Inline: the same call measures. */
|
||||
audio_driver_st.pipe_threaded = false;
|
||||
m = audio_driver_ff_mult(&audio_driver_st, FRAMES);
|
||||
CHECK(m == 1.0 && audio_driver_st.last_flush_time == fake_now,
|
||||
"inline: the same call measures at the flush");
|
||||
}
|
||||
|
||||
/* --- the producer end to end ---------------------------------------- */
|
||||
|
||||
static void test_producer_publishes_at_its_cadence(void)
|
||||
{
|
||||
int i;
|
||||
int16_t block[FRAMES * 2];
|
||||
settings_t *settings = config_get_ptr();
|
||||
double got;
|
||||
|
||||
fresh();
|
||||
memset(block, 0, sizeof(block));
|
||||
settings->bools.audio_fastforward_speedup = true;
|
||||
audio_driver_st.pipe_threaded = true;
|
||||
AUDIO_FLAGS_SET(&audio_driver_st, AUDIO_FLAG_PIPELINE_THREADED);
|
||||
if (!retro_spsc_init(&audio_driver_st.pipe_ring, 4096))
|
||||
{
|
||||
CHECK(false, "ring allocated");
|
||||
return;
|
||||
}
|
||||
audio_driver_st.pipe_lock = slock_new();
|
||||
audio_driver_st.pipe_cond = scond_new();
|
||||
retro_atomic_store_release_int(&audio_driver_st.pipe_ff_mult_q16, 65536);
|
||||
|
||||
/* A 1.0x publish first, so the re-entry seed is armed. */
|
||||
audio_driver_submit(&audio_driver_st, 1.0f, block, FRAMES * 2, false, false);
|
||||
|
||||
/* Then fast-forward at 4x, with nobody draining the ring: every
|
||||
* block past the first is dropped, and still counts. */
|
||||
for (i = 0; i < 64; i++)
|
||||
{
|
||||
fake_now += ONE_X / 4;
|
||||
audio_driver_submit(&audio_driver_st, 1.0f, block, FRAMES * 2, false, true);
|
||||
}
|
||||
got = (double)retro_atomic_load_acquire_int(
|
||||
&audio_driver_st.pipe_ff_mult_q16) / 65536.0;
|
||||
CHECK(near(got, 0.25), "producer at 4x into a full ring publishes 0.25");
|
||||
|
||||
/* Released: a 1.0x publish re-arms the seed. */
|
||||
audio_driver_submit(&audio_driver_st, 1.0f, block, FRAMES * 2, false, false);
|
||||
CHECK(audio_driver_st.last_flush_time == 0,
|
||||
"a publish outside fast-forward re-arms the seed");
|
||||
|
||||
retro_spsc_free(&audio_driver_st.pipe_ring);
|
||||
slock_free(audio_driver_st.pipe_lock);
|
||||
scond_free(audio_driver_st.pipe_cond);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("fast-forward audio speedup:\n");
|
||||
test_inline_measures_speed();
|
||||
test_reentry_ignores_idle_gap();
|
||||
test_threaded_consumer_takes_producer_figure();
|
||||
test_producer_publishes_at_its_cadence();
|
||||
|
||||
if (failures)
|
||||
{
|
||||
printf("FAILED: %d\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("ok: the speedup follows the core's cadence on both pipelines, "
|
||||
"never the device's, and re-entry starts from unity\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/* Frontend, mixer and resampler symbols audio/audio_driver.c
|
||||
* references. The subject of this harness is the driver's own locking
|
||||
* around mixer_streams[], so the mixer stubs are just enough to make
|
||||
* load/play/stop/destroy produce distinguishable non-NULL handles and
|
||||
* voices - the state machine that add_stream and stop_stream drive
|
||||
* lives in audio_driver.c and is exercised for real.
|
||||
*
|
||||
* Signatures are copied from the tree's headers rather than guessed.
|
||||
* Anything the driver should not reach on these paths aborts instead
|
||||
* of quietly returning. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <audio/audio_mixer.h>
|
||||
|
||||
#include "../../../configuration.h"
|
||||
#include "../../../record/record_driver.h"
|
||||
#include "../../../runloop.h"
|
||||
#include "../../../menu/menu_driver.h"
|
||||
#include "../../../gfx/video_driver.h"
|
||||
#include "../../../command.h"
|
||||
|
||||
void RARCH_LOG(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_WARN(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_DBG(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_LOG_OUTPUT(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
void RARCH_ERR(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
printf("ERR: ");
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
bool verbosity_is_enabled(void) { return false; }
|
||||
|
||||
settings_t *config_get_ptr(void)
|
||||
{
|
||||
static settings_t settings;
|
||||
return &settings;
|
||||
}
|
||||
|
||||
static runloop_state_t runloop_st;
|
||||
runloop_state_t *runloop_state_get_ptr(void) { return &runloop_st; }
|
||||
uint32_t runloop_get_flags(void) { return 0; }
|
||||
|
||||
static recording_state_t recording_st;
|
||||
recording_state_t *recording_state_get_ptr(void) { return &recording_st; }
|
||||
|
||||
static video_driver_state_t video_st;
|
||||
video_driver_state_t *video_state_get_ptr(void) { return &video_st; }
|
||||
|
||||
static struct menu_state menu_st;
|
||||
struct menu_state *menu_state_get_ptr(void) { return &menu_st; }
|
||||
|
||||
/* --- mixer ----------------------------------------------------------- */
|
||||
|
||||
/* Distinct non-NULL cookies: the driver only ever stores and compares
|
||||
* these, and freeing them here would hide a use-after-free in the
|
||||
* driver rather than expose one, so each allocation is real. */
|
||||
/* The real audio_mixer_destroy() frees the buffer the sound was loaded
|
||||
* from for the streaming types (it owns it once loaded, unless it was
|
||||
* borrowed via data_owner). The stub mirrors that ownership so leak
|
||||
* checking over these paths means something. */
|
||||
struct audio_mixer_sound { void *owned_data; };
|
||||
struct audio_mixer_voice { int tag; };
|
||||
|
||||
static audio_mixer_sound_t *new_sound_owning(void *data)
|
||||
{
|
||||
audio_mixer_sound_t *s = (audio_mixer_sound_t*)calloc(1, sizeof(*s));
|
||||
if (s)
|
||||
s->owned_data = data;
|
||||
return s;
|
||||
}
|
||||
|
||||
static audio_mixer_sound_t *new_sound(void)
|
||||
{
|
||||
return new_sound_owning(NULL);
|
||||
}
|
||||
|
||||
static audio_mixer_voice_t *new_voice(void)
|
||||
{
|
||||
audio_mixer_voice_t *v = (audio_mixer_voice_t*)calloc(1, sizeof(*v));
|
||||
return v;
|
||||
}
|
||||
|
||||
void audio_mixer_init(unsigned rate) { (void)rate; }
|
||||
void audio_mixer_done(void) { }
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_wav(void *buffer, size_t size,
|
||||
const char *resampler_ident, enum resampler_quality quality,
|
||||
bool want_s16)
|
||||
{
|
||||
(void)buffer; (void)size; (void)resampler_ident; (void)quality;
|
||||
(void)want_s16;
|
||||
return new_sound();
|
||||
}
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_wav_stream(void *buffer,
|
||||
size_t size)
|
||||
{
|
||||
(void)size;
|
||||
return new_sound_owning(buffer);
|
||||
}
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_ogg(void *buffer, size_t size)
|
||||
{
|
||||
(void)size;
|
||||
return new_sound_owning(buffer);
|
||||
}
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_mod(void *buffer, size_t size)
|
||||
{
|
||||
(void)size;
|
||||
return new_sound_owning(buffer);
|
||||
}
|
||||
|
||||
void audio_mixer_destroy(audio_mixer_sound_t *sound)
|
||||
{
|
||||
if (!sound)
|
||||
return;
|
||||
if (sound->owned_data)
|
||||
free(sound->owned_data);
|
||||
free(sound);
|
||||
}
|
||||
|
||||
audio_mixer_voice_t *audio_mixer_play(audio_mixer_sound_t *sound,
|
||||
bool repeat, float volume, const char *resampler_ident,
|
||||
enum resampler_quality quality, audio_mixer_stop_cb_t stop_cb)
|
||||
{
|
||||
(void)sound; (void)repeat; (void)volume; (void)resampler_ident;
|
||||
(void)quality; (void)stop_cb;
|
||||
return new_voice();
|
||||
}
|
||||
|
||||
audio_mixer_voice_t *audio_mixer_play_s16(audio_mixer_sound_t *sound,
|
||||
bool repeat, int32_t gain, enum resampler_quality quality,
|
||||
audio_mixer_stop_cb_t stop_cb)
|
||||
{
|
||||
(void)sound; (void)repeat; (void)gain; (void)quality; (void)stop_cb;
|
||||
return new_voice();
|
||||
}
|
||||
|
||||
void audio_mixer_stop(audio_mixer_voice_t *voice)
|
||||
{
|
||||
free(voice);
|
||||
}
|
||||
|
||||
void audio_mixer_mix(float *buffer, size_t num_frames,
|
||||
float gain_override, bool override)
|
||||
{
|
||||
(void)buffer; (void)num_frames; (void)gain_override; (void)override;
|
||||
}
|
||||
|
||||
void audio_mixer_mix_s16(int16_t *buffer, size_t num_frames,
|
||||
int32_t gain_override, bool override)
|
||||
{
|
||||
(void)buffer; (void)num_frames; (void)gain_override; (void)override;
|
||||
}
|
||||
|
||||
void audio_mixer_voice_set_volume(audio_mixer_voice_t *voice, float val)
|
||||
{
|
||||
(void)voice; (void)val;
|
||||
}
|
||||
|
||||
void audio_mixer_voice_set_avail(audio_mixer_voice_t *voice, size_t avail)
|
||||
{
|
||||
(void)voice; (void)avail;
|
||||
}
|
||||
|
||||
void audio_mixer_sound_set_avail(audio_mixer_sound_t *sound, size_t avail)
|
||||
{
|
||||
(void)sound; (void)avail;
|
||||
}
|
||||
|
||||
void audio_mixer_sound_set_data_owner(audio_mixer_sound_t *sound,
|
||||
void *owner, void (*owner_free)(void *owner))
|
||||
{
|
||||
(void)sound; (void)owner; (void)owner_free;
|
||||
}
|
||||
|
||||
size_t audio_mixer_voice_buffer_tell(audio_mixer_voice_t *voice)
|
||||
{
|
||||
(void)voice;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool audio_mixer_has_float_voices(void) { return true; }
|
||||
bool audio_mixer_has_s16_voices(void) { return false; }
|
||||
|
||||
/* --- everything the locking paths must not reach --------------------- */
|
||||
|
||||
static void unreachable(const char *what)
|
||||
{
|
||||
fprintf(stderr, "stub %s reached unexpectedly\n", what);
|
||||
abort();
|
||||
}
|
||||
|
||||
bool command_event(enum event_command action, void *data)
|
||||
{
|
||||
(void)action; (void)data;
|
||||
unreachable("command_event");
|
||||
return false;
|
||||
}
|
||||
|
||||
void *task_push_audio_mixer_load(const char *path,
|
||||
retro_task_callback_t cb, void *user_data, bool system,
|
||||
enum audio_mixer_slot_selection_type type, int slot)
|
||||
{
|
||||
(void)path; (void)cb; (void)user_data; (void)system; (void)type;
|
||||
(void)slot;
|
||||
unreachable("task_push_audio_mixer_load");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* --- remaining frontend references ----------------------------------- */
|
||||
|
||||
#include "../../../list_special.h"
|
||||
#include "../../../driver.h"
|
||||
#include "../../../midi_driver.h"
|
||||
#include "../../../file_path_special.h"
|
||||
#include "../../../msg_hash.h"
|
||||
#include "../../../audio/audio_thread_wrapper.h"
|
||||
|
||||
const char *char_list_new_special(enum string_list_type type, void *data)
|
||||
{
|
||||
(void)type; (void)data;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int driver_find_index(const char *label, const char *drv)
|
||||
{
|
||||
(void)label; (void)drv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool midi_driver_synth_active(void) { return false; }
|
||||
|
||||
bool midi_driver_render_audio(float *out, size_t frames, unsigned rate)
|
||||
{
|
||||
(void)out; (void)frames; (void)rate;
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *audio_thread_wrapped_ident(void *data)
|
||||
{
|
||||
(void)data;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const audio_driver_t *audio_thread_wrapped_driver(void *data)
|
||||
{
|
||||
(void)data;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool audio_init_thread(const audio_driver_t **out_driver, void **out_data,
|
||||
const char *device, unsigned out_rate, unsigned *new_rate,
|
||||
unsigned latency, unsigned block_frames, bool raise_priority,
|
||||
bool prefer_fast_cores, const audio_driver_t *driver)
|
||||
{
|
||||
(void)out_driver; (void)out_data; (void)device; (void)out_rate;
|
||||
(void)new_rate; (void)latency; (void)block_frames;
|
||||
(void)raise_priority; (void)prefer_fast_cores; (void)driver;
|
||||
unreachable("audio_init_thread");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t fill_pathname_application_special(char *s, size_t len,
|
||||
enum application_special_type type)
|
||||
{
|
||||
(void)type;
|
||||
if (len)
|
||||
s[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *msg_hash_to_str(enum msg_hash_enums msg)
|
||||
{
|
||||
(void)msg;
|
||||
return "";
|
||||
}
|
||||
|
||||
/* retro_resampler_realloc: the real one is linked in. */
|
||||
@@ -0,0 +1,73 @@
|
||||
compiler := gcc
|
||||
TARGET := pipeline_rate_control_test
|
||||
|
||||
CORE_DIR = ../../..
|
||||
LIBRETRO_COMM_DIR = $(CORE_DIR)/libretro-common
|
||||
INCDIRS := -I$(LIBRETRO_COMM_DIR)/include -I$(CORE_DIR) \
|
||||
-I$(CORE_DIR)/deps -I$(CORE_DIR)/deps/stb
|
||||
|
||||
CC := $(compiler)
|
||||
flags += -std=gnu99
|
||||
|
||||
SOURCES_C := \
|
||||
pipeline_rate_control_test.c \
|
||||
stubs_retroarch.c \
|
||||
$(LIBRETRO_COMM_DIR)/lists/string_list.c \
|
||||
$(LIBRETRO_COMM_DIR)/lists/dir_list.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/retro_dirent.c \
|
||||
$(LIBRETRO_COMM_DIR)/streams/file_stream.c \
|
||||
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \
|
||||
$(LIBRETRO_COMM_DIR)/rthreads/rthreads.c \
|
||||
$(LIBRETRO_COMM_DIR)/queues/retro_spsc.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/conversion/float_to_s16.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/conversion/s16_to_float.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/audio_resampler.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/sinc_resampler_int16.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/nearest_resampler_int16.c \
|
||||
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
|
||||
$(LIBRETRO_COMM_DIR)/memmap/memalign.c \
|
||||
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
|
||||
$(LIBRETRO_COMM_DIR)/string/rstrtod.c \
|
||||
$(LIBRETRO_COMM_DIR)/time/rtime.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_userdata.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/sinc_resampler.c \
|
||||
$(LIBRETRO_COMM_DIR)/audio/resampler/drivers/nearest_resampler.c \
|
||||
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
|
||||
$(LIBRETRO_COMM_DIR)/compat/compat_strldup.c \
|
||||
$(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c
|
||||
|
||||
CFLAGS += -Wall $(INCDIRS)
|
||||
CFLAGS += -DRARCH_INTERNAL -DHAVE_THREADS -DHAVE_AUDIOMIXER -DHAVE_MENU \
|
||||
-DHAVE_CONFIGFILE -D_GNU_SOURCE
|
||||
LDFLAGS += -lm -lpthread
|
||||
|
||||
ifneq ($(SAN),)
|
||||
SANITIZER := $(SAN)
|
||||
endif
|
||||
ifneq ($(SANITIZER),)
|
||||
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer -g $(CFLAGS)
|
||||
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
|
||||
endif
|
||||
|
||||
OBJS := $(SOURCES_C:.c=.o)
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(INCDIRS) $(CFLAGS) $(flags) -c -o$@ $<
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o$@ $(OBJS) $(LDFLAGS)
|
||||
|
||||
check: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS)
|
||||
rm -f $(TARGET)
|
||||
|
||||
.PHONY: clean all check
|
||||
@@ -0,0 +1,410 @@
|
||||
/* Rate control on the threaded audio pipeline, against a device whose
|
||||
* fill drains in real time.
|
||||
*
|
||||
* The consumer waits for half the device's buffer and writes half, so
|
||||
* the device's fill sits between half and full whatever the clocks do;
|
||||
* a controller reading it, at any point of the pass, sees a constant
|
||||
* error and pins the ratio at a bound - read after the wait it is
|
||||
* never above half, read before it is never below. What the clocks
|
||||
* move is the pipe ring in front of the device. Rate control's fill is
|
||||
* the two together, sampled by the producer before each publish; see
|
||||
* pipe_ctrl_avail in audio_driver.h.
|
||||
*
|
||||
* The device here is what a real driver's timing looks like: a 384-
|
||||
* frame buffer (8 ms at 48 kHz) that the "hardware" drains at exactly
|
||||
* 48000 frames a second of wall-clock time; write_raw() puts frames in,
|
||||
* scaled by the rate adjustment it is handed as a resampling driver
|
||||
* would; wait_writable() sleeps until there is room for min(len, half).
|
||||
* The producer publishes one 800-frame video frame every 1/60 s on an
|
||||
* absolute schedule, so production and consumption match to the clock
|
||||
* and the right ratio is 1.0.
|
||||
*
|
||||
* Includes audio/audio_driver.c so the shipping producer, consumer and
|
||||
* controller run. The device records every rate adjustment it is
|
||||
* handed; over three seconds after a four-second settle their mean
|
||||
* must be within 500 ppm of 1.0,
|
||||
* and the device must have taken at least 99.5% of what the producer
|
||||
* published - the rest being the start-up transient. Reading the
|
||||
* device alone, the mean sits at a bound - +delta after the wait,
|
||||
* -delta before it - and the pipe ring overflows behind it. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <math.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <retro_atomic.h>
|
||||
|
||||
/* The sink estimate runs short here - windows of 0.4 s, a baseline of
|
||||
* 3 s - so a seven-second real-time run reaches an application. */
|
||||
#define AUDIO_SINK_WINDOW_USEC 400000
|
||||
#define AUDIO_SINK_BASELINE_USEC 3000000
|
||||
#include "../../../audio/audio_driver.c"
|
||||
|
||||
static unsigned failures = 0;
|
||||
|
||||
#define CHECK(c, ...) do { if (!(c)) { printf("FAIL: "); printf(__VA_ARGS__); printf("\n"); failures++; } } while (0)
|
||||
|
||||
/* --- the clock ------------------------------------------------------- */
|
||||
|
||||
static double now_s(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
||||
}
|
||||
|
||||
static void sleep_until(double t)
|
||||
{
|
||||
struct timespec ts;
|
||||
double d = t - now_s();
|
||||
if (d <= 0.0)
|
||||
return;
|
||||
ts.tv_sec = (time_t)d;
|
||||
ts.tv_nsec = (long)((d - (double)ts.tv_sec) * 1e9);
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
/* --- the device ------------------------------------------------------ */
|
||||
|
||||
#define DEV_RATE 48000.0
|
||||
static size_t DEV_CAPACITY = 384; /* frames: 8 ms; 3072 for a 64 ms default */
|
||||
|
||||
static pthread_mutex_t dev_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static double dev_fill; /* frames, fractional */
|
||||
static double dev_last; /* when dev_fill was last brought up to date */
|
||||
static double dev_took; /* frames accepted, total */
|
||||
static double dev_adjust_sum; /* rate adjustments handed to write_raw */
|
||||
static unsigned dev_adjust_n;
|
||||
|
||||
/* Bring the fill up to now: the hardware drains at DEV_RATE. */
|
||||
static double dev_underrun; /* frames of silence the hardware played */
|
||||
static size_t dev_underrun_events;/* times it ran dry, as a driver counts periods */
|
||||
|
||||
static void dev_drain_locked(void)
|
||||
{
|
||||
double t = now_s();
|
||||
double before = dev_fill;
|
||||
dev_fill -= (t - dev_last) * DEV_RATE;
|
||||
if (dev_fill < 0.0)
|
||||
{
|
||||
dev_underrun -= dev_fill;
|
||||
if (before > 0.0)
|
||||
dev_underrun_events++;
|
||||
dev_fill = 0.0;
|
||||
}
|
||||
dev_last = t;
|
||||
}
|
||||
|
||||
static size_t dev_underruns(void *d)
|
||||
{
|
||||
size_t n;
|
||||
(void)d;
|
||||
pthread_mutex_lock(&dev_lock);
|
||||
dev_drain_locked();
|
||||
n = dev_underrun_events;
|
||||
pthread_mutex_unlock(&dev_lock);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void *dev_init(const char *device, unsigned rate, unsigned latency,
|
||||
unsigned block_frames, unsigned *new_rate)
|
||||
{
|
||||
static int handle = 1;
|
||||
(void)device; (void)rate; (void)latency; (void)block_frames; (void)new_rate;
|
||||
dev_last = now_s();
|
||||
return &handle;
|
||||
}
|
||||
|
||||
static ssize_t dev_write(void *data, const void *buf, size_t size)
|
||||
{
|
||||
(void)data; (void)buf; (void)size;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static ssize_t dev_write_raw(void *data, const int16_t *samples,
|
||||
size_t frames, unsigned input_rate, double rate_adjust, float gain)
|
||||
{
|
||||
double want, room, put;
|
||||
(void)data; (void)samples; (void)input_rate; (void)gain;
|
||||
pthread_mutex_lock(&dev_lock);
|
||||
dev_drain_locked();
|
||||
want = (double)frames * rate_adjust;
|
||||
room = DEV_CAPACITY - dev_fill;
|
||||
put = (want < room) ? want : room;
|
||||
if (put < 0.0)
|
||||
put = 0.0;
|
||||
dev_fill += put;
|
||||
dev_took += put;
|
||||
dev_adjust_sum += rate_adjust;
|
||||
dev_adjust_n++;
|
||||
pthread_mutex_unlock(&dev_lock);
|
||||
return (ssize_t)put;
|
||||
}
|
||||
|
||||
static size_t dev_write_avail(void *d)
|
||||
{
|
||||
double room;
|
||||
(void)d;
|
||||
pthread_mutex_lock(&dev_lock);
|
||||
dev_drain_locked();
|
||||
room = DEV_CAPACITY - dev_fill;
|
||||
pthread_mutex_unlock(&dev_lock);
|
||||
return (size_t)room * 4;
|
||||
}
|
||||
|
||||
static size_t dev_buffer_size(void *d) { (void)d; return DEV_CAPACITY * 4; }
|
||||
|
||||
/* What the hardware has consumed, for the sink estimate: the drain
|
||||
* counted in whole 96-frame periods, as a device counts it. */
|
||||
static size_t dev_frames_consumed(void *d)
|
||||
{
|
||||
double drained;
|
||||
(void)d;
|
||||
pthread_mutex_lock(&dev_lock);
|
||||
dev_drain_locked();
|
||||
drained = dev_took - dev_fill;
|
||||
pthread_mutex_unlock(&dev_lock);
|
||||
if (drained < 0.0)
|
||||
drained = 0.0;
|
||||
return ((size_t)drained / 96) * 96;
|
||||
}
|
||||
|
||||
static size_t dev_wait_writable(void *data, size_t len)
|
||||
{
|
||||
size_t want = len / 4;
|
||||
size_t half = DEV_CAPACITY / 2;
|
||||
size_t room;
|
||||
if (want > half)
|
||||
want = half;
|
||||
for (;;)
|
||||
{
|
||||
room = dev_write_avail(data) / 4;
|
||||
if (room >= want)
|
||||
return room * 4;
|
||||
usleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
static bool dev_stop(void *d) { (void)d; return true; }
|
||||
static bool dev_start(void *d, bool s) { (void)d; (void)s; return true; }
|
||||
static bool dev_alive(void *d) { (void)d; return true; }
|
||||
static void dev_set_nonblock(void *d, bool s){ (void)d; (void)s; }
|
||||
static void dev_free(void *d) { (void)d; }
|
||||
static bool dev_use_float(void *d) { (void)d; return false; }
|
||||
|
||||
static audio_driver_t scripted_driver = {
|
||||
dev_init, dev_write, dev_stop, dev_start, dev_alive, dev_set_nonblock,
|
||||
dev_free, dev_use_float, "scripted", NULL, NULL, dev_write_avail,
|
||||
dev_buffer_size, dev_write_raw, dev_wait_writable, dev_frames_consumed,
|
||||
dev_underruns
|
||||
};
|
||||
|
||||
/* --- the consumer thread ---------------------------------------------- */
|
||||
|
||||
static retro_atomic_int_t consumer_run = RETRO_ATOMIC_INT_INITIALIZER(1);
|
||||
|
||||
static void *consumer(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (retro_atomic_load_acquire_int(&consumer_run))
|
||||
audio_driver_pipeline_consume(&audio_driver_st);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* --- fixture --------------------------------------------------------- */
|
||||
|
||||
static double dbg_pipe, dbg_avail, dbg_eff; static unsigned dbg_n;
|
||||
static bool sync_on = false; /* audio sync: the producer's flag and the setting */
|
||||
static bool jitter = false; /* a core that delivers late now and then */
|
||||
|
||||
static bool pipeline_up(size_t ring_bytes)
|
||||
{
|
||||
audio_driver_state_t *st = &audio_driver_st;
|
||||
|
||||
memset(st, 0, sizeof(*st));
|
||||
st->current_audio = &scripted_driver;
|
||||
st->context_audio_data = scripted_driver.init(NULL, 48000, 8, 0, NULL);
|
||||
st->input = 48000.0;
|
||||
st->src_ratio_orig = 1.0;
|
||||
st->src_ratio_curr = 1.0;
|
||||
st->cached_rate_adjust = 1.0;
|
||||
st->volume_gain = 1.0f;
|
||||
st->buffer_size = scripted_driver.buffer_size(st->context_audio_data);
|
||||
st->output_samples_buf = (float*)malloc(65536);
|
||||
st->pipe_scratch = (int16_t*)malloc(65536);
|
||||
st->pipe_pass_int16s = 1600;
|
||||
retro_atomic_store_release_int(&st->pipe_ctrl_avail, -1);
|
||||
st->rate_control_delta = 0.005f;
|
||||
st->drc_threshold_int16s = 1600;
|
||||
st->sink_bias = 1.0;
|
||||
config_get_ptr()->bools.audio_sink_rate_estimation = true;
|
||||
config_get_ptr()->uints.audio_output_sample_rate = 48000;
|
||||
config_get_ptr()->bools.audio_sync = sync_on;
|
||||
if (!retro_spsc_init(&st->pipe_ring, ring_bytes))
|
||||
return false;
|
||||
st->pipe_lock = slock_new();
|
||||
st->pipe_cond = scond_new();
|
||||
st->pipe_data_cond = scond_new();
|
||||
st->state_lock = slock_new();
|
||||
st->pipe_threaded = true;
|
||||
st->pipe_priming = true;
|
||||
AUDIO_FLAGS_SET(st, AUDIO_FLAG_ACTIVE | AUDIO_FLAG_STARTED
|
||||
| AUDIO_FLAG_PIPELINE_THREADED | AUDIO_FLAG_CONTROL);
|
||||
if (!sync_on)
|
||||
AUDIO_FLAGS_SET(st, AUDIO_FLAG_NONBLOCK);
|
||||
return st->pipe_lock && st->pipe_cond && st->pipe_data_cond
|
||||
&& st->state_lock && st->output_samples_buf && st->pipe_scratch;
|
||||
}
|
||||
|
||||
static int16_t frame_audio[800 * 2];
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
pthread_t cons;
|
||||
unsigned i, frames = 420, warm = 240;
|
||||
if (argc > 1)
|
||||
frames = (unsigned)atoi(argv[1]) * 60;
|
||||
if (argc > 2)
|
||||
warm = (unsigned)atoi(argv[2]) * 60;
|
||||
if (argc > 3 && strstr(argv[3], "sync"))
|
||||
sync_on = true;
|
||||
if (argc > 3 && strstr(argv[3], "jitter"))
|
||||
jitter = true;
|
||||
/* The 64 ms default buffer: the consumer's chunks are whole
|
||||
* publishes, and the pipe's fill swings by one with the phase
|
||||
* between the core and the consumer. A threshold on that fill
|
||||
* dropped healthy audio at some phases; nothing may be dropped
|
||||
* from a steady core at any phase. */
|
||||
if (argc > 3 && strstr(argv[3], "big"))
|
||||
DEV_CAPACITY = 3072;
|
||||
double t0, produced, mean;
|
||||
|
||||
/* The ring as the frontend sizes it: three publishes, and with audio
|
||||
* sync off a buffer's worth on top. */
|
||||
if (!pipeline_up((800 * 3 + (sync_on ? 0 : (DEV_CAPACITY > 800 ? DEV_CAPACITY : 800) * 2)) * 2 * sizeof(int16_t)))
|
||||
{
|
||||
printf("FAIL: could not stand the pipeline up\n");
|
||||
return 1;
|
||||
}
|
||||
for (i = 0; i < 800 * 2; i++)
|
||||
frame_audio[i] = (int16_t)((i & 1) ? 3000 : -3000);
|
||||
|
||||
pthread_create(&cons, NULL, consumer, NULL);
|
||||
|
||||
/* Three seconds of frames on an absolute 60 Hz schedule, audio sync
|
||||
* off: the producer never blocks, so anything the pipe cannot hold
|
||||
* is dropped, as it is in the frontend. */
|
||||
t0 = now_s();
|
||||
for (i = 0; i < frames; i++)
|
||||
{
|
||||
/* A late frame: one in every 120 takes 60 ms - three frames'
|
||||
* worth - as an unstable core does, and the loop, its schedule
|
||||
* kept, delivers the next two as soon as it can. */
|
||||
if (jitter && i % 120 == 60)
|
||||
sleep_until(t0 + (double)i / 60.0 + 0.060);
|
||||
else
|
||||
sleep_until(t0 + (double)i / 60.0);
|
||||
/* The settle: the device fills from empty, the controller pushes
|
||||
* to fill it, and the pipe - a large integrator against the
|
||||
* controller's gain - takes a few seconds to centre. Measured
|
||||
* from there on. */
|
||||
if (i == warm)
|
||||
{
|
||||
pthread_mutex_lock(&dev_lock);
|
||||
dev_adjust_sum = 0.0;
|
||||
dev_adjust_n = 0;
|
||||
dev_took = 0.0;
|
||||
dev_underrun = 0.0;
|
||||
pthread_mutex_unlock(&dev_lock);
|
||||
}
|
||||
if (i >= warm)
|
||||
{
|
||||
dbg_pipe += (double)retro_spsc_read_avail(&audio_driver_st.pipe_ring) / 4.0;
|
||||
dbg_avail += (double)dev_write_avail(NULL) / 4.0;
|
||||
dbg_eff += (double)retro_atomic_load_acquire_int(&audio_driver_st.pipe_ctrl_avail) / 4.0;
|
||||
dbg_n++;
|
||||
}
|
||||
audio_driver_submit(&audio_driver_st, 3.0f, frame_audio,
|
||||
sizeof(frame_audio) / sizeof(int16_t), false, false);
|
||||
audio_driver_pipeline_signal(&audio_driver_st);
|
||||
}
|
||||
if (dbg_n)
|
||||
printf(" pre-publish: pipe %.0f frames, device free %.0f frames, controller free %.0f frames (setpoint %u, target %u)\n",
|
||||
dbg_pipe / dbg_n, dbg_avail / dbg_n, dbg_eff / dbg_n, (unsigned)(DEV_CAPACITY / 2),
|
||||
(unsigned)audio_driver_pipe_target_frames(&audio_driver_st));
|
||||
sleep_until(t0 + (double)frames / 60.0 + 0.05);
|
||||
|
||||
retro_atomic_store_release_int(&consumer_run, 0);
|
||||
audio_driver_pipeline_signal(&audio_driver_st);
|
||||
pthread_join(cons, NULL);
|
||||
|
||||
produced = (double)(frames - warm) * 800.0;
|
||||
pthread_mutex_lock(&dev_lock);
|
||||
printf(" %u ms device, audio sync %s, %s: the hardware played %.1f ms of silence over %.0f s, ran dry %u times\n",
|
||||
(unsigned)(DEV_CAPACITY * 1000 / (size_t)DEV_RATE),
|
||||
sync_on ? "on " : "off", jitter ? "a core late by 60 ms every 2 s" : "a steady core",
|
||||
dev_underrun * 1000.0 / DEV_RATE, (double)(frames - warm) / 60.0, (unsigned)dev_underrun_events);
|
||||
mean = dev_adjust_n ? dev_adjust_sum / dev_adjust_n : 0.0;
|
||||
printf(" %u passes; mean rate adjustment %+.0f ppm; device took %.1f%% of %.0f frames\n",
|
||||
dev_adjust_n, (mean - 1.0) * 1e6, 100.0 * dev_took / produced, produced);
|
||||
CHECK(dev_adjust_n > 0, "the consumer ran");
|
||||
if (!jitter)
|
||||
{
|
||||
CHECK(fabs(mean - 1.0) < 500e-6,
|
||||
"the mean rate adjustment is within 500 ppm of 1.0: %+.0f ppm", (mean - 1.0) * 1e6);
|
||||
CHECK(dev_took >= produced * 0.995,
|
||||
"the device took at least 99.5%% of what was produced: %.2f%%", 100.0 * dev_took / produced);
|
||||
CHECK(dev_underrun < DEV_RATE * 0.002,
|
||||
"a steady core underran the device: %.1f ms of silence", dev_underrun * 1000.0 / DEV_RATE);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* A late core: the silence is the stalls' own, less the margin,
|
||||
* and no more - the audio that arrives late is not kept, so the
|
||||
* output does not fall behind and the controller is not pinned
|
||||
* against a backlog. With audio sync on the writer waits instead,
|
||||
* the backlog is the game slowing, and nothing is dropped. */
|
||||
double stalls = (double)(frames - warm) / 120.0;
|
||||
CHECK(dev_underrun * 1000.0 / DEV_RATE < stalls * 60.0,
|
||||
"more silence than the stalls themselves: %.1f ms over %.0f stalls", dev_underrun * 1000.0 / DEV_RATE, stalls);
|
||||
/* Pinned high is the pipe refilling to its target after the
|
||||
* silence, at rate control's pace; pinned low is a backlog of
|
||||
* late audio it can never drain, which the discard prevents. */
|
||||
if (!sync_on)
|
||||
CHECK(mean - 1.0 > -4000e-6,
|
||||
"rate control pinned against a backlog of late audio: %+.0f ppm", (mean - 1.0) * 1e6);
|
||||
else
|
||||
CHECK(dev_took >= produced * 0.995,
|
||||
"with audio sync on, late audio was dropped: %.2f%%", 100.0 * dev_took / produced);
|
||||
}
|
||||
pthread_mutex_unlock(&dev_lock);
|
||||
|
||||
/* The sink estimate, run on the producer with windows of whole
|
||||
* publishes: the clocks match, so it settles, applies, and finds
|
||||
* about nothing. Closed on the consumer it never settled - a window
|
||||
* held a fraction of a burst, thousands of ppm of phase noise - and
|
||||
* the bias stayed at zero for the session. */
|
||||
printf(" sink estimate: applied %u time(s), bias %+.0f ppm, source shown at %+.0f ppm\n",
|
||||
audio_driver_st.sink_applied, (audio_driver_st.sink_bias - 1.0) * 1e6,
|
||||
(audio_driver_st.sink_source_hz / 48000.0 - 1.0) * 1e6);
|
||||
if (!jitter)
|
||||
CHECK(audio_driver_st.sink_applied > 0, "the sink estimate never settled on the threaded pipeline");
|
||||
/* Within the device's period over the short baseline: 96 frames in
|
||||
* 3 s is 667 ppm of noise, which the real thirty seconds and the
|
||||
* session's sum reduce to tens. */
|
||||
CHECK(fabs(audio_driver_st.sink_bias - 1.0) < 700e-6,
|
||||
"the sink estimate found a clock that is not there: %+.0f ppm", (audio_driver_st.sink_bias - 1.0) * 1e6);
|
||||
|
||||
if (failures)
|
||||
{
|
||||
printf("%u failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("pipeline rate control: on pipe and device together, the controller holds the ratio at the clocks and the pipe holds\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/* Frontend, mixer and resampler symbols audio/audio_driver.c
|
||||
* references. The subject of this harness is the driver's own locking
|
||||
* around mixer_streams[], so the mixer stubs are just enough to make
|
||||
* load/play/stop/destroy produce distinguishable non-NULL handles and
|
||||
* voices - the state machine that add_stream and stop_stream drive
|
||||
* lives in audio_driver.c and is exercised for real.
|
||||
*
|
||||
* Signatures are copied from the tree's headers rather than guessed.
|
||||
* Anything the driver should not reach on these paths aborts instead
|
||||
* of quietly returning. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <audio/audio_mixer.h>
|
||||
|
||||
#include "../../../configuration.h"
|
||||
#include "../../../record/record_driver.h"
|
||||
#include "../../../runloop.h"
|
||||
#include "../../../menu/menu_driver.h"
|
||||
#include "../../../gfx/video_driver.h"
|
||||
#include "../../../command.h"
|
||||
|
||||
void RARCH_LOG(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_WARN(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_DBG(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_LOG_OUTPUT(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
void RARCH_ERR(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
printf("ERR: ");
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
bool verbosity_is_enabled(void) { return false; }
|
||||
|
||||
settings_t *config_get_ptr(void)
|
||||
{
|
||||
static settings_t settings;
|
||||
return &settings;
|
||||
}
|
||||
|
||||
static runloop_state_t runloop_st;
|
||||
runloop_state_t *runloop_state_get_ptr(void) { return &runloop_st; }
|
||||
uint32_t runloop_get_flags(void) { return 0; }
|
||||
|
||||
static recording_state_t recording_st;
|
||||
recording_state_t *recording_state_get_ptr(void) { return &recording_st; }
|
||||
|
||||
static video_driver_state_t video_st;
|
||||
video_driver_state_t *video_state_get_ptr(void) { return &video_st; }
|
||||
|
||||
static struct menu_state menu_st;
|
||||
struct menu_state *menu_state_get_ptr(void) { return &menu_st; }
|
||||
|
||||
/* --- mixer ----------------------------------------------------------- */
|
||||
|
||||
/* Distinct non-NULL cookies: the driver only ever stores and compares
|
||||
* these, and freeing them here would hide a use-after-free in the
|
||||
* driver rather than expose one, so each allocation is real. */
|
||||
/* The real audio_mixer_destroy() frees the buffer the sound was loaded
|
||||
* from for the streaming types (it owns it once loaded, unless it was
|
||||
* borrowed via data_owner). The stub mirrors that ownership so leak
|
||||
* checking over these paths means something. */
|
||||
struct audio_mixer_sound { void *owned_data; };
|
||||
struct audio_mixer_voice { int tag; };
|
||||
|
||||
static audio_mixer_sound_t *new_sound_owning(void *data)
|
||||
{
|
||||
audio_mixer_sound_t *s = (audio_mixer_sound_t*)calloc(1, sizeof(*s));
|
||||
if (s)
|
||||
s->owned_data = data;
|
||||
return s;
|
||||
}
|
||||
|
||||
static audio_mixer_sound_t *new_sound(void)
|
||||
{
|
||||
return new_sound_owning(NULL);
|
||||
}
|
||||
|
||||
static audio_mixer_voice_t *new_voice(void)
|
||||
{
|
||||
audio_mixer_voice_t *v = (audio_mixer_voice_t*)calloc(1, sizeof(*v));
|
||||
return v;
|
||||
}
|
||||
|
||||
void audio_mixer_init(unsigned rate) { (void)rate; }
|
||||
void audio_mixer_done(void) { }
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_wav(void *buffer, size_t size,
|
||||
const char *resampler_ident, enum resampler_quality quality,
|
||||
bool want_s16)
|
||||
{
|
||||
(void)buffer; (void)size; (void)resampler_ident; (void)quality;
|
||||
(void)want_s16;
|
||||
return new_sound();
|
||||
}
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_wav_stream(void *buffer,
|
||||
size_t size)
|
||||
{
|
||||
(void)size;
|
||||
return new_sound_owning(buffer);
|
||||
}
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_ogg(void *buffer, size_t size)
|
||||
{
|
||||
(void)size;
|
||||
return new_sound_owning(buffer);
|
||||
}
|
||||
|
||||
audio_mixer_sound_t *audio_mixer_load_mod(void *buffer, size_t size)
|
||||
{
|
||||
(void)size;
|
||||
return new_sound_owning(buffer);
|
||||
}
|
||||
|
||||
void audio_mixer_destroy(audio_mixer_sound_t *sound)
|
||||
{
|
||||
if (!sound)
|
||||
return;
|
||||
if (sound->owned_data)
|
||||
free(sound->owned_data);
|
||||
free(sound);
|
||||
}
|
||||
|
||||
audio_mixer_voice_t *audio_mixer_play(audio_mixer_sound_t *sound,
|
||||
bool repeat, float volume, const char *resampler_ident,
|
||||
enum resampler_quality quality, audio_mixer_stop_cb_t stop_cb)
|
||||
{
|
||||
(void)sound; (void)repeat; (void)volume; (void)resampler_ident;
|
||||
(void)quality; (void)stop_cb;
|
||||
return new_voice();
|
||||
}
|
||||
|
||||
audio_mixer_voice_t *audio_mixer_play_s16(audio_mixer_sound_t *sound,
|
||||
bool repeat, int32_t gain, enum resampler_quality quality,
|
||||
audio_mixer_stop_cb_t stop_cb)
|
||||
{
|
||||
(void)sound; (void)repeat; (void)gain; (void)quality; (void)stop_cb;
|
||||
return new_voice();
|
||||
}
|
||||
|
||||
void audio_mixer_stop(audio_mixer_voice_t *voice)
|
||||
{
|
||||
free(voice);
|
||||
}
|
||||
|
||||
void audio_mixer_mix(float *buffer, size_t num_frames,
|
||||
float gain_override, bool override)
|
||||
{
|
||||
(void)buffer; (void)num_frames; (void)gain_override; (void)override;
|
||||
}
|
||||
|
||||
void audio_mixer_mix_s16(int16_t *buffer, size_t num_frames,
|
||||
int32_t gain_override, bool override)
|
||||
{
|
||||
(void)buffer; (void)num_frames; (void)gain_override; (void)override;
|
||||
}
|
||||
|
||||
void audio_mixer_voice_set_volume(audio_mixer_voice_t *voice, float val)
|
||||
{
|
||||
(void)voice; (void)val;
|
||||
}
|
||||
|
||||
void audio_mixer_voice_set_avail(audio_mixer_voice_t *voice, size_t avail)
|
||||
{
|
||||
(void)voice; (void)avail;
|
||||
}
|
||||
|
||||
void audio_mixer_sound_set_avail(audio_mixer_sound_t *sound, size_t avail)
|
||||
{
|
||||
(void)sound; (void)avail;
|
||||
}
|
||||
|
||||
void audio_mixer_sound_set_data_owner(audio_mixer_sound_t *sound,
|
||||
void *owner, void (*owner_free)(void *owner))
|
||||
{
|
||||
(void)sound; (void)owner; (void)owner_free;
|
||||
}
|
||||
|
||||
size_t audio_mixer_voice_buffer_tell(audio_mixer_voice_t *voice)
|
||||
{
|
||||
(void)voice;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool audio_mixer_has_float_voices(void) { return true; }
|
||||
bool audio_mixer_has_s16_voices(void) { return false; }
|
||||
|
||||
/* --- everything the locking paths must not reach --------------------- */
|
||||
|
||||
static void unreachable(const char *what)
|
||||
{
|
||||
fprintf(stderr, "stub %s reached unexpectedly\n", what);
|
||||
abort();
|
||||
}
|
||||
|
||||
bool command_event(enum event_command action, void *data)
|
||||
{
|
||||
(void)action; (void)data;
|
||||
unreachable("command_event");
|
||||
return false;
|
||||
}
|
||||
|
||||
void *task_push_audio_mixer_load(const char *path,
|
||||
retro_task_callback_t cb, void *user_data, bool system,
|
||||
enum audio_mixer_slot_selection_type type, int slot)
|
||||
{
|
||||
(void)path; (void)cb; (void)user_data; (void)system; (void)type;
|
||||
(void)slot;
|
||||
unreachable("task_push_audio_mixer_load");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* --- remaining frontend references ----------------------------------- */
|
||||
|
||||
#include "../../../list_special.h"
|
||||
#include "../../../driver.h"
|
||||
#include "../../../midi_driver.h"
|
||||
#include "../../../file_path_special.h"
|
||||
#include "../../../msg_hash.h"
|
||||
#include "../../../audio/audio_thread_wrapper.h"
|
||||
|
||||
const char *char_list_new_special(enum string_list_type type, void *data)
|
||||
{
|
||||
(void)type; (void)data;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int driver_find_index(const char *label, const char *drv)
|
||||
{
|
||||
(void)label; (void)drv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool midi_driver_synth_active(void) { return false; }
|
||||
|
||||
bool midi_driver_render_audio(float *out, size_t frames, unsigned rate)
|
||||
{
|
||||
(void)out; (void)frames; (void)rate;
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *audio_thread_wrapped_ident(void *data)
|
||||
{
|
||||
(void)data;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const audio_driver_t *audio_thread_wrapped_driver(void *data)
|
||||
{
|
||||
(void)data;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool audio_init_thread(const audio_driver_t **out_driver, void **out_data,
|
||||
const char *device, unsigned out_rate, unsigned *new_rate,
|
||||
unsigned latency, unsigned block_frames, bool raise_priority,
|
||||
bool prefer_fast_cores, const audio_driver_t *driver)
|
||||
{
|
||||
(void)out_driver; (void)out_data; (void)device; (void)out_rate;
|
||||
(void)new_rate; (void)latency; (void)block_frames;
|
||||
(void)raise_priority; (void)prefer_fast_cores; (void)driver;
|
||||
unreachable("audio_init_thread");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t fill_pathname_application_special(char *s, size_t len,
|
||||
enum application_special_type type)
|
||||
{
|
||||
(void)type;
|
||||
if (len)
|
||||
s[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *msg_hash_to_str(enum msg_hash_enums msg)
|
||||
{
|
||||
(void)msg;
|
||||
return "";
|
||||
}
|
||||
|
||||
/* retro_resampler_realloc: the real one is linked in. */
|
||||
@@ -312,11 +312,33 @@ int main(void)
|
||||
CHECK(stalled_now(), "second stall: not recorded");
|
||||
CHECK(worst < 50.0, "second stall: a frame cost %.1f ms", worst);
|
||||
|
||||
/* 5. A driver's reinit request is not acted on by the producer or
|
||||
* the consumer: neither may run the reinit, which frees the state
|
||||
* lock they hold and joins the thread they may be. Frames keep
|
||||
* flowing, command_event() - which the stub aborts on - is never
|
||||
* reached, and the request is there for the runloop to take, once. */
|
||||
STAGE(6);
|
||||
retro_atomic_store_release_int(&dev_stalled, 0);
|
||||
retro_atomic_store_release_int(&audio_driver_st.reinit_request, 1);
|
||||
for (i = 0; i < 20; i++)
|
||||
produce_frame();
|
||||
CHECK(audio_driver_take_reinit_request(), "the request was consumed off the main thread");
|
||||
CHECK(!audio_driver_take_reinit_request(), "the request was not cleared when taken");
|
||||
/* The same on the frame-synchronous path: flush under the state
|
||||
* lock, on this thread. */
|
||||
retro_atomic_store_release_int(&consumer_run, 0);
|
||||
audio_driver_pipeline_wake();
|
||||
pthread_join(cons, NULL);
|
||||
AUDIO_FLAGS_CLEAR(&audio_driver_st, AUDIO_FLAG_PIPELINE_THREADED);
|
||||
audio_driver_st.pipe_threaded = false;
|
||||
retro_atomic_store_release_int(&audio_driver_st.reinit_request, 1);
|
||||
for (i = 0; i < 20; i++)
|
||||
audio_driver_submit(&audio_driver_st, 3.0f, frame_audio,
|
||||
sizeof(frame_audio) / sizeof(int16_t), false, false);
|
||||
CHECK(audio_driver_st.state_lock != NULL, "the state lock was freed under a flush");
|
||||
CHECK(audio_driver_take_reinit_request(), "the request was consumed inside flush");
|
||||
|
||||
STAGE(7);
|
||||
pipeline_down();
|
||||
|
||||
STAGE(-1);
|
||||
@@ -325,6 +347,6 @@ int main(void)
|
||||
printf("%u failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("pipeline stall: consumer takes only what it can deliver; producer drops at once once stalled\n");
|
||||
printf("pipeline stall: consumer takes only what it can deliver; producer drops at once once stalled, and neither takes a reinit request\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
/* Sink rate estimation: the frontend's slow, integral rate term.
|
||||
/* Sink rate estimation: the frontend's slow, integral rate term, put
|
||||
* through its paces.
|
||||
*
|
||||
* A scripted driver on a synthetic clock, the estimator driven with
|
||||
* the same clock, so the run is instant and every figure exact. The
|
||||
* the same clock, so every run is instant and every figure exact. The
|
||||
* device counts what it took in whole 480-frame periods, as a shared
|
||||
* WASAPI engine does - the count is only known to a period, 2500 ppm
|
||||
* of noise over four seconds, 330 over thirty - and the writer refuses
|
||||
* half a percent of what it is offered, as a non-blocking writer
|
||||
* against a small buffer does. Both were missing from the first
|
||||
* harness, and both were what the field showed: a bias of +1867 ppm
|
||||
* against a device measured at +11, and readings of -16045 and -5061
|
||||
* ppm from a device surely within a hundred. */
|
||||
* WASAPI engine does; the writer can refuse a fraction of what it is
|
||||
* offered, as a non-blocking writer against a small buffer does; the
|
||||
* core can pause, run off rate, or warm up; the threaded pipeline's
|
||||
* ring can fill and drain; the main thread can stall. Each scenario
|
||||
* is one of those, from a field report or a harness that found it,
|
||||
* and asserts what the estimator must do about it.
|
||||
*
|
||||
* Includes audio/audio_driver.c so the shipping estimator runs. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -18,14 +20,18 @@
|
||||
#include "../../../audio/audio_driver.c"
|
||||
|
||||
static unsigned failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: "); printf(__VA_ARGS__); printf("\n"); failures++; } } while (0)
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { printf(" FAIL: "); printf(__VA_ARGS__); printf("\n"); failures++; } } while (0)
|
||||
|
||||
static double dev_ppm = 0.0;
|
||||
/* --- the device and the source ---------------------------------------- */
|
||||
|
||||
static double dev_ppm = 0.0; /* the device's clock against the host's */
|
||||
static int64_t clock_usec = 0;
|
||||
static bool dev_frozen = false;
|
||||
static bool dev_frozen = false; /* the device stopped consuming */
|
||||
static size_t dev_frozen_at = 0;
|
||||
static unsigned dev_quantum = 480;
|
||||
static double drop_fraction = 0.0;
|
||||
static unsigned dev_quantum = 480; /* it counts in whole periods */
|
||||
static double drop_fraction = 0.0; /* the writer refuses this much */
|
||||
static double core_pause_sec = 0.0; /* per second: time the core produced nothing */
|
||||
static double src_ppm = 0.0; /* the source's own clock, apart from the ratio */
|
||||
|
||||
static size_t dev_frames_consumed(void *data)
|
||||
{
|
||||
@@ -53,29 +59,29 @@ static audio_driver_t scripted = {
|
||||
dev_buffer_size, NULL, NULL, dev_frames_consumed
|
||||
};
|
||||
|
||||
/* One second of synthetic time in which the frontend offered 48000 x
|
||||
* bias frames - what the resampler produces - and the driver took all
|
||||
* but the drop fraction; then the estimator runs. The write sites do
|
||||
* this accounting in the frontend; here it is done as they do. */
|
||||
static double core_pause_sec = 0.0; /* per second: time the core produced nothing */
|
||||
/* The source's own rate against the host clock, apart from the
|
||||
* resampler's ratio. Zero is a source that produces exactly nominal;
|
||||
* a real one need not - a core that misses frames, or is paced by a
|
||||
* display that is not quite its nominal rate, produces measurably
|
||||
* less or more per host second, and the estimator sees that as part
|
||||
* of the ratio it corrects. */
|
||||
static double src_ppm = 0.0;
|
||||
static void run_second(audio_driver_state_t *st)
|
||||
/* A span of synthetic time, as the write sites account for it: the
|
||||
* frontend offered 48000 x ratio frames a second, the driver took all
|
||||
* but the refused fraction, and the estimator ran. */
|
||||
static void run_usec(audio_driver_state_t *st, int64_t usec)
|
||||
{
|
||||
double ratio = st->src_ratio_curr / st->src_ratio_orig;
|
||||
double offered = 48000.0 * ratio * (1.0 + src_ppm / 1e6)
|
||||
double span = (double)usec / 1e6;
|
||||
double offered = 48000.0 * span * ratio * (1.0 + src_ppm / 1e6)
|
||||
* (1.0 - core_pause_sec);
|
||||
clock_usec += 1000000;
|
||||
clock_usec += usec;
|
||||
st->sink_offered_raw += (uint64_t)offered;
|
||||
st->sink_offered += offered / ratio;
|
||||
st->sink_accepted += (uint64_t)(offered * (1.0 - drop_fraction));
|
||||
audio_driver_sink_refused(st);
|
||||
audio_driver_sink_update(st, clock_usec);
|
||||
}
|
||||
static void run_second(audio_driver_state_t *st) { run_usec(st, 1000000); }
|
||||
static void run_seconds(audio_driver_state_t *st, int n)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < n; i++)
|
||||
run_second(st);
|
||||
}
|
||||
|
||||
static void reset(audio_driver_state_t *st, bool control)
|
||||
{
|
||||
@@ -86,13 +92,14 @@ static void reset(audio_driver_state_t *st, bool control)
|
||||
st->src_ratio_curr = 1.0;
|
||||
st->sink_bias = 1.0;
|
||||
st->rate_control_delta = 0.005f;
|
||||
/* A non-blocking writer: the source has its own clock. The blocking
|
||||
* case is its own test below. */
|
||||
/* A non-blocking writer: the source has its own clock. */
|
||||
AUDIO_FLAGS_SET(st, AUDIO_FLAG_ACTIVE | AUDIO_FLAG_NONBLOCK);
|
||||
if (control)
|
||||
AUDIO_FLAGS_SET(st, AUDIO_FLAG_CONTROL);
|
||||
clock_usec = 0;
|
||||
dev_ppm = 0.0;
|
||||
dev_frozen = false;
|
||||
dev_quantum = 480;
|
||||
drop_fraction = 0.0;
|
||||
core_pause_sec = 0.0;
|
||||
config_get_ptr()->bools.audio_sink_rate_estimation = true;
|
||||
@@ -101,116 +108,150 @@ static void reset(audio_driver_state_t *st, bool control)
|
||||
}
|
||||
|
||||
static double bias_ppm(const audio_driver_state_t *st) { return (st->sink_bias - 1.0) * 1e6; }
|
||||
static double dev_meas_ppm(const audio_driver_state_t *st) { return (st->sink_rate_hz / 48000.0 - 1.0) * 1e6; }
|
||||
static double src_meas_ppm(const audio_driver_state_t *st) { return (st->sink_source_hz / 48000.0 - 1.0) * 1e6; }
|
||||
|
||||
int main(void)
|
||||
/* --- the clocks ------------------------------------------------------- */
|
||||
|
||||
/* A device 120 ppm fast, counting in periods, rate control off. Nothing
|
||||
* before thirty seconds; the first application within a period's worth
|
||||
* of noise over thirty seconds; after five minutes within 40. */
|
||||
static void s_fast_device(audio_driver_state_t *st)
|
||||
{
|
||||
audio_driver_state_t *st = &audio_driver_st;
|
||||
int i;
|
||||
|
||||
/* 1. A device 120 ppm fast, counting in 480-frame periods, rate
|
||||
* control off. Nothing is applied before thirty seconds; the
|
||||
* first application is within the period's worth of noise over
|
||||
* thirty seconds - 333 ppm - and after five minutes the baseline
|
||||
* has grown enough that it is within 40. */
|
||||
reset(st, false);
|
||||
dev_ppm = 120.0;
|
||||
for (i = 0; i < 29; i++)
|
||||
run_second(st);
|
||||
run_seconds(st, 29);
|
||||
CHECK(st->sink_applied == 0 && st->sink_bias == 1.0, "a bias was applied before thirty seconds");
|
||||
for (; i < 34; i++)
|
||||
run_second(st);
|
||||
printf(" +120 ppm device, 480-frame periods: after %d s bias %+.0f ppm, measured %.1f Hz\n",
|
||||
i, bias_ppm(st), st->sink_rate_hz);
|
||||
CHECK(st->sink_applied >= 1, "no bias applied by 34 s");
|
||||
run_seconds(st, 9);
|
||||
printf(" at 38 s: bias %+.0f ppm, device measured %+.0f ppm\n", bias_ppm(st), dev_meas_ppm(st));
|
||||
CHECK(st->sink_applied >= 1, "no bias applied by 38 s");
|
||||
CHECK(fabs(bias_ppm(st) - 120.0) < 400.0, "first application %+.0f ppm, off by more than a period's worth", bias_ppm(st));
|
||||
for (; i < 300; i++)
|
||||
run_second(st);
|
||||
printf(" after %d s: bias %+.0f ppm, measured %.1f Hz, applied %u times\n",
|
||||
i, bias_ppm(st), st->sink_rate_hz, st->sink_applied);
|
||||
run_seconds(st, 262);
|
||||
printf(" at 300 s: bias %+.0f ppm, applied %u times\n", bias_ppm(st), st->sink_applied);
|
||||
CHECK(fabs(bias_ppm(st) - 120.0) < 40.0, "after five minutes bias %+.0f ppm, expected +120", bias_ppm(st));
|
||||
CHECK(fabs(st->src_ratio_curr - st->sink_bias) < 1e-12, "ratio not set from the bias with rate control off");
|
||||
CHECK(fabs(audio_driver_sink_bias(st) - st->sink_bias) < 1e-7, "the bias the resampler reads is not the bias");
|
||||
}
|
||||
|
||||
/* 2. The same device with a writer that refuses half a percent of
|
||||
* what it is offered. The ratio is built on offered frames, so the
|
||||
* bias still finds the device; the refused frames are warned
|
||||
* about, not chased. Before this it read the refusal as a device
|
||||
* 5000 ppm fast and sped the resampler up to match. */
|
||||
/* The other direction. */
|
||||
static void s_slow_device(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
dev_ppm = -80.0;
|
||||
run_seconds(st, 300);
|
||||
printf(" bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) + 80.0) < 40.0, "bias %+.0f ppm, expected -80", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* Two hours: the sums are doubles over a session, and must neither
|
||||
* drift nor lose the crystal. */
|
||||
static void s_long_session(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
dev_ppm = 37.0;
|
||||
run_seconds(st, 7200);
|
||||
printf(" after two hours: bias %+.0f ppm, applied %u times\n", bias_ppm(st), st->sink_applied);
|
||||
CHECK(fabs(bias_ppm(st) - 37.0) < 5.0, "after two hours bias %+.0f ppm, expected +37", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* The estimator is driven from every write; a core writing per frame
|
||||
* and one writing per second must measure the same clock. */
|
||||
static void s_write_cadence(audio_driver_state_t *st)
|
||||
{
|
||||
double per_frame, per_second;
|
||||
int i;
|
||||
reset(st, false);
|
||||
dev_ppm = 120.0;
|
||||
for (i = 0; i < 300 * 60; i++)
|
||||
run_usec(st, 16667);
|
||||
per_frame = bias_ppm(st);
|
||||
reset(st, false);
|
||||
dev_ppm = 120.0;
|
||||
run_seconds(st, 300);
|
||||
per_second = bias_ppm(st);
|
||||
printf(" per frame %+.0f ppm, per second %+.0f ppm\n", per_frame, per_second);
|
||||
CHECK(fabs(per_frame - per_second) < 30.0, "the write cadence changed the estimate: %+.0f vs %+.0f", per_frame, per_second);
|
||||
}
|
||||
|
||||
/* --- what is not a clock ---------------------------------------------- */
|
||||
|
||||
/* A writer refusing half a percent: the ratio is on offered frames, so
|
||||
* the bias still finds the device; the refusal is warned about once. */
|
||||
static void s_refused_frames(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
dev_ppm = 120.0;
|
||||
drop_fraction = 0.005;
|
||||
for (i = 0; i < 300; i++)
|
||||
run_second(st);
|
||||
printf(" +120 ppm device, 0.5%% refused: bias %+.0f ppm, drop warning %s\n",
|
||||
bias_ppm(st), st->sink_drop_warned ? "raised" : "not raised");
|
||||
run_seconds(st, 300);
|
||||
printf(" bias %+.0f ppm, drop warning %s\n", bias_ppm(st),
|
||||
(st->sink_warned & AUDIO_SINK_WARNED_DROPPED) ? "raised" : "not raised");
|
||||
CHECK(fabs(bias_ppm(st) - 120.0) < 40.0, "with refused frames the bias is %+.0f ppm, expected +120", bias_ppm(st));
|
||||
CHECK(st->sink_drop_warned, "half a percent refused for five minutes and no warning");
|
||||
CHECK(st->sink_warned & AUDIO_SINK_WARNED_DROPPED, "half a percent refused for five minutes and no warning");
|
||||
}
|
||||
|
||||
/* 3. A device 80 ppm slow: the other direction. */
|
||||
reset(st, false);
|
||||
dev_ppm = -80.0;
|
||||
for (i = 0; i < 300; i++)
|
||||
run_second(st);
|
||||
printf(" -80 ppm device: bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) + 80.0) < 40.0, "bias %+.0f ppm, expected -80", bias_ppm(st));
|
||||
|
||||
/* 4. A ratio too far off to be a crystal: refused, not clamped.
|
||||
*
|
||||
* This used to assert the clamp, and the clamp is what made
|
||||
* CoreAudio crackle about a minute in: a bias of 2000 ppm empties
|
||||
* a 64 ms buffer in thirty-two seconds, and the first application
|
||||
* lands at sixty. No crystal is 5000 ppm out - a ratio like this
|
||||
* means the two counts are not measuring the same thing - so the
|
||||
* only safe reading is to leave the resampler alone. The rate is
|
||||
* still measured and still reported, which is what a mismeasuring
|
||||
* driver needs in order to be found. */
|
||||
/* Too far off to be a crystal: refused, never clamped, and the rate is
|
||||
* still measured and shown so a mismeasuring driver can be found. */
|
||||
static void s_implausible_device(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
dev_ppm = 5000.0;
|
||||
for (i = 0; i < 120; i++)
|
||||
run_second(st);
|
||||
printf(" +5000 ppm device: bias %+.0f ppm (refused as implausible)\n", bias_ppm(st));
|
||||
CHECK(fabs(st->sink_bias - 1.0) < 1e-9,
|
||||
"bias %.6f, expected no bias at all", st->sink_bias);
|
||||
CHECK(st->sink_rate_hz > 48000.0,
|
||||
"the rate must still be measured and reported, got %.1f", st->sink_rate_hz);
|
||||
run_seconds(st, 120);
|
||||
printf(" +5000 ppm: bias %+.0f ppm, device shown at %+.0f ppm\n", bias_ppm(st), dev_meas_ppm(st));
|
||||
CHECK(fabs(st->sink_bias - 1.0) < 1e-9, "bias %.6f, expected none at all", st->sink_bias);
|
||||
CHECK(fabs(dev_meas_ppm(st) - 5000.0) < 500.0, "the rate must still be measured and shown, got %+.0f", dev_meas_ppm(st));
|
||||
CHECK(st->sink_warned & AUDIO_SINK_WARNED_IMPLAUSIBLE, "no refusal was logged");
|
||||
|
||||
/* 4b. Either side of the plausibility line, so the line itself is
|
||||
* covered rather than just the far side of it. */
|
||||
reset(st, false);
|
||||
dev_ppm = 400.0;
|
||||
for (i = 0; i < 300; i++)
|
||||
run_second(st);
|
||||
printf(" +400 ppm device: bias %+.0f ppm (inside the plausible range)\n", bias_ppm(st));
|
||||
CHECK(bias_ppm(st) > 200.0, "a 400 ppm device must still be corrected, got %+.0f", bias_ppm(st));
|
||||
run_seconds(st, 300);
|
||||
printf(" +400 ppm: bias %+.0f ppm (inside the band)\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 400.0) < 60.0, "+400 ppm: bias %+.0f, expected +400", bias_ppm(st));
|
||||
|
||||
reset(st, false);
|
||||
dev_ppm = 900.0;
|
||||
for (i = 0; i < 300; i++)
|
||||
run_second(st);
|
||||
printf(" +900 ppm device: bias %+.0f ppm (outside it)\n", bias_ppm(st));
|
||||
CHECK(fabs(st->sink_bias - 1.0) < 1e-9,
|
||||
"a 900 ppm ratio is not a crystal; expected no bias, got %.6f", st->sink_bias);
|
||||
run_seconds(st, 300);
|
||||
printf(" +900 ppm: bias %+.0f ppm (outside it)\n", bias_ppm(st));
|
||||
CHECK(st->sink_bias == 1.0, "+900 ppm: bias %+.0f, expected refused", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* 5. Rate control on, with a non-blocking writer: the fill sits
|
||||
* pinned full and rate control says "slow" by its whole delta
|
||||
* forever - the buffer, not the clock. Its adjustment is in the
|
||||
* ratio the frontend resamples by and is divided out of the
|
||||
* offered count; the bias must still find the device's +150 ppm,
|
||||
* where before it walked to the clamp absorbing the mean. */
|
||||
/* A source paced at the display's rate rather than its own - video
|
||||
* sync off, a 60.00 Hz panel against a 59.94 core - is +1000 ppm for
|
||||
* the whole session. It never comes into the band, no bias is ever
|
||||
* applied, the rates are shown, and it is said once. */
|
||||
static void s_source_at_display_rate(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
dev_ppm = 20.0;
|
||||
src_ppm = 1000.0;
|
||||
run_seconds(st, 180);
|
||||
printf(" bias %+.0f ppm, source shown at %+.0f ppm, device at %+.0f\n",
|
||||
bias_ppm(st), src_meas_ppm(st), dev_meas_ppm(st));
|
||||
CHECK(st->sink_bias == 1.0 && st->sink_applied == 0, "a display-paced source was biased: %+.0f ppm", bias_ppm(st));
|
||||
CHECK(fabs(src_meas_ppm(st) - 1000.0) < 100.0, "the source's rate is not shown: %+.0f", src_meas_ppm(st));
|
||||
CHECK(st->sink_warned & AUDIO_SINK_WARNED_UNSETTLED, "a source off the band for three minutes was never said");
|
||||
}
|
||||
|
||||
/* Rate control on with a non-blocking writer: its adjustment sits at
|
||||
* its bound and is in the ratio; divided out of the offered count, the
|
||||
* bias must still find the device's +150. */
|
||||
static void s_rate_control_pinned(audio_driver_state_t *st)
|
||||
{
|
||||
int i;
|
||||
reset(st, true);
|
||||
dev_ppm = 150.0;
|
||||
for (i = 0; i < 300; i++)
|
||||
{
|
||||
/* What compute_rate_adjust() would set: orig x adjust x bias. */
|
||||
st->src_ratio_curr = st->src_ratio_orig * (1.0 - 0.005) * st->sink_bias;
|
||||
run_second(st);
|
||||
}
|
||||
printf(" rate control pinned slow, +150 ppm device: bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 150.0) < 40.0, "bias %+.0f ppm, expected +150; rate control's pinned adjustment leaked in", bias_ppm(st));
|
||||
printf(" bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 150.0) < 40.0, "bias %+.0f ppm, expected +150; rate control's adjustment leaked in", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* 5b. Save-state loads: the core pauses half a second every ten,
|
||||
* the device plays on. Those windows are excluded, and the bias
|
||||
* still finds the device. Before, the 1.5%% shortfall read as a
|
||||
* device 15000 ppm fast and the bias hit the clamp. */
|
||||
/* The core pauses half a second every ten - save states loading - and
|
||||
* the device plays on. Those windows are out; the bias finds +120. */
|
||||
static void s_core_pauses(audio_driver_state_t *st)
|
||||
{
|
||||
int i;
|
||||
reset(st, false);
|
||||
dev_ppm = 120.0;
|
||||
for (i = 0; i < 300; i++)
|
||||
@@ -218,175 +259,271 @@ int main(void)
|
||||
core_pause_sec = (i % 10 == 0) ? 0.5 : 0.0;
|
||||
run_second(st);
|
||||
}
|
||||
printf(" +120 ppm device with a half-second pause every ten: bias %+.0f ppm\n", bias_ppm(st));
|
||||
core_pause_sec = 0.0;
|
||||
printf(" bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 120.0) < 40.0, "bias %+.0f ppm, expected +120; pauses leaked into the ratio", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* 6. A stall in the middle - the device frozen for a check window -
|
||||
* starts the baseline over and moves nothing. */
|
||||
/* The main thread held for 50 ms once every two minutes - a state save,
|
||||
* a shader built on first use - with the device at rate. In a window
|
||||
* that is a source 1.25% slow; diluted it is -416 ppm, plausible. The
|
||||
* source has to be within the band in every kept window. */
|
||||
static void s_main_thread_stalls(audio_driver_state_t *st)
|
||||
{
|
||||
int i;
|
||||
reset(st, false);
|
||||
run_seconds(st, 40);
|
||||
for (i = 0; i < 240; i++)
|
||||
{
|
||||
core_pause_sec = (i % 120 == 0) ? 0.05 : 0.0;
|
||||
run_second(st);
|
||||
}
|
||||
core_pause_sec = 0.0;
|
||||
printf(" bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st)) < 60.0, "the stalls were summed as a slow source: bias %+.0f ppm", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* The device frozen for a window - a stall - moves nothing and is left
|
||||
* out; the estimate goes on from the next window. */
|
||||
static void s_device_stall(audio_driver_state_t *st)
|
||||
{
|
||||
double before;
|
||||
reset(st, false);
|
||||
dev_ppm = 100.0;
|
||||
for (i = 0; i < 40; i++)
|
||||
run_second(st);
|
||||
{
|
||||
double before = st->sink_bias;
|
||||
dev_frozen_at = dev_frames_consumed(NULL);
|
||||
dev_frozen = true;
|
||||
for (i = 0; i < 5; i++)
|
||||
run_second(st);
|
||||
CHECK(st->sink_bias == before, "a stalled window moved the bias");
|
||||
CHECK(st->sink_discarded >= 1, "a stalled window was not discarded");
|
||||
dev_frozen = false;
|
||||
}
|
||||
run_seconds(st, 40);
|
||||
before = st->sink_bias;
|
||||
dev_frozen_at = dev_frames_consumed(NULL);
|
||||
dev_frozen = true;
|
||||
run_seconds(st, 5);
|
||||
CHECK(st->sink_bias == before, "a stalled window moved the bias");
|
||||
CHECK(st->sink_discarded >= 1, "a stalled window was not left out");
|
||||
dev_frozen = false;
|
||||
run_seconds(st, 200);
|
||||
printf(" bias %+.0f ppm after the stall\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 100.0) < 40.0, "after a stall the bias is %+.0f, expected +100", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* 6b. A blocking writer: the core runs as fast as the resampler
|
||||
* drains, so the source's rate is the device's over the ratio and
|
||||
* carries no clock. A bias set from it feeds back - the field
|
||||
* showed the source climbing +2300 to +3450 ppm with the bias at
|
||||
* the clamp. The rate is measured; the bias is not set. */
|
||||
/* A source slow for its first half minute - a core warming up after
|
||||
* load - then at nominal. The sums open only once the source has been
|
||||
* in the band for two windows, so the warm-up is never in them. */
|
||||
static void s_slow_start(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
src_ppm = -1500.0;
|
||||
run_seconds(st, 12);
|
||||
CHECK(st->sink_rate_hz > 0.0 && src_meas_ppm(st) < -1000.0,
|
||||
"unsettled, the rates are not shown or wrong: device %.1f source %+.0f ppm", st->sink_rate_hz, src_meas_ppm(st));
|
||||
run_seconds(st, 18);
|
||||
src_ppm = 0.0;
|
||||
run_seconds(st, 90);
|
||||
printf(" bias %+.0f ppm at 120 s, source shown at %+.0f ppm\n", bias_ppm(st), src_meas_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st)) < 60.0, "the slow start is in the estimate two minutes on: bias %+.0f ppm", bias_ppm(st));
|
||||
CHECK(st->sink_applied > 0, "the bias was never applied after the source settled");
|
||||
CHECK(fabs(src_meas_ppm(st)) < 100.0, "the shown source still carries the warm-up: %+.0f ppm", src_meas_ppm(st));
|
||||
}
|
||||
|
||||
/* --- the threaded pipeline ---------------------------------------------- */
|
||||
|
||||
/* On the threaded pipeline the source is counted where it is exact:
|
||||
* what the core published into the ring, at the producer, where the
|
||||
* windows close. Neither the ring's occupancy nor what it refused is
|
||||
* in the count, so a window holds whole publishes. Modelled here as
|
||||
* publishes of 800 frames every 1/60 s with a device at +50 ppm: the
|
||||
* bias must find the device, not the burst. The end-to-end run, with
|
||||
* the shipping producer and consumer against a real-time device, is
|
||||
* samples/audio/pipeline_rate_control. */
|
||||
static void s_publishes(audio_driver_state_t *st)
|
||||
{
|
||||
int i;
|
||||
reset(st, true);
|
||||
st->pipe_threaded = true;
|
||||
dev_ppm = 50.0;
|
||||
for (i = 0; i < 300 * 60; i++)
|
||||
{
|
||||
clock_usec += 16667;
|
||||
st->sink_offered += 800.0 * st->src_ratio_orig;
|
||||
audio_driver_sink_update(st, clock_usec);
|
||||
}
|
||||
printf(" bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 50.0) < 40.0, "publishes measured as a source: bias %+.0f ppm, expected +50", bias_ppm(st));
|
||||
st->pipe_threaded = false;
|
||||
}
|
||||
|
||||
/* --- modes and lifetimes ---------------------------------------------- */
|
||||
|
||||
/* A blocking writer: the core runs as fast as the resampler drains, so
|
||||
* the source carries no clock of its own and a bias would feed back.
|
||||
* The rate is measured and shown; nothing is applied. */
|
||||
static void s_blocking_writer(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
AUDIO_FLAGS_CLEAR(st, AUDIO_FLAG_NONBLOCK);
|
||||
dev_ppm = 120.0;
|
||||
for (i = 0; i < 120; i++)
|
||||
run_second(st);
|
||||
printf(" blocking writer, +120 ppm device: measured %.1f Hz, bias %+.0f ppm\n", st->sink_rate_hz, bias_ppm(st));
|
||||
CHECK(fabs(st->sink_rate_hz - 48005.76) < 5.0, "blocking: the rate was not measured (%.1f)", st->sink_rate_hz);
|
||||
run_seconds(st, 120);
|
||||
printf(" measured %+.0f ppm, bias %+.0f ppm\n", dev_meas_ppm(st), bias_ppm(st));
|
||||
CHECK(fabs(dev_meas_ppm(st) - 120.0) < 60.0, "blocking: the rate was not measured (%+.0f)", dev_meas_ppm(st));
|
||||
CHECK(st->sink_bias == 1.0, "blocking: a bias was applied, %+.0f ppm", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* 7. Disabled: nothing at all. */
|
||||
/* Off: nothing at all, and no rates shown. Turned off mid-session the
|
||||
* bias is dropped at once; turned back on, it is measured afresh. */
|
||||
static void s_disabled_and_toggled(audio_driver_state_t *st)
|
||||
{
|
||||
reset(st, false);
|
||||
config_get_ptr()->bools.audio_sink_rate_estimation = false;
|
||||
dev_ppm = 120.0;
|
||||
for (i = 0; i < 60; i++)
|
||||
run_second(st);
|
||||
run_seconds(st, 60);
|
||||
CHECK(st->sink_bias == 1.0 && st->sink_applied == 0, "disabled, yet the bias moved");
|
||||
CHECK(st->sink_rate_hz == 0.0, "disabled, yet a rate is shown");
|
||||
|
||||
reset(st, false);
|
||||
dev_ppm = 120.0;
|
||||
run_seconds(st, 120);
|
||||
CHECK(st->sink_bias != 1.0, "no bias to drop");
|
||||
config_get_ptr()->bools.audio_sink_rate_estimation = false;
|
||||
run_second(st);
|
||||
CHECK(st->sink_bias == 1.0 && audio_driver_sink_bias(st) == 1.0, "turned off, the bias stayed");
|
||||
config_get_ptr()->bools.audio_sink_rate_estimation = true;
|
||||
run_seconds(st, 120);
|
||||
printf(" off then on: bias %+.0f ppm\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 120.0) < 60.0, "turned back on, the bias is %+.0f, expected +120 afresh", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* A driver reinit starts the estimate over - the counts are the new
|
||||
* driver's - and the bias stands until re-derived. */
|
||||
static void s_driver_restart(audio_driver_state_t *st)
|
||||
{
|
||||
double before;
|
||||
reset(st, false);
|
||||
dev_ppm = 90.0;
|
||||
run_seconds(st, 120);
|
||||
before = st->sink_bias;
|
||||
st->sink_started = 0; /* what init does, with the counts */
|
||||
st->sink_offered = 0.0; st->sink_offered_raw = 0; st->sink_accepted = 0;
|
||||
clock_usec += 500000;
|
||||
run_second(st);
|
||||
CHECK(st->sink_bias == before, "a restart dropped the bias before it was re-derived");
|
||||
run_seconds(st, 200);
|
||||
printf(" bias %+.0f ppm after the restart\n", bias_ppm(st));
|
||||
CHECK(fabs(bias_ppm(st) - 90.0) < 40.0, "after a restart the bias is %+.0f, expected +90", bias_ppm(st));
|
||||
}
|
||||
|
||||
/* --- the buffer it corrects ------------------------------------------- */
|
||||
|
||||
/* From a CoreAudio field report: a 10.7 ms buffer, the device at +47,
|
||||
* the source at -442, audio breaking up about a minute in, when the
|
||||
* first bias lands. The buffer is modelled: it is empty long before
|
||||
* any bias exists, so whatever broke it was not the correction - and
|
||||
* the correction, when it comes, is what would hold it. */
|
||||
static void s_small_buffer_report(audio_driver_state_t *st)
|
||||
{
|
||||
const double cap = 0.0107 * 48000.0;
|
||||
int sink_on, i;
|
||||
for (sink_on = 0; sink_on < 2; sink_on++)
|
||||
{
|
||||
double level = cap * 0.5;
|
||||
int unders = 0, first = -1;
|
||||
reset(st, false);
|
||||
dev_ppm = 47.0;
|
||||
src_ppm = -442.0;
|
||||
for (i = 0; i < 180; i++)
|
||||
{
|
||||
double ratio, delivered, consumed;
|
||||
if (sink_on)
|
||||
run_second(st);
|
||||
else
|
||||
clock_usec += 1000000;
|
||||
ratio = st->src_ratio_curr / st->src_ratio_orig;
|
||||
delivered = 48000.0 * ratio * (1.0 + src_ppm / 1e6);
|
||||
consumed = 48000.0 * (1.0 + dev_ppm / 1e6);
|
||||
level += delivered - consumed;
|
||||
if (level < 0.0)
|
||||
{
|
||||
unders++;
|
||||
if (first < 0)
|
||||
first = i + 1;
|
||||
level = 0.0;
|
||||
}
|
||||
else if (level > cap)
|
||||
level = cap;
|
||||
}
|
||||
printf(" estimation %s: first underrun at %d s, %d underrunning second(s), bias %+.0f ppm\n",
|
||||
sink_on ? "on " : "off", first, unders, bias_ppm(st));
|
||||
CHECK(first > 0 && first < 30, "expected the buffer to drain before any bias could be applied, first underrun at %d s", first);
|
||||
if (sink_on)
|
||||
CHECK(bias_ppm(st) > 300.0, "the correction should have found the mismatch, got %+.0f ppm", bias_ppm(st));
|
||||
}
|
||||
}
|
||||
|
||||
/* A correction every thirty seconds holds only a buffer that lasts
|
||||
* thirty seconds of the mismatch; a smaller one is said so, once. */
|
||||
static void s_cadence_warning(audio_driver_state_t *st)
|
||||
{
|
||||
struct { size_t bytes; double ppm; const char *what; int want; } cases[] = {
|
||||
{ (size_t)(0.0107 * 48000) * 4, 490.0, "10.7 ms, 490 ppm apart", 1 },
|
||||
{ (size_t)(0.0107 * 48000) * 4, 11.0, "10.7 ms, 11 ppm apart", 0 },
|
||||
{ (size_t)(0.200 * 48000) * 4, 490.0, "200 ms, 490 ppm apart", 0 }
|
||||
};
|
||||
size_t c;
|
||||
for (c = 0; c < sizeof(cases) / sizeof(cases[0]); c++)
|
||||
{
|
||||
int fired;
|
||||
reset(st, false);
|
||||
st->buffer_size = cases[c].bytes;
|
||||
dev_ppm = cases[c].ppm;
|
||||
run_seconds(st, 120);
|
||||
fired = (st->sink_warned & AUDIO_SINK_WARNED_TOO_SLOW) != 0;
|
||||
printf(" %-24s -> %s\n", cases[c].what, fired ? "too small for the cadence" : "the cadence can hold it");
|
||||
CHECK(fired == cases[c].want, "%s: expected the warning to %sfire", cases[c].what, cases[c].want ? "" : "not ");
|
||||
}
|
||||
}
|
||||
|
||||
/* --- the runner ------------------------------------------------------- */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *name;
|
||||
void (*run)(audio_driver_state_t *st);
|
||||
} scenario_t;
|
||||
|
||||
static const scenario_t scenarios[] = {
|
||||
{ "a device 120 ppm fast, counting in periods", s_fast_device },
|
||||
{ "a device 80 ppm slow", s_slow_device },
|
||||
{ "two hours at +37 ppm", s_long_session },
|
||||
{ "written per frame and per second", s_write_cadence },
|
||||
{ "a writer refusing half a percent", s_refused_frames },
|
||||
{ "ratios too far off to be a crystal", s_implausible_device },
|
||||
{ "a source paced at the display's rate", s_source_at_display_rate },
|
||||
{ "rate control pinned at its bound", s_rate_control_pinned },
|
||||
{ "the core pausing half a second every ten", s_core_pauses },
|
||||
{ "the main thread stalling 50 ms every two minutes", s_main_thread_stalls },
|
||||
{ "the device frozen for a window", s_device_stall },
|
||||
{ "a slow start, then nominal", s_slow_start },
|
||||
{ "publishes counted at the producer", s_publishes },
|
||||
{ "a blocking writer", s_blocking_writer },
|
||||
{ "disabled, and toggled off and on", s_disabled_and_toggled },
|
||||
{ "a driver restart", s_driver_restart },
|
||||
{ "the CoreAudio small-buffer report", s_small_buffer_report },
|
||||
{ "the correction's cadence against the buffer", s_cadence_warning },
|
||||
};
|
||||
|
||||
int main(void)
|
||||
{
|
||||
size_t i;
|
||||
printf("sink rate estimation, %u scenarios:\n", (unsigned)(sizeof(scenarios) / sizeof(scenarios[0])));
|
||||
for (i = 0; i < sizeof(scenarios) / sizeof(scenarios[0]); i++)
|
||||
{
|
||||
unsigned before = failures;
|
||||
printf(" %s\n", scenarios[i].name);
|
||||
scenarios[i].run(&audio_driver_st);
|
||||
if (failures != before)
|
||||
printf(" -> FAILED\n");
|
||||
}
|
||||
if (failures)
|
||||
{
|
||||
printf("%u failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 10. Does the bias cause pops, or prevent them?
|
||||
*
|
||||
* From a field report on CoreAudio: an 8 ms latency setting
|
||||
* giving a 10.7 ms device buffer, the device measured at +47 ppm
|
||||
* - an ordinary crystal - and the source at -442 ppm. Audio
|
||||
* broke up about a minute in, which is also about when the first
|
||||
* bias lands, so the bias was the obvious suspect.
|
||||
*
|
||||
* The buffer is modelled here rather than the estimator alone:
|
||||
* each second the source delivers at its rate times the
|
||||
* resampler's ratio and the device takes at its own, and the
|
||||
* level moves by the difference. An underrun is a pop. The point
|
||||
* is to separate "the correction broke it" from "it was already
|
||||
* draining and the correction is what stops it". */
|
||||
{
|
||||
const double cap = 0.0107 * 48000.0; /* the reported 10.7 ms */
|
||||
int sink_on;
|
||||
|
||||
for (sink_on = 0; sink_on < 2; sink_on++)
|
||||
{
|
||||
double level = cap * 0.5;
|
||||
int unders = 0;
|
||||
int first = -1;
|
||||
|
||||
reset(st, false);
|
||||
dev_ppm = 47.0;
|
||||
src_ppm = -442.0;
|
||||
if (!sink_on)
|
||||
{
|
||||
/* Estimation off: nothing ever moves the ratio. */
|
||||
st->sink_bias = 1.0;
|
||||
}
|
||||
|
||||
for (i = 0; i < 180; i++)
|
||||
{
|
||||
double ratio, delivered, consumed;
|
||||
if (sink_on)
|
||||
run_second(st);
|
||||
else
|
||||
clock_usec += 1000000;
|
||||
|
||||
ratio = st->src_ratio_curr / st->src_ratio_orig;
|
||||
delivered = 48000.0 * ratio * (1.0 + src_ppm / 1e6);
|
||||
consumed = 48000.0 * (1.0 + dev_ppm / 1e6);
|
||||
level += delivered - consumed;
|
||||
|
||||
if (level < 0.0)
|
||||
{
|
||||
unders++;
|
||||
if (first < 0)
|
||||
first = i + 1;
|
||||
level = 0.0;
|
||||
}
|
||||
else if (level > cap)
|
||||
level = cap;
|
||||
}
|
||||
|
||||
if (first < 0)
|
||||
printf(" %-20s no underruns in 180 s, bias %+.0f ppm\n",
|
||||
sink_on ? "estimation on:" : "estimation off:", bias_ppm(st));
|
||||
else
|
||||
printf(" %-20s first underrun at %d s, %d underrunning second(s), bias %+.0f ppm\n",
|
||||
sink_on ? "estimation on:" : "estimation off:",
|
||||
first, unders, bias_ppm(st));
|
||||
|
||||
/* The finding, asserted so it cannot quietly stop being true:
|
||||
* the buffer is empty long before the first correction exists.
|
||||
* A 490 ppm mismatch drains 10.7 ms from half full in about
|
||||
* eleven seconds, and the earliest a bias can be applied is
|
||||
* thirty. Whatever breaks up the audio here, it is not the
|
||||
* correction - it has not happened yet. */
|
||||
CHECK(first > 0 && first < 30,
|
||||
"%s: expected the buffer to drain before any bias could be "
|
||||
"applied, first underrun at %d s",
|
||||
sink_on ? "estimation on" : "estimation off", first);
|
||||
if (sink_on)
|
||||
CHECK(bias_ppm(st) > 300.0,
|
||||
"the correction should have found the mismatch, got %+.0f ppm",
|
||||
bias_ppm(st));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 11. The correction's cadence against the buffer it corrects.
|
||||
*
|
||||
* Thirty seconds between corrections is a floor, not a choice: a
|
||||
* device that counts in whole periods is only known to a period,
|
||||
* so applying sooner would apply noise. A buffer that empties
|
||||
* inside that floor therefore cannot be held by this mechanism,
|
||||
* and the frontend says so once rather than pretending. Checked
|
||||
* both ways round, since a warning that always fires is no more
|
||||
* use than one that never does. */
|
||||
{
|
||||
struct { size_t bytes; double ppm; const char *what; int want; } cases[] = {
|
||||
/* 10.7 ms of int16 stereo at 48 kHz, the field report's buffer */
|
||||
{ (size_t)(0.0107 * 48000) * 4, 490.0,
|
||||
"10.7 ms buffer, 490 ppm apart", 1 },
|
||||
/* the same buffer, a mismatch small enough to outlast the cadence */
|
||||
{ (size_t)(0.0107 * 48000) * 4, 11.0,
|
||||
"10.7 ms buffer, 11 ppm apart", 0 },
|
||||
/* a roomy buffer with the same large mismatch */
|
||||
{ (size_t)(0.200 * 48000) * 4, 490.0,
|
||||
"200 ms buffer, 490 ppm apart", 0 }
|
||||
};
|
||||
size_t c;
|
||||
|
||||
for (c = 0; c < sizeof(cases) / sizeof(cases[0]); c++)
|
||||
{
|
||||
double buffer_sec = (double)cases[c].bytes / 4.0 / 48000.0;
|
||||
double drain_sec = (buffer_sec * 0.5) / (cases[c].ppm / 1e6);
|
||||
int fires = drain_sec < 30.0;
|
||||
|
||||
printf(" %-32s half drains in %5.1f s -> %s\n",
|
||||
cases[c].what, drain_sec,
|
||||
fires ? "too small for the cadence" : "the cadence can hold it");
|
||||
CHECK(fires == cases[c].want,
|
||||
"%s: expected the warning to %sfire", cases[c].what,
|
||||
cases[c].want ? "" : "not ");
|
||||
}
|
||||
}
|
||||
|
||||
printf("sink rate: measured through period-quantised counts and refused frames, converged, implausible ratios refused, rate control's pinned adjustment and pauses kept out\n");
|
||||
printf("sink rate: every scenario measures the clocks and nothing else\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -4,13 +4,14 @@
|
||||
* were shipped on the strength of a throwaway model rather than
|
||||
* anything that would notice a later change:
|
||||
*
|
||||
* - runloop_pace_gap_engages(): the frame limiter holds the loop at
|
||||
* 1.0x when nothing else is pacing it at all. The condition that
|
||||
* matters is what it must NOT do - engage while another source is
|
||||
* already holding the loop, or under fast-forward, where running
|
||||
* unthrottled is the point. Both are silent failures: the first
|
||||
* double-paces and the frontend runs slow, the second throttles
|
||||
* fast-forward to 1x and looks like a performance bug.
|
||||
* - runloop_pace_gap_engages(): the frame limiter holds the loop to
|
||||
* the display rate when nothing else is pacing it and either audio
|
||||
* rate control can follow or Scanline Sync is between locks. The
|
||||
* condition that matters is what it must NOT do - engage while
|
||||
* another source is already holding the loop, with neither of
|
||||
* those, or under fast-forward, where running unthrottled is the
|
||||
* point. All are silent failures: double-pacing runs the frontend
|
||||
* slow, and a throttled fast-forward looks like a performance bug.
|
||||
*
|
||||
* - runloop_content_frame_time_us(): the period those waits use. A
|
||||
* core reports its own rate and is free to report nonsense; zero
|
||||
@@ -27,8 +28,8 @@
|
||||
* versions. Nothing here is a copy: change the header and this test
|
||||
* changes with it, which is the point.
|
||||
*
|
||||
* The gap predicate is checked over every combination of the five pace
|
||||
* bits by the two boolean inputs - 128 cases, exhaustive, not a
|
||||
* The gap predicate is checked over every combination of the six pace
|
||||
* bits by the four boolean inputs - 512 cases, exhaustive, not a
|
||||
* sample. The period is checked over the rates a core can produce
|
||||
* including the degenerate ones, and for monotonicity, since a
|
||||
* clamp that inverts is a clamp nobody notices. The sample filter is
|
||||
@@ -37,6 +38,7 @@
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <retro_common_api.h>
|
||||
@@ -61,46 +63,57 @@ static void check(bool ok, const char *what)
|
||||
static void test_gap_predicate(void)
|
||||
{
|
||||
unsigned pace;
|
||||
int nb, fm;
|
||||
int nb, fm, ss, rc;
|
||||
unsigned engaged = 0;
|
||||
|
||||
for (pace = 0; pace < 64; pace++)
|
||||
for (nb = 0; nb < 2; nb++)
|
||||
for (fm = 0; fm < 2; fm++)
|
||||
{
|
||||
bool got = runloop_pace_gap_engages(pace, nb != 0, fm != 0);
|
||||
bool want = (pace == RUNLOOP_PACE_NONE) && !nb && !fm;
|
||||
char msg[128];
|
||||
for (ss = 0; ss < 2; ss++)
|
||||
for (rc = 0; rc < 2; rc++)
|
||||
{
|
||||
bool got = runloop_pace_gap_engages(pace,
|
||||
nb != 0, fm != 0, ss != 0, rc != 0);
|
||||
bool want = (pace == RUNLOOP_PACE_NONE)
|
||||
&& !nb && !fm && (ss || rc);
|
||||
char msg[128];
|
||||
|
||||
if (got)
|
||||
engaged++;
|
||||
snprintf(msg, sizeof(msg),
|
||||
"pace=0x%02x nonblocking=%d fastmotion=%d -> %d, wanted %d",
|
||||
pace, nb, fm, (int)got, (int)want);
|
||||
check(got == want, msg);
|
||||
}
|
||||
if (got)
|
||||
engaged++;
|
||||
snprintf(msg, sizeof(msg),
|
||||
"pace=0x%02x nonblocking=%d fastmotion=%d "
|
||||
"scanline=%d ratecontrol=%d -> %d, wanted %d",
|
||||
pace, nb, fm, ss, rc, (int)got, (int)want);
|
||||
check(got == want, msg);
|
||||
}
|
||||
|
||||
/* Exactly one of the 128 combinations may engage. */
|
||||
check(engaged == 1, "exactly one combination engages the gap limiter");
|
||||
/* Three of the 512 combinations may engage: nothing pacing, not
|
||||
* fast-forwarding, with rate control, Scanline Sync, or both. */
|
||||
check(engaged == 3, "exactly three combinations engage the gap limiter");
|
||||
|
||||
/* Named cases, so a failure above reads as something rather than a
|
||||
* bit pattern. */
|
||||
check(runloop_pace_gap_engages(RUNLOOP_PACE_NONE, false, false),
|
||||
"nothing pacing, not fast-forwarding: engages");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_NONE, true, false),
|
||||
check(runloop_pace_gap_engages(RUNLOOP_PACE_NONE, false, false, false, true),
|
||||
"nothing pacing, rate control on, not fast-forwarding: engages");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_NONE, false, false, false, false),
|
||||
"rate control off: the loop runs unlimited as configured");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_NONE, true, false, false, true),
|
||||
"fast-forward (nonblocking) must stay unthrottled");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_NONE, false, true),
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_NONE, false, true, false, true),
|
||||
"FASTMOTION must stay unthrottled");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_VSYNC, false, false),
|
||||
check(runloop_pace_gap_engages(RUNLOOP_PACE_NONE, false, false, true, false),
|
||||
"Scanline Sync enabled but unlocked, no rate control: bridges the "
|
||||
"recalibration at the display rate");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_VSYNC, false, false, false, true),
|
||||
"vsync already paces: must not double up");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_AUDIO, false, false),
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_AUDIO, false, false, false, true),
|
||||
"audio already paces: must not double up");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_TIMER, false, false),
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_TIMER, false, false, false, true),
|
||||
"the frame limiter already paces: must not double up");
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_NOWINDOW, false, false),
|
||||
check(!runloop_pace_gap_engages(RUNLOOP_PACE_NOWINDOW, false, false, false, true),
|
||||
"the no-window wait already paces: must not double up");
|
||||
|
||||
printf(" gap predicate: 128 combinations, exactly one engages\n");
|
||||
printf(" gap predicate: 512 combinations, exactly three engage\n");
|
||||
}
|
||||
|
||||
/* --- the frame period -------------------------------------------- */
|
||||
@@ -224,6 +237,91 @@ static void test_sample_filter(void)
|
||||
"everything under\n");
|
||||
}
|
||||
|
||||
/* --- the schedule --------------------------------------------------- */
|
||||
|
||||
static void test_schedule(void)
|
||||
{
|
||||
const int64_t period = 16683350; /* 59.94 Hz, in nanoseconds */
|
||||
int64_t anchor;
|
||||
retro_time_t sleep;
|
||||
unsigned i;
|
||||
|
||||
/* On time: the sleep is what is left, rounded up to a microsecond,
|
||||
* and the anchor is the slot. */
|
||||
anchor = 1000000000LL;
|
||||
sleep = runloop_pace_schedule(&anchor, period, 1000000 + 12000);
|
||||
check(sleep == 4684 && anchor == 1000000000LL + period,
|
||||
"on time: sleep to the slot, rounded up, anchor on it");
|
||||
|
||||
/* Late by less than a period: no sleep, and the anchor stays on the
|
||||
* slot, so the next frame is due a period after it, not after now -
|
||||
* the lateness is caught up, not kept. */
|
||||
anchor = 1000000000LL;
|
||||
sleep = runloop_pace_schedule(&anchor, period, 1000000 + 16683 + 400);
|
||||
check(sleep == 0 && anchor == 1000000000LL + period,
|
||||
"late by less than a period: anchor stays on the slot");
|
||||
sleep = runloop_pace_schedule(&anchor, period, 1000000 + 16683 + 400 + 15000);
|
||||
check(sleep == 1284,
|
||||
"the next frame gets a sleep shorter by the lateness");
|
||||
|
||||
/* Late by a period or more: a stall; the schedule restarts from now
|
||||
* rather than trying to fit two frames into one. */
|
||||
anchor = 1000000000LL;
|
||||
sleep = runloop_pace_schedule(&anchor, period, 1000000 + 3 * 16683);
|
||||
check(sleep == 0 && anchor == (1000000 + 3 * 16683) * 1000LL,
|
||||
"late by a period or more: restart from now");
|
||||
|
||||
/* A sleep that overshoots every frame does not slow the loop, and
|
||||
* a period that is not a whole microsecond is kept exactly: over
|
||||
* 6000 frames the slots are exactly 6000 periods apart, to the
|
||||
* nanosecond - a whole-microsecond period would be 2.1 ms behind
|
||||
* by then, 21 ppm. */
|
||||
{
|
||||
const retro_time_t overshoot = 900; /* a coalesced nanosleep */
|
||||
const retro_time_t work = 15000; /* the frame's own time */
|
||||
retro_time_t now = 0;
|
||||
anchor = 0;
|
||||
for (i = 0; i < 6000; i++)
|
||||
{
|
||||
now += work;
|
||||
sleep = runloop_pace_schedule(&anchor, period, now);
|
||||
if (sleep > 0)
|
||||
now += sleep + overshoot;
|
||||
}
|
||||
check(anchor == 6000 * period,
|
||||
"6000 overshooting frames land on the 6000th slot, to the nanosecond");
|
||||
}
|
||||
}
|
||||
|
||||
/* --- the sleep margin ------------------------------------------------ */
|
||||
|
||||
static void test_margin(void)
|
||||
{
|
||||
const retro_time_t period = 16683;
|
||||
retro_time_t margin = 0;
|
||||
unsigned i;
|
||||
|
||||
/* Up at once. */
|
||||
margin = runloop_pace_margin_update(margin, 700, period);
|
||||
check(margin == 700, "one overshoot of 700 us sets the margin to 700");
|
||||
/* Down slowly: sixteen quiet sleeps take off well under all of it. */
|
||||
for (i = 0; i < 16; i++)
|
||||
margin = runloop_pace_margin_update(margin, 0, period);
|
||||
check(margin > 200 && margin < 400,
|
||||
"sixteen quiet sleeps leave the margin at about a third");
|
||||
/* Never negative, never past a quarter period. */
|
||||
check(runloop_pace_margin_update(100, -5000, period) < 100,
|
||||
"a negative overshoot counts as none");
|
||||
check(runloop_pace_margin_update(0, 100000, period) == period / 4,
|
||||
"a huge overshoot is capped at a quarter period");
|
||||
/* Settles on a steady overshoot. */
|
||||
margin = 0;
|
||||
for (i = 0; i < 200; i++)
|
||||
margin = runloop_pace_margin_update(margin, 250 + (i & 1) * 50, period);
|
||||
check(margin >= 250 && margin <= 300,
|
||||
"a 250-300 us overshoot settles the margin between them");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("runloop pacing decisions:\n");
|
||||
@@ -231,6 +329,8 @@ int main(void)
|
||||
test_gap_predicate();
|
||||
test_frame_period();
|
||||
test_sample_filter();
|
||||
test_schedule();
|
||||
test_margin();
|
||||
|
||||
if (failures)
|
||||
{
|
||||
@@ -239,7 +339,8 @@ int main(void)
|
||||
}
|
||||
|
||||
printf("ok: the gap limiter engages only when nothing else paces and "
|
||||
"fast-forward is off, the period is always a sane frame, and "
|
||||
"a stall never moves the measured rate\n");
|
||||
"fast-forward is off, the period is always a sane frame, a "
|
||||
"stall never moves the measured rate, an overshooting sleep "
|
||||
"never slows the loop, and the margin follows the overshoot\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user