Commit Graph
48357 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 964b5dc00a SoftGPU: Move the GE clip and viewport helpers to GEMath.h
So the hardware backends' software transform can use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 0b4c29c883 Clip and cull in software when the GPU lacks cull distances
Without them nothing culls triangles beyond the far plane: depth clamp draws
them, as with the stands over the pitch in FIFA Soccer on macOS. Fixes FIFA
and Hellboy on OpenGL there, and DBZ Tenkaichi Tag Team, Monster Hunter 3rd
and Street Riders on both backends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 7c5eda25f4 Software transform clipping: split clipped quads like the GE, cull beyond far first
With one vertex behind the near plane, the GE splits the remaining quad from
the vertex before the clipped one (gpu/probe exp43, exp44, as in SoftGPU's
Clipper.cpp). The other diagonal could put a far off-screen near-plane vertex
in both halves, so range culling dropped the whole triangle: the floor in
#16131's box test vanished. The index generator reverses triangles to flip
culling, so the batch's order is passed along.

Triangles entirely beyond the far plane are now culled even when in front of
the near plane, with the cull-distance epsilon, so z/w = 1.0000172 (exactly
1.0 to the GE) still counts as inside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 03c5c4e115 Don't write fragment depth when the depth test is effectively off
Nothing writes depth then, so the shader write is wasted. On Apple's OpenGL
it also broke Tales of Phantasia (PixelDepthRounding): once the shader writes
gl_FragDepth, a discarded fragment still writes stencil, so a color-tested
stencil pass marked the whole area and the background tiles drawn with
stencil EQUAL 0 vanished.

New FS ID bit, so both shader caches get a version bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 709e6729ad GLES: Enable the clip planes the vertex shader actually writes
The vertex shader puts min/max Z in clip distances 0 and 1 and the near plane
in 2, but the program flags still followed an older layout and never enabled
plane 2. Geometry crossing the near plane was then only depth clamped, and
smeared across the screen (Hellboy, FIFA Soccer, Hoto no Ken frame dumps).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik Rydgård d74f16ef51 Merge pull request #22330 from hrydgard/android-render-thread-lifecycle
Claude code review: Android render thread lifecycle fixes
2026-10-08 15:17:30 +02:00
Henrik Rydgård c8814f545f Merge pull request #22449 from hrydgard/stencil-write-mask
Shader write mask: Remove the compat flag (after tweaking it), improve emulation
2026-10-08 13:10:10 +02:00
Henrik Rydgård b5c030db31 Merge pull request #22448 from hrydgard/gedump-id-from-name
Read a frame dump's game ID after a leading issue number in its name
2026-10-08 13:05:36 +02:00
Henrik RydgårdandClaude Opus 5.5 ec8a590c81 Read a frame dump's game ID after a leading issue number in its name
Dumps before version 4 don't store the game ID, so it's taken from the file
name, which PPSSPP writes as ULUS10064_0001.ppdmp. The ppsspp-framedumps
collection prefixes the GitHub issue number ("11928 ULUS10064_0001 OutRun
water.ppdmp"); skip that, so those dumps get their compat settings too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:33:09 +02:00
Henrik RydgårdandClaude Opus 5.5 d0e6174389 Apply the pixel masks to clears in the hardware backends
On the PSP the color and alpha/stencil write masks apply to clears like to
drawing (gpu/stencil/writemask, ppsspp-re geprobe exp254). Clear-mode draws
now take the same per-channel heuristic as other draws, and a clear with RGB
masked is drawn rather than done as a hardware clear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:42:27 +02:00
Henrik RydgårdandClaude Opus 5.5 21bf2b878e Add gpu/stencil/writemask to the test list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:28:40 +02:00
Henrik RydgårdandClaude Opus 5.5 feaa41fadb Always emulate partial color write masks in the shader, drop ShaderColorBitmask
Partial red, green and blue write masks now always go through the shader with
a framebuffer read. Across the frame dump set this only improves games outside
the old list (Split/Second, Ultimate Ghosts 'n Goblins, older DiRT 2 and OutRun
dumps without a game ID). Partial alpha masks keep the old heuristic: alpha is
the stencil, with its own write mask, and taking the shader path for them made
Pro Yakyu Spirits and Gangs of London worse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:18:05 +02:00
Henrik Rydgård 8e596032f1 Merge pull request #22446 from hrydgard/misc-rendering-fixes
Misc rendering fixes
2026-10-08 11:16:14 +02:00
Henrik RydgårdandClaude Opus 5.5 2f9f9bf77e Compat: ShaderColorBitmask for Ultimate Ghosts 'n Goblins
Its glow sets the 5551 alpha through a 565 draw with only the top blue bit
unmasked. Without the flag the whole blue channel was written, tinting the
glow; 25276 -> 4274 pixels off the PSP in issue #20728's dump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:46:54 +02:00
Henrik RydgårdandClaude Opus 5.5 89fabe21fb Rebuild the stencil from alpha after reinterpreting a framebuffer
On the PSP the stencil is the alpha bits, so after the colors of another
format are reinterpreted into a 5551, 4444 or 8888 buffer, its stencil has to
follow the new alpha. Ultimate Ghosts 'n Goblins sets 5551 alpha with a masked
565 blue write and then stencil tests on it; Vulkan drew its glow through a
stale stencil. The memory stencil upload's pipeline and passes are shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:45:57 +02:00
Henrik RydgårdandClaude Opus 5.5 d717a38a34 Tools: Add image_compare.py, comparing renderers' screenshots past rasterizer differences
A pixel counts as different only if it's outside the range of the other
image's 3x3 neighborhood (widened by a tolerance) and enough of its neighbors
are too, so edges a pixel off, filtering rounding and speckle drop out.
8x8 block averages catch broad shifts separately, and heatmaps show where.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 10:36:19 +02:00
Henrik RydgårdandClaude Opus 5.5 f22d9298f6 Headless: Convert hardware depth like ReadbackDepthbuffer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:19 +02:00
Henrik RydgårdandClaude Opus 5.5 6c04b9f068 Headless: Save the depth buffer from the hardware renderers too
--depth-save converts their float depth (z / 65536) to the PSP's 16-bit
values, or writes the unrounded values when the path ends in .f32.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:19 +02:00
Henrik RydgårdandClaude Opus 5.5 bcfb4daa78 Depth readback: Scale by 65536, and decode in full precision
The vertex shader stores depth as z / 65536, so reading it back with 65535
came out one too low whenever z's fraction was below z / 65536. Both the CPU
conversion and the shader path (used on GLES and at higher render
resolutions) had it. The shader also ran at the fragment shader's default
lowp, which mobile GPUs honor; 24 bits of depth need highp, sampler included
on GLES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:19 +02:00
Henrik RydgårdandClaude Opus 5.5 c2ce8d1f2e OpenGL: No hardware logic ops on Apple GPUs
Apple's OpenGL runs on Metal, which has no logic ops, and ignores glLogicOp.
Emulate them in the shader like MoltenVK does, which fixes OutRun's water
(without the BlueToAlpha compat flag) on macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:18 +02:00
Henrik RydgårdandClaude Opus 5.5 b40c8a0197 TextureCLUTInShader: Build the CLUT texture from the raw CLUT
The GLES texture cache keeps 16-bit CLUTs in clutBuf_ converted to its own
component order, and GetClutTexture converted them again, so Fushigi no
Dungeon 4's tiles came out black on OpenGL. The framebuffer depal path
already passed clutBufRaw_.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:18 +02:00
Henrik RydgårdandClaude Opus 5.5 e30d67946a OpenGL: Don't scissor framebuffer blits
glBlitFramebuffer obeys the scissor test, which stays enabled between render
passes with the last pass's rectangle. Resizing a framebuffer after drawing to
a small one copied only that corner, so FF Type-0's battle screen turned into
the red overlay. Disable it around the blit and restore it.

Also fixes Assassin's Creed Bloodlines, Split/Second, Super Stardust's bloom
and MotorStorm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:18 +02:00
Henrik RydgårdandClaude Opus 5.5 a66e94c015 Vulkan: Don't merge an empty clear pass past a copy into the framebuffer
PreprocessSteps moved the clear of an empty clear pass into the next render
pass on the same framebuffer, stopping only at copies out of it. Resizing a
framebuffer clears the new one, copies the old contents in, then renders, so
the clear landed after the copy and wiped it. Stop at any copy, blit or
readback that touches the framebuffer.

Fixes black or missing layers in Asphalt Urban GT 2, FF Type-0, Street Riders,
Kurohyou, Split/Second and Super Stardust.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:18 +02:00
Henrik RydgårdandClaude Opus 5.5 0b79588838 Use shader depal for CLUT8 from an 8888 framebuffer also when rendering to it
The separate depal pass reads whole pixels, not the byte each texel picks, so
Star Wars: The Force Unleashed's color grade turned magenta. Vulkan and
OpenGL now match each other and the PSP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:18 +02:00
Henrik Rydgård 13c1773bc2 Merge pull request #22444 from sum2012/saving
Fix Unbound saga saving issue
2026-10-08 04:50:37 +02:00
sum2012 bef8e2d9ad Fix Unbound saga saving issue
Find bug in #13111
2026-10-08 06:14:08 +08:00
Henrik RydgårdandClaude Fable 5.1 5b677d93a1 Android: Don't send the launch parameter to the native side twice
On a cold start, onCreate passed the intent's shortcut parameter to
NativeApp.init and then also posted it as a "shortcutParam" message, which
is only meant for when the native side is already up. For a game shortcut
the second boot request was rejected with an error. For the Args extra,
the raw command line got booted as if it was a game path, so something
like --loglevel=4 applied fine and then popped up a load failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 00:09:09 +02:00
Henrik RydgårdandClaude Fable 5.1 3cb6ada66a Android GL: Don't run stale GL commands when joining the emu thread
EmuThread_Join drains the render queue on the calling thread. On Android
that is either the UI thread with no GL context (NativeApp.shutdown), or
the GL thread with a brand new context after the old one was lost
(displayInit the second time). The skip flag was only set once the emu
thread reached NativeShutdownGraphics, so everything queued before that
ran for real, against nothing or against the wrong context.

Add GraphicsContext::NotifyContextLost() and call it before the join in
both places. skipGLCalls_ is now atomic, since it's set from one thread
and read from another.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 00:09:09 +02:00
Henrik RydgårdandClaude Fable 5.1 7641514084 Android: Join the Vulkan render thread when the surface is destroyed
The surface may not be used after surfaceDestroyed returns. We relied on
onPause having joined the thread first, which is the usual order but not a
guaranteed one. When it didn't hold, the thread kept spinning on a lost
surface, and the replacement surface was refused as "Already running".

Also forget a surface deferred while paused if it gets destroyed before
the resume that would have used it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 00:09:09 +02:00
Henrik RydgårdandClaude Fable 5.1 2c68ce0001 Android: Recover from a Vulkan render thread that exited by itself
If InitSurface failed, the thread returned but stayed joinable, so the next
surface was refused as "Already running" until the next pause. Tell a live
thread from a finished one with renderLoopRunning, and reap the latter.

Also make VulkanGraphicsContext::InitSurface clean up when it fails after
creating the draw context. Callers don't call ShutdownSurface on failure,
so draw_ (with its render manager) and the surface leaked, and the next
attempt overwrote the pointer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 00:09:08 +02:00
Henrik RydgårdandClaude Fable 5.1 99f65012f9 Android: Always join the Vulkan render thread on an exit request
requestExitVulkanRenderLoop went by renderLoopRunning, which the thread
only set once it got going. A pause landing between thread creation and
that point returned without signalling or joining, leaving a render thread
running while paused, and every later start refused as "Already running".

Go by whether there is a thread to join instead, set the running flag
before the thread is created, and let the requester reset exitRenderLoop
after the join rather than the thread itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 00:08:19 +02:00
Henrik Rydgård 79cf8592f4 Merge pull request #22442 from Narugakuruga/patch-39
Update pause text in Chinese localization
2026-10-07 13:42:13 -06:00
Henrik Rydgård 1e70b4dedb Merge pull request #22443 from sum2012/AMD-Patch
Fix Hot Pixel titlescreen problem For AMD GPU  by Zcode
2026-10-07 06:20:58 -06:00
sum2012 27400295fe Fix Hot Pixel titlescreen problem For AMD by Zcode
Fix Hot Pixel titlescreen problem For AMD (#15826)

AMD required PixelDepthRounding for this game
2026-10-07 19:03:45 +08:00
Narugakuruga f995206cd3 Update pause text in Chinese localization
Fix a mistake in Pause / Pause (No menu). 
Reverse the phrase to Pause (Open menu) / Pause the Image to reflect Chinese sentence logic
2026-10-07 18:51:15 +08:00
Henrik Rydgård 3c6c7f548b Merge pull request #22433 from hrydgard/softgpu-threading
SoftGPU: Improve threading
2026-10-06 08:09:17 -06:00
Henrik Rydgård adda3ab1de Merge pull request #22434 from hrydgard/softgpu-nan-vertex
SoftGPU: Treat a NaN screen coordinate as outside the range
2026-10-05 21:49:34 -06:00
Henrik Rydgård a26160f66e Merge pull request #22431 from hrydgard/safe-path-component
Validate untrusted IDs before using them as directory names
2026-10-05 20:17:08 -06:00
Henrik RydgårdandClaude Opus 5.5 6e5258f570 SoftGPU: Declare the wake chain as u64 for LeastSignificantSetBit
Linux's uint64_t is unsigned long, which neither of its u32 and u64
(unsigned long long) overloads takes exactly, so the call was ambiguous.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:28:21 -06:00
Henrik RydgårdandClaude Opus 5.5 f949fd5187 SoftGPU: Treat a NaN screen coordinate as outside the range
The range check was written as tests for outside, which are all false for
NaN, so a NaN vertex passed and was converted to an integer. That differs
by CPU: arm64 gives 0, which lands off screen, while x86-64 gives INT_MIN,
which wraps, and the triangle's area overflowed into a huge sliver. In the
recording-opengl and recording-vulkan dumps that drew a beam across the
top right that a PSP replay of the dump doesn't show. Now such a vertex is
flagged, and its triangles dropped, on both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:09:55 -06:00
Henrik RydgårdandClaude Opus 5.5 aa39b6460a SoftGPU: Grow the state ring when it fills
A full state ring flushed the binner, and the next state change filled
it again in the same way. Now the flush also doubles the ring, up to
32768 states, so it happens at most a few times. Nothing draws right
after a flush, so the ring can move then. In the benchmark games, no
more than 512 states were queued between flushes, so 4096 stays the
starting size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 9d842d74af SoftGPU: Track whether anything was queued since a flush with a flag
The binner kept a bounding box of every queued item's range, updated for
each item, only to tell whether anything had been queued since the last
flush. A flag says the same. Expand, which grew the box, is now
ItemQueued.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 f015bd111b SoftGPU: Don't change a state the threads may be drawing with
Each queued primitive adds its vertex flags (alpha, fog, colors) to its
state, and states are optimized for those flags before their items are
distributed. A state can still gain primitives after some of its items
are in tiles. Re-optimizing it then changed its pixel and sampler
functions and IDs while threads drew with them. With the x86 pixel JIT,
the span function could turn null between checking and calling it, as
in Coded Arms' frame dump. God of War hit this about four times a frame.

Now a state is live once any of its items is in a tile, until the tiles
are drawn and reset, and a live state isn't changed. A primitive that
would change a live state's flags starts a new, unoptimized state from
the same registers instead, optimized for its own primitives only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 8f699056f9 SoftGPU: Don't flush for JIT compiles where there's no JIT
The pixel and sampler JITs only exist on x86-64, but elsewhere a cache
miss still flushed the binner before the compile that did nothing, and
the cache never filled. So on ARM every switch to a new pixel or sampler
state was a full flush: hundreds per frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 2db66333d4 Tools: Add headless_bench.py and dump_compare.py
headless_bench.py compares the speed of headless binaries on savestates
or frame dumps. It alternates the binaries between rounds and reports
medians of wall time, CPU time, and instructions and cycles where the OS
counts them. Instructions vary by well under 0.1% between identical runs,
where wall time swings by several percent. It checks for mains power
first, and stops on a run that failed or didn't get where it should.

dump_compare.py replays every frame dump in a directory with two binaries
and reports the dumps that render differently, or differently from run
to run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00
Henrik RydgårdandClaude Opus 5.5 15d0972f86 SoftGPU: Fix merging pending write ranges that start lower in the buffer
Expanding a pending range by a rectangle starting below its start added
a negative row count to an unsigned height, which wrapped to millions of
rows. Overlap tests then overflowed and missed real overlaps. Now a
merged range is whole rows from the lower start to the higher end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00
Henrik RydgårdandClaude Opus 5.5 c6c953ff47 SoftGPU: Order texturing from pending drawing per primitive, not by flushing
A draw whose texture rectangle overlapped what queued primitives write
flushed, once per draw. NeedsOrder already orders each primitive by the
texels it actually reads, so let it. God of War's bloom passes texture
from the main framebuffer declared as 512x512, which reaches into the
buffer they draw to, and flushed every 30 primitives: 6% faster there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00
Henrik RydgårdandClaude Opus 5.5 4bec735331 SoftGPU: Pick the tile size from the thread count
Every tile a triangle touches sets it up again, so larger tiles do less
of that. The tile size is now picked at startup, as large as it can be
with at least sixteen tiles per thread over the screen. With fewer, a
scene whose drawing is concentrated on part of the screen leaves threads
idle: on eight threads, 128x32 tiles (four per thread) took LocoRoco a
third longer than 64x16, and God of War 5% longer. So eight threads keep
64x16, and four or fewer get larger tiles.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00
Henrik RydgårdandClaude Opus 5.5 3c3025a087 SoftGPU: Keep drawing queued when a display list stalls
Returning to the CPU flushed all queued drawing, also when a list only reached its stall address.
Wipeout Pure builds its lists as it goes and stalls about a hundred times a frame, so the main thread
spent a third of its time finishing the drawing at those flushes. A stalled list isn't done, so now
only a finished one flushes there, which makes Wipeout Pure 15% faster.

What relied on that flush now flushes itself: memory copies, sets, readbacks and uploads that overlap
the queued drawing, and save states, before memory is saved or loaded (FlushPendingDrawing). Syncs and
the display already did. Queued drawing still reads textures from PSP memory when it runs, so a game
that rewrites a texture between stalls without syncing would see the new one; on hardware the GE has
usually drawn with the old one by then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00
Henrik RydgårdandClaude Opus 5.5 1228837688 SoftGPU: Wake only threads with tiles to draw, and chain the wakes
Waking a worker is a condition variable signal, a system call, and the main thread paid one per idle
worker every 48 queued items. With Tekken 6's tiny draws (about 5 vertices each) the workers keep
running dry, and those signals were about 10% of the main thread, which is what limits it there.

Now no more threads are woken than tiles have work, and only the first is woken from the main thread;
it wakes the rest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00