The four-pixel bilinear sampler read its sixteen texels one at a time,
each with its CLUT lookup and index transform, and loaded them as
vectors afterwards. For CLUT4 and CLUT8 with a 32-bit palette and no
index shift (by far the most common), and for 8888, the texels are now
gathered in lanes: the index bytes, the nibble, the mask and start, then
the palette entries. CrossSIMD gets Vec4S32::Gather and GatherU8 for it,
one load per lane. LocoRoco executes 5.5% fewer instructions, Burnout 3%.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SSE2 has no 32-bit lane multiply, so Vec4S32::Mul is emulated with two
pmuludq and shuffles, and the bilinear sampler spent a tenth of its time
there. The texel row offsets, the texture functions, fog and blending
only multiply values that fit 16 bits, which Mul16 does in one pmaddwd.
The bilinear lerp of two channels packed as 16-bit halves uses the new
MulHalves (pmullw), which multiplies both at once.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GEUVSpan stored the four 64-bit plane values, and the reciprocal table's
entries, to arrays one by one and loaded them as vectors, a store
forwarding stall each time, which took most of its time on x86.
CrossSIMD gets LoadS64Low (the low halves of four 64-bit values) and
LoadPairs (four pairs of 32-bit values split into two vectors) to do it
without memory. Wipeout runs about 2% faster.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ReadVertex took the GE's viewport arithmetic one coordinate at a time,
with a reciprocal of w for each, and stored the position, the UVs and
the whole vertex in pieces that were then reloaded with wider loads,
each a store forwarding stall. x, y and z now go through one GERecip and
the four-lane multiply and add (the scalar path stays for non-finite
input and zero or denormal w), the position is loaded as a vector, and
the vertex is written straight into its destination.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fallback (which RISC-V uses) did its integer arithmetic in plain int,
where signed overflow and left shifts of negative values are undefined,
and converted out of range floats with a cast, which is undefined too:
RISC-V gives INT_MAX for NaN where SSE gives INT_MIN and NEON 0. With GCC
on RISC-V that broke GEUVSpanCore in the GEMath unit test. Now the
arithmetic wraps through uint32_t, like the SIMD instructions, and an out
of range or NaN conversion gives INT_MIN, like SSE.
Also skip GERowSum's zero terms before computing their shift, as their
exponent can be INT_MIN. Found with -fsanitize=undefined on the fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CheckDepthTestPassed4 only has an SSE path, so on ARM it compared lane by
lane. Adds Vec4S32::LoadU16 for the depth values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the four pixels share their mip level (always, for a triangle's
span), their texel coordinates, clamping or wrapping and texel offsets
are computed in vector lanes, and only the texel and palette reads go
one by one. Adds Vec4S32::Min.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GEDot3 forms one term per lane and sums across the lanes, about 1.5-2x
as fast as the scalar GEDot. Adds Vec4S32::GetLane.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
They're truncated to float24 with one AND, and their exponents, fractions
and signs worked out for all terms at once, each term then broadcasting its
lane (new CrossSIMD Vec4S32::SplatLane). The term count becomes a template
parameter so the lanes are constants. About 75-83 M transforms/s on an M1,
from 68; results unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The four clip space rows of a vertex, the texture matrix's three, the world
normal's three, the vector to a light, and each row of the combined matrix
now take one GERowSum4 call. Same results (pspautotests, frametests and
the probe regression set unchanged).
GERowSum4 scales its result by adding to the exponent instead of
multiplying, and CrossSIMD gets a Vec4S32::Max for the largest lsb. About
68 M transforms/s against 26 for four scalar GERowSums on an M1 (a pure
NEON version with per-lane shifts did about 79); replaying the frametest
dumps gets only about 1% faster, up to 11% on Taiko no Tatsujin.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MulAdd is fused on ARM64 and LoongArch, which always have it. Rounding to
nearest is a single instruction on every ISA except ARM32.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RecipSqrt is exact (sqrt and divide) except on ARM32, which has no vector
divide and refines vrsqrte with two Newton steps.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
LoadConvertU8, StoreConvertToU8 and LoadTranspose existed in the SSE2,
NEON and LSX implementations but not in the scalar one, so anything using
them wouldn't build on a target without SIMD.
The two LSX bugs were found by the new CrossSIMD unit test, run under
qemu-loongarch64:
- Vec4F32::operator[] had a switch with no breaks, so every index fell
through to the default and returned lane 3.
- StoreConvertToU8 narrowed with the logical (unsigned) saturating shifts,
which turn a negative value into a huge unsigned one and saturate it to
255. It should clamp to 0, as the packs/packus pair in the SSE version
does. Narrow signed->signed and then signed->unsigned instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The riscv64 cross build is the first target to take the non-SIMD path,
and it didn't compile:
- LoadF24x4 called LoadR24x3_One, which doesn't exist. It should shift
all four lanes, like the SSE/NEON/LSX versions do.
- isnan/isinf were unqualified, and <cmath> wasn't included.
- WithLane3From and AnyCompareBitsSet were missing entirely.
LoadF24x3_One also left lane 3 as zero, where all three SIMD versions set
it to 1.0f - a behavioural bug that would only have shown up once someone
ran this path.
Verified by flipping TEST_FALLBACK in the header: the whole tree builds,
and with the scalar path in use the unit tests and pspautotests both pass
in full (342/342, software renderer, which leans on this code heavily).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
initPostfixExpression() filled a fixed char subStr[256] with no bound
check while tokenizing numeric literals and identifiers. A 256+ char
token in a debugger expression (breakpoint condition, watch,
memory.search) - settable over the network via the WebSocket debugger
- smashed the stack. Now bails out with an error once the token
reaches the buffer size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
This enables a behavior seen on the real PSP where 0 * NaN == 0 in the
GPU (NOTE: This means the vertex transform pipeline).
However, this
doesn't touch INFs unfortunately, and we need to modify those too...
This, together with #21715, finally fixes#20204 .
gcc9 and the version of msvc19 that the libretro builders use don't
have _mm_storeu_si32. Using _m_cvtsi128_si32 followed by memcpy is
picked up by compilers that support _mm_storeu_si32 as being the same
and they generate the same assembly, so this is no harm to them.