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