Commit Graph
10378 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 ba55ee7e5f Screenshots: Take the game image while the game is on a TV
An output screenshot reads back the device's own screen, which only has
the touch controls on it then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:07:10 +02:00
Henrik RydgårdandClaude Opus 5.5 013460b2fe Outputs: No Cardboard VR on a TV
The design doc said Cardboard VR turns off on a Game output, but only the
config was checked, so with it on the TV got the split screen too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:03:43 +02:00
Henrik RydgårdandClaude Opus 5.5 5799bdee59 Outputs: Rebuild the post-shader chain when the game moves to another output
Post-shader framebuffers (for shaders at output resolution, and those
that keep the previous frame) are sized for the output they were built
for. Moving the game to a TV or back, or the TV changing size, left them
sized for the other one. SetOutputOverride now asks for a rebuild when
the output changes.

Also corrects the design doc: "auto" render resolution follows the
device's screen, not the TV.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:00:13 +02:00
Henrik RydgårdandClaude Opus 5.5 d491fc90cf Secondary outputs, and --game-window on Mac to show the game in a second window
The ScreenManager keeps a list of secondary outputs, each with its own
swapchain, recreated after a lost device. Platform code adds, resizes and
removes them from any thread through Native_AddOutput() and friends, which
take effect at the start of the next frame.

Each frame, every output is drawn by the topmost screen that claims it
(Screen::renderOutput). EmuScreen claims Game outputs: it presents the
game there, laid out for that output, and leaves it out of the backbuffer,
where the touch controls and the menus stay. Not in non-buffered mode,
which draws straight into the backbuffer.

--game-window opens such an output on Mac, with Metal. It's a smoke test
for multiple swapchains, and a stand-in for an AirPlay display.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 56c76ace35 thin3d: Swapchain objects, and multiple swapchains on Metal
The backbuffer is now a Swapchain, from GetPrimarySwapchain(), and
callers bind, measure and read it back through that instead of nullptr.
nullptr still means it too. Backends with one swapchain map the primary
to their old backbuffer path.

On Metal, CreateSwapchain() adds more, each with its own layer, depth
buffer and drawable. Passes merge per swapchain, and Present() presents
every drawable acquired that frame. The backbuffer is just the first
of them. Nothing creates a second one yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 4922859675 PresentationCommon: Take the output's size, insets and rotation as parameters
OutputParams describes the surface the game is presented to.
PrimaryOutputParams() builds it from g_display and the system's safe
area insets, as before. SetOutputOverride() lets the game go to another
output, like a TV, which will have its own size, rotation and insets.

No change for the primary output: frametests on Metal and Vulkan match
before and after.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
LunaMooandClaude Opus 5.5 519f282f99 Overlay: follow display offset/scale, avoid needless reloads
- Apply the display layout's scale and offset to the overlay itself instead
  of always centering it, so moving/resizing the display in Display Layout
  moves the overlay with the game inside it, and the portrait default
  (screen high up) is respected.
- Rescanning overlays (done whenever the display layout screen recreates its
  views, e.g. after closing the picker popup) no longer unloads the active
  overlay unless its definition changed.
- Re-picking the current overlay no longer posts GPU config change
  notifications, which caused an audio glitch with a game running behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBg32EFPsTXjAFu7dnTcz
2026-10-22 14:09:51 +02:00
LunaMooandClaude Opus 5.5 3ce3973781 Add overlay (bezel) support
Draws a user image around or behind the PSP screen, to fill the black bars
on displays whose aspect ratio doesn't match the PSP's (#20325).

- Overlays are PNGs in PSP/OVERLAYS. A plain PNG there just works: it's
  drawn in front of the game, and its transparent hole is found by walking
  from the image center to each edge until a nearly opaque pixel. Existing
  RetroArch overlay images can be dropped in as-is.
- A subfolder with an overlay.ini can define overlays with a Name, Layer
  (Foreground/Background) and an explicit Hole=x,y,w,h.
- "Load overlay image..." in the display layout screen imports any PNG
  (including Android content URIs) into the overlays folder and selects it.
- The overlay is fit to the screen (aspect preserved, centered). The hole is
  mapped with the same scale, and the game is fit into it with the normal
  stretch / integer scale / aspect ratio options. Offset and scale don't
  apply while an overlay with a hole is active. This goes through
  CalculateDisplayOutputRect, so touch mapping and post shaders agree.
- The selection is a per-orientation, per-game DisplayLayout setting.
- Disabled in VR, Cardboard, and with skip buffer effects (except software
  rendering), since the presentation path doesn't run there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBg32EFPsTXjAFu7dnTcz
2026-10-22 14:09:51 +02:00
Henrik RydgårdandClaude Opus 5.5 4240dda80e GPU: Size the video texture clamp from what was written
Video textures were clamped to a 480x272 frame on both axes, so filtering
can't pull in whatever lies next to the frame in memory. The VSH's UMD
Video player converts 480 rows, into a 256 or 512-wide texture it uses as
a ring that its strips wrap around: the rows past 272 smeared, and the
wrapped strips were stretched. The clamp now covers the rows written at
the texture, and the columns the strips written into its first row cover.
An axis the frame fills keeps the game's wrap mode. A game's 480x272
frame is clamped as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 17:32:44 +02:00
Henrik RydgårdandClaude Opus 5.5 140d1e12c3 GPU: Record PS1 games' frame dumps under POPS00000
A replay looks compat.ini up by the ID in the dump's header, so PS1 games'
dumps now get the PS1 emulator's settings when replayed with them
(--dump-compat). The file name keeps the game's own ID.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:59:40 +02:00
Henrik RydgårdandClaude Opus 5.5 ef7ca8b85f GPU: Move sprites on pixel centers back half a pixel above 1x (compat SnapHalfPixelSprites)
The PS1 emulator draws sprites with their corners on pixel centers (x.5) and
their texture coordinates on texel centers. At 1x that covers whole pixels,
but above 1x it covers half a pixel more on the right and bottom and reads
the neighbouring texels, which shows as seams between tiles. With compat.ini's
SnapHalfPixelSprites, such sprites and their texture coordinates are moved back
half a pixel, which draws the same at 1x. Texture coordinates are also nudged
a 64th of a texel in, as a scaled sprite can sample right on a texel edge,
where interpolation can fall short of the texel the GE reads (the emulator's
video unpacking does).

Set for PS1 games (POPS00000).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:51:06 +02:00
Henrik RydgårdandClaude Opus 5.5 cf788d72ca GPU: Optionally draw logic ops done on a framebuffer copy one at a time (compat SeparateLogicOpDraws)
Where the backend has no logic op and no framebuffer fetch, the logic op is
done in the shader against a copy of the framebuffer taken when the batch is
drawn, so draws batched together don't see each other. The PS1 emulator puts
each 24-bit video frame together by ORing passes into the same pixels, so it
lost all but the last. With compat.ini's SeparateLogicOpDraws, such draws are
flushed one at a time, and the rectangles of one draw are split, as they can
overlap too.

It costs a framebuffer copy per draw. Set for PS1 games (POPS00000).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:51:06 +02:00
Henrik RydgårdandClaude Opus 5.5 da0ecd838e GPU: Framebuffers kept at native resolution (compat NativeResolutionFramebuffer)
compat.ini's NativeResolutionFramebuffer (an address) keeps that framebuffer at
1x whatever the render resolution, for one that holds data read back texel by
texel: the PS1 emulator unpacks 24-bit video into one three bytes per pixel.

Set for PS1 games (POPS00000).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:51:06 +02:00
Henrik RydgårdandClaude Opus 5.5 c9641b1332 GPU: Fixed framebuffers, whole from the start (compat FixedFramebuffer)
A framebuffer the game always keeps at one size, and also reads and writes
outside the area it draws: the PS1 emulator's VRAM, a 1024x512 16-bit surface
it copies between and reads back from the CPU. compat.ini's FixedFramebuffer
("address,width,height") makes it that size instead of one estimated from the
drawing, initializes it from memory even when the first draw doesn't look like
one, and uploads into it without filtering, since what goes in is data that's
read back (#22686 saw FFVI crash on bilinear uploads at 4x).

Set for PS1 games (POPS00000).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:51:06 +02:00
Henrik Rydgård 92f394d769 Reject framebuffers for display by buffer size, not current width/height
width/height is volatile and influenced by the framebuffer size detection
of the latest draw so chck bufferHeight instead.

The old check broke the homebrew OOT port.
2026-10-21 11:03:20 +02:00
Henrik RydgårdandClaude Opus 5.5 a5e5fc079a GPU: Pick up a changed Skip Buffer Effects even without a render resize
The framebuffer manager only switches between buffered and non-buffered
rendering on a render resize. Turning on Auto FrameSkip clears Skip Buffer
Effects without posting one, so after resuming, EmuScreen ran the buffered
path with a GPU still in non-buffered mode. A frame displayed from RAM was
then drawn straight to output with no render pass bound, crashing in
VulkanRenderManager::SetScissor. Treat a mismatch as a resize in
BeginHostFrame, whatever changed the setting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 23:03:00 +02:00
Henrik RydgårdandClaude Opus 5.5 d3a23e94e9 Software transform: Don't write past the vertex buffer when clipping against the near plane
ClipTrianglesAgainstNearPlane took limits for the transformed vertices and
the output indices but never checked them, and the caller passed constants
(one of them a typo, 65336). Each partially clipped triangle adds two
vertices, so a large draw with many triangles crossing the near plane ran
off the end of transformed_ and crashed. Pass the real limits, and drop
triangles that don't fit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 23:01:24 +02:00
Henrik Rydgård f36b458bbf Crashfix in the framedump recorder 2026-10-20 16:32:44 +02:00
Henrik RydgårdandClaude Opus 5.5 7adf0c15ee Splines: Draw a line patch's shared quad edges once
A patch drawn as lines is a zigzag per quad (gpu/probe exp175), but the GE
draws a quad's right edge only in the patch's last column, since the next
quad's left edge is the same line. Edges between patches are still drawn
twice. Opaque lines can't show the difference; NFS Most Wanted's
alpha-blended spline visualizer (#22104) came out brighter wherever the
edges were drawn twice. gpu/probe exp257 counts the draws per pixel with
additive blending, for Bezier and spline patches; both are now exact, as is
the NFS frame against the PSP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 14:11:06 +02:00
Henrik RydgårdandClaude Opus 5.5 cffb12fd36 TextureCache: Raise the eviction thresholds to go with the real sizes
Nothing is evicted until the cache estimate reaches these, and they were tuned when the estimate
counted every texture at its PSP size. Counted at real size, upscaled textures crossed 16MB scale^2
times sooner (and re-scaling an evicted one is slow), and a single 2048x2048 replacement filled the
4MB secondary cache. 128MB covers what upscaling up to about 3x effectively had before. Also remove
TEXTURE_KILL_AGE_LOWMEM, unused since the low memory mode went away.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:57:57 +02:00
Henrik RydgårdandClaude Opus 5.5 0e396a99cb TextureCache: Keep polling pending replacements when over the frame budget
A texture waiting for its replacement was only polled while the frame's replacement budget lasted,
so if something used the budget up early in every frame, the textures drawn after it kept their
originals until the cache was cleared (like when minimizing on Android). Poll every time, and only
let the rebuild wait for the budget, keeping TO_REPLACE set so it's retried.

Possibly related to #21586.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 149d8786bf TextureCache: Count replaced and upscaled textures at their real size
The cache decides when to start evicting textures by their estimated memory use, which only counted
the PSP size. A 4x replacement takes 16 times that and texture packs often go higher, so with a big
pack the cache could hold far more than it meant to before evicting anything. Record the size when
the texture is built. Unscaled textures without replacements get the same estimate as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 5cc5a2d802 Replacement: Find pack files whose names differ in case
- Fixes #18746

Packs are mostly made on Windows, where "portraits/x.png" in textures.ini finds Portraits/x.png. On a
case sensitive file system (Steam Deck, Linux) it didn't, while zipped packs already ignored case.

DirectoryReader gets an opt-in ignoreCase mode, used for texture packs. On the first miss it indexes
the directory by lowercased path and retries there, so packs where every name matches never pay for
it, and the walk happens on the IO thread that loads the texture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 034901ee87 Replacement: Copy levels on the calling thread, and end loading at a bad filename
The copies went through the thread pool, where they could wait behind other tasks like pipeline
compiles, and one core already saturates memory bandwidth.

A filename with a parent dir component left the texture in CANCEL_INIT, which the texture cache
keeps polling forever. Treat it like a missing file instead. Also remove an unused lastUsed field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 f74111835f Replacement: Dump all mip levels with ignoreMipmap = false
Saving level 0 marked the whole texture as saved, so the other levels were skipped. Track it per level.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 b18792e40e Replacement: Treat a fake mipmap level as level 0 of its own texture
- Fixes #19623

With a fake mipmap change (Japanese Tactics Ogre), each PSP level is cached and hashed as its own
2D texture, but since 73c06bb776 (v1.14) the replacement and dump code used the PSP level as the
replacement level. Loading a replacement for a level-2 font sheet asked for level 2 of a one-level
replacement and hit the "Invalid miplevel" assert, and dumps were named _2, which the ini then
treats as an ignored texture. Use the created texture's level for both again, as 1.13.2 did.

Also don't force mip filtering from a mipmapped replacement when only one level was uploaded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 f5c5991f98 Splines: Fix line and point curves too large to expand in one draw
- Fixes #22104

NFS Most Wanted's music visualizer is a 32x16 spline of lines at 8x8
tessellation: 144768 indices, which software transform expands to far more
than VERTEX_BUFFER_MAX vertices, so SubmitPrim splits it and flushes between
the parts. The curve's indices sat at the start of decIndex_, where each
flush writes its generated and expanded indices, so the first flush
overwrote most of the later parts and about half the visualizer was lost.
Tessellate the indices into the upper half instead, and don't use the
predecoded output for a curve that will be split, as the flushes decode
over it.

The game also sends an empty PRIM with VADDR 0 every frame, which the
hardware backends logged as a bad vertex address. Skip zero-count PRIMs
before the address check, as the software renderer does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 08:55:22 +02:00
Henrik RydgårdandClaude Opus 5.5 ca0e40bb0a GPU: Don't load the host's window-minimized bit from a savestate
skipDrawReason was saved whole, including the bit that says the host
window is minimized. A state saved while minimized (a rewind snapshot)
kept it set after loading in a normal window, and the other way round.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 130ea9ced9 GPU: Save the next display list id, and restart frame dumps on load
- Which list slot and id the next sceGeListEnQueue gets wasn't saved, so
  after a load it depended on the session before it, and replays and
  rewinds diverged. Older states start it at 0, and also get the default
  EDRAM translation and visible matrices they don't carry, rather than the
  previous session's.
- A frame dump recording during a load replayed the loaded state's
  commands on the registers from before it. It now starts over from the
  loaded state.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 814593ee4d Framebuffers: Forget the depth shift tracking with the framebuffers
DestroyAllFBOs (on a savestate load) kept depthShiftedFor_ and
lastRenderAddress_, so a new framebuffer at the same address as the old
one skipped the depth shift for a frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 fa3eff96fb Display: Don't read past memory presenting a framebuffer from RAM
The PSP accepts display strides that run past the end of VRAM, and
negative ones (setframebuf.expected has -64 and 2048), and presenting
straight from memory read all 272 rows of whatever stride was set.
Without a matching framebuffer, that's a read far outside PSP memory.
Now the whole image has to be in memory, or the display shows black, on
the hardware backends and the software renderer.

A savestate's display framebuffer with an address outside RAM and VRAM,
or an invalid format, turns the display off instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 6e16f46445 GPU: Keep textures across a savestate load, rehashing them first
A load cleared the texture cache, so every texture the game drew next
was decoded and uploaded again. Now each one is rehashed against its
memory the next time it's used, and only decoded again if it changed. In
the three seconds after a load, Ridge Racer decodes no textures instead
of 109, Force Unleashed none instead of 43. Rendered frames are the same.

Framebuffers are still all destroyed on load, since loading a state is a
known way out of some framebuffer problems.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:30:02 +02:00
Henrik RydgårdandClaude Opus 5.5 71ed8421ec Metal: End the shader cache in a checksum, like the Vulkan and GL ones
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:07:52 +02:00
Henrik RydgårdandClaude Opus 5.5 b2fc4a80fe Metal: Leave the backend out of the libretro cores
They get their graphics context from the frontend, so they compiled and
linked the Metal backend without being able to use it. Adds
PPSSPP_API_METAL, like PPSSPP_API_D3D11, for the code that only exists
with it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:05:14 +02:00
Henrik RydgårdandClaude Opus 5.5 b2e6113f21 Metal: Hardware texture scaling
Runs the same compute shaders as the Vulkan backend, translated to MSL,
in the upload command buffer: levels decode unscaled into the push
buffer and get scaled into a view of the level. Push constants go in a
buffer of their own. Multipass shaders (NNEDI3) use scratch textures
that live for the frame.

Shaders with a constant buffer (only the smiley test shader) aren't
supported.

All the texture shaders give the same result as Vulkan's on LocoRoco
and God of War savestates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 55109b39fb TextureCacheVulkan: Move the texture scaling upload shader and passes to common
So the Metal backend can use them too. No functional change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 fe24def528 Metal: Generate mipmaps at max quality filtering
Like the Vulkan backend, with "Auto Max Quality" texture filtering give
textures up to 256x256 a full mip chain, generating the levels the game
didn't supply with a blit. Mipmapping is now controlled by the
sampler's LOD clamps alone, as on Vulkan, so the mip filter is always
set.

With max quality on, the frame dumps that change are the same 829 on
both backends, and match Vulkan's to within 41 pixels.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 957579d9d0 Metal: Keep 16-bit textures 16-bit on Apple GPUs
Like the Vulkan backend, upload the decoder's 5650, 5551 and 4444
pixels as they are, in B5G6R5Unorm, BGR5A1Unorm and ABGR4Unorm with a
swizzle (Metal names packed channels from the lowest bits up, the PSP
stores red lowest), instead of expanding them to 8888. Halves the
decoding writes and memory for those textures.

It also makes Everybody's Golf's dump identical to Vulkan's, which
already sampled these at 16 bits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 51d775cb97 Metal: Compile shaders and pipelines in the background, and cache them
Pipeline states are created on compile threads, along with the shader
functions they need, which compile once on first use. Draws record a
handle to the pipeline state, and the render thread waits for it if it
gets there first, as with Vulkan, so the emu thread never waits for a
compile. Translation to MSL stays synchronous, as recording needs the
texture masks it produces.

Each game's shader IDs and pipeline keys are saved to a
<discID>.mtlshadercache, and recreated while the game boots. macOS's own
Metal cache then makes them quick: Wipeout Pure's 36 pipelines took
25 ms, or 260 ms with an empty Metal cache, instead of stalls during
play.

Cold Wipeout Pure, 5 emulated seconds from a savestate: 3.0 s before,
1.6 s with the background compiles alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 6a668eed27 Metal: Use programmable blending for shader blending
On Apple GPUs, the fragment shader reads the framebuffer directly
([[color(0)]]) instead of blending against a copy of it. The Vulkan GLSL
generator path for this was already there apart from the input
declaration: SPIRV-Cross turns the subpassInput into framebuffer fetch.
The Vulkan backend never sets the fetch bit.

Reading the framebuffer as it is also sees earlier triangles of the same
draw, like the PSP does, where the copy only had what was there before
the draw. Of the dumps in ppsspp-framedumps whose output changes, all
ten with a PSP capture got closer to it, Tenchu's from about 10000
differing pixels to 900. Ridge Racer runs about 6% faster.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 327b5dd558 Metal: Wait for the GPU before freeing caches on device loss
DeviceLost frees the draw engine's depth stencil states and samplers
right away, while the render thread could still be replaying commands
that use them. Caught by the validation layer on shutdown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 9514e02037 Metal: Encode on a render thread, and merge render passes
The render manager now records commands on the emu thread, keeping all
of its decisions there (lazy passes, clear merging, redundant state),
and they're encoded on a render thread, or inline when
bRenderMultiThreading is off, as in the Vulkan backend.

Before encoding, passes to the same framebuffer are merged across
independent work, by the rules of VulkanQueueRunner's render pass merge.
God of War alternates with block transfers to other framebuffers, and
went from about 41 to 18 render passes a frame, which more than halved
its GPU time.

Passes where nothing tests or writes depth or stencil leave the
attachment unloaded and unstored, like the Vulkan backend, which
doesn't attach depth then.

On the eight benchmark states, Metal now uses 4-13% less CPU than
Vulkan, with wall times from 6% faster to 4% slower.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 51a90c8336 Metal: Cut ARC traffic and redundant bindings from the draw path
Draws use an unretained encoder pointer and pipeline struct pointers
instead of passing ObjC objects around (each return goes through
autorelease and retain), and only bind the texture slots the fragment
shader uses. Brings the emu thread's retain/release work down to 0.6%,
and instruction counts 2-11% below Vulkan's on the benchmark states.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 330be6c29a Metal: Don't let command buffers retain resources
Resources that the GPU may still use are now released through the render
manager's per-frame list, freed once the frame's work completes, instead
of command buffers retaining everything that's bound.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 4793d21e5d Metal: Add the GPU backend
GPU/Metal, modelled on GPU/Vulkan since the shaders are the same Vulkan
GLSL, translated to MSL. Drawing goes through the render manager's state
setters, so it shares encoders with thin3d.

thin3d fixes found along the way:
- Binding the framebuffer that's already the target continues the pass,
  applying clears and ignoring DontCare, like VulkanRenderManager.
- Separate depth or stencil copies, through a buffer, so the stencil
  survives a framebuffer resize (Need for Speed Shift).
- Stencil export, so stencil uploads take one pass.

Out of the 1061 dumps in ppsspp-framedumps, 1058 render identically to
Vulkan (two of the others are bad dumps), as do the eight benchmark
savestates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 8d7ce57fa8 Metal: Hook up the backend on Mac, with the null GPU for now
Selectable with --graphics=metal, the backend choice in settings and the
Mac menu bar. SDL creates a Metal view for it, and headless renders
offscreen. Games run on the null GPU until there's a GPU/Metal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:22 +02:00
Henrik RydgårdandClaude Opus 5.5 e666ab49de Remove leftovers from hardware tessellation
Nothing reads from textures or storage buffers in vertex shaders since
hardware tessellation was removed. Drop what was left for it: the
vertexTexture flag of VulkanTexture::EndCreate (always false), the
STORAGE_BUFFER_VERTEX binding type, GL's maxVertexTextureUnits, and the
tesselationShaderSupported cap, which nothing read (and which was always
false, since the feature was never enabled).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 12:02:10 +02:00
Henrik Rydgård d44f0129b1 Merge pull request #22654 from hrydgard/fix-android-crashes
Fix Android crashes and ANRs from the 1.20.5-beta1 Play reports
2026-10-19 10:53:52 +02:00
Henrik Rydgård b21a83ced9 Merge pull request #22639 from hrydgard/vulkan-pipeline-cache-validate
Shader caches: Add a checksum to the Vulkan and GL cache files
2026-10-19 10:01:14 +02:00
Henrik RydgårdandClaude Opus 5.5 d3f33d96d3 Create the GPU on the main thread during boot
The loader thread now only loads the game. PSP_InitUpdate creates the GPU on
the thread that owns the graphics device, so device loss can't race the
creation, and the device-lost flag and the GPU init on libretro's separate
path are gone. Vulkan loads its shader cache without waiting for the
pipelines, and PSP_InitUpdate keeps the boot in Booting until they're
created. Exiting during boot joins the loader thread in PSP_Shutdown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 09:26:15 +02:00