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>
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>
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>
- 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
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
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>
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>
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>
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>
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>
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>
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.
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Loading trusted whatever was in the file, apart from a few sanity
checks, and a damaged entry stayed in the cache for good, since loading
it put it back in the pipeline map and every save wrote it out again.
A Syphon Filter: Logan's Shadow cache had pipeline keys with garbage in
useHWTransform, which MoltenVK refused to create ("Vertex attribute
texcoord(1) is missing from the vertex descriptor").
Now the files end in an XXH3 checksum of everything before it, checked
before anything is read, and a cache that fails it is deleted and
rebuilt.
Bumps both cache versions, since the files have a new layout. Old files
fail the checksum anyway.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Smaller framebuffers are mostly effects (blur, bloom), and MSAA changes
how those come out, like a higher resolution would. Syphon Filter:
Logan's Shadow grew a glow around its lit floor panels that the PSP
doesn't have. Effects in Call of Duty: Road to Victory and Assassin's
Creed: Bloodlines shifted too.
A framebuffer that grows to full size gets MSAA from then on. The
temporary buffer that shifts depth for offset rendering now takes the
samples of the framebuffer it shifts, since they can differ.
Over the frame dumps at 2x and 4x, the differences away from edges
compared to no MSAA drop by a fifth and 53 dumps get closer. The one
that moved away, Silent Hill: Origins' flashlight shadow, now matches
the PSP; without MSAA it shows a known replay artifact there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Framebuffer ages are measured in flips, and the count carried over from
the previous game. Headless replaying several frame dumps in one process
(frametests.py --ios does, 100 per launch) then rendered a dump
differently from replaying it alone: Colin McRae Rally lost its car and
trees whenever any dump came before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>