Commit Graph
3189 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 6d105ae35d ARM32: Start an IR-based JIT
The backend follows the ARM64 one: a dispatcher, a register cache on top of the IR one, and
native exits, syscalls, breakpoints and transfers. Everything else goes to the IR interpreter
for now. Floats are mapped one lane per S register (no SIMD mapping yet), and addresses are
masked, as on every 32-bit build.

The JIT core now uses it on ARM32 in place of the old ArmJit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:32:53 +02:00
Henrik RydgårdandClaude Opus 5.5 a56e5f2f6a ARM64 IR JIT: Keep the address out of SCRATCH1 across flushes
A flush stores pending imms through SCRATCH1, so in a non-constant MemoryCheck, and in
ValidateAddress's bad-address path, the address computed into SCRATCH1 could be replaced by an imm
before IRRunMemCheck or ReportBadAddress saw it. It now waits in SCRATCH2, which flushes don't use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:10:35 +02:00
Henrik RydgårdandClaude Opus 5.5 985440a0c6 IR regcache: Load a dest that's a later lane of a source
When floats are mapped one lane at a time (RISC-V and LoongArch), a NOINIT dest was only upgraded
to a loaded one when a source had the same first reg. In Vec2Pack32To16 vf3, vf2,vf3 the dest is
the source's second lane, so it could be mapped without a load and the op read garbage. Found
with an IR-based ARM32 JIT, which failed vi2s.q C002, C000 in cpu/vfpu/overlap because of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:10:35 +02:00
Henrik RydgårdandClaude Opus 5.5 f27b86c687 VFPU: Don't assert on mfv/mtv past the control registers
The encodings are reachable from guest code, and the CPUEquivalence fuzzer generates them, which
stopped a debug-build "UnitTest all" at the interpreter's assert. Every core already leaves rt
alone for mfv and writes nothing for mtv there (the IR interprets them), so only the asserts go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:10:35 +02:00
Henrik RydgårdandClaude Opus 5.5 66d7975e7f Remove the legacy (non-IR) x86-64 and ARM64 JITs
Their IR-based replacements stay. The JIT core now creates the IR JIT on both, so "JIT using
IR" is the same as "JIT" on every architecture (ARM32 still uses its legacy JIT for both). The
settings no longer offer it, a saved JIT_IR loads as JIT, and the JIT harness and CPUEquivalence
fuzzer drop the duplicate run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 14:39:45 +02:00
Henrik RydgårdandClaude Opus 5.5 48f9752235 ARM JIT: Leave matrix inits with a prefix to the interpreter, like the others
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:29:49 +02:00
Henrik RydgårdandClaude Opus 5.5 ad6f0ceea2 ARM JIT: Fix mtvc masking and vsgn of denormals, and interpret vh2f
mtvc masks the written control register like the other JITs, vsgn tests the
exponent (so a denormal gives 0, as in the interpreter), and the hardware
half-to-float conversion quiets NaNs, so vh2f goes to the interpreter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:29:49 +02:00
Henrik RydgårdandClaude Opus 5.5 0e908f466c ARM JIT: Gate VFPU ops on prefixes like the other JITs, and drop its vasin
The ARM32 JIT only checked for unknown prefixes, so a swizzle naming a lane
past the op's size, or a T prefix on vscl, read the wrong register or
asserted in VMOV. It now uses the ARM64 JIT's gates, and sends what they
refuse to the interpreter.

Its vasin was a polynomial approximation, not the bit-exact function the
other cores use (cpu/vfpu/exact failed 364 lines), so it goes to the
interpreter too, like vsin and vcos already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:29:49 +02:00
Henrik RydgårdandClaude Opus 5.5 d7e449ab75 ARM JIT: Fix ins with msb below pos, ext past bit 31, and constant max/min
ins with msb below pos encoded an invalid BFI instead of doing nothing,
as the interpreter and the other JITs do, and ext with pos + size past 32
an invalid UBFX. max and min of two constants compared them unsigned.
Found by CPUEquivalence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:29:49 +02:00
Henrik RydgårdandClaude Opus 5.5 b025e9f819 VFPU: vcmov with imm3 = 7 tests a CC bit that is always clear
So vcmovt never moves and vcmovf always moves (cpu/vfpu/undefined). The
interpreter did nothing for either, and the JITs took it as imm3 = 6,
one CC bit per lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:59:35 +02:00
Henrik RydgårdandClaude Opus 5.5 1a9e06e2d6 ARM JITs: Write vmfvc's 0 for a control register past the defined ones
The register was mapped without MAP_DIRTY, so the 0 never reached it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:59:35 +02:00
Henrik RydgårdandClaude Opus 5.5 9a08ff39be VFPU: vmin and vmax order zeros and denormals as equal, on every core
On the PSP, zeros and denormals all compare equal, a tie returns the
second operand, and the result keeps its bits (cpu/vfpu/minmax_zero,
minmax_tie, fcr31). The interpreter got the ties and NaNs right but
compared denormals as numbers, and each JIT had its own fast path with
host min/max, which returned the first operand on a tie and flushed or
not depending on the host's mode.

All cores now compare by vfpu_minmax_key(), an integer key that is 0
for a zero exponent and orders the rest by sign and magnitude, and
select the operand by it, without branching on NaN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:59:35 +02:00
Henrik RydgårdandClaude Opus 5.5 9fe0ab593b MIPSTables: Include <algorithm> and <string_view> for the dispatch generator
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:05:42 +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 90510d9e88 ARM JIT: Keep the guest's flush-to-zero in round, ceil and floor
They swap only the rounding mode bits in FPSCR now instead of resetting it,
which kept FZ off, so a denormal input is flushed like on the other hosts
(cpu/fpu/denormal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:38:29 +02:00
Henrik RydgårdandClaude Opus 5.5 8d8948ee37 ARM JIT: round.w.s of NaN, and the guest's rounding mode after conversions
round.w.s gave 0 for a NaN instead of 0x7FFFFFFF like the other
conversions. round.w.s, ceil.w.s and floor.w.s reset FPSCR to
round-to-nearest and left it there, so cvt.w.s and the float ops after
them ignored the guest's mode (cpu/fpu/roundmode).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:38:29 +02:00
Henrik RydgårdandClaude Opus 5.5 b44f61cba4 IR interpreter: Force the host's mode back in RestoreRoundingMode
A native backend can run this op through its generic fallback after its own
asm applied the guest's mode, which RestoreHostRoundingMode() doesn't know
about, so it has to clear the bits even when it didn't save them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:26:29 +02:00
Henrik RydgårdandClaude Opus 5.5 39813ffaf1 JITs: Notice a rounding mode that didn't come from a compiled ctc1
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>
2026-10-14 09:26:28 +02:00
Henrik RydgårdandClaude Opus 5.5 241e781810 JIT: Switch rounding modes around interpreted ctc1 and syscalls only
Interpreted ops run in the guest's mode. A syscall left to the interpreter
(with JitDisable flags, say) is host code and now gets the host's mode, like a
compiled one, and an interpreted ctc1 to fcr31 now takes effect right away:
nothing set hasSetRounding for it, so the mode only reached the host at the
next time slice. Every other interpreted op skips the two calls it used to
make after it. The IR frontend's Comp_Generic, which the IR interpreter and the
IR native backends share, gets the same treatment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:26:28 +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 f0c40e521d IR interpreter: Use the NEON Vec4 paths only on ARM64
ARMv7 NEON always flushes denormals and rounds to nearest, whatever FPSCR
says, so on ARM32 the scalar paths are used, which follow the guest's mode
like everything else.

The Vec4 ops only come from VFPU code, and the VFPU itself always rounds to
nearest and flushes denormals (cpu/vfpu/fcr31, see docs/cpu-equivalence.md),
so the NEON paths were arguably closer to the PSP there. But they disagreed
with every other host and core, and agreement between those comes first.
Running VFPU ops in the VFPU's own mode on every core would get both, and is
still to do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:26:28 +02:00
Henrik RydgårdandClaude Opus 5.5 ff5483a43e ARM32: Apply the guest's rounding mode and flush-to-zero in the interpreters
ApplyHostRoundingMode fell back to fesetround, which can't set FPSCR.FZ,
so the interpreter and IR interpreter didn't flush with FS set as every
other host does. FPSCR has the mode and FZ at the same bits as ARM64's
FPCR. Found with CPUEquivalence on a Galaxy S8+ running the armeabi-v7a
build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:49:00 +02:00
Henrik RydgårdandClaude Opus 5.5 7708f30d08 JIT: Run interpreter fallbacks in the guest's rounding mode
Comp_Generic restored the host's default mode around the interpreter call, so
an instruction the JIT doesn't compile rounded and flushed differently than in
the interpreter. Now the guest's mode stays in place, and is reapplied after the
call in case the instruction changed fcr31.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:49:00 +02:00
Henrik RydgårdandClaude Opus 5.5 f5ca0b7932 x86: Set DAZ along with FTZ for the guest's flush-to-zero mode
With FS set in fcr31, x86 flushed only denormal results (FTZ) while
ARM64's FZ also flushes denormal inputs, so the hosts disagreed whenever
a denormal reached an FPU or VFPU op. With DAZ too, x86 flushes the way
ARM64 does. That matches the PSP's VFPU, which always flushes inputs and
outputs (cpu/vfpu/fcr31); the PSP's FPU flushes only results
(cpu/fpu/denormal), which neither host does now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:49:00 +02:00
Henrik RydgårdandClaude Opus 5.5 5ea015a510 IR interpreter: Apply the guest's rounding mode in the dispatcher
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>
2026-10-14 08:49:00 +02:00
Henrik RydgårdandClaude Opus 5.5 bdc62d9ea2 LoongArch IR JIT: Check Vec4Scale/Vec4Dot overlap before mapping
The reg cache asserts when one reg is mapped as a vector and as a scalar, so
the overlap check has to come first, like the other ops do it. Found by
CPUEquivalence under qemu on CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:11:59 +02:00
Henrik RydgårdandClaude Opus 5.5 d712974701 JIT: Leave vidt.s and vidt.t to the interpreter
None of the legacy JITs handled them: the destination was mapped without
being written, so it kept whatever the host register held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 484ea34fe0 MIPSAnalyst: Only writes to the CC register clobber the VFPU condition
mtv and vmtvc to other control registers counted as replacing CC, and vcmp
below .q, which keeps the bits of the lanes it doesn't compare, didn't count as
reading it. So a JIT could drop a pending CC write when the register cache
evicted it. Likewise mfv/mfvc of a control register past the defined ones
leaves rt alone, so it doesn't clobber rt either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 9ed4cc2690 x86 JIT: Leave 1 in fpcond after c.cond.s, not the compare's mask
The other cores store 0 or 1. Branches and cfc1 only looked at bit 0, so
the guest couldn't tell, but the state differed between cores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 f4d0c34e83 MIPSAnalyst: Only ctc1/cfc1 of control register 31 touch the FPU condition
ctc1 is flagged as writing fpcond whatever the register, so the x86 JIT's
register cache took fpcond as about to be overwritten and dropped its
dirty value when it needed the host register. A ctc1 to another control
register doesn't write it, and the result of the compare before was lost.
Found by CPUEquivalence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 8568ae45f0 x86-64 IR JIT: Flush RCX and RDX before calls outside Windows
The System V ABI doesn't preserve them either, and the allocator hands
them out, so a helper call (vsin, vrsq and the like) could clobber a
mapped register. CPUEquivalence found it once it ran the IR JIT on
macOS: a k0 pointer in RDX came back as the helper's 1.0f.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 1d61f72a22 ARM64 JIT: Don't write $zero in mfhi and mflo
mfhi/mflo into $zero mapped $zero as a destination and wrote HI or LO
into its host register, so a later instruction reading $zero (found by
CPUEquivalence as maddu/msub) saw that value. The other JITs and the IR
already skip them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 f0ab8a0062 IR native backends: Fix ll into $zero, and sc losing its result
ll into $zero mapped $zero for writing, which asserts. On ARM64 and
x86-64, sc wrote its 0/1 result into rt without marking it dirty, so it
was never stored (RISC-V and LoongArch already did this right).

See docs/cpu-equivalence.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 b5d3152ce3 IR: Don't assume a matrix that isn't a Vec4 is a transposed one
vmmul, vtfm, vmscl, vmmov and the matrix inits took any 4x4 matrix
failing IsMatrixVec4 to be transposed, so one with a row/column offset
(like E723) produced Vec4 ops on unaligned registers: the native IR
backends asserted, the IR interpreter computed garbage. Also, for 1x1
matrices the transposed bit picks a different register, so it can't be
normalized away, and vmmul's fallback never wrote a 1x1 result. And with
every lane masked by the D prefix, the mask blend went into an unaligned
destination.

See docs/cpu-equivalence.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 a166439435 x86 JIT: Gate more VFPU ops on prefixes like the IR, fix sc zero and vmmul crashes
vdot, vhdp, vcmp, vf2i, vh2f, vi2f, vi2x, vocp, vsgn, vcmov, the color
conversions, the matrix inits, vmmov and vmscl now leave the prefixes
they don't handle to the interpreter, with the IR frontend's gates. A T
prefix on vcmov applies to D, which only the interpreter did.

sc into $zero wrote its result to an immediate. vmmul's SIMD path
crashed on vmmul.s and on matrices that wrap around the 4x4 block, which
MatrixWrapsAround() now detects.

See docs/cpu-equivalence.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 c75c885792 ARM64 JIT: Fix asserts and rounding differences found by CPUEquivalence
- ext with pos + size past 32 and ins with msb below pos asserted in
  UBFX/BFI. The results now match the other cores.
- A swizzle naming a lane past the op's size read an uninitialized
  register in ApplyPrefixST. vdot, vcmp, vi2f, vi2x, vocp, vhdp, vcmov
  and the matrix inits now leave odd prefixes to the interpreter, with
  the same gates as the IR frontend.
- vdot, vhdp, vmmul and vqmul used fused multiply-add, which rounds
  differently from every other core. They now multiply and add
  separately, in the interpreter's order.

See docs/cpu-equivalence.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 f7db56628c ARM disassembler: Rewrite, cover everything the emitter can emit, add a test
The old disarm handled the integer instructions but only a fraction of VFP
and NEON, and got several of those wrong. The new one decodes ARMv7-A ARM
state with VFPv4 and NEON, printing UAL with upper case mnemonics.

The new ArmDisasm test runs every ARM emitter function through it, two
examples each. Tools/arm_disasm_check.py compares the test's output with
llvm-mc; they agree on all of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:23:14 +02:00
Henrik RydgårdandClaude Opus 5.5 a361dfc490 Drop unused Config.h and SmallDataConvert.h includes
Config.h is edited about 100 times a year and each edit recompiled 198
files; now 172. SmallDataConvert.h pulls in arm_neon.h and was included by
headers that don't use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:13:42 +02:00
Henrik RydgårdandClaude Opus 5.5 ae68a7e73f Coded Arms: Contagion: Read back the stencil the crosshair checks
The crosshair turns red, and lock-on works, when stencil bit 3 is set at
the centre of the frame, which the game tests by reading the pixel from
VRAM with the CPU (#17936). Hook that and read back the stencil of that
row into the alpha bytes in RAM. The alpha the hardware backends keep
doesn't follow stencil everywhere, so reading color isn't enough.

Adds PerformReadbackStencilToMemory for this (8888 only for now).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 12:15:56 +02:00
Henrik RydgårdandClaude Opus 5.5 1f89313034 SSX On Tour: Recognize the game's memcpy, so its videos show
The game converts each video frame into RAM and copies it into the
framebuffer with its own memcpy, which we didn't recognize. The hardware
backends never saw the copy and kept showing the last frame the GE drew.
Same function in the EU (ULES00192) and Korean (ULKS46038) versions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 09:39:46 +02:00
Henrik RydgårdandClaude Opus 5.5 24ea7c4193 Everybody's Golf: Read back the screen under the loading spinner
The spinner copies the displayed framebuffer under it into the transparent
texels of its frames, which came out as stale VRAM around the disc. Hook it
after it gets the framebuffer and read back the rows it covers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 15:10:34 +02:00
Henrik RydgårdandClaude Opus 5.5 94d943664e Everybody's Golf: Hook the player portrait copy too
The first game builds its portrait like Golf 2 and Tennis, but its copy
function keeps y and h in different registers and loads the framebuffer pointer
at a different offset. Move the readbacks into a helper and add a hook for this
layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 15:06:24 +02:00
Henrik RydgårdandClaude Opus 5.5 6f56798c44 Everybody's Tennis: Hook the player portrait copy too
Tennis builds its portraits like Everybody's Golf 2: it memcpys a 64x64 rect of
the framebuffer and sets each texel's alpha bit from depth read straight from
VRAM, so the circles in the corners came out empty. Its copy function is
scheduled differently and hashes differently, so add it as a second entry for
the same hook, which is renamed to fit both.

Fixes #17826.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 15:00:40 +02:00
Henrik RydgårdandClaude Opus 5.5 648766dad4 Everybody's Golf 2: Read back color and depth for the player portraits
The game builds each 64x64 portrait by memcpying a rect of the framebuffer one
partial row at a time, then setting each texel's alpha bit from the depth
buffer, which it reads directly from VRAM. Neither got read back: the row
copies don't line up with a framebuffer, and depth is never downloaded.

Hook the copy function to read back both, whole rows at a time. That needed two
fixes to requested depth copies: they were dropped when the depth buffer lies
above all color buffers, and only an exact full-buffer size matched.

The portrait texture now matches the software renderer, though it still
doesn't show up on screen with Vulkan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 13:25:10 +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 48973c83c6 Debugger: Show replacement hooks in the disassembly again
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>
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 59fed92dac IR native: Invalidate linked blocks again
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>
2026-10-11 10:33:30 +02:00
Henrik Rydgård b0ea555b79 Fix a couple of problems with recent commits in MSVC debug builds 2026-10-09 17:52:55 +02:00