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>
With LSX, the Vec2 pack and unpack ops went to the interpreter, as they
addressed the lanes of a pair one by one. They're now an interleave or a
pick of halfwords in the pair's register.
That makes pairs, so mapping a quad whose halves are mapped pairs (two
vs2i.s feeding a vi2f.q) joins them with VILVL_D rather than storing both
and loading the quad back, like the ARM64 JIT's ZIP1 (3d20d51c70).
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>
Like the ARM64 JIT (e33f38afa6), each jr and jalr does the dispatcher's
lookup itself, for an indirect branch of its own, and goes to the
dispatcher when out of downcount or with no block yet.
A bad target now faults in the block rather than at dispatchFetch, so the
fault handler takes any fault in the block shadow as a bad jump, with the
masked target from the address. That covers ARM64's too, which was handled
as a bad memory access.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PPSSPP_CPUFUZZ_BLOCKS prints the IR and host code of the blocks the JITs ran in each case, which
is what found the vmv.s.x encoding.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
As on RISC-V: the accurate dot products called the IR interpreter. Now the JIT calls a routine
generated at startup, vfpu_dot_lsx() with LSX (the operands go in v8 and v9) and vfpu_dot_scalar()
without (through mips->vfpuDotArgs), with inf and NaN going to vfpu_dot_reference(). With LSX, a
result that lands in one of the operands' Vec4s goes in with VEXTRINS.
VFPUDotJit covers both versions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The accurate dot products went to the IR interpreter, flushing everything around each one. Now
the JIT stores the operands to mips->vfpuDotArgs and calls a routine generated at startup, which
clobbers only registers late in the allocation order. Inf and NaN go to vfpu_dot_reference().
The routine is vfpu_dot_scalar() in RV64, using Zbb and Zicond where it has them, or with the V
extension, vfpu_dot_sse2() in RVV. The rounding at the end comes from FCVT.S.LU with a static
round-to-nearest-even, which also gives the exponent, so there's no need for Zbb's CLZ.
VFPUDotJit runs each variant the host can run against the reference on VFPUDot's inputs, the RVV
one with poisoned vector registers. PPSSPP_CPUFUZZ_HOSTEXT=- now turns V off too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
They used the OPMVV and OPFVV funct3, which makes them vmv.x.s and vfmv.f.s: the scalar register
was written instead of the vector. Nothing used them until now. Checked against GNU as.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hosts without NEON or SSE2 ran vfpu_dot_reference() for every dot product. vfpu_dot_scalar() is
the same computation without its branches, finished like the SIMD versions, and is what RISC-V
gets. LoongArch builds with LSX (GCC's default la64v1.0 has it) get vfpu_dot_lsx(), the SSE2
version in LSX.
VFPUDot checks vfpu_dot_scalar() on every host, and its inputs come from ForEachVFPUDotTestCase(),
for the JITs' routines to use too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PPSSPP_CPUFUZZ_HOSTEXT=+ turns on the RISC-V extensions that detection only
reports for a few SoCs, and - turns them off (LSX on LoongArch), to test both
paths of the JIT. PPSSPP_CPUFUZZ_UNKNOWNPREFIX compiles the way the IR
frontend does after any block has left a prefix uneaten, which in a run
happens at the first case that does it. Without it, such a later case
doesn't reproduce on its own.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
vsgn takes the sign of s - t, and a NaN's sign counts (cpu/vfpu/specials).
RISC-V's subtraction always makes the positive canonical NaN, so there vsgn
of a negative NaN gave 1 instead of -1, unlike the PSP and the IR cores.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
A fused multiply-add rounds once where the IR interpreter rounds twice, so
vdot, vhdp, vtfm and vmmul gave different low bits. Multiply and add
separately, in the IR interpreter's order. (LoongArch's LSX path already
did, in the pairwise order of the x86 and ARM64 JITs.)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
FMIN/FMAX return the other operand for a NaN, so the saturating ops clamped
a NaN to the bound. They now compare and select like vfpu_clamp(). vcmp NE
used the ordered not-equal, false for a NaN.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
c.lbu and c.sb keep the offset's bit 0 in bit 6 and bit 1 in bit 5, so
offsets 1 and 2 were swapped, and the JIT loaded and stored the wrong byte.
FLI's only NaN is the canonical one, but CanFLI took any NaN for it: the
pattern went through a float to double conversion that quieted it, which on
RISC-V also drops the sign and payload. vfim of a NaN lost its sign.
Neither is detected on real hardware yet (we never set Zcb or Zfa), and the
CPU fuzzer found both with the extensions forced on under qemu.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#22578
Since loads became deferred (#22507), the file is read from the event at the
end of the wait, where there is no current thread and so no current
directory. Crash Tag Team Racing loads its PRXs as "prx/sc_sascore.prx" and
so on, got NOCWD for all of them and never started. Make the path absolute
with the caller's current directory before waiting.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#20420
Each shot's light lasts 0.1s of effect time. At 30fps the 303 assault rifle
fires every 0.1s, and the wall goes dark for a frame per shot. At 60fps it
fires every 1/12s, the lights overlap and the wall stays lit. Hook the flash
effect's constructor and shorten the light to 0.075s, which keeps the 30fps
pattern and leaves one dark frame in five at 60fps.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
The debris smoke trails add a node on every update, so at 60fps the smoke was twice as dense
as at 30fps and spawned twice as many trails. Keep the nodes 1/30 of the effect's clock apart
instead. In the angel boss explosion the display list now peaks at 0x483ec bytes without the
overflow guard's help, against 0x637f4 before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#19755
The game adds smoke trail nodes per update but expires them by time, so the faster we run, the
more trails are alive. After the angel boss in Bonn, the debris trails grow the display list
past its 0x5DC40 byte buffer at 60fps. The overflow lands in the music stream's Atrac buffer
(the "Invalid start bit" errors), corrupts textures, and crashes the game. Locking the CPU to
222MHz, the known workaround, slows the frame rate enough to avoid it.
Hook the trail draw and skip a trail when the current list buffer, whose limit the game keeps
next to its write pointer, is nearly full.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A module the game loaded before the state was saved, but that this run never loaded, was never
scanned, so loading such a state left all of its code without function replacements and hooks
(and without functions in the debugger). Scan any loaded module with no analyzed functions in its
text range before rehashing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Mapping a quad whose halves were in registers as pairs (two vs2i.p
feeding a vi2f.q) flushed both and loaded the quad back, a store
forwarding stall.
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>
Each jr and jalr now does the dispatcher's lookup itself, so each has its
own indirect branch, which the CPU predicts much better than the one in
the dispatcher that every one of them shared. Out of downcount or with no
block yet, it goes to the dispatcher as before.
Cycles over 10 emulated seconds: -2.3% in ATV, -1.4% Tekken 6 and Ridge
Racer, -1.2% Daxter, -1.1% Killzone, and -0.3% to -0.8% in the rest.
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>
Class b turns some instructions into conditional branches to a later
one. Minimizing code with branches makes instructions nops rather than
removing them, so no branch moves its target or lands in a delay slot,
and the reports leave the nops out.
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>
It returned 1 for W, where the SIMD versions return the matrix's fourth
row times the vector, and added in a different order. The depth raster
and the culling test (compared with the old version in a unit test,
which failed on RISC-V) read W.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The triangle is convex, so once a row has been entered and left, the
rest of it is outside. This cut cycles by 5% in Wipeout Pure, where depth
raster is on for the lens flares.
The test checks the covered pixels against a per-pixel edge test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestBoundingBoxFast transformed and tested one vertex per iteration,
with a scalar divide and a min/max chain each vertex had to wait on.
It now transposes four vertices into lanes and does it all as vectors,
with the same operations in the same order, so the results don't change.
It was 16.6% of the emulator thread in Wipeout Pure (with depth raster
off) and 5% in GTA: Vice City Stories.
Per vertex, it's 1.8x faster on an M1, 2x on x86-64, 1.7x on a Galaxy S8+
and 10% faster in 32-bit mode there.
The test compares it with the old version on random draws in every
position and index format.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LoadS8Norm and friends read four bytes through a u32 pointer. On ARM32
the compiler turns that into a NEON load that requires 4-byte alignment,
and 8-bit positions can sit at any byte offset in a vertex, so
TestBoundingBoxFast could crash with SIGBUS (seen in a unit test on an
S8+ in 32-bit mode).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
JIT memory is now mapped with MAP_JIT, and pthread_jit_write_protect_np()
switches the calling thread's view of it, in place of two mprotect() calls
for every block compiled and every exit linked. This makes compiling
cheaper, which matters mostly in the first seconds after a game or state
loads. A hardened runtime without the JIT entitlement refuses the mapping,
and then it's mprotect() as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>