Commit Graph
48378 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 eb59b00a37 GE dump playback: Ignore a second INIT in a dump
Before 71210f3fa2, a second dump request during a recording started it again, writing another
INIT (and initial CLUT) into the dump: the GE state of that moment, which already includes the
register writes the dump only has after it. Playback applied it, so the last draws ran with the
wrong state: GOD EATER BURST (13950) and GOD EATER 2 (6343) came out black and Street Riders
(14746) garbled. The recorder only writes an INIT when it starts, so skip any later one. The 15
such dumps in the GitHub issue set now match the PSP's replay, 10 of them exactly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 a418514376 GE dump playback: Load the initial CLUT, and show the last framebuffer when the display is off
The recorder saves the CLUT the GE had loaded right after INIT, as a CLUT command with no
LOADCLUT to follow, unlike the CLUTs it records at a LOADCLUT. Playback only pointed the CLUT
address at it, so draws before the game's first LOADCLUT used whatever CLUT was loaded before:
HotBrain (16131, ULUS10268) drew its save screen background black. Load it, and put the game's
CLUT address back.

A dump whose last DISPLAY turns the display off (address 0), or has none, now shows the last
framebuffer drawn to (Auditorium 9213). The PSP replayer does both the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 13c1b4407d frametests: Update IL-2's software reference
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 2317ecbfbd GE dump playback: Write recorded memory into the depth mirrors as the PSP does
A dump records a texture at a swizzled VRAM mirror as the game sampled
it, and the PSP's replayer writes it back with the CPU, which the PSP
sends through the depth layout at any access width. PPSSPP's playback
wrote it raw, so with depth stored in the GE's layout the texture came
back permuted: IL-2's depth texture at 0x04710000 and the two Gensou
Suikoden swizzle dumps. Texture copies, memcpys and memsets into a
mirror now go through the layout, 32 aligned bytes at a time. Against
PSP replays: both Suikoden dumps are now exact (from 1912 and 8083
pixels off), Star Wars #15832 is 67 off (from 2590) and IL-2 894 (from
70505, the rest from a write the next draw reads before it lands).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:30 +02:00
Henrik RydgårdandClaude Opus 5.5 ae32477764 Core: Let 16-bit CPU accesses through the depth mirrors see the GE's layout
With the software renderer, which now stores depth as the GE does, a
16-bit read or write at VRAM's 0x04200000 or 0x04600000 mirror goes to
where the GE's 16-bit or 32-bit color layout puts that depth pixel
(ppsspp-re geprobe exp88: the mirrors are the layout's exact inverse).
Games access depth there with nothing else, so other widths stay plain.

The accessors check the address, then whether the GPU core is the
software renderer (CoreParameter); the hardware renderers keep depth plain
and handle the layout their own way. The renderer keeps the layouts up to
date with the EDRAM address translation. With it, the JITs send 16-bit
loads and stores to the interpreter, and the IR passes don't change a
load's width to or from 16 bits.
The pspautotests that read depth through the mirrors now do so 16 bits
at a time, re-recorded on hardware unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:23 +02:00
Henrik RydgårdandClaude Opus 5.5 a11fd22553 SoftGPU: Store depth in VRAM as the GE does
The software renderer kept depth linear in VRAM, where the GE stores it
in the layout GPU/Common/DepthSwizzle.h describes, from the EDRAM address
translation and the color format. So a texture or anything else sharing
VRAM with a depth buffer saw depth land in the wrong places (the player's
shadow in Silent Hill: Origins, #22415). Now every depth access goes
through the layout: the generic pixel functions, the 2x2 depth test, the
depth clear in 16-pixel runs, and the x86 pixel JIT, whose IDs carry the
translation so the layout's constants are compiled in. It changes with
the color format and sceGeEdramSetAddrTranslation, after a flush.

Reading VRAM through its 0x04200000 and 0x04600000 mirrors undoes the
layout, as the GE does: texturing from them (drawn from a linear copy
made after a flush), and block transfers from or to them (Hayate no
Gotoku restores its depth buffer through 0x04600000, #17878). Saved and
debugger depth buffers read through the layout, so they're linear.

Socom's lens flare dump now matches a PSP replay where it changes. About
2-3% more cycles in God of War, Tekken 6, Wipeout and Burnout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:35:48 +02:00
Henrik RydgårdandClaude Opus 5.5 84582c10f1 GPU: Add the GE's depth buffer layout in VRAM
Where the GE stores each depth pixel, from the EDRAM address translation
and the color format: with 32-bit color, address bits 5 up to log2(T) - 1
rotate left by one, then (T << 3) | 0x40 is XORed in (0x600, and bits 9
and 10 swapped, at T 0). The CPU sees the same mapping through VRAM's
0x04200000 and 0x04600000 mirrors. Measured in ppsspp-re geprobe exp8-10
(every pixel of 512x272 buffers at each T) and exp88; the unit test checks
it against the model fitted there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:35:48 +02:00
Henrik Rydgård 8d0cb09a6e Merge pull request #22453 from hrydgard/vulkan-suboptimal-transform
Vulkan: Don't recreate the swapchain over a pre-transform we chose ourselves
2026-10-08 18:10:51 +02:00
Henrik RydgårdandClaude Fable 5.1 7f751c7ba1 Vulkan: Don't recreate the swapchain over a pre-transform we chose ourselves
InitSwapchain deliberately refuses the 270 degree pre-transform. The driver
then reports VK_SUBOPTIMAL_KHR on every present, which we counted towards
recreating the swapchain - and the new one got the same treatment, so in
reverse landscape we rebuilt it every single frame.

Remember that the mismatch is ours and stop treating SUBOPTIMAL as a reason
to recreate when it is. Any other cause still counts.

Split out of the Android rotation work, where it was found: it's a
platform-independent Vulkan fix and stands on its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 17:15:09 +02:00
Henrik Rydgård bd04d064a4 Merge pull request #22452 from hrydgard/hw-render-fixes
OpenGL and Vulkan rendering fixes on MacOS, upgrade MoltenVK, raise iOS/Mac min versions
2026-10-08 16:57:47 +02:00
Henrik Rydgård 5cb2cfd577 Merge pull request #22451 from hrydgard/android-egl-replace-javagl
Android: Replace JavaGL init with EGL
2026-10-08 16:16:55 +02:00
Henrik RydgårdandClaude Opus 5.5 07b2d1d335 iOS: Link StoreKit in IAP builds
It was only linked when the In-App Purchase capability was added by hand in
Xcode, which current Xcode no longer lists. There's no entitlement for it
on iOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 281b18c522 iOS toolchain: Pass IOS_APP_STORE to CMake's compiler checks
They re-read the toolchain file without the cache, so they used the non-App
Store deployment target, which current Xcode no longer supports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 2cfcbcd473 iOS App Store: Update the static MoltenVK to 1.4.2, raise the minimum to iOS 15
1.4.0 and older don't apply dynamic blend constants. The stock 1.4.2 build
needs iOS 15. Non-App Store builds keep their targets and dylib.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 f5fe32c64c Mac: Update the bundled MoltenVK to 1.4.2, raise the macOS minimum to 12.0
1.4.0 never applied dynamic blend constants, so fixed-color blends lost
their constant factor on Vulkan (the Enemy panel in Tales of Phantasia
went dark, also Block Transfer effects). MoltenVK 1.4.2 requires macOS 12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 73b5d2a4a2 Self-texture copy: Include the texel past each edge of the UV bounds
The copy for texturing from the current render target was cropped to the
UV bounds, but bilinear filtering at the edges reads one texel beyond them,
which then came from whatever the temp FBO last held. On Vulkan that put
magenta streaks into Need for Speed Shift's sun glow, and also hit Call of
Duty, Burnout Dominator (#12831) and Assassin's Creed Bloodlines (#15821).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 f1a6232841 Pixel depth rounding: Floor with room for float error, store Z like clears
The fragment Z is the PSP's Z / 65536, and flat draws land exactly on an
integer Z. floor(z * 65535) put that Z - Z/65536 just above the boundary, so
float error (which differs with W) could drop it by one: Patlabor redraws a
HUD icon at the same depth with GEQUAL, and the second draw failed on
Vulkan. Floor Z + 0.01 instead, and store Z / 65535, as clears and the depth
clamp path do. Also fixes Heroes Phantasia #6485.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 b4619757ad Software transform: Check rectangle depth against the range like the GE
A rectangle's depth is flat, so right at a depth range limit our float
rounding drops or keeps the whole thing. Mana Khemia 2 draws a conversation
background at GE screen Z 1 with minz 1, and we computed 0, so it vanished
(#12858). When the GE's depth would land on the other side of the range
check, use it; otherwise keep ours, which matches the hardware-transformed
draws a rectangle may be depth tested against (rounding all rectangle depths
to the GE's broke the Need for Speed HUD).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
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årdandClaude Opus 5 bd94e8141f Android: Converge the GL and Vulkan lifecycles on a shared RunGraphicsLoop
Stage 5 of the JavaGL removal. Now that the GL path owns its own EGL context and
render thread, Android's two lifecycle branches and the Win32 one in
MainThreadFunc were the same code three times over, so this moves the branch into
one place - RunGraphicsLoop() in Core/EmuThread.cpp. It picks the thread
arrangement the context asks for: either this thread becomes the render thread
and spawns the emu thread (OpenGL), or it just runs the main loop because the
backend spawns its own render thread (Vulkan, D3D11). Android, Windows and
headless all go through it now, which is what the "TODO: Merge with the Win32
EmuThread and so on" in app-android.cpp was asking for.

Falls out of having one implementation instead of three:

- MainThreadFunc called graphicsContext->ThreadStart() right after
  EmuThread_Start(), which already calls it, and ThreadEnd() right after
  EmuThread_Join(), which already calls that too. So Windows and headless GL ran
  CreateDeviceObjects()/DestroyDeviceObjects() twice per context, leaking a VAO
  on the way in and double-deleting it on the way out. Dates back to 16206d18ab,
  which moved those calls into EmuThread_Start/Join without removing the
  originals.

- RECREATE_VIEWS is gone. Only the GL path ever posted it; Vulkan goes through
  the same destroy/recreate without it and always has. Verified on device: views
  survive pause/resume, and both a real rotation and a density change - each one
  an activity destroy and recreate - lay them out again by themselves.

- The non-separate path is now named EmuThread rather than MainThread on
  Windows. It's the thread that runs NativeFrame(), which is what
  DebuggerThreading.md calls the emu thread, and the real main thread there is
  the GUI message pump.

The loop keeps an explicit exit flag, as a shouldExit callback, rather than
relying on ThreadFrame() returning false. In VR we skip ThreadFrame() entirely
while the XR session is idle, and the session going idle is exactly what
backgrounding the app does - so the exit would never be observed, the render
thread would sit in that sleep forever, and the UI thread joining it would hang.
That is not hypothetical: it deadlocked a Quest 2 on the first try.

Also drops a dead EmuThreadState enum and an unused JNIEnv local in
app-android.cpp, both orphaned when the JavaGL JNI entry points went away.

Tested on a nubia Z2464N (Mali-G57, Android 15) on both backends: cold start,
booting a homebrew, pause/resume in the menu, in a settings screen and in-game,
rotation back and forth, a density-change activity recreate with the game live,
the pause menu, and exit to menu. ThreadStart/ThreadEnd log exactly once per
context, and every one of those cycles tears the EGL context down and builds a
new one.

Also tested on a Quest 2 with the vr flavor, which is what shook out the deadlock
above: cold start into a focused XR session at 72fps, backgrounding (render
thread joins in 27ms where it used to never return), resuming back into VR with
a fresh EGL context, and exiting from in-app. xrCreateSession runs on the render
loop thread, which is where the EGL context is current - VRBase.cpp builds its
graphics binding from eglGetCurrentDisplay/eglGetCurrentContext, so that ordering
matters.

Windows x64 debug and the Android gradle builds are clean.

Not converged, and not a small change: NeedsSeparateEmuThread() itself. GL needs
its API calls on the thread its context is current on, so somebody has to pump
ThreadFrame() there, whereas VulkanRenderManager owns its thread outright.
Getting rid of the virtual means teaching GLRenderManager to spawn and own its
own render thread, for every GL platform at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:29:12 +02:00
Henrik RydgårdandClaude Opus 5 812d8f4c09 Android GL: Delete the JavaGL path, leaving only native EGL
The EGL path from the previous commit is now the only one, so GLSurfaceView
and everything that existed to serve it are gone:

- NativeRenderer.java and NativeGLSurfaceView.java, deleted.
- The javaGL branches in PpssppActivity. GL and Vulkan now share one
  lifecycle: a plain SurfaceView, startRenderLoopThread() when the surface
  arrives, joinRenderLoopThread() when it goes away.
- The "androidJavaGL" queryConfig, and the AndroidNativeEGL setting and its
  developer-tools checkbox.
- displayInit/displayRender, and with them the "Second time, joining the
  emuthread and starting it up again" dance - restarting the emu thread from
  inside a JNI callback because a lost context was only observable as a
  second onSurfaceCreated.
- GraphicsContext::NotifyContextLost, which existed solely because that join
  drained the render queue on a thread with no current context. The render
  thread now joins the emu thread with the context still current, so the
  drain is real work and nothing has to be skipped.
- g_emuThread, now that nothing starts an emu thread outside the render loop.

Three things displayInit/displayRender did that nothing else did, moved
rather than dropped:

- EnterVR and the per-frame StartVRRender/UpdateVRInput/FinishVRRender hooks
  now live in RenderLoopThread's GL branch. Java's GL thread could simply
  return early and wait for the next vsync when the VR session was inactive;
  this loop drives itself, so that case sleeps 16ms instead of spinning.
  Not tested - no headset here.
- RECREATE_VIEWS after the emu thread starts, as before. Vulkan never did
  this, so it's a candidate for removal when the two paths converge.
- The thin3d error callback, which surfaces GL errors as OSD messages. That
  one moved into AndroidEGLGraphicsContext::InitSurface, next to the rest of
  the context's own setup, matching WindowsGLContext.

Tested on a nubia Z2464N (Unisoc UMS9632, Mali-G57, Android 15): cold start,
UI, booting a homebrew, backgrounding and resuming both in the menu and
in-game, and a density change, which destroys and recreates the activity the
way a rotation does. Every cycle tears the EGL context down and builds a new
one, and the renderer comes back with FBOs and textures intact. The config
ladder's first choice, RGBA8888 D24S8, is what the Java chooser landed on
here too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:22:25 +02:00
Henrik RydgårdandClaude Opus 5 f868976af2 Android GL: Add a native EGL path, behind a dev toggle
First step towards removing JavaGL. The point isn't EGL itself - it's who
owns the render thread. With GLSurfaceView, Java creates the thread and the
context and calls us back, so a lost context is only observable after the
fact as a second onSurfaceCreated. That's what forces displayInit to tear
down and restart the emu thread from inside a JNI callback, and to call
NotifyContextLost first because the queue is about to be drained on a thread
with no context. None of that is needed when we own EGL: the render thread
joins the emu thread itself, with the context still current, so the drain is
real work rather than something to skip.

AndroidEGLGraphicsContext subclasses OpenGLGraphicsContext (whose own
InitSurface assumes someone else already made a context current) and adds
display/config/surface/context plus a swap function. With it, GL reuses the
Vulkan lifecycle wholesale: queryConfig("androidJavaGL") returns false, so
Java builds a plain SurfaceView and starts the native render loop when a
surface arrives - including all of the surface/pause ordering fixes from the
previous branch. runVulkanRenderLoop/requestExitVulkanRenderLoop are now
runRenderLoop/requestExitRenderLoop and serve both backends; the thread they
spawn branches on NeedsSeparateEmuThread, because the two backends split the
work in opposite directions (Vulkan: this thread is the main loop and the
render manager spawns the render thread; GL: this thread *is* the render
thread and spawns the emu thread).

Deliberately a port, not an improvement:

- GLES client version still follows NativeRenderer - 3 for VR, 2 otherwise.
  Not a cap; GLES3 is detected from GL_VERSION at runtime.
- No swap-interval function. GLSurfaceView gave us no way to set one, so
  PresentMode::IMMEDIATE has always been ignored here, and honoring it now
  would be a behavior change. Worth revisiting once this path is the only one.

The config ladder does take the user's word that Mali 400 and the Xperia Play
are no longer a factor: it asks for RGBA8888/D24S8 and falls back twice,
rather than reproducing GLSurfaceView's ask-for-nothing default that the
comments in PpssppActivity.onCreate exist to explain. ANativeWindow's format
is set from EGL_NATIVE_VISUAL_ID, which is the proper fix for the EGL_BAD_MATCH
those comments describe.

Off by default, behind Developer tools / Graphics / "Use native EGL instead
of GLSurfaceView" (OpenGL backend, Android only). Needs an app restart, since
the context is chosen in NativeApp.init.

Builds for arm64-v8a and on Windows; NOT yet run on a device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:22:25 +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