The PSP's add.s, sub.s, mul.s and div.s make 0x7FC00000 from operands
that aren't NaN (cpu/fpu/fpu_nan), as ARM64 does. x86 makes 0xFFC00000,
which only mul.s corrected, so the hosts disagreed on inf - inf and 0/0.
A NaN operand still comes through as the host passes it.
The x86 JITs check the result and branch past the fix unless it's a NaN.
They compare the operands up front only when the dest is one of them,
which also makes mul.s cheaper in the common case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VFPU never keeps a NaN's payload (cpu/vfpu/nan_gen): vadd and vsub
produce 0x7F800001, and vmul, vdiv, vscl and vcrs the same with the
sign of s ^ t. The cores returned whatever NaN the host made, which
differs between x86 and ARM64. Now all four cores on both hosts match
the PSP for these ops. The horizontal ops (vdot and friends) are left
for when they move to the accurate dot product.
The IR gets FVfpuNaN and FVfpuProductNaN, scalar and Vec4, run after
the arithmetic. The native backends check for a NaN and branch past
the fixup, which only runs when there is one. PurgeTemps no longer
renames the dest of an FPR op that also reads it, which the product
ops are the first to do.
vsat0 and vsat1 also keep a NaN's bits now. The ARM64 JITs quieted a
signaling NaN through FMIN/FMAX, and the x86-64 IR JIT turned NaN into
0 in vsat0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the single precision 4S form, which the next commit uses to check
four lanes for a NaN at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
Merges the settings in an ini file over headless's own defaults, for runs that
need something like JitDisableFlags. Unlike in the app, the result is never
saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Mali-G71 and PowerVR GE8320 Vulkan lack it, and without it WipEout Pure,
Juiced 2 and 16131 UCES00001 render like they do there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
Rewrites most of DisArm64.cpp. Previously missing: test-and-branch,
exceptions other than brk, hints and barriers, adc/sbc, conditional
compare, extr, literal loads, exclusives, unscaled and FP register
offset loads, ldpsw, SIMD structure loads/stores, scalar pairwise,
fixed-point conversions, fccmp, most two-reg misc ops, modified
immediates, smov/umov/dup from GPR, and the scalar shift forms.
Fixed: sp printed as x31, unsigned load offsets sign-extended, ldrsb and
friends shown as stores, adrp ignoring the page, double precision
1-source ops shown as single, fcmpe shown as fcmp, smulh/umulh, the
bitmask decode for 64-bit elements, and fpcr shown as fpsr. Common
aliases are used now (cset, mul, tst, cmn, lsl/asr/ubfx, sxtw...).
TestArm64Disasm runs two examples of every emitter function through it.
The expected strings were verified with Tools/arm64_disasm_check.py,
which compares all 833 instruction words against llvm-mc.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Vulkan: Batch Jackass' tiled in-place color grade
Jackass: The Game color grades the framebuffer in 32x64 tiles, one channel
per pass, texturing from the framebuffer it renders to. Each of the 225
draws became a depal pass plus a pass drawing it back, 450 render passes
per frame. A new queue hack, enabled by the TiledDepalHack compat flag,
sorts these pairs into waves of non-overlapping tiles and runs each wave
as one depal pass and one draw pass, leaving 6.
Output is byte-identical to before on both dumps from the issue, at 1x
and 3x on desktop, and at 1x on both phones below.
Time saved per post-processed frame, from replaying the dumps from the
issue with headless --bench (each replay renders that frame once):
Redmi 9A (PowerVR GE8320): ~145ms at 1x, ~180ms at 2x
Unisoc UMS9632 (Mali-G57): ~55ms at 1x and 2x
Either is more than a whole 33ms frame, so on those GPUs this pass alone
ruled out 30fps.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Gradle build goes through CMake and so gets _LIBCPP_REMOVE_TRANSITIVE_INCLUDES,
which CI's ndk-build Android jobs don't.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--disable-gpu-features=clip-distance,cull-distance,depth-clamp acts as if the
GPU lacked those, to test the fallbacks for old Mali on any machine. It
replaces the simulateOldMali constant. --hardware-transform=false runs every
draw through software transform.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VertexDecoderRiscV.cpp got <cfloat> through SmallDataConvert.h, and three
headers used malloc/free without <cstdlib>, which libc++ no longer brings in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
ExtractZipContents checked each archive path for parent components
before removing stripChars. A name could become ../ only after
stripping and escape the install directory, allowing a crafted archive
to overwrite files in sibling PSP directories.
Validate both archive and output paths, and apply stripping consistently
when checking for overwrites.
initNetwork() runs on the friend finder thread and its connect loop only
gave up on power-down or after the timeout. sceNetAdhocctlTerm joins that
thread from the emulation thread, so terminating while the server was
unreachable could freeze emulation for up to adhocDefaultTimeout.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each retry went through ScheduleSaveScreenshot(), which reset the
failure count, so the limit was never reached and a state saved while no
screenshot could be taken kept retrying every frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Save() hands the newest state to a compress thread and returns. A rewind
right after could take lock_ first and decode that slot's old buffer
against the new base, loading a garbage state.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Siri Remote setup builds on iOS too, the device type comes from the
interface idiom, and requests for things the platform says it lacks are
refused by checking the property. What's left ifdeffed is API tvOS doesn't
have at all. Reports say tvOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>