From 51d775cb975dc362bce3ecb2499bdf14d57c8603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 18 Oct 2026 00:21:25 +0200 Subject: [PATCH] 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 .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) --- Common/GPU/Metal/MetalRenderManager.h | 24 ++++- Common/GPU/Metal/MetalRenderManager.mm | 94 ++++++++++++++++- Common/GPU/Metal/thin3d_metal.mm | 18 ++-- GPU/Metal/DrawEngineMetal.h | 10 +- GPU/Metal/DrawEngineMetal.mm | 138 ++++++++++++++++++++----- GPU/Metal/GPU_Metal.h | 5 + GPU/Metal/GPU_Metal.mm | 91 ++++++++++++++++ GPU/Metal/ShaderManagerMetal.h | 9 +- GPU/Metal/ShaderManagerMetal.mm | 35 ++++++- 9 files changed, 378 insertions(+), 46 deletions(-) diff --git a/Common/GPU/Metal/MetalRenderManager.h b/Common/GPU/Metal/MetalRenderManager.h index 07804d6f99..4f1bcb0b6f 100644 --- a/Common/GPU/Metal/MetalRenderManager.h +++ b/Common/GPU/Metal/MetalRenderManager.h @@ -32,6 +32,7 @@ #undef ABS #endif +#include #include #include #include @@ -138,6 +139,25 @@ struct MetalTranslatedShader { bool TranslateVulkanGLSLToMSL(ShaderStage stage, const char *glsl, MetalTranslatedShader *out, std::string *errorMessage); id CreateMetalFunction(id 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 work); + +// A shader function that's compiled when first needed, from any thread. +@interface MetalShaderFunction : NSObject +- (instancetype)initWithDevice:(id)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)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)pipeline; +// Blocks until it's compiled. nil if it failed. +- (id)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 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 state, bool usesDepthStencil); @@ -456,7 +476,7 @@ private: // Cached encoder state. struct EncoderState { - __unsafe_unretained id pipeline; + __unsafe_unretained MetalPipelineState *pipeline; __unsafe_unretained id depthStencil; bool depthStencilUsed; uint32_t stencilRef; diff --git a/Common/GPU/Metal/MetalRenderManager.mm b/Common/GPU/Metal/MetalRenderManager.mm index f0e6a5b14a..f3c1ab9fc8 100644 --- a/Common/GPU/Metal/MetalRenderManager.mm +++ b/Common/GPU/Metal/MetalRenderManager.mm @@ -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 CreateMetalFunction(id device, const MetalTranslatedS return function; } +class MetalCompileTask : public Task { +public: + MetalCompileTask(std::function 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 work_; +}; + +void RunOnCompileThread(std::function work) { + g_threadManager.EnqueueTask(new MetalCompileTask(std::move(work))); +} + +@implementation MetalShaderFunction { + id device_; + MetalTranslatedShader shader_; + std::string tag_; + std::once_flag once_; + id function_; +} + +- (instancetype)initWithDevice:(id)device shader:(const MetalTranslatedShader &)shader tag:(const std::string &)tag { + if (self = [super init]) { + device_ = device; + shader_ = shader; + tag_ = tag; + } + return self; +} + +- (id)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 ready_; + id pipeline_; +} + +- (void)setPipeline:(id)pipeline { + { + std::lock_guard guard(mutex_); + pipeline_ = pipeline; + ready_.store(true, std::memory_order_release); + } + cond_.notify_all(); +} + +- (id)wait { + if (!ready_.load(std::memory_order_acquire)) { + std::unique_lock 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 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 texture copy.dst = (__bridge void *)texture; } -void MetalRenderManager::SetPipeline(__unsafe_unretained id 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 &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)c.bind.obj]; break; + case MetalCmd::PIPELINE: + { + id pso = [(__bridge MetalPipelineState *)c.bind.obj wait]; + pipelineOK = pso != nil; + if (pso) { + [enc setRenderPipelineState:pso]; + } + break; + } case MetalCmd::DEPTH_STENCIL: [enc setDepthStencilState:(__bridge id)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)c.bind.obj atIndex:c.index]; break; case MetalCmd::VERTEX_TEXTURE: [enc setVertexTexture:(__bridge id)c.bind.obj atIndex:c.index]; break; case MetalCmd::VERTEX_SAMPLER: [enc setVertexSamplerState:(__bridge id)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)c.drawIndexed.buffer indexBufferOffset:c.drawIndexed.offset]; break; default: diff --git a/Common/GPU/Metal/thin3d_metal.mm b/Common/GPU/Metal/thin3d_metal.mm index 5c5ccc72d0..0228ecb025 100644 --- a/Common/GPU/Metal/thin3d_metal.mm +++ b/Common/GPU/Metal/thin3d_metal.mm @@ -234,7 +234,8 @@ public: } } - id GetPSO(id device, const MetalRenderTargetFormat &format); + // UI pipelines are few and simple, so they're compiled right away. nil if that failed. + MetalPipelineState *GetPSO(id 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> psos; + std::unordered_map psos; private: MetalRenderManager *rm_; }; -id MetalPipeline::GetPSO(id device, const MetalRenderTargetFormat &format) { +MetalPipelineState *MetalPipeline::GetPSO(id device, const MetalRenderTargetFormat &format) { uint32_t key = format.Key(); auto iter = psos.find(key); if (iter != psos.end()) { @@ -297,8 +298,13 @@ id MetalPipeline::GetPSO(id 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 pso = curPipeline_->GetPSO(device_, rm_->CurTargetFormat()); + MetalPipelineState *pso = curPipeline_->GetPSO(device_, rm_->CurTargetFormat()); if (!pso) { return false; } diff --git a/GPU/Metal/DrawEngineMetal.h b/GPU/Metal/DrawEngineMetal.h index 88e3f5e356..9f42f7b073 100644 --- a/GPU/Metal/DrawEngineMetal.h +++ b/GPU/Metal/DrawEngineMetal.h @@ -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 depal, bool smooth) { @@ -122,12 +128,14 @@ private: void ConvertStateToKeys(int prim); void BindShaderBlendTex(); struct Pipeline { - id 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 GetDepthStencilState(const MetalDepthStencilKey &key); bool ApplyState(const Pipeline *pipeline, bool overrideStencilRef, uint8_t stencilRef); void BindResources(const Pipeline *pipeline); diff --git a/GPU/Metal/DrawEngineMetal.mm b/GPU/Metal/DrawEngineMetal.mm index cb67b5fca7..4f8a30983d 100644 --- a/GPU/Metal/DrawEngineMetal.mm +++ b/GPU/Metal/DrawEngineMetal.mm @@ -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 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 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 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 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; } diff --git a/GPU/Metal/GPU_Metal.h b/GPU/Metal/GPU_Metal.h index b26ebd03a3..628f256750 100644 --- a/GPU/Metal/GPU_Metal.h +++ b/GPU/Metal/GPU_Metal.h @@ -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_; diff --git a/GPU/Metal/GPU_Metal.mm b/GPU/Metal/GPU_Metal.mm index 6e0baa41b6..74933071a7 100644 --- a/GPU/Metal/GPU_Metal.mm +++ b/GPU/Metal/GPU_Metal.mm @@ -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(); } diff --git a/GPU/Metal/ShaderManagerMetal.h b/GPU/Metal/ShaderManagerMetal.h index de0968d4ac..ba653b2c82 100644 --- a/GPU/Metal/ShaderManagerMetal.h +++ b/GPU/Metal/ShaderManagerMetal.h @@ -19,6 +19,7 @@ // Objective-C++ only. +#include #include #include "Common/Data/Collections/Hashmaps.h" @@ -45,10 +46,12 @@ class MetalShader { public: MetalShader(id 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 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 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_; } diff --git a/GPU/Metal/ShaderManagerMetal.mm b/GPU/Metal/ShaderManagerMetal.mm index 1bbcbc5d83..9cecf3bf9c 100644 --- a/GPU/Metal/ShaderManagerMetal.mm +++ b/GPU/Metal/ShaderManagerMetal.mm @@ -36,10 +36,7 @@ MetalShader::MetalShader(id 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 ShaderManagerMetal::DebugGetShaderIDs(DebugShaderType type) { std::vector ids; switch (type) {