mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
* fix(dac): handle SCLK rate changes for correct audio pitch Replace fixed 48 kHz sample-and-hold with a proper resampler that adapts to runtime SCLK changes. Games like Skyhammer and Iron Soldier 2 change the I2S serial clock mid-frame for pitch effects, causing audio clipping when the output buffer assumes a constant rate. The fix captures DSP-produced samples (LTXD/RTXD writes) into a ring buffer at the hardware I2S rate, then linearly interpolates to 48 kHz in DSPSampleCallback. When SCLK changes, DACUpdateSCLKRate() recomputes the ratio using the JTRM formula: rate = sysclk / (64 * (SCLK + 1)). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(dac): increase ring buffer and upper clamp for extreme SCLK values SCLK=0 gives divider=64, yielding ~415kHz I2S rate (ratio ~8.66 to 48kHz host). The previous 4.0 upper clamp rejected this valid setting, and the 4096-sample ring buffer was too small for ~6900 samples/frame at that rate. - Raise upper clamp from 4.0 to 16.0 (covers all valid SCLK 0-255) - Increase I2S_RING_SIZE from 4096 to 8192 Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(dac): address review feedback on resampler edge cases - Increase I2S ring buffer to 16384 (covers PAL SCLK=0 at ~8311 samples/frame) - Preserve fractional interpolation phase across frame boundaries (prevents clicks) - Lower SCLK ratio floor from 0.1 to 0.01 (allows valid SCLK=255 at ~1624 Hz) Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(dac): fix frame-boundary interpolation and rename misleading counter - Seed ring[0] with previous frame's last sample so interpolation has a valid left endpoint across frame boundaries (eliminates periodic click) - Rename i2sReadCount → i2sWriteCount (it counts writes, not reads) - Track last sample pair for frame-boundary seeding Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(dac): seed ring from registers, remove i2sLast fields - Seed ring[0..1] with current *ltxd/*rtxd at frame start instead of tracking separate i2sLastL/R. This maintains "hold current DAC value" behavior and provides two valid interpolation endpoints. - Guard path uses i2sWriteCount < 2 (reachable only after mid-frame reset). - Removes i2sLastL/R (no extra savestate fields needed). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(dac): use canonical clock constants, fix byte-write SCLK path - Use RISC_CLOCK_RATE_NTSC/PAL from jaguar.h instead of hardcoded literals (stays in sync if clock values are corrected elsewhere) - Fix DACWriteByte: forward to SCLK+2 (not SCLK+0) so byte writes via MOVE.B correctly trigger DACUpdateSCLKRate() Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor(clock): add SYSTEM_CLOCK_RATE macro, DRY clock selection Add SYSTEM_CLOCK_RATE and M68K_CLOCK_RATE macros to jaguar.h that encapsulate the NTSC/PAL ternary. Replaces duplicated patterns in dac.c and jerry.c. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(dac): make DACUpdateSCLKRate static + handle SMODE byte writes DACUpdateSCLKRate is only used within dac.c — remove from public header and make it static with a forward declaration. Add SMODE+3 byte-write forwarding in DACWriteByte so games using MOVE.B to toggle SMODE bits correctly update the serial mode register (same pattern as the existing SCLK byte path). Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(pit): accept SYSTEM_CLOCK_RATE as valid RISC clock reference The PIT clock rate regression guard greps function bodies for literal RISC_CLOCK_RATE_* strings. Since JERRYGetPIT1/2Frequency now uses the SYSTEM_CLOCK_RATE macro (which expands to the RISC clock ternary), add it to the allowlist so the test correctly recognizes it as using the full system clock. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
189 lines
5.3 KiB
C
189 lines
5.3 KiB
C
/*
|
|
* test_pit_clock_rate.c -- regression guard for the recurring PIT clock
|
|
* rate bug.
|
|
*
|
|
* Per JTRM (docs/jtrm-clocks-timing.md): "The PIT clock is the FULL
|
|
* system clock (26.59 MHz), NOT half. ... If you see system_clock / 2
|
|
* in PIT calculations, it is a bug."
|
|
*
|
|
* This bug has been re-introduced multiple times with the rationale
|
|
* that "Doom plays correctly at half rate" -- but actually halving the
|
|
* PIT rate breaks Doom and Rayman music timing.
|
|
*
|
|
* This test is a textual guard that fails the build if anyone schedules
|
|
* JERRYPIT1/2 or TOMPIT callbacks using the M68K clock constants
|
|
* (M68K_CYCLE_IN_USEC / M68K_CYCLE_PAL_IN_USEC) instead of the RISC
|
|
* constants (RISC_CYCLE_IN_USEC / RISC_CYCLE_PAL_IN_USEC), or if the
|
|
* frequency helpers use M68K_CLOCK_RATE_NTSC/PAL.
|
|
*
|
|
* If a real cycle-accurate emulation reason ever surfaces to change
|
|
* this, update the test and the JTRM doc together -- don't just edit
|
|
* the source.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static int g_failures = 0;
|
|
|
|
static char *load_file(const char *path)
|
|
{
|
|
FILE *f = fopen(path, "rb");
|
|
long sz;
|
|
char *buf;
|
|
size_t got;
|
|
if (!f)
|
|
{
|
|
fprintf(stderr, "FATAL: cannot open %s\n", path);
|
|
exit(2);
|
|
}
|
|
fseek(f, 0, SEEK_END);
|
|
sz = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
buf = (char *)malloc((size_t)sz + 1);
|
|
got = fread(buf, 1, (size_t)sz, f);
|
|
buf[got] = 0;
|
|
fclose(f);
|
|
return buf;
|
|
}
|
|
|
|
/* Returns pointer to the start of `func_name`'s body in `src`, or NULL.
|
|
* Does the dumb thing: finds "func_name(", then walks to the next '{'.
|
|
* Returns the matching '}' end via *end_out.
|
|
*
|
|
* This is good enough for our two callers (well-formed C, no nested
|
|
* function definitions, single open-brace function bodies).
|
|
*/
|
|
static char *find_func_body(const char *src, const char *func_name, char **end_out)
|
|
{
|
|
const char *p;
|
|
const char *open;
|
|
int depth;
|
|
size_t needle_len;
|
|
char needle[128];
|
|
snprintf(needle, sizeof(needle), "%s(", func_name);
|
|
needle_len = strlen(needle);
|
|
|
|
p = src;
|
|
while ((p = strstr(p, needle)) != NULL)
|
|
{
|
|
/* Walk forward to first '{' that opens a body. Bail if we hit ';'
|
|
* before '{' (that's a forward declaration, not a definition). */
|
|
const char *q = p + needle_len;
|
|
while (*q && *q != '{' && *q != ';')
|
|
q++;
|
|
if (*q != '{')
|
|
{
|
|
p = q;
|
|
continue;
|
|
}
|
|
open = q;
|
|
depth = 0;
|
|
while (*q)
|
|
{
|
|
if (*q == '{')
|
|
depth++;
|
|
else if (*q == '}')
|
|
{
|
|
depth--;
|
|
if (depth == 0)
|
|
{
|
|
*end_out = (char *)q;
|
|
return (char *)open;
|
|
}
|
|
}
|
|
q++;
|
|
}
|
|
return NULL; /* unbalanced */
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void check_func_uses_risc(const char *path, const char *src, const char *func)
|
|
{
|
|
char *end = NULL;
|
|
char *body = find_func_body(src, func, &end);
|
|
size_t len;
|
|
char *snippet;
|
|
int has_risc, has_m68k;
|
|
|
|
if (!body || !end)
|
|
{
|
|
fprintf(stderr, "FAIL: %s: cannot locate function %s\n", path, func);
|
|
g_failures++;
|
|
return;
|
|
}
|
|
len = (size_t)(end - body);
|
|
snippet = (char *)malloc(len + 1);
|
|
memcpy(snippet, body, len);
|
|
snippet[len] = 0;
|
|
|
|
has_risc = (strstr(snippet, "RISC_CYCLE_IN_USEC") != NULL)
|
|
|| (strstr(snippet, "RISC_CYCLE_PAL_IN_USEC") != NULL)
|
|
|| (strstr(snippet, "RISC_CLOCK_RATE_NTSC") != NULL)
|
|
|| (strstr(snippet, "RISC_CLOCK_RATE_PAL") != NULL)
|
|
|| (strstr(snippet, "SYSTEM_CLOCK_RATE") != NULL);
|
|
has_m68k = (strstr(snippet, "M68K_CYCLE_IN_USEC") != NULL)
|
|
|| (strstr(snippet, "M68K_CYCLE_PAL_IN_USEC") != NULL)
|
|
|| (strstr(snippet, "M68K_CLOCK_RATE_NTSC") != NULL)
|
|
|| (strstr(snippet, "M68K_CLOCK_RATE_PAL") != NULL);
|
|
|
|
if (has_m68k)
|
|
{
|
|
fprintf(stderr,
|
|
"FAIL: %s: function %s references M68K_*_IN_USEC / "
|
|
"M68K_CLOCK_RATE_*\n"
|
|
" PIT clock must be RISC (full system clock) per JTRM.\n"
|
|
" See docs/jtrm-clocks-timing.md.\n",
|
|
path, func);
|
|
g_failures++;
|
|
}
|
|
if (!has_risc)
|
|
{
|
|
fprintf(stderr,
|
|
"FAIL: %s: function %s does not reference any RISC_*_IN_USEC "
|
|
"constant.\n"
|
|
" Expected the PIT scheduling to use RISC_CYCLE_IN_USEC / "
|
|
"RISC_CYCLE_PAL_IN_USEC.\n",
|
|
path, func);
|
|
g_failures++;
|
|
}
|
|
free(snippet);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
const char *jerry_path = "src/jerry/jerry.c";
|
|
const char *tom_path = "src/tom/tom.c";
|
|
char *jerry_src;
|
|
char *tom_src;
|
|
|
|
if (argc > 1)
|
|
jerry_path = argv[1];
|
|
if (argc > 2)
|
|
tom_path = argv[2];
|
|
|
|
jerry_src = load_file(jerry_path);
|
|
tom_src = load_file(tom_path);
|
|
|
|
check_func_uses_risc(jerry_path, jerry_src, "JERRYResetPIT1");
|
|
check_func_uses_risc(jerry_path, jerry_src, "JERRYResetPIT2");
|
|
check_func_uses_risc(jerry_path, jerry_src, "JERRYGetPIT1Frequency");
|
|
check_func_uses_risc(jerry_path, jerry_src, "JERRYGetPIT2Frequency");
|
|
check_func_uses_risc(tom_path, tom_src, "TOMResetPIT");
|
|
|
|
free(jerry_src);
|
|
free(tom_src);
|
|
|
|
if (g_failures)
|
|
{
|
|
fprintf(stderr, "FAIL: %d PIT clock rate violation(s) detected.\n",
|
|
g_failures);
|
|
return 1;
|
|
}
|
|
printf("PASS: PIT scheduling functions use the RISC (full system clock) "
|
|
"rate.\n");
|
|
return 0;
|
|
}
|