Commit Graph
9902 Commits
Author SHA1 Message Date
Henrik Rydgård 68fd30bbba Merge pull request #22217 from hrydgard/gpu-fixes
Claude code review: Vulkan
2026-09-04 18:03:47 -06:00
Henrik RydgårdandClaude Opus 5 859d09cc75 Vulkan: Two small fixes
The pipeline debug listing printed the color blend factors in the alpha slot,
which is doubly unhelpful since that branch is only taken when the alpha factors
differ from the defaults.

CompileShaderModuleAsync takes ownership of the tag but only deleted it on the
success path, leaking it whenever GLSLtoSPV failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGz54K3ZXa2Qzyc3aMEYyY
2026-09-04 12:10:03 -06:00
Henrik RydgårdandClaude Opus 5 9273764a3b Vulkan: Don't insert the same key twice when compute pipeline creation fails
The failure branch inserted a null pipeline and then fell through to the normal
insert of the same key, which trips DenseHashMap's duplicate-key assert - and
_assert_msg_ is live in release builds, so a logged error became a crash.

Also skip the null entries when deleting cached pipelines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGz54K3ZXa2Qzyc3aMEYyY
2026-09-04 12:10:03 -06:00
Henrik RydgårdandClaude Opus 5 41d69e61e1 Vulkan: Set USES_DEPTH_STENCIL/USES_BLEND_CONSTANT before creating the pipeline
They were OR-ed into pipelineFlags just after the CreateGraphicsPipeline call that
consumes them, so the render manager's "don't compile a pipeline that requires
depth for a non-depth renderpass type" check could never fire for game pipelines.
thin3d_vulkan.cpp sets the flag before its call, which is the intended order.

Note this can now legitimately skip some variants when loading the shader cache -
those were invalid combinations that the check was written to reject.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGz54K3ZXa2Qzyc3aMEYyY
2026-09-04 12:10:03 -06:00
Henrik RydgårdandClaude Opus 5 38d5ab657e Vulkan: Disable hardware texture scaling if the constant buffer fails to load
If reading the shader's constant buffer file failed, we'd skip writing descriptor
binding 4 but still dispatch the compute shader, which declares it - a statically
used but unwritten descriptor. It also re-read the missing file on every single
texture upload. Now we drop the scaling shaders instead, so following textures
take the CPU scaling path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGz54K3ZXa2Qzyc3aMEYyY
2026-09-04 12:10:03 -06:00
Henrik RydgårdandClaude Opus 5 79a48c7dbe Vulkan: Fix crash when a replaced texture fails to allocate
The out-of-VRAM retry path cleared plan.replaced but left plan.doReplace set.
GetMipSize() dereferences plan.replaced when doReplace is true, so the fallback
crashed instead of recovering. The common code sets both together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGz54K3ZXa2Qzyc3aMEYyY
2026-09-04 12:10:03 -06:00
Henrik Rydgård 425f6e2c37 D3D11: Don't silently draw with a shader that failed to compile
The shader creation helpers returned S_FALSE when compilation produced no
bytecode - but S_FALSE is a success code, so the FAILED() checks in the
D3D11VertexShader/D3D11FragmentShader constructors never fired and failed_ was
never set. Return E_FAIL instead, and only hand out the bytecode when the shader
object was actually created.

Failed() had no callers at all, so a failed shader was handed to the draw as
usual: VSSetShader(nullptr), CreateInputLayout on empty bytecode, and the
ignored HRESULT from SetupDecFmtForDraw meant IASetInputLayout(nullptr). The
draw then did nothing, with no log line to explain it. Skip the draw and warn
instead, like the GL backend does.
2026-09-04 12:08:21 -06:00
Henrik Rydgård 15c702f203 D3D11: Fix texture upload buffer leaks
The cleanup loop was hardcoded to 12 entries while the array holds more and
levels can exceed that with a texture replacement pack - a 8192-pixel
replacement has 14 mip levels, so every level from 12 up leaked on each texture
build. Use ARRAY_SIZE, and shrink the array to 16 since D3D11 caps textures at
16384 pixels anyway.

The out-of-memory bail-out returned before that loop, leaking every level
decoded so far. Since the entry ends up without a texture it gets rebuilt, and
leaks again, every following frame.
2026-09-04 12:07:50 -06:00
Henrik Rydgård 050cacbbdf Merge pull request #22214 from hrydgard/gles-fixes
Claude code review: OpenGL backend
2026-09-04 11:45:42 -06:00
Henrik Rydgård 256984d561 Fix some Claude-isms 2026-09-04 10:41:31 -06:00
Henrik Rydgård ac016201dc GLES: Small cleanups
Scissor the stencil readback to the region actually being read back - latent,
every caller passes a zero origin today.

Remove a DecodeVerts call that can never do anything: both branches above it
have already advanced decodeVertsCounter_ to numDrawVerts_. Worse than useless,
since in the non-skinning branch the vertices went to the push buffer, so
decoded_ doesn't hold them.
2026-09-04 10:41:31 -06:00
Henrik Rydgård 78692deca2 GLES: Set IS_3D when creating the 3D texture, not after uploading it
The out-of-memory bail-out added in the previous commit returned before the
status flag was set, leaving a GL_TEXTURE_3D object bound while ApplyTexture
told the shader generator it was a 2D texture. The entry stays cached, so it
would repeat every frame, not just the one that failed to allocate.
2026-09-04 10:41:31 -06:00
Henrik Rydgård 89ebb8acc6 GLES: Actually apply anisotropic filtering
TextureCacheGLES passed a hardcoded 0.0f instead of key.aniso, so the
Anisotropic Filtering setting did nothing at all on the OpenGL backend, even
though GPU_USE_ANISOTROPY was advertised and D3D11/Vulkan both honor it. Looks
like it was left behind by the 2017 render manager refactor.

The queue runner now clamps to the device maximum it already queried into
maxAnisotropyLevel_ (until now unused), and only touches the parameter when the
extension is actually supported - the anisotropy branch there has been dead
since every caller passed 0.0f, so this is the first time it runs.

0.0f keeps its meaning of "don't care" for the CLUT/fragment-test/thin3d
callers; the texture cache now passes 1.0f when the setting is off, so turning
it off takes effect on already-uploaded textures instead of only new ones.

TexCache: Never use anisotropic filtering for CLUT8-indexed textures

What gets sampled for those is palette indices, depalettized by the shader
afterwards - averaging indices across an anisotropic footprint produces garbage
colors. Affects all backends, not just the GL one that just started honoring
key.aniso.

TexCache: Clear key.aniso wherever filtering is forced to nearest

It was only cleared in the two places inside the AUTO_MAX_QUALITY branch, so the
TEX_FILTER_AUTO path (pixel-mapped textures, the ugly color test heuristic), the
FORCE_NEAREST setting and the replacement-texture override could all end up
requesting nearest filtering with anisotropy still on.

Doing it in the switch that applies forceFiltering covers every path, so it
can't drift apart again.

GLES: Only record the applied anisotropy, and log skipped draws

The queue runner updated tex->anisotropy even when it skipped the call because
the value was 0.0f ("don't care") - harmless while nothing ever set anisotropy,
but now it would make the tracked state disagree with GL, so a later request for
the value it thinks is set would be wrongly skipped.

Also log when a draw is skipped for a missing vertex shader. The failure is
cached per shader ID, so without it geometry silently disappears for the rest of
the session after the one-shot OSD message.
2026-09-04 10:41:15 -06:00
Henrik Rydgård 388eef9d88 GLES: Harden the shader disk cache loader and the 3D texture upload path
The cache loader indexed &vec[0] on vectors that can legitimately be empty (a
header-sized file with zero counts passes both sanity checks), and the counts
are signed ints where only the upper bound was checked - a negative count would
reach resize() as a huge size_t.

The 3D texture branch had the out-of-memory assert but not the bail-out the 2D
branch has, so an ignored assert fell straight into memset(nullptr).
2026-09-03 20:43:06 -06:00
Henrik Rydgård ad131e522f GLES: Handle shader compilation failure in the hardware transform path
ApplyVertexShader can return null - if the requested shader fails to compile it
retries with a software transform ID, and if that fails too it returns (and
caches) null. We then called UseHWTransform() on it.

ApplyFragmentShader can likewise return null, and the hardware path ignored it,
unlike the software path. Without a linked shader nothing binds a program for
this render pass, so the draw would have gone through with whatever program a
previous pass left bound.
2026-09-03 20:43:06 -06:00
Henrik Rydgård 245ed61c0f GLES: Actually apply the stencil write mask in ApplyDrawStateLate
53aa2cc596 changed the first argument from "true" to stencilState_.writeMask,
but that slot is "bool enabled" - the writeMask argument stayed hardcoded to
0xFF, so the mask still never reached GL. The clear-mode call just above gets
the slots right.

Reachable because SoftwareTransformCommon refuses the fast clear path when the
stencil write mask is partial, so exactly those clears end up here.
2026-09-03 20:43:06 -06:00
Henrik Rydgård de0dc2d7d4 Remove the leftover geometry shader scaffolding
Nothing has generated or used a geometry shader since the GS paths were removed
- GeometryShaderGenerator is gone, and ShaderWriter's BeginGSMain/EndGSMain had
no callers at all. Removes ShaderStage::Geometry and everything hanging off it:
the GS preambles and GSMain helpers in ShaderWriter, the stage mappings in all
three thin3d backends, the D3D11 geometry shader plumbing (curGS_, the pipeline
and module members, gs_4_0 compilation), CreateGeometryShaderD3D11, the unused
PipelineFlags::USES_GEOMETRY_SHADER and PipelineManagerVulkan's
UsesGeometryShader().

Also stop enabling the Vulkan geometryShader device feature, since we no longer
have any use for it.

Kept on purpose: the device feature is still listed in the feature dumps (like
other capabilities we don't use), and the Vulkan shader cache header keeps its
now-always-zero geometry shader count so the on-disk format stays compatible.
2026-09-03 11:24:03 -06:00
Henrik Rydgård 293cf0c78c Revert the logic changes from "Add new TexCache logging channel"
This partially reverts commit ee1314f803.
2026-09-02 15:56:10 -06:00
Henrik Rydgård d4b6965db0 TestBoundingBox: bail out when the indices reach past the scratch space
corners and verts are carved out of decoded_ at fixed offsets 6*65536 bytes apart,
and NormalizeVertices fills corners with indexUpperBound - indexLowerBound + 1
SimpleVertex. The vertexCount > 1024 guard doesn't bound that: the index values come
from the game, so 1024 indices can span the full 16-bit range and run corners into
verts, making the cull decision from overwritten data. Bail on an index over 1024
and report visible - a bbox test that large isn't worth doing anyway.
2026-09-02 18:06:47 +02:00
Henrik Rydgård 506cfb6701 Bound two values that texture packs and shader inis supply
A hashrange of 'addr,w,h = 0,0' passed validation (0 isn't bigger than the source),
became desc_.newW/newH, and ReplacedTexture::Prepare divides by them. A post-shader
SSAA level multiplies the render resolution with no upper bound, while the
texture-shader Scale sitting a few lines away is checked against 2..8.
2026-09-02 18:06:47 +02:00
Henrik Rydgård 56bba5f6f5 Merge pull request #22189 from hrydgard/debug-input-rewind-overflows
Fix three more out-of-bounds writes (minor)
2026-08-31 17:53:10 +02:00
Henrik Rydgård 9cb50459d6 Merge pull request #22186 from hrydgard/medium-correctness-fixes
Medium-severity correctness fixes
2026-08-31 16:30:53 +02:00
Henrik Rydgård 1ee9168711 Merge pull request #22184 from hrydgard/fix-adreno-workaround-regression
Fix fragment shader logic error (Adreno stencil driver bug workaround)
2026-08-31 13:30:19 +02:00
Henrik Rydgård 496eddb7fb Merge pull request #22185 from hrydgard/framebuffer-and-null-deref-fixes
Framebuffer and null deref fixes
2026-08-31 13:29:40 +02:00
Henrik Rydgård 7604ae67ff FramebufferManagerCommon: don't deref dstRect.vfb when no dst buffer was found
A failed FindTransferFramebuffer leaves dstRect zero-initialized, and RASTER_COLOR is
0, so the channel check passes and vfb is read through a null pointer. Only reachable
for a depth-source transfer to an address with no matching framebuffer.
Check dstBuffer first, like every other use of dstRect.vfb below.
2026-08-31 13:02:53 +02:00
Henrik Rydgård a99ab6f6f6 TextureReplacer: reject traversal in the override ini name, fix dangling vfs_
The per-game override filename from [games] went straight to LoadFromVFS with no
check, while the [hashes] filenames and ReplacedTexture::Prepare both run theirs
through HasParentDirComponent. For a directory-backed pack DirectoryReader resolves
it against the pack directory, so '../../..' reads anything on disk - and texture
packs are third-party downloads. Check it the same way. (Zip-backed packs weren't
affected.)

Turning replacement off mid-session did 'delete vfs_; vfs_ = nullptr;' without
updating the cached ReplacedTextures that hold the same pointer - LoadIni has a fixup
loop for exactly this when it swaps the VFS, and the disable path needed one too.
Decimate(ALL) right after doesn't help: it only frees their data, it doesn't erase
the entries. A texture still PENDING (or one whose try_lock in Decimate failed) then
used the freed VFS from a worker thread, or from ~ReplacedTexture's ReleaseFile.
Clear the back-pointers, and make the destructor and Prepare() tolerate a null one.
2026-08-31 12:58:47 +02:00
Henrik Rydgård 838cf82e04 Fix fragment shader logic error (Adreno stencil driver bug workaround)
This regressed recently. 0eede05f5f / f8b153ba2b
2026-08-31 12:41:18 +02:00
Henrik Rydgård 9b896e0f7d Merge pull request #22173 from hrydgard/range-validation-fixes
Claude code review: Fix a batch of missing or wrong range validation
2026-08-31 12:32:26 +02:00
Henrik Rydgård 98e8ffe7cd Check framebuffer copy sources, and fix two easy crashes
The three framebuffer upload paths took Memory::GetPointerUnchecked() on a
GE-supplied source address and then read height rows of it, without ever checking
that span was mapped. Only the destination was validated (and DoBlockTransfer's own
memcpy is carefully guarded, so the intent was clearly there). A copy whose source
starts near the end of RAM walks straight off the end of the view. Clamp the row
count to what's actually mapped, and warn when we do.

GhidraClient dereferenced getArray()->value for both "symbols" and "types" without a
null check, and the getTag() test underneath could never catch it - getArray() has
already filtered by tag, so it returns either a JSON_ARRAY node or nullptr. Any
HTTP 200 that parses as JSON but isn't the shape we expect - {}, a bare array, an
incompatible ghidra-rest-api, or the host/port pointed at some other JSON service -
crashed the worker thread. FetchTypes() runs first, so that's the one you'd hit.

RiscV and LoongArch CPU detection divided TotalLogicalCount() by ProcessorCount()
before checking it. ProcessorCount() returns 0 whenever /proc/cpuinfo can't be read
or doesn't parse, which is SIGFPE during static init of the cpu_info global - before
anything could handle it. The existing <= 0 guard sat after the division.

314 pspautotests pass; frametests show the same 3 pre-existing failures as master.
2026-08-31 12:15:50 +02:00
Henrik RydgårdandClaude Opus 5 016f976b1f Fix a batch of missing or wrong range validation
Memory::IsValidAddress and friends tested the extended-RAM range with
(address & 0x3F000000), i.e. at 16MB granularity, so they accepted the whole 16MB
block containing the end of RAM. That's harmless at 32MB and 64MB, but the Sora no
Kiseki SC/3rd HD remasters run with 0x04C00000, so addresses from 0x0CC00000 to
0x0CFFFFFF read as valid, and MaxSizeAtAddress then underflowed to ~4GB there -
which defeats ClampValidSizeAt and IsValidRange entirely for that window. Mask
with 0x3FFFFFFF instead, in all five helpers and the copies in MemMapFunctions.cpp.

IsValidTextureAddress's extended-RAM branch repeated the first branch's whole mask
rather than just its alignment bits, so it was dead code and extended RAM was never
accepted as a texture source.

ComputeTextureHash checked IsValidAddress(addr + sizeInRAM), i.e. only the end
address, which can land in a different valid region than the start - a VRAM texture
with a large enough computed size ends exactly at the base of RAM and "passes"
while reading far past the 8MB VRAM view. Use IsValidRange.

TextureReplacer::ComputeHash's strided path had no range check at all, unlike the
contiguous path right above it. Also clamp the pack-supplied reduce-hash factor to
1.0 - it's a reduction, and the ini parser only rejects exactly 0.

ZipExtractFileToMemory read an uninitialized zip_stat when zip_stat_index failed
(it ignored the return value) and sized a host allocation directly from the zip's
declared uncompressed size. Reached just by opening an archive.

Memory::Reinit ignored Init()'s return value, and DoState fed it a memory size
taken straight from the savestate. A bogus size made the map fail to allocate and
left base null, after which DoMemoryVoid wrote RAM through it. Validate the size,
propagate the failure, and roll back to the previous size if reinit fails.

314 pspautotests pass, all unit tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 23:05:04 +02:00
Henrik RydgårdandClaude Opus 5 9f4dcf359b Fix three more out-of-bounds writes
GetCurrentDrawAsDebugVertices (GE debugger vertex preview) sized its index scratch
buffer at a fixed 65536 and then ran both expanding steps into it: index generation
turns strips/fans into up to 3 indices per input index, and RunSoftwareTransform can
then expand points/lines/rects into 6 more each. A 30000-vertex triangle strip wrote
~90000 entries. Size the buffer from the count instead.

The Expand{Rectangles,Lines,Points} capacity checks were also off: they compared the
expansion against indsSize but write the expanded indices at inds + vertexCount, so
the input count has to be part of the sum.

ControlMapper::Axis wrote rawAxisValue_[axis.axisId] with no bounds check, one line
below an explicit check on axis.deviceId. axisId comes straight from the device -
Android reports AXIS_GENERIC_13..16 as 44..47, against a 44-entry array - so it wrote
into the neighbouring deviceTimestamps_. NativeAxis had the same unchecked write into
HLEPlugins::PluginDataAxis, where it goes out of the object entirely.

Rewind's LockedDecompress computed its copy-from-base block size as
base.size() - result.size() in size_t and truncated to int, so it went negative once
the output grew past the base, and insert() then ran with last < first. That happens
because a state can outlive the base it was compressed against: there are 20 states
but only 2 bases, rotated every 16 saves. Track a generation per base and refuse to
decode a state whose base is gone, and bound the block size against the base itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 22:45:50 +02:00
Henrik RydgårdandClaude Opus 5 7a13881245 DepthRaster: fix host buffer overflows in the depth raster queue
CalculateDepthDraw guarded depthVertexCount_ with vertexCount, which is the
index count - but depthVertexCount_ grows by the number of decoded vertices,
which for an indexed draw can be far larger. Two draws with 6 indices spanning
40000 vertices each therefore passed the check and wrote ~700KB past the end of
depthTransformed_. It also never bounded depthIndexCount_ against depthIndices_
at all, which only has room for 3 indices per vertex slot while draws routinely
produce more. Pass the decoded count in separately and check both.

DepthRasterClipIndexedTriangles duplicated culling-disabled triangles twice: once
in the collect loop (added for Syphon Filter, #21498) and again in the output
stage, which was the older code and should have been removed then. So it emitted
four triangles per input triangle into buffers sized for one, and did twice the
rasterization work it needed to in that mode. Removed the output-stage copy, and
gave the function the output capacity so it stops when full - even at 2x, a
culling-disabled draw over ~32k indices doesn't fit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 22:33:01 +02:00
Henrik Rydgård 4d26523ba8 Note the Kitten Cannon oversized-texture case
Homebrew "Kitten Cannon" hits the bad-dimensions path with a clearly invalid
512x32768 texture, likely a noise bit in the texture size command.
2026-08-29 11:42:47 +02:00
Henrik Rydgård 46d5fae342 VSH prep: Free up kernel memory by not storing the PPGe atlas in it 2026-08-19 18:45:52 +02:00
Henrik Rydgård 14338534e7 Improve some memory address checking functions 2026-08-19 18:40:01 +02:00
Henrik Rydgård cd5cf87120 Fix performance bug in IRJit when using rewind states 2026-08-18 14:31:13 +02:00
Henrik Rydgård fbfcb8925e Merge pull request #22099 from n00mkrad/softgpu-dither-opt
Allow disabling dithering when using software rendering
2026-08-17 12:47:57 +02:00
nmkd 8e9a8b8d76 Allow disabling dithering when using software rendering 2026-08-16 15:27:41 +02:00
Henrik Rydgård 5cffcd34ad Get rid of the confusing old USING_WIN_UI define. Make a more clear system property for headless. 2026-08-16 12:19:41 +02:00
Henrik Rydgård 2096179dce Drive-by code cleanup 2026-08-15 18:31:20 +02:00
Henrik Rydgård c5e4d0d90d Rename the get-memory-pointer functions to make it clear where CPU exceptions can happen. 2026-08-12 14:06:16 +02:00
Henrik Rydgård e9a3449ede More MIPSState * plumbing (manual) 2026-08-12 14:02:19 +02:00
Henrik Rydgård 4d8a5d74e7 Merge pull request #22086 from hrydgard/read-u32-more
Some fixes to Claude's paranoia, more memory access function cleanup
2026-08-11 23:52:17 +02:00
Henrik Rydgård e062c90bf7 Try to fix the test difference (basically by replicating an old misfeature of headless...) 2026-08-11 22:36:47 +02:00
Henrik Rydgård a44c2c0bde Replace System_SendDebugOutput with a registered callback
I normally try to avoid registrations when not needed, but in this case
only headless uses this, so it's motivated.
2026-08-11 22:36:47 +02:00
Henrik Rydgård 5198317e24 Back out some excessive checking in the latest changes. Change TOOD: to TODO: . 2026-08-11 22:27:44 +02:00
Henrik Rydgård 0596ee97f6 More memory access cleanup 2026-08-11 20:14:01 +02:00
Henrik Rydgård 4f8859d0ec Centralize a disassembly utility function between the debuggers 2026-08-11 09:08:52 +02:00
Henrik Rydgård 2be4d995f2 More Read_U32 cleanup 2026-08-10 11:23:23 +02:00
Henrik RydgårdandClaude Sonnet 5 b322b0621c ReplacedTexture: fix stack OOB write from ZIM mip array contract
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
2026-08-09 19:31:02 +02:00