- Metal: when a layer has no drawable to give (it can take a second to
say so), don't ask again in the same frame for every pass to it.
- Swapchains other than the primary are created, resized and destroyed
between frames (Metal asserts it), and destroyed with the last
reference to them (ReleaseAssertLast, Metal and Vulkan).
- BindFramebufferSamplesAsTexture asserts it isn't given a swapchain,
like BindFramebufferAsTexture.
- An offscreen Metal swapchain can't be resized, asserted.
- VulkanRenderManager asserts that no additional swapchain outlives it.
- Native_RemoveOutputAndWait asserts it isn't called on the thread that
handles the requests, where it would always time out.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only out of date and a lost surface recreated one, so a TV whose mode
changed without a resize (MoltenVK reports SUBOPTIMAL when the layer's
size no longer matches) kept a stretched picture. Like the main
swapchain, it's now recreated after a few SUBOPTIMAL presents in a row.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
g_display lags behind surfaceChanged until the render thread applies the
resize, and a recreate after a suboptimal present can run in between.
The check then swapped a correct extent for a frame. Android now sets the
draw size callback to the latest size from backbufferResize, and the
check uses that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A swapchain pass closes its step label and opens one in presentCmd, but
only if it gets an image. With the main window minimized it doesn't, and
the end of the step closed a label that wasn't open, in presentCmd if an
extra swapchain's pass had moved recording there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
e32e558aab dropped it, assuming the stale extent came from our own resize
race. A Redmi 9A (PowerVR, Android 11) shows it doesn't: after a rotation,
vkGetPhysicalDeviceSurfaceCapabilitiesKHR returns the new transform with the
old extent, even when queried after surfaceChanged and ApplyResize have
given us the new size, and the wrong swapchain then stays until the next
rotation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Swapchains other than the main one are VKRSwapchains, owned by the
render manager, each with its own surface, image views, framebuffers
and depth buffer, in the main swapchain's format so the backbuffer render
pass and pipelines work for all of them. A render step with no
framebuffer can name one.
They don't pace the frame: acquiring doesn't wait, and the pass is
skipped if there's no image. The frame's last submit waits on and
signals the semaphores of every swapchain acquired that frame, and each
is presented after the main one. When one goes out of date, it's
recreated at the start of the next frame.
The main swapchain's path is unchanged; frametests on Vulkan match.
VulkanContext::CreateSurface is the old per-platform surface code, now
also used for the extra ones.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
We queried VkPhysicalDevicePortabilitySubsetFeaturesKHR but never enabled
it at device creation, so to the validation layers we were using image
view swizzles (and sampler LOD bias) without having them. Enable whatever
the device offers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Through the Khronos loader (like the Vulkan SDK's, needed for the
validation layers), MoltenVK is only listed if the instance enables
VK_KHR_portability_enumeration. Without it, instance creation failed with
VK_ERROR_INCOMPATIBLE_DRIVER and we fell back to OpenGL. Loading MoltenVK
directly, as the app normally does, works as before.
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>
Like the Vulkan backend: a multisampled framebuffer has multisampled
color and depth textures that are drawn to and kept between passes, and
each pass resolves them into the ordinary ones (depth and stencil to
sample 0), which textures, copies and readbacks use.
BindFramebufferSamplesAsTexture binds the samples, for the shared code's
sample 0 copies. Copies between multisampled framebuffers copy the
samples too, both depth and stencil, since Metal can't copy one plane of
a multisampled texture.
Also skips empty draws, which Metal doesn't allow.
With 4x MSAA, 1012 of the 1061 frame dumps are identical to Vulkan's.
Most of the rest differ at edges because programmable blending blends
each sample with its own color. In Resistance: Retribution, the sample 0
copy for a palette lookup matches the render without MSAA here, and
Vulkan's doesn't.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Remove what nothing used: the vertex texture and sampler commands,
the byte-upload setters, and some unused render manager members.
- thin3d pushes its uniform buffer once for both shader stages.
- Build the thin3d depth/stencil cache key from bytes, so memcmp
doesn't compare padding.
- Only create the upload command buffer when there are uploads.
- Compare copy destinations by their base texture when merging passes,
like the textures the passes read.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Headless renders to an offscreen backbuffer that nothing ever draws to,
so the GE debugger's screenshot read uninitialized memory. Fail the
readback instead, like headless Vulkan (which has no backbuffer), so the
debugger falls back to the displayed PSP framebuffer.
All the GE debugger buffers (screenshot, render color/depth/stencil,
texture, CLUT) now read back the same as Vulkan's.
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>
The vertex decoder can write past the end of its output, so a draw
decoded into the last bytes of a push buffer block wrote into unmapped
memory and crashed, now and then, in DrawEngineMetal::Flush. Keep 256
bytes free after each allocation, like the Vulkan backend's push pool.
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>
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>
The view controller creates a Metal context when that's the configured
backend, and the backbuffer gets its size from the layer's bounds, as the
drawable size isn't set up yet when the surface is created.
Also pass --options the process was launched with (from Xcode or
devicectl) on to the command line parser, so --graphics=metal works.
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>
thin3d on Metal, plus a small render manager that owns frames, command
buffers, lazily started render passes and a per-frame push buffer.
Shaders are the same Vulkan GLSL the Vulkan backend gets, translated to
MSL through glslang and SPIRV-Cross, with the Vulkan binding numbers as
Metal buffer/texture indices. flip_vert_y makes Metal's clip space match
Vulkan's, so everything else follows the Vulkan conventions.
Adds GPUBackend::METAL (4) and builds the files on Apple platforms. Not
yet selectable: the SDL window setup, the UI choice and the GPU core
come next.
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>
The transposed extents f7ec86c5d5 worked around came from the size race
the previous commit fixed: with the size now applied between frames, the
workaround didn't fire once in 38 rotations on the Galaxy S8+ that hit it
in about one in four.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ResizeFramebufFBO now asks the framebuffer whether it has a depth/stencil
buffer instead of guessing from the z address and stride.
Vulkan CopyFramebuffer gets the debug asserts and bail-out BlitFramebuffer
already had, and GL copies and blits get the same, so a copy out of a
framebuffer without depth/stencil asserts where it's queued instead of in
the queue runner.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GL_TEXTURE_LOD_BIAS is desktop GL only, and the queue runner skipped it on
GLES, but GLES still reported sampler LOD bias as supported, so the bias was
dropped and textures used the wrong mip levels: Super Monkey Ball Adventure's
intro came out blurred on the Adreno 840. Report it unsupported, so the shader
applies it (GPU_USE_SHADER_LOD_BIAS), and give the GL backend the uniform that
Vulkan has in its uniform block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the default -1..1, window depth 0 comes from z = -w, where the
rasterizer's rounding is an ulp of 1.0. Puyo Puyo Fever 2's ground at z = 0
got depths just above 0 with depth clamp on, and the sprites on it that test
GEQUAL lost pixels (910 px off the PSP on an Adreno 840, now 0). Use
glClipControl (GL 4.5, ARB/EXT_clip_control) as in Vulkan. Not in libretro,
where the frontend shares the context.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the default of 4, the driver reads past the end of any upload whose rows
aren't a multiple of 4 bytes, like a 16-bit texture of odd width. The Adreno
840 crashed in glTexImage2D on one in SOCOM Fireteam Bravo 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GLES drivers with GL_EXT_depth_clamp reported depth clamp as supported, but
glEnable(GL_DEPTH_CLAMP) was compiled out with USING_GLES2. The shaders then
relied on clamping that never happened, and geometry outside the depth range
was clipped: on the Adreno 840, Hokuto no Ken's title screen came out black and
Hot Pixel's background was missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vulkan: Batch Jackass' tiled in-place color grade
Jackass: The Game color grades the framebuffer in 32x64 tiles, one channel
per pass, texturing from the framebuffer it renders to. Each of the 225
draws became a depal pass plus a pass drawing it back, 450 render passes
per frame. A new queue hack, enabled by the TiledDepalHack compat flag,
sorts these pairs into waves of non-overlapping tiles and runs each wave
as one depal pass and one draw pass, leaving 6.
Output is byte-identical to before on both dumps from the issue, at 1x
and 3x on desktop, and at 1x on both phones below.
Time saved per post-processed frame, from replaying the dumps from the
issue with headless --bench (each replay renders that frame once):
Redmi 9A (PowerVR GE8320): ~145ms at 1x, ~180ms at 2x
Unisoc UMS9632 (Mali-G57): ~55ms at 1x and 2x
Either is more than a whole 33ms frame, so on those GPUs this pass alone
ruled out 30fps.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
System_AudioPushSamples converts straight into the output buffer: the
block loop never advanced the source pointer and copied numSamples*2
int16s out of a 2048-sample stack buffer, a stack OOB read for any push
over 1024 frames. Realloc failure drops samples instead of crashing and
log_cb is NULL-checked. retro_serialize refuses to write a state larger
than the frontend's buffer, retro_get_memory_data/size return nothing
until PSP memory is mapped, deferred unserialize no longer leaks the
previous buffer, and the cheat entry points stop leaking a CWCheatEngine
per call.
The emu thread acknowledges a pause with a CAS so it cannot clobber a
concurrent quit request. EmuThreadStop stops and joins from any live
state, so unloading while paused no longer leaves a thread running over
freed state. EmuThreadPause pumps queued frames until the thread parks,
via a new non-blocking GLRenderManager::HasQueuedThreadWork() check,
instead of draining one frame, which deadlocked when the thread was
blocked on a readback sync in the next frame. retro_run resumes a paused
thread and pauses it before check_variables() rewrites g_Config,
retro_reset stops it before PSP_Shutdown(), and the sleep_ms(4) after
resume is gone.
Config.h is edited about 100 times a year and each edit recompiled 198
files; now 172. SmallDataConvert.h pulls in arm_neon.h and was included by
headers that don't use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
D3D11DrawContext takes its base context from the deviceContext
argument, and the blend state is bound through the base context, which
accepts an ID3D11BlendState1 as is. ID3D11DeviceContext1 is optional:
RetroArch's threaded video hands v1 cores a deferred-context proxy that
answers ID3D11DeviceContext only, and the core now starts on it.
The 270 degree pre-rotation was disabled in e6b7817cc6 for #15773 (UI
disappearing in reverse landscape on a Poco X3 Pro), which left reverse
landscape to the compositor, and every present returning SUBOPTIMAL. Try
it again: rotation is handled in place now, and the stale extent after a
rotation is fixed, so it needs retesting on Adreno.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since rotation is handled in place, the swapchain is recreated right
after surfaceChanged, and the surface can still report the previous
orientation's extent. On a Galaxy S8+ this happened in about one in four
rotations, leaving a portrait swapchain on a landscape window or the
other way around, squashed into half the screen until the next rotation.
If the extent is exactly the window size transposed, use the window size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CopyFramebufferToMemory always returned true, even when glReadPixels
failed, and then copied out whatever the readback buffer held from before.
Depth was labeled D24_S8 while GL reads it as float, and the conversion
ignored the source format, so only a D32F destination came out right.
Record the format and the result of each read, convert from that, and
return it. That also fixes the image readback reusing the aspect of an
earlier framebuffer readback.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The backbuffer is a different image every frame, so there's no earlier copy
of it to return, and the lookup fell through to the sync readback buffer.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The repack loop used the texture height instead of the row, and the pack
row length was the rect's right edge while glGetTexImage writes whole rows.
Only reachable from the texture debugger.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The OLD_DATA_OK readback cache was keyed by framebuffer and size only, so
two equal-size rects of one framebuffer shared a buffer and each read got
whichever was copied last. Add x, y and the aspect to the key.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Without the shared 512x512 pack texture, the staging texture was made
bw x bh, but the copy lands at bx, by and the read starts there. Depth and
stencil were worse: CopyResource needs the whole size, so nothing was
copied, and the read ran past the end of the mapping and crashed. Only
whole-buffer reads from 0,0 (like the debugger's) worked.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
StopThreads is called inside the
FramebufferManagerCommon::NotifyRenderResized. Issue is, StopThreads is
called inside an active frame, then clears that active frame's timestamp
descriptions. Rendering resumes, and QueueRunner uses the empty list’s
size to select query 0, which was already written.
Fix this by preserving timestamp descriptions for the current frame
when insideFrame_ is true while retaining the cleanup for other frames.
Sideload builds used to dlopen a bundled libMoltenVK.dylib, which was
only worth having while they could still fall back to OpenGL. Now both
variants link the static MoltenVK from ios/MoltenVK and call Vulkan
directly. PPSSPP_VK_STATIC replaces the IOS_APP_STORE checks in the
Vulkan loader (the iOS libretro core still loads at runtime).
The static MoltenVK needs iOS 15.0, so that's now the deployment target
for all iOS builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app now always uses the Metal view controller with Vulkan (MoltenVK),
for both the App Store and sideload builds. The GL view controller is gone,
and Common/GPU/OpenGL (except GLFeatures, used by the shader generators) and
GPU/GLES are no longer compiled for it. PPSSPP_API_ANY_GL is now off for
all of iOS except the libretro core, which still uses GLES.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Now that EGL is ours rather than GLSurfaceView's, the swap interval can be set,
so turning vsync off (and fast-forward without skipping flips) works on Android
GL too. Not in VR, where OpenXR presents, and not with a config whose minimum
swap interval is 1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>