Commit Graph
642 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 88d6befc02 CPU: jr and jalr ignore the target's low two bits on the main CPU too
On a PSP, jr and jalr to an address with its low two bits set raise no
address error: they run from the aligned address, the PC keeps no low
bits, and jalr's link is unaffected (cpu/jump/unaligned, recorded on
firmware 6.61). This was done only for the Media Engine.

The firmware's PS1 emulator returns through such an address. The
interpreters stopped there with an invalid jump, and the JITs found the
block at the aligned address only if it was already compiled, and
otherwise compiled one at the unaligned address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 14:23:39 +02:00
Henrik RydgårdandClaude Opus 5.5 8560a2df75 IR: Log compiled jumps to invalid addresses at debug level
A block that jumps to an invalid address is usually code that never runs,
such as a call through an import that isn't linked yet, and the error-level
"Jump to invalid address" sent people chasing it. A jump that actually runs
still fails as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 17:32:43 +02:00
Henrik RydgårdandClaude Opus 5.5 4b6aea51e7 IR interpreter: Halt with a memory exception on a bad guest access
HandleFault only handled faults in JIT code, so a game's bad load or store
under the IR interpreter crashed the app. Now IRInterpret marks itself
(a thread-local, cleared around HLE calls, replacements and the MIPS
interpreter fallback) and the handler steps over the faulting instruction,
reports the exception and stops the core, or just ignores the access when
IgnoreBadMemAccess is on. If the instruction can't be decoded it still
crashes, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:35:00 +02:00
Henrik RydgårdandClaude Opus 5.5 bafdb45128 ME: Syscalls to the firmware's cache functions do nothing
Kernel code the ME runs calls a few firmware functions directly: POPS's
ME loop calls sceKernelDcacheWritebackInvalidateAll for every sample. On
a PSP the loader links kernel imports as direct jumps, so the ME runs
the function's code; here they're HLE syscall stubs. The cache functions
only work on the calling CPU's caches, and the ME's aren't emulated,
so they return 0 and do nothing, as does an unresolved import (as on
the main CPU). Others are still an error, now naming the function.

The IR ends the block after one, as for a syscall on the main CPU: a jr
with it in its delay slot leaves the exit to it, and the ME ran on into
the next stub.

A syscall instruction of the ME's own should take its exception vector
(ExcCode 8); none has come up yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 12:50:46 +02:00
Henrik RydgårdandClaude Opus 5.5 090e215a0a ME: Boot Sony's ME firmware, as me_wrapper does
Instead of only putting the image's code at 0x88300000, the ME now runs
it to where a PSP's ME is after boot: idle in its main loop's halt,
waiting for commands. As me_wrapper's boot (6.61, sceMeCore_driver
5dff5c50 and 0x1c30) and its ME stub leave things: the code at
0x88300000, the image's second stream still compressed after it, the
boot page's parameters (0x70C, 0x710 the CPU clock in MHz, 0x714), and
the ME at 0x88300000 with a0 at the second stream, which the code
decompresses into eDRAM itself. The stub's part (KL4E, caches) is done
here, since it runs main CPU kernel routines.

On a PSP the firmware has long since booted when a game starts, so it
runs to its idle halt the first time the ME runs, outside emulated time
(the JIT can't run earlier, at kernel init). About 80ms of startup in a
Debug build.

The compressed copy and the starting stack go below our idle threads'
stacks at the top of kernel memory, where a PSP has them higher up.

The firmware also needs:
- the ME to ignore the low two bits of jr/jalr targets: its command
  table keeps flags there (interpreter, and the IR, for the ME only)
- source 5 of the ME's interrupt controller, the codec hardware it
  starts and waits for in halt during init, which isn't emulated, so it
  finishes at once
- sysreg 0x54 and 0x5C kept

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 12:06:11 +02:00
Henrik RydgårdandClaude Opus 5.5 6e475258da ME: Run the Media Engine on the ARM64 and x64 JITs
The native dispatcher generated for the ME leaves when its downcount
runs out instead of calling CoreTiming::Advance, and IRNativeJit gets
RunDowncount. Pointerify is decided from the CPU's own memory base. ARM32,
RISC-V and LoongArch64 still give the ME the IR interpreter.

LoadKernel/StoreKernel are now compiled on ARM64 and x64 rather than
going through the IR interpreter one op at a time: memory directly, or a
call to fixed code (which saves the caller-saved registers guest values
live in) for 0xBC000000-0xBFFFFFFF. ARM64's block size estimate counts
them as such. The 16-bit ones stay generic while the software renderer's
depth mirrors are active, as Load16/Store16 do. That matters for the main
CPU's kernel code too, and for all code while the ME is emulated, which
runs in kernel mode everywhere. mcidclan's me-soft-interrupt went from
476 billion to 6.4 billion host instructions for 3 emulated seconds,
ending with the same ME counter as the IR interpreter.

The ME unit tests run on the native JIT too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 10:28:50 +02:00
Henrik RydgårdandClaude Opus 5.5 fa49876414 ME: Run the Media Engine on the IR interpreter
Unless the interpreter is selected, the ME gets its own IRInterpreterJit,
with its own block shadow, and runs it without CoreTiming
(RunDowncount, split out of the dispatcher loop). Its MIPSState never
touches the main CPU's replacement hook.

The IR frontend compiles through the CPU's address space: instructions
read as they are in the ME's memory (no main CPU replacements or hooks),
kernel mode always, jump targets and block bookkeeping checked against
its rules, syscall left to the interpreter (which traps it). An
interpreted instruction that ends a block now gets its own pc first, so
halt goes on from the right place.

The main CPU's icache invalidations are passed on to the ME, which runs
code from the same RAM.

The unit tests run the ME on both cores. mcidclan's me-soft-interrupt
runs about four times as fast.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 10:28:50 +02:00
Henrik RydgårdandClaude Opus 5.5 e52cc38866 ME: Reset, interrupts and the other registers homebrew runs the ME with
Sysreg (0xBC100000) gets the ME's reset bit (0x4C bit 2), the soft
interrupt (0x44, which interrupts the other CPU: the ME's controller from
the main CPU, PSP_MECODEC_INTR from the ME), the hardware mutex (0x48,
each CPU sets its own bit only while the other's is clear) and the clock
and config registers it writes. The ME's interrupt controller (0xBC300000,
write-1-to-clear, mask at 8) answers the ME only. All of it is the ME's
state and stays unhandled without an ME.

On the ME: interrupts to EBase with EPC and EXL, halt sleeping until an
unmasked interrupt, and Count, which doesn't run: it stays 0 across 10ms
with ERL set after reset and with Status cleared, and reads back what was
written. Reset keeps the GPRs and sets Status ERL/BEV/SR, which homebrew
relies on: mcidclan's samples run C on the stack Sony's ME code left. The
values are from a kernel probe on a PSP-1000 (sp 0x801FFFE0, COP0 22 = 1,
Status 0x20500404, mfic 0, which doesn't gate interrupts).

The main CPU runs base interrupt handlers the way interruptman calls them
(a0 intno, a1 arg, a2 interrupted pc; -2 disables), and interrupt 31 has
a stand-in for me_wrapper's handler, which homebrew releases first.

While the ME is emulated, any main CPU code may access hardware registers:
homebrew calls its own code in kernel mode through a kcall module. This
is a stopgap (g_kernelModeAnywhere, marked HACK): kernel mode should be
part of the CPU's state, switched on while a kernel module's export runs.
The memcpy/memmove replacements copy through MMIO when an end is a
register address, for code copied to the boot page.

mcidclan's me-soft-interrupt sample now runs, counting each interrupt in
both directions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 09:33:26 +02:00
Henrik RydgårdandClaude Opus 5.5 41851007a2 Memory: Give the Media Engine an address space of its own
The ME sees the main CPU's RAM, but has its own eDRAM at 0 (with
0x80000000 and 0xA0000000 mirrors) where the main CPU has its scratchpad,
its own memory where the main CPU has VRAM, and both CPUs share the boot
page at 0xBFC00000.

- The ME's own memory is views in the main arena flagged MV_ME_ONLY, which
  only Memory::CreateMESpace() maps, into a second reservation along with
  the shared views at their same arena positions. It gets its own block
  shadow.
- The eDRAM (MV_EDRAM) is the ME's half of the eDRAM chip, the GE's VRAM
  being the other: 2MB on the PSP-1000 (whose resident ME image says so),
  4MB on the others (from mcidclan's libmecore, not hardware-checked).
- At VRAM's addresses, through 0x44000000 and 0xA4000000 alike, the ME
  doesn't see VRAM but memory of its own (pspautotest me/vram, on a
  PSP-1000): where homebrew keeps the VME's buffers. Its size isn't known;
  2MB with VRAM's mirrors here, and no depth layout on 16-bit accesses.
- 0xA0000000 reaches physical memory uncached, so RAM is mapped at
  0xA8000000 too.
- The boot page is a view too, which the main CPU reaches through a new
  MMIO device (in savestates from MMIO section version 2). The ME runs
  code from it, uncached, so the main CPU's writes invalidate code there.
  On masked platforms it isn't mapped, and the device uses a plain
  buffer.
- AddressSpace has IsValidAddress etc, with the ME's rules when edramSize
  is set, and the interpreters and the IR interpreter check and fetch
  instructions through their state's space. The IR compiler's checks still
  use the main CPU's, until the ME gets a JIT.
- On Darwin, MemArena can hold several view reservations.

Masked platforms (iOS, 32-bit) don't get an ME space yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 22:22:40 +02:00
Henrik RydgårdandClaude Opus 5.5 3ec2c5369b IR: Find blocks to invalidate by walking the table for huge ranges
sceKernelIcacheInvalidateAll invalidates 0..0x3FFFFFFF, which is a
million 1 KB pages, each looked up in the hash map. A homebrew calling
it once per frame spent most of its time there. When the range has more
pages than the table has entries, go through the table instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 21:18:21 +02:00
Henrik RydgårdandClaude Opus 5.5 c5d2adb303 IR: Compile data cache ops to nothing
The interpreter does nothing for any cache op but icache invalidation, but
the IR only knew four of them and sent the rest to the interpreter one at
a time. Media Engine code flushes its data cache with a loop of 0x14s,
often every pass of its main loop: with the ME emulated (on a branch),
mcidclan's me-simple-count went from 15.9 to 1.0 seconds for 3 emulated
ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 21:18:21 +02:00
Henrik RydgårdandClaude Opus 5.5 11296d32b1 CPU: Give each MIPSState an address space for its memory accesses
Memory::AddressSpace is where a CPU's addresses point (base) and its block
shadow, with the unchecked accessors and block shadow helpers as methods. The
main CPU's is Memory::g_mainSpace, kept in step with Memory::base and
blockShadow, and MIPSState::space points at it.

The CPU cores now go through their state's space instead of the globals: the
interpreter's and IR interpreter's loads and stores (which also saves the IR
interpreter reloading the global base), the IR interpreter's block lookup,
the block table's shadow entries, and the native JITs' memory base register,
block shadow lookups and pointerified registers. Address validity and
everything outside the cores still use the globals.

No change for the main CPU; this is so another CPU (the Media Engine) can see
memory differently. Benchmarked against master (God of War: Chains of
Olympus, Tekken 6): JIT instructions retired within noise, IR interpreter
2% fewer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 18:41:28 +02:00
Henrik RydgårdandClaude Opus 5.5 7dd1475ed7 HLE: Pass the MIPSState to syscalls and their implementations
HLEFunc and ReplaceFunc now take the MIPSState they run on. The dispatch
paths (both interpreters and every JIT backend) pass in the state they
belong to, and CallSyscall*, hleFinishSyscall and the HLE mips call
machinery use it instead of currentMIPS.

The wrappers in FunctionWrappers.h take the function as `auto` and call it
through HLECall, which also accepts an implementation with an extra leading
MIPSState * parameter and passes the state to it. Any other signature still
fails to compile, as before. PARAM/RETURN now refer to a local `mips`.

The HLE functions (and replacement functions) that used currentMIPS now take
the state that way. Callers outside a syscall's own frame - CoreTiming
events, the adhoc matching thread, dialogs, PPGe, the debugger UIs - pass
currentMIPS or &mipsr4k explicitly. The kernel's thread internals still use
currentMIPS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 15:45:14 +02:00
Henrik RydgårdandClaude Opus 5.5 53ac2c7c56 IR: Compile kernel loads and stores instead of interpreting them
Kernel code's loads and stores may reach hardware registers, which only the
interpreter handled, so the IR frontend sent every one of them (in kernel
blocks, only run by the VSH boot) to it.

They now become LoadKernel*/StoreKernel* IR ops (one per size, like the
plain loads and stores), which check at run time whether the address is
memory or a hardware register (MMIO). After constant
propagation, ResolveKernelMemory turns the ones at a constant address that's
plain memory into ordinary loads and stores, so only the hardware registers,
and addresses only known at run time, stay checked. The native JITs run the
two ops through their IR fallback. They're barriers, since a register access
has side effects, but can't change the GPRs, so constants stay known across.

The kernel mode check for MMIO moves to the interpreter, the only caller that
can run user code there. TestMMIO covers the new paths and which accesses
stay checked. Under --vsh, the JIT and the interpreter now make the same
unhandled register accesses, in the same numbers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 13:40:47 +02:00
Henrik RydgårdandClaude Opus 5.5 b7b4a265b7 MMIO: Register hardware register blocks as devices
The interpreter's MMIO dispatch was a hardcoded chain for GPIO and the syscon
serial port. Devices now register an address range in Core/HW/MMIO and get
each access with its width and the MIPSState making it, for registers that
answer each CPU differently. An access no device handles is logged and reads
as poison, as before.

GPIO and syscon serial become the first two devices. They're reset at boot,
which they never were, and saved in savestates (SaveStart section version 4).

ll/sc, lwl/lwr and lwc1/swc1 reach MMIO in the interpreter too, and kernel
blocks leave lwc1/swc1 to it like the other loads and stores. swl/swr and
lv/sv still treat it as a bad address. TestMMIO runs a kernel-mode access on
each core and the registers through a savestate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:36:52 +02:00
Henrik RydgårdandClaude Opus 5.5 99335adb40 CPU: Keep COP0 in MIPSState, and interpret eret
The COP0 register file was a static array in the interpreter, shared by
anything that ran kernel code and lost on a savestate load. It's now part of
MIPSState and saved with it (MIPSState section version 5).

eret returns to EPC, or to ErrorEPC with Status.ERL set, as MIPS32 defines
it; that isn't checked against a PSP yet. It and halt carry a new ENDS_BLOCK
flag: the IR frontend ends the block after them and exits to whatever PC the
interpreter left, and the interpreter's IR-matching block timing stops there
too. TestCPUEret runs it on the interpreter, IR interpreter and JIT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:36:52 +02:00
Henrik RydgårdandClaude Opus 5.5 39e6e7b29b IR JITs: Split the IR interpreter out, and keep IR to it
IRJit is now the base the IR JITs share: the frontend, the table of guest
blocks, and compiling and invalidating them. IRInterpreterJit and
IRNativeJit are the two kinds. Only IRInterpreterJit keeps IR, in its arena,
along with its dispatch loop and per-block profile.

The native JITs no longer keep the IR they compiled from. The debugger and
the --jit-profile report compile a native block's IR again when they want it,
on a copy of the frontend's state, which gives what ran unless the code or
that state has changed since.

IRBlockCache and IRBlock become JitBlockTable and JitBlock: where each block
came from, the page lookup for invalidation, and the block shadow entry (an
arena or native code offset, depending on the JIT). One IRJitDebugInterface
serves both JITs, taking the MIPS code from the table and the IR and host code
from the JIT, in place of the native JIT's own debug interface.

The native backends don't see the table at all. They compile a span of IR for
a block number and start PC, and keep what they need about their own blocks
(start PC, code offset, which block starts where, for linking) next to the
exits they already tracked. The register cache takes the instructions
directly.

The tracer's block index now goes into the IR before the block is stored, so
native blocks get it too. The unused FindByCookie goes. TestJit checks that
the native JIT keeps no IR, and can still show it. The JIT stats overlay no
longer shows IR for the native JITs. MIPSState can't be copied, now that it
owns its JIT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:09:16 +02:00
Henrik RydgårdandClaude Opus 5.5 21d3b29965 IR native JITs: Forget an invalidated block's links by their targets
linksTo_ maps an exit's target PC to the blocks exiting there, but
EraseAllLinks erased the entry keyed by the block number instead. The dead
block's exits were cleared anyway, so its stale entries only piled up until
the next cache clear.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:44:06 +02:00
Henrik RydgårdandClaude Opus 5.5 5a9cbf007c CPU: Pass the MIPSState explicitly where it was already at hand
The JIT backends' calls out of generated code (the MIPS and IR interpreter
fallbacks, and the bad address report) now take the state the backend was
built for instead of reading currentMIPS. Likewise the interpreter's tick
check, the IR dispatcher's Advance, and a couple of debugger helpers.

MIPSState::InitRegisters() no longer sets currentMIPS; the boot paths set it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 09:47:48 +02:00
Henrik RydgårdandClaude Opus 5.5 79d340f4fe SaveState: Keep JIT blocks in module code a load doesn't change
Loading a state started the JIT over, so every block the game runs had to
be compiled again, which stutters. Now the loaded modules' code is
compared with the state's as RAM is loaded (a chunk that already matches
isn't copied either), and blocks entirely inside unchanged code stay.
Everything else is invalidated as before, and without a JIT, or with no
unchanged code, the JIT still starts over.

The JIT throws its blocks away if the loaded state needs code they weren't
compiled for (rounding mode switches, or no default prefixes at block
start). The other way round it keeps them, along with the flags they were
compiled under: before, a loaded hasSetRounding of false, or the reset of
lastSetRounding, had the next compile rebuild everything anyway.

In the three seconds after loading a state, Ridge Racer compiles 28 blocks
instead of 4823, Force Unleashed 31 instead of 7727. Frames rendered after
the load are identical either way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 19:47:05 +02:00
Henrik RydgårdandClaude Opus 5.5 ab94767522 VFPU: Drop the inaccurate dot product paths
- Fixes #12082

The bit-exact VFPU dot product is the default everywhere and fast enough,
so the plain multiply-add paths in the interpreter and the IR frontend go,
along with the AccurateVdot setting, --accurate-vdot, the ForceAccurateVdot
compat flag and the Vec4Dot IR op, which nothing emits any more.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 12:11:46 +02:00
Henrik RydgårdandClaude Opus 5.5 b175bc859a IR JIT: Grow the native code space to 24 MB
Big games fill 16 MB in normal play. It's now one constant shared by
every backend. ARM32's 32 MB branch range is the limit on going higher
there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 15:11:47 +02:00
Henrik RydgårdandClaude Opus 5.5 315fd0e490 IR JIT: Cut the IR arena to 4M instructions
The native code space fills long before the arena did: Tekken 6 has it
30% full with the arena at 3%. The old limit was the 24 bits an emuhack
opcode had for the offset, which the block shadow no longer needs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 15:00:47 +02:00
Henrik RydgårdandClaude Opus 5.5 86791f6c3a JIT stats overlay: Count block invalidations
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 14:49:24 +02:00
Henrik RydgårdandClaude Opus 5.5 2b0b1ed561 Add a JIT stats debug overlay
Shows the number of blocks, how full the native code space and the IR
arena are, and how many times each filling up cleared the cache.

The "Ran out of block numbers" message was logged for both of those, and
there is no block number limit. It now says which one filled up.

Also hide the GPU profile and allocator overlays by value on backends
that lack them. Cutting the last two off the list had been hiding the
framebuffer list since it was added after them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 14:49:24 +02:00
Henrik RydgårdandClaude Opus 5.5 6a458bf148 Debugger: Fix breakpoints swallowed or reported twice when resuming
The skip-first marker suppressed everything at the address you resume or
step from. That's needed for the pause, or you could never get off a
breakpoint, but it also dropped the log and hit count. So a log breakpoint
you'd stepped onto, or were parked on for some other reason, never logged
when execution moved on.

Now the skip only drops the pause, unless the breakpoint at that address is
the one that stopped us, which has already logged and counted this
execution. The check moves into the Exec* functions instead of being
repeated in the interpreter, IR interpreter and three JITs, where the
register breakpoint path only applied it to the pause and so logged twice
when stepping off a log+pause one. The marker gets an explicit valid flag,
since address 0 meant "none", and Core_Break() clears it.

Based on the earlier cleanup-skip-first branch, reduced to one marker plus
a flag. Checked over the WebSocket debugger on the interpreter, IR, x86 JIT
and JIT-IR: log breakpoints on a parked and a stepped-onto address each log
once, and stepping off a log+pause one doesn't report it again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 18:16:33 +02:00
Henrik Rydgård d36af22430 Merge pull request #22589 from hrydgard/cpu-timing-determinism
CPU: Make the interpreter and IR cores agree on timing
2026-10-15 22:27:54 +02:00
Henrik RydgårdandClaude Opus 5.5 5631c3c9b6 Interpreter: Continue past branches where the IR cores do
The IR frontend compiles on past a conditional branch that isn't taken,
so the IR cores don't check downcount there, while the interpreter
ended its block at every branch. Events then landed on different
instructions, and a game's trace left the interpreter's within a few
hundred syscalls.

The interpreter now applies the same rule at run time: not likely, the
operands not decided by their registers, no branch in the delay slot,
no prefix pending after the delay slot, and the same instruction and
branch limits, counted the same way. It also goes on past a beql/bnel
over a break, which the IR frontend drops. The limits move to IRInst.h
so both use them.

With it, the interpreter, IR interpreter and ARM64 JIT produce the same
trace of syscalls and thread switches over 5 emulated seconds of God of
War, Tekken 6 and Daxter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 17:43:31 +02:00
Henrik RydgårdandClaude Opus 5.5 6a2fa313e6 CPU: Make the interpreter and IR cores agree on timing
The cores charged cycles differently and checked downcount at different
places, so interrupts and thread switches landed on different
instructions, and a game could take a different path on each core.

- IR: j, jal, bc1t and bc1f now charge their delay slot.
- IR: a syscall sees every earlier instruction charged, including a
  branch it's the delay slot of, and is charged once it returns. It used
  to charge itself first, or, in a delay slot, never.
- Interpreter: a skipped likely delay slot costs what the instruction
  would, as in the IR cores, instead of 1.
- Interpreter: both run loops go on to the end of what the IR cores run
  as a block before checking downcount, and charge each instruction as
  it runs.

The CPUEquivalence fuzzer now compares the cycles up to its terminating
syscall. With continueBranches off, the three cores now produce the same
trace of syscalls and thread switches, ticks included, over several
emulated seconds of commercial games. With it on, the IR cores leave the
interpreter within a few hundred syscalls, as it moves block ends.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 17:43:21 +02:00
Henrik RydgårdandClaude Opus 5.5 ab21def9d6 IR interpreter: Go straight on to the next block at exits
Each block was a call into IRInterpret from RunBlocks: register save and restore, the first
dispatch through the shared switch, then the shadow lookup in RunBlocks. Now the exits look up the
next block themselves and keep dispatching while downcount lasts, the same check RunBlocks makes.
Anything else (a hook, an uncompiled block, an invalid address, downcount gone) still returns.

Without the vertex JIT, as on iOS: 4.5-10% fewer instructions and 1-4% fewer cycles in the games
measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 17:05:16 +02:00
Henrik RydgårdandClaude Opus 5.5 d4a2cd3e43 RISC-V and LoongArch64 IR JITs: Name the fixed code routines, and profile on Linux
Both now list their routines (GetFixedCodeLabels), so the disassembly
names jump targets and --jit-profile charges routine samples to the
calling block. The profiler samples them on Linux, and its report
disassembles their host code per instruction (RISC-V's in 2- and 4-byte
steps).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 15:17:50 +02:00
Henrik RydgårdandClaude Opus 5.5 a80cea2b9e RISC-V and LoongArch64 IR JITs: Vec4 vmin, vmax, vslt, vsge and vi2f
The ops from 108e66f2d1, so both now get moreVec4Ops. With LSX they're
vector code: vfpu_minmax_key() of each lane picked by VBITSEL, a compare
ANDed with 1.0, and VFFINT with a scale. RISC-V and LoongArch without LSX
do them lane by lane, vslt and vsge as a compare into a GPR rather than
through fpcond.

Vec4VfpuMultiDot stays ARM64-only, so the frontend still doesn't emit it
for these.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 15:17:50 +02:00
Henrik RydgårdandClaude Opus 5.5 a6193abcd7 IR: Fix vmmov between transposed matrices whose rows wrap around
For a matrix like E020, the register layout checks pass, but transposing
the register names and taking their columns gives unaligned Vec4s. The IR
interpreter crashed on x86 on the aligned store (vmscl.p M000, M702, S301
then vmmov.q E020, E100). Take the Vec4s from the checked layout instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 14:41:51 +02:00
Henrik RydgårdandClaude Opus 5.5 20063fe526 IR JITs: Name the fixed code routines that jumps go to in the disassembly
DescribeCodePtr() called everything in the fixed code other than a few
routines "fixedCode". It now uses the routine list the profiler has
(moved to the backend as GetAllFixedCodeLabels()), with an offset when
not at the start, so a block ends in "b dispatcherCheckCoreState".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 12:22:35 +02:00
Henrik RydgårdandClaude Opus 5.5 2718010a71 IR: Build a dot operand of one quad's lanes and constants as a vector
vhdp's s, [x y z 1], went lane by lane, splitting the quad apart in the
register cache only to zip it back together for the dot. It's now a
Vec4Init and a Vec4Blend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 11:02:17 +02:00
Henrik RydgårdandClaude Opus 5.5 108e66f2d1 IR: Vec4 vmin, vmax, vslt, vsge and vi2f
New IR ops do them four lanes at a time where the backend has them, the
ARM64 JIT and the IR interpreter so far. vmin and vmax compare by
vfpu_minmax_key() in NEON, which took about twelve instructions a lane
through the integer registers before; vslt and vsge are a compare and an
AND with 1.0.

vi2f becomes one SCVTF with fractional bits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 11:02:17 +02:00
Henrik RydgårdandClaude Opus 5.5 f247df5e4d IR: Compile on past conditional branches
A non-likely conditional branch now exits to its target when taken, and
the block goes on with the not-taken path, up to 8 branches and 96
instructions. Loops written as a few blocks become one with one exit per
iteration, and code around branches gets optimized together. Branches
decided by their operands, like b, still end the block.

Not with a prefix pending, as blocks assume default prefixes on entry
and only the end of a block checks that it leaves them so.

10 emulated seconds, Vulkan, ARM64: cycles -4.6% in God of War, -3.5%
Killzone, -3.0% God Eater, -2.8% Daxter, -2.3% Tekken 6, -2.2% GTA,
-2.1% ATV, about -1% in the rest.

docs/cpu-equivalence.md gets the fuzzer's new classes and what it found
on this branch and in #22576.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 11:02:17 +02:00
Henrik RydgårdandClaude Opus 5.5 533baffb24 IR: Compute runs of VFPU dot products four at a time
A new IR op, Vec4VfpuMultiDot, does four vfpu_dot()s against one vector,
the other operands being the rows or Vec4s of a 4x4 block of registers.
The frontend emits it for vmmul and vtfm/vhtfm, and for runs of vdot that
fit it, like a matrix times a vector written out.

On ARM64 it calls a routine that has the dots in lanes rather than the
terms, so nothing is horizontal and nothing goes through the integer
registers. The block comes in with LD1, or LD4 for the transposed form,
when none of it is in host registers. The IR interpreter runs it through
vfpu_dot(); the other backends don't get it yet.

The CPU fuzzer gets runs of vdot shaped for it, and finds no difference
between the cores on them.

There's a routine per number of terms, so vdot.t runs don't pay for a
fourth. The vector's significands are worked out once and multiplied by
element, and the rounding at the end is UCVTF's in round-to-nearest,
which is what games run in; the integer version stays for the other
modes. The routine saves nothing, and the call site keeps only those
registers it clobbers that hold guest state.

JitDotBench, ns per dot: vtfm4.q 3.40, vmmul.q 3.12, a run of vdot.t
2.83, against 4.6 for single dots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 11:02:17 +02:00
Henrik RydgårdandClaude Opus 5.5 1d5e2689b5 JIT: Add --jit-profile, a sampling profiler for the CPU cores
The native IR JITs get the emulator thread's host PC sampled every 50 us (a
signal on macOS and Linux, SuspendThread on Windows), mapped to blocks, the
routines they call and host functions. The report has each hot block's MIPS,
IR and host code with the samples per host instruction. The IR interpreter's
block timing, previously behind IR_PROFILING, now runs on the same switch.
Both cost nothing when off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 11:01:52 +02:00
Henrik RydgårdandClaude Opus 5.5 9de0f6e45d bltzal and friends test rs from before they link
As on hardware (cpu/cpu_alu/branch_link_ra), with ra as rs the branch
sees its old value, not the return address. Every core linked first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 38322d12ad IR: vmtvc to a masked control register reads vs
It read the VFPU register numbered like the control register instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 8c3f929615 IR: Keep d in vcmov's lanes that don't move under a D prefix
With a D prefix the result goes through temps, and a lane the condition
doesn't move kept whatever the temp held. They now start as d. That
exposed OptimizeFPMoves renaming the src3 of FCmovVfpuCC, which is also
its destination, so it no longer replaces a register an op reads as src3
and writes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 be09c1b25b IR: Compare store offsets as signed in OptimizeLoadsAfterStores
A store at a negative offset whose end wraps past zero, like 16 bytes at
-16, counted as ending before a load at -12, so the load reused an older
value from before the store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 91a0ac7567 IR JITs: Flushing a host register that holds zero unmaps it
FlushNativeReg left a register caching MIPS zero mapped, as there's
nothing to store, but its callers flush to clobber the register. After
the dot product routine's call, a store of zero stored its result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 130a637511 IR: vf2i and vi2f go through temps when the result overlaps the source
They wrote each lane straight to the destination, so with a destination
that holds a later lane's source (a wrapped row over a column, say) that
lane read the converted value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 cc2fd7dcbb IR: Fix the matrix inits of transposed matrices with an offset
Like vmscl, they flipped the transposed bit to get Vec4s, which names
other registers for a matrix that wraps around. Taking the elements in
transposed order does it without.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 289c65d7a2 IR: Don't propagate an FP copy into a result that covers its source
OptimizeFPMoves replaced a temp read by vs2i (from a copy of a lane of
its own destination) with the original register, which the ARM64 JIT
then lost by mapping the destination without loading it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 8ca487910e IR: vc2i and vuc2i read their source at the size encoded
They read one lane, the first of the source vector at the size in the
instruction, as the interpreter does, which for the pair and quad
encodings is another register than the single the IR took.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 480ebbe601 VFPU: vfim converts its half float like vh2f
As on hardware (cpu/vfpu/vfim), a denormal half becomes a signed zero,
and inf and NaN keep their mantissa bits in place. The interpreter got
denormals half their value, the IR the IEEE one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00
Henrik RydgårdandClaude Opus 5.5 a1b845d051 IR: Fix vmscl of two transposed matrices with an offset
Both were turned into their untransposed forms to get Vec4s, but for a
matrix whose offset makes it wrap around, flipping the transposed bit
names other registers. Taking the elements of both in transposed order
gets the same Vec4s without that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:55:35 +02:00