mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
fix(dac): continuous resample cursors + variable-length audio batches
Second half of the 60 Hz crackle fix (the word-strobe capture is the
first). Two remaining per-frame discontinuities, one cause each:
1. An NTSC field is 524 halflines = 16651.56 us = 799.27 periods of the
48 kHz sample clock, so demanding a fixed 800 samples per frame is
unsatisfiable -- the event system delivers 799 and SoundCallback
fabricated the last one from a raw LTXD/RTXD register read.
SoundCallback now submits exactly what the sample clock produced;
libretro batches are variable-length by design and the frontend's
dynamic rate control absorbs the 0.27-sample-per-frame remainder.
2. DACPrepareFrame reset the ring and re-anchored the read phase at the
newest register value every frame, discarding the resampler's
position. The ring and both cursors now persist across frames:
monotonic write cursor, monotonic fractional read cursor trailing
it, both rebased together once per ring lap. Gross drift (SCLK
reprogramming, savestate load, DSP stops feeding the port) snaps
the read cursor back; steady-state never resyncs because capture
and consumption both derive from SCLK.
Savestate v9: the ring contents join the state (the cursors alone
reference data a load could not reconstruct -- the replay-determinism
test caught exactly that). STATE_SIZE grows 0x260000 -> 0x280000;
retro_unserialize floors the size check at the v8 layout so every
pre-v9 state still loads, with the ring zeroed and the loaded cursor
pair left as-is (mutually consistent in old states).
New test: test_audio_boundary fails when audio-batch seams are
statistically discontinuous (mean seam delta > 4x interior mean and
above an absolute floor). The clipping and presence tests are both
blind to this class -- it is neither saturation nor silence; Atari
Karts passed clipping at 0.000% saturated while emitting a 60 Hz
impulse train. Wired into make test with the existing find-rom /
skip-ledger pattern; negative control: 10.05x on the pre-fix core,
0.98x fixed.
Measured (in-game, 3300 frames, batch-aware seam metric):
Atari Karts 697.6 -> 93.4 (8.9x -> 1.0x interior)
Super Burnout 466.0 -> 92.9 (baseline had NO waveform: pure
60 Hz staircase, interior delta 0.0)
every other corpus title lands at 1.0x, RMS unchanged
Primal Rage CD (slave mode): 0.99x, RMS 2422 both builds
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
971545981c
commit
2c99748583
7 files changed
+444
-51
No files matched your search
@@ -824,7 +824,7 @@ clean:
|
||||
test/test_dsp_ops test/test_dsp_unit test/test_hle_bios \
|
||||
test/test_subsystem_init test/test_subsystem_timeline \
|
||||
test/test_irq_cascade test/test_boot_patterns test/test_audio_pipeline \
|
||||
test/test_audio_clipping test/test_audio_presence test/test_pit_clock_rate \
|
||||
test/test_audio_clipping test/test_audio_presence test/test_audio_boundary test/test_pit_clock_rate \
|
||||
test/test_blitter_mmio test/test_blitter_cmd test/test_eeprom_lifecycle \
|
||||
test/test_eeprom_read_race \
|
||||
test/test_tom_visible_window test/test_framebuffer_integrity \
|
||||
@@ -961,6 +961,16 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
else \
|
||||
bash scripts/test-skip.sh record "Atari Karts (clipping neg. control)" "no ROM matching 'Atari Karts*' in the private corpus"; \
|
||||
fi
|
||||
@# Frame-boundary discontinuity (60 Hz crackle class). The clipping
|
||||
@# and presence tests are both blind to this: it is neither
|
||||
@# saturation nor silence. Atari Karts' attract mode measured 10x
|
||||
@# on the broken core vs 1.0x fixed; see test_audio_boundary.c.
|
||||
@rom=$$(bash scripts/find-rom.sh 'Atari Karts (1995).jag' 'Atari Karts*.jag' 'Atari Karts*.j64'); \
|
||||
if [ -n "$$rom" ]; then \
|
||||
./test/test_audio_boundary ./$(TARGET) "$$rom" --label "Atari Karts (boundary)" --quiet; \
|
||||
else \
|
||||
bash scripts/test-skip.sh record "Atari Karts (audio boundary)" "no ROM matching 'Atari Karts*' in the private corpus"; \
|
||||
fi
|
||||
@# Formerly known-broken titles (DSP-synth saturation class): fixed by
|
||||
@# the MMULT secondary-bank fix (JTRM: the vector operand is always
|
||||
@# register bank 1, not "the non-current bank"). These now assert
|
||||
@@ -1325,6 +1335,10 @@ test/test_audio_presence: test/test_audio_presence.c
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
-o $@ test/test_audio_presence.c -ldl -lm
|
||||
|
||||
test/test_audio_boundary: test/test_audio_boundary.c
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
-o $@ test/test_audio_boundary.c -ldl -lm
|
||||
|
||||
test/test_memtrack: test/test_memtrack.c src/core/memtrack.c src/core/memtrack.h
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
-o $@ test/test_memtrack.c src/core/memtrack.c -lm
|
||||
|
||||
+8
-1
@@ -1170,7 +1170,11 @@ bool retro_unserialize(const void *data, size_t size)
|
||||
extern uint8_t jerry_ram_8[];
|
||||
extern bool lowerField;
|
||||
|
||||
if (!data || size < STATE_SIZE)
|
||||
/* Floor at the smallest layout any accepted version can occupy; the
|
||||
* exact floor for the declared version is enforced below, once the
|
||||
* header has been read. A flat `size < STATE_SIZE` here would
|
||||
* reject every state written before the v9 size increase. */
|
||||
if (!data || size < STATE_SIZE_V8)
|
||||
return false;
|
||||
|
||||
buf = (const uint8_t *)data;
|
||||
@@ -1189,6 +1193,9 @@ bool retro_unserialize(const void *data, size_t size)
|
||||
|| version < STATE_MIN_VERSION || version > STATE_VERSION)
|
||||
return false;
|
||||
|
||||
if (version >= STATE_VERSION_DAC_I2S_RING && size < STATE_SIZE)
|
||||
return false;
|
||||
|
||||
/* Large memory blocks */
|
||||
STATE_LOAD_BUF(buf, jaguarMainRAM, 0x200000);
|
||||
STATE_LOAD_BUF(buf, tomRam8, 0x4000);
|
||||
|
||||
+14
-2
@@ -37,7 +37,7 @@ extern "C" {
|
||||
* jaggd.c). One shared bump — all in-flight changes since the
|
||||
* v3.1.0 release use v8. */
|
||||
#define STATE_MAGIC 0x564A5353 /* "VJSS" */
|
||||
#define STATE_VERSION 8
|
||||
#define STATE_VERSION 9
|
||||
/* Oldest layout retro_unserialize still accepts. States between
|
||||
* STATE_MIN_VERSION and STATE_VERSION load by reading each chunk in the
|
||||
* layout the header version names (see DACStateLoad, CDROMStateLoad);
|
||||
@@ -92,13 +92,25 @@ extern "C" {
|
||||
* protect, idle SPI) — see JGDStateLoad / the caller's else branch. */
|
||||
#define STATE_VERSION_JAGGD 8
|
||||
|
||||
/* v9: the I2S resample ring contents. The ring and its cursors now
|
||||
* persist across frames (the per-frame reset was half of the 60 Hz
|
||||
* audio step), so replay determinism requires the ring data itself in
|
||||
* the state -- the cursors alone reference content the load cannot
|
||||
* reconstruct. */
|
||||
#define STATE_VERSION_DAC_I2S_RING 9
|
||||
|
||||
/* Header flags */
|
||||
#define STATE_FLAG_MEMTRACK 0x01
|
||||
#define STATE_FLAG_CDROM 0x02
|
||||
|
||||
/* Fixed save state size (~2.4 MB).
|
||||
* Must never increase between retro_load_game() and retro_unload_game(). */
|
||||
#define STATE_SIZE 0x260000 /* 2,490,368 bytes */
|
||||
/* v9 grew the payload by the 64 KB I2S resample ring
|
||||
* (STATE_VERSION_DAC_I2S_RING). States written by v8-and-older cores
|
||||
* are STATE_SIZE_V8 bytes; retro_unserialize sizes its floor check by
|
||||
* the version the state itself declares, so those still load. */
|
||||
#define STATE_SIZE 0x280000 /* 2,621,440 bytes */
|
||||
#define STATE_SIZE_V8 0x260000 /* 2,490,368 bytes -- all pre-v9 layouts */
|
||||
|
||||
/* Helper macros for sequential buffer writes/reads.
|
||||
* These advance the buffer pointer automatically. */
|
||||
|
||||
+93
-42
@@ -47,6 +47,15 @@ extern retro_audio_sample_batch_t audio_batch_cb;
|
||||
#define I2S_RING_SIZE 16384 /* Power of 2, must be > max samples/frame (PAL SCLK=0 ~8311) */
|
||||
#define I2S_RING_MASK (I2S_RING_SIZE - 1)
|
||||
|
||||
/* Steady-state distance the read cursor keeps behind the write cursor,
|
||||
* and the drift beyond which it snaps back instead of free-running.
|
||||
* Capture (word strobe) and consumption (rate ratio) both derive from
|
||||
* SCLK, so in steady state the lag only jitters by a fraction of a
|
||||
* sample; the resync handles discontinuities -- SCLK reprogramming,
|
||||
* savestate loads, a DSP that stops feeding the port. */
|
||||
#define I2S_TARGET_LAG 2
|
||||
#define I2S_RESYNC_LAG 256
|
||||
|
||||
/* Jaguar memory locations */
|
||||
|
||||
#define LTXD 0xF1A148
|
||||
@@ -164,6 +173,18 @@ static void DACCaptureSample(int16_t left, int16_t right)
|
||||
i2sWriteCount++;
|
||||
if (left != 0 || right != 0)
|
||||
i2sNonZeroCount++;
|
||||
|
||||
/* Both cursors run monotonically for the whole session; fold them
|
||||
* back together once per ring lap. Subtracting a whole ring size
|
||||
* leaves every masked index unchanged, keeps the uint32 write cursor
|
||||
* from wrapping and the double read cursor in a range where adding
|
||||
* a fractional step still resolves exactly. */
|
||||
if (i2sWritePos >= (uint32_t)(2 * I2S_RING_SIZE)
|
||||
&& i2sPhase >= (double)I2S_RING_SIZE)
|
||||
{
|
||||
i2sWritePos -= (uint32_t)I2S_RING_SIZE;
|
||||
i2sPhase -= (double)I2S_RING_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
/* One I2S word strobe has elapsed: latch whatever the DSP left in
|
||||
@@ -194,26 +215,46 @@ void DSPSampleCallback(void)
|
||||
double frac;
|
||||
int32_t s0L, s1L, s0R, s1R;
|
||||
|
||||
/* Guard: hold current register value if ring is empty (e.g. after reset) */
|
||||
if (i2sWriteCount < 2)
|
||||
/* Guard: hold current register value until the ring has data */
|
||||
if (i2sWritePos < 2)
|
||||
{
|
||||
outL = (int16_t)(*ltxd);
|
||||
outR = (int16_t)(*rtxd);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Linear interpolation between ring buffer samples.
|
||||
* i2sPhase is our fractional position in the I2S sample stream.
|
||||
* We advance by i2sRateRatio for each 48 kHz output sample. */
|
||||
/* i2sPhase is a monotonic read cursor into the captured I2S
|
||||
* stream; it advances by the rate ratio per 48 kHz output sample
|
||||
* and carries across frames. Both cursors are rebased together
|
||||
* in DACCaptureSample, which leaves masked indices unchanged.
|
||||
*
|
||||
* Resetting the cursor every frame (the old scheme) discarded the
|
||||
* read position and re-anchored at the newest sample, stepping
|
||||
* the output once per frame at 60 Hz. */
|
||||
double lag = (double)i2sWritePos - i2sPhase;
|
||||
|
||||
/* Resync only on gross drift: a rate change mid-stream (SCLK
|
||||
* write), a savestate load, or a DSP that stopped feeding the
|
||||
* port. In steady state capture and consumption both derive
|
||||
* from SCLK, so the lag stays put. */
|
||||
if (lag < 0.0 || lag > (double)I2S_RESYNC_LAG)
|
||||
{
|
||||
i2sPhase = (double)i2sWritePos - (double)I2S_TARGET_LAG;
|
||||
if (i2sPhase < 0.0)
|
||||
i2sPhase = 0.0;
|
||||
lag = (double)i2sWritePos - i2sPhase;
|
||||
}
|
||||
|
||||
idx0 = (uint32_t)i2sPhase;
|
||||
frac = i2sPhase - (double)idx0;
|
||||
idx1 = idx0 + 1;
|
||||
|
||||
/* Clamp indices to available data */
|
||||
if (idx0 >= i2sWriteCount)
|
||||
idx0 = i2sWriteCount - 1;
|
||||
if (idx1 >= i2sWriteCount)
|
||||
idx1 = i2sWriteCount - 1;
|
||||
/* Never read at or past the write cursor: those slots hold the
|
||||
* previous lap of the ring. */
|
||||
if (idx0 >= i2sWritePos)
|
||||
idx0 = i2sWritePos - 1;
|
||||
if (idx1 >= i2sWritePos)
|
||||
idx1 = i2sWritePos - 1;
|
||||
|
||||
s0L = (int32_t)i2sRingL[idx0 & I2S_RING_MASK];
|
||||
s1L = (int32_t)i2sRingL[idx1 & I2S_RING_MASK];
|
||||
@@ -223,8 +264,10 @@ void DSPSampleCallback(void)
|
||||
outL = (int16_t)(s0L + (int32_t)((double)(s1L - s0L) * frac));
|
||||
outR = (int16_t)(s0R + (int32_t)((double)(s1R - s0R) * frac));
|
||||
|
||||
/* Advance phase by the rate ratio */
|
||||
i2sPhase += i2sRateRatio;
|
||||
/* Underrun: hold position until the next capture lands rather
|
||||
* than running past the write head. */
|
||||
if (lag > 1.0)
|
||||
i2sPhase += i2sRateRatio;
|
||||
}
|
||||
|
||||
sampleBuffer[bufferIndex + 0] = (uint16_t)outL;
|
||||
@@ -242,30 +285,18 @@ void DSPSampleCallback(void)
|
||||
|
||||
void DACPrepareFrame(int length)
|
||||
{
|
||||
int16_t seedL;
|
||||
int16_t seedR;
|
||||
|
||||
RemoveCallback(DSPSampleCallback);
|
||||
bufferIndex = 0;
|
||||
numberOfSamples = length;
|
||||
bufferDone = false;
|
||||
|
||||
/* Seed the ring with the last sample actually transmitted, so the new
|
||||
* frame's interpolation starts where the old one stopped. Seeding from
|
||||
* LTXD/RTXD instead put a step at every frame boundary: the holding
|
||||
* register already holds the value for the *next* word strobe, one
|
||||
* sample ahead of the ring tail. */
|
||||
seedL = (int16_t)(*ltxd);
|
||||
seedR = (int16_t)(*rtxd);
|
||||
|
||||
i2sRingL[0] = seedL;
|
||||
i2sRingR[0] = seedR;
|
||||
i2sRingL[1] = seedL;
|
||||
i2sRingR[1] = seedR;
|
||||
i2sWritePos = 2;
|
||||
i2sWriteCount = 2;
|
||||
/* Per-frame diagnostic counters only (DACGetI2SWriteCount /
|
||||
* DACGetI2SNonZeroCount consumers). The ring and both cursors
|
||||
* deliberately survive the frame boundary -- resetting and
|
||||
* re-anchoring them at the newest register value every frame was
|
||||
* the other half of the 60 Hz step. */
|
||||
i2sWriteCount = 0;
|
||||
i2sNonZeroCount = 0;
|
||||
i2sPhase = i2sPhase - (double)(uint32_t)i2sPhase;
|
||||
|
||||
/* Refresh rate ratio in case SCLK was written between frames */
|
||||
DACUpdateSCLKRate();
|
||||
@@ -275,20 +306,19 @@ void DACPrepareFrame(int length)
|
||||
|
||||
void SoundCallback(void * userdata, uint16_t * buffer, int length)
|
||||
{
|
||||
int idx;
|
||||
|
||||
RemoveCallback(DSPSampleCallback);
|
||||
|
||||
if (bufferIndex < length)
|
||||
{
|
||||
for (idx = bufferIndex; idx < length; idx += 2)
|
||||
{
|
||||
buffer[idx + 0] = (uint16_t)((int16_t)(*ltxd));
|
||||
buffer[idx + 1] = (uint16_t)((int16_t)(*rtxd));
|
||||
}
|
||||
}
|
||||
|
||||
audio_batch_cb((int16_t *)buffer, length / 2);
|
||||
/* Submit exactly what the 48 kHz sample clock produced this frame.
|
||||
* An NTSC field is 524 halflines = 16651.56 us = 799.27 sample
|
||||
* periods, so a fixed count of 800 is unsatisfiable: the 800th
|
||||
* sample does not exist yet, and fabricating it from a raw LTXD/RTXD
|
||||
* read put a step at the tail of every frame -- a 60 Hz tick over
|
||||
* everything. libretro batches are variable-length by design;
|
||||
* delivering 799-or-800 lets the frontend's rate control absorb the
|
||||
* 0.27-sample-per-frame remainder, which is its job. */
|
||||
(void)length;
|
||||
if (bufferIndex > 0)
|
||||
audio_batch_cb((int16_t *)buffer, bufferIndex / 2);
|
||||
}
|
||||
|
||||
/* LTXD/RTXD/SCLK/SMODE ($F1A148/4C/50/54) */
|
||||
@@ -400,6 +430,11 @@ size_t DACStateSave(uint8_t *buf)
|
||||
STATE_SAVE_VAR(buf, rrxd);
|
||||
STATE_SAVE_VAR(buf, sstat);
|
||||
|
||||
/* v9: ring contents -- the cursors above index into this data and
|
||||
* both now survive frame boundaries (STATE_VERSION_DAC_I2S_RING). */
|
||||
STATE_SAVE_BUF(buf, i2sRingL, sizeof(i2sRingL));
|
||||
STATE_SAVE_BUF(buf, i2sRingR, sizeof(i2sRingR));
|
||||
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
|
||||
@@ -468,5 +503,21 @@ size_t DACStateLoad(const uint8_t *buf, uint32_t stateVersion)
|
||||
DACUpdateSCLKRate();
|
||||
}
|
||||
|
||||
if (stateVersion >= STATE_VERSION_DAC_I2S_RING)
|
||||
{
|
||||
STATE_LOAD_BUF(buf, i2sRingL, sizeof(i2sRingL));
|
||||
STATE_LOAD_BUF(buf, i2sRingR, sizeof(i2sRingR));
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Older states carry no ring data: zero it. The loaded cursor
|
||||
* pair is left alone -- a v8 state's phase and writePos are
|
||||
* mutually consistent (both were frame-local), and the read
|
||||
* cursor's gross-drift resync covers anything pathological on
|
||||
* the first sample after load. */
|
||||
memset(i2sRingL, 0, sizeof(i2sRingL));
|
||||
memset(i2sRingR, 0, sizeof(i2sRingR));
|
||||
}
|
||||
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/* test_audio_boundary.c -- Detect per-frame audio discontinuities.
|
||||
*
|
||||
* The failure mode this guards: a step in the sample stream at every
|
||||
* audio-batch boundary -- a 60 Hz impulse train heard as constant
|
||||
* crackle. Shipped for months because the other two audio tests are
|
||||
* blind to it: it is not saturation (test_audio_clipping passed Atari
|
||||
* Karts at 0.000% saturated while it buzzed) and not silence
|
||||
* (test_audio_presence saw normal RMS).
|
||||
*
|
||||
* Metric: mean |s[i]-s[i-1]| over sample pairs adjacent to a batch
|
||||
* boundary vs. the mean over interior pairs. A healthy stream is
|
||||
* statistically identical at the seam (ratio ~1); the 2026-08 bug
|
||||
* measured 20x on Atari Karts' attract mode with an absolute boundary
|
||||
* mean of 1601 against an interior of 80.
|
||||
*
|
||||
* FAIL requires both: ratio > RATIO_LIMIT and boundary mean >
|
||||
* ABS_FLOOR. The floor keeps near-silent titles (interior ~0) from
|
||||
* failing on numerically meaningless ratios.
|
||||
*
|
||||
* Usage: ./test/test_audio_boundary <core> <rom> [--frames N] [--label TAG]
|
||||
* [--ratio-limit R] [--quiet]
|
||||
* Exit: 0 PASS, 1 FAIL, 2 SKIP (ROM missing / no audio)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include <dlfcn.h>
|
||||
#include "../libretro-common/include/libretro.h"
|
||||
|
||||
#define DEFAULT_FRAMES 1200
|
||||
#define RATIO_LIMIT 4.0
|
||||
#define ABS_FLOOR 150.0
|
||||
|
||||
static void *core_handle;
|
||||
static void (*p_retro_init)(void);
|
||||
static void (*p_retro_deinit)(void);
|
||||
static void (*p_retro_set_environment)(retro_environment_t);
|
||||
static void (*p_retro_set_video_refresh)(retro_video_refresh_t);
|
||||
static void (*p_retro_set_audio_sample)(retro_audio_sample_t);
|
||||
static void (*p_retro_set_audio_sample_batch)(retro_audio_sample_batch_t);
|
||||
static void (*p_retro_set_input_poll)(retro_input_poll_t);
|
||||
static void (*p_retro_set_input_state)(retro_input_state_t);
|
||||
static bool (*p_retro_load_game)(const struct retro_game_info *);
|
||||
static void (*p_retro_unload_game)(void);
|
||||
static void (*p_retro_run)(void);
|
||||
|
||||
/* Accumulators: boundary-adjacent vs interior deltas, left channel.
|
||||
* "Boundary-adjacent" = the last delta inside a batch and the delta
|
||||
* across the seam into the next batch, so the metric is insensitive to
|
||||
* whether a bug lands the bad sample on the tail or the head. */
|
||||
static double boundary_sum = 0.0, interior_sum = 0.0;
|
||||
static uint64_t boundary_cnt = 0, interior_cnt = 0;
|
||||
static double boundary_max = 0.0;
|
||||
static int16_t prev_l = 0;
|
||||
static int have_prev = 0;
|
||||
static uint64_t nonsilent = 0;
|
||||
|
||||
static size_t audio_batch(const int16_t *data, size_t frames)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < frames; i++)
|
||||
{
|
||||
int16_t l = data[i * 2];
|
||||
if (l > 32 || l < -32)
|
||||
nonsilent++;
|
||||
if (have_prev)
|
||||
{
|
||||
double d = fabs((double)l - (double)prev_l);
|
||||
/* First delta of a batch crosses the seam; last delta of a
|
||||
* batch is handled on the next call's first iteration only
|
||||
* for the seam itself, so also flag the final in-batch pair. */
|
||||
if (i == 0 || i == frames - 1)
|
||||
{
|
||||
boundary_sum += d;
|
||||
boundary_cnt++;
|
||||
if (d > boundary_max)
|
||||
boundary_max = d;
|
||||
}
|
||||
else
|
||||
{
|
||||
interior_sum += d;
|
||||
interior_cnt++;
|
||||
}
|
||||
}
|
||||
prev_l = l;
|
||||
have_prev = 1;
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
static void video_refresh(const void *d, unsigned w, unsigned h, size_t p)
|
||||
{ (void)d; (void)w; (void)h; (void)p; }
|
||||
static void audio_sample(int16_t l, int16_t r) { (void)l; (void)r; }
|
||||
static void input_poll(void) {}
|
||||
static int16_t input_state(unsigned p, unsigned d, unsigned i, unsigned id)
|
||||
{ (void)p; (void)d; (void)i; (void)id; return 0; }
|
||||
|
||||
static int log_quiet = 0;
|
||||
static void log_printf(enum retro_log_level level, const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
if (log_quiet || level < RETRO_LOG_WARN) return;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
static struct retro_log_callback log_cb = { log_printf };
|
||||
|
||||
static bool environment(unsigned cmd, void *data)
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
|
||||
*(struct retro_log_callback *)data = log_cb;
|
||||
return true;
|
||||
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
|
||||
case RETRO_ENVIRONMENT_SET_VARIABLES:
|
||||
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2:
|
||||
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE:
|
||||
case RETRO_ENVIRONMENT_SET_MEMORY_MAPS:
|
||||
case RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS:
|
||||
case RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS:
|
||||
case RETRO_ENVIRONMENT_SET_GEOMETRY:
|
||||
case RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION:
|
||||
case RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER:
|
||||
return true;
|
||||
case RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
|
||||
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
|
||||
*(const char **)data = "/tmp";
|
||||
return true;
|
||||
case RETRO_ENVIRONMENT_GET_VARIABLE:
|
||||
{
|
||||
struct retro_variable *var = (struct retro_variable *)data;
|
||||
var->value = NULL;
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool load_core(const char *path)
|
||||
{
|
||||
core_handle = dlopen(path, RTLD_NOW);
|
||||
if (!core_handle)
|
||||
{
|
||||
fprintf(stderr, "dlopen(%s): %s\n", path, dlerror());
|
||||
return false;
|
||||
}
|
||||
#define LOAD(sym) do { p_##sym = dlsym(core_handle, #sym); \
|
||||
if (!p_##sym) { fprintf(stderr, "Missing symbol: %s\n", #sym); return false; } } while (0)
|
||||
LOAD(retro_init);
|
||||
LOAD(retro_deinit);
|
||||
LOAD(retro_set_environment);
|
||||
LOAD(retro_set_video_refresh);
|
||||
LOAD(retro_set_audio_sample);
|
||||
LOAD(retro_set_audio_sample_batch);
|
||||
LOAD(retro_set_input_poll);
|
||||
LOAD(retro_set_input_state);
|
||||
LOAD(retro_load_game);
|
||||
LOAD(retro_unload_game);
|
||||
LOAD(retro_run);
|
||||
#undef LOAD
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *core_path = NULL, *rom_path = NULL, *label = NULL;
|
||||
unsigned total_frames = DEFAULT_FRAMES;
|
||||
double ratio_limit = RATIO_LIMIT;
|
||||
uint8_t *rom_data;
|
||||
size_t rom_size;
|
||||
FILE *f;
|
||||
struct retro_game_info game;
|
||||
double interior_mean, boundary_mean, ratio;
|
||||
unsigned i;
|
||||
|
||||
for (i = 1; i < (unsigned)argc; i++)
|
||||
{
|
||||
if (!strcmp(argv[i], "--frames") && i + 1 < (unsigned)argc)
|
||||
total_frames = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--label") && i + 1 < (unsigned)argc)
|
||||
label = argv[++i];
|
||||
else if (!strcmp(argv[i], "--ratio-limit") && i + 1 < (unsigned)argc)
|
||||
ratio_limit = atof(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--quiet"))
|
||||
log_quiet = 1;
|
||||
else if (argv[i][0] != '-' && !core_path)
|
||||
core_path = argv[i];
|
||||
else if (argv[i][0] != '-' && !rom_path)
|
||||
rom_path = argv[i];
|
||||
}
|
||||
if (!core_path || !rom_path)
|
||||
{
|
||||
fprintf(stderr, "Usage: %s <core> <rom> [--frames N] [--label TAG] "
|
||||
"[--ratio-limit R] [--quiet]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (!label)
|
||||
label = rom_path;
|
||||
|
||||
printf("\n=== Audio boundary check: %s ===\n", label);
|
||||
|
||||
f = fopen(rom_path, "rb");
|
||||
if (!f)
|
||||
{
|
||||
printf(" SKIP: ROM not found at %s\n", rom_path);
|
||||
return 2;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
rom_size = (size_t)ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
rom_data = (uint8_t *)malloc(rom_size);
|
||||
if (!rom_data || fread(rom_data, 1, rom_size, f) != rom_size)
|
||||
{
|
||||
fclose(f);
|
||||
free(rom_data);
|
||||
printf(" SKIP: could not read ROM\n");
|
||||
return 2;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (!load_core(core_path))
|
||||
{
|
||||
free(rom_data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
p_retro_set_environment(environment);
|
||||
p_retro_init();
|
||||
p_retro_set_video_refresh(video_refresh);
|
||||
p_retro_set_audio_sample(audio_sample);
|
||||
p_retro_set_audio_sample_batch(audio_batch);
|
||||
p_retro_set_input_poll(input_poll);
|
||||
p_retro_set_input_state(input_state);
|
||||
|
||||
memset(&game, 0, sizeof(game));
|
||||
game.path = rom_path;
|
||||
game.data = rom_data;
|
||||
game.size = rom_size;
|
||||
if (!p_retro_load_game(&game))
|
||||
{
|
||||
printf(" FAIL: retro_load_game rejected ROM\n");
|
||||
free(rom_data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < total_frames; i++)
|
||||
p_retro_run();
|
||||
|
||||
p_retro_unload_game();
|
||||
p_retro_deinit();
|
||||
dlclose(core_handle);
|
||||
free(rom_data);
|
||||
|
||||
if (interior_cnt < 10000 || nonsilent < 4800)
|
||||
{
|
||||
printf(" SKIP: not enough audio to judge (%llu interior deltas, "
|
||||
"%llu non-silent samples)\n",
|
||||
(unsigned long long)interior_cnt,
|
||||
(unsigned long long)nonsilent);
|
||||
return 2;
|
||||
}
|
||||
|
||||
interior_mean = interior_sum / (double)interior_cnt;
|
||||
boundary_mean = boundary_sum / (double)boundary_cnt;
|
||||
ratio = (interior_mean > 0.0) ? boundary_mean / interior_mean : 0.0;
|
||||
|
||||
printf(" Frames: %u\n", total_frames);
|
||||
printf(" Interior mean delta: %.1f (%llu pairs)\n",
|
||||
interior_mean, (unsigned long long)interior_cnt);
|
||||
printf(" Boundary mean delta: %.1f (%llu pairs, max %.0f)\n",
|
||||
boundary_mean, (unsigned long long)boundary_cnt, boundary_max);
|
||||
printf(" Ratio: %.2fx (limit %.1fx, abs floor %.0f)\n",
|
||||
ratio, ratio_limit, ABS_FLOOR);
|
||||
|
||||
if (ratio > ratio_limit && boundary_mean > ABS_FLOOR)
|
||||
{
|
||||
printf(" FAIL: batch boundaries are discontinuous -- a per-frame "
|
||||
"step (60 Hz crackle). See src/jerry/dac.c word-strobe "
|
||||
"capture + continuous ring cursors.\n");
|
||||
return 1;
|
||||
}
|
||||
printf(" PASS: batch seams statistically match the interior\n");
|
||||
return 0;
|
||||
}
|
||||
+6
-2
@@ -698,9 +698,13 @@ int main(int argc, char **argv)
|
||||
{
|
||||
uint32_t hdr_ver;
|
||||
memcpy(&hdr_ver, state + STATE_OFF_VERSION, 4);
|
||||
/* The JGD chunk arrived in v8; the header must carry the
|
||||
* current version, whatever that has grown to since. */
|
||||
check(hdr_ver == (uint32_t)STATE_VERSION
|
||||
&& STATE_VERSION == 8,
|
||||
"v8_header", "state header version=%u (expect 8)", hdr_ver);
|
||||
&& STATE_VERSION >= STATE_VERSION_JAGGD,
|
||||
"v8_header", "state header version=%u (expect %u, >= %u)",
|
||||
hdr_ver, (uint32_t)STATE_VERSION,
|
||||
(uint32_t)STATE_VERSION_JAGGD);
|
||||
}
|
||||
|
||||
/* Console reset returns the mapping to identity... */
|
||||
|
||||
@@ -114,7 +114,14 @@ const char *__lsan_default_suppressions(void) {
|
||||
* field, THIS TEST FAILS LOUDLY rather than silently splicing the wrong
|
||||
* four bytes out of the fixture and "passing". To update, re-derive both
|
||||
* numbers from DACStateSave's field order and sizes. */
|
||||
#define EXPECTED_DAC_BLOCK_SIZE 51
|
||||
/* STATE_VERSION_DAC_I2S_RING (v9) appends the resample ring contents:
|
||||
* i2sRingL + i2sRingR, 16384 entries x int16 each = 65536 bytes, for
|
||||
* 65587. The ring and its read/write cursors persist across frames
|
||||
* since the 60 Hz-step fix, so replay determinism needs the data
|
||||
* itself in the state. Appended after sstat: every pre-v9 offset
|
||||
* below is unaffected. */
|
||||
#define EXPECTED_DAC_BLOCK_SIZE 65587
|
||||
#define EXPECTED_DAC_BLOCK_SIZE_V8 51
|
||||
#define DAC_I2S_NONZEROCOUNT_OFFSET 17
|
||||
#define DAC_I2S_NONZEROCOUNT_SIZE 4
|
||||
|
||||
@@ -334,8 +341,14 @@ int main(int argc, char **argv)
|
||||
int *width_ptr, *height_ptr;
|
||||
uint8_t *state_v3 = NULL, *state_v2 = NULL, *state_v1 = NULL,
|
||||
*state_v3l = NULL, *scratch = NULL;
|
||||
uint8_t dac_v3[256], dac_now[256], dac_expect[256], dac_post[256];
|
||||
uint8_t dac_expect_v1[256];
|
||||
/* Static: the v9 DAC block is ~64 KB (ring contents) -- five of
|
||||
* them on the stack was a segfault, and did in fact segfault when
|
||||
* the ring landed and these were 256-byte automatics. */
|
||||
static uint8_t dac_v3[EXPECTED_DAC_BLOCK_SIZE + 16];
|
||||
static uint8_t dac_now[EXPECTED_DAC_BLOCK_SIZE + 16];
|
||||
static uint8_t dac_expect[EXPECTED_DAC_BLOCK_SIZE + 16];
|
||||
static uint8_t dac_post[EXPECTED_DAC_BLOCK_SIZE + 16];
|
||||
static uint8_t dac_expect_v1[EXPECTED_DAC_BLOCK_SIZE + 16];
|
||||
size_t state_size, dac_size, dac_size_now, dac_off = 0;
|
||||
size_t dac_off_v2 = 0;
|
||||
size_t cdrom_size, joy_size, mt_size, nvm_size, cdrom_end = 0;
|
||||
|
||||
Reference in new issue
Block a user