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>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-19 12:02:22 +02:00
1 parent 27edb50409
commit 51d775cb97
9 files changed
+378 -46

No files matched your search

+22 -2
View File
@@ -32,6 +32,7 @@
#undef ABS
#endif
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
@@ -138,6 +139,25 @@ struct MetalTranslatedShader {
bool TranslateVulkanGLSLToMSL(ShaderStage stage, const char *glsl, MetalTranslatedShader *out, std::string *errorMessage);
id<MTLFunction> CreateMetalFunction(id<MTLDevice> device, const MetalTranslatedShader &shader, const char *tag, std::string *errorMessage);
// Compiling MSL takes long (around 100ms the first time), so it's done off the emu thread, on a thread of its own.
void RunOnCompileThread(std::function<void()> work);
// A shader function that's compiled when first needed, from any thread.
@interface MetalShaderFunction : NSObject
- (instancetype)initWithDevice:(id<MTLDevice>)device shader:(const MetalTranslatedShader &)shader tag:(const std::string &)tag;
// Compiles on the first call, other callers wait for it. nil if it failed.
- (id<MTLFunction>)function;
@end
// A render pipeline state that may still be compiling. Recorded commands refer to these, and the replay waits
// for them, so the emu thread never waits for a compile (the same as with Vulkan).
@interface MetalPipelineState : NSObject
- (void)setPipeline:(id<MTLRenderPipelineState>)pipeline;
// Blocks until it's compiled. nil if it failed.
- (id<MTLRenderPipelineState>)wait;
- (BOOL)isReady;
@end
const char *MetalPixelFormatToString(MTLPixelFormat fmt);
enum class MetalCmd : uint8_t {
@@ -353,7 +373,7 @@ public:
MetalPushBuffer &Push() { return frames_[curFrame_].push; }
// Encoder state with redundancy elimination. Only valid after EnsureRenderEncoder() returned true.
void SetPipeline(__unsafe_unretained id<MTLRenderPipelineState> pipeline);
void SetPipeline(__unsafe_unretained MetalPipelineState *pipeline);
// usesDepthStencil: the state tests or writes depth or stencil. A pass where no draw does gets its
// depth/stencil attachment neither loaded nor stored.
void SetDepthStencilState(__unsafe_unretained id<MTLDepthStencilState> state, bool usesDepthStencil);
@@ -456,7 +476,7 @@ private:
// Cached encoder state.
struct EncoderState {
__unsafe_unretained id<MTLRenderPipelineState> pipeline;
__unsafe_unretained MetalPipelineState *pipeline;
__unsafe_unretained id<MTLDepthStencilState> depthStencil;
bool depthStencilUsed;
uint32_t stencilRef;
+90 -4
View File
@@ -26,6 +26,7 @@
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/Thread/ThreadManager.h"
#include "ext/SPIRV-Cross/spirv_msl.hpp"
@@ -214,6 +215,81 @@ id<MTLFunction> CreateMetalFunction(id<MTLDevice> device, const MetalTranslatedS
return function;
}
class MetalCompileTask : public Task {
public:
MetalCompileTask(std::function<void()> work) : work_(std::move(work)) {}
TaskType Type() const override { return TaskType::DEDICATED_THREAD; }
TaskPriority Priority() const override { return TaskPriority::NORMAL; }
void Run() override { @autoreleasepool { work_(); } }
private:
std::function<void()> work_;
};
void RunOnCompileThread(std::function<void()> work) {
g_threadManager.EnqueueTask(new MetalCompileTask(std::move(work)));
}
@implementation MetalShaderFunction {
id<MTLDevice> device_;
MetalTranslatedShader shader_;
std::string tag_;
std::once_flag once_;
id<MTLFunction> function_;
}
- (instancetype)initWithDevice:(id<MTLDevice>)device shader:(const MetalTranslatedShader &)shader tag:(const std::string &)tag {
if (self = [super init]) {
device_ = device;
shader_ = shader;
tag_ = tag;
}
return self;
}
- (id<MTLFunction>)function {
std::call_once(once_, [self] {
std::string errorMessage;
function_ = CreateMetalFunction(device_, shader_, tag_.c_str(), &errorMessage);
if (!function_) {
ERROR_LOG(Log::G3D, "%s\n%s", errorMessage.c_str(), LineNumberString(shader_.msl).c_str());
}
// Not needed anymore.
shader_.msl.clear();
});
return function_;
}
@end
@implementation MetalPipelineState {
std::mutex mutex_;
std::condition_variable cond_;
std::atomic<bool> ready_;
id<MTLRenderPipelineState> pipeline_;
}
- (void)setPipeline:(id<MTLRenderPipelineState>)pipeline {
{
std::lock_guard<std::mutex> guard(mutex_);
pipeline_ = pipeline;
ready_.store(true, std::memory_order_release);
}
cond_.notify_all();
}
- (id<MTLRenderPipelineState>)wait {
if (!ready_.load(std::memory_order_acquire)) {
std::unique_lock<std::mutex> lock(mutex_);
cond_.wait(lock, [self] { return ready_.load(std::memory_order_acquire); });
}
return pipeline_;
}
- (BOOL)isReady {
return ready_.load(std::memory_order_acquire);
}
@end
MetalRenderManager::MetalRenderManager(id<MTLDevice> device, CAMetalLayer *layer, int offscreenWidth, int offscreenHeight, bool useThread) : device_(device), layer_(layer) {
queue_ = [device_ newCommandQueue];
queue_.label = @"PPSSPP";
@@ -609,7 +685,7 @@ void MetalRenderManager::GenerateMips(__unsafe_unretained id<MTLTexture> texture
copy.dst = (__bridge void *)texture;
}
void MetalRenderManager::SetPipeline(__unsafe_unretained id<MTLRenderPipelineState> pipeline) {
void MetalRenderManager::SetPipeline(__unsafe_unretained MetalPipelineState *pipeline) {
if (state_.pipeline != pipeline) {
Record(MetalCmd::PIPELINE).bind.obj = (__bridge void *)pipeline;
state_.pipeline = pipeline;
@@ -1086,11 +1162,21 @@ void MetalRenderManager::ReplayPass(const MetalCommandChunk &chunk, MetalStep &s
}
const std::vector<MetalCommand> &commands = chunk.commands;
// Draws with a pipeline that failed to compile are dropped.
bool pipelineOK = false;
for (const auto &range : step.ranges) {
for (uint32_t i = range.first; i < range.second; i++) {
const MetalCommand &c = commands[i];
switch (c.cmd) {
case MetalCmd::PIPELINE: [enc setRenderPipelineState:(__bridge id<MTLRenderPipelineState>)c.bind.obj]; break;
case MetalCmd::PIPELINE:
{
id<MTLRenderPipelineState> pso = [(__bridge MetalPipelineState *)c.bind.obj wait];
pipelineOK = pso != nil;
if (pso) {
[enc setRenderPipelineState:pso];
}
break;
}
case MetalCmd::DEPTH_STENCIL: [enc setDepthStencilState:(__bridge id<MTLDepthStencilState>)c.bind.obj]; break;
case MetalCmd::STENCIL_REF: [enc setStencilReferenceValue:c.value]; break;
case MetalCmd::BLEND_COLOR: [enc setBlendColorRed:c.color[0] green:c.color[1] blue:c.color[2] alpha:c.color[3]]; break;
@@ -1107,9 +1193,9 @@ void MetalRenderManager::ReplayPass(const MetalCommandChunk &chunk, MetalStep &s
case MetalCmd::FRAGMENT_SAMPLER: [enc setFragmentSamplerState:(__bridge id<MTLSamplerState>)c.bind.obj atIndex:c.index]; break;
case MetalCmd::VERTEX_TEXTURE: [enc setVertexTexture:(__bridge id<MTLTexture>)c.bind.obj atIndex:c.index]; break;
case MetalCmd::VERTEX_SAMPLER: [enc setVertexSamplerState:(__bridge id<MTLSamplerState>)c.bind.obj atIndex:c.index]; break;
case MetalCmd::DRAW: [enc drawPrimitives:(MTLPrimitiveType)c.prim vertexStart:c.draw.start vertexCount:c.draw.count]; break;
case MetalCmd::DRAW: if (pipelineOK) [enc drawPrimitives:(MTLPrimitiveType)c.prim vertexStart:c.draw.start vertexCount:c.draw.count]; break;
case MetalCmd::DRAW_INDEXED:
[enc drawIndexedPrimitives:(MTLPrimitiveType)c.prim indexCount:c.drawIndexed.count indexType:MTLIndexTypeUInt16
if (pipelineOK) [enc drawIndexedPrimitives:(MTLPrimitiveType)c.prim indexCount:c.drawIndexed.count indexType:MTLIndexTypeUInt16
indexBuffer:(__bridge id<MTLBuffer>)c.drawIndexed.buffer indexBufferOffset:c.drawIndexed.offset];
break;
default:
+12 -6
View File
@@ -234,7 +234,8 @@ public:
}
}
id<MTLRenderPipelineState> GetPSO(id<MTLDevice> device, const MetalRenderTargetFormat &format);
// UI pipelines are few and simple, so they're compiled right away. nil if that failed.
MetalPipelineState *GetPSO(id<MTLDevice> device, const MetalRenderTargetFormat &format);
MetalShaderModule *vs = nullptr;
MetalShaderModule *fs = nullptr;
@@ -246,13 +247,13 @@ public:
uint8_t *ubo = nullptr;
size_t uboSize = 0;
std::string tag;
std::unordered_map<uint32_t, id<MTLRenderPipelineState>> psos;
std::unordered_map<uint32_t, MetalPipelineState *> psos;
private:
MetalRenderManager *rm_;
};
id<MTLRenderPipelineState> MetalPipeline::GetPSO(id<MTLDevice> device, const MetalRenderTargetFormat &format) {
MetalPipelineState *MetalPipeline::GetPSO(id<MTLDevice> device, const MetalRenderTargetFormat &format) {
uint32_t key = format.Key();
auto iter = psos.find(key);
if (iter != psos.end()) {
@@ -297,8 +298,13 @@ id<MTLRenderPipelineState> MetalPipeline::GetPSO(id<MTLDevice> device, const Met
MetalPixelFormatToString(format.color), MetalPixelFormatToString(format.depth),
error ? [[error localizedDescription] UTF8String] : "?");
}
psos[key] = pso;
return pso;
MetalPipelineState *state = nil;
if (pso) {
state = [MetalPipelineState new];
[state setPipeline:pso];
}
psos[key] = state;
return state;
}
class MetalBuffer : public Buffer {
@@ -1237,7 +1243,7 @@ bool MetalDrawContext::ApplyState() {
return false;
}
id<MTLRenderPipelineState> pso = curPipeline_->GetPSO(device_, rm_->CurTargetFormat());
MetalPipelineState *pso = curPipeline_->GetPSO(device_, rm_->CurTargetFormat());
if (!pso) {
return false;
}
+9 -1
View File
@@ -106,6 +106,12 @@ public:
}
void ClearPipelines();
// The shader cache: the pipelines this game has used, and the formats of their targets.
bool SavePipelines(FILE *f);
static uint32_t PipelineRecordSize();
// Creates the pipelines, which compile in the background. waitForCompiles waits for all of them.
bool LoadPipelines(FILE *f, int count, bool waitForCompiles);
int GetNumPipelines() const { return (int)pipelines_.size(); }
void SetDepalTexture(id<MTLTexture> depal, bool smooth) {
@@ -122,12 +128,14 @@ private:
void ConvertStateToKeys(int prim);
void BindShaderBlendTex();
struct Pipeline {
id<MTLRenderPipelineState> pso;
MetalPipelineState *state; // May still be compiling.
MetalRenderTargetFormat format;
uint32_t fragmentArrayTextureMask;
uint32_t fragmentTextureMask;
};
Pipeline *GetPipeline(const VShaderID &vsid, const FShaderID &fsid, bool useHWTransform);
Pipeline *GetOrCreatePipeline(const MetalPipelineKey &key, const DecVtxFormat *decFmt, const MetalRenderTargetFormat &format);
id<MTLDepthStencilState> GetDepthStencilState(const MetalDepthStencilKey &key);
bool ApplyState(const Pipeline *pipeline, bool overrideStencilRef, uint8_t stencilRef);
void BindResources(const Pipeline *pipeline);
+110 -28
View File
@@ -190,7 +190,7 @@ void DrawEngineMetal::DeviceRestore(Draw::DrawContext *draw) {
void DrawEngineMetal::ClearPipelines() {
pipelines_.Iterate([&](const MetalPipelineKey &, Pipeline *pipeline) {
if (rm_) {
rm_->DeferRelease(pipeline->pso);
rm_->DeferRelease(pipeline->state);
}
delete pipeline;
});
@@ -251,26 +251,31 @@ id<MTLDepthStencilState> DrawEngineMetal::GetDepthStencilState(const MetalDepthS
}
DrawEngineMetal::Pipeline *DrawEngineMetal::GetPipeline(const VShaderID &vsid, const FShaderID &fsid, bool useHWTransform) {
const MetalRenderTargetFormat &format = rm_->CurTargetFormat();
MetalPipelineKey key{};
key.vsid = vsid;
key.fsid = fsid;
key.vtxFmtId = useHWTransform ? dec_->decFmt.id : SOFTWARE_TRANSFORM_VTXFMT;
key.blend = blendKey_.value;
key.targetFormat = format.Key();
key.targetFormat = rm_->CurTargetFormat().Key();
return GetOrCreatePipeline(key, useHWTransform ? &dec_->decFmt : nullptr, rm_->CurTargetFormat());
}
DrawEngineMetal::Pipeline *DrawEngineMetal::GetOrCreatePipeline(const MetalPipelineKey &key, const DecVtxFormat *decFmt, const MetalRenderTargetFormat &format) {
Pipeline *pipeline = pipelines_.GetOrNull(key);
if (pipeline) {
return pipeline;
}
MetalVertexShader *vs = shaderManager_->GetVertexShaderFromID(vsid);
MetalFragmentShader *fs = shaderManager_->GetFragmentShaderFromID(fsid);
MetalVertexShader *vs = shaderManager_->GetVertexShaderFromID(key.vsid);
MetalFragmentShader *fs = shaderManager_->GetFragmentShaderFromID(key.fsid);
pipeline = new Pipeline();
pipeline->fragmentArrayTextureMask = fs->info.arrayTextureMask;
pipeline->fragmentTextureMask = fs->info.textureMask;
pipeline->state = [MetalPipelineState new];
pipeline->format = format;
pipelines_.Insert(key, pipeline);
if (vs->Failed() || fs->Failed()) {
[pipeline->state setPipeline:nil];
return pipeline;
}
@@ -279,13 +284,13 @@ DrawEngineMetal::Pipeline *DrawEngineMetal::GetPipeline(const VShaderID &vsid, c
if (iter != vertexDescriptors_.end()) {
vd = iter->second;
} else {
vd = CreateVertexDescriptor(useHWTransform ? &dec_->decFmt : nullptr);
vd = CreateVertexDescriptor(decFmt);
vertexDescriptors_[key.vtxFmtId] = vd;
}
MetalBlendKey blend;
blend.value = key.blend;
MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new];
desc.vertexFunction = vs->function;
desc.fragmentFunction = fs->function;
desc.vertexDescriptor = vd;
desc.rasterSampleCount = format.sampleCount;
desc.depthAttachmentPixelFormat = format.depth;
@@ -294,30 +299,107 @@ DrawEngineMetal::Pipeline *DrawEngineMetal::GetPipeline(const VShaderID &vsid, c
MTLRenderPipelineColorAttachmentDescriptor *att = desc.colorAttachments[0];
att.pixelFormat = format.color;
MTLColorWriteMask mask = MTLColorWriteMaskNone;
if (blendKey_.colorWriteMask & 1) mask |= MTLColorWriteMaskRed;
if (blendKey_.colorWriteMask & 2) mask |= MTLColorWriteMaskGreen;
if (blendKey_.colorWriteMask & 4) mask |= MTLColorWriteMaskBlue;
if (blendKey_.colorWriteMask & 8) mask |= MTLColorWriteMaskAlpha;
if (blend.colorWriteMask & 1) mask |= MTLColorWriteMaskRed;
if (blend.colorWriteMask & 2) mask |= MTLColorWriteMaskGreen;
if (blend.colorWriteMask & 4) mask |= MTLColorWriteMaskBlue;
if (blend.colorWriteMask & 8) mask |= MTLColorWriteMaskAlpha;
att.writeMask = mask;
att.blendingEnabled = blendKey_.blendEnable;
if (blendKey_.blendEnable) {
att.rgbBlendOperation = blendEqToMetal[blendKey_.eqColor];
att.alphaBlendOperation = blendEqToMetal[blendKey_.eqAlpha];
att.sourceRGBBlendFactor = blendFactorToMetal[blendKey_.srcColor];
att.destinationRGBBlendFactor = blendFactorToMetal[blendKey_.dstColor];
att.sourceAlphaBlendFactor = blendFactorToMetal[blendKey_.srcAlpha];
att.destinationAlphaBlendFactor = blendFactorToMetal[blendKey_.dstAlpha];
att.blendingEnabled = blend.blendEnable;
if (blend.blendEnable) {
att.rgbBlendOperation = blendEqToMetal[blend.eqColor];
att.alphaBlendOperation = blendEqToMetal[blend.eqAlpha];
att.sourceRGBBlendFactor = blendFactorToMetal[blend.srcColor];
att.destinationRGBBlendFactor = blendFactorToMetal[blend.dstColor];
att.sourceAlphaBlendFactor = blendFactorToMetal[blend.srcAlpha];
att.destinationAlphaBlendFactor = blendFactorToMetal[blend.dstAlpha];
}
}
NSError *error = nil;
pipeline->pso = [rm_->Device() newRenderPipelineStateWithDescriptor:desc error:&error];
if (!pipeline->pso) {
ERROR_LOG(Log::G3D, "Failed to create pipeline (%s / %s): %s", vs->desc.c_str(), fs->desc.c_str(), error ? [[error localizedDescription] UTF8String] : "?");
}
// The shader functions compile on first use (around 100ms each when not in Metal's cache), and then the
// pipeline, so all of it goes to a compile thread. Draws record the pipeline state, and the render thread
// waits for it if it gets there first.
id<MTLDevice> device = rm_->Device();
MetalPipelineState *state = pipeline->state;
MetalShaderFunction *vsFunction = vs->function;
MetalShaderFunction *fsFunction = fs->function;
std::string description = vs->desc + " / " + fs->desc;
RunOnCompileThread([=]() {
desc.vertexFunction = [vsFunction function];
desc.fragmentFunction = [fsFunction function];
id<MTLRenderPipelineState> pso;
if (desc.vertexFunction && desc.fragmentFunction) {
NSError *error = nil;
pso = [device newRenderPipelineStateWithDescriptor:desc error:&error];
if (!pso) {
ERROR_LOG(Log::G3D, "Failed to create pipeline (%s): %s", description.c_str(), error ? [[error localizedDescription] UTF8String] : "?");
}
}
[state setPipeline:pso];
});
return pipeline;
}
struct StoredMetalPipeline {
MetalPipelineKey key;
uint32_t color;
uint32_t depth;
uint32_t stencil;
uint32_t sampleCount;
};
uint32_t DrawEngineMetal::PipelineRecordSize() {
return sizeof(StoredMetalPipeline);
}
bool DrawEngineMetal::SavePipelines(FILE *f) {
bool ok = true;
pipelines_.Iterate([&](const MetalPipelineKey &key, Pipeline *pipeline) {
StoredMetalPipeline stored{};
stored.key = key;
stored.color = (uint32_t)pipeline->format.color;
stored.depth = (uint32_t)pipeline->format.depth;
stored.stencil = (uint32_t)pipeline->format.stencil;
stored.sampleCount = (uint32_t)pipeline->format.sampleCount;
ok = ok && fwrite(&stored, sizeof(stored), 1, f) == 1;
});
return ok;
}
bool DrawEngineMetal::LoadPipelines(FILE *f, int count, bool waitForCompiles) {
std::vector<MetalPipelineState *> states;
bool ok = true;
for (int i = 0; i < count; i++) {
StoredMetalPipeline stored;
if (fread(&stored, sizeof(stored), 1, f) != 1) {
ok = false;
break;
}
MetalRenderTargetFormat format;
format.color = (MTLPixelFormat)stored.color;
format.depth = (MTLPixelFormat)stored.depth;
format.stencil = (MTLPixelFormat)stored.stencil;
format.sampleCount = (int)stored.sampleCount;
if (format.Key() != stored.key.targetFormat) {
// Damaged.
ok = false;
break;
}
DecVtxFormat decFmt;
const bool useHWTransform = stored.key.vtxFmtId != SOFTWARE_TRANSFORM_VTXFMT;
if (useHWTransform) {
decFmt.InitializeFromID(stored.key.vtxFmtId);
}
Pipeline *pipeline = GetOrCreatePipeline(stored.key, useHWTransform ? &decFmt : nullptr, format);
states.push_back(pipeline->state);
}
if (waitForCompiles) {
for (MetalPipelineState *state : states) {
[state wait];
}
}
return ok;
}
void DrawEngineMetal::ConvertStateToKeys(int prim) {
bool useBufferedRendering = framebufferManager_->UseBufferedRendering();
@@ -487,7 +569,7 @@ bool DrawEngineMetal::ApplyState(const Pipeline *pipeline, bool overrideStencilR
if (!rm_->EnsureRenderEncoder()) {
return false;
}
rm_->SetPipeline(pipeline->pso);
rm_->SetPipeline(pipeline->state);
rm_->SetDepthStencilState(GetDepthStencilState(depthStencilKey_), depthStencilKey_.depthTestEnable || depthStencilKey_.stencilTestEnable);
if (depthStencilKey_.stencilTestEnable) {
rm_->SetStencilRef(overrideStencilRef ? stencilRef : dynState_.stencilRef);
@@ -651,7 +733,7 @@ void DrawEngineMetal::Flush() {
}
Pipeline *pipeline = updatePipeline(true);
if (!pipeline || !pipeline->pso || !ApplyState(pipeline, false, 0)) {
if (!pipeline || !ApplyState(pipeline, false, 0)) {
ResetAfterSkippedDraw();
return;
}
@@ -715,7 +797,7 @@ void DrawEngineMetal::Flush() {
}
if (action == SW_DRAW_INDEXED) {
Pipeline *pipeline = updatePipeline(false);
if (!pipeline || !pipeline->pso || !ApplyState(pipeline, result.setStencil, result.stencilValue)) {
if (!pipeline || !ApplyState(pipeline, result.setStencil, result.stencilValue)) {
ResetAfterSkippedDraw();
return;
}
+5
View File
@@ -19,6 +19,7 @@
// Plain C++, so GPU.cpp can create it. The parts are Objective-C++.
#include "Common/File/Path.h"
#include "GPU/GPUCommonHW.h"
class FramebufferManagerMetal;
@@ -44,6 +45,10 @@ protected:
void FinishDeferred() override;
private:
void LoadCache(const Path &filename);
void SaveCache(const Path &filename);
Path shaderCachePath_;
FramebufferManagerMetal *framebufferManagerMetal_;
TextureCacheMetal *textureCacheMetal_;
ShaderManagerMetal *shaderManagerMetal_;
+91
View File
@@ -20,7 +20,12 @@
#include "Common/Log.h"
#include "Common/Data/Text/StringWriter.h"
#include "Common/GPU/thin3d.h"
#include "Common/File/FileUtil.h"
#include "Common/TimeUtil.h"
#include "Core/Config.h"
#include "Core/Reporting.h"
#include "Core/System.h"
#include "Core/ELF/ParamSFO.h"
#include "GPU/GPUState.h"
#include "GPU/Metal/GPU_Metal.h"
#include "GPU/Metal/DrawEngineMetal.h"
@@ -56,9 +61,94 @@ GPU_Metal::GPU_Metal(GraphicsContext *gfxCtx, Draw::DrawContext *draw) : GPUComm
UpdateCmdInfo();
BuildReportingInfo();
textureCache_->NotifyConfigChanged();
std::string discID = g_paramSFO.GetDiscID();
if (!discID.empty()) {
File::CreateFullPath(GetSysDirectory(DIRECTORY_APP_CACHE));
shaderCachePath_ = GetSysDirectory(DIRECTORY_APP_CACHE) / (discID + ".mtlshadercache");
LoadCache(shaderCachePath_);
}
}
namespace {
// Increase when the stored keys change meaning (MetalPipelineKey, the shader IDs, the formats).
constexpr uint32_t METAL_CACHE_MAGIC = 0x4C54534D; // "MSTL"
constexpr uint32_t METAL_CACHE_VERSION = 1;
struct MetalCacheHeader {
uint32_t magic;
uint32_t version;
uint32_t useFlags;
uint32_t pipelineRecordSize; // Catches layout changes that forgot the version.
int32_t numVertexShaders;
int32_t numFragmentShaders;
int32_t numPipelines;
};
} // namespace
// Runs on the loader thread, before the game starts. Waits for the pipelines, like the Vulkan backend, so
// they don't stutter in the first frames.
void GPU_Metal::LoadCache(const Path &filename) {
if (!g_Config.bShaderCache) {
return;
}
FILE *f = File::OpenCFile(filename, "rb");
if (!f) {
return;
}
MetalCacheHeader header{};
bool ok = fread(&header, sizeof(header), 1, f) == 1 && header.magic == METAL_CACHE_MAGIC && header.version == METAL_CACHE_VERSION &&
header.pipelineRecordSize == drawEngine_->PipelineRecordSize();
if (ok && header.useFlags != gstate_c.GetUseFlags()) {
// The IDs carry the flags they were made with, so this just means some of them won't be used.
WARN_LOG(Log::G3D, "Metal shader cache useFlags mismatch, %08x, expected %08x", header.useFlags, gstate_c.GetUseFlags());
}
double start = time_now_d();
ok = ok && shaderManagerMetal_->LoadCache(f, header.numVertexShaders, header.numFragmentShaders);
ok = ok && drawEngine_->LoadPipelines(f, header.numPipelines, true);
fclose(f);
if (ok) {
INFO_LOG(Log::G3D, "Loaded Metal shader cache: %d vertex and %d fragment shaders, %d pipelines in %0.1f ms",
header.numVertexShaders, header.numFragmentShaders, header.numPipelines, (time_now_d() - start) * 1000.0);
} else {
WARN_LOG(Log::G3D, "Incompatible or damaged Metal shader cache, deleting it");
File::Delete(filename);
}
}
void GPU_Metal::SaveCache(const Path &filename) {
// After DeviceLost (which saved it), the shaders are gone. Don't overwrite the cache with nothing.
if (!g_Config.bShaderCache || !filename.Valid() || !draw_) {
return;
}
FILE *f = File::OpenCFile(filename, "wb");
if (!f) {
return;
}
MetalCacheHeader header{};
header.magic = METAL_CACHE_MAGIC;
header.version = METAL_CACHE_VERSION;
header.useFlags = gstate_c.GetUseFlags();
header.pipelineRecordSize = drawEngine_->PipelineRecordSize();
header.numVertexShaders = shaderManagerMetal_->GetNumVertexShaders();
header.numFragmentShaders = shaderManagerMetal_->GetNumFragmentShaders();
header.numPipelines = drawEngine_->GetNumPipelines();
bool ok = fwrite(&header, sizeof(header), 1, f) == 1;
ok = ok && shaderManagerMetal_->SaveCache(f);
ok = ok && drawEngine_->SavePipelines(f);
fclose(f);
if (ok) {
INFO_LOG(Log::G3D, "Saved Metal shader cache: %d vertex and %d fragment shaders, %d pipelines", header.numVertexShaders, header.numFragmentShaders, header.numPipelines);
} else {
ERROR_LOG(Log::G3D, "Failed to write the Metal shader cache, disk full?");
File::Delete(filename);
}
}
GPU_Metal::~GPU_Metal() {
SaveCache(shaderCachePath_);
if (draw_) {
MetalRenderManager *rm = (MetalRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
rm->WaitIdle();
@@ -103,6 +193,7 @@ void GPU_Metal::DeviceLost() {
MetalRenderManager *rm = (MetalRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
rm->WaitIdle();
}
SaveCache(shaderCachePath_);
drawEngine_->ClearPipelines();
GPUCommonHW::DeviceLost();
}
+8 -1
View File
@@ -19,6 +19,7 @@
// Objective-C++ only.
#include <cstdio>
#include <string>
#include "Common/Data/Collections/Hashmaps.h"
@@ -45,10 +46,12 @@ class MetalShader {
public:
MetalShader(id<MTLDevice> device, ShaderStage stage, const std::string &source, const std::string &desc);
// Only translation failures. Compile failures show up when the pipeline is created.
bool Failed() const { return function == nil; }
std::string GetShaderString(DebugShaderStringType type) const;
id<MTLFunction> function;
// Compiled when a pipeline first needs it, on a compile thread.
MetalShaderFunction *function;
MetalTranslatedShader info;
std::string source;
std::string desc;
@@ -90,6 +93,10 @@ public:
std::vector<std::string> DebugGetShaderIDs(DebugShaderType type) override;
std::string DebugGetShaderString(std::string id, DebugShaderType type, DebugShaderStringType stringType) override;
// The shader cache: the IDs of this game's shaders.
bool SaveCache(FILE *f);
bool LoadCache(FILE *f, int numVertexShaders, int numFragmentShaders);
// Returns the dirty uniform flags.
uint64_t UpdateUniforms(bool useBufferedRendering, bool pixelMapped);
const UB_VS_FS_Base &BaseUniforms() const { return ub_base_; }
+31 -4
View File
@@ -36,10 +36,7 @@ MetalShader::MetalShader(id<MTLDevice> device, ShaderStage stage, const std::str
Reporting::ReportMessage("Metal shader translation failed: %s / code: %s", errorMessage.c_str(), source.c_str());
return;
}
function = CreateMetalFunction(device, info, desc.c_str(), &errorMessage);
if (!function) {
ERROR_LOG(Log::G3D, "%s\n%s", errorMessage.c_str(), LineNumberString(info.msl).c_str());
}
function = [[MetalShaderFunction alloc] initWithDevice:device shader:info tag:desc];
}
std::string MetalShader::GetShaderString(DebugShaderStringType type) const {
@@ -150,6 +147,36 @@ void ShaderManagerMetal::GetShaderIDs(int prim, u32 vertexType, VShaderID *vshad
_dbg_assert_msg_(lastVSID_.Bit(VS_BIT_USE_HW_TRANSFORM) == useHWTransform, "Bad vshader ID was computed");
}
bool ShaderManagerMetal::SaveCache(FILE *f) {
bool ok = true;
vsCache_.Iterate([&](const VShaderID &id, MetalVertexShader *) {
ok = ok && fwrite(&id, sizeof(id), 1, f) == 1;
});
fsCache_.Iterate([&](const FShaderID &id, MetalFragmentShader *) {
ok = ok && fwrite(&id, sizeof(id), 1, f) == 1;
});
return ok;
}
bool ShaderManagerMetal::LoadCache(FILE *f, int numVertexShaders, int numFragmentShaders) {
// Only translated here. They're compiled along with the pipelines that use them.
for (int i = 0; i < numVertexShaders; i++) {
VShaderID id;
if (fread(&id, sizeof(id), 1, f) != 1) {
return false;
}
GetVertexShaderFromID(id);
}
for (int i = 0; i < numFragmentShaders; i++) {
FShaderID id;
if (fread(&id, sizeof(id), 1, f) != 1) {
return false;
}
GetFragmentShaderFromID(id);
}
return true;
}
std::vector<std::string> ShaderManagerMetal::DebugGetShaderIDs(DebugShaderType type) {
std::vector<uint64_t> ids;
switch (type) {