mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
fix(tom): derive HC readback from emulated time so AvP marines attack (#1094)
HC ($F00004) free-runs 0..HP once per half-line on hardware (JTRM), so its low bits at a CPU read are effectively random. Alien vs Predator uses them as its marines' fire roll -- `move.w $F00004,d0 / and #7 / cmp #2` at 68K $015ADA -- while its Predators roll the game's software RNG. The core returned a per-read counter reset to 0 / (HP+1)/2 every half-line, so a once-per-AI-tick read always saw $0000 or $05A6: low bits 0 or 6, never 2. Marines walked up to an Alien player and never fired (30 of 30 rolls measured from the corridor savestate). The readback is now the elapsed fraction of the current half-line: time to the next half-line event at slice start, less how far the 68K has run into the slice (JaguarSliceElapsedUs, stall and clock scale included), scaled to HP+1 ticks. Byte reads of $F00004/5 use it too. tomRam8[HC] is unchanged (the OP's second-half-line test reads bit 10), and the read-phase counter is gone; nothing new is serialized. Measured after: the same marine rolls all eight HC&7 values (2 on 12 of 111), crouches, fires, and the Alien's health drops. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c1653da97b
commit
1fb655012e
6 files changed
+147
-29
No files matched your search
@@ -132,5 +132,6 @@ _InitializeEventList
|
||||
_SubtractEventTimes
|
||||
_GetTimeToNextEvent
|
||||
_HandleNextEvent
|
||||
_SetCallbackTime
|
||||
_EventStateSave
|
||||
_EventStateLoad
|
||||
@@ -43,6 +43,7 @@
|
||||
SubtractEventTimes;
|
||||
GetTimeToNextEvent;
|
||||
HandleNextEvent;
|
||||
SetCallbackTime;
|
||||
EventStateSave;
|
||||
EventStateLoad;
|
||||
TOM*;
|
||||
|
||||
@@ -1715,6 +1715,36 @@ uint8_t * GetRamPtr(void)
|
||||
}
|
||||
|
||||
|
||||
/* Slice progress for JaguarSliceElapsedUs(). Never serialized: a
|
||||
* savestate is taken between frames, where all of it is back at its
|
||||
* idle value. */
|
||||
static bool m68kSliceActive = false;
|
||||
static uint32_t m68kSliceStall = 0; /* 68K cycles stalled before executing */
|
||||
static uint32_t m68kSliceBudget = 0; /* cycles handed to m68k_execute() */
|
||||
static double sliceDoneUs = 0.0;
|
||||
|
||||
double JaguarSliceElapsedUs(void)
|
||||
{
|
||||
double cycleUs = vjs.hardwareTypeNTSC ? M68K_CYCLE_IN_USEC
|
||||
: M68K_CYCLE_PAL_IN_USEC;
|
||||
int run;
|
||||
|
||||
if (!m68kSliceActive)
|
||||
return sliceDoneUs;
|
||||
|
||||
/* m68k_cycles_run() is not monotonic after m68k_end_timeslice() (see
|
||||
* its comment); the budget bound keeps it inside the slice. */
|
||||
run = m68k_cycles_run();
|
||||
if (run < 0)
|
||||
run = 0;
|
||||
if ((uint32_t)run > m68kSliceBudget)
|
||||
run = (int)m68kSliceBudget;
|
||||
|
||||
/* Executed cycles are scaled cycles; wall time is unscaled. */
|
||||
return ((double)m68kSliceStall
|
||||
+ (double)run * 100.0 / (double)m68kClockScalePct) * cycleUs;
|
||||
}
|
||||
|
||||
/* Run a 68K slice minus any pending OP-fetch/refresh stall. pending
|
||||
* is in system clocks; the 68K runs at system/2, so two pending clocks
|
||||
* consume one 68K cycle. A slice can be fully consumed (the 68K runs
|
||||
@@ -1723,6 +1753,7 @@ uint8_t * GetRamPtr(void)
|
||||
static void M68KExecuteWithStalls(uint32_t cycles)
|
||||
{
|
||||
uint32_t stall;
|
||||
m68kSliceStall = 0;
|
||||
/* The drain is deliberately NOT gated on busArbiter.enabled: the
|
||||
* pending-stall channel is shared by the dram_timing model (whose
|
||||
* charge sites gate on the option) and blitter bus-time charges
|
||||
@@ -1749,6 +1780,7 @@ static void M68KExecuteWithStalls(uint32_t cycles)
|
||||
stall = cycles - keep;
|
||||
busArbiter.m68k_pending_stall -= stall << 1;
|
||||
cycles -= stall;
|
||||
m68kSliceStall = stall;
|
||||
VJT_BUS_M68K_STALL(stall);
|
||||
/* A slice fully consumed by stall must not fall through to
|
||||
* m68k_execute(0): the UAE core's main loop is a do/while, so
|
||||
@@ -1779,11 +1811,17 @@ static void M68KExecuteWithStalls(uint32_t cycles)
|
||||
m68kScaleAccum = (uint32_t)(budget % 100u);
|
||||
if (scaled == 0)
|
||||
return;
|
||||
m68kSliceBudget = scaled;
|
||||
m68kSliceActive = true;
|
||||
m68k_execute(scaled);
|
||||
m68kSliceActive = false;
|
||||
VJT_BUS_M68K_END(regs.remainingCycles);
|
||||
return;
|
||||
}
|
||||
m68kSliceBudget = cycles;
|
||||
m68kSliceActive = true;
|
||||
m68k_execute(cycles);
|
||||
m68kSliceActive = false;
|
||||
VJT_BUS_M68K_END(regs.remainingCycles);
|
||||
}
|
||||
|
||||
@@ -1833,8 +1871,10 @@ void JaguarExecuteNew(void)
|
||||
VJP_ENTER(VJP_M68K);
|
||||
M68KExecuteWithStalls(USEC_TO_M68K_CYCLES(timeDelta));
|
||||
VJP_LEAVE(VJP_M68K);
|
||||
sliceDoneUs = timeDelta;
|
||||
GPUExec(GPUSliceRemaining());
|
||||
DSPExec(DSPSliceRemaining());
|
||||
sliceDoneUs = 0.0;
|
||||
SubtractEventTimes(timeDelta, EVENT_MAIN);
|
||||
HandleNextEvent(EVENT_JERRY);
|
||||
}
|
||||
@@ -1853,8 +1893,10 @@ void JaguarExecuteNew(void)
|
||||
VJP_ENTER(VJP_M68K);
|
||||
M68KExecuteWithStalls(USEC_TO_M68K_CYCLES(timeDelta));
|
||||
VJP_LEAVE(VJP_M68K);
|
||||
sliceDoneUs = timeDelta;
|
||||
GPUExec(GPUSliceRemaining());
|
||||
DSPExec(DSPSliceRemaining());
|
||||
sliceDoneUs = 0.0;
|
||||
SubtractEventTimes(timeDelta, EVENT_JERRY);
|
||||
HandleNextEvent(EVENT_MAIN);
|
||||
}
|
||||
|
||||
@@ -102,6 +102,17 @@ uint32_t JaguarGetDefaultFieldHalflines(void);
|
||||
/* 1e6 / (halflines * halfline_us): 60.05445 Hz NTSC / 50.08013 Hz PAL. */
|
||||
double JaguarGetFieldRateHz(void);
|
||||
|
||||
/* The half-line event; TOM's HC readback asks the scheduler how long
|
||||
* until it fires next. */
|
||||
void HalflineCallback(void);
|
||||
/* Emulated microseconds elapsed since the start of the current
|
||||
* JaguarExecuteNew() slice: the 68K's own progress while it runs (its
|
||||
* bus-stall share included), the whole slice while GPU/DSP finish it,
|
||||
* 0 between slices. Event times are only rebased at slice end, so this
|
||||
* plus a GetCallbackTime() remainder places a bus access inside the
|
||||
* slice -- what a free-running counter such as HC needs. */
|
||||
double JaguarSliceElapsedUs(void);
|
||||
|
||||
/* Clock-scale enhancement levers (issue #314), in percent of the stock
|
||||
* rate (100 = stock). Config, not state: set from the core options in
|
||||
* libretro.c::check_variables(), never serialized. Applied ONLY where
|
||||
|
||||
+60
-28
@@ -414,7 +414,6 @@ uint32_t tomWidth, tomHeight;
|
||||
uint32_t tomTimerPrescaler;
|
||||
uint32_t tomTimerDivider;
|
||||
int32_t tomTimerCounter;
|
||||
static uint16_t tomHCReadPhase;
|
||||
|
||||
/* Rows TOMExecHalfline actually wrote, tracked live so a mid-frame VDB/VDE
|
||||
* change cannot make the count disagree with what was drawn. `Cur`
|
||||
@@ -1529,9 +1528,11 @@ void tom_render_16bpp_rgb_scanline(uint32_t * VJ_RESTRICT backbuffer)
|
||||
* That is exactly what this models: called from TOMExecHalfline for every
|
||||
* half-line, it latches when the beam reaches the aimed-at row. No beam
|
||||
* hit-test window, no pulse generator, and above all no per-clock
|
||||
* horizontal counter -- which this core does not have (HC advances once
|
||||
* per half-line here) and which a corpus scan says nothing needs: no
|
||||
* shipped title reads HC/VC ($F00004/$F00006) at all, Balloons included.
|
||||
* horizontal counter stepping through the line -- HC's bus readback is
|
||||
* derived from emulated time on demand (TOMReadHC), and nothing beam-
|
||||
* driven needs more. Balloons never reads HC/VC ($F00004/$F00006); the
|
||||
* titles that do read HC use its low bits as a random number (see
|
||||
* TOMReadHC).
|
||||
* Synthesising the latched value from the aim point is therefore not a
|
||||
* shortcut around a timing model, it is the whole of the observable
|
||||
* behaviour.
|
||||
@@ -1645,17 +1646,12 @@ void TOMExecHalfline(uint16_t halfline, bool render)
|
||||
// Update HC to approximate position within the scanline.
|
||||
// Bit 10 (0x0400) is the half-line indicator, analogous to VC's
|
||||
// field bit 11. The OP tests this via CONDITION_SECOND_HALF_LINE.
|
||||
// Bits 0-9 approximate the position within the half-line.
|
||||
// Bits 0-9 are only a placeholder here: a bus read of HC derives the
|
||||
// count from emulated time instead (TOMReadHC).
|
||||
if (halfline & 0x01)
|
||||
{
|
||||
SET16(tomRam8, HC, 0x0400 | (hp > 0 ? (hp + 1) / 2 : 0));
|
||||
tomHCReadPhase = hp > 0 ? (hp + 1) / 2 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
SET16(tomRam8, HC, 0);
|
||||
tomHCReadPhase = 0;
|
||||
}
|
||||
|
||||
/* Light gun LP edge (#438). Before the odd-half-line early-out below,
|
||||
* because an aim point can sit on an odd half-line and the latch is
|
||||
@@ -2024,7 +2020,6 @@ void TOMReset(void)
|
||||
tomTimerPrescaler = 0; // TOM PIT is disabled
|
||||
tomTimerDivider = 0;
|
||||
tomTimerCounter = 0;
|
||||
tomHCReadPhase = 0;
|
||||
}
|
||||
|
||||
void TOMPITCallback(void);
|
||||
@@ -2033,6 +2028,54 @@ void TOMPITCallback(void);
|
||||
* Derive them from the already-serialized scheduled expiration, in full
|
||||
* system clocks (JTRM v8 p.15). Reads have scheduler-slice resolution;
|
||||
* they must not advance time or reschedule the interrupt. */
|
||||
/* HC as the bus sees it. On hardware HC is a free-running counter: "a
|
||||
* ten bit counter which counts from zero up to the value in the
|
||||
* horizontal period register twice per video line. An eleventh bit
|
||||
* determines which half of the display is being generated. The counter
|
||||
* is incremented by the pixel clock" (JTRM Rev 8, Technical Reference
|
||||
* p.11). So its low bits at the instant of a CPU read are effectively
|
||||
* arbitrary, and games use them as a cheap random source -- the JTRM
|
||||
* calls the register "only for ASIC test purposes", but a scan of 160
|
||||
* cart dumps found sixteen reading it in attract mode alone (Atari Karts,
|
||||
* Bubsy, Iron Soldier 1/2, NBA Jam TE, White Men Can't Jump, Gorf 2000,
|
||||
* ...), and Alien vs Predator reads it in gameplay.
|
||||
*
|
||||
* AvP is where the old model showed. Its marine AI rolls
|
||||
* `move.w $F00004,d0 / and #7 / cmp #2` (68K $015ADA, RAM-resident code)
|
||||
* to decide whether to open fire, while its Predators roll the game's own
|
||||
* software RNG. The previous model returned a per-read counter that was
|
||||
* reset to 0 / (HP+1)/2 every half-line, so a read that happens once per
|
||||
* AI tick always saw $0000 or $05A6 -- low bits 0 or 6, never 2 -- and
|
||||
* marines never fired on an Alien player (measured: 30 of 30 rolls).
|
||||
*
|
||||
* The count is the elapsed fraction of the current half-line: the time to
|
||||
* the next half-line event at slice start, less how far into the slice
|
||||
* the reading processor has got (JaguarSliceElapsedUs), scaled to HP+1
|
||||
* ticks. The 68K is placed to the cycle; a GPU/DSP read is placed only
|
||||
* to the slice resolution its scheduling has. Derived purely from serialized
|
||||
* event time, so savestates and run-ahead see the same value. */
|
||||
static uint16_t TOMReadHC(void)
|
||||
{
|
||||
uint16_t hc = GET16(tomRam8, HC);
|
||||
uint32_t ticks = (uint32_t)(GET16(tomRam8, HP) & 0x03FF) + 1;
|
||||
double period = JaguarGetHalflinePeriodUs();
|
||||
double left = GetCallbackTime(HalflineCallback, EVENT_MAIN);
|
||||
double elapsed;
|
||||
uint32_t count;
|
||||
|
||||
if (left < 0.0)
|
||||
return hc;
|
||||
|
||||
elapsed = period - (left - JaguarSliceElapsedUs());
|
||||
if (elapsed < 0.0)
|
||||
elapsed = 0.0;
|
||||
count = (uint32_t)(elapsed * (double)ticks / period);
|
||||
if (count >= ticks)
|
||||
count = ticks - 1;
|
||||
|
||||
return (uint16_t)((hc & 0x0400) | count);
|
||||
}
|
||||
|
||||
static uint16_t TOMReadPIT(int divider)
|
||||
{
|
||||
double usecs = GetCallbackTime(TOMPITCallback, EVENT_MAIN);
|
||||
@@ -2085,6 +2128,10 @@ uint8_t TOMReadByte(uint32_t offset, uint32_t who)
|
||||
return TOMReadPIT(1) >> 8;
|
||||
else if (offset == 0xF00053)
|
||||
return TOMReadPIT(1) & 0xFF;
|
||||
else if (offset == 0xF00004)
|
||||
return TOMReadHC() >> 8;
|
||||
else if (offset == 0xF00005)
|
||||
return TOMReadHC() & 0xFF;
|
||||
else if (offset == 0xF000E0)
|
||||
return 0;
|
||||
else if (offset == 0xF000E1)
|
||||
@@ -2108,21 +2155,7 @@ uint16_t TOMReadWord(uint32_t offset, uint32_t who)
|
||||
return data;
|
||||
}
|
||||
else if (offset == 0xF00004)
|
||||
{
|
||||
uint16_t hc = GET16(tomRam8, HC);
|
||||
uint16_t hp = GET16(tomRam8, HP);
|
||||
uint16_t limit = hp > 0 ? (hp + 1) / 2 : 0x400;
|
||||
uint16_t phase = tomHCReadPhase;
|
||||
|
||||
if (limit == 0)
|
||||
limit = 1;
|
||||
|
||||
tomHCReadPhase++;
|
||||
if (tomHCReadPhase >= limit)
|
||||
tomHCReadPhase = 0;
|
||||
|
||||
return (hc & 0x0400) | (phase & 0x03FF);
|
||||
}
|
||||
return TOMReadHC();
|
||||
else if ((offset >= GPU_CONTROL_RAM_BASE) && (offset < GPU_CONTROL_RAM_BASE + 0x20))
|
||||
return GPUReadWord(offset, who);
|
||||
else if ((offset >= GPU_WORK_RAM_BASE) && (offset < GPU_WORK_RAM_BASE + 0x1000))
|
||||
@@ -2501,7 +2534,6 @@ size_t TOMStateLoad(const uint8_t *buf)
|
||||
STATE_LOAD_VAR(buf, tom_video_int_pending);
|
||||
STATE_LOAD_VAR(buf, tomWidth);
|
||||
STATE_LOAD_VAR(buf, tomHeight);
|
||||
tomHCReadPhase = GET16(tomRam8, HC) & 0x03FF;
|
||||
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
@@ -13,6 +13,8 @@ static size_t (*save_events)(uint8_t *);
|
||||
static size_t (*load_events)(const uint8_t *);
|
||||
static uint8_t (*read_byte)(uint32_t,uint32_t);
|
||||
static uint32_t (*read_long)(uint32_t,uint32_t);
|
||||
static void (*set_callback)(void (*)(void),double,int);
|
||||
static void (*halfline_cb)(void);
|
||||
static void reset_timer(void)
|
||||
{
|
||||
c.TOMReset(); init_events();
|
||||
@@ -90,6 +92,33 @@ TEST(readable_memory)
|
||||
c.TOMWriteWord(0xF02020,0x5678,0);
|
||||
ASSERT_EQ(c.TOMReadWord(0xF02020,0),0x5678);
|
||||
}
|
||||
/* HC free-runs 0..HP across each half-line (JTRM), so its low bits at a
|
||||
* read are the read's position in the line. Alien vs Predator rolls its
|
||||
* marine fire decision on `HC & 7 == 2`; a readback that was a per-read
|
||||
* counter reset every half-line only ever showed 0 or 6 there. */
|
||||
TEST(hc_free_runs)
|
||||
{
|
||||
const double period = 31.777777777, tick = period / 845.0;
|
||||
unsigned k;
|
||||
c.TOMReset(); init_events();
|
||||
((struct VJSettings *)c.vjs)->hardwareTypeNTSC=true;
|
||||
c.TOMWriteWord(0xF0002E, 844, 0); /* HP: 845 ticks */
|
||||
set_callback(halfline_cb, period, 0);
|
||||
ASSERT_EQ(c.TOMReadWord(0xF00004,0),0);
|
||||
/* Every low-bit value is reachable, and a read does not move it. */
|
||||
for(k=0;k<8;k++)
|
||||
{
|
||||
subtract_time(k ? tick : 0.5 * tick, 0);
|
||||
ASSERT_EQ(c.TOMReadWord(0xF00004,0),k);
|
||||
ASSERT_EQ(c.TOMReadWord(0xF00004,0),k);
|
||||
}
|
||||
subtract_time(400.0 * tick, 0);
|
||||
ASSERT_EQ(c.TOMReadWord(0xF00004,0),407);
|
||||
ASSERT_EQ(read_byte(0xF00004,0),407 >> 8);
|
||||
ASSERT_EQ(read_byte(0xF00005,0),407 & 0xFF);
|
||||
subtract_time(437.0 * tick, 0); /* tick 844.5 */
|
||||
ASSERT_EQ(c.TOMReadWord(0xF00004,0),844);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
TEST_INIT("TOM hardware readback");
|
||||
@@ -104,9 +133,11 @@ int main(void)
|
||||
RESOLVE(load_events,"EventStateLoad");
|
||||
RESOLVE(read_byte,"TOMReadByte");
|
||||
RESOLVE(read_long,"JaguarReadLong");
|
||||
RESOLVE(set_callback,"SetCallbackTime");
|
||||
RESOLVE(halfline_cb,"HalflineCallback");
|
||||
RUN_TEST(pit_live); RUN_TEST(pit_rollover_and_state);
|
||||
RUN_TEST(pit_full_width); RUN_TEST(memcon_fallback);
|
||||
RUN_TEST(readable_memory);
|
||||
RUN_TEST(readable_memory); RUN_TEST(hc_free_runs);
|
||||
vj_core_unload(&c);
|
||||
return TEST_REPORT();
|
||||
}
|
||||
Reference in new issue
Block a user