mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
Seven homebrew carts froze retro_run under the default (Accurate) blitter. The blitter was the victim, not the cause: - Raw BJL binaries and .abs images carry no vector table, and many end their 68K main with ILLEGAL (the Alpine/BJL "drop into the debugger" idiom; the GPU/OP keep running the demo). HLE installed RTE stubs only for cartridges, and file.c guarded only Alpine/JagServer images, so the trap went through a zero vector to $0, ran the vector table as code and walked the stack down through $0, $FFxxxx and into TOM. - There the 68K's own exception frames land on B_COUNT/B_CMD ($F0A238 reaches $F02238 through TOMWriteWord's $FF3FFF mask): SR becomes B_CMD's low word ($2208 = S|IPL2|N for DEMO1B), the pushed PC becomes B_COUNT, and the write starts a blit with the zeroed A1 registers -> phrase mode, 1bpp, X=0. - That blit never ends on real hardware either. INNER.NET's inner-counter decrement (Inc0-Inc3) only uses the 8/16/32 bpp decodes beyond bit 0, so below 8bpp it is dstxp[0]; phrase-mode X add truncates to a phrase boundary (JTRM v8 p.71), so after the first write the counter never moves. BlitterMidsummer2 matches the netlist term for term, which is why it spun forever; the Fast engine just shrugs the garbage off. Fix: JaguarReset parks vector 4 at a BRA.S * loop at $1000 for every non-cartridge, non-CD image -- the guard file.c already used for Alpine and JagServer, now re-armed on every reset so retro_reset()'s RAM clear cannot wipe it. Only vector 4 is parked; skipped if the image covers the vector or the loop. No blitter semantics change, no savestate change. Also: - crash_detect: inframe_hang now also fires (never_ends=1) for a blit whose inner counter cannot decrement (phrase mode below 8bpp). DEMO1B's blit (14.4M pixels) was under the 2^24 pixel rule and logged nothing. The "inner 0 = one step" assumption now cites INNER.NET's zero/underflow detect. CrashDetectNoteBlit takes the destination flags and X. - cart_boot_matrix.sh: CART_MATRIX_PROBE_ARGS knob so the matrix can sweep the Accurate blitter. Folded into the row-cache id and default log dir, and refused without an explicit CART_MATRIX_OUT, so it can never reuse or publish over the Fast rows. - test/tools/test_upload_illegal_park: ROM-free synthetic raw binary that ends in ILLEGAL must park at $1000, also after retro_reset (the old core hangs on it); plus a ROM-gated DEMO1B run under the Accurate blitter. test_crash_detect_inframe gains 11 never_ends rows. Before/after, Accurate blitter, 600 frames: before, all 7 carts timed out in HLE (120 s cap) and 6 of 7 in real-BIOS mode (90 s cap; JagMania (Jul 8) completed); after, all 7 complete in both boot modes. Chroma-Luma (bin) and JagMania (Jul 8) no longer hang but stay broken: their raw-binary load address is inferred wrong (a JSR lands in font data / mid-instruction), a separate loader bug. Native Demo (bin) HLE, already broken (68K wild by frame 2, GPU runaway at frame 1), now takes a different chaotic path that reaches the same never-ending blit at frame 170, so it hangs under Accurate where it used to end on a black screen. That is the #794 class below. Music Demo (ScatoLOGIC) --bios (#794) is a cartridge in real-BIOS mode, so this guard does not apply; its hanging blit is the same never-ending 1bpp phrase-mode shape and is now named by never_ends=1. Closes #800 Refs #794 Co-Authored-By: Claude Opus 5.5 <[email protected]>
241 lines
8.9 KiB
C
241 lines
8.9 KiB
C
/*
|
|
* Unit test for blitter 64-bit register MMIO byte ordering.
|
|
*
|
|
* Verifies that the longword swap in BlitterWriteByte correctly maps
|
|
* the Jaguar's F-bus layout (lower address = low longword) to the
|
|
* internal blitter_ram layout expected by GET64 and the Gouraud
|
|
* byte-level reads.
|
|
*
|
|
* Build (via Makefile):
|
|
* make TEST_EXPORTS=1 test/test_blitter_mmio
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
#include "libretro.h"
|
|
|
|
#include "vjag_memory.h"
|
|
#include "settings.h"
|
|
#include "blitter.h"
|
|
|
|
/* Provide blitter_ram storage (normally in blitter.c) */
|
|
uint8_t blitter_ram[0x100];
|
|
|
|
/* Stubs for dependencies we don't need */
|
|
struct VJSettings vjs = { 0 };
|
|
/* blitter_mmio.c logs a refused nested blit (#659) through src/core/log.h.
|
|
* NULL here makes VJ_LOG fall back to its vj_log_stderr path, so the warning
|
|
* still works standalone instead of the include becoming a link error. */
|
|
retro_log_printf_t vj_log_cb = NULL;
|
|
int BlitterCompareIsEnabled(void) { return 0; }
|
|
void BlitterRunComparison(void) {}
|
|
void blitter_blit(uint32_t cmd) { (void)cmd; }
|
|
void BlitterMidsummer2(void) {}
|
|
/* The B_CMD decode statics live in blitter.c, which this test
|
|
* deliberately does not link; the real one is exercised by
|
|
* test/tools/blitter_static_leak (#479). */
|
|
void BlitterResetDecodeState(void) {}
|
|
int BlitMemoLaunch(void) { return 0; } /* memo off: dispatch as before */
|
|
int texDumpEnabled = 0; /* texture dump off */
|
|
void TexDumpLaunch(void) {}
|
|
int texReplaceEnabled = 0; /* texture replacement off */
|
|
int TexReplacePreBlit(void) { return 0; }
|
|
void TexReplacePostBlit(void) {}
|
|
void CrashDetectNoteBlit(uint32_t b_count, uint32_t b_cmd, uint32_t a1_base,
|
|
uint32_t dst_flags, uint32_t dst_x)
|
|
{ (void)b_count; (void)b_cmd; (void)a1_base; (void)dst_flags; (void)dst_x; } /* inframe_hang (#740, #800) */
|
|
|
|
/* Blitter bus-time model dependencies (vjs.blitterTiming stays 0 in the
|
|
* stub settings above, so the timing path short-circuits; these only
|
|
* need to link). */
|
|
#include "bus_arbiter.h"
|
|
struct BusArbiter busArbiter;
|
|
void GPUChargeBusStall(uint32_t sysclks) { (void)sysclks; }
|
|
|
|
static int failures = 0;
|
|
|
|
#define CHECK(cond, fmt, ...) do { \
|
|
if (!(cond)) { \
|
|
fprintf(stderr, "FAIL: " fmt "\n", __VA_ARGS__); \
|
|
failures++; \
|
|
} \
|
|
} while(0)
|
|
|
|
/* Register offsets (same as blitter_mmio.c) */
|
|
#define SRCDATA ((uint32_t)0x40)
|
|
#define DSTDATA ((uint32_t)0x48)
|
|
#define DSTZ ((uint32_t)0x50)
|
|
#define SRCZINT ((uint32_t)0x58)
|
|
#define SRCZFRAC ((uint32_t)0x60)
|
|
#define PATTERNDATA ((uint32_t)0x68)
|
|
#define PHRASEINT0 ((uint32_t)0x7C)
|
|
#define PHRASEINT1 ((uint32_t)0x80)
|
|
#define PHRASEINT2 ((uint32_t)0x84)
|
|
#define PHRASEINT3 ((uint32_t)0x88)
|
|
|
|
static void write_long(uint32_t offset, uint32_t val)
|
|
{
|
|
BlitterWriteByte(offset + 0, (val >> 24) & 0xFF, 0);
|
|
BlitterWriteByte(offset + 1, (val >> 16) & 0xFF, 0);
|
|
BlitterWriteByte(offset + 2, (val >> 8) & 0xFF, 0);
|
|
BlitterWriteByte(offset + 3, (val) & 0xFF, 0);
|
|
}
|
|
|
|
static void test_swap_one_register(const char *name, uint32_t reg)
|
|
{
|
|
uint64_t got;
|
|
|
|
BlitterReset();
|
|
|
|
/* Simulate GPU writing two 32-bit longwords to a 64-bit register.
|
|
* On hardware, the low F-bus address (reg+0) carries the LOW longword
|
|
* and the high address (reg+4) carries the HIGH longword.
|
|
* After the swap, GET64 should return (high_data << 32) | low_data. */
|
|
write_long(reg + 0, 0x11223344); /* low longword to low address */
|
|
write_long(reg + 4, 0xAABBCCDD); /* high longword to high address */
|
|
|
|
got = GET64(blitter_ram, reg);
|
|
|
|
CHECK(got == 0xAABBCCDD11223344ULL,
|
|
"%s: GET64 = 0x%016llX, expected 0xAABBCCDD11223344",
|
|
name, (unsigned long long)got);
|
|
}
|
|
|
|
static void test_phraseint_unaffected(void)
|
|
{
|
|
BlitterReset();
|
|
|
|
/* PHRASEINT writes bypass the swap and go directly to blitter_ram.
|
|
* Pixel 0 intensity → PATTERNDATA byte 7
|
|
* Pixel 3 intensity → PATTERNDATA byte 1 */
|
|
BlitterWriteByte(PHRASEINT0 + 1, 0xAA, 0); /* pixel 0 → PATTERNDATA+7 */
|
|
BlitterWriteByte(PHRASEINT3 + 1, 0xBB, 0); /* pixel 3 → PATTERNDATA+1 */
|
|
|
|
CHECK(blitter_ram[PATTERNDATA + 7] == 0xAA,
|
|
"PHRASEINT0 pixel 0 intensity: got 0x%02X at PATTERNDATA+7, expected 0xAA",
|
|
blitter_ram[PATTERNDATA + 7]);
|
|
CHECK(blitter_ram[PATTERNDATA + 1] == 0xBB,
|
|
"PHRASEINT3 pixel 3 intensity: got 0x%02X at PATTERNDATA+1, expected 0xBB",
|
|
blitter_ram[PATTERNDATA + 1]);
|
|
}
|
|
|
|
static void test_gouraud_byte_layout(void)
|
|
{
|
|
/* Verify the Gouraud init reads (gd_c[]/gd_i[] in blitter.c)
|
|
* see the correct per-pixel data after PHRASEINT writes.
|
|
*
|
|
* The Gouraud code reads:
|
|
* gd_c[0] = blitter_ram[PATTERNDATA + 6] (pixel 0 color)
|
|
* gd_i[0] = blitter_ram[PATTERNDATA + 7] (pixel 0 intensity)
|
|
* gd_c[3] = blitter_ram[PATTERNDATA + 0] (pixel 3 color)
|
|
* gd_i[3] = blitter_ram[PATTERNDATA + 1] (pixel 3 intensity)
|
|
*/
|
|
BlitterReset();
|
|
|
|
/* Write distinct values for each pixel via PHRASEINT */
|
|
BlitterWriteByte(PHRASEINT0 + 1, 0x10, 0); /* pixel 0 intensity → byte 7 */
|
|
BlitterWriteByte(PHRASEINT1 + 1, 0x20, 0); /* pixel 1 intensity → byte 5 */
|
|
BlitterWriteByte(PHRASEINT2 + 1, 0x30, 0); /* pixel 2 intensity → byte 3 */
|
|
BlitterWriteByte(PHRASEINT3 + 1, 0x40, 0); /* pixel 3 intensity → byte 1 */
|
|
|
|
/* Verify Gouraud read positions */
|
|
CHECK(blitter_ram[PATTERNDATA + 7] == 0x10,
|
|
"Gouraud pixel 0 intensity at byte 7: got 0x%02X, expected 0x10",
|
|
blitter_ram[PATTERNDATA + 7]);
|
|
CHECK(blitter_ram[PATTERNDATA + 5] == 0x20,
|
|
"Gouraud pixel 1 intensity at byte 5: got 0x%02X, expected 0x20",
|
|
blitter_ram[PATTERNDATA + 5]);
|
|
CHECK(blitter_ram[PATTERNDATA + 3] == 0x30,
|
|
"Gouraud pixel 2 intensity at byte 3: got 0x%02X, expected 0x30",
|
|
blitter_ram[PATTERNDATA + 3]);
|
|
CHECK(blitter_ram[PATTERNDATA + 1] == 0x40,
|
|
"Gouraud pixel 3 intensity at byte 1: got 0x%02X, expected 0x40",
|
|
blitter_ram[PATTERNDATA + 1]);
|
|
}
|
|
|
|
static void test_direct_write_does_not_corrupt_phraseint(void)
|
|
{
|
|
/* A direct write to PATTERNDATA+0..3 should go to blitter_ram+4..7
|
|
* (via swap), NOT to bytes 0..3 which hold pixel 2-3 PHRASEINT data. */
|
|
BlitterReset();
|
|
|
|
/* First set pixel 3 intensity via PHRASEINT */
|
|
BlitterWriteByte(PHRASEINT3 + 1, 0xFF, 0); /* → PATTERNDATA byte 1 */
|
|
CHECK(blitter_ram[PATTERNDATA + 1] == 0xFF,
|
|
"pre-check: pixel 3 intensity at byte 1: got 0x%02X",
|
|
blitter_ram[PATTERNDATA + 1]);
|
|
|
|
/* Now do a direct 32-bit write to PATTERNDATA+0 (low address).
|
|
* With the swap this goes to bytes 4-7, not bytes 0-3.
|
|
* Pixel 3's intensity at byte 1 must be preserved. */
|
|
write_long(PATTERNDATA + 0, 0xDEADBEEF);
|
|
|
|
CHECK(blitter_ram[PATTERNDATA + 1] == 0xFF,
|
|
"direct write to PATTERNDATA+0..3 must not clobber byte 1: got 0x%02X",
|
|
blitter_ram[PATTERNDATA + 1]);
|
|
|
|
/* The written data should land at bytes 4-7 */
|
|
CHECK(blitter_ram[PATTERNDATA + 4] == 0xDE,
|
|
"PATTERNDATA+0 write → byte 4: got 0x%02X, expected 0xDE",
|
|
blitter_ram[PATTERNDATA + 4]);
|
|
CHECK(blitter_ram[PATTERNDATA + 7] == 0xEF,
|
|
"PATTERNDATA+3 write → byte 7: got 0x%02X, expected 0xEF",
|
|
blitter_ram[PATTERNDATA + 7]);
|
|
}
|
|
|
|
static void test_asymmetric_halves(void)
|
|
{
|
|
/* This is the Battle Sphere regression case: writing different values
|
|
* to the two halves of a 64-bit register must produce the correct
|
|
* GET64 result. Without the swap, the halves get transposed. */
|
|
uint64_t got;
|
|
|
|
BlitterReset();
|
|
|
|
/* Write pattern: low half = all zeros, high half = white pixels */
|
|
write_long(PATTERNDATA + 0, 0x00000000); /* low address = low data */
|
|
write_long(PATTERNDATA + 4, 0xFFFFFFFF); /* high address = high data */
|
|
|
|
got = GET64(blitter_ram, PATTERNDATA);
|
|
|
|
CHECK(got == 0xFFFFFFFF00000000ULL,
|
|
"asymmetric PATTERNDATA: GET64 = 0x%016llX, expected 0xFFFFFFFF00000000",
|
|
(unsigned long long)got);
|
|
|
|
/* Reverse pattern */
|
|
BlitterReset();
|
|
write_long(PATTERNDATA + 0, 0xFFFFFFFF);
|
|
write_long(PATTERNDATA + 4, 0x00000000);
|
|
|
|
got = GET64(blitter_ram, PATTERNDATA);
|
|
CHECK(got == 0x00000000FFFFFFFFULL,
|
|
"asymmetric PATTERNDATA reversed: GET64 = 0x%016llX, expected 0x00000000FFFFFFFF",
|
|
(unsigned long long)got);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("test_blitter_mmio: 64-bit register byte ordering\n");
|
|
|
|
test_swap_one_register("SRCDATA", SRCDATA);
|
|
test_swap_one_register("DSTDATA", DSTDATA);
|
|
test_swap_one_register("DSTZ", DSTZ);
|
|
test_swap_one_register("SRCZINT", SRCZINT);
|
|
test_swap_one_register("SRCZFRAC", SRCZFRAC);
|
|
test_swap_one_register("PATTERNDATA", PATTERNDATA);
|
|
|
|
test_phraseint_unaffected();
|
|
test_gouraud_byte_layout();
|
|
test_direct_write_does_not_corrupt_phraseint();
|
|
test_asymmetric_halves();
|
|
|
|
if (failures == 0)
|
|
printf("All tests passed.\n");
|
|
else
|
|
printf("%d test(s) FAILED.\n", failures);
|
|
|
|
return failures ? 1 : 0;
|
|
}
|