- 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>
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>