Files
Joseph MattielloandClaude Opus 5.5 c1eb2b10a0 fix(hle): park ILLEGAL for uploaded executables so homebrew stops hanging the accurate blitter
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]>
2026-10-07 06:28:41 -04:00

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;
}