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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
FakeJit was the native JIT for architectures without one, but the config
already forces those onto the IR interpreter, so CreateNativeJit() now
returns that instead. The vertex decoder fallback no longer needs
FakeEmitter.
JitBlockCache was the block cache of the pre-IR JITs. Its header keeps
the debug interface types the IR block caches use. Also removes the
JitInterface link hooks only it called, and the JitState fields tied to
it. This fixes a null dereference in the ImDebugger memory section,
which read GetBlockCache(), always null with the IR JITs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A JIT only switched modes around syscalls, and only took the mode into account
in conversions, once it had compiled a ctc1 (hasSetRounding). A mode from an
interpreted ctc1, a savestate made on another core, a core switch or a thread
switch went unnoticed, and HLE then ran in the guest's mode. UpdateFCR31()
now sets the flag when fcr31 isn't the default, and clears the block cache at
the next safe point, since blocks compiled before don't switch either. A new
JIT gets an UpdateFCR31() too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It only applied fcr31's rounding mode and flush-to-zero after a ctc1 in
the same block, so add.s, mul.s, cvt and friends rounded to nearest in
every other block. It's now applied once per time slice in RunLoopUntil,
where the native dispatchers do it, and the host's mode is put back
around CoreTiming, compiling, syscalls and replacement functions.
See docs/cpu-equivalence.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Memory no longer holds the hooks, so the disassembly fetches the op the CPU runs, which is the
CallRepl pseudo-op at a hooked address. That lookup can come from the Win32 debugger's UI thread,
so the CPU thread now changes the hook map under a lock (and reads it without, being the only
writer).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
78c75e1a0b dropped IRNativeJit::InvalidateCacheAt as a duplicate, and with it the only call to
the backend's InvalidateBlock. Since then a block linked to an invalidated one kept jumping into
its old code, past the dispatcher. Call it from IRJit::InvalidateCacheAt through a virtual, like
the other native block hooks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes#19349. Strange that the game worked at all.
The issue is that the arena could get resized due to precompile during
sceKernelModuleLoad . Now respect the flag that turns precompile off
properly (we should probably make it work differently instead, if we're
gonna have it).
* Rename LogType to Log
* Explicitly use the Log:: enum when logging. Allows for autocomplete when editing.
* Mac/ARM64 buildfix
* Do the same with the hle result log macros
* Rename the log names to mixed case while at it.
* iOS buildfix
* Qt buildfix attempt, ARM32 buildfix