Jit_GetTexelCoordsQuad converted s*w*256 with CVTPS2DQ, which rounds. The C++
reference casts with (int), and the nearest JIT paths use CVTTPS2DQ - the quad
path just never got converted when the others did. The JIT's sample point sat up
to 1/512 texel further along than the interpreter's, so roughly one pixel in
sixteen picked a different frac_u/frac_v, and at exact texel boundaries a
different texel.
That mismatch is visible wherever the two paths coexist: x86-64 desktop runs the
JIT, 32-bit x86 and UWP have no sampler JIT at all, and even within one x86-64
session the first draws with a new SamplerID run the C++ path while later ones
run the JIT. CVTPS2DQ also honors MXCSR's rounding mode, so anything that left a
non-default mode in the render thread would have changed rasterized output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
Expand did `height += ((int)base - (int)newBase) / (stride * bpp)`. The right
operand is unsigned, so when newBase was above base the negative difference
converted to ~4 billion before the division and height wrapped. After that,
HasPendingWrite()'s `start >= base + height * strideBytes` early-out was taken
for every query, so the binner stopped noticing that a draw textures from the
framebuffer it's writing - skipping the flush and leaving maxTasks_ high, which
makes a self-sampling draw depend on which worker thread got there first.
Reachable without exotic state: scissor changes mark BINNER_RANGE dirty without
forcing a flush, so drawing with the scissor top at y=0 and then moving it down
is enough. Handle both directions explicitly instead.
ClearDirty was missing the bounds clamp its twin MarkDirty has. start is masked
to [0, 2047] but bytes isn't bounded - IsVRAMAddress accepts the whole mirrored
8MB window - so a large guest framebuffer near the top of VRAM runs the loop off
the end of vramDirty_[2048] and writes into whatever follows it. Only active with
frameskip enabled, but then it runs every flip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
The jitted GE_LOGIC_NAND ANDed into temp1Reg, which is uninitialized at that
point and whose result is never read - leaving just the NOT, i.e. COPY_INVERTED
rather than ~(new & old). Every bit where new is 1 and old is 0 came out
inverted. It differed between x86-64 (which has the pixel JIT) and ARM64/UWP/x86
(which don't), and even within one x86-64 run, since GetSingleFunc falls back to
the C++ path when it has to queue a compile.
Subtractive blending clamped to 0 on SSE and in the JIT (PSUBUSW) but not on
NEON (vqsubq saturates at INT_MIN) or the generic path. Normally invisible
because ToRGB() clamps at the end - except the caller adds the dither value in
between, so an underflowing dithered pixel came out up to 7/255 brighter on x86
than on ARM64. Clamp in all paths.
stencil << 24 is signed overflow for stencil >= 128, which is the common case,
not an edge case. Cast to u32 - SetPixelStencil already does this correctly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
Rasterizer.cpp: the secondary color fallback does `prim_color[i] = ` where the
SSE and NEON branches add, so the base/texture color is thrown away and the
triangle renders as pure specular. Introduced by 250abe0d56 (Loongarch64 build
fixes) changing one character; the structurally identical block in DrawRectangle
still has the `+=`. Live on ARM32, LoongArch64, RISC-V64 and anything else that
isn't SSE2 or ARM64.
Lighting.cpp: IsLargerThanHalf's scalar path assigns instead of accumulating in
its loop, so it returns only `v[2] > 1` and ignores the other components, and the
NEON path computes a max where SSE computes a sum. All three disagreed. The
question being asked is "is this color factor non-zero" - the test this replaced
in fcc3b7684e was `!(colorFactor == ones)` - and since LightColorFactor produces
2*c+1, every component is >= 1 and the sum of four is >= 4, which is why the SSE
sum > 4 is the correct one. Made the other two match it.
Getting this wrong doesn't shift a shade, it enables or disables a whole light:
x86-32 and ARM64 were switching lights off that x86-64 left on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
LoadZIMPtr() writes width[]/height[]/image[] as arrays (one entry per
mip level, up to ZIM_MAX_MIP_LEVELS) whenever the file has
ZIM_HAS_MIPS set, per its documented contract - but this caller passed
plain scalar locals. A texture-replacement .zim file with that flag
set caused multiple out-of-bounds stack writes. Now passes properly
sized arrays and only uses level 0, matching the existing "we don't
support ZIM mips yet" behavior. Also fixes a pre-existing leak of
image[0] on the "changed since header read" error path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY
Add unittest/TestTextureReplacer which creates a fictive texture pack
(textures.ini with readable invented hashes plus real PNG files), loads
it via the replacer, and verifies lookups, filtering, hashranges, mip
levels, and missing/ignored entries.
To make the replacer runnable outside the emulator:
- The constructor now accepts a null DrawContext (formats just default
to unsupported).
- FindReplacement/FindFiltering use the replaceEnabled_ member instead
of the global config.
- Added TextureReplacer::LoadPackForTesting() to load an ini from a
path directly.
Vuln 17: ReplacedTexture::LoadLevelData let a KTX2/DDS file at a higher
mip level resize the shared data_ vector to its own (attacker-controlled)
mip count, so data_[mipLevel + i] indexed out of bounds and the KTX2
branch resized a different element than it wrote to. Disallow mixing
image formats across mip levels, cap the container mip count, and resize
the same element that is used as the transcode destination.
Vuln 18: DecodeTextureLevel only validated the start address for
non-DXT textures, so guest-controlled w/h/bufw could drive reads past
mapped RAM. Validate the needed range like the DXT path does and clamp
the height; ReadIndexedTex now takes the clamped w/h.
Texture pack filenames/aliases from textures.ini were used to build read
and write paths with no '..' check, letting a malicious pack read or
write files outside the pack directory.
- LoadIniValues rejects entries with a parent dir component via
HasParentDirComponent.
- ReplacedTexture::Prepare skips such filenames as defense in depth.
- PSPLoaders savestate migration now uses the shared HasPathTraversal
helper instead of inline separator checks.
Mirrors the earlier Common extraction. The old "core" target folded in
all of GPU/ (~200 files) plus a few ext/ files wholesale; Windows
already treats GPU as its own project (GPU.vcxproj), so GPU/CMakeLists.txt
splits that out too. GPU has a genuine two-way dependency with Core
(Core/System.cpp calls GPU_Init(), GPU/* calls back into Core for
Memory/Config/CoreTiming/etc), so GPU is a CMake OBJECT library: its
object files are always included wherever consumed instead of being
lazily pulled from an archive, avoiding the GNU ld single-pass
archive-ordering problem a two-way STATIC dependency would hit.
Also fixed a few library misattributions discovered while tracing what
each file actually uses:
- GlslangLibs (glslang/spirv-cross) moved from Core to Common, since
it's Common/GPU/ShaderTranslation.cpp and VulkanContext.cpp that
call into it directly. It only worked before because Core happened
to always be linked after Common.
- ZSTD and OPENGL_LIBRARIES/X11_LIBRARIES moved from Core to GPU,
matching where they're actually called (GPU/Debugger/Record.cpp and
Playback.cpp for ZSTD, GPU/GLES for raw gl*() calls).
- GPU also needs Ext::Snappy directly (Playback.cpp calls
snappy_uncompress) and the libretro-common include dir under
LIBRETRO, both previously inherited for free by accident.
Also fixed USE_DISCORD's add_compile_definitions ordering: it was
being defined after ppsspp_ui's add_library call, so the UI target
never actually saw it on non-MSVC platforms.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU