mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
fix(blitter): a blit hardware never finishes hangs the blitter, not the host
Phrase-mode blits below 8bpp never end on hardware. INNER.NET builds the inner-counter decrement (Inc0-Inc3) from the 8/16/32bpp decodes, 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 and inner0 is never reached. The synchronous accurate engine reproduced that faithfully and never returned from retro_run (Music Demo --bios, #794; Native Demo (bin) HLE). Policy: hang the emulated blitter, never the host. - BlitterNeverEnds() (blitter_mmio.c) is the single exact predicate. The accurate engine, the dispatch path and the crash watchdog (CrashDetectBlitNeverEnds now delegates to it) all use it. - The accurate engine runs the stuck step for exactly one destination wrap period, then returns. That is 65536/ppp phrase writes (1024, 2048 or 4096 at 1/2/4bpp), 65536 with the Y-add bit, +1 when the start X is not phrase-aligned. Bounded at 65,536 writes. It sets a stop flag only after the inner0/mask logic, so no write mask changes. The collapsed pattern-fill and copy paths are bypassed for these blits. - The result is EXACT when repeating the step is idempotent on memory. Every observed homebrew hang has that shape: DEMO1B $2208, Chroma $2704, Native Demo $2718 (LFU clear, no accumulators). - Otherwise it is a defined one-pass approximation: BlitterNeverEndsApprox() flags Gouraud/Z/shade accumulators, comparator masks, ADDDSEL, LFU feeding back D, or LFU fed by a separately stepping source (Music Demo, JagMania). The watchdog line now says never_ends=1 approx=1 for those. - Dispatch then sets a sticky "blitter hung" flag, under both engines. - B_CMD status reads busy (JTRM v8 p.75): IDLE clear, outer-INNER and inner-DWRITE set, and the stuck inner count in bits 16-31. - Further B_CMD starts are ignored, because OUTER.NET leaves idle only on go (innert[0] = idle AND go). - The flag clears only in BlitterReset (reset / power-on). - Known limit: hardware still latches B_COUNT/B_CMD/pointer writes mid-blit (FDSYNC on countld/cmdld), so a B_COUNT write with inner 0 would un-stick it. JTRM p.70 calls that undefined; this model does not resume a hung blit. Savestate: v16 extended in place, no bump. A "BLH1" chunk (hung flag and stuck count, 7 bytes) is appended strictly after #814's CDX1, with a room check before writing. It is read only for version >= 16. A missing or wrong magic, or an older state, loads as not hung. Tests: - test/tools/test_blitter_hung, ROM-free, accurate and fast engines. - The synthetic never-ending blit returns. - Exactly the 8 KB wrap period is written and the next phrase is untouched. - Status reads busy with count $DC, and a second start writes nothing. - serialize -> retro_reset clears the flag -> unserialize restores it. - ROM-gated runs of Music Demo --bios and Native Demo (bin): 600 frames each, and the blitter ends hung. - test_crash_detect_inframe gains 10 approx rows. Closes #794 Refs #800 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c1eb2b10a0
commit
d3bc33e001
13 files changed
+707
-48
No files matched your search
@@ -1355,7 +1355,7 @@ clean:
|
||||
test/tools/test_frame_timing test/tools/test_runahead_determinism test/tools/test_pertitle_db \
|
||||
test/tools/test_disk_control test/tools/cd_wedge_probe \
|
||||
test/test_biosdb test/test_cart_bios_loader \
|
||||
test/test_titledb test/test_titlehook test/tools/test_hook_gate test/tools/test_upload_illegal_park \
|
||||
test/test_titledb test/test_titlehook test/tools/test_hook_gate test/tools/test_upload_illegal_park test/tools/test_blitter_hung \
|
||||
test/tools/test_wedge_spin test/tools/test_texdump test/tools/test_texreplace test/test_voicechat test/test_voice_netpacket test/tools/test_voicechat_inertness test/tools/voicechat_pair test/tools/i2s_lag_probe \
|
||||
test/tools/dsp_idle_probe_falsify test/tools/dsp_idle_ab \
|
||||
test/tools/gpu_idle_probe_falsify test/tools/gpu_idle_ab \
|
||||
@@ -1430,7 +1430,7 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
test/test_audio_dac test/test_blitter \
|
||||
test/tools/test_memory_map test/tools/test_op_gpu_object test/tools/test_option_visibility test/test_memtrack test/test_nvmbios test/test_uart_core test/test_netlink_host \
|
||||
test/tools/netlink_pair test/tools/netlink_latency test/tools/netlink_delay_proxy test/tools/netlink_discover_probe test/tools/netlink_rebuild_witness test/tools/netlink_mismatch_witness test/tools/perf_iface_witness test/tools/voicemodem_pair test/tools/voicechat_pair test/tools/test_voicechat_inertness test/tools/netlink_game test/tools/test_pertitle_db test/tools/test_disk_control \
|
||||
test/tools/test_hook_gate test/tools/test_upload_illegal_park \
|
||||
test/tools/test_hook_gate test/tools/test_upload_illegal_park test/tools/test_blitter_hung \
|
||||
test/tools/i2s_lag_probe test/tools/joymatrix_identity \
|
||||
test/tools/teamtap_ports \
|
||||
test/test_quadrature test/test_axistune test/tools/mouse_decode_test \
|
||||
@@ -1854,6 +1854,24 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
@# (it hung retro_run forever under the accurate blitter). The tool
|
||||
@# forces the accurate blitter and arms its own wall-clock alarm.
|
||||
./test/tools/test_upload_illegal_park ./$(TARGET)
|
||||
@# A blit hardware never finishes (#800/#794) hangs the emulated
|
||||
@# blitter, never the host: synthetic never-ending blit under both
|
||||
@# engines (returns, B_CMD busy, starts ignored, reset clears,
|
||||
@# savestate restores), then the two measured reproducers.
|
||||
./test/tools/test_blitter_hung ./$(TARGET)
|
||||
./test/tools/test_blitter_hung ./$(TARGET) --fast
|
||||
@rom=$$(bash scripts/find-rom.sh 'Music Demo (2002) (ScatoLOGIC).jag' 'Music Demo*.jag'); \
|
||||
if [ -n "$$rom" ]; then \
|
||||
./test/tools/test_blitter_hung ./$(TARGET) --rom "$$rom" --bios; \
|
||||
else \
|
||||
bash scripts/test-skip.sh record "Hung blitter (Music Demo --bios, #794)" "no ROM matching 'Music Demo*' in the private corpus"; \
|
||||
fi
|
||||
@rom=$$(bash scripts/find-rom.sh 'Native Demo (bin) (1997).jag' 'Native Demo (bin)*'); \
|
||||
if [ -n "$$rom" ]; then \
|
||||
./test/tools/test_blitter_hung ./$(TARGET) --rom "$$rom"; \
|
||||
else \
|
||||
bash scripts/test-skip.sh record "Hung blitter (Native Demo (bin), #800)" "no ROM matching 'Native Demo (bin)*' in the private corpus"; \
|
||||
fi
|
||||
@rom=$$(bash scripts/find-rom.sh 'DEMO1B (PD) [[]a1[]].jag' 'DEMO1B (PD).jag' 'DEMO1B*.jag'); \
|
||||
if [ -n "$$rom" ]; then \
|
||||
./test/tools/test_upload_illegal_park ./$(TARGET) --rom "$$rom" --frames 600; \
|
||||
@@ -2852,6 +2870,14 @@ test/test_crash_detect_inframe: test/test_crash_detect_inframe.c
|
||||
|
||||
# Uploaded-executable ILLEGAL park (#800): harness tool, needs the wide
|
||||
# test ABI for m68k_get_reg.
|
||||
# Never-ending blit -> hung emulated blitter (#800/#794).
|
||||
test/tools/test_blitter_hung: test/tools/test_blitter_hung.c \
|
||||
test/harness/harness.c test/harness/harness.h
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
-o $@ test/tools/test_blitter_hung.c \
|
||||
test/harness/harness.c \
|
||||
$(if $(filter Linux,$(shell uname -s)),-ldl) -lm
|
||||
|
||||
test/tools/test_upload_illegal_park: test/tools/test_upload_illegal_park.c \
|
||||
test/harness/harness.c test/harness/harness.h
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
|
||||
@@ -169,9 +169,15 @@ heartbeat every 600 frames). Cost enabled: one indirect call + ~256-px hash/fram
|
||||
Williams/Telegames carts write `B_COUNT=0` at boot and run fine). Also fires with
|
||||
`never_ends=1` for a blit whose inner counter never decrements: phrase mode below 8bpp, where
|
||||
INNER.NET's decrement is `dstxp[0]` and phrase-aligned X keeps it 0 — real hardware never
|
||||
finishes it either (#800). The blitter runs synchronously inside one register write, so such a
|
||||
blit can keep `retro_run` from returning forever and no per-frame signature ever gets a turn;
|
||||
this line is logged *before* it runs. Reproducer: Music Demo (ScatoLOGIC) `--bios` (#794).
|
||||
finishes it either (#800). `approx=1` marks a hang whose memory effect below is only a
|
||||
one-pass approximation (`BlitterNeverEndsApprox`). Policy: hang the emulated blitter, never the
|
||||
host -- the accurate engine runs the stuck step for one destination wrap period (exact when
|
||||
the step is idempotent on memory), then `B_CMD` reads busy and further starts are ignored
|
||||
until reset (`test/tools/test_blitter_hung`; saved in the v16 `"BLH1"` chunk). Known limit: on
|
||||
hardware a `B_COUNT` write while hung would un-stick it; not modelled. Absurd-size blits are
|
||||
still run synchronously and can keep `retro_run` from returning for hours; this line is logged
|
||||
*before* the blit runs. Reproducers: Music Demo (ScatoLOGIC) `--bios` (#794, `approx=1`),
|
||||
Native Demo (bin) HLE.
|
||||
Matrix runs killed at the wall-clock cap carry it in their evidence column.
|
||||
- Seen as garbage blits from a runaway 68K whose exception frames land on the blitter's
|
||||
`$F0A238` mirror. Uploaded executables (raw BJL/.abs) that end in ILLEGAL used to do this in
|
||||
|
||||
@@ -82,6 +82,29 @@ file written by a v3.7.0 build (`274fa74`), Hover Strike real-BIOS. Regression:
|
||||
`test_state_compat` (`v16_*`, `v15_*` rows) and the ROM-gated real-BIOS Hover
|
||||
Strike run in `make test`.
|
||||
|
||||
### v16, extended in place: hung blitter (#800, #794)
|
||||
|
||||
A third trailing chunk, strictly after `"CDX1"`, 7 bytes; `STATE_VERSION`
|
||||
stays 16.
|
||||
|
||||
| Chunk | Magic | Fields | Owner |
|
||||
|---|---|---|---|
|
||||
| Hung blitter | `"BLH1"` | `blitterHung`, `blitterHungICount` | `src/tom/blitter_mmio.c` |
|
||||
|
||||
A blit that never finishes on hardware (phrase mode below 8bpp: INNER.NET's
|
||||
inner-counter decrement is `dstxp[0]`, which phrase-aligned X keeps at 0)
|
||||
leaves the emulated blitter hung until reset: `B_CMD` reads busy (IDLE clear,
|
||||
the stuck inner count in bits 16-31) and further starts are ignored. That flag
|
||||
is machine state, so it is saved: without it a rollback across the hang would
|
||||
replay with an idle blitter that accepts blits hardware would ignore.
|
||||
|
||||
Loading: read only for `version >= 16`, after `"CDX1"`. A v16 state written
|
||||
before this chunk existed ends in the blob's zero-filled tail at that offset,
|
||||
so the magic misses and it loads as not hung; v15 and older reset it to not
|
||||
hung too. Saving room-checks before writing, like the other trailing chunks.
|
||||
Regression: `test/tools/test_blitter_hung` (serialize, `retro_reset` clears,
|
||||
unserialize restores).
|
||||
|
||||
## v8: Jaguar GameDrive chunk
|
||||
|
||||
v8 (one shared bump per release policy — all in-flight changes since v3.1.0
|
||||
|
||||
@@ -64,6 +64,10 @@ _cd_boot_strategy_*
|
||||
_jaguar_cd_*
|
||||
_ResolveBootConfig
|
||||
_BlitterCompare*
|
||||
_BlitterIsHung
|
||||
_BlitterNeverEndsApprox
|
||||
_BlitterReadLong
|
||||
_BlitterWriteLong
|
||||
_DAC*
|
||||
_Joystick*
|
||||
_joypad0Buttons
|
||||
|
||||
+12
@@ -4678,6 +4678,13 @@ bool retro_serialize(void *data, size_t size)
|
||||
return false;
|
||||
buf += CDROMStateExtSave(buf);
|
||||
|
||||
/* v16, extended in place (#800): the sticky "blitter hung" flag -- a
|
||||
* blit hardware never finishes leaves the blitter busy until reset.
|
||||
* Its own magic ("BLH1"), STRICTLY after CDX1; room checked first. */
|
||||
if ((size_t)(buf - start) + BlitterHungStateSize() > STATE_SIZE)
|
||||
return false;
|
||||
buf += BlitterHungStateSave(buf);
|
||||
|
||||
written = (size_t)(buf - start);
|
||||
if (written > STATE_SIZE)
|
||||
return false;
|
||||
@@ -4931,7 +4938,12 @@ bool retro_unserialize(const void *data, size_t size)
|
||||
{
|
||||
buf += JaguarCDBiosStateLoad(buf);
|
||||
buf += CDROMStateExtLoad(buf);
|
||||
/* #800: hung-blitter chunk. A v16 blob written before it existed
|
||||
* ends in the zero-filled tail here: wrong magic -> not hung. */
|
||||
buf += BlitterHungStateLoad(buf);
|
||||
}
|
||||
else
|
||||
BlitterHungStateReset(); /* older states: not hung */
|
||||
|
||||
/* tomRam8 was restored raw above; recompute the DRAM/refresh timing
|
||||
* that bus_arbiter derives from MEMCON1/MEMCON2 so it matches the
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
jaguar_cd_*;
|
||||
ResolveBootConfig;
|
||||
BlitterCompare*;
|
||||
BlitterIsHung;
|
||||
BlitterNeverEndsApprox;
|
||||
BlitterReadLong;
|
||||
BlitterWriteLong;
|
||||
DAC*;
|
||||
Joystick*;
|
||||
joypad0Buttons;
|
||||
|
||||
+11
-28
@@ -4,6 +4,7 @@
|
||||
#include "log.h"
|
||||
#include "../jerry/dsp.h" /* DSPIsRunning() returns bool -- match the canonical decl */
|
||||
#include "../tom/gpu.h" /* GPUIsRunning() */
|
||||
#include "../tom/blitter.h" /* BlitterNeverEnds() -- shared never-ending-blit predicate */
|
||||
#include "../cd/cdrom.h" /* CDROMDiagGetSeekWedgeState(), CDTraceDump() */
|
||||
#include "../tom/shadowfb.h" /* shadowHiresActive + resolve counters */
|
||||
#include "settings.h" /* bootConfig.isCDGame */
|
||||
@@ -439,33 +440,13 @@ int CrashDetectBlitIsAbsurd(uint32_t b_count)
|
||||
return outer > INFRAME_BLIT_PIXELS_MAX / inner;
|
||||
}
|
||||
|
||||
/* A blit whose first inner loop can never end (issue #800). The inner
|
||||
* counter's decrement comes from INNER.NET (jag_sim netlists/tom/INNER.NET,
|
||||
* Inc0-Inc3 at lines ~611-635): in pixel mode it is 1; in phrase mode it is
|
||||
* built from the destination X's low bits, but only the 8/16/32 bpp decodes
|
||||
* (pixel8/pixel16/pixel32) contribute beyond bit 0. At pixsize 0-2
|
||||
* (1/2/4 bpp) the decrement is just dstxp[0]. Phrase-mode X add is "add
|
||||
* phrase width and truncate to phrase boundary" (JTRM v8 p.71, A1_FLAGS
|
||||
* bits 16-17 = 00), so after the first write X is phrase-aligned, dstxp[0]
|
||||
* is 0 and the counter never moves again. The line ends only if the first
|
||||
* write already reaches the count: inner 0 (INNER.NET Inner0t: count == 0,
|
||||
* one write) or inner 1 with an odd starting X. Real hardware never
|
||||
* finishes such a blit; the synchronous engine runs it forever inside one
|
||||
* register write. dst_flags = A1_FLAGS, or A2_FLAGS when DSTA2 (B_CMD bit
|
||||
* 11) is set; dst_x = that pointer's X (low word of A1/A2_PIXEL). */
|
||||
/* A blit whose first inner loop can never end (issue #800). The
|
||||
* predicate is the blitter's own (BlitterNeverEnds, src/tom/blitter_mmio.c,
|
||||
* where the INNER.NET derivation lives), so the watchdog, the dispatch
|
||||
* path and the accurate engine cannot drift apart. */
|
||||
int CrashDetectBlitNeverEnds(uint32_t b_count, uint32_t dst_flags, uint32_t dst_x)
|
||||
{
|
||||
uint32_t inner = b_count & 0xFFFFu;
|
||||
uint32_t pixsize = (dst_flags >> 3) & 0x07u;
|
||||
int phrase_mode = ((dst_flags >> 16) & 0x03u) == 0;
|
||||
|
||||
if (!phrase_mode || pixsize >= 3)
|
||||
return 0;
|
||||
if (inner == 0)
|
||||
return 0;
|
||||
if (inner == 1 && (dst_x & 1u))
|
||||
return 0;
|
||||
return 1;
|
||||
return BlitterNeverEnds(b_count, dst_flags, dst_x);
|
||||
}
|
||||
|
||||
void CrashDetectNoteBlit(uint32_t b_count, uint32_t b_cmd, uint32_t a1_base,
|
||||
@@ -483,11 +464,13 @@ void CrashDetectNoteBlit(uint32_t b_count, uint32_t b_cmd, uint32_t a1_base,
|
||||
if (never_ends)
|
||||
{
|
||||
LOG_ERR("[CRASH-DETECT] inframe_hang frame=%u where=blitter b_count=$%08X "
|
||||
"never_ends=1 dst_flags=$%08X dst_x=%u b_cmd=$%08X a1_base=$%08X "
|
||||
"never_ends=1%s dst_flags=$%08X dst_x=%u b_cmd=$%08X a1_base=$%08X "
|
||||
"gpu_pc=$%08X gpu_run=%d dsp_pc=$%08X dsp_run=%d m68k_pc=$%06X "
|
||||
"(phrase-mode blit below 8bpp: the inner counter never "
|
||||
"decrements, so the blit never finishes)\n",
|
||||
frame_no + 1, b_count, dst_flags, (unsigned)(dst_x & 0xFFFFu),
|
||||
"decrements; the emulated blitter stays hung until reset)\n",
|
||||
frame_no + 1, b_count,
|
||||
BlitterNeverEndsApprox(b_cmd) ? " approx=1" : "",
|
||||
dst_flags, (unsigned)(dst_x & 0xFFFFu),
|
||||
b_cmd, a1_base, pc_canonical(gpu_pc), (int)GPUIsRunning(),
|
||||
pc_canonical(dsp_pc), (int)DSPIsRunning(),
|
||||
(unsigned)(m68k_get_reg(NULL, M68K_REG_PC) & PC_ALIAS_MASK));
|
||||
|
||||
+10
-1
@@ -89,7 +89,12 @@ extern "C" {
|
||||
* a magic word: "CDB1" (boot stub injected yet) and "CDX1" (FIFO
|
||||
* refill accumulator + CD IRQ edge detector). First bump of
|
||||
* the v3.7.1 cycle, so any other in-flight state change this cycle
|
||||
* extends v16 in place rather than bumping again. */
|
||||
* extends v16 in place rather than bumping again.
|
||||
* EXTENDED IN PLACE (still v16): #800 appended "BLH1" strictly after
|
||||
* CDX1: the sticky "blitter hung" flag + the stuck inner count (a
|
||||
* blit hardware never finishes leaves the blitter busy until reset).
|
||||
* A v16 blob written before it ends in the zero-filled tail there, so
|
||||
* the magic misses and it loads as not hung; v15 and older too. */
|
||||
#define STATE_MAGIC 0x564A5353 /* "VJSS" */
|
||||
#define STATE_VERSION 16
|
||||
/* Oldest layout retro_unserialize still accepts. States between
|
||||
@@ -212,6 +217,10 @@ extern "C" {
|
||||
* GPU-magic stomp over the game's own RAM. Older states do not carry it:
|
||||
* the live session's value is left alone (v3.7.0 behaviour). */
|
||||
#define STATE_VERSION_BIOS_CD_BOOT 16
|
||||
/* v16, extended in place (#800): trailing "BLH1" hung-blitter chunk
|
||||
* (BlitterHungStateSave, src/tom/blitter_mmio.c), read only for
|
||||
* version >= STATE_VERSION_BIOS_CD_BOOT, strictly after CDX1. Missing or
|
||||
* wrong magic, or an older version: not hung. */
|
||||
|
||||
/* Header flags */
|
||||
#define STATE_FLAG_MEMTRACK 0x01
|
||||
|
||||
+56
-4
@@ -2339,6 +2339,9 @@ void BlitterMidsummer2(void)
|
||||
|
||||
bool phrase_mode;
|
||||
uint16_t a1FracCInX = 0, a1FracCInY = 0;
|
||||
/* Never-ending blit (#800/#794): see the block after phrase_mode. */
|
||||
bool never_ends;
|
||||
uint32_t stuck_writes = 0, stuck_limit = 0;
|
||||
|
||||
// Bugs in Jaguar I
|
||||
|
||||
@@ -2357,6 +2360,44 @@ void BlitterMidsummer2(void)
|
||||
|
||||
phrase_mode = ((!dsta2 && a1addx == 0) || (dsta2 && a2addx == 0) ? true : false); // From ACONTROL
|
||||
|
||||
/* A blit hardware never finishes (#800, #794): phrase mode below 8bpp,
|
||||
* where INNER.NET's inner-counter decrement is dstxp[0] and phrase-
|
||||
* aligned X keeps it at zero (BlitterNeverEnds, blitter_mmio.c, has the
|
||||
* derivation; the same predicate gates the watchdog and the dispatch
|
||||
* path's sticky "hung" flag). The real blitter keeps stepping forever:
|
||||
* every step writes one destination phrase and moves X by a phrase (64,
|
||||
* 32 or 16 pixels at 1/2/4bpp) and, with the Y add bit, Y by one. The
|
||||
* pointers are 16 bits and wrap, so the destination sweep repeats with
|
||||
* a period of 65536/ppp steps (65536 with Y add).
|
||||
*
|
||||
* POLICY: hang the emulated blitter, never the host. Run the stuck
|
||||
* step for exactly one full wrap period (+1 when the start X is not
|
||||
* phrase-aligned, so the partial first phrase is followed by every
|
||||
* aligned one), then stop; dispatch marks the blitter hung, B_CMD reads
|
||||
* busy and further starts are ignored until reset. That memory result
|
||||
* is EXACT whenever repeating the step is idempotent on memory -- the
|
||||
* shapes every observed homebrew hang has (DEMO1B $2208, Chroma $2704,
|
||||
* Native Demo $2718: LFU clear, no accumulators) -- because after one
|
||||
* period every location the hardware will ever write holds its final
|
||||
* value. Otherwise (BlitterNeverEndsApprox: Gouraud/Z/shade
|
||||
* accumulators, comparator masks, LFU feeding back D or a separately
|
||||
* stepping S -- Music Demo, JagMania) hardware memory has no final
|
||||
* state and this is a defined one-pass approximation. Bounded at
|
||||
* 65536 phrase writes. The pointers end one period on, i.e. where they
|
||||
* started (outer updates never run: the outer loop never sees indone).
|
||||
* The collapsed fast paths below are bypassed so the reference state
|
||||
* machine handles it. */
|
||||
never_ends = BlitterNeverEnds(GET32(blitter_ram, PIXLINECOUNTER),
|
||||
dsta2 ? GET32(blitter_ram, A2_FLAGS) : GET32(blitter_ram, A1_FLAGS),
|
||||
(uint16_t)(dsta2 ? a2_x : a1_x)) ? true : false;
|
||||
if (never_ends)
|
||||
{
|
||||
uint32_t ppp_stuck = 64u >> pixsize; /* pixsize 0-2 here */
|
||||
stuck_limit = a1addy ? 0x10000u : (0x10000u / ppp_stuck);
|
||||
if (((uint16_t)(dsta2 ? a2_x : a1_x)) & (ppp_stuck - 1u))
|
||||
stuck_limit++;
|
||||
}
|
||||
|
||||
// Stopgap vars to simulate various lines
|
||||
|
||||
|
||||
@@ -2486,6 +2527,7 @@ void BlitterMidsummer2(void)
|
||||
bool idle_inner = true, sreadx = false, szreadx = false, sread = false,
|
||||
szread = false, dread = false, dzread = false, dwrite = false, dzwrite = false;
|
||||
bool inner0 = false;
|
||||
bool stuck_done = false; /* never_ends: one wrap period written */
|
||||
bool idle_inneri, sreadxi, szreadxi, sreadi, szreadi, dreadi, dzreadi, dwritei, dzwritei;
|
||||
//other stuff
|
||||
uint8_t srcshift = 0;
|
||||
@@ -2574,7 +2616,7 @@ void BlitterMidsummer2(void)
|
||||
*
|
||||
* The existing state machine is the fallback for all other configs.
|
||||
*=================================================================*/
|
||||
if (patdsel && !srcen && !srcenx && !dsten && !dstenz && !dstwrz
|
||||
if (!never_ends && patdsel && !srcen && !srcenx && !dsten && !dstenz && !dstwrz
|
||||
&& !gourd && !gourz && !srcshade && !adddsel
|
||||
&& !bcompen && !dcompen && !a2update && zmode == 0)
|
||||
{
|
||||
@@ -2838,7 +2880,7 @@ void BlitterMidsummer2(void)
|
||||
* 7. Step both A1 and A2 addresses
|
||||
* 8. Decrement icount, check inner0
|
||||
*=================================================================*/
|
||||
if (srcen && !srcenx && !srcenz && !dsten && !dstenz && !dstwrz
|
||||
if (!never_ends && srcen && !srcenx && !srcenz && !dsten && !dstenz && !dstwrz
|
||||
&& !bcompen && !dcompen && !gourd && !gourz && !srcshade
|
||||
&& !adddsel && !patdsel && zmode == 0
|
||||
&& a1addx != 3 && a2addx != 3
|
||||
@@ -3163,8 +3205,8 @@ void BlitterMidsummer2(void)
|
||||
/* State machine: step is always true (no bus contention in
|
||||
Jaguar I), textext/txtread never assert. Both eliminated. */
|
||||
|
||||
if ((dzwrite && inner0)
|
||||
|| (dwrite && !dstwrz && inner0))
|
||||
if ((dzwrite && (inner0 || stuck_done))
|
||||
|| (dwrite && !dstwrz && (inner0 || stuck_done)))
|
||||
{
|
||||
idle_inneri = true;
|
||||
break;
|
||||
@@ -3510,6 +3552,11 @@ A2ptrldi := NAN2 (a2ptrldi, a2update\, a2pldt);*/
|
||||
|
||||
if (icount == 0 || ((icount & 0x8000) && !(oldicount & 0x8000)))
|
||||
inner0 = true;
|
||||
/* Never-ending blit: stop after one wrap period. Set only
|
||||
* here, AFTER inner0, so it changes no write mask; the
|
||||
* idle_inner test above ends the step after its Z write. */
|
||||
if (never_ends && ++stuck_writes >= stuck_limit)
|
||||
stuck_done = true;
|
||||
// X/Y stepping is also done here, I think...No. It's done when a1_add or a2_add is asserted...
|
||||
|
||||
//*********************************************************************************
|
||||
@@ -4012,6 +4059,11 @@ fc_inner_done:
|
||||
|
||||
if (ocount == 0)
|
||||
outer0 = true;
|
||||
/* Never-ending blit stopped by the wrap-period bound: the outer
|
||||
* machine goes straight to idle (no A1/A2 outer updates -- the
|
||||
* hardware never gets that far). */
|
||||
if (stuck_done)
|
||||
outer0 = true;
|
||||
}
|
||||
|
||||
if (a1fupdate)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef __BLITTER_H__
|
||||
#define __BLITTER_H__
|
||||
|
||||
#include <stddef.h>
|
||||
#include "vjag_memory.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -22,6 +23,29 @@ void BlitterReset(void);
|
||||
void BlitterResetDecodeState(void);
|
||||
void BlitterDone(void);
|
||||
|
||||
/* Blits that never end on hardware (issues #800, #794).
|
||||
*
|
||||
* BlitterNeverEnds: whether a blit's first inner loop can never finish.
|
||||
* Exact netlist condition, shared by the accurate engine, the dispatch
|
||||
* path and the crash watchdog so they cannot drift apart. dst_flags =
|
||||
* A1_FLAGS, or A2_FLAGS when B_CMD.DSTA2; dst_x = that pointer's X.
|
||||
*
|
||||
* BlitterNeverEndsApprox: for such a blit, whether the one-wrap-period
|
||||
* memory effect the accurate engine produces is only an approximation
|
||||
* (the repeated step is not provably idempotent on memory).
|
||||
*
|
||||
* BlitterIsHung: the sticky "blitter hung" flag. Set at dispatch when
|
||||
* a never-ending blit was started; B_CMD then reads busy and further
|
||||
* starts are ignored, until reset / power-on. Serialised as the
|
||||
* trailing "BLH1" savestate chunk (v16, after CDX1). */
|
||||
int BlitterNeverEnds(uint32_t b_count, uint32_t dst_flags, uint32_t dst_x);
|
||||
int BlitterNeverEndsApprox(uint32_t b_cmd);
|
||||
int BlitterIsHung(void);
|
||||
size_t BlitterHungStateSize(void);
|
||||
size_t BlitterHungStateSave(uint8_t *buf);
|
||||
size_t BlitterHungStateLoad(const uint8_t *buf);
|
||||
void BlitterHungStateReset(void);
|
||||
|
||||
uint8_t BlitterReadByte(uint32_t, uint32_t who);
|
||||
uint16_t BlitterReadWord(uint32_t, uint32_t who);
|
||||
uint32_t BlitterReadLong(uint32_t, uint32_t who);
|
||||
|
||||
+178
-9
@@ -11,6 +11,7 @@
|
||||
#include "../core/crash_detect.h"
|
||||
#include "settings.h"
|
||||
#include "vjag_memory.h"
|
||||
#include "../core/state.h"
|
||||
|
||||
#define A1_BASE ((uint32_t)0x00)
|
||||
#define A1_FLAGS ((uint32_t)0x04)
|
||||
@@ -183,6 +184,138 @@ static void BlitterTimingChargeAccess(uint32_t who)
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Blits that never end on hardware (issues #800, #794) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* B_CMD bits used by the classification below (JTRM v8 p.73-75). */
|
||||
#define CMD_SRCEN ((uint32_t)0x00000001)
|
||||
#define CMD_SRCENX ((uint32_t)0x00000004)
|
||||
#define CMD_DSTEN ((uint32_t)0x00000008)
|
||||
#define CMD_DSTWRZ ((uint32_t)0x00000020)
|
||||
#define CMD_GOURD ((uint32_t)0x00001000)
|
||||
#define CMD_GOURZ ((uint32_t)0x00002000)
|
||||
#define CMD_ADDDSEL ((uint32_t)0x00020000)
|
||||
#define CMD_ZMODE ((uint32_t)0x001C0000)
|
||||
#define CMD_BCOMPEN ((uint32_t)0x04000000)
|
||||
#define CMD_DCOMPEN ((uint32_t)0x08000000)
|
||||
#define CMD_SRCSHADE ((uint32_t)0x40000000)
|
||||
|
||||
/* B_CMD status while hung (JTRM v8 p.75, F02238 read-only): bit 0 IDLE
|
||||
* clear, bit 12 "outer INNER" set (the outer state machine never leaves
|
||||
* its inner-loop-active state), bit 9 "inner DWRITE" (every stuck step
|
||||
* writes), bits 16-31 the inner count the counter is stuck at. Bits
|
||||
* 2-31 are documented as diagnostic only. */
|
||||
#define STATUS_HUNG_BITS ((uint32_t)0x00001200)
|
||||
|
||||
#define BLITTER_HUNG_MAGIC 0x31484C42u /* "BLH1" little-endian */
|
||||
|
||||
static uint8_t blitterHung;
|
||||
static uint16_t blitterHungICount;
|
||||
|
||||
/* The inner counter's decrement is INNER.NET's Inc0-Inc3 (jag_sim
|
||||
* netlists/tom/INNER.NET ~611-635): 1 in pixel mode; in phrase mode only
|
||||
* the 8/16/32bpp decodes contribute beyond bit 0, so at pixsize 0-2 it is
|
||||
* dstxp[0]. Phrase-mode X add truncates to a phrase boundary (JTRM v8
|
||||
* p.71, A1/A2_FLAGS bits 16-17 = 00), so after the first write dstxp[0] is
|
||||
* 0 and the count never moves again; inner0 (count == 0 or underflow,
|
||||
* INNER.NET ~662-673) is never reached. The line ends only if the first
|
||||
* write already reaches the count: inner 0 (one write) or inner 1 with an
|
||||
* odd starting X. Nothing else ends an inner loop, so the blit never
|
||||
* returns to idle on hardware. */
|
||||
int BlitterNeverEnds(uint32_t b_count, uint32_t dst_flags, uint32_t dst_x)
|
||||
{
|
||||
uint32_t inner = b_count & 0xFFFFu;
|
||||
uint32_t pixsize = (dst_flags >> 3) & 0x07u;
|
||||
int phrase_mode = ((dst_flags >> 16) & 0x03u) == 0;
|
||||
|
||||
if (!phrase_mode || pixsize >= 3)
|
||||
return 0;
|
||||
if (inner == 0)
|
||||
return 0;
|
||||
if (inner == 1 && (dst_x & 1u))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Whether repeating the stuck step forever is NOT provably idempotent on
|
||||
* memory, i.e. the accurate engine's one-wrap-period result is only a
|
||||
* defined approximation of the (never final) hardware memory state.
|
||||
* Conservative: any per-step data that can change from one wrap to the
|
||||
* next -- Gouraud/Z accumulators that are written or compared, intensity
|
||||
* shading, data/bit/Z comparator masks, adder mode, destination data fed
|
||||
* back through the LFU (read-modify-write), or source data fed into the
|
||||
* LFU (the source pointer has its own period and may overlap the
|
||||
* destination). */
|
||||
int BlitterNeverEndsApprox(uint32_t b_cmd)
|
||||
{
|
||||
uint32_t lfu = (b_cmd >> 21) & 0x0Fu; /* minterms !S!D, !SD, S!D, SD */
|
||||
int lfu_reads_d = ((lfu & 1u) != ((lfu >> 1) & 1u))
|
||||
|| (((lfu >> 2) & 1u) != ((lfu >> 3) & 1u));
|
||||
int lfu_reads_s = ((lfu & 1u) != ((lfu >> 2) & 1u))
|
||||
|| (((lfu >> 1) & 1u) != ((lfu >> 3) & 1u));
|
||||
|
||||
if (b_cmd & (CMD_GOURD | CMD_SRCSHADE | CMD_ADDDSEL
|
||||
| CMD_BCOMPEN | CMD_DCOMPEN | CMD_ZMODE))
|
||||
return 1;
|
||||
if ((b_cmd & CMD_GOURZ) && (b_cmd & CMD_DSTWRZ))
|
||||
return 1;
|
||||
if ((b_cmd & CMD_DSTEN) && lfu_reads_d)
|
||||
return 1;
|
||||
if ((b_cmd & (CMD_SRCEN | CMD_SRCENX)) && lfu_reads_s)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int BlitterIsHung(void)
|
||||
{
|
||||
return blitterHung != 0;
|
||||
}
|
||||
|
||||
void BlitterHungStateReset(void)
|
||||
{
|
||||
blitterHung = 0;
|
||||
blitterHungICount = 0;
|
||||
}
|
||||
|
||||
size_t BlitterHungStateSize(void)
|
||||
{
|
||||
return 4u + 1u + 2u; /* magic, hung, stuck inner count */
|
||||
}
|
||||
|
||||
size_t BlitterHungStateSave(uint8_t *buf)
|
||||
{
|
||||
uint8_t *start = buf;
|
||||
uint32_t magic = BLITTER_HUNG_MAGIC;
|
||||
|
||||
STATE_SAVE_VAR(buf, magic);
|
||||
STATE_SAVE_VAR(buf, blitterHung);
|
||||
STATE_SAVE_VAR(buf, blitterHungICount);
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
|
||||
/* A missing or wrong magic (a state written before this chunk existed:
|
||||
* the blob's zero-filled tail) means not hung. */
|
||||
size_t BlitterHungStateLoad(const uint8_t *buf)
|
||||
{
|
||||
const uint8_t *start = buf;
|
||||
uint32_t magic;
|
||||
|
||||
STATE_LOAD_VAR(buf, magic);
|
||||
if (magic != BLITTER_HUNG_MAGIC)
|
||||
{
|
||||
BlitterHungStateReset();
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
STATE_LOAD_VAR(buf, blitterHung);
|
||||
STATE_LOAD_VAR(buf, blitterHungICount);
|
||||
blitterHung = blitterHung ? 1 : 0;
|
||||
if (!blitterHung)
|
||||
blitterHungICount = 0;
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
|
||||
|
||||
void BlitterInit(void)
|
||||
{
|
||||
BlitterReset();
|
||||
@@ -192,6 +325,9 @@ void BlitterInit(void)
|
||||
void BlitterReset(void)
|
||||
{
|
||||
memset(blitter_ram, 0x00, 0xA0);
|
||||
/* A hung blitter (one that started a blit hardware never finishes)
|
||||
* stays hung until reset / power-on -- this is that reset. */
|
||||
BlitterHungStateReset();
|
||||
/* The register file is not the whole of the blitter's serialised
|
||||
* state: the B_CMD decode statics live in blitter.c and used to
|
||||
* survive teardown into the next session's savestate (#479). */
|
||||
@@ -213,6 +349,13 @@ uint8_t BlitterReadByte(uint32_t offset, uint32_t who/*=UNKNOWN*/)
|
||||
|
||||
BlitterTimingChargeAccess(who);
|
||||
|
||||
/* Hung (a never-ending blit is still running, #800): not idle. */
|
||||
if (blitterHung && offset >= COMMAND && offset <= (COMMAND + 3))
|
||||
{
|
||||
uint32_t status = ((uint32_t)blitterHungICount << 16) | STATUS_HUNG_BITS;
|
||||
return (uint8_t)(status >> (8 * (3 - (offset - COMMAND))));
|
||||
}
|
||||
|
||||
/* Status read-back: $00000805 = IDLE (bit 0) | inner IDLE (bit 2) |
|
||||
* outer IDLE (bit 11). The value itself is not printed in the JTRM;
|
||||
* it is derived from the documented status bits (JTRM Rev 8 p.75
|
||||
@@ -332,6 +475,13 @@ void BlitterWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
|
||||
* shadow registers as they run. */
|
||||
uint32_t busClks = 0;
|
||||
int trBlit = 0;
|
||||
uint32_t hbCount = GET32(blitter_ram, PIXLINECOUNTER);
|
||||
uint32_t hbCmd = GET32(blitter_ram, COMMAND);
|
||||
uint32_t hbFlags = (hbCmd & CMD_DSTA2) ? GET32(blitter_ram, A2_FLAGS)
|
||||
: GET32(blitter_ram, A1_FLAGS);
|
||||
uint32_t hbX = (hbCmd & CMD_DSTA2) ? GET16(blitter_ram, A2_PIXEL + 2)
|
||||
: GET16(blitter_ram, A1_PIXEL + 2);
|
||||
int neverEnds;
|
||||
|
||||
/* Re-entrancy guard (issue #659).
|
||||
*
|
||||
@@ -387,6 +537,20 @@ void BlitterWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
|
||||
return;
|
||||
}
|
||||
|
||||
/* A hung blitter (#800) is still running the blit it can never
|
||||
* finish. A B_CMD write cannot start another one: OUTER.NET only
|
||||
* leaves idle on go (innert[0] = idle AND go), and the outer state
|
||||
* machine never returns to idle. The register write itself is kept
|
||||
* (above). KNOWN LIMIT: on hardware the counter/command/pointer
|
||||
* registers still latch mid-blit (FDSYNC on cmdld/countld), so e.g.
|
||||
* a B_COUNT write with inner count 0 would end the stuck line and let
|
||||
* the blit continue; JTRM v8 p.70 calls register writes while busy
|
||||
* undefined, and this model does not resume a hung blit. */
|
||||
if (blitterHung)
|
||||
return;
|
||||
|
||||
neverEnds = BlitterNeverEnds(hbCount, hbFlags, hbX);
|
||||
|
||||
if (vjs.blitterTiming)
|
||||
busClks = BlitDurationSysclks();
|
||||
|
||||
@@ -408,15 +572,8 @@ void BlitterWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
|
||||
/* Log-only in-frame hang signature (issue #740): a garbage B_COUNT
|
||||
* runs to completion inside this register write and the frame never
|
||||
* ends, so the per-frame watchdog would never see it. */
|
||||
CrashDetectNoteBlit(GET32(blitter_ram, PIXLINECOUNTER),
|
||||
GET32(blitter_ram, COMMAND),
|
||||
GET32(blitter_ram, A1_BASE),
|
||||
(GET32(blitter_ram, COMMAND) & CMD_DSTA2)
|
||||
? GET32(blitter_ram, A2_FLAGS)
|
||||
: GET32(blitter_ram, A1_FLAGS),
|
||||
(GET32(blitter_ram, COMMAND) & CMD_DSTA2)
|
||||
? GET16(blitter_ram, A2_PIXEL + 2)
|
||||
: GET16(blitter_ram, A1_PIXEL + 2));
|
||||
CrashDetectNoteBlit(hbCount, hbCmd, GET32(blitter_ram, A1_BASE),
|
||||
hbFlags, hbX);
|
||||
|
||||
blit_in_progress = 1;
|
||||
if (BlitterCompareIsEnabled())
|
||||
@@ -431,6 +588,18 @@ void BlitterWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
|
||||
}
|
||||
blit_in_progress = 0;
|
||||
|
||||
/* A blit hardware never finishes (#800/#794). The accurate engine
|
||||
* has run it for one destination wrap period and returned (see
|
||||
* BlitterMidsummer2); the fast engine ran its own approximation.
|
||||
* Either way the emulated blitter is now hung -- not the host --
|
||||
* until reset. Engine-independent, so B_CMD reads busy under both.
|
||||
* The counter sits at inner - dstxp[0] after the first write. */
|
||||
if (neverEnds)
|
||||
{
|
||||
blitterHung = 1;
|
||||
blitterHungICount = (uint16_t)((hbCount & 0xFFFFu) - (hbX & 1u));
|
||||
}
|
||||
|
||||
if (trBlit)
|
||||
TexReplacePostBlit();
|
||||
|
||||
|
||||
@@ -25,6 +25,31 @@
|
||||
|
||||
typedef int (*absurd_fn)(uint32_t);
|
||||
typedef int (*never_fn)(uint32_t, uint32_t, uint32_t);
|
||||
typedef int (*approx_fn)(uint32_t);
|
||||
|
||||
/* Third table: BlitterNeverEndsApprox() -- whether the accurate engine's
|
||||
* one-wrap-period result for a never-ending blit is exact (repeating the
|
||||
* step is idempotent on memory) or a defined one-pass approximation. */
|
||||
struct approx_row {
|
||||
const char *name;
|
||||
uint32_t b_cmd;
|
||||
int want;
|
||||
};
|
||||
|
||||
static const struct approx_row approx_rows[] = {
|
||||
/* Measured hangs, LFU clear and no accumulators: exact. */
|
||||
{ "DEMO1B $00002208 (DSTEN, GOURZ w/o DSTWRZ)", 0x00002208u, 0 },
|
||||
{ "Chroma-Luma $00002704 (SRCENX, LFU clear)", 0x00002704u, 0 },
|
||||
{ "Native Demo $00002718 (DSTEN/DSTENZ, LFU 0)", 0x00002718u, 0 },
|
||||
{ "pattern fill, LFU S only, no source read", 0x01810000u, 0 },
|
||||
/* Not provably idempotent: approximation. */
|
||||
{ "Music Demo $2710826D (BCOMPEN, ZMODE, S&D)", 0x2710826Du, 1 },
|
||||
{ "JagMania $FFFFFFFB (everything)", 0xFFFFFFFBu, 1 },
|
||||
{ "DSTEN + LFU NOT D (toggles every pass)", 0x00A00008u, 1 },
|
||||
{ "SRCEN + LFU S (source steps separately)", 0x01800001u, 1 },
|
||||
{ "GOURD", 0x00001000u, 1 },
|
||||
{ "GOURZ + DSTWRZ", 0x00002020u, 1 },
|
||||
};
|
||||
|
||||
/* A1_FLAGS / A2_FLAGS: pixel size in bits 3-5, X add control in bits
|
||||
* 16-17 (00 = phrase mode, 01 = add pixel size). */
|
||||
@@ -96,6 +121,7 @@ int main(int argc, char **argv)
|
||||
void *handle;
|
||||
absurd_fn fn;
|
||||
never_fn never;
|
||||
approx_fn approx;
|
||||
unsigned i, fails = 0;
|
||||
|
||||
handle = dlopen(core, RTLD_LAZY);
|
||||
@@ -142,6 +168,24 @@ int main(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
approx = (approx_fn)dlsym(handle, "BlitterNeverEndsApprox");
|
||||
if (!approx)
|
||||
{
|
||||
fprintf(stderr, "FAIL: BlitterNeverEndsApprox not exported (build with TEST_EXPORTS=1)\n");
|
||||
return 1;
|
||||
}
|
||||
for (i = 0; i < sizeof(approx_rows) / sizeof(approx_rows[0]); i++)
|
||||
{
|
||||
const struct approx_row *r = &approx_rows[i];
|
||||
int got = approx(r->b_cmd);
|
||||
if (got != r->want)
|
||||
{
|
||||
fprintf(stderr, "FAIL: %s (B_CMD=$%08X): approx got %d want %d\n",
|
||||
r->name, (unsigned)r->b_cmd, got, r->want);
|
||||
fails++;
|
||||
}
|
||||
}
|
||||
|
||||
if (fails)
|
||||
{
|
||||
fprintf(stderr, "test_crash_detect_inframe: %u failure(s)\n", fails);
|
||||
@@ -149,6 +193,7 @@ int main(int argc, char **argv)
|
||||
}
|
||||
printf("test_crash_detect_inframe: %u cases PASS\n",
|
||||
(unsigned)(sizeof(rows) / sizeof(rows[0])
|
||||
+ sizeof(never_rows) / sizeof(never_rows[0])));
|
||||
+ sizeof(never_rows) / sizeof(never_rows[0])
|
||||
+ sizeof(approx_rows) / sizeof(approx_rows[0])));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* test/tools/test_blitter_hung.c -- a blit hardware never finishes hangs the
|
||||
* emulated blitter, never the host (issues #800, #794).
|
||||
*
|
||||
* Phrase mode below 8bpp: INNER.NET's inner-counter decrement is dstxp[0],
|
||||
* and phrase-aligned X keeps it at zero, so the blit never returns to idle
|
||||
* on hardware. Policy: the accurate engine runs the stuck step for one
|
||||
* destination wrap period (exact when the step is idempotent on memory),
|
||||
* then the blitter stays "hung" until reset: B_CMD reads busy (IDLE bit 0
|
||||
* clear) and further starts are ignored. The flag is saved in the "BLH1"
|
||||
* savestate chunk.
|
||||
*
|
||||
* ROM-free: boots a 48-byte synthetic raw binary that parks its 68K, then
|
||||
* programs the blitter directly through the exported register path:
|
||||
* A1_BASE = $100000, A1_FLAGS = 0 (phrase mode, 1bpp, contiguous),
|
||||
* A1_PIXEL = 0, B_COUNT = $000100DC (1 line, inner 220), B_CMD = 0
|
||||
* (LFU clear: every write is zeros -- the idempotent class).
|
||||
* Asserts (accurate engine):
|
||||
* - the blit returns (a wall-clock alarm turns a host hang into a FAIL);
|
||||
* - memory: exactly one wrap period, $100000..$101FFF (65536 px at 1bpp
|
||||
* = 1024 phrases), is zero and the next phrase is untouched;
|
||||
* - B_CMD status: IDLE clear, stuck inner count $DC in bits 16-31;
|
||||
* - a second B_CMD start while hung writes nothing;
|
||||
* - serialize -> retro_reset clears the flag (status idle) ->
|
||||
* unserialize restores it.
|
||||
* With --fast, the same hung reporting under the fast engine (its memory
|
||||
* effect is not asserted).
|
||||
*
|
||||
* ROM mode (--rom PATH [--bios]): runs PATH for 600 frames under the
|
||||
* accurate blitter and asserts every frame completes AND the blitter ended
|
||||
* hung -- i.e. the title really issued a never-ending blit and the host
|
||||
* survived it. Reproducers: Music Demo (ScatoLOGIC) --bios (#794, frame
|
||||
* 158) and Native Demo (bin) HLE (frame 170); both used to hang retro_run
|
||||
* forever.
|
||||
*
|
||||
* Build: cc -O2 -Wall -std=c99 -I. -I./src -I./libretro-common/include \
|
||||
* -o test/tools/test_blitter_hung test/tools/test_blitter_hung.c \
|
||||
* test/harness/harness.c -ldl -lm
|
||||
* Run: test_blitter_hung <core> [--fast]
|
||||
* test_blitter_hung <core> --rom PATH [--bios]
|
||||
*/
|
||||
|
||||
#define _DEFAULT_SOURCE 1
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include "../harness/harness.h"
|
||||
|
||||
#define WALL_CLOCK_SECS 120u
|
||||
|
||||
#define BLIT_REGS 0x00F02200u
|
||||
#define R_A1_BASE 0x00u
|
||||
#define R_A1_FLAGS 0x04u
|
||||
#define R_A1_PIXEL 0x0Cu
|
||||
#define R_B_CMD 0x38u
|
||||
#define R_B_COUNT 0x3Cu
|
||||
|
||||
#define DST_BASE 0x00100000u
|
||||
#define WRAP_BYTES 0x2000u /* 65536 px at 1bpp */
|
||||
#define SECOND_BASE 0x00180000u
|
||||
|
||||
typedef void (*blit_wl_fn)(uint32_t, uint32_t, uint32_t);
|
||||
typedef uint32_t (*blit_rl_fn)(uint32_t, uint32_t);
|
||||
typedef int (*is_hung_fn)(void);
|
||||
typedef void (*reset_fn)(void);
|
||||
typedef size_t (*ser_size_fn)(void);
|
||||
typedef int (*ser_fn)(void *, size_t); /* bool in the core */
|
||||
typedef int (*unser_fn)(const void *, size_t);
|
||||
|
||||
/* Same synthetic image as test_upload_illegal_park: BJL startup idiom,
|
||||
* SP = $200000, two JSRs, ILLEGAL -> 68K parks at $1000. */
|
||||
static const unsigned char synth_image[] = {
|
||||
0x23, 0xFC, 0x00, 0x07, 0x00, 0x07, 0x00, 0xF0, 0x21, 0x0C,
|
||||
0x2E, 0x7C, 0x00, 0x20, 0x00, 0x00,
|
||||
0x4E, 0xB9, 0x00, 0x80, 0x20, 0x20,
|
||||
0x4E, 0xB9, 0x00, 0x80, 0x20, 0x20,
|
||||
0x4A, 0xFC,
|
||||
0x4E, 0x71,
|
||||
0x4E, 0x75,
|
||||
0x4E, 0x71, 0x4E, 0x71, 0x4E, 0x71, 0x4E, 0x71, 0x4E, 0x71,
|
||||
0x4E, 0x71, 0x4E, 0x71
|
||||
};
|
||||
|
||||
static int fails = 0;
|
||||
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (!(cond)) { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); fails++; } \
|
||||
} while (0)
|
||||
|
||||
static int run_rom(const char *core, const char *rom, int bios)
|
||||
{
|
||||
static char a_prog[] = "test_blitter_hung";
|
||||
static char a_frames_flag[] = "--frames";
|
||||
static char a_frames[] = "600";
|
||||
static char a_option_flag[] = "--option";
|
||||
static char a_acc[] = "virtualjaguar_usefastblitter=disabled";
|
||||
static char a_bios[] = "--bios";
|
||||
static char a_quiet[] = "--quiet";
|
||||
char *hargv[12];
|
||||
int hargc = 0;
|
||||
harness_config cfg = HARNESS_CONFIG_DEFAULT;
|
||||
is_hung_fn is_hung;
|
||||
unsigned frames;
|
||||
int hung;
|
||||
|
||||
hargv[hargc++] = a_prog;
|
||||
hargv[hargc++] = (char *)core;
|
||||
hargv[hargc++] = (char *)rom;
|
||||
hargv[hargc++] = a_frames_flag;
|
||||
hargv[hargc++] = a_frames;
|
||||
hargv[hargc++] = a_option_flag;
|
||||
hargv[hargc++] = a_acc;
|
||||
if (bios)
|
||||
hargv[hargc++] = a_bios;
|
||||
hargv[hargc++] = a_quiet;
|
||||
hargv[hargc] = NULL;
|
||||
|
||||
if (!harness_init_from_args(&cfg, hargc, hargv) || !harness_load_rom(&cfg))
|
||||
{
|
||||
fprintf(stderr, "FAIL: could not load %s\n", rom);
|
||||
return 1;
|
||||
}
|
||||
is_hung = (is_hung_fn)harness_dlsym(&cfg, "BlitterIsHung");
|
||||
if (!is_hung)
|
||||
{
|
||||
fprintf(stderr, "FAIL: BlitterIsHung not exported (TEST_EXPORTS=1)\n");
|
||||
harness_shutdown(&cfg);
|
||||
return 1;
|
||||
}
|
||||
harness_run(&cfg);
|
||||
frames = cfg.current_frame;
|
||||
hung = is_hung();
|
||||
harness_shutdown(&cfg);
|
||||
|
||||
CHECK(frames >= 600, "%s: only %u of 600 frames", rom, frames);
|
||||
CHECK(hung, "%s: no never-ending blit was issued (blitter not hung) -- "
|
||||
"the reproducer no longer exercises the path", rom);
|
||||
if (fails)
|
||||
return 1;
|
||||
printf("PASS test_blitter_hung (%s%s): 600 frames, blitter hung, host not\n",
|
||||
rom, bios ? ", BIOS" : "");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
static char a_prog[] = "test_blitter_hung";
|
||||
static char a_frames_flag[] = "--frames";
|
||||
static char a_frames[] = "10";
|
||||
static char a_option_flag[] = "--option";
|
||||
static char a_acc[] = "virtualjaguar_usefastblitter=disabled";
|
||||
static char a_fast[] = "virtualjaguar_usefastblitter=enabled";
|
||||
static char a_quiet[] = "--quiet";
|
||||
char path[] = "/tmp/vj_blitter_hung_XXXXXX.jag";
|
||||
char *hargv[10];
|
||||
int hargc = 0, fd, fast = 0;
|
||||
harness_config cfg = HARNESS_CONFIG_DEFAULT;
|
||||
blit_wl_fn wl;
|
||||
blit_rl_fn rl;
|
||||
is_hung_fn is_hung;
|
||||
reset_fn do_reset;
|
||||
ser_size_fn ser_size;
|
||||
ser_fn ser;
|
||||
unser_fn unser;
|
||||
uint8_t **ram_pp;
|
||||
uint8_t *ram;
|
||||
uint32_t status, i, nonzero = 0;
|
||||
size_t sz;
|
||||
void *blob;
|
||||
|
||||
if (argc < 2)
|
||||
{
|
||||
fprintf(stderr, "usage: %s <core> [--fast]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (argc > 2 && !strcmp(argv[2], "--fast"))
|
||||
fast = 1;
|
||||
|
||||
alarm(WALL_CLOCK_SECS);
|
||||
|
||||
if (argc > 3 && !strcmp(argv[2], "--rom"))
|
||||
return run_rom(argv[1], argv[3], argc > 4 && !strcmp(argv[4], "--bios"));
|
||||
|
||||
fd = mkstemps(path, 4);
|
||||
if (fd < 0 || write(fd, synth_image, sizeof(synth_image)) != (ssize_t)sizeof(synth_image))
|
||||
{
|
||||
perror("synthetic image");
|
||||
return 1;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
hargv[hargc++] = a_prog;
|
||||
hargv[hargc++] = argv[1];
|
||||
hargv[hargc++] = path;
|
||||
hargv[hargc++] = a_frames_flag;
|
||||
hargv[hargc++] = a_frames;
|
||||
hargv[hargc++] = a_option_flag;
|
||||
hargv[hargc++] = fast ? a_fast : a_acc;
|
||||
hargv[hargc++] = a_quiet;
|
||||
hargv[hargc] = NULL;
|
||||
|
||||
if (!harness_init_from_args(&cfg, hargc, hargv) || !harness_load_rom(&cfg))
|
||||
{
|
||||
fprintf(stderr, "FAIL: could not boot the synthetic image\n");
|
||||
unlink(path);
|
||||
return 1;
|
||||
}
|
||||
unlink(path);
|
||||
|
||||
wl = (blit_wl_fn)harness_dlsym(&cfg, "BlitterWriteLong");
|
||||
rl = (blit_rl_fn)harness_dlsym(&cfg, "BlitterReadLong");
|
||||
is_hung = (is_hung_fn)harness_dlsym(&cfg, "BlitterIsHung");
|
||||
do_reset = (reset_fn)harness_dlsym(&cfg, "retro_reset");
|
||||
ser_size = (ser_size_fn)harness_dlsym(&cfg, "retro_serialize_size");
|
||||
ser = (ser_fn)harness_dlsym(&cfg, "retro_serialize");
|
||||
unser = (unser_fn)harness_dlsym(&cfg, "retro_unserialize");
|
||||
ram_pp = (uint8_t **)harness_dlsym(&cfg, "jaguarMainRAM");
|
||||
if (!wl || !rl || !is_hung || !do_reset || !ser_size || !ser || !unser || !ram_pp)
|
||||
{
|
||||
fprintf(stderr, "FAIL: symbols missing (build the core with TEST_EXPORTS=1)\n");
|
||||
harness_shutdown(&cfg);
|
||||
return 1;
|
||||
}
|
||||
|
||||
harness_run(&cfg); /* 68K parks; nothing else touches the blitter */
|
||||
ram = *ram_pp;
|
||||
|
||||
CHECK(!is_hung(), "blitter hung before any blit");
|
||||
status = rl(BLIT_REGS + R_B_CMD, 0);
|
||||
CHECK(status & 1u, "status $%08X: IDLE clear before any blit", (unsigned)status);
|
||||
|
||||
memset(ram + DST_BASE, 0xFF, WRAP_BYTES + 16);
|
||||
memset(ram + SECOND_BASE, 0xFF, 64);
|
||||
|
||||
wl(BLIT_REGS + R_A1_BASE, DST_BASE, 0);
|
||||
wl(BLIT_REGS + R_A1_FLAGS, 0, 0);
|
||||
wl(BLIT_REGS + R_A1_PIXEL, 0, 0);
|
||||
wl(BLIT_REGS + R_B_COUNT, 0x000100DCu, 0);
|
||||
wl(BLIT_REGS + R_B_CMD, 0, 0); /* starts the blit; must return */
|
||||
|
||||
CHECK(is_hung(), "never-ending blit did not leave the blitter hung");
|
||||
status = rl(BLIT_REGS + R_B_CMD, 0);
|
||||
CHECK(!(status & 1u), "status $%08X: IDLE set while hung", (unsigned)status);
|
||||
CHECK((status >> 16) == 0xDCu, "status $%08X: inner count %u, want 220",
|
||||
(unsigned)status, (unsigned)(status >> 16));
|
||||
|
||||
if (!fast)
|
||||
{
|
||||
for (i = 0; i < WRAP_BYTES; i++)
|
||||
if (ram[DST_BASE + i] != 0)
|
||||
nonzero++;
|
||||
CHECK(nonzero == 0, "%u of %u bytes in the wrap period not cleared",
|
||||
(unsigned)nonzero, (unsigned)WRAP_BYTES);
|
||||
for (i = WRAP_BYTES; i < WRAP_BYTES + 16; i++)
|
||||
CHECK(ram[DST_BASE + i] == 0xFF,
|
||||
"byte $%06X past the wrap period was written",
|
||||
(unsigned)(DST_BASE + i));
|
||||
}
|
||||
|
||||
/* A start while hung is ignored (OUTER.NET: go only leaves idle). */
|
||||
wl(BLIT_REGS + R_A1_BASE, SECOND_BASE, 0);
|
||||
wl(BLIT_REGS + R_A1_FLAGS, 0x00010018u, 0); /* 8bpp pixel mode */
|
||||
wl(BLIT_REGS + R_B_COUNT, 0x00010010u, 0);
|
||||
wl(BLIT_REGS + R_B_CMD, 0, 0);
|
||||
for (i = 0; i < 64; i++)
|
||||
CHECK(ram[SECOND_BASE + i] == 0xFF, "start while hung wrote $%06X",
|
||||
(unsigned)(SECOND_BASE + i));
|
||||
|
||||
/* Savestate round trip, with reset in between. */
|
||||
sz = ser_size();
|
||||
blob = malloc(sz);
|
||||
CHECK(blob && ser(blob, sz), "retro_serialize failed");
|
||||
do_reset();
|
||||
CHECK(!is_hung(), "retro_reset did not clear the hung flag");
|
||||
status = rl(BLIT_REGS + R_B_CMD, 0);
|
||||
CHECK(status & 1u, "status $%08X after reset: IDLE clear", (unsigned)status);
|
||||
if (blob)
|
||||
{
|
||||
CHECK(unser(blob, sz), "retro_unserialize failed");
|
||||
CHECK(is_hung(), "hung flag not restored from the savestate");
|
||||
status = rl(BLIT_REGS + R_B_CMD, 0);
|
||||
CHECK(!(status & 1u) && (status >> 16) == 0xDCu,
|
||||
"status $%08X after unserialize", (unsigned)status);
|
||||
free(blob);
|
||||
}
|
||||
|
||||
harness_shutdown(&cfg);
|
||||
if (fails)
|
||||
{
|
||||
fprintf(stderr, "test_blitter_hung (%s): %d failure(s)\n",
|
||||
fast ? "fast" : "accurate", fails);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS test_blitter_hung (%s engine): never-ending blit returned, "
|
||||
"status busy, start ignored, reset clears, savestate restores\n",
|
||||
fast ? "fast" : "accurate");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user