Commit Graph
45 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 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 ed25f9c436 ME: Let mcidclan's MIST restart Sony's ME firmware and run tasks
MIST (and psp-media-engine-safe-task, and NJEMU through it) restart the
ME through me_wrapper's bootstrap at 0xBFC00040, after patching it to
point a0 at main RAM, which makes the firmware take its resume-from-
sleep path: restore eDRAM from a0 and go back to the main loop. They
then put a task's address in the firmware's command table with
DMACplus and send a command with the soft interrupt.

- The boot now leaves a bootstrap with 6.61's layout at 0xBFC00040,
  and a stand-in for me_wrapper's ME stub (which isn't loaded under
  HLE) that runs the image.
- The firmware never sets Status.IE: it gates interrupts with mtic
  alone, so either one enables them now. Its main loop enables them for
  only a few instructions, so mtic takes a pending one right away.
- Opcode 0x2C, which the resume path runs, is a no-op on the ME.
- Sysreg 0x00 reads 0 (the bootstrap's debug handler check), 0x70 is
  kept, and the DDR flush register (0xBD000004) is always done.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 12:06:12 +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 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 9f07f1d0c0 CPU: Ask the MIPSState whether it's the Media Engine
MIPSState::IsMediaEngine() forwards to its address space, so callers
don't have to reach through mips->space for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 09:33:25 +02:00
Henrik RydgårdandClaude Opus 5.5 fef84cf0ca CPU: Add the Media Engine core, off by default
MeCore owns a second MIPSState in the ME's address space, held in reset
until released (SetReset). It runs on the interpreter, a slice behind the
main CPU: CoreTiming runs it for the cycles the main CPU has just used, in
Advance and ForceCheck. Its own PRId (COP0 22) and mfic/mtic flag; MMIO
counts as kernel mode on it, and a syscall stops it with an error rather
than reaching HLE. The interpreter's block stepping reads delay slots
through the stepping CPU's memory, and cache instructions log at verbose
level, as ME code flushes its caches in a loop.

Created at boot only for homebrew (PBP with MG category or no SFO, ELF;
the VSH counts, as vshmain.prx loads as an ELF), when "Enable ME
emulation for homebrew" is on: a global checkbox in developer settings,
off by default and not translated yet, or --me-core. Saved in its own
section after MMIO (SaveStart v5), with its own memory; loading a state
creates or removes the ME to match.

The boot page now starts with a reset stub that sends the ME on to
0xBFC00040, where homebrew puts its code (from the firmware's reset code,
read on a PSP-1000).

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 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 ad40eec12e CPU: cfc0 and ctc0
COP0's control registers, which the firmware's exception code (and
ProTrekkr's Media Engine stub, clearing $14 and $15) keeps state in: 32
words next to cop0 in MIPSState, saved from section version 6.
Interpreted, as the other COP0 instructions are.

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 9b1399b789 CPU: eret is checked on a PSP now
cpu/cop0/eret, recorded on firmware 6.61, has eret go to ErrorEPC and clear
only ERL with Status.ERL set, otherwise to EPC clearing EXL, with no delay
slot. Int_Eret already does that; the test joins tests_good.

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 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 a3c4ef8e06 CPU: Let each MIPSState own its JIT
The global MIPSComp::jit becomes MIPSState::jit, created and deleted by the
state it runs. JitAt() compiles with the JIT of the state it's handed, so
the dispatchers no longer go through a global. Code that is about the main
CPU by nature (the UI, the crash handler, the disassembler's symbol lookup,
the JIT harness) uses mipsr4k.jit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 09:52:17 +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 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 4849ef020e Decode the unused bits of shifts and Allegrex bit ops as the PSP does
srl/srlv rotate when the low bit of their unused field (rs or sa) is
set and shift when it's clear, whatever the other bits hold
(cpu/cpu_alu/shift_encodings). The interpreter treated values above 1 as
invalid and the other cores shifted.

The Allegrex bit ops decode only some bits of sa, so all 32 values are
one of wsbh, wsbw, seb, seh and bitrev (cpu/cpu_alu/allegrex_encodings).
AllegrexBitOp() gives the canonical one for every core. Special3
function 0x18 does nothing on the PSP, where it was decoded as an alias
of 0x20.

The interpreter dispatch is regenerated, and its generator now lists
each instruction name once. CPUEquivalence no longer skips the srl/srlv
encodings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:56:48 +02:00
Henrik RydgårdandClaude Opus 5.5 4631c7b2f3 Save the host's FPU mode on apply, so restoring it is just a write
RestoreHostRoundingMode read the control register back to clear our bits, even
when nothing had been applied. Now ApplyHostRoundingMode saves the host's mode
the first time it changes it, and restore writes that back only if it did. With
the guest in the default mode, both are now just a flag check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:26:28 +02:00
Henrik RydgårdandClaude Opus 5.5 4b7baaf629 ARM32: Flush denormal inputs to float-to-int conversions under FS
ARM32's libm ceilf/floorf don't honor flush-to-zero, so the interpreters flush
the input themselves there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:26:28 +02:00
Henrik RydgårdandClaude Opus 5.5 5fa4a475f9 Rename the replacement pseudo-op, drop the leftover emuhack defines
With block entries out of memory, the only fake opcode left is the one the CPU cores fetch in
place of a hooked instruction. Call it MIPS_CALL_REPLACEMENT_OP, and its handlers
Int_CallReplacement and Dis_CallReplacement, and remove the sub-op defines and the unused
MIPS_IS_REPLACEMENT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:33:30 +02:00
Henrik RydgårdandClaude Opus 5.5 6d122f416e Keep JIT block entries and replacement hooks out of PSP memory
The JITs used to find a block by overwriting its first instruction with an "emuhack" holding
the offset into the code space, and replacement functions installed their own emuhack the same
way. Anything that read code (savestates, the debugger, memory dumps, cheats, the function
hasher, games reading their own code) had to translate them back.

Blocks now live in a block shadow: one u32 per guest instruction, indexed by the PC masked with
BLOCK_SHADOW_MASK (1GB on 64-bit, folding the kernel and uncached mirrors; 256MB on 32-bit, iOS
and Switch, where address space is short), with the low bits cleared so a misaligned PC can't
read across two entries. Only the ranges under memory views are committed, so a bad PC still
faults in the dispatcher, and the shadow is reserved before the memory map so it can't land in
a gap of the 4GB window. Entries hold full 32-bit offsets, lifting the 16MB limit. The
dispatchers on every backend, and the IR interpreter's loop, look blocks up there.

A hooked instruction without a compiled block gets the JIT's hook value in its entry: the offset
of the dispatcher's compile path (1 for the interpreters, the IR arena keeping that offset
empty), so dispatching there compiles a block, and the frontends substitute the hook. Compiling
replaces the entry with the block's, and destroying the block puts the hook value back. The hook
value is rewritten when the JIT changes or regenerates its dispatcher.

Hooks live in a map keyed by the masked address, and are dropped when the game overwrites the
hooked instruction, or when an icache invalidation finds the function's hash (immediates masked,
as when matching it) changed. Those checks run when queued invalidations are processed, since
cheats and the debugger queue them before writing. Installing and removing hooks invalidates
deferred, as it happens in HLE calls that can run inside a block. The cache instruction goes
through MIPSState, so it drops changed hooks too.

With clean memory, the save/clear/restore around savestates, memory reads and dumps goes away,
as do the source-range invalidations in the memcpy replacements, sceDmac and the cheats.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:33:30 +02:00
Henrik RydgårdandClaude Opus 5.5 feb6caa3c4 Implement mfic and mtic
Both were no-ops, so mfic left its destination unchanged. They read and
write the interrupt enable flag that sceKernelCpuSuspendIntr/ResumeIntr
use. Only bit 0 counts for mtic, which also goes for
sceKernelCpuResumeIntr, since on hardware it's just mtic.

Adds the intr/mfic test, recorded on hardware.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 11:08:13 -06:00
Henrik RydgårdandClaude Fable 5.1 2d14cad065 FPU: sqrt.s of a negative gives the PSP's positive NaN
x86 (SQRTSS and libm alike) returns 0xffc00000 for it, the PSP 0x7fc00000.
-0 stays -0 and a NaN input comes through as it is, so only a negative
input needs the sign cleared: a compare and an xor in the x64 JITs, a
branch in the interpreters. cpu/fpu/roundmode.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 14:25:41 -06:00
Henrik RydgårdandClaude Fable 5.1 fc9dbf5ff5 FPU: saturate float-to-int in the interpreters
The C cast is undefined past the int32 range, and x86 makes it INT_MIN, so
round/trunc/ceil/floor/cvt.w.s of anything from 2^31 up gave 0x80000000 on
x86 hosts while the PSP saturates to 0x7fffffff (cpu/fpu/roundmode). Route
all of them through SaturatedFloatToInt, which also covers NaN and inf, and
drop the special cases that did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 14:25:41 -06:00
Henrik RydgårdandClaude Fable 5.1 e0f09c55d5 MIPS: INT_MIN / -1 leaves HI at zero, like the hardware
Every path special-cased the one signed division overflow and set the
remainder to -1. cpu/cpu_alu/cpu_div, recorded on a PSP, says it's 0.
The classic arm64 JIT was the only one that got it right, by not
special-casing it at all.

arm64, RISC-V and LoongArch all produce INT_MIN with remainder 0 natively,
so their fixup blocks go away. x86 has to keep the check (IDIV traps) but
sets HI to 0 now, and so do both interpreters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 11:02:20 -06:00
Henrik RydgårdandClaude Opus 5 675ad0acd6 Interpreter: assorted fixes found while reviewing Core/MIPS
WriteMMIO_U32 was missing the return after the kernel-mode check, so a
user-mode write raised the exception and then went through anyway. The
other five MMIO accessors already return here; this one lost it when the
GPIO/syscon branches were added.

Int_Vrot scanned all four entries of dregs, but GetVectorRegs only fills
the first n - so a vrot with vs == 0 (S000) matched a lane that isn't
there and took the cosine from it. The IR backend gets this right via
IsOverlapSafe, so the two disagreed.

The breakpoint checks in MIPSInterpret and RunUntilDowncountZeroWithChecks
read instr->flags without checking for null, which MIPSGetInstruction
returns for the eight primary opcodes (and many subops) that don't decode.
With a memcheck or register breakpoint active, landing on one of those
crashed instead of raising ILLEGAL.

Also: GetVectorOverlap decoded the second vector with size1 (no callers
today), and RegisterFunction left most of its AnalyzedFunction
uninitialized, including the size that ends up in knownfuncs.ini.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 10:02:46 -06:00
Henrik RydgårdandClaude Opus 5 bd505b7f1c Interpreter: implement the FPU divide-by-zero exception
div.s now maintains fcr31's Cause.Z and (when the trap is masked) sticky
Flag.Z bits, in the standard MIPS bit positions. Only a finite non-zero
dividend counts, so 0/0, inf/0 and NaN operands are excluded per IEEE 754.

When the guest has the trap unmasked, the new Core_FPUException() reports it
with the usual module suffix and MIPS call stack, and fd is left unwritten as
hardware would. That's gated behind a new developer setting, off by default:
PSP threads start with fcr31 = 0x00000e00, i.e. three of the traps already
enabled, and games divide by zero without meaning anything by it. The fcr31
bits are updated either way, so what the game reads back doesn't depend on
the setting.

Interpreter only - the JITs are unchanged, and none of this is reachable
under them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9fKXvYBrnqtp1QQGaGWVv
2026-08-29 23:40:48 +02:00
Henrik Rydgård 3a1162475b Interpreter: Return 0 on all types of bad memory reads - probably best for IgnoreBadMemoryAccess
But ideally I want to get rid of this at some point.
2026-08-28 14:32:42 +02:00
Henrik RydgårdandClaude Opus 5 d79117146d Interpreter: fix the ins mask when the encoded msb is below pos
ins derived its width as (_SIZE + 1) - pos, which is zero or negative when the
encoded msb is below pos: the following shift is then 32 or more, undefined, and
on x86 produces an all-ones mask that writes bits the JITs don't touch. Build
the mask from msb and shift it down instead, which is what the JITs do and can't
shift out of range. Hardware calls that encoding unpredictable, so consistency
is all that's wanted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 19:01:23 +02:00
Henrik RydgårdandClaude Opus 5 484898385f Interpreter: don't spin forever on a memory access that's set to be ignored
The alignment checks added in d8edeb7649 return out of the instruction handler
without advancing PC, so with IgnoreBadMemAccess - which is the default, and
which makes Core_MemoryException log and return - the run loop comes straight
back to the same instruction and never gets past it. cpu/crash/crash_read_u32
under -i logged the same SIGSEGV 585096 times in 30 seconds before being killed;
the JIT runs it to completion.

Continue instead, the way Memory::Read_U32 did before those checks existed and
the way the JIT's safe-memory path still does: loads produce zero, stores are
dropped, PC advances. When the exception is set to break rather than ignore,
Core_Break has already stopped the core by the time we get here, so nothing
changes for that case.

lv.q/sv.q are left alone - they already fall through and do the access.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 16:43:43 +02:00
Henrik RydgårdandClaude Opus 5 3fa67f22ed Interpreter: honor the guest's FPU rounding mode and flush-to-zero
Every JIT backend puts the host FPU into the mode fcr31 asks for (bits 0-1 and
24) before running emulated code, and takes it back out before calling any host
code. The plain interpreter did none of that, so all its float math rounded to
nearest with denormals intact no matter what the game had set - cpu/fpu/fpu
fails under -i and passes under the JIT on exactly this.

Move the helpers the IR interpreter already had for this out of IRInterpreter
and into MIPS.cpp as ApplyHostRoundingMode/RestoreHostRoundingMode, and use them
around the interpreter's run loop and single step, restoring around syscalls and
replacement functions, which are host code. ctc1 re-applies immediately, since
the interpreter has no block boundary to defer it to.

round.w.s changes with it: it was floorf(x + 0.5f), which is half-away-from-zero
rather than the half-to-even every JIT produces, and the add would now pick up
the guest's rounding mode on top of that. round_ieee_754 is both correct and
mode-independent, and is what cvt.w.s already used for the same rounding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 16:43:06 +02:00
Henrik Rydgård a810597ac0 Implement MMIO for the JIT (by falling back to the interpreter for load/stores from kernel addresses)
Fixes the VSH in JIT mode (but NOT ir)
2026-08-25 00:22:40 +02:00
Henrik Rydgård ed5c05fb28 VSH boot: load real kd/ kernel driver modules
Extends LoadAndStartVshKernelModules() to load the 11 real kd/*.prx
kernel drivers for --vsh (dmacman, systimer, memlmd_01g,
loadexec_01g, lowio, idstorage, syscon, rtc, wlan, wlanfirm_01g, utility),
ahead of the existing 4 VSH-specific modules.
Only active when g_runningVSH, no effect on normal game boot.

Improve implementations of sceKernelSm1ReferOperations and sceKernelIsIntrContext.

Add some more MMIO stubs (GPIO, SYSCON).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
(cherry picked from commit 7f3168b7df85e47438900016c9ee7d7ef01a0a28)
2026-08-24 10:58:35 +02:00
Henrik Rydgård 4fb5d5965c MMIO: Simplest possible kernel-mode permission check, start work on supporting in JIT 2026-08-21 10:32:22 +02:00
Henrik Rydgård 9f90512ef6 Make instruction cache invalidation (for us, jit cache invalidation) clearer 2026-08-16 16:59:52 +02:00
Henrik Rydgård 67ddf899ba Plumb through the PC value for syscalls, so we can get better diagnostics for unresolved ones. 2026-08-15 19:14:13 +02:00
Henrik Rydgård 0c7f8ca07c Poison unknown MMIO reads, make COP0 Count free-run; rule out both for the break
Two diagnostic experiments while chasing the divide-by-zero break from the
previous commits (see docs/VSHBootInvestigation.md "Attempt 8"/"Attempt 9"):

- Unregistered MMIO reads now return a distinctive poison value
  (0x1337BEEF) instead of 0, so a future trace can tell at a glance when a
  value traces back to an unimplemented register instead of looking like an
  ordinary zero.
- COP0 register 9 (Count) now returns a live CoreTiming-derived value
  instead of a static shadow-array read, matching how real hardware free-
  runs it regardless of software writes.

Neither change altered the reboot.bin free-run's outcome at all (identical
break, same PC) - ruling out both as the source of the zero divisor traced
in the previous commit. Kept anyway: both are straightforwardly more
correct/useful than what was there before, independent of this specific
bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
(cherry picked from commit ac446449d9031b628d7d9c4dbc101638840f8434)
2026-08-14 09:27:21 +02:00
Henrik RydgårdandClaude Sonnet 5 5dbe9bea91 Add dummy interpreter implementations for the basic COP0 instructions
mfc0/mtc0/rdpgpr/mfmc0/wrpgpr had no interpreter execution function at
all - ordinary PSP user-mode code never executes COP0 instructions directly
so nobody needed one. Real kernel-mode boot code does the opposite:
flash0:/reboot.bin's very first instruction is mfc0.

Int_Cop0 (Interpreter.cpp) backs these with a small file-scope shadow
register array - not real COP0 semantics (no interrupts/exceptions/
TLB), just enough to not fault and give plausible
write-then-read-back-same-value behavior for boot code that pokes
Status and other registers. Might later be added to MIPSState.

Also regenerated Core/MIPS/InterpreterDispatch.cpp (`PPSSPPHeadless
--generate-interpreter-dispatch`).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-13 11:34:00 +02:00
Henrik Rydgård 5a8e24f583 Interpreter: Hook up dummy MMIO handlers for future experiments 2026-08-12 14:48:23 +02:00
Henrik Rydgård b10e4ad1b5 Code style improvement, handle unknown instructions with a pseudo CPU exception 2026-08-12 14:02:19 +02:00
Henrik RydgårdandClaude Sonnet 5 ffad0acea1 Plumb an explicit MIPSState *mips through the interpreter's Int_* handlers
All ~82 MIPSInt::Int_* functions (Interpreter.cpp/.h,
InterpreterVFPU.cpp/.h) now take an explicit MIPSState *mips instead
of reaching for the global currentMIPS internally, along with their
file-local helpers (DelayBranchTo, SkipLikely, ApplySwizzleS/T,
ApplyPrefixD/ST, RetainInvalidSwizzleST, EatPrefixes). MIPSInterpretFunc,
Interpret(), ExecInstruction()/InterpreterDispatch.cpp (regenerated),
and RunUntilFast() all thread mips through accordingly.

Deliberately left on currentMIPS for now: MIPSVFPUUtils.cpp's
ReadVector/WriteVector/ReadMatrix/WriteMatrix/VFPURewritePrefix -
these are shared with every JIT backend's compile-time VFPU code, so
parameterizing them would balloon this into a JIT-wide refactor. This
is a partial refactor; that's the next boundary to push on.

Several JIT backends (x86 Jit.cpp, ARM/ArmJit.cpp, ARM64/Arm64Jit.cpp,
x86/X64IRJit.cpp, RiscV/RiscVJit.cpp, LoongArch64/LoongArch64Jit.cpp,
ARM64/Arm64IRJit.cpp) bake the raw interpreter function pointer
directly into JIT-generated machine code as their "fall back to the
interpreter for this one op" mechanism, with only a single argument
register set up for the call. Rather than hand-editing register
allocation across four architectures that can't be build-tested here,
added MIPSInterpretTrampoline(MIPSOpcode op) - a 1-arg wrapper around
MIPSInterpret(currentMIPS, op) - and pointed all 7 such call sites at
it instead, leaving that codegen untouched. Two other call sites
(JitLogMiss, JitBranchLog) were plain C++ calls and just got the
extra argument directly.

Verified (Windows x64): PPSSPPWindows/PPSSPPHeadless/UnitTest all
build clean, 49/49 unit tests pass. `test.py -g --graphics=software`:
interpreter 312/314 (cpu/fpu/fpu is the pre-existing, unrelated
interpreter-vs-JIT denormal difference; gpu/rendertarget/copy passes
standalone, so was cross-test state bleed in the batch run, not a
regression), default JIT 314/314, jit-ir 313/314 (gpu/vertices/morph
is an expected difference from the vertex decoder taking a different
mode with this core change, not a bug).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SKhm9wKEQzRUsx9mTrrtQ
2026-08-12 14:02:19 +02:00
Henrik Rydgård cc90546301 Prep for plumbing the MIPS context pointer into the interpreter. 2026-08-12 11:25:06 +02:00
Henrik RydgårdandClaude Sonnet 5 d718bda178 Rename MIPSInt/MIPSIntVFPU to Interpreter/InterpreterVFPU
Just a file rename (plus updating all build configs and includes) to
give the interpreter source files clearer names. No functional change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SKhm9wKEQzRUsx9mTrrtQ
2026-08-12 10:04:40 +02:00