Skip to content

FIFA Street 2 bring-up fixes: recompiler semantics, VIF1 interrupt stall, PATH3 priority, EE interpreter and more - #396

Open
1lawl wants to merge 17 commits into
ran-j:mainfrom
1lawl:fix/lq-alignment-cdrom-case-pal-vblank
Open

1lawl wants to merge 17 commits into
ran-j:mainfrom
1lawl:fix/lq-alignment-cdrom-case-pal-vblank

Conversation

@1lawl

@1lawl 1lawl commented Oct 25, 2026 •

Copy link
Copy Markdown

What this fixes

Independent fixes found while bringing up FIFA Street 2 (SLES-53797, PAL) on Linux, from boot to in-game matches. Each commit carries its own test, so they can be reviewed or cherry-picked one by one. Sections 6 to 17 were added after the first review round.

1. fix(recomp): align LQ/SQ/LQC2/SQC2 addresses to 16 bytes

The R5900 ignores the low four bits of the effective address for quadword loads/stores, but the translator passed the raw address through. Unaligned memcpy routines depend on that implicit alignment: they LQ from an unaligned source pointer and splice the quadwords with QFSRV. In FIFA Street 2 the memcpy at 0x3cbdd8 copied a config struct from an 8-byte-aligned source, zeroed its size field, and left a later allocator callback NULL (JALR to 0x0 during boot).

The mask is applied in both the expression path and the constant-address (memory hint) path. Rebuilding with it changed 16k generated functions in this game.

2. fix(runtime): match cdrom0: paths case-insensitively

ISO9660 names are case-insensitive and games spell them freely (this game opens anims.big, the disc has ANIMS.BIG). The sceCd* layer already falls back to a case-insensitive lookup, but fio/VFS opens resolved cdrom0: paths verbatim, so they failed on case-sensitive hosts. When the exact path is missing, cdrom0: paths are now matched one component at a time, ignoring ASCII case. host0: and mc0: keep exact matching.

3. feat(runtime): take the CD image path from PS2X_CD_IMAGE

IoPaths::cdImage exists and readCdSectors falls back to it, but nothing on desktop sets it. Games with their own ISO9660 reader (EA's ASYNCFILE reads sector 16, then raw file extents) could never read anything. configureIoPathsFromElf now sets cdImage from the PS2X_CD_IMAGE environment variable.

4. fix(kernel): use 50 Hz VBlanks when SetGsCrt selects PAL

The VBlank period was fixed at 16.667 ms, so PAL games ran their VSync-paced logic 20% fast. SetGsCrt now selects 20 ms for mode 3 (PAL), effective from the next scheduled VBlank. Every other mode keeps exactly the previous 16.667 ms, so NTSC titles are unaffected. A small EeScheduler::vblankPeriod() getter was added for the test.

5. fix(iop): load sceSifLoadModuleBuffer images from IOP RAM

sceSifLoadModuleBuffer takes an IOP address. The EE first copies the IRX into IOP memory (sceSifAllocIopHeap + sceSifSetDma), then asks the IOP to load it from there. The emulator read the image from EE RAM at that address instead, found no ELF header, and failed silently. FIFA Street 2 loads SIO2MAN, MTAPMAN, PADMAN, LIBSD, SNDDRV, MCMAN and MCSERV this way from DATA/IOP.BIG, so none of them started and sceMtapInit spun forever binding SID 0x80000900. The loader now reads IOP RAM first and falls back to EE RAM, so callers that pass an EE address keep working.

6. fix(recomp): compare sign-testing branches on the full 64-bit register

BLEZ, BGTZ, BLTZ and BGEZ (and their likely and linking forms) test bit 63, but the generated conditions used GPR_S32. A value like 0x0000000080000000 (from DSUBU, LD or a 64-bit shift) branched as negative. In FIFA Street 2 this broke the comparator of EA's file-system hash table and its movies failed to open.

7. fix(recomp): read the SQRT.S operand from ft

SQRT.S fd, ft encodes its source in ft (fs is 0); the translator used fs, so every SQRT.S returned sqrt(f0).

8. fix(runtime): truncate CVT.W.S toward zero and saturate

The EE's CVT.W.S always rounds toward zero and saturates from 2^31 up; FPU_CVT_W_S used nearbyintf. (int)(log(112) / log(2)) came out as 7 instead of 6, so FIFA Street 2 sized its text textures twice too wide and its menu text was cut in half.

9. fix(runtime): advance COP0 Count with the EE cycle counter

MFC0 Count read a field nothing updated. Count is now derived from the scheduler's EE cycle counter; MTC0 rebases it. Adds EeScheduler::eeCycle().

10. fix(recomp): keep resume entries from taking other functions' addresses

Resume points are registered after the normal functions but before stubs and library functions. A resume point on a stub's first instruction (a final branch whose delay slot is the next function's entry) sent calls to that stub into the middle of the previous function. Such resume points are now skipped.

11. fix(runtime): keep unwinding nested calls after a scheduler checkpoint

When a checkpoint or jump unwinds to the scheduler, a dispatchGuestBranch call whose callee came back with ctx->pc equal to the callee's entry took that for "returned without setting pc" and continued at its fallthrough. With recursion and an inner loop on the function's first instruction, the outer frame ran on with the inner frame's registers (FIFA Street 2 crashed with a jalr to 0). An unwind flag, set on every unwinding path and cleared by the scheduler before each dispatch, keeps all frames returning.

12. fix(pad): report the DualShock 2 pressure mode ID 0x79

In pressure mode the pad data's second byte is 0x79 (9 halfwords), not 0x73. EA's pad library re-runs its setup forever while it still sees 0x73.

13. fix(iop): complete SPU2 auto-DMA transfers at playback speed

With ADMAS set the SPU2 takes PCM only as fast as it plays it (192 IOP cycles per byte at 48 kHz stereo 16-bit). Completing those transfers at bus speed made sceSdBlockTrans mixers such as EA's snddrv refill back-to-back and kept the IOP 100% busy. Adds a ps2_iop_memory_tests executable.

14. fix(vif): stall VIF1 after a VIFcode with the interrupt bit

An I-bit VIFcode raises the INTC VIF1 interrupt and stops VIF1 until VIF1_FBRST.STC (unless ERR.MII masks it); VIF1 only set STAT.INT and went on. EA's renderer syncs PATH3 uploads with VIF1 this way and waited forever on its first loading screen. The remaining DMA data waits for STC, the VIF1 channel completes after it, and INTC causes queued by PS2Memory reach the scheduler's INTC dispatch (also from the sceDmaSend HLE path, through the new PS2Runtime::dispatchCompletedDmacCause).

15. fix(gif): let PATH2 data resumed after a VIF1 stall beat PATH3

The interrupt handlers restart GIF before VIF1, and the game relies on the PATH2 draws behind the stall winning the arbitration: the PATH3 uploads are for the next batch and may reuse VRAM those draws still read. The runtime ran the whole GIF chain at the CHCR write, so a UI texture overwrote a flag's shading data before the flag was drawn. A GIF chain started while VIF1 is stalled with data left now waits for the next I/O write. If that write is FBRST.STC, the resumed VIF1 data goes first. Any other write, a read of GIF_STAT/GIF_CHCR/D_STAT, or FLUSHA runs it at once, so code waiting for the transfer cannot hang.

16. fix(kernel): set the VBlank rate in sceGsResetGraph too

The libgraph sceGsResetGraph ends with SetGsCrt, so it now selects the PAL/NTSC VBlank rate like the syscall in section 4.

17. feat(runtime): interpret EE code that has no recompiled function

FIFA Street 2's sound engine ships MIPS code modules inside its .ABK banks, links them into the heap and calls them every frame, which a static recompilation cannot see. User RAM (1 MB and up) outside the recompiled code regions now runs in an EE interpreter that follows the recompiler's semantics. It hands control back as soon as it reaches recompiled code or leaves that RAM, and takes a scheduler checkpoint every 32 instructions. hasFunction() keeps meaning "has a recompiled function" (HLE code probes optional helpers with it); the new canRunGuestCode() gates dispatch. PS2X_NO_INTERPRETER=1 turns it off.

Related

This game also needs #354 (sceCdCallback). That PR found the same callback dependency in the NTSC release, so it isn't duplicated here.

Verification

  • Linux (Fedora 44, GCC 16.2), Release, Ninja, -msse4.1 as in CI. Configured with -DPS2X_ENABLE_FFMPEG=OFF -DPLATFORM=SDL because those development packages aren't installed locally.
  • ps2x_tests: 459/459 pass (446 existing + 13 new; sections 9 and 12 extend existing tests). ps2xIOP tests: all five executables pass, including the new ps2_iop_memory_tests.
  • The new tests for sections 10, 11, 13 and 15 were run against the code without their fix and fail there. The others check the exact generated code or value that their fix changes.
  • In-game: with these fixes (plus performance work that is still in my own patches) FIFA Street 2 runs from boot through its menus and loading screens into matches.

🤖 Generated with Claude Code

1lawl and others added 5 commits October 24, 2026 23:34
The R5900 ignores the low four bits of the effective address for quadword
loads and stores. The translator passed the raw address through, so code
that relies on the implicit alignment read and wrote the wrong bytes.

The common case is an unaligned memcpy: libc-style copies load aligned
quadwords with LQ from an unaligned source pointer and splice them with
QFSRV. In FIFA Street 2 (SLES-53797) the memcpy at 0x3cbdd8 copied a
config struct from an 8-byte-aligned source this way, zeroed its size
field, and left a later allocator callback NULL.

Mask the address in both the expression path and the constant-address
(memory hint) path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ISO9660 names are case-insensitive and games spell them freely; FIFA
Street 2, for example, opens lowercase names that the disc stores in
uppercase. The sceCd* layer already falls back to a case-insensitive
lookup, but fio/VFS opens resolved cdrom0: paths verbatim, so they failed
on case-sensitive host filesystems such as Linux.

When the exact host path does not exist, resolve cdrom0: paths one
component at a time, ignoring ASCII case. Other devices (host0:, mc0:)
keep their exact-match behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IoPaths::cdImage exists and readCdSectors falls back to it for LBNs that
are not backed by a sceCdSearchFile entry, but nothing on desktop ever
set it. Games that parse ISO9660 themselves (EA's ASYNCFILE, used by
FIFA Street 2, reads sector 16 and then raw file extents) could therefore
never read anything.

Set cdImage from the PS2X_CD_IMAGE environment variable when the ELF's
I/O paths are configured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VBlank period was fixed at 16.667 ms, so PAL games ran their
VSync-paced logic 20% fast. SetGsCrt now picks the rate from the video
mode argument (3 = PAL, 50 Hz; anything else keeps the previous 60 Hz),
effective from the next scheduled VBlank.

Also correct the mode comment: SetGsCrt uses 2 for NTSC and 3 for PAL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sceSifLoadModuleBuffer takes an IOP address: the EE first copies the IRX
into IOP memory (sceSifAllocIopHeap + sceSifSetDma) and then asks the IOP
to load it from there. The emulator read the image from EE RAM at that
address instead, found no ELF header, and failed the load silently.

FIFA Street 2 loads SIO2MAN, MTAPMAN, PADMAN, LIBSD, SNDDRV, MCMAN and
MCSERV this way from DATA/IOP.BIG, so none of them started and
sceMtapInit spun forever trying to bind SID 0x80000900.

Read the image from IOP RAM first and fall back to EE RAM, so callers
that still hand over an EE address keep working. The ELF reader is
shared between both sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1lawl 1lawl changed the title Fix LQ/SQ alignment, case-insensitive cdrom0 paths, PS2X_CD_IMAGE and PAL VBlank rate Fix LQ/SQ alignment, cdrom0 path case, PS2X_CD_IMAGE, PAL VBlank rate and IOP-RAM module buffers Oct 25, 2026
1lawl and others added 12 commits October 27, 2026 19:11
BLEZ, BGTZ, BLTZ and BGEZ (and their likely and linking forms) test bit
63 of the 64-bit GPR. The generated conditions only looked at the low
32 bits, so a value such as 0x0000000080000000 (from DSUBU, LD or a
64-bit shift) branched as if it were negative. In FIFA Street 2 this
broke a comparison in EA's file system and its movies failed to open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SQRT.S fd, ft takes its source in the ft field and encodes fs as 0. The
translator used fs, so every SQRT.S returned sqrt(f0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The EE's CVT.W.S always rounds toward zero, whatever the FCR31 rounding
mode, and saturates to INT32_MAX/INT32_MIN from 2^31 up. FPU_CVT_W_S
used nearbyintf (round to nearest). Games that compute log2 with
(int)(log(w) / log(2)) got 7 instead of 6 for a width of 112, and
FIFA Street 2 sized its text textures twice too wide, cutting its menu
text in half.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Count increments every EE cycle, but MFC0 read a field nothing updated,
so code that waits or measures with Count saw no time pass. Count is now
derived from the scheduler's EE cycle counter; MTC0 rebases it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resume points are registered after the normal functions but before
stubs and library functions. When a resume point fell on the first
instruction of a stub (a final branch whose delay slot is the next
function's first instruction), the table sent calls to that stub into
the middle of the previous function. Resume points that are another
function's start are now skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a checkpoint or jump unwinds to the scheduler, every enclosing
dispatchGuestBranch call has to return as well. A call whose callee
came back with ctx->pc equal to the callee's entry was instead treated
as "the callee returned without setting pc" and continued at its
fallthrough. That happens with recursion when the inner call unwinds at
a loop on the function's first instruction: the outer frame then ran on
with the inner frame's registers (FIFA Street 2 crashed with a jalr to
0). An unwind flag, set on every unwinding path and cleared by the
scheduler before each dispatch, now keeps all frames returning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second byte of the pad data is the terminal ID and the payload
length in halfwords. In pressure mode a DualShock 2 sends 9 halfwords,
so it reports 0x79, not the analog 0x73. EA's pad library checks this
after entering pressure mode and restarts its setup forever while it
still reads 0x73.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With auto-DMA (ADMAS) the SPU2 takes PCM data only as fast as it plays
it, 48 kHz stereo 16-bit or 192 IOP cycles per byte. The IOP DMA
completed such transfers at bus speed, so software mixers that stream
through sceSdBlockTrans (EA's snddrv) refilled back-to-back and kept
the IOP busy 100% of the time.

Adds ps2_iop_memory_tests for the DMA timing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A VIFcode with the I bit sets STAT.INT, raises the INTC VIF1 interrupt
and stops VIF1 once the command is done, until the CPU writes
VIF1_FBRST.STC (unless ERR.MII masks it). VIF1 only set STAT.INT and
went on. EA's renderer syncs PATH3 texture uploads with VIF1 this way:
its handlers restart GIF and VIF1 when both have stopped, and without
the stall and interrupt it waited forever on the first loading screen.

The remaining DMA data waits until FBRST.STC (a further stall inside it
only moves an offset), the VIF1 channel completes after that data has
run, and INTC causes queued by PS2Memory reach the scheduler's INTC
dispatch, also from the sceDmaSend path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATH3 has the lowest GIF priority. Games that sync GIF and VIF1 with a
VIF1 interrupt restart both from the handler, GIF first, and rely on
the PATH2 draws behind the stall winning the arbitration: the PATH3
uploads are for the next batch and may reuse VRAM those draws still
read. The runtime ran the whole GIF chain at the CHCR write, so in FIFA
Street 2 a UI texture overwrote a flag's shading data before the flag
was drawn.

A GIF chain started while VIF1 is stalled with data left now waits for
the next I/O register write. If that write is VIF1_FBRST.STC, the
resumed VIF1 data goes first. Any other write, a read of GIF_STAT,
GIF_CHCR or D_STAT, or VIF1 FLUSHA runs the chain right away, so code
that waits for the GIF transfer cannot hang.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The libgraph sceGsResetGraph ends with SetGsCrt(interlace, omode,
ffmode), so a PAL game that only calls sceGsResetGraph also runs at
50 Hz. The HLE version did not select the VBlank rate; it now does, like
the SetGsCrt syscall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some games build or load MIPS code in RAM at run time, which a static
recompilation cannot see. FIFA Street 2's sound engine (EA AEMS) ships
code modules inside its .ABK sound banks, links them into the heap and
calls them every frame. Such calls stopped the runtime with a missing
function.

User RAM (1 MB and up) outside the recompiled code regions now runs in
an EE interpreter that follows the recompiler's semantics. It hands
control back as soon as it reaches recompiled code or leaves that RAM,
calls recompiled functions through dispatchGuestBranch, and takes a
scheduler checkpoint every 32 instructions. hasFunction() keeps meaning
"has a recompiled function" (HLE code probes optional helpers with it);
the new canRunGuestCode() also accepts interpretable addresses and gates
dispatch. PS2X_NO_INTERPRETER=1 turns the interpreter off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1lawl 1lawl changed the title Fix LQ/SQ alignment, cdrom0 path case, PS2X_CD_IMAGE, PAL VBlank rate and IOP-RAM module buffers FIFA Street 2 bring-up fixes: recompiler semantics, VIF1 interrupt stall, PATH3 priority, EE interpreter and more Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant