mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
fix(core): carry sub-cycle remainders in the M68K scale; exercise scales in make test
Review finding on #316: at sub-1x scales a small slice's budget rounds to zero, and m68k_execute(0) runs one instruction regardless (UAE do/while), quietly defeating underclocking. Rather than only guarding the zero, carry the remainder in hundredths of a cycle across slices (the fifoRefillAccum error-diffusion pattern), so 0.5x is exact over time and zero-budget slices genuinely skip. Accumulator resets on any option change; the 100% path is the untouched pre-existing branch. Suite now exercises the scale paths with audio output: - test_frontend_pacing at risc=2x+m68k=2x and at m68k=0.5x (yarc, committed in-tree, so these run in CI) asserting the audio-rate contract -- the host-speed check is defused on those rows only, since ~double work per frame makes it a property of the runner - test_audio_presence gains a generic --option K=V passthrough and runs Iron Soldier 1 at risc=2x inside the measured RMS envelope Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
5983652fca
commit
f534ffbedf
5 files changed
+102
-2
No files matched your search
@@ -915,6 +915,17 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
else \
|
||||
bash scripts/test-skip.sh record "Iron Soldier 1 (audio presence)" "no ROM matching 'Iron Soldier*' in the private corpus"; \
|
||||
fi
|
||||
@# Same title at risc=2x (issue #314): real game audio through the DSP
|
||||
@# with the RISC compute budget doubled. Presence must stay inside the
|
||||
@# measured envelope -- if 2x silences it or blows it out, the scale is
|
||||
@# leaking into the audio path. Same skip discipline as above.
|
||||
@rom=$$(bash scripts/find-rom.sh 'Iron Soldier (1994).jag' 'Iron Soldier (World)*.j64' 'Iron Soldier.jag'); \
|
||||
if [ -n "$$rom" ]; then \
|
||||
./test/test_audio_presence ./$(TARGET) "$$rom" --label "Iron Soldier 1 (risc=2x)" --rms-floor 200 --rms-ceiling 25000 --quiet \
|
||||
--option virtualjaguar_risc_clock_scale=2x; \
|
||||
else \
|
||||
bash scripts/test-skip.sh record "Iron Soldier 1 (audio presence, risc=2x)" "no ROM matching 'Iron Soldier*' in the private corpus"; \
|
||||
fi
|
||||
./test/test_butch_cd
|
||||
./test/test_cd_hle_idempotent
|
||||
./test/test_cd_pregap
|
||||
@@ -990,6 +1001,29 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
@# so a missing ROM means a broken checkout and should fail the suite
|
||||
@# rather than silently read as a pass.
|
||||
./test/test_frontend_pacing ./$(TARGET) test/roms/yarc.j64 --quiet
|
||||
@# Clock-scale (issue #314) audio-pacing contract at non-1x. The one
|
||||
@# invariant the scale options must never break is sample pacing:
|
||||
@# I2S/DAC event scheduling stays on the unscaled sysclock, so the
|
||||
@# audio_rate_contract (exactly 48000/fps samples per frame, 1:1
|
||||
@# batches) must hold at every scale -- a violation here is a pitch
|
||||
@# shift, the PR #170 regression class. yarc.j64 is committed
|
||||
@# in-tree, so these run everywhere including CI. The m68k=0.5x row
|
||||
@# also exercises the underclock error-diffusion path (zero-budget
|
||||
@# slices must skip, not execute).
|
||||
@# --max-fastest-frame-fraction 100 defuses the host-speed check on
|
||||
@# these rows only: at 2x/2x the emulator does ~double the work per
|
||||
@# frame, so "fastest frame < 0.5x period" is a property of the host,
|
||||
@# not of the scale options, and would flake on loaded CI runners
|
||||
@# (observed locally: 11.3 ms vs the 8.3 ms limit under parallel
|
||||
@# builds). The assertions that matter here -- audio_rate_contract,
|
||||
@# one_batch_per_frame, geometry_stability -- keep full strength.
|
||||
./test/test_frontend_pacing ./$(TARGET) test/roms/yarc.j64 --quiet \
|
||||
--max-fastest-frame-fraction 100 \
|
||||
--option virtualjaguar_risc_clock_scale=2x \
|
||||
--option virtualjaguar_m68k_clock_scale=2x
|
||||
./test/test_frontend_pacing ./$(TARGET) test/roms/yarc.j64 --quiet \
|
||||
--max-fastest-frame-fraction 100 \
|
||||
--option virtualjaguar_m68k_clock_scale=0.5x
|
||||
@# EEPROM lifecycle test: generates a test ROM, then exercises load/unload/reload.
|
||||
@$(CC) -O2 -Wall -o /tmp/gen_eeprom_test_rom test/tools/gen_eeprom_test_rom.c && \
|
||||
/tmp/gen_eeprom_test_rom /tmp/eeprom_lifecycle_test.j64 && \
|
||||
|
||||
@@ -578,6 +578,9 @@ static void check_variables(void)
|
||||
else if (strcmp(var.value, "3x") == 0)
|
||||
m68kClockScalePct = 300;
|
||||
}
|
||||
/* Drop any carried sub-cycle remainder when the scale (possibly)
|
||||
* changed, so a new scale starts from a clean accumulator. */
|
||||
M68KClockScaleReset();
|
||||
|
||||
var.key = "virtualjaguar_risc_clock_scale";
|
||||
var.value = NULL;
|
||||
|
||||
+33
-2
@@ -261,6 +261,19 @@ uint32_t jaguarLoadedRAMStart, jaguarLoadedRAMEnd;
|
||||
* (or a harness that never sets the options) gets stock timing. See the
|
||||
* block comment in jaguar.h for where these do and do not apply. */
|
||||
uint32_t m68kClockScalePct = 100;
|
||||
/* Error-diffusion remainder for the M68K scale (hundredths of a cycle,
|
||||
* same pattern as cdrom.c's fifoRefillAccum). Without it, a sub-1x
|
||||
* scale rounds each small slice down independently -- a 1-cycle slice
|
||||
* at 0.5x scales to 0, and the UAE do/while then executes one full
|
||||
* instruction anyway, quietly defeating underclocking. Carrying the
|
||||
* remainder makes the scale exact over time and lets zero-budget
|
||||
* slices genuinely skip. Reset whenever the scale changes. */
|
||||
static uint32_t m68kScaleAccum = 0;
|
||||
|
||||
void M68KClockScaleReset(void)
|
||||
{
|
||||
m68kScaleAccum = 0;
|
||||
}
|
||||
uint32_t riscClockScalePct = 100;
|
||||
|
||||
bool jaguarCartInserted = false;
|
||||
@@ -1204,8 +1217,26 @@ static void M68KExecuteWithStalls(uint32_t cycles)
|
||||
* executes, AFTER the stall deduction: the stall models real bus
|
||||
* occupancy (OP fetch, DRAM refresh) in wall time, which an
|
||||
* overclocked CPU on modified hardware would still sit out in
|
||||
* full. At 100 this is an exact identity. */
|
||||
m68k_execute(SCALE_M68K_CYCLES(cycles));
|
||||
* full. At 100 the else branch is the exact pre-existing path.
|
||||
*
|
||||
* Non-100 uses error diffusion: the remainder in hundredths of a
|
||||
* cycle carries to the next slice, so 0.5x is exact over time and a
|
||||
* slice whose scaled budget is zero genuinely skips -- passing 0 to
|
||||
* m68k_execute() would run one instruction regardless (UAE
|
||||
* do/while), which is precisely the underclock-defeating edge the
|
||||
* review flagged. */
|
||||
if (m68kClockScalePct != 100u)
|
||||
{
|
||||
uint64_t budget = (uint64_t)cycles * m68kClockScalePct
|
||||
+ m68kScaleAccum;
|
||||
uint32_t scaled = (uint32_t)(budget / 100u);
|
||||
m68kScaleAccum = (uint32_t)(budget % 100u);
|
||||
if (scaled == 0)
|
||||
return;
|
||||
m68k_execute(scaled);
|
||||
return;
|
||||
}
|
||||
m68k_execute(cycles);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ extern uint32_t jaguarLoadedRAMStart, jaguarLoadedRAMEnd;
|
||||
* (c * 100 / 100) is an exact identity, so 1x is bit-identical to the
|
||||
* unscaled build. */
|
||||
extern uint32_t m68kClockScalePct;
|
||||
void M68KClockScaleReset(void);
|
||||
extern uint32_t riscClockScalePct;
|
||||
|
||||
#define SCALE_M68K_CYCLES(c) ((uint32_t)(((uint64_t)(c) * m68kClockScalePct) / 100u))
|
||||
|
||||
@@ -120,6 +120,12 @@ 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 use_bios = 0;
|
||||
|
||||
/* --option K=V pairs served through RETRO_ENVIRONMENT_GET_VARIABLE. */
|
||||
#define MAX_OPTS 8
|
||||
static const char *opt_keys[MAX_OPTS];
|
||||
static const char *opt_vals[MAX_OPTS];
|
||||
static int opt_count = 0;
|
||||
static int log_quiet = 0;
|
||||
|
||||
static void log_printf(enum retro_log_level level, const char *fmt, ...)
|
||||
@@ -158,11 +164,22 @@ static bool environment(unsigned cmd, void *data)
|
||||
case RETRO_ENVIRONMENT_GET_VARIABLE:
|
||||
{
|
||||
struct retro_variable *var = (struct retro_variable *)data;
|
||||
int oi;
|
||||
if (var->key && strcmp(var->key, "virtualjaguar_bios") == 0)
|
||||
{
|
||||
var->value = use_bios ? "enabled" : "disabled";
|
||||
return true;
|
||||
}
|
||||
/* Generic --option K=V passthrough (added for the clock-scale
|
||||
* suite rows, issue #314; same contract as the shared harness). */
|
||||
for (oi = 0; oi < opt_count; oi++)
|
||||
{
|
||||
if (var->key && strcmp(var->key, opt_keys[oi]) == 0)
|
||||
{
|
||||
var->value = opt_vals[oi];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
var->value = NULL;
|
||||
return false;
|
||||
}
|
||||
@@ -271,6 +288,20 @@ int main(int argc, char **argv)
|
||||
else if (!strcmp(a, "--rms-ceiling") && i + 1 < argc) rms_ceiling = atof(argv[++i]);
|
||||
else if (!strcmp(a, "--max-zero-run-pct") && i + 1 < argc) max_zero_run_pct = atof(argv[++i]);
|
||||
else if (!strcmp(a, "--label") && i + 1 < argc) label = argv[++i];
|
||||
else if (!strcmp(a, "--option") && i + 1 < argc)
|
||||
{
|
||||
char *kv = argv[++i];
|
||||
char *eq = strchr(kv, '=');
|
||||
if (!eq || opt_count >= MAX_OPTS)
|
||||
{
|
||||
fprintf(stderr, "Bad or excess --option (want K=V): %s\n", kv);
|
||||
return 2;
|
||||
}
|
||||
*eq = '\0';
|
||||
opt_keys[opt_count] = kv;
|
||||
opt_vals[opt_count] = eq + 1;
|
||||
opt_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "Unknown arg: %s\n", a);
|
||||
|
||||
Reference in new issue
Block a user