Files
ppsspp/GPU/Metal/DrawEngineMetal.mm
T
Henrik RydgårdandClaude Opus 5.5 51d775cb97 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>
2026-10-19 12:02:22 +02:00

840 lines
33 KiB
Plaintext

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "ppsspp_config.h"
#include <algorithm>
#include <functional>
#include "Common/Log.h"
#include "Common/Profiler/Profiler.h"
#include "Common/GPU/thin3d.h"
#include "Core/Config.h"
#include "GPU/GPUState.h"
#include "GPU/GPUCommon.h"
#include "GPU/ge_constants.h"
#include "GPU/Common/SoftwareTransformCommon.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/Common/FramebufferManagerCommon.h"
#include "GPU/Metal/DrawEngineMetal.h"
#include "GPU/Metal/ShaderManagerMetal.h"
#include "GPU/Metal/TextureCacheMetal.h"
#include "GPU/Metal/FramebufferManagerMetal.h"
static const MTLBlendFactor blendFactorToMetal[(size_t)BlendFactor::COUNT] = {
MTLBlendFactorZero,
MTLBlendFactorOne,
MTLBlendFactorSourceColor,
MTLBlendFactorOneMinusSourceColor,
MTLBlendFactorDestinationColor,
MTLBlendFactorOneMinusDestinationColor,
MTLBlendFactorSourceAlpha,
MTLBlendFactorOneMinusSourceAlpha,
MTLBlendFactorDestinationAlpha,
MTLBlendFactorOneMinusDestinationAlpha,
MTLBlendFactorBlendColor,
MTLBlendFactorOneMinusBlendColor,
MTLBlendFactorBlendAlpha,
MTLBlendFactorOneMinusBlendAlpha,
MTLBlendFactorSource1Color,
MTLBlendFactorOneMinusSource1Color,
MTLBlendFactorSource1Alpha,
MTLBlendFactorOneMinusSource1Alpha,
MTLBlendFactorZero, // INVALID
};
static const MTLBlendOperation blendEqToMetal[(size_t)BlendEq::COUNT] = {
MTLBlendOperationAdd,
MTLBlendOperationSubtract,
MTLBlendOperationReverseSubtract,
MTLBlendOperationMin,
MTLBlendOperationMax,
};
// In GE order.
static const MTLCompareFunction compareToMetal[8] = {
MTLCompareFunctionNever,
MTLCompareFunctionAlways,
MTLCompareFunctionEqual,
MTLCompareFunctionNotEqual,
MTLCompareFunctionLess,
MTLCompareFunctionLessEqual,
MTLCompareFunctionGreater,
MTLCompareFunctionGreaterEqual,
};
static const MTLStencilOperation stencilOpToMetal[8] = {
MTLStencilOperationKeep,
MTLStencilOperationZero,
MTLStencilOperationReplace,
MTLStencilOperationInvert,
MTLStencilOperationIncrementClamp,
MTLStencilOperationDecrementClamp,
MTLStencilOperationKeep,
MTLStencilOperationKeep,
};
// Not a valid decoded format ID (those can be 0).
static constexpr uint32_t SOFTWARE_TRANSFORM_VTXFMT = 0xFFFFFFFF;
static MTLVertexFormat DecFmtToMetal(int fmt) {
switch (fmt) {
case DEC_FLOAT_1: return MTLVertexFormatFloat;
case DEC_FLOAT_2: return MTLVertexFormatFloat2;
case DEC_FLOAT_3: return MTLVertexFormatFloat3;
case DEC_FLOAT_4: return MTLVertexFormatFloat4;
case DEC_S8_3: return MTLVertexFormatChar4Normalized;
case DEC_S16_3: return MTLVertexFormatShort4Normalized;
case DEC_U8_1:
case DEC_U8_2:
case DEC_U8_3:
case DEC_U8_4: return MTLVertexFormatUChar4Normalized;
case DEC_U16_1:
case DEC_U16_2: return MTLVertexFormatUShort2Normalized;
case DEC_U16_3:
case DEC_U16_4: return MTLVertexFormatUShort4Normalized;
default: return MTLVertexFormatInvalid;
}
}
static void SetAttribute(MTLVertexDescriptor *vd, PspAttributeLocation location, int fmt, int offset) {
MTLVertexAttributeDescriptor *attr = vd.attributes[(int)location];
attr.format = DecFmtToMetal(fmt);
attr.offset = offset;
attr.bufferIndex = METAL_VERTEX_BUFFER_INDEX;
}
static MTLVertexDescriptor *CreateVertexDescriptor(const DecVtxFormat *decFmt) {
MTLVertexDescriptor *vd = [MTLVertexDescriptor vertexDescriptor];
if (decFmt) {
if (decFmt->w0fmt != 0) SetAttribute(vd, PspAttributeLocation::W1, decFmt->w0fmt, decFmt->w0off);
if (decFmt->w1fmt != 0) SetAttribute(vd, PspAttributeLocation::W2, decFmt->w1fmt, decFmt->w1off);
if (decFmt->uvfmt != 0) SetAttribute(vd, PspAttributeLocation::TEXCOORD, decFmt->uvfmt, decFmt->uvoff);
if (decFmt->c0fmt != 0) SetAttribute(vd, PspAttributeLocation::COLOR0, decFmt->c0fmt, decFmt->c0off);
if (decFmt->c1fmt != 0) SetAttribute(vd, PspAttributeLocation::COLOR1, decFmt->c1fmt, decFmt->c1off);
if (decFmt->nrmfmt != 0) SetAttribute(vd, PspAttributeLocation::NORMAL, decFmt->nrmfmt, decFmt->nrmoff);
SetAttribute(vd, PspAttributeLocation::POSITION, DecVtxFormat::PosFmt(), decFmt->posoff);
vd.layouts[METAL_VERTEX_BUFFER_INDEX].stride = decFmt->stride;
} else {
// Software transformed. Unused attributes don't hurt.
SetAttribute(vd, PspAttributeLocation::POSITION, DEC_FLOAT_4, offsetof(TransformedVertex, pos));
SetAttribute(vd, PspAttributeLocation::TEXCOORD, DEC_FLOAT_3, offsetof(TransformedVertex, uv));
SetAttribute(vd, PspAttributeLocation::COLOR0, DEC_U8_4, offsetof(TransformedVertex, color0_32));
SetAttribute(vd, PspAttributeLocation::COLOR1, DEC_U8_4, offsetof(TransformedVertex, color1_32));
SetAttribute(vd, PspAttributeLocation::NORMAL, DEC_FLOAT_1, offsetof(TransformedVertex, fog));
vd.layouts[METAL_VERTEX_BUFFER_INDEX].stride = sizeof(TransformedVertex);
}
vd.layouts[METAL_VERTEX_BUFFER_INDEX].stepFunction = MTLVertexStepFunctionPerVertex;
return vd;
}
DrawEngineMetal::DrawEngineMetal(Draw::DrawContext *draw) : draw_(draw), pipelines_(256) {
decOptions_.expand8BitNormalsToFloat = false;
InitDeviceObjects();
}
DrawEngineMetal::~DrawEngineMetal() {
ClearPipelines();
if (draw_) {
draw_->SetInvalidationCallback(InvalidationCallback());
}
}
void DrawEngineMetal::InitDeviceObjects() {
rm_ = (MetalRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
id<MTLDevice> device = rm_->Device();
MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new];
sd.sAddressMode = MTLSamplerAddressModeClampToEdge;
sd.tAddressMode = MTLSamplerAddressModeClampToEdge;
sd.rAddressMode = MTLSamplerAddressModeClampToEdge;
samplerNearest_ = [device newSamplerStateWithDescriptor:sd];
sd.minFilter = MTLSamplerMinMagFilterLinear;
sd.magFilter = MTLSamplerMinMagFilterLinear;
samplerLinear_ = [device newSamplerStateWithDescriptor:sd];
nullTexture_ = (__bridge id<MTLTexture>)(void *)draw_->GetNativeObject(Draw::NativeObject::NULL_IMAGEVIEW);
}
void DrawEngineMetal::DeviceLost() {
ClearPipelines();
depthStencilCache_.clear();
vertexDescriptors_.clear();
if (draw_) {
draw_->SetInvalidationCallback(InvalidationCallback());
invalidationCallbackInstalled_ = false;
}
boundSecondary_ = nil;
boundDepal_ = nil;
draw_ = nullptr;
rm_ = nullptr;
}
void DrawEngineMetal::DeviceRestore(Draw::DrawContext *draw) {
draw_ = draw;
InitDeviceObjects();
}
void DrawEngineMetal::ClearPipelines() {
pipelines_.Iterate([&](const MetalPipelineKey &, Pipeline *pipeline) {
if (rm_) {
rm_->DeferRelease(pipeline->state);
}
delete pipeline;
});
pipelines_.Clear();
lastPipeline_ = nullptr;
}
void DrawEngineMetal::BeginFrame() {
DrawEngineCommon::BeginFrame();
if (!invalidationCallbackInstalled_) {
draw_->SetInvalidationCallback(std::bind(&DrawEngineMetal::Invalidate, this, std::placeholders::_1));
invalidationCallbackInstalled_ = true;
}
lastPipeline_ = nullptr;
boundDepal_ = nil;
boundSecondary_ = nil;
baseUBO_ = MetalPushAlloc{};
lightUBO_ = MetalPushAlloc{};
gstate_c.Dirty(DIRTY_TEXTURE_IMAGE);
AssertEmpty();
}
void DrawEngineMetal::Invalidate(InvalidationCallbackFlags flags) {
if (flags & InvalidationCallbackFlags::RENDER_PASS_STATE) {
// The render target might have a different format. The encoder state itself is tracked by the render manager.
gstate_c.Dirty(DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_DEPTHSTENCIL_STATE | DIRTY_BLEND_STATE | DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS);
lastPipeline_ = nullptr;
}
}
id<MTLDepthStencilState> DrawEngineMetal::GetDepthStencilState(const MetalDepthStencilKey &key) {
auto iter = depthStencilCache_.find(key.value);
if (iter != depthStencilCache_.end()) {
return iter->second;
}
MTLDepthStencilDescriptor *dd = [MTLDepthStencilDescriptor new];
if (key.depthTestEnable) {
dd.depthCompareFunction = compareToMetal[key.depthCompare];
dd.depthWriteEnabled = key.depthWriteEnable;
} else {
dd.depthCompareFunction = MTLCompareFunctionAlways;
dd.depthWriteEnabled = NO;
}
if (key.stencilTestEnable) {
MTLStencilDescriptor *sd = [MTLStencilDescriptor new];
sd.stencilCompareFunction = compareToMetal[key.stencilCompare];
sd.stencilFailureOperation = stencilOpToMetal[key.stencilFail];
sd.depthFailureOperation = stencilOpToMetal[key.stencilDepthFail];
sd.depthStencilPassOperation = stencilOpToMetal[key.stencilPass];
sd.readMask = (uint32_t)key.compareMask;
sd.writeMask = (uint32_t)key.writeMask;
dd.frontFaceStencil = sd;
dd.backFaceStencil = sd;
}
id<MTLDepthStencilState> dss = [rm_->Device() newDepthStencilStateWithDescriptor:dd];
depthStencilCache_[key.value] = dss;
return dss;
}
DrawEngineMetal::Pipeline *DrawEngineMetal::GetPipeline(const VShaderID &vsid, const FShaderID &fsid, bool useHWTransform) {
MetalPipelineKey key{};
key.vsid = vsid;
key.fsid = fsid;
key.vtxFmtId = useHWTransform ? dec_->decFmt.id : SOFTWARE_TRANSFORM_VTXFMT;
key.blend = blendKey_.value;
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(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;
}
MTLVertexDescriptor *vd;
auto iter = vertexDescriptors_.find(key.vtxFmtId);
if (iter != vertexDescriptors_.end()) {
vd = iter->second;
} else {
vd = CreateVertexDescriptor(decFmt);
vertexDescriptors_[key.vtxFmtId] = vd;
}
MetalBlendKey blend;
blend.value = key.blend;
MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new];
desc.vertexDescriptor = vd;
desc.rasterSampleCount = format.sampleCount;
desc.depthAttachmentPixelFormat = format.depth;
desc.stencilAttachmentPixelFormat = format.stencil;
if (format.color != MTLPixelFormatInvalid) {
MTLRenderPipelineColorAttachmentDescriptor *att = desc.colorAttachments[0];
att.pixelFormat = format.color;
MTLColorWriteMask mask = MTLColorWriteMaskNone;
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 = 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];
}
}
// 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();
if (gstate_c.IsDirty(DIRTY_BLEND_STATE)) {
if (gstate.isModeClear()) {
blendKey_.blendEnable = false;
blendKey_.eqColor = (uint32_t)BlendEq::ADD;
blendKey_.eqAlpha = (uint32_t)BlendEq::ADD;
blendKey_.srcColor = (uint32_t)BlendFactor::ONE;
blendKey_.srcAlpha = (uint32_t)BlendFactor::ONE;
blendKey_.dstColor = (uint32_t)BlendFactor::ZERO;
blendKey_.dstAlpha = (uint32_t)BlendFactor::ZERO;
blendKey_.colorWriteMask = ClearModeChannelMask();
dynState_.useBlendColor = false;
} else {
pipelineState_.Convert(draw_->GetShaderLanguageDesc().bitwiseOps, gstate_c.Use(GPU_USE_SHADER_BLENDING));
GenericMaskState &maskState = pipelineState_.maskState;
GenericBlendState &blendState = pipelineState_.blendState;
if (pipelineState_.FramebufferRead() && useBufferedRendering) {
ApplyFramebufferRead(&fboTexBindState_);
// The shader takes over the responsibility for blending, so recompute.
ApplyStencilReplaceAndLogicOpIgnoreBlend(blendState.replaceAlphaWithStencil, blendState);
dirtyRequiresRecheck_ |= DIRTY_FRAGMENTSHADER_STATE;
gstate_c.Dirty(DIRTY_FRAGMENTSHADER_STATE);
} else if (fboTexBound_) {
boundSecondary_ = nil;
fboTexBound_ = false;
dirtyRequiresRecheck_ |= DIRTY_FRAGMENTSHADER_STATE;
gstate_c.Dirty(DIRTY_FRAGMENTSHADER_STATE);
}
if (blendState.blendEnabled) {
blendKey_.blendEnable = true;
blendKey_.eqColor = (uint32_t)blendState.eqColor;
blendKey_.eqAlpha = (uint32_t)blendState.eqAlpha;
blendKey_.srcColor = (uint32_t)blendState.srcColor;
blendKey_.srcAlpha = (uint32_t)blendState.srcAlpha;
blendKey_.dstColor = (uint32_t)blendState.dstColor;
blendKey_.dstAlpha = (uint32_t)blendState.dstAlpha;
if (blendState.dirtyShaderBlendFixValues) {
dirtyRequiresRecheck_ |= DIRTY_SHADERBLEND;
gstate_c.Dirty(DIRTY_SHADERBLEND);
}
dynState_.useBlendColor = blendState.useBlendColor;
if (blendState.useBlendColor) {
blendState.blendColorToFloat4(dynState_.blendColor);
}
} else {
blendKey_.blendEnable = false;
blendKey_.eqColor = (uint32_t)BlendEq::ADD;
blendKey_.eqAlpha = (uint32_t)BlendEq::ADD;
blendKey_.srcColor = (uint32_t)BlendFactor::ONE;
blendKey_.srcAlpha = (uint32_t)BlendFactor::ONE;
blendKey_.dstColor = (uint32_t)BlendFactor::ZERO;
blendKey_.dstAlpha = (uint32_t)BlendFactor::ZERO;
dynState_.useBlendColor = false;
}
blendKey_.colorWriteMask = maskState.channelMask;
}
}
if (gstate_c.IsDirty(DIRTY_RASTER_STATE)) {
bool wantCull = !gstate.isModeClear() && prim != GE_PRIM_RECTANGLES && prim > GE_PRIM_LINE_STRIP && gstate.isCullEnabled();
dynState_.cull = wantCull ? (gstate.getCullMode() ? MTLCullModeFront : MTLCullModeBack) : MTLCullModeNone;
if (gstate.isModeClear() || gstate.isModeThrough()) {
dynState_.depthClamp = false;
} else if (gstate.getDepthRangeMin() == 0 || gstate.getDepthRangeMax() == 65535) {
dynState_.depthClamp = gstate.isDepthClipEnabled() && gstate_c.Use(GPU_USE_DEPTH_CLAMP);
} else {
dynState_.depthClamp = false;
}
}
if (gstate_c.IsDirty(DIRTY_DEPTHSTENCIL_STATE)) {
GenericStencilFuncState stencilState;
ConvertStencilFuncState(stencilState);
MetalDepthStencilKey &key = depthStencilKey_;
key.value = 0;
if (gstate.isModeClear()) {
key.depthTestEnable = true;
key.depthCompare = GE_COMP_ALWAYS;
key.depthWriteEnable = gstate.isClearModeDepthMask();
if (gstate.isClearModeAlphaMask()) {
key.stencilTestEnable = true;
key.stencilCompare = GE_COMP_ALWAYS;
key.stencilPass = GE_STENCILOP_REPLACE;
key.stencilFail = GE_STENCILOP_REPLACE;
key.stencilDepthFail = GE_STENCILOP_REPLACE;
key.writeMask = stencilState.writeMask;
key.compareMask = 0xFF;
// Overridden from software transform for clear rectangles.
dynState_.stencilRef = 0xFF;
}
} else {
if (!IsDepthTestEffectivelyDisabled()) {
key.depthTestEnable = true;
key.depthCompare = gstate.getDepthTestFunction();
key.depthWriteEnable = gstate.isDepthWriteEnabled();
UpdateEverUsedEqualDepth(gstate.getDepthTestFunction());
}
if (stencilState.enabled) {
key.stencilTestEnable = true;
key.stencilCompare = stencilState.testFunc;
key.stencilPass = stencilState.zPass;
key.stencilFail = stencilState.sFail;
key.stencilDepthFail = stencilState.zFail;
key.writeMask = stencilState.writeMask;
key.compareMask = stencilState.testMask;
dynState_.stencilRef = stencilState.testRef;
// Nasty special case for Spongebob and similar, see StateMappingVulkan.cpp.
if (SpongebobDepthInverseConditions(stencilState)) {
blendKey_.blendEnable = true;
blendKey_.eqAlpha = (uint32_t)BlendEq::ADD;
blendKey_.eqColor = (uint32_t)BlendEq::ADD;
blendKey_.srcColor = (uint32_t)BlendFactor::ZERO;
blendKey_.dstColor = (uint32_t)BlendFactor::ZERO;
blendKey_.srcAlpha = (uint32_t)BlendFactor::ZERO;
blendKey_.dstAlpha = (uint32_t)BlendFactor::ZERO;
blendKey_.colorWriteMask = 8;
key.depthCompare = GE_COMP_LESS; // Inverse of GREATER_EQUAL
key.stencilCompare = GE_COMP_ALWAYS;
key.stencilPass = GE_STENCILOP_ZERO;
key.stencilFail = GE_STENCILOP_ZERO;
key.stencilDepthFail = GE_STENCILOP_KEEP;
dirtyRequiresRecheck_ |= DIRTY_BLEND_STATE | DIRTY_DEPTHSTENCIL_STATE;
gstate_c.Dirty(DIRTY_BLEND_STATE | DIRTY_DEPTHSTENCIL_STATE);
}
}
}
}
if (gstate_c.IsDirty(DIRTY_VIEWPORTSCISSOR_STATE)) {
ViewportAndScissor vpAndScissor;
ConvertViewportAndScissor(
framebufferManager_->GetDisplayLayoutConfigCopy(),
useBufferedRendering,
framebufferManager_->GetRenderWidth(), framebufferManager_->GetRenderHeight(),
framebufferManager_->GetTargetBufferWidth(), framebufferManager_->GetTargetBufferHeight(),
vpAndScissor);
dynState_.viewport = MTLViewport{ vpAndScissor.viewportX, vpAndScissor.viewportY, vpAndScissor.viewportW, vpAndScissor.viewportH, 0.0, 1.0 };
dynState_.scissor[0] = vpAndScissor.scissorX;
dynState_.scissor[1] = vpAndScissor.scissorY;
dynState_.scissor[2] = std::max(0, vpAndScissor.scissorW);
dynState_.scissor[3] = std::max(0, vpAndScissor.scissorH);
}
}
void DrawEngineMetal::BindShaderBlendTex() {
if (!gstate.isModeClear() && fboTexBindState_ == FBO_TEX_COPY_BIND_TEX) {
VirtualFramebuffer *curRenderVfb = framebufferManager_->GetCurrentRenderVFB();
bool bindResult = framebufferManager_->BindFramebufferAsColorTexture(1, curRenderVfb, BINDFBCOLOR_MAY_COPY | BINDFBCOLOR_UNCACHED, Draw::ALL_LAYERS);
_dbg_assert_(bindResult);
boundSecondary_ = (__bridge id<MTLTexture>)(void *)draw_->GetNativeObject(Draw::NativeObject::BOUND_TEXTURE1_IMAGEVIEW);
fboTexBound_ = true;
fboTexBindState_ = FBO_TEX_NONE;
// Must dirty blend state here so we re-copy next time. Example: Lunar's spell effects.
dirtyRequiresRecheck_ |= DIRTY_BLEND_STATE;
} else if (gstate.isModeClear() || !fboTexBound_) {
boundSecondary_ = nil;
}
}
bool DrawEngineMetal::ApplyState(const Pipeline *pipeline, bool overrideStencilRef, uint8_t stencilRef) {
if (!rm_->EnsureRenderEncoder()) {
return false;
}
rm_->SetPipeline(pipeline->state);
rm_->SetDepthStencilState(GetDepthStencilState(depthStencilKey_), depthStencilKey_.depthTestEnable || depthStencilKey_.stencilTestEnable);
if (depthStencilKey_.stencilTestEnable) {
rm_->SetStencilRef(overrideStencilRef ? stencilRef : dynState_.stencilRef);
}
if (dynState_.useBlendColor) {
rm_->SetBlendColor(dynState_.blendColor[0], dynState_.blendColor[1], dynState_.blendColor[2], dynState_.blendColor[3]);
}
rm_->SetCullMode(dynState_.cull);
rm_->SetFrontFacing(MTLWindingCounterClockwise);
rm_->SetDepthClip(dynState_.depthClamp ? MTLDepthClipModeClamp : MTLDepthClipModeClip);
rm_->SetViewport(dynState_.viewport);
return rm_->SetScissor(dynState_.scissor[0], dynState_.scissor[1], dynState_.scissor[2], dynState_.scissor[3]);
}
static inline void BindTexture(MetalRenderManager *rm, __unsafe_unretained id<MTLTexture> tex, __unsafe_unretained id<MTLSamplerState> sampler, int index, uint32_t arrayMask) {
if (arrayMask & (1 << index)) {
tex = rm->ArrayView(tex);
}
rm->SetFragmentTexture(tex, index);
rm->SetFragmentSampler(sampler, index);
}
void DrawEngineMetal::BindResources(const Pipeline *pipeline) {
uint64_t dirty = shaderManager_->UpdateUniforms(framebufferManager_->UseBufferedRendering(), false);
if ((dirty & DIRTY_BASE_UNIFORMS) || !baseUBO_.buffer || uboFrame_ != rm_->FrameNumber()) {
baseUBO_ = rm_->Push().Push(&shaderManager_->BaseUniforms(), sizeof(UB_VS_FS_Base), 256);
}
if ((dirty & DIRTY_LIGHT_UNIFORMS) || !lightUBO_.buffer || uboFrame_ != rm_->FrameNumber()) {
lightUBO_ = rm_->Push().Push(&shaderManager_->LightUniforms(), sizeof(UB_VS_Lights), 256);
}
uboFrame_ = rm_->FrameNumber();
rm_->SetVertexBuffer(baseUBO_.buffer, baseUBO_.offset, METAL_DRAW_BINDING_DYNUBO_BASE);
rm_->SetFragmentBuffer(baseUBO_.buffer, baseUBO_.offset, METAL_DRAW_BINDING_DYNUBO_BASE);
rm_->SetVertexBuffer(lightUBO_.buffer, lightUBO_.offset, METAL_DRAW_BINDING_DYNUBO_LIGHT);
// Only the slots the fragment shader actually uses.
const uint32_t used = pipeline->fragmentTextureMask;
const uint32_t arrayMask = pipeline->fragmentArrayTextureMask;
if (used & (1 << METAL_DRAW_BINDING_TEXTURE)) {
__unsafe_unretained id<MTLTexture> tex = textureCache_->curTexture;
__unsafe_unretained id<MTLSamplerState> sampler = textureCache_->curSampler;
BindTexture(rm_, tex ? tex : nullTexture_, sampler ? sampler : samplerNearest_, METAL_DRAW_BINDING_TEXTURE, arrayMask);
}
if (used & (1 << METAL_DRAW_BINDING_2ND_TEXTURE)) {
BindTexture(rm_, boundSecondary_ ? boundSecondary_ : nullTexture_, samplerNearest_, METAL_DRAW_BINDING_2ND_TEXTURE, arrayMask);
}
if (used & (1 << METAL_DRAW_BINDING_DEPAL_TEXTURE)) {
BindTexture(rm_, boundDepal_ ? boundDepal_ : nullTexture_, boundDepalSmoothed_ ? samplerLinear_ : samplerNearest_, METAL_DRAW_BINDING_DEPAL_TEXTURE, arrayMask);
}
}
void DrawEngineMetal::DrawPrimitives(MTLPrimitiveType prim, __unsafe_unretained id<MTLBuffer> vbuf, uint32_t vbOffset, int count, const uint16_t *inds) {
rm_->SetVertexBuffer(vbuf, vbOffset, METAL_VERTEX_BUFFER_INDEX);
if (inds) {
MetalPushAlloc ialloc = rm_->Push().Push(inds, count * sizeof(uint16_t), 4);
rm_->DrawIndexed(prim, count, ialloc.buffer, ialloc.offset);
} else {
rm_->Draw(prim, 0, count);
}
}
void DrawEngineMetal::ResetAfterSkippedDraw() {
indexGen.Reset();
numDecodedVerts_ = 0;
numDrawVerts_ = 0;
numDrawInds_ = 0;
vertexCountInDrawCalls_ = 0;
expandedVertsInDrawCalls_ = 0;
numVertsToDecode_ = 0;
decodeIndsCounter_ = 0;
decodeVertsCounter_ = 0;
gstate_c.vertexFullAlpha = true;
}
void DrawEngineMetal::Flush() {
if (!numDrawVerts_) {
return;
}
PROFILE_THIS_SCOPE("Flush");
GEPrimitiveType prim = prevPrim_;
// Metal's provoking vertex is the first one, so flat shading goes through software transform.
bool provokingVertexOk = gstate.getShadeMode() != GE_SHADE_FLAT;
bool useHWTransform = CanUseHardwareTransform(prim) && provokingVertexOk;
if (clipInfoFlags_ & ClipInfoFlags::Valid) {
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
}
}
if (clipInfoFlags_ != lastClipInfoFlags_) {
ClipInfoFlags changed = (ClipInfoFlags)((u32)clipInfoFlags_ ^ (u32)lastClipInfoFlags_);
if (changed & (ClipInfoFlags::DepthClampFragment | ClipInfoFlags::MinMaxZDiscard)) {
gstate_c.Dirty(DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE | DIRTY_RASTER_STATE);
}
if (changed & ClipInfoFlags::FlatZ) {
gstate_c.Dirty(DIRTY_TEXTURE_PARAMS);
}
lastClipInfoFlags_ = clipInfoFlags_;
}
if (useHWTransform != lastUseHwTransform_) {
gstate_c.Dirty(DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE | DIRTY_RASTER_STATE);
lastUseHwTransform_ = useHWTransform;
}
auto updateVertexFullAlpha = [&]() {
bool hasColor = (lastVType_ & GE_VTYPE_COL_MASK) != GE_VTYPE_COL_NONE;
if (gstate.isModeThrough()) {
gstate_c.vertexFullAlpha = gstate_c.vertexFullAlpha && (hasColor || gstate.getMaterialAmbientA() == 255);
} else {
gstate_c.vertexFullAlpha = gstate_c.vertexFullAlpha && ((hasColor && (gstate.materialupdate & 1)) || gstate.getMaterialAmbientA() == 255) && (!gstate.isLightingEnabled() || gstate.getAmbientA() == 255);
}
};
auto updatePipeline = [&](bool hwTransform) -> Pipeline * {
if (!lastPipeline_ || gstate_c.IsDirty(DIRTY_BLEND_STATE | DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_RASTER_STATE | DIRTY_DEPTHSTENCIL_STATE | DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE) || prim != lastPrim_) {
if (prim != lastPrim_ || gstate_c.IsDirty(DIRTY_BLEND_STATE | DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_RASTER_STATE | DIRTY_DEPTHSTENCIL_STATE)) {
ConvertStateToKeys(prim);
}
VShaderID vsid;
FShaderID fsid;
shaderManager_->GetShaderIDs(prim, dec_->VertexType(), &vsid, &fsid, pipelineState_, hwTransform, clipInfoFlags_);
// This might copy the framebuffer, which interrupts the render pass. So it goes before the pipeline lookup,
// which depends on the render target.
BindShaderBlendTex();
lastPipeline_ = GetPipeline(vsid, fsid, hwTransform);
gstate_c.Clean(DIRTY_BLEND_STATE | DIRTY_DEPTHSTENCIL_STATE | DIRTY_RASTER_STATE | DIRTY_VIEWPORTSCISSOR_STATE);
gstate_c.Dirty(dirtyRequiresRecheck_);
dirtyRequiresRecheck_ = 0;
}
lastPrim_ = prim;
return lastPipeline_;
};
if (useHWTransform) {
MetalPushAlloc valloc;
if (lastVType_ & GE_VTYPE_WEIGHT_MASK) {
// If skinning, we're predecoding into "decoded". So make sure we're done, then push that content.
DecodeVerts(dec_, decoded_);
valloc = rm_->Push().Push(decoded_, numDecodedVerts_ * dec_->GetDecVtxFmt().stride, 16);
} else {
int vertsToDecode = ComputeNumVertsToDecode();
valloc = rm_->Push().Allocate(vertsToDecode * dec_->GetDecVtxFmt().stride, 16);
DecodeVerts(dec_, valloc.ptr);
}
int vertexCount;
int maxIndex;
bool useElements;
DecodeIndsAndGetData(&prim, &vertexCount, &maxIndex, &useElements, false);
gpuStats.perFrame.numVertsDrawn += vertexCount;
updateVertexFullAlpha();
if (gstate_c.IsDirty(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS) && !gstate.isModeClear() && gstate.isTextureMapEnabled()) {
gstate_c.Clean(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS);
const TextureApplyResult textureResult = textureCache_->ApplyTexture(true);
textureCache_->ApplySampler(textureResult, clipInfoFlags_ & ClipInfoFlags::FlatZ, false);
} else if (gstate.getTextureAddress(0) == (gstate.getFrameBufRawAddress() | 0x04000000)) {
// This catches the case of clearing a texture.
gstate_c.Dirty(DIRTY_TEXTURE_IMAGE);
}
Pipeline *pipeline = updatePipeline(true);
if (!pipeline || !ApplyState(pipeline, false, 0)) {
ResetAfterSkippedDraw();
return;
}
BindResources(pipeline);
MTLPrimitiveType mtlPrim = prim == GE_PRIM_TRIANGLE_STRIP ? MTLPrimitiveTypeTriangleStrip : MTLPrimitiveTypeTriangle;
DrawPrimitives(mtlPrim, valloc.buffer, valloc.offset, vertexCount, useElements ? decIndex_ : nullptr);
if (useDepthRaster_) {
DepthRasterSubmitRaw(prim, dec_, dec_->VertexType(), vertexCount);
}
} else {
gpuStats.perFrame.numSoftTransformedDraws++;
PROFILE_THIS_SCOPE("soft");
DecodeVerts(dec_, decoded_);
int vertexCount = DecodeInds();
updateVertexFullAlpha();
gpuStats.perFrame.numVertsDrawn += vertexCount;
prim = IndexGenerator::GeneralPrim((GEPrimitiveType)drawInds_[0].prim);
if (useDepthRaster_) {
DepthRasterPredecoded(prim, decoded_, numDecodedVerts_, dec_, vertexCount);
}
u16 *inds = decIndex_;
if (gstate.getShadeMode() == GE_SHADE_FLAT) {
// Rotate the index buffer to simulate a different provoking vertex.
IndexBufferProvokingLastToFirst(prim, inds, vertexCount);
}
bool textureNeedsApply = false;
TextureApplyResult textureResult;
if (gstate_c.IsDirty(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS) && !gstate.isModeClear() && gstate.isTextureMapEnabled()) {
gstate_c.Clean(DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS);
gstate_c.dstSquared = false;
textureResult = textureCache_->ApplyTexture(true);
// NOTE: After this is set, we MUST call ApplySampler before returning.
textureNeedsApply = true;
} else if (gstate.getTextureAddress(0) == (gstate.getFrameBufRawAddress() | 0x04000000)) {
gstate_c.Dirty(DIRTY_TEXTURE_IMAGE);
}
SoftwareTransformResult result{};
SoftwareTransformParams params{};
params.decoded = decoded_;
params.transformed = transformed_;
params.transformedExpanded = transformedExpanded_;
// Clears through the render pass don't respect the scissor, so only allow them when buffered.
params.allowClear = framebufferManager_->UseBufferedRendering();
params.allowSeparateAlphaClear = false;
params.everUsedEqualDepth = everUsedEqualDepth_;
params.clipInfoFlags = clipInfoFlags_;
params.trianglesReversed = BatchTrianglesReversed();
const SoftwareTransformAction action = RunSoftwareTransform(params, prim, dec_->VertexType(), dec_->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
if (textureNeedsApply) {
textureCache_->ApplySampler(textureResult, clipInfoFlags_ & ClipInfoFlags::FlatZ, result.pixelMapped);
if (gstate_c.dstSquared) {
gstate_c.Dirty(DIRTY_BLEND_STATE);
}
}
if (action == SW_DRAW_INDEXED) {
Pipeline *pipeline = updatePipeline(false);
if (!pipeline || !ApplyState(pipeline, result.setStencil, result.stencilValue)) {
ResetAfterSkippedDraw();
return;
}
// UpdateUniforms in BindResources would otherwise use pixelMapped = false.
uint64_t dirty = shaderManager_->UpdateUniforms(framebufferManager_->UseBufferedRendering(), result.pixelMapped);
if (dirty & DIRTY_BASE_UNIFORMS) {
baseUBO_ = MetalPushAlloc{};
}
if (dirty & DIRTY_LIGHT_UNIFORMS) {
lightUBO_ = MetalPushAlloc{};
}
BindResources(pipeline);
MetalPushAlloc valloc = rm_->Push().Push(result.drawBuffer, result.drawVertexCount * sizeof(TransformedVertex), 16);
DrawPrimitives(MTLPrimitiveTypeTriangle, valloc.buffer, valloc.offset, result.drawIndexCount, inds);
} else if (action == SW_CLEAR) {
// Note: we won't get here if the clear is alpha but not color, or color but not alpha.
bool clearColor = gstate.isClearModeColorMask();
bool clearAlpha = gstate.isClearModeAlphaMask(); // and stencil
bool clearDepth = gstate.isClearModeDepthMask();
Draw::Aspect mask = Draw::Aspect::NO_BIT;
if (clearColor || clearAlpha) mask |= Draw::Aspect::COLOR_BIT;
if (clearDepth) mask |= Draw::Aspect::DEPTH_BIT;
if (clearAlpha) mask |= Draw::Aspect::STENCIL_BIT;
// The alpha channel and the stencil channel are shared on the PSP.
draw_->Clear(mask, result.color, result.depth, result.color >> 24);
if (gstate_c.Use(GPU_USE_CLEAR_RAM_HACK) && gstate.isClearModeColorMask() && (gstate.isClearModeAlphaMask() || gstate.FrameBufFormat() == GE_FORMAT_565)) {
int scissorX1 = gstate.getScissorX1();
int scissorY1 = gstate.getScissorY1();
int scissorX2 = gstate.getScissorX2() + 1;
int scissorY2 = gstate.getScissorY2() + 1;
framebufferManager_->ApplyClearToMemory(scissorX1, scissorY1, scissorX2, scissorY2, result.color);
}
}
}
ResetAfterDrawInline();
framebufferManager_->SetColorUpdated(gstate_c.skipDrawReason);
gpuCommon_->NotifyFlush();
}