mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Sizes up the work against the existing backends, lays out the shader generation vs. SPIRV-Cross translation choice, and argues that programmable blending (the framebufferFetchSupported cap that Vulkan hardcodes to false) is a better reason to do it than dropping the MoltenVK dependency.
117 lines
6.2 KiB
Markdown
117 lines
6.2 KiB
Markdown
# Feasibility of a native Metal backend
|
|
|
|
An analysis of what it would take to add a Metal rendering backend to PPSSPP, and whether it's worth
|
|
doing. Written 2026-09-03 against the state of the tree at that time; the line counts and file
|
|
references will drift.
|
|
|
|
**Summary:** technically very feasible, and the tree is already shaped for it. But "get rid of the
|
|
MoltenVK dependency" is the weakest reason to do it - the good reason is programmable blending.
|
|
|
|
## What's already in place
|
|
|
|
- `ios/ViewControllerMetal.h` says, in its own comment, *"Used by both Vulkan/MoltenVK and the future
|
|
Metal backend"*. The presentation layer is already a `CAMetalLayer`, so the window-system half of
|
|
the job is done.
|
|
- `ext/SPIRV-Cross.vcxproj` already compiles `spirv_msl.cpp`. The CMake build only links
|
|
`spirv-cross-glsl` (and `spirv-cross-hlsl` conditionally), so enabling MSL translation there is a
|
|
one-line change.
|
|
- `GPUBackend` in `Core/ConfigValues.h` has a free slot at 1, where D3D9 used to live.
|
|
|
|
## Scale of the work
|
|
|
|
A backend is two layers. Measured against the existing three:
|
|
|
|
| Layer | D3D11 | GLES | Vulkan |
|
|
|---|---|---|---|
|
|
| `Common/GPU/<B>/` - thin3d driver, render manager | 2,093 | 8,308 | 14,223 |
|
|
| `GPU/<B>/` - draw engine, texture cache, framebuffers, shaders, state mapping | 2,697 | 3,540 | 5,129 |
|
|
|
|
`thin3d.h` has roughly 53 pure virtuals to satisfy across its interfaces.
|
|
|
|
Metal sits architecturally between D3D11 and Vulkan: explicit pipeline state objects and command
|
|
buffers like Vulkan, but automatic residency and resource lifetime like D3D11 - no descriptor sets,
|
|
no memory allocator, no manual barriers for the common cases. So `Common/GPU/Metal/` should land
|
|
closer to D3D11's 2k than to Vulkan's 14k. It does need a `MetalRenderManager` modelled on
|
|
`VulkanRenderManager` for render pass batching, because Apple tilers punish mid-pass flushes hard and
|
|
PPSSPP's framebuffer juggling causes a lot of them.
|
|
|
|
Estimate: **6-9k lines**, in Objective-C++.
|
|
|
|
## The real decision: how to get shaders
|
|
|
|
Game shaders are generated per shader-ID at runtime by `GPU/Common/FragmentShaderGenerator.cpp` and
|
|
`VertexShaderGenerator.cpp` through `Common/GPU/ShaderWriter.cpp`, parameterised by
|
|
`ShaderLanguageDesc`. There are 68 language-conditional sites across those three files.
|
|
|
|
**Option A - teach ShaderWriter and the generators to emit MSL.** Most work, best end result, no
|
|
runtime translator. But MSL is not a GLSL/HLSL dialect: it's C++14, with struct-based stage I/O,
|
|
explicit `[[attribute(n)]]` / `[[buffer(n)]]` binding attributes, and textures and samplers passed as
|
|
function arguments rather than declared as globals. `ShaderWriter` is built around the family
|
|
resemblance between GLSL and HLSL, so this is closer to writing a third code generator than to adding
|
|
a flag. Adding `MSL = 32` to the `ShaderLanguage` bitmask in `Common/GPU/Shader.h` is the trivial part.
|
|
|
|
**Option B - generate `GLSL_VULKAN` and translate at runtime**, glslang -> SPIR-V ->
|
|
`spirv_cross::CompilerMSL`. `Common/GPU/ShaderTranslation.cpp` already does exactly this shape for
|
|
HLSL. Far less work, and the right thing to start with - but be clear that it is reimplementing
|
|
MoltenVK's shader half, so glslang and SPIRV-Cross stay as runtime dependencies and only the Vulkan
|
|
API emulation is shed. Metal PSO creation from MSL source is also slower than Vulkan pipeline creation
|
|
from SPIR-V, so the existing asynchronous pipeline compilation machinery matters more, not less.
|
|
|
|
Option A is an optimisation that can be done later, or never.
|
|
|
|
## What a Metal backend actually buys
|
|
|
|
The strongest argument is one line of code:
|
|
|
|
```cpp
|
|
// Common/GPU/Vulkan/thin3d_vulkan.cpp
|
|
caps_.framebufferFetchSupported = false;
|
|
```
|
|
|
|
`framebufferFetchSupported` is a first-class thin3d capability. `GPU/GPUCommonHW.cpp` reads it and,
|
|
when true, switches shader blending from `FBO_TEX_COPY_BIND_TEX` to `FBO_TEX_READ_FRAMEBUFFER` - no
|
|
framebuffer copy at all. Today only the GL backend sets it, via `EXT_shader_framebuffer_fetch` /
|
|
`ARM_shader_framebuffer_fetch`. Vulkan hardcodes it to false, and MoltenVK cannot portably change
|
|
that.
|
|
|
|
Metal on Apple GPUs has native programmable blending (`[[color(0)]]` as a fragment shader input). A
|
|
Metal backend sets that capability to true and immediately lights up an existing, already-tested code
|
|
path, removing a framebuffer copy per draw in every game that needs PSP blend mode emulation. That is
|
|
a structural win on exactly the workload PPSSPP is bottlenecked on, and no MoltenVK release can
|
|
deliver it.
|
|
|
|
Secondary wins:
|
|
|
|
- `MTLStorageModeMemoryless` for depth/stencil on tilers - never allocate backing store for buffers
|
|
that don't survive the render pass.
|
|
- Real Xcode GPU frame capture and shader profiling, instead of debugging through a translation layer.
|
|
- No vendored `ios/MoltenVK/MoltenVK.xcframework/ios-arm64/libMoltenVK.a` in the repo.
|
|
|
|
That last one - the actual "remove the dependency" item - is the smallest of them.
|
|
|
|
## What it costs
|
|
|
|
A permanent additional backend to maintain, testable only on Apple hardware, duplicating effort every
|
|
time something changes in the shared GPU code. PPSSPP already carries three. MoltenVK is
|
|
Khronos-maintained and works. If the Metal backend ships and isn't clearly faster, the project has
|
|
taken on maintenance for nothing.
|
|
|
|
One thing that is *not* a cost: the feature envelope. Metal has no geometry shaders, but neither does
|
|
MoltenVK, so the Vulkan backend's `geometryShader` feature check already fails on Apple and PPSSPP
|
|
already runs the fallback paths there. A Metal backend targets the exact reduced feature set Apple
|
|
users run today - there are no new gaps to fill.
|
|
|
|
## Suggested sequencing
|
|
|
|
Do the risky part first and make it provable:
|
|
|
|
1. `Common/GPU/Metal/thin3d_metal.mm` implementing `DrawContext`, with shaders via option B.
|
|
2. Bring up the **UI** on it - the UI renders through thin3d directly, so this exercises the ~53
|
|
virtuals, the render manager design and the shader pipeline against something you can see, with
|
|
none of the emulation layer involved.
|
|
3. Only then port the emulation layer into `GPU/Metal/`, cribbing structure from `GPU/D3D11/`, which
|
|
is the closest existing fit.
|
|
|
|
Step 2 is the go/no-go gate: if the numbers don't justify continuing, everything up to that point is
|
|
cheap to throw away.
|