mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-12 00:59:41 +01:00
- Metal: when a layer has no drawable to give (it can take a second to say so), don't ask again in the same frame for every pass to it. - Swapchains other than the primary are created, resized and destroyed between frames (Metal asserts it), and destroyed with the last reference to them (ReleaseAssertLast, Metal and Vulkan). - BindFramebufferSamplesAsTexture asserts it isn't given a swapchain, like BindFramebufferAsTexture. - An offscreen Metal swapchain can't be resized, asserted. - VulkanRenderManager asserts that no additional swapchain outlives it. - Native_RemoveOutputAndWait asserts it isn't called on the thread that handles the requests, where it would always time out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1631 lines
60 KiB
Plaintext
1631 lines
60 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/.
|
|
|
|
// thin3d on Metal. Shaders are Vulkan GLSL (the same ones the Vulkan backend gets), translated to MSL,
|
|
// so bindings follow the Vulkan layout: the UBO is at buffer index 0 and texture slot n is at index n + 1.
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include <algorithm>
|
|
#include <cfloat>
|
|
#include <map>
|
|
#include <unordered_map>
|
|
|
|
#include "Common/GPU/thin3d.h"
|
|
#include "Common/GPU/thin3d_create.h"
|
|
#include "Common/GPU/Metal/MetalRenderManager.h"
|
|
#include "Common/Data/Convert/ColorConv.h"
|
|
#include "Common/Data/Convert/SmallDataConvert.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/TimeUtil.h"
|
|
|
|
namespace Draw {
|
|
|
|
static constexpr int THIN3D_TEXTURE_BINDING_BASE = 1;
|
|
|
|
static MTLPixelFormat DataFormatToMetal(DataFormat format) {
|
|
switch (format) {
|
|
case DataFormat::R8_UNORM: return MTLPixelFormatR8Unorm;
|
|
case DataFormat::R8G8_UNORM: return MTLPixelFormatRG8Unorm;
|
|
case DataFormat::R8G8B8A8_UNORM: return MTLPixelFormatRGBA8Unorm;
|
|
case DataFormat::R8G8B8A8_UNORM_SRGB: return MTLPixelFormatRGBA8Unorm_sRGB;
|
|
case DataFormat::B8G8R8A8_UNORM: return MTLPixelFormatBGRA8Unorm;
|
|
case DataFormat::B8G8R8A8_UNORM_SRGB: return MTLPixelFormatBGRA8Unorm_sRGB;
|
|
case DataFormat::R8G8B8A8_SNORM: return MTLPixelFormatRGBA8Snorm;
|
|
case DataFormat::R8G8B8A8_UINT: return MTLPixelFormatRGBA8Uint;
|
|
case DataFormat::R8G8B8A8_SINT: return MTLPixelFormatRGBA8Sint;
|
|
case DataFormat::R16_UNORM: return MTLPixelFormatR16Unorm;
|
|
case DataFormat::R16_FLOAT: return MTLPixelFormatR16Float;
|
|
case DataFormat::R16G16_FLOAT: return MTLPixelFormatRG16Float;
|
|
case DataFormat::R16G16B16A16_FLOAT: return MTLPixelFormatRGBA16Float;
|
|
case DataFormat::R32_FLOAT: return MTLPixelFormatR32Float;
|
|
case DataFormat::R32G32_FLOAT: return MTLPixelFormatRG32Float;
|
|
case DataFormat::R32G32B32A32_FLOAT: return MTLPixelFormatRGBA32Float;
|
|
case DataFormat::BC1_RGBA_UNORM_BLOCK: return MTLPixelFormatBC1_RGBA;
|
|
case DataFormat::BC2_UNORM_BLOCK: return MTLPixelFormatBC2_RGBA;
|
|
case DataFormat::BC3_UNORM_BLOCK: return MTLPixelFormatBC3_RGBA;
|
|
case DataFormat::BC4_UNORM_BLOCK: return MTLPixelFormatBC4_RUnorm;
|
|
case DataFormat::BC5_UNORM_BLOCK: return MTLPixelFormatBC5_RGUnorm;
|
|
case DataFormat::BC7_UNORM_BLOCK: return MTLPixelFormatBC7_RGBAUnorm;
|
|
case DataFormat::ETC2_R8G8B8_UNORM_BLOCK: return MTLPixelFormatETC2_RGB8;
|
|
case DataFormat::ETC2_R8G8B8A1_UNORM_BLOCK: return MTLPixelFormatETC2_RGB8A1;
|
|
case DataFormat::ETC2_R8G8B8A8_UNORM_BLOCK: return MTLPixelFormatEAC_RGBA8;
|
|
case DataFormat::ASTC_4x4_UNORM_BLOCK: return MTLPixelFormatASTC_4x4_LDR;
|
|
case DataFormat::S8: return MTLPixelFormatStencil8;
|
|
case DataFormat::D16: return MTLPixelFormatDepth16Unorm;
|
|
case DataFormat::D32F: return MTLPixelFormatDepth32Float;
|
|
case DataFormat::D32F_S8: return MTLPixelFormatDepth32Float_Stencil8;
|
|
default: return MTLPixelFormatInvalid;
|
|
}
|
|
}
|
|
|
|
static MTLVertexFormat DataFormatToMetalVertex(DataFormat format) {
|
|
switch (format) {
|
|
case DataFormat::R32_FLOAT: return MTLVertexFormatFloat;
|
|
case DataFormat::R32G32_FLOAT: return MTLVertexFormatFloat2;
|
|
case DataFormat::R32G32B32_FLOAT: return MTLVertexFormatFloat3;
|
|
case DataFormat::R32G32B32A32_FLOAT: return MTLVertexFormatFloat4;
|
|
case DataFormat::R8G8B8A8_UNORM: return MTLVertexFormatUChar4Normalized;
|
|
case DataFormat::R8G8B8A8_SNORM: return MTLVertexFormatChar4Normalized;
|
|
case DataFormat::R8G8B8A8_UINT: return MTLVertexFormatUChar4;
|
|
case DataFormat::R8G8B8A8_SINT: return MTLVertexFormatChar4;
|
|
case DataFormat::R16_FLOAT: return MTLVertexFormatHalf;
|
|
case DataFormat::R16G16_FLOAT: return MTLVertexFormatHalf2;
|
|
case DataFormat::R16G16B16A16_FLOAT: return MTLVertexFormatHalf4;
|
|
default: return MTLVertexFormatInvalid;
|
|
}
|
|
}
|
|
|
|
static const MTLCompareFunction compareToMetal[] = {
|
|
MTLCompareFunctionNever,
|
|
MTLCompareFunctionLess,
|
|
MTLCompareFunctionEqual,
|
|
MTLCompareFunctionLessEqual,
|
|
MTLCompareFunctionGreater,
|
|
MTLCompareFunctionNotEqual,
|
|
MTLCompareFunctionGreaterEqual,
|
|
MTLCompareFunctionAlways,
|
|
};
|
|
|
|
static const MTLStencilOperation stencilOpToMetal[] = {
|
|
MTLStencilOperationKeep,
|
|
MTLStencilOperationZero,
|
|
MTLStencilOperationReplace,
|
|
MTLStencilOperationIncrementClamp,
|
|
MTLStencilOperationDecrementClamp,
|
|
MTLStencilOperationInvert,
|
|
MTLStencilOperationIncrementWrap,
|
|
MTLStencilOperationDecrementWrap,
|
|
};
|
|
|
|
static const MTLBlendOperation blendOpToMetal[] = {
|
|
MTLBlendOperationAdd,
|
|
MTLBlendOperationSubtract,
|
|
MTLBlendOperationReverseSubtract,
|
|
MTLBlendOperationMin,
|
|
MTLBlendOperationMax,
|
|
};
|
|
|
|
static const MTLBlendFactor blendFactorToMetal[] = {
|
|
MTLBlendFactorZero,
|
|
MTLBlendFactorOne,
|
|
MTLBlendFactorSourceColor,
|
|
MTLBlendFactorOneMinusSourceColor,
|
|
MTLBlendFactorDestinationColor,
|
|
MTLBlendFactorOneMinusDestinationColor,
|
|
MTLBlendFactorSourceAlpha,
|
|
MTLBlendFactorOneMinusSourceAlpha,
|
|
MTLBlendFactorDestinationAlpha,
|
|
MTLBlendFactorOneMinusDestinationAlpha,
|
|
MTLBlendFactorBlendColor,
|
|
MTLBlendFactorOneMinusBlendColor,
|
|
MTLBlendFactorBlendAlpha,
|
|
MTLBlendFactorOneMinusBlendAlpha,
|
|
MTLBlendFactorSource1Color,
|
|
MTLBlendFactorOneMinusSource1Color,
|
|
MTLBlendFactorSource1Alpha,
|
|
MTLBlendFactorOneMinusSource1Alpha,
|
|
};
|
|
|
|
static const MTLPrimitiveType primToMetal[] = {
|
|
MTLPrimitiveTypePoint,
|
|
MTLPrimitiveTypeLine,
|
|
MTLPrimitiveTypeLineStrip,
|
|
MTLPrimitiveTypeTriangle,
|
|
MTLPrimitiveTypeTriangleStrip,
|
|
MTLPrimitiveTypeTriangle, // Fans aren't supported.
|
|
};
|
|
|
|
static MTLSamplerAddressMode AddressModeToMetal(TextureAddressMode mode) {
|
|
switch (mode) {
|
|
case TextureAddressMode::REPEAT: return MTLSamplerAddressModeRepeat;
|
|
case TextureAddressMode::REPEAT_MIRROR: return MTLSamplerAddressModeMirrorRepeat;
|
|
case TextureAddressMode::CLAMP_TO_EDGE: return MTLSamplerAddressModeClampToEdge;
|
|
case TextureAddressMode::CLAMP_TO_BORDER: return MTLSamplerAddressModeClampToBorderColor;
|
|
}
|
|
return MTLSamplerAddressModeClampToEdge;
|
|
}
|
|
|
|
static MTLClearColor ClearColorFromRGBA(uint32_t color) {
|
|
float c[4];
|
|
Uint8x4ToFloat4(c, color);
|
|
return MTLClearColorMake(c[0], c[1], c[2], c[3]);
|
|
}
|
|
|
|
static MTLLoadAction RPActionToMetal(RPAction action) {
|
|
switch (action) {
|
|
case RPAction::CLEAR: return MTLLoadActionClear;
|
|
case RPAction::DONT_CARE: return MTLLoadActionDontCare;
|
|
case RPAction::KEEP:
|
|
default:
|
|
return MTLLoadActionLoad;
|
|
}
|
|
}
|
|
|
|
class MetalBlendState : public BlendState {
|
|
public:
|
|
BlendStateDesc desc;
|
|
};
|
|
|
|
class MetalDepthStencilState : public DepthStencilState {
|
|
public:
|
|
DepthStencilStateDesc desc;
|
|
};
|
|
|
|
class MetalRasterState : public RasterState {
|
|
public:
|
|
MTLCullMode cull = MTLCullModeNone;
|
|
MTLWinding winding = MTLWindingCounterClockwise;
|
|
};
|
|
|
|
class MetalSamplerState : public SamplerState {
|
|
public:
|
|
MetalSamplerState(MetalRenderManager *rm) : rm_(rm) {}
|
|
~MetalSamplerState() {
|
|
rm_->DeferRelease(sampler);
|
|
}
|
|
id<MTLSamplerState> sampler;
|
|
|
|
private:
|
|
MetalRenderManager *rm_;
|
|
};
|
|
|
|
class MetalInputLayout : public InputLayout {
|
|
public:
|
|
MTLVertexDescriptor *descriptor;
|
|
int stride = 0;
|
|
};
|
|
|
|
class MetalShaderModule : public ShaderModule {
|
|
public:
|
|
MetalShaderModule(ShaderStage stage, std::string_view tag) : stage_(stage), tag_(tag) {}
|
|
ShaderStage GetStage() const override { return stage_; }
|
|
|
|
id<MTLFunction> function;
|
|
MetalTranslatedShader info;
|
|
ShaderStage stage_;
|
|
std::string tag_;
|
|
};
|
|
|
|
class MetalPipeline : public Pipeline {
|
|
public:
|
|
MetalPipeline(MetalRenderManager *rm) : rm_(rm) {}
|
|
~MetalPipeline() {
|
|
if (vs) vs->Release();
|
|
if (fs) fs->Release();
|
|
delete[] ubo;
|
|
for (auto &iter : psos) {
|
|
rm_->DeferRelease(iter.second);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
AutoRef<MetalInputLayout> input;
|
|
AutoRef<MetalBlendState> blend;
|
|
AutoRef<MetalDepthStencilState> depthStencil;
|
|
AutoRef<MetalRasterState> raster;
|
|
MTLPrimitiveType prim = MTLPrimitiveTypeTriangle;
|
|
uint8_t *ubo = nullptr;
|
|
size_t uboSize = 0;
|
|
std::string tag;
|
|
std::unordered_map<uint32_t, MetalPipelineState *> psos;
|
|
|
|
private:
|
|
MetalRenderManager *rm_;
|
|
};
|
|
|
|
MetalPipelineState *MetalPipeline::GetPSO(id<MTLDevice> device, const MetalRenderTargetFormat &format) {
|
|
uint32_t key = format.Key();
|
|
auto iter = psos.find(key);
|
|
if (iter != psos.end()) {
|
|
return iter->second;
|
|
}
|
|
|
|
MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new];
|
|
desc.label = [NSString stringWithUTF8String:tag.c_str()];
|
|
desc.vertexFunction = vs->function;
|
|
desc.fragmentFunction = fs ? fs->function : nil;
|
|
if (input) {
|
|
desc.vertexDescriptor = input->descriptor;
|
|
}
|
|
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;
|
|
const BlendStateDesc &b = blend->desc;
|
|
MTLColorWriteMask mask = MTLColorWriteMaskNone;
|
|
if (b.colorMask & COLOR_MASK_R) mask |= MTLColorWriteMaskRed;
|
|
if (b.colorMask & COLOR_MASK_G) mask |= MTLColorWriteMaskGreen;
|
|
if (b.colorMask & COLOR_MASK_B) mask |= MTLColorWriteMaskBlue;
|
|
if (b.colorMask & COLOR_MASK_A) mask |= MTLColorWriteMaskAlpha;
|
|
att.writeMask = mask;
|
|
att.blendingEnabled = b.enabled;
|
|
if (b.enabled) {
|
|
att.rgbBlendOperation = blendOpToMetal[(int)b.eqCol];
|
|
att.alphaBlendOperation = blendOpToMetal[(int)b.eqAlpha];
|
|
att.sourceRGBBlendFactor = blendFactorToMetal[(int)b.srcCol];
|
|
att.destinationRGBBlendFactor = blendFactorToMetal[(int)b.dstCol];
|
|
att.sourceAlphaBlendFactor = blendFactorToMetal[(int)b.srcAlpha];
|
|
att.destinationAlphaBlendFactor = blendFactorToMetal[(int)b.dstAlpha];
|
|
}
|
|
}
|
|
|
|
NSError *error = nil;
|
|
id<MTLRenderPipelineState> pso = [device newRenderPipelineStateWithDescriptor:desc error:&error];
|
|
if (!pso) {
|
|
ERROR_LOG(Log::G3D, "Failed to create pipeline '%s' (color %s, depth %s): %s", tag.c_str(),
|
|
MetalPixelFormatToString(format.color), MetalPixelFormatToString(format.depth),
|
|
error ? [[error localizedDescription] UTF8String] : "?");
|
|
}
|
|
MetalPipelineState *state = nil;
|
|
if (pso) {
|
|
state = [MetalPipelineState new];
|
|
[state setPipeline:pso];
|
|
}
|
|
psos[key] = state;
|
|
return state;
|
|
}
|
|
|
|
class MetalBuffer : public Buffer {
|
|
public:
|
|
MetalBuffer(size_t size) : data(size) {}
|
|
|
|
// Buffers are small and updated every frame where they're used at all, so they live in CPU
|
|
// memory and get copied to the push buffer when (re)used. That makes updating them safe no matter
|
|
// what the GPU is doing.
|
|
void Get(MetalRenderManager *rm, id<MTLBuffer> *buffer, uint32_t *offset) {
|
|
if (dirty || pushedFrame != rm->FrameNumber() || !pushed.buffer) {
|
|
pushed = rm->Push().Push(data.data(), data.size(), 16);
|
|
pushedFrame = rm->FrameNumber();
|
|
dirty = false;
|
|
}
|
|
*buffer = pushed.buffer;
|
|
*offset = pushed.offset;
|
|
}
|
|
|
|
std::vector<uint8_t> data;
|
|
MetalPushAlloc pushed;
|
|
uint64_t pushedFrame = 0;
|
|
bool dirty = true;
|
|
};
|
|
|
|
class MetalTexture : public Texture {
|
|
public:
|
|
MetalTexture(MetalRenderManager *rm, const TextureDesc &desc) : rm_(rm) {
|
|
width_ = desc.width;
|
|
height_ = desc.height;
|
|
depth_ = desc.depth;
|
|
format_ = desc.format;
|
|
mipLevels_ = desc.mipLevels;
|
|
}
|
|
|
|
~MetalTexture() {
|
|
rm_->DeferRelease(texture);
|
|
}
|
|
|
|
id<MTLTexture> texture;
|
|
int mipLevels_ = 1;
|
|
uint64_t lastUsedFrame = (uint64_t)-1;
|
|
|
|
private:
|
|
MetalRenderManager *rm_;
|
|
};
|
|
|
|
class MetalFramebuffer : public Framebuffer {
|
|
public:
|
|
MetalFramebuffer(MetalRenderManager *rm, const FramebufferDesc &desc) : rm_(rm), tag_(desc.tag ? desc.tag : "") {
|
|
width_ = desc.width;
|
|
height_ = desc.height;
|
|
layers_ = 1;
|
|
multiSampleLevel_ = desc.multiSampleLevel;
|
|
hasDepthStencil_ = desc.z_stencil;
|
|
}
|
|
void UpdateTag(const char *tag) override {
|
|
tag_ = tag;
|
|
color.label = [NSString stringWithUTF8String:tag];
|
|
if (depth) {
|
|
depth.label = [NSString stringWithFormat:@"%s_depth", tag];
|
|
}
|
|
if (msaaColor) {
|
|
msaaColor.label = [NSString stringWithFormat:@"%s_msaa", tag];
|
|
}
|
|
if (msaaDepth) {
|
|
msaaDepth.label = [NSString stringWithFormat:@"%s_msaa_depth", tag];
|
|
}
|
|
}
|
|
const char *Tag() const override { return tag_.c_str(); }
|
|
|
|
id<MTLTexture> StencilView() {
|
|
if (!stencilView && depth) {
|
|
stencilView = [depth newTextureViewWithPixelFormat:MTLPixelFormatX32_Stencil8];
|
|
}
|
|
return stencilView;
|
|
}
|
|
|
|
~MetalFramebuffer() {
|
|
rm_->DeferRelease(color);
|
|
rm_->DeferRelease(depth);
|
|
rm_->DeferRelease(stencilView);
|
|
rm_->DeferRelease(msaaColor);
|
|
rm_->DeferRelease(msaaDepth);
|
|
}
|
|
|
|
// With MSAA, these are what each pass resolves to, and what's read from (as textures, by readbacks).
|
|
id<MTLTexture> color;
|
|
id<MTLTexture> depth;
|
|
id<MTLTexture> stencilView;
|
|
// With MSAA, what's drawn to. Kept between passes.
|
|
id<MTLTexture> msaaColor;
|
|
id<MTLTexture> msaaDepth;
|
|
|
|
private:
|
|
MetalRenderManager *rm_;
|
|
std::string tag_;
|
|
};
|
|
|
|
class MetalSwapchainFB : public Swapchain {
|
|
public:
|
|
MetalSwapchainFB(MetalSwapchain *swapchain, const char *tag) : swapchain(swapchain), tag_(tag ? tag : "") {
|
|
UpdateSize();
|
|
}
|
|
void UpdateSize() {
|
|
width_ = swapchain->width;
|
|
height_ = swapchain->height;
|
|
}
|
|
const char *Tag() const override { return tag_.c_str(); }
|
|
|
|
MetalSwapchain *swapchain;
|
|
|
|
private:
|
|
std::string tag_;
|
|
};
|
|
|
|
// Bytes only, so there's no padding for memcmp to compare.
|
|
struct MetalDepthStencilKey {
|
|
uint8_t depthTest;
|
|
uint8_t depthWrite;
|
|
uint8_t depthCompare;
|
|
uint8_t stencilEnabled;
|
|
uint8_t failOp;
|
|
uint8_t passOp;
|
|
uint8_t depthFailOp;
|
|
uint8_t compareOp;
|
|
uint8_t writeMask;
|
|
uint8_t compareMask;
|
|
|
|
bool operator < (const MetalDepthStencilKey &other) const {
|
|
return memcmp(this, &other, sizeof(MetalDepthStencilKey)) < 0;
|
|
}
|
|
};
|
|
|
|
class MetalDrawContext : public DrawContext {
|
|
public:
|
|
MetalDrawContext(MetalRenderManager *renderManager);
|
|
~MetalDrawContext();
|
|
|
|
const DeviceCaps &GetDeviceCaps() const override { return caps_; }
|
|
uint32_t GetSupportedShaderLanguages() const override { return (uint32_t)ShaderLanguage::GLSL_VULKAN; }
|
|
uint32_t GetDataFormatSupport(DataFormat fmt) const override;
|
|
std::vector<std::string> GetDeviceList() const override { return { [[device_ name] UTF8String] }; }
|
|
std::vector<std::string> GetFeatureList() const override;
|
|
|
|
DepthStencilState *CreateDepthStencilState(const DepthStencilStateDesc &desc) override;
|
|
BlendState *CreateBlendState(const BlendStateDesc &desc) override;
|
|
SamplerState *CreateSamplerState(const SamplerStateDesc &desc) override;
|
|
RasterState *CreateRasterState(const RasterStateDesc &desc) override;
|
|
InputLayout *CreateInputLayout(const InputLayoutDesc &desc) override;
|
|
ShaderModule *CreateShaderModule(ShaderStage stage, ShaderLanguage language, const uint8_t *data, size_t dataSize, const char *tag) override;
|
|
Pipeline *CreateGraphicsPipeline(const PipelineDesc &desc, const char *tag) override;
|
|
|
|
Buffer *CreateBuffer(size_t size, uint32_t usageFlags) override;
|
|
Texture *CreateTexture(const TextureDesc &desc) override;
|
|
Framebuffer *CreateFramebuffer(const FramebufferDesc &desc) override;
|
|
|
|
void UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t offset, size_t size, UpdateBufferFlags flags) override;
|
|
void UpdateTextureLevels(Texture *texture, const uint8_t **data, TextureCallback initDataCallback, int numLevels) override;
|
|
void UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) override;
|
|
|
|
void CopyFramebufferImage(Framebuffer *src, int level, int x, int y, int z, Framebuffer *dst, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) override;
|
|
bool BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) override;
|
|
bool CopyFramebufferToMemory(Framebuffer *src, Aspect aspect, int x, int y, int w, int h, Draw::DataFormat format, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) override;
|
|
DataFormat PreferredFramebufferReadbackFormat(Framebuffer *src) override {
|
|
return ToSwapchain(src) ? DataFormat::B8G8R8A8_UNORM : DataFormat::R8G8B8A8_UNORM;
|
|
}
|
|
|
|
Swapchain *CreateSwapchain(const SwapchainDesc &desc, std::string *error) override;
|
|
void DestroySwapchain(Swapchain *swapchain) override;
|
|
void ResizeSwapchain(Swapchain *swapchain, int width, int height) override;
|
|
|
|
void BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) override;
|
|
void BindFramebufferAsTexture(Framebuffer *fbo, int binding, Aspect aspect, int layer) override;
|
|
void BindFramebufferSamplesAsTexture(Framebuffer *fbo, int binding, int layer) override;
|
|
void GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) override;
|
|
void InvalidateFramebuffer(FBInvalidationStage stage, Aspect aspects) override {}
|
|
|
|
void SetScissorRect(int left, int top, int width, int height) override;
|
|
void SetViewport(const Viewport &viewport) override;
|
|
void SetBlendFactor(float color[4]) override {
|
|
memcpy(blendFactor_, color, sizeof(blendFactor_));
|
|
}
|
|
void SetStencilParams(uint8_t refValue, uint8_t writeMask, uint8_t compareMask) override {
|
|
stencilRef_ = refValue;
|
|
stencilWriteMask_ = writeMask;
|
|
stencilCompareMask_ = compareMask;
|
|
}
|
|
|
|
void BindSamplerStates(int start, int count, SamplerState **states) override;
|
|
void BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) override;
|
|
void BindVertexBuffer(Buffer *vertexBuffer, int offset) override {
|
|
curVBuffer_ = (MetalBuffer *)vertexBuffer;
|
|
curVBufferOffset_ = offset;
|
|
}
|
|
void BindIndexBuffer(Buffer *indexBuffer, int offset) override {
|
|
curIBuffer_ = (MetalBuffer *)indexBuffer;
|
|
curIBufferOffset_ = offset;
|
|
}
|
|
void BindNativeTexture(int sampler, void *nativeTexture) override;
|
|
void UpdateDynamicUniformBuffer(const void *ub, size_t size) override;
|
|
|
|
void Invalidate(InvalidationFlags flags) override;
|
|
void BindPipeline(Pipeline *pipeline) override {
|
|
curPipeline_ = (MetalPipeline *)pipeline;
|
|
}
|
|
|
|
void Draw(int vertexCount, int offset) override;
|
|
void DrawIndexed(int indexCount, int offset) override;
|
|
void DrawUP(const void *vdata, int vertexCount) override;
|
|
void DrawIndexedUP(const void *vdata, int vertexCount, const void *idata, int indexCount) override;
|
|
void DrawIndexedClippedBatchUP(const void *vdata, int vertexCount, const void *idata, int indexCount, Slice<ClippedDraw> draws, const void *ub, size_t ubSize) override;
|
|
|
|
void BeginFrame(DebugFlags debugFlags) override;
|
|
void EndFrame() override;
|
|
void Present(PresentMode presentMode) override;
|
|
PresentMode GetCurrentPresentMode() const override { return presentMode_; }
|
|
|
|
void Clear(Aspect aspects, uint32_t colorval, float depthVal, int stencilVal) override;
|
|
|
|
std::string GetInfoString(InfoField info) const override;
|
|
uint64_t GetNativeObject(NativeObject obj, void *srcObject) override;
|
|
void HandleEvent(Event ev, int width, int height, void *param1, void *param2) override;
|
|
|
|
void SetInvalidationCallback(InvalidationCallback callback) override {
|
|
invalidationCallback_ = callback;
|
|
}
|
|
int GetFrameCount() override { return frameCount_; }
|
|
std::string GetGpuProfileString() const override;
|
|
|
|
private:
|
|
bool ApplyState();
|
|
void DrawIndexedInternal(id<MTLBuffer> ibuf, uint32_t ibufOffset, int indexCount);
|
|
id<MTLDepthStencilState> GetDepthStencilState(const DepthStencilStateDesc &desc);
|
|
void UploadTextureData(MetalTexture *tex, int level, int x, int y, int w, int h, const uint8_t *data, int srcStride, TextureCallback callback, bool useUploadBuffer);
|
|
void ResetViewportScissor();
|
|
// nullptr and the primary swapchain mean the backbuffer. Returns nullptr for an ordinary framebuffer.
|
|
MetalSwapchain *ToSwapchain(Framebuffer *fb) const {
|
|
if (!fb || fb == primarySwapchain_) {
|
|
return rm_->Backbuffer();
|
|
}
|
|
return fb->IsSwapchain() ? ((MetalSwapchainFB *)fb)->swapchain : nullptr;
|
|
}
|
|
|
|
MetalRenderManager *rm_;
|
|
id<MTLDevice> device_;
|
|
DeviceCaps caps_{};
|
|
|
|
AutoRef<MetalPipeline> curPipeline_;
|
|
MetalBuffer *curVBuffer_ = nullptr;
|
|
int curVBufferOffset_ = 0;
|
|
MetalBuffer *curIBuffer_ = nullptr;
|
|
int curIBufferOffset_ = 0;
|
|
|
|
id<MTLTexture> boundTextures_[MAX_TEXTURE_SLOTS];
|
|
MetalTexture *boundThin3DTextures_[MAX_TEXTURE_SLOTS]{};
|
|
id<MTLSamplerState> boundSamplers_[MAX_TEXTURE_SLOTS];
|
|
id<MTLTexture> nullTexture_;
|
|
id<MTLSamplerState> defaultSampler_;
|
|
|
|
std::map<MetalDepthStencilKey, id<MTLDepthStencilState>> depthStencilCache_;
|
|
|
|
float blendFactor_[4]{};
|
|
uint8_t stencilRef_ = 0;
|
|
uint8_t stencilWriteMask_ = 0xFF;
|
|
uint8_t stencilCompareMask_ = 0xFF;
|
|
|
|
Viewport viewport_{};
|
|
int scissor_[4]{};
|
|
|
|
// For knowing when the UBO needs to be resent.
|
|
uint32_t uboGeneration_ = 0xFFFFFFFF;
|
|
MetalPipeline *uboPipeline_ = nullptr;
|
|
bool uboDirty_ = true;
|
|
|
|
id<MTLBuffer> readbackBuffer_;
|
|
|
|
InvalidationCallback invalidationCallback_;
|
|
PresentMode presentMode_ = PresentMode::FIFO;
|
|
int frameCount_ = FRAME_TIME_HISTORY_LENGTH;
|
|
};
|
|
|
|
MetalDrawContext::MetalDrawContext(MetalRenderManager *renderManager) : rm_(renderManager) {
|
|
device_ = rm_->Device();
|
|
|
|
caps_.coordConvention = CoordConvention::Vulkan;
|
|
caps_.vendor = GPUVendor::VENDOR_APPLE;
|
|
NSString *name = [device_ name];
|
|
if ([name rangeOfString:@"AMD"].location != NSNotFound || [name rangeOfString:@"Radeon"].location != NSNotFound) {
|
|
caps_.vendor = GPUVendor::VENDOR_AMD;
|
|
} else if ([name rangeOfString:@"Intel"].location != NSNotFound) {
|
|
caps_.vendor = GPUVendor::VENDOR_INTEL;
|
|
} else if ([name rangeOfString:@"NVIDIA"].location != NSNotFound) {
|
|
caps_.vendor = GPUVendor::VENDOR_NVIDIA;
|
|
}
|
|
caps_.deviceName = [name UTF8String];
|
|
|
|
caps_.maxTextureSize = 16384;
|
|
caps_.maxClipDistances = 8;
|
|
caps_.maxCullDistances = 0;
|
|
caps_.preferredDepthBufferFormat = DataFormat::D32F_S8;
|
|
caps_.preferredShadowMapFormatLow = DataFormat::D16;
|
|
caps_.preferredShadowMapFormatHigh = DataFormat::D32F;
|
|
caps_.anisoSupported = true;
|
|
caps_.depthRangeMinusOneToOne = false;
|
|
caps_.dualSourceBlend = true;
|
|
caps_.logicOpSupported = false;
|
|
caps_.depthClampSupported = true;
|
|
caps_.framebufferCopySupported = true;
|
|
caps_.framebufferBlitSupported = false;
|
|
caps_.framebufferDepthCopySupported = true;
|
|
caps_.framebufferSeparateDepthCopySupported = true;
|
|
caps_.framebufferDepthBlitSupported = false;
|
|
caps_.framebufferStencilBlitSupported = false;
|
|
// Programmable blending: the fragment shader can read the color it's about to blend with ([[color(0)]]),
|
|
// which saves shader blending a copy of the framebuffer. Apple GPUs only.
|
|
caps_.framebufferFetchSupported = rm_->IsAppleGPU();
|
|
caps_.texture3DSupported = true;
|
|
caps_.fragmentShaderFullPrecisionFloat = true;
|
|
caps_.fragmentShaderInt32Supported = true;
|
|
caps_.fragmentShaderDepthWriteSupported = true;
|
|
caps_.fragmentShaderStencilWriteSupported = true;
|
|
caps_.textureDepthSupported = true;
|
|
caps_.blendMinMaxSupported = true;
|
|
caps_.multiViewSupported = false;
|
|
caps_.isTilingGPU = rm_->IsAppleGPU();
|
|
caps_.sampleRateShadingSupported = false;
|
|
caps_.setMaxFrameLatencySupported = true;
|
|
caps_.textureSwizzleSupported = true;
|
|
caps_.requiresHalfPixelOffset = false;
|
|
caps_.provokingVertexLast = false;
|
|
caps_.verySlowShaderCompiler = false;
|
|
caps_.fullScreenExclusiveSupported = false;
|
|
caps_.samplerLodControl = true;
|
|
// Metal samplers have no LOD bias.
|
|
caps_.samplerLodBias = false;
|
|
caps_.clipPlanesSupported = 0;
|
|
caps_.presentMaxInterval = 1;
|
|
caps_.presentInstantModeChange = true;
|
|
#if PPSSPP_PLATFORM(MAC)
|
|
caps_.presentModesSupported = PresentMode::FIFO | PresentMode::IMMEDIATE;
|
|
#else
|
|
caps_.presentModesSupported = PresentMode::FIFO;
|
|
#endif
|
|
caps_.multipleSwapchains = true;
|
|
caps_.multiSampleLevelsMask = 1;
|
|
for (int level = 1; level <= 3; level++) {
|
|
if ([device_ supportsTextureSampleCount:1 << level]) {
|
|
caps_.multiSampleLevelsMask |= 1 << level;
|
|
}
|
|
}
|
|
caps_.framebufferSampleReadSupported = true;
|
|
|
|
shaderLanguageDesc_.Init(GLSL_VULKAN);
|
|
|
|
MTLTextureDescriptor *nullDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:1 height:1 mipmapped:NO];
|
|
nullDesc.usage = MTLTextureUsageShaderRead;
|
|
nullTexture_ = [device_ newTextureWithDescriptor:nullDesc];
|
|
nullTexture_.label = @"null_texture";
|
|
const uint32_t zero = 0;
|
|
[nullTexture_ replaceRegion:MTLRegionMake2D(0, 0, 1, 1) mipmapLevel:0 withBytes:&zero bytesPerRow:4];
|
|
|
|
MTLSamplerDescriptor *samplerDesc = [MTLSamplerDescriptor new];
|
|
defaultSampler_ = [device_ newSamplerStateWithDescriptor:samplerDesc];
|
|
|
|
for (int i = 0; i < (int)MAX_TEXTURE_SLOTS; i++) {
|
|
boundTextures_[i] = nullTexture_;
|
|
boundSamplers_[i] = defaultSampler_;
|
|
}
|
|
}
|
|
|
|
MetalDrawContext::~MetalDrawContext() {
|
|
DestroyPresets();
|
|
rm_->WaitIdle();
|
|
curPipeline_ = nullptr;
|
|
}
|
|
|
|
std::vector<std::string> MetalDrawContext::GetFeatureList() const {
|
|
std::vector<std::string> features;
|
|
features.push_back(StringFromFormat("Unified memory: %s", rm_->HasUnifiedMemory() ? "yes" : "no"));
|
|
features.push_back(StringFromFormat("Apple GPU family: %s", rm_->IsAppleGPU() ? "yes" : "no"));
|
|
if (@available(iOS 16.4, tvOS 16.4, *)) {
|
|
features.push_back(StringFromFormat("BC texture compression: %s", device_.supportsBCTextureCompression ? "yes" : "no"));
|
|
}
|
|
return features;
|
|
}
|
|
|
|
uint32_t MetalDrawContext::GetDataFormatSupport(DataFormat fmt) const {
|
|
MTLPixelFormat mtl = DataFormatToMetal(fmt);
|
|
uint32_t support = 0;
|
|
if (DataFormatToMetalVertex(fmt) != MTLVertexFormatInvalid) {
|
|
support |= FMT_INPUTLAYOUT;
|
|
}
|
|
if (mtl == MTLPixelFormatInvalid) {
|
|
return support;
|
|
}
|
|
if (DataFormatIsDepthStencil(fmt)) {
|
|
return support | FMT_DEPTHSTENCIL | FMT_TEXTURE;
|
|
}
|
|
int blockSize = 0;
|
|
if (DataFormatIsBlockCompressed(fmt, &blockSize)) {
|
|
bool supported = false;
|
|
switch (fmt) {
|
|
case DataFormat::BC1_RGBA_UNORM_BLOCK:
|
|
case DataFormat::BC2_UNORM_BLOCK:
|
|
case DataFormat::BC3_UNORM_BLOCK:
|
|
case DataFormat::BC4_UNORM_BLOCK:
|
|
case DataFormat::BC5_UNORM_BLOCK:
|
|
case DataFormat::BC7_UNORM_BLOCK:
|
|
if (@available(iOS 16.4, tvOS 16.4, *)) {
|
|
supported = device_.supportsBCTextureCompression;
|
|
}
|
|
break;
|
|
default:
|
|
// ETC2 and ASTC.
|
|
supported = rm_->IsAppleGPU();
|
|
break;
|
|
}
|
|
return supported ? (support | FMT_TEXTURE) : support;
|
|
}
|
|
support |= FMT_TEXTURE | FMT_BLIT;
|
|
switch (fmt) {
|
|
case DataFormat::R8G8B8A8_UNORM:
|
|
case DataFormat::B8G8R8A8_UNORM:
|
|
case DataFormat::R8G8B8A8_UNORM_SRGB:
|
|
case DataFormat::B8G8R8A8_UNORM_SRGB:
|
|
case DataFormat::R16G16B16A16_FLOAT:
|
|
case DataFormat::R8_UNORM:
|
|
case DataFormat::R16_FLOAT:
|
|
case DataFormat::R32_FLOAT:
|
|
support |= FMT_RENDERTARGET | FMT_AUTOGEN_MIPS;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return support;
|
|
}
|
|
|
|
DepthStencilState *MetalDrawContext::CreateDepthStencilState(const DepthStencilStateDesc &desc) {
|
|
MetalDepthStencilState *dss = new MetalDepthStencilState();
|
|
dss->desc = desc;
|
|
return dss;
|
|
}
|
|
|
|
BlendState *MetalDrawContext::CreateBlendState(const BlendStateDesc &desc) {
|
|
MetalBlendState *bs = new MetalBlendState();
|
|
bs->desc = desc;
|
|
return bs;
|
|
}
|
|
|
|
RasterState *MetalDrawContext::CreateRasterState(const RasterStateDesc &desc) {
|
|
MetalRasterState *rs = new MetalRasterState();
|
|
switch (desc.cull) {
|
|
case CullMode::FRONT: rs->cull = MTLCullModeFront; break;
|
|
case CullMode::BACK: rs->cull = MTLCullModeBack; break;
|
|
default: rs->cull = MTLCullModeNone; break;
|
|
}
|
|
rs->winding = desc.frontFace == Facing::CCW ? MTLWindingCounterClockwise : MTLWindingClockwise;
|
|
return rs;
|
|
}
|
|
|
|
SamplerState *MetalDrawContext::CreateSamplerState(const SamplerStateDesc &desc) {
|
|
MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new];
|
|
sd.magFilter = desc.magFilter == TextureFilter::LINEAR ? MTLSamplerMinMagFilterLinear : MTLSamplerMinMagFilterNearest;
|
|
sd.minFilter = desc.minFilter == TextureFilter::LINEAR ? MTLSamplerMinMagFilterLinear : MTLSamplerMinMagFilterNearest;
|
|
sd.mipFilter = desc.mipFilter == TextureFilter::LINEAR ? MTLSamplerMipFilterLinear : MTLSamplerMipFilterNearest;
|
|
sd.sAddressMode = AddressModeToMetal(desc.wrapU);
|
|
sd.tAddressMode = AddressModeToMetal(desc.wrapV);
|
|
sd.rAddressMode = AddressModeToMetal(desc.wrapW);
|
|
sd.maxAnisotropy = std::clamp((int)desc.maxAniso, 1, 16);
|
|
if (desc.shadowCompareEnabled) {
|
|
sd.compareFunction = compareToMetal[(int)desc.shadowCompareFunc];
|
|
}
|
|
switch (desc.borderColor) {
|
|
case BorderColor::OPAQUE_BLACK: sd.borderColor = MTLSamplerBorderColorOpaqueBlack; break;
|
|
case BorderColor::OPAQUE_WHITE: sd.borderColor = MTLSamplerBorderColorOpaqueWhite; break;
|
|
default: sd.borderColor = MTLSamplerBorderColorTransparentBlack; break;
|
|
}
|
|
sd.lodMinClamp = 0.0f;
|
|
sd.lodMaxClamp = FLT_MAX;
|
|
MetalSamplerState *ss = new MetalSamplerState(rm_);
|
|
ss->sampler = [device_ newSamplerStateWithDescriptor:sd];
|
|
return ss;
|
|
}
|
|
|
|
InputLayout *MetalDrawContext::CreateInputLayout(const InputLayoutDesc &desc) {
|
|
MetalInputLayout *layout = new MetalInputLayout();
|
|
MTLVertexDescriptor *vd = [MTLVertexDescriptor vertexDescriptor];
|
|
for (const AttributeDesc &attr : desc.attributes) {
|
|
vd.attributes[attr.location].format = DataFormatToMetalVertex(attr.format);
|
|
vd.attributes[attr.location].offset = attr.offset;
|
|
vd.attributes[attr.location].bufferIndex = METAL_VERTEX_BUFFER_INDEX;
|
|
}
|
|
vd.layouts[METAL_VERTEX_BUFFER_INDEX].stride = desc.stride;
|
|
vd.layouts[METAL_VERTEX_BUFFER_INDEX].stepFunction = MTLVertexStepFunctionPerVertex;
|
|
layout->descriptor = vd;
|
|
layout->stride = desc.stride;
|
|
return layout;
|
|
}
|
|
|
|
ShaderModule *MetalDrawContext::CreateShaderModule(ShaderStage stage, ShaderLanguage language, const uint8_t *data, size_t dataSize, const char *tag) {
|
|
if (language != GLSL_VULKAN) {
|
|
ERROR_LOG(Log::G3D, "Metal: Unsupported shader language %s", ShaderLanguageAsString(language));
|
|
return nullptr;
|
|
}
|
|
std::string source((const char *)data, dataSize);
|
|
MetalShaderModule *module = new MetalShaderModule(stage, tag);
|
|
std::string errorMessage;
|
|
if (!TranslateVulkanGLSLToMSL(stage, source.c_str(), &module->info, &errorMessage)) {
|
|
ERROR_LOG(Log::G3D, "Failed to translate shader %s: %s\n%s", tag, errorMessage.c_str(), LineNumberString(source).c_str());
|
|
module->Release();
|
|
return nullptr;
|
|
}
|
|
module->function = CreateMetalFunction(device_, module->info, tag, &errorMessage);
|
|
if (!module->function) {
|
|
ERROR_LOG(Log::G3D, "%s\n%s", errorMessage.c_str(), LineNumberString(module->info.msl).c_str());
|
|
module->Release();
|
|
return nullptr;
|
|
}
|
|
return module;
|
|
}
|
|
|
|
Pipeline *MetalDrawContext::CreateGraphicsPipeline(const PipelineDesc &desc, const char *tag) {
|
|
MetalPipeline *pipeline = new MetalPipeline(rm_);
|
|
pipeline->tag = tag;
|
|
for (ShaderModule *module : desc.shaders) {
|
|
MetalShaderModule *mm = (MetalShaderModule *)module;
|
|
mm->AddRef();
|
|
if (mm->GetStage() == ShaderStage::Vertex) {
|
|
pipeline->vs = mm;
|
|
} else if (mm->GetStage() == ShaderStage::Fragment) {
|
|
pipeline->fs = mm;
|
|
} else {
|
|
mm->Release();
|
|
}
|
|
}
|
|
if (!pipeline->vs) {
|
|
ERROR_LOG(Log::G3D, "Pipeline %s lacks a vertex shader", tag);
|
|
pipeline->Release();
|
|
return nullptr;
|
|
}
|
|
pipeline->input = (MetalInputLayout *)desc.inputLayout;
|
|
pipeline->blend = (MetalBlendState *)desc.blend;
|
|
pipeline->depthStencil = (MetalDepthStencilState *)desc.depthStencil;
|
|
pipeline->raster = (MetalRasterState *)desc.raster;
|
|
pipeline->prim = primToMetal[(int)desc.prim];
|
|
if (desc.uniformDesc) {
|
|
pipeline->uboSize = desc.uniformDesc->uniformBufferSize;
|
|
pipeline->ubo = new uint8_t[pipeline->uboSize]{};
|
|
}
|
|
// Create the pipeline state for the most likely target up front, to catch errors early.
|
|
MetalRenderTargetFormat fmt;
|
|
fmt.color = MTLPixelFormatRGBA8Unorm;
|
|
fmt.depth = MTLPixelFormatDepth32Float_Stencil8;
|
|
fmt.stencil = MTLPixelFormatDepth32Float_Stencil8;
|
|
if (!pipeline->GetPSO(device_, fmt)) {
|
|
pipeline->Release();
|
|
return nullptr;
|
|
}
|
|
return pipeline;
|
|
}
|
|
|
|
Buffer *MetalDrawContext::CreateBuffer(size_t size, uint32_t usageFlags) {
|
|
return new MetalBuffer(size);
|
|
}
|
|
|
|
void MetalDrawContext::UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t offset, size_t size, UpdateBufferFlags flags) {
|
|
MetalBuffer *buf = (MetalBuffer *)buffer;
|
|
_dbg_assert_(offset + size <= buf->data.size());
|
|
memcpy(buf->data.data() + offset, data, size);
|
|
buf->dirty = true;
|
|
}
|
|
|
|
static void GetLevelLayout(DataFormat format, int w, int h, int *bytesPerRow, int *rows) {
|
|
int blockSize = 0;
|
|
if (DataFormatIsBlockCompressed(format, &blockSize)) {
|
|
*bytesPerRow = ((w + 3) / 4) * blockSize;
|
|
*rows = (h + 3) / 4;
|
|
} else {
|
|
*bytesPerRow = w * (int)DataFormatSizeInBytes(format);
|
|
*rows = h;
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::UploadTextureData(MetalTexture *tex, int level, int x, int y, int w, int h, const uint8_t *data, int srcStride, TextureCallback callback, bool useUploadBuffer) {
|
|
int bytesPerRow, rows;
|
|
GetLevelLayout(tex->Format(), w, h, &bytesPerRow, &rows);
|
|
MetalPushAlloc alloc = rm_->Push().Allocate(bytesPerRow * rows, 16);
|
|
if (!callback || !callback(alloc.ptr, data, w, h, 1, bytesPerRow, bytesPerRow * rows)) {
|
|
if (!srcStride || srcStride == bytesPerRow) {
|
|
memcpy(alloc.ptr, data, bytesPerRow * rows);
|
|
} else {
|
|
for (int row = 0; row < rows; row++) {
|
|
memcpy(alloc.ptr + row * bytesPerRow, data + row * srcStride, bytesPerRow);
|
|
}
|
|
}
|
|
}
|
|
rm_->CopyBufferToTexture(alloc.buffer, alloc.offset, bytesPerRow, bytesPerRow * rows, MTLSizeMake(w, h, 1), tex->texture, 0, level, MTLOriginMake(x, y, 0), MTLBlitOptionNone, useUploadBuffer);
|
|
}
|
|
|
|
Texture *MetalDrawContext::CreateTexture(const TextureDesc &desc) {
|
|
MTLPixelFormat format = DataFormatToMetal(desc.format);
|
|
if (format == MTLPixelFormatInvalid || !(GetDataFormatSupport(desc.format) & FMT_TEXTURE)) {
|
|
ERROR_LOG(Log::G3D, "Metal: Unsupported texture format %s", DataFormatToString(desc.format));
|
|
return nullptr;
|
|
}
|
|
if (desc.type != TextureType::LINEAR2D) {
|
|
// TODO: 3D textures. Not used by the UI.
|
|
ERROR_LOG(Log::G3D, "Metal: Unsupported texture type %d", (int)desc.type);
|
|
return nullptr;
|
|
}
|
|
MetalTexture *tex = new MetalTexture(rm_, desc);
|
|
bool generateMips = desc.generateMips && (GetDataFormatSupport(desc.format) & FMT_AUTOGEN_MIPS);
|
|
int mipLevels = std::max(1, desc.mipLevels);
|
|
if (desc.generateMips && mipLevels == 1) {
|
|
mipLevels = 1;
|
|
int dim = std::max(desc.width, desc.height);
|
|
while (dim > 1) {
|
|
dim >>= 1;
|
|
mipLevels++;
|
|
}
|
|
}
|
|
tex->mipLevels_ = mipLevels;
|
|
|
|
MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format width:desc.width height:desc.height mipmapped:NO];
|
|
td.mipmapLevelCount = mipLevels;
|
|
td.usage = MTLTextureUsageShaderRead;
|
|
td.storageMode = MTLStorageModePrivate;
|
|
switch (desc.swizzle) {
|
|
case TextureSwizzle::R8_AS_ALPHA:
|
|
td.swizzle = MTLTextureSwizzleChannelsMake(MTLTextureSwizzleOne, MTLTextureSwizzleOne, MTLTextureSwizzleOne, MTLTextureSwizzleRed);
|
|
break;
|
|
case TextureSwizzle::R8_AS_GRAYSCALE:
|
|
td.swizzle = MTLTextureSwizzleChannelsMake(MTLTextureSwizzleRed, MTLTextureSwizzleRed, MTLTextureSwizzleRed, MTLTextureSwizzleOne);
|
|
break;
|
|
case TextureSwizzle::R8_AS_PREMUL_ALPHA:
|
|
td.swizzle = MTLTextureSwizzleChannelsMake(MTLTextureSwizzleRed, MTLTextureSwizzleRed, MTLTextureSwizzleRed, MTLTextureSwizzleRed);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
tex->texture = [device_ newTextureWithDescriptor:td];
|
|
if (!tex->texture) {
|
|
tex->Release();
|
|
return nullptr;
|
|
}
|
|
if (desc.tag) {
|
|
tex->texture.label = [NSString stringWithUTF8String:desc.tag];
|
|
}
|
|
|
|
int w = desc.width;
|
|
int h = desc.height;
|
|
for (int i = 0; i < (int)desc.initData.size(); i++) {
|
|
UploadTextureData(tex, i, 0, 0, w, h, desc.initData[i], 0, desc.initDataCallback, true);
|
|
w = std::max(1, w / 2);
|
|
h = std::max(1, h / 2);
|
|
if (generateMips) {
|
|
break;
|
|
}
|
|
}
|
|
if (generateMips && !desc.initData.empty()) {
|
|
rm_->GenerateMips(tex->texture, true);
|
|
}
|
|
return tex;
|
|
}
|
|
|
|
void MetalDrawContext::UpdateTextureLevels(Texture *texture, const uint8_t **data, TextureCallback initDataCallback, int numLevels) {
|
|
MetalTexture *tex = (MetalTexture *)texture;
|
|
// If the texture has been drawn with this frame, the update has to go in order with the draws.
|
|
bool useUpload = tex->lastUsedFrame != rm_->FrameNumber();
|
|
int w = tex->Width();
|
|
int h = tex->Height();
|
|
for (int i = 0; i < numLevels; i++) {
|
|
UploadTextureData(tex, i, 0, 0, w, h, data[i], 0, initDataCallback, useUpload);
|
|
w = std::max(1, w / 2);
|
|
h = std::max(1, h / 2);
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) {
|
|
MetalTexture *tex = (MetalTexture *)texture;
|
|
bool useUpload = tex->lastUsedFrame != rm_->FrameNumber();
|
|
for (int i = 0; i < numRegions; i++) {
|
|
const TextureRegionUpdate &r = regions[i];
|
|
UploadTextureData(tex, level, r.x, r.y, r.w, r.h, r.data, r.byteStride, nullptr, useUpload);
|
|
}
|
|
}
|
|
|
|
Framebuffer *MetalDrawContext::CreateFramebuffer(const FramebufferDesc &desc) {
|
|
_dbg_assert_(desc.numLayers == 1);
|
|
MetalFramebuffer *fb = new MetalFramebuffer(rm_, desc);
|
|
MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:desc.width height:desc.height mipmapped:NO];
|
|
td.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
|
|
td.storageMode = MTLStorageModePrivate;
|
|
fb->color = [device_ newTextureWithDescriptor:td];
|
|
if (!fb->color) {
|
|
fb->Release();
|
|
return nullptr;
|
|
}
|
|
if (desc.z_stencil) {
|
|
MTLTextureDescriptor *dd = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 width:desc.width height:desc.height mipmapped:NO];
|
|
dd.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsagePixelFormatView;
|
|
dd.storageMode = MTLStorageModePrivate;
|
|
fb->depth = [device_ newTextureWithDescriptor:dd];
|
|
if (!fb->depth) {
|
|
fb->Release();
|
|
return nullptr;
|
|
}
|
|
}
|
|
if (desc.multiSampleLevel > 0) {
|
|
// Read by the shared code's sample copies (BindFramebufferSamplesAsTexture), so not memoryless.
|
|
td.textureType = MTLTextureType2DMultisample;
|
|
td.sampleCount = 1 << desc.multiSampleLevel;
|
|
fb->msaaColor = [device_ newTextureWithDescriptor:td];
|
|
if (!fb->msaaColor) {
|
|
fb->Release();
|
|
return nullptr;
|
|
}
|
|
if (desc.z_stencil) {
|
|
MTLTextureDescriptor *dd = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 width:desc.width height:desc.height mipmapped:NO];
|
|
dd.textureType = MTLTextureType2DMultisample;
|
|
dd.sampleCount = td.sampleCount;
|
|
dd.usage = MTLTextureUsageRenderTarget;
|
|
dd.storageMode = MTLStorageModePrivate;
|
|
fb->msaaDepth = [device_ newTextureWithDescriptor:dd];
|
|
if (!fb->msaaDepth) {
|
|
fb->Release();
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
if (desc.tag) {
|
|
fb->UpdateTag(desc.tag);
|
|
}
|
|
return fb;
|
|
}
|
|
|
|
void MetalDrawContext::CopyFramebufferImage(Framebuffer *srcfb, int level, int x, int y, int z, Framebuffer *dstfb, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) {
|
|
_dbg_assert_(srcfb && !srcfb->IsSwapchain() && dstfb && !dstfb->IsSwapchain());
|
|
MetalFramebuffer *src = (MetalFramebuffer *)srcfb;
|
|
MetalFramebuffer *dst = (MetalFramebuffer *)dstfb;
|
|
|
|
// Clip the source rectangle.
|
|
if (x < 0) {
|
|
width += x;
|
|
dstX -= x;
|
|
x = 0;
|
|
}
|
|
if (y < 0) {
|
|
height += y;
|
|
dstY -= y;
|
|
y = 0;
|
|
}
|
|
if (dstX < 0) {
|
|
width += dstX;
|
|
x -= dstX;
|
|
dstX = 0;
|
|
}
|
|
if (dstY < 0) {
|
|
height += dstY;
|
|
y -= dstY;
|
|
dstY = 0;
|
|
}
|
|
width = std::min(width, std::min(src->Width() - x, dst->Width() - dstX));
|
|
height = std::min(height, std::min(src->Height() - y, dst->Height() - dstY));
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
const MTLOrigin srcOrigin = MTLOriginMake(x, y, 0);
|
|
const MTLOrigin dstOrigin = MTLOriginMake(dstX, dstY, 0);
|
|
const MTLSize size = MTLSizeMake(width, height, 1);
|
|
// Between multisampled framebuffers, the samples are copied too. Otherwise only the resolved images, like the
|
|
// Vulkan backend.
|
|
const bool copySamples = src->MultiSampleLevel() > 0 && src->MultiSampleLevel() == dst->MultiSampleLevel();
|
|
if (aspects & Aspect::COLOR_BIT) {
|
|
rm_->CopyTexture(src->color, level, srcOrigin, size, dst->color, dstLevel, dstOrigin);
|
|
if (copySamples) {
|
|
rm_->CopyTexture(src->msaaColor, 0, srcOrigin, size, dst->msaaColor, 0, dstOrigin);
|
|
}
|
|
}
|
|
if (copySamples && (aspects & (Aspect::DEPTH_BIT | Aspect::STENCIL_BIT)) && src->msaaDepth && dst->msaaDepth) {
|
|
// Metal can't copy a single plane of a multisampled texture (not even through a buffer), so both go.
|
|
rm_->CopyTexture(src->msaaDepth, 0, srcOrigin, size, dst->msaaDepth, 0, dstOrigin);
|
|
}
|
|
const bool copyDepth = (aspects & Aspect::DEPTH_BIT) != 0;
|
|
const bool copyStencil = (aspects & Aspect::STENCIL_BIT) != 0;
|
|
if ((copyDepth || copyStencil) && src->depth && dst->depth) {
|
|
if (copyDepth && copyStencil) {
|
|
rm_->CopyTexture(src->depth, level, srcOrigin, size, dst->depth, dstLevel, dstOrigin);
|
|
} else {
|
|
// Texture-to-texture copies take both planes of a combined format, so a single one goes through a buffer.
|
|
const bool depth = copyDepth;
|
|
const MTLBlitOption option = depth ? MTLBlitOptionDepthFromDepthStencil : MTLBlitOptionStencilFromDepthStencil;
|
|
const int bytesPerRow = width * (depth ? 4 : 1);
|
|
MetalPushAlloc alloc = rm_->Push().Allocate(bytesPerRow * height, 16);
|
|
rm_->CopyTextureToBuffer(src->depth, level, srcOrigin, size, alloc.buffer, alloc.offset, bytesPerRow, bytesPerRow * height, option);
|
|
rm_->CopyBufferToTexture(alloc.buffer, alloc.offset, bytesPerRow, bytesPerRow * height, size, dst->depth, 0, dstLevel, dstOrigin, option, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MetalDrawContext::BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) {
|
|
// Not supported (framebufferBlitSupported is false). Metal blits can't scale.
|
|
return false;
|
|
}
|
|
|
|
bool MetalDrawContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspect, int x, int y, int w, int h, Draw::DataFormat destFormat, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) {
|
|
MetalSwapchain *swapchain = ToSwapchain(srcfb);
|
|
// A swapchain's texture is only known when the commands are encoded.
|
|
id<MTLTexture> srcTex;
|
|
int texW;
|
|
int texH;
|
|
MTLPixelFormat srcFormat = rm_->BackbufferFormat();
|
|
if (swapchain) {
|
|
if (aspect != Aspect::COLOR_BIT || !swapchain->drawn) {
|
|
// Like headless Vulkan, which has no backbuffer. The GE debugger shows the PSP's framebuffer instead.
|
|
return false;
|
|
}
|
|
texW = swapchain->width;
|
|
texH = swapchain->height;
|
|
} else {
|
|
MetalFramebuffer *fb = (MetalFramebuffer *)srcfb;
|
|
srcTex = aspect == Aspect::COLOR_BIT ? fb->color : fb->depth;
|
|
if (!srcTex) {
|
|
return false;
|
|
}
|
|
texW = (int)srcTex.width;
|
|
texH = (int)srcTex.height;
|
|
srcFormat = srcTex.pixelFormat;
|
|
}
|
|
if (x < 0) { w += x; x = 0; }
|
|
if (y < 0) { h += y; y = 0; }
|
|
w = std::min(w, texW - x);
|
|
h = std::min(h, texH - y);
|
|
if (w <= 0 || h <= 0) {
|
|
return true;
|
|
}
|
|
|
|
int srcPixelSize = 4;
|
|
MTLBlitOption options = MTLBlitOptionNone;
|
|
if (aspect == Aspect::DEPTH_BIT) {
|
|
options = MTLBlitOptionDepthFromDepthStencil;
|
|
} else if (aspect == Aspect::STENCIL_BIT) {
|
|
options = MTLBlitOptionStencilFromDepthStencil;
|
|
srcPixelSize = 1;
|
|
}
|
|
int srcStride = w * srcPixelSize;
|
|
size_t size = (size_t)srcStride * h;
|
|
if (!readbackBuffer_ || readbackBuffer_.length < size) {
|
|
rm_->DeferRelease(readbackBuffer_);
|
|
readbackBuffer_ = [device_ newBufferWithLength:std::max(size, (size_t)(1024 * 1024)) options:MTLResourceStorageModeShared];
|
|
readbackBuffer_.label = @"readback";
|
|
}
|
|
|
|
rm_->CopyTextureToBuffer(srcTex, 0, MTLOriginMake(x, y, 0), MTLSizeMake(w, h, 1), readbackBuffer_, 0, srcStride, (uint32_t)size, options, swapchain);
|
|
rm_->Flush(true);
|
|
|
|
const uint8_t *src = (const uint8_t *)readbackBuffer_.contents;
|
|
switch (aspect) {
|
|
case Aspect::COLOR_BIT:
|
|
if (srcFormat == MTLPixelFormatBGRA8Unorm) {
|
|
ConvertFromBGRA8888((uint8_t *)pixels, src, pixelStride, w, w, h, destFormat);
|
|
} else {
|
|
ConvertFromRGBA8888((uint8_t *)pixels, src, pixelStride, w, w, h, destFormat);
|
|
}
|
|
break;
|
|
case Aspect::DEPTH_BIT:
|
|
if (destFormat == DataFormat::D32F) {
|
|
ConvertToD32F((uint8_t *)pixels, src, pixelStride, w, w, h, DataFormat::D32F);
|
|
} else if (destFormat == DataFormat::D16) {
|
|
ConvertToD16((uint8_t *)pixels, src, pixelStride, w, w, h, DataFormat::D32F);
|
|
} else {
|
|
_dbg_assert_(false);
|
|
return false;
|
|
}
|
|
break;
|
|
case Aspect::STENCIL_BIT:
|
|
_dbg_assert_(destFormat == DataFormat::S8);
|
|
for (int row = 0; row < h; row++) {
|
|
memcpy((uint8_t *)pixels + row * pixelStride, src + row * srcStride, w);
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void MetalDrawContext::ResetViewportScissor() {
|
|
int w = rm_->CurPassWidth();
|
|
int h = rm_->CurPassHeight();
|
|
viewport_ = Viewport{ 0.0f, 0.0f, (float)w, (float)h, 0.0f, 1.0f };
|
|
scissor_[0] = 0;
|
|
scissor_[1] = 0;
|
|
scissor_[2] = w;
|
|
scissor_[3] = h;
|
|
}
|
|
|
|
void MetalDrawContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) {
|
|
MetalSwapchain *swapchain = ToSwapchain(fbo);
|
|
if (swapchain) {
|
|
rm_->BindBackbufferPass(swapchain, RPActionToMetal(rp.color), ClearColorFromRGBA(rp.clearColor), tag);
|
|
} else {
|
|
MetalFramebuffer *fb = (MetalFramebuffer *)fbo;
|
|
MetalRenderPassDesc desc{};
|
|
desc.color = fb->color;
|
|
desc.depth = fb->depth;
|
|
desc.msaaColor = fb->msaaColor;
|
|
desc.msaaDepth = fb->msaaDepth;
|
|
desc.colorLoad = RPActionToMetal(rp.color);
|
|
desc.depthLoad = RPActionToMetal(rp.depth);
|
|
desc.stencilLoad = RPActionToMetal(rp.stencil);
|
|
desc.clearColor = ClearColorFromRGBA(rp.clearColor);
|
|
desc.clearDepth = rp.clearDepth;
|
|
desc.clearStencil = rp.clearStencil;
|
|
desc.tag = tag;
|
|
rm_->BindRenderPass(desc);
|
|
}
|
|
ResetViewportScissor();
|
|
if (invalidationCallback_) {
|
|
invalidationCallback_(InvalidationCallbackFlags::RENDER_PASS_STATE);
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::BindFramebufferAsTexture(Framebuffer *fbo, int binding, Aspect aspect, int layer) {
|
|
_dbg_assert_(binding < (int)MAX_TEXTURE_SLOTS);
|
|
_dbg_assert_(fbo && !fbo->IsSwapchain());
|
|
MetalFramebuffer *fb = (MetalFramebuffer *)fbo;
|
|
id<MTLTexture> tex;
|
|
switch (aspect) {
|
|
case Aspect::COLOR_BIT: tex = fb->color; break;
|
|
case Aspect::DEPTH_BIT: tex = fb->depth; break;
|
|
case Aspect::STENCIL_BIT: tex = fb->StencilView(); break;
|
|
default: break;
|
|
}
|
|
boundTextures_[binding] = tex ? tex : nullTexture_;
|
|
boundThin3DTextures_[binding] = nullptr;
|
|
}
|
|
|
|
void MetalDrawContext::BindFramebufferSamplesAsTexture(Framebuffer *fbo, int binding, int layer) {
|
|
_dbg_assert_(binding < (int)MAX_TEXTURE_SLOTS);
|
|
_dbg_assert_(fbo && !fbo->IsSwapchain());
|
|
MetalFramebuffer *fb = (MetalFramebuffer *)fbo;
|
|
boundTextures_[binding] = fb->msaaColor ? fb->msaaColor : fb->color;
|
|
boundThin3DTextures_[binding] = nullptr;
|
|
}
|
|
|
|
void MetalDrawContext::GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) {
|
|
if (MetalSwapchain *swapchain = ToSwapchain(fbo)) {
|
|
*w = swapchain->width;
|
|
*h = swapchain->height;
|
|
} else {
|
|
*w = fbo->Width();
|
|
*h = fbo->Height();
|
|
}
|
|
}
|
|
|
|
Swapchain *MetalDrawContext::CreateSwapchain(const SwapchainDesc &desc, std::string *error) {
|
|
// Swapchains other than the primary come and go between frames.
|
|
_dbg_assert_(!rm_->InFrame());
|
|
if (desc.winsys != WINDOWSYSTEM_METAL_EXT) {
|
|
*error = "Metal swapchains need a CAMetalLayer";
|
|
return nullptr;
|
|
}
|
|
// A null layer gives an offscreen swapchain of the given size, like headless.
|
|
CAMetalLayer *layer = (__bridge CAMetalLayer *)desc.data1;
|
|
if (!layer && (desc.width <= 0 || desc.height <= 0)) {
|
|
*error = "No size for an offscreen swapchain";
|
|
return nullptr;
|
|
}
|
|
return new MetalSwapchainFB(rm_->CreateSwapchain(layer, desc.width, desc.height), desc.tag);
|
|
}
|
|
|
|
void MetalDrawContext::DestroySwapchain(Swapchain *swapchain) {
|
|
_dbg_assert_(swapchain && swapchain != primarySwapchain_);
|
|
_dbg_assert_(!rm_->InFrame());
|
|
MetalSwapchainFB *fb = (MetalSwapchainFB *)swapchain;
|
|
rm_->DestroySwapchain(fb->swapchain);
|
|
fb->swapchain = nullptr;
|
|
// Nothing else may hold it, it no longer has a swapchain to be.
|
|
fb->ReleaseAssertLast();
|
|
}
|
|
|
|
void MetalDrawContext::ResizeSwapchain(Swapchain *swapchain, int width, int height) {
|
|
MetalSwapchain *metalSwapchain = ToSwapchain(swapchain);
|
|
_dbg_assert_(metalSwapchain);
|
|
// An offscreen one keeps its size, as there's no layer to resize.
|
|
_dbg_assert_(metalSwapchain->layer || swapchain == primarySwapchain_);
|
|
_dbg_assert_(swapchain == primarySwapchain_ || !rm_->InFrame());
|
|
rm_->Resize(metalSwapchain, width, height);
|
|
if (swapchain == primarySwapchain_) {
|
|
primarySwapchain_->SetSize(metalSwapchain->width, metalSwapchain->height);
|
|
} else {
|
|
((MetalSwapchainFB *)swapchain)->UpdateSize();
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::SetScissorRect(int left, int top, int width, int height) {
|
|
scissor_[0] = left;
|
|
scissor_[1] = top;
|
|
scissor_[2] = width;
|
|
scissor_[3] = height;
|
|
}
|
|
|
|
void MetalDrawContext::SetViewport(const Viewport &viewport) {
|
|
viewport_ = viewport;
|
|
}
|
|
|
|
void MetalDrawContext::BindSamplerStates(int start, int count, SamplerState **states) {
|
|
_dbg_assert_(start + count <= (int)MAX_TEXTURE_SLOTS);
|
|
for (int i = 0; i < count; i++) {
|
|
MetalSamplerState *s = (MetalSamplerState *)states[i];
|
|
boundSamplers_[start + i] = s ? s->sampler : defaultSampler_;
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) {
|
|
_dbg_assert_(start + count <= (int)MAX_TEXTURE_SLOTS);
|
|
for (int i = 0; i < count; i++) {
|
|
MetalTexture *tex = (MetalTexture *)textures[i];
|
|
boundTextures_[start + i] = tex ? tex->texture : nullTexture_;
|
|
boundThin3DTextures_[start + i] = tex;
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::BindNativeTexture(int sampler, void *nativeTexture) {
|
|
_dbg_assert_(sampler < (int)MAX_TEXTURE_SLOTS);
|
|
id<MTLTexture> tex = (__bridge id<MTLTexture>)nativeTexture;
|
|
boundTextures_[sampler] = tex ? tex : nullTexture_;
|
|
boundThin3DTextures_[sampler] = nullptr;
|
|
}
|
|
|
|
void MetalDrawContext::UpdateDynamicUniformBuffer(const void *ub, size_t size) {
|
|
_dbg_assert_(curPipeline_ && size <= curPipeline_->uboSize);
|
|
memcpy(curPipeline_->ubo, ub, size);
|
|
uboDirty_ = true;
|
|
}
|
|
|
|
void MetalDrawContext::Invalidate(InvalidationFlags flags) {
|
|
if (flags & InvalidationFlags::CACHED_RENDER_STATE) {
|
|
uboDirty_ = true;
|
|
uboPipeline_ = nullptr;
|
|
}
|
|
}
|
|
|
|
id<MTLDepthStencilState> MetalDrawContext::GetDepthStencilState(const DepthStencilStateDesc &desc) {
|
|
MetalDepthStencilKey key{};
|
|
key.depthTest = desc.depthTestEnabled;
|
|
key.depthWrite = desc.depthWriteEnabled;
|
|
key.depthCompare = (uint8_t)desc.depthCompare;
|
|
key.stencilEnabled = desc.stencilEnabled;
|
|
if (desc.stencilEnabled) {
|
|
key.failOp = (uint8_t)desc.stencil.failOp;
|
|
key.passOp = (uint8_t)desc.stencil.passOp;
|
|
key.depthFailOp = (uint8_t)desc.stencil.depthFailOp;
|
|
key.compareOp = (uint8_t)desc.stencil.compareOp;
|
|
key.writeMask = stencilWriteMask_;
|
|
key.compareMask = stencilCompareMask_;
|
|
}
|
|
auto iter = depthStencilCache_.find(key);
|
|
if (iter != depthStencilCache_.end()) {
|
|
return iter->second;
|
|
}
|
|
MTLDepthStencilDescriptor *dd = [MTLDepthStencilDescriptor new];
|
|
dd.depthCompareFunction = desc.depthTestEnabled ? compareToMetal[(int)desc.depthCompare] : MTLCompareFunctionAlways;
|
|
dd.depthWriteEnabled = desc.depthTestEnabled && desc.depthWriteEnabled;
|
|
if (desc.stencilEnabled) {
|
|
MTLStencilDescriptor *sd = [MTLStencilDescriptor new];
|
|
sd.stencilCompareFunction = compareToMetal[(int)desc.stencil.compareOp];
|
|
sd.stencilFailureOperation = stencilOpToMetal[(int)desc.stencil.failOp];
|
|
sd.depthFailureOperation = stencilOpToMetal[(int)desc.stencil.depthFailOp];
|
|
sd.depthStencilPassOperation = stencilOpToMetal[(int)desc.stencil.passOp];
|
|
sd.readMask = stencilCompareMask_;
|
|
sd.writeMask = stencilWriteMask_;
|
|
dd.frontFaceStencil = sd;
|
|
dd.backFaceStencil = sd;
|
|
}
|
|
id<MTLDepthStencilState> dss = [device_ newDepthStencilStateWithDescriptor:dd];
|
|
depthStencilCache_[key] = dss;
|
|
return dss;
|
|
}
|
|
|
|
bool MetalDrawContext::ApplyState() {
|
|
if (!curPipeline_) {
|
|
return false;
|
|
}
|
|
if (!rm_->EnsureRenderEncoder()) {
|
|
return false;
|
|
}
|
|
|
|
MetalPipelineState *pso = curPipeline_->GetPSO(device_, rm_->CurTargetFormat());
|
|
if (!pso) {
|
|
return false;
|
|
}
|
|
rm_->SetPipeline(pso);
|
|
const DepthStencilStateDesc &dsDesc = curPipeline_->depthStencil->desc;
|
|
rm_->SetDepthStencilState(GetDepthStencilState(dsDesc), dsDesc.depthTestEnabled || dsDesc.stencilEnabled);
|
|
if (dsDesc.stencilEnabled) {
|
|
rm_->SetStencilRef(stencilRef_);
|
|
}
|
|
rm_->SetBlendColor(blendFactor_[0], blendFactor_[1], blendFactor_[2], blendFactor_[3]);
|
|
rm_->SetCullMode(curPipeline_->raster->cull);
|
|
rm_->SetFrontFacing(curPipeline_->raster->winding);
|
|
rm_->SetDepthClip(MTLDepthClipModeClip);
|
|
|
|
MTLViewport vp{ viewport_.TopLeftX, viewport_.TopLeftY, viewport_.Width, viewport_.Height, viewport_.MinDepth, viewport_.MaxDepth };
|
|
rm_->SetViewport(vp);
|
|
if (!rm_->SetScissor(scissor_[0], scissor_[1], scissor_[2], scissor_[3])) {
|
|
return false;
|
|
}
|
|
|
|
if (curPipeline_->fs) {
|
|
const MetalTranslatedShader &info = curPipeline_->fs->info;
|
|
for (int i = 0; i < (int)MAX_TEXTURE_SLOTS; i++) {
|
|
int index = i + THIN3D_TEXTURE_BINDING_BASE;
|
|
if (!(info.textureMask & (1 << index))) {
|
|
continue;
|
|
}
|
|
id<MTLTexture> tex = boundTextures_[i];
|
|
if (info.arrayTextureMask & (1 << index)) {
|
|
tex = rm_->ArrayView(tex);
|
|
}
|
|
rm_->SetFragmentTexture(tex, index);
|
|
rm_->SetFragmentSampler(boundSamplers_[i], index);
|
|
if (boundThin3DTextures_[i]) {
|
|
boundThin3DTextures_[i]->lastUsedFrame = rm_->FrameNumber();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (curPipeline_->uboSize) {
|
|
uint32_t gen = rm_->EncoderGeneration();
|
|
if (uboDirty_ || uboPipeline_ != curPipeline_.ptr || uboGeneration_ != gen) {
|
|
MetalPushAlloc alloc = rm_->Push().Push(curPipeline_->ubo, curPipeline_->uboSize, 256);
|
|
rm_->SetVertexBuffer(alloc.buffer, alloc.offset, 0);
|
|
rm_->SetFragmentBuffer(alloc.buffer, alloc.offset, 0);
|
|
uboDirty_ = false;
|
|
uboPipeline_ = curPipeline_.ptr;
|
|
uboGeneration_ = gen;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void MetalDrawContext::Draw(int vertexCount, int offset) {
|
|
if (!ApplyState()) {
|
|
return;
|
|
}
|
|
if (curVBuffer_) {
|
|
id<MTLBuffer> buf;
|
|
uint32_t bufOffset;
|
|
curVBuffer_->Get(rm_, &buf, &bufOffset);
|
|
rm_->SetVertexBuffer(buf, bufOffset + curVBufferOffset_, METAL_VERTEX_BUFFER_INDEX);
|
|
}
|
|
rm_->Draw(curPipeline_->prim, offset, vertexCount);
|
|
}
|
|
|
|
void MetalDrawContext::DrawIndexedInternal(id<MTLBuffer> ibuf, uint32_t ibufOffset, int indexCount) {
|
|
if (ibufOffset & 3) {
|
|
// Metal wants aligned index buffer offsets. Copy the indices to somewhere that is.
|
|
MetalPushAlloc alloc = rm_->Push().Push((const uint8_t *)ibuf.contents + ibufOffset, indexCount * sizeof(uint16_t), 4);
|
|
ibuf = alloc.buffer;
|
|
ibufOffset = alloc.offset;
|
|
}
|
|
rm_->DrawIndexed(curPipeline_->prim, indexCount, ibuf, ibufOffset);
|
|
}
|
|
|
|
void MetalDrawContext::DrawIndexed(int indexCount, int offset) {
|
|
if (!curIBuffer_ || !ApplyState()) {
|
|
return;
|
|
}
|
|
if (curVBuffer_) {
|
|
id<MTLBuffer> buf;
|
|
uint32_t bufOffset;
|
|
curVBuffer_->Get(rm_, &buf, &bufOffset);
|
|
rm_->SetVertexBuffer(buf, bufOffset + curVBufferOffset_, METAL_VERTEX_BUFFER_INDEX);
|
|
}
|
|
id<MTLBuffer> ibuf;
|
|
uint32_t ibufOffset;
|
|
curIBuffer_->Get(rm_, &ibuf, &ibufOffset);
|
|
DrawIndexedInternal(ibuf, ibufOffset + curIBufferOffset_ + offset * sizeof(uint16_t), indexCount);
|
|
}
|
|
|
|
void MetalDrawContext::DrawUP(const void *vdata, int vertexCount) {
|
|
if (!vertexCount || !ApplyState()) {
|
|
return;
|
|
}
|
|
MetalPushAlloc alloc = rm_->Push().Push(vdata, vertexCount * curPipeline_->input->stride, 16);
|
|
rm_->SetVertexBuffer(alloc.buffer, alloc.offset, METAL_VERTEX_BUFFER_INDEX);
|
|
rm_->Draw(curPipeline_->prim, 0, vertexCount);
|
|
}
|
|
|
|
void MetalDrawContext::DrawIndexedUP(const void *vdata, int vertexCount, const void *idata, int indexCount) {
|
|
if (!indexCount || !ApplyState()) {
|
|
return;
|
|
}
|
|
MetalPushAlloc valloc = rm_->Push().Push(vdata, vertexCount * curPipeline_->input->stride, 16);
|
|
rm_->SetVertexBuffer(valloc.buffer, valloc.offset, METAL_VERTEX_BUFFER_INDEX);
|
|
MetalPushAlloc ialloc = rm_->Push().Push(idata, indexCount * sizeof(uint16_t), 4);
|
|
DrawIndexedInternal(ialloc.buffer, ialloc.offset, indexCount);
|
|
}
|
|
|
|
void MetalDrawContext::DrawIndexedClippedBatchUP(const void *vdata, int vertexCount, const void *idata, int indexCount, Slice<ClippedDraw> draws, const void *ub, size_t ubSize) {
|
|
if (draws.is_empty() || !vertexCount || !indexCount) {
|
|
return;
|
|
}
|
|
BindPipeline(draws[0].pipeline);
|
|
MetalPushAlloc valloc = rm_->Push().Push(vdata, vertexCount * curPipeline_->input->stride, 16);
|
|
MetalPushAlloc ialloc = rm_->Push().Push(idata, indexCount * sizeof(uint16_t), 4);
|
|
|
|
for (size_t i = 0; i < draws.size(); i++) {
|
|
const ClippedDraw &draw = draws[i];
|
|
BindPipeline(draw.pipeline);
|
|
UpdateDynamicUniformBuffer(ub, ubSize);
|
|
if (draw.bindTexture) {
|
|
BindTexture(0, draw.bindTexture);
|
|
} else if (draw.bindFramebufferAsTex) {
|
|
BindFramebufferAsTexture(draw.bindFramebufferAsTex, 0, draw.aspect, 0);
|
|
} else if (draw.bindNativeTexture) {
|
|
BindNativeTexture(0, draw.bindNativeTexture);
|
|
}
|
|
SamplerState *sampler = draw.samplerState;
|
|
BindSamplerStates(0, 1, &sampler);
|
|
SetScissorRect(draw.clipx, draw.clipy, draw.clipw, draw.cliph);
|
|
if (!ApplyState()) {
|
|
continue;
|
|
}
|
|
rm_->SetVertexBuffer(valloc.buffer, valloc.offset, METAL_VERTEX_BUFFER_INDEX);
|
|
DrawIndexedInternal(ialloc.buffer, ialloc.offset + draw.indexOffset * sizeof(uint16_t), draw.indexCount);
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::Clear(Aspect aspects, uint32_t colorval, float depthVal, int stencilVal) {
|
|
rm_->Clear((aspects & Aspect::COLOR_BIT) != 0, (aspects & Aspect::DEPTH_BIT) != 0, (aspects & Aspect::STENCIL_BIT) != 0,
|
|
ClearColorFromRGBA(colorval), depthVal, stencilVal);
|
|
}
|
|
|
|
void MetalDrawContext::BeginFrame(DebugFlags debugFlags) {
|
|
FrameTimeData &frameTimeData = frameTimeHistory_.Add(frameCount_);
|
|
frameTimeData.frameBegin = time_now_d();
|
|
rm_->BeginFrame();
|
|
frameTimeData.afterFenceWait = time_now_d();
|
|
}
|
|
|
|
void MetalDrawContext::EndFrame() {
|
|
rm_->EndFrame();
|
|
frameTimeHistory_[frameCount_].firstSubmit = time_now_d();
|
|
curPipeline_ = nullptr;
|
|
curVBuffer_ = nullptr;
|
|
curIBuffer_ = nullptr;
|
|
for (int i = 0; i < (int)MAX_TEXTURE_SLOTS; i++) {
|
|
boundTextures_[i] = nullTexture_;
|
|
boundThin3DTextures_[i] = nullptr;
|
|
}
|
|
uboPipeline_ = nullptr;
|
|
}
|
|
|
|
void MetalDrawContext::Present(PresentMode presentMode) {
|
|
frameTimeHistory_[frameCount_].queuePresent = time_now_d();
|
|
presentMode_ = presentMode;
|
|
rm_->Present(presentMode == PresentMode::FIFO);
|
|
rm_->UnbindRenderPass();
|
|
frameCount_++;
|
|
}
|
|
|
|
std::string MetalDrawContext::GetInfoString(InfoField info) const {
|
|
switch (info) {
|
|
case InfoField::APINAME: return "Metal";
|
|
case InfoField::APIVERSION:
|
|
if (@available(macOS 13.0, iOS 16.0, tvOS 16.0, *)) {
|
|
if ([device_ supportsFamily:MTLGPUFamilyMetal3]) {
|
|
return "Metal 3";
|
|
}
|
|
}
|
|
return "Metal 2";
|
|
case InfoField::VENDORSTRING: return [[device_ name] UTF8String];
|
|
case InfoField::VENDOR: return "";
|
|
case InfoField::DRIVER: return "-";
|
|
case InfoField::SHADELANGVERSION: return "MSL 2.3";
|
|
default: return "?";
|
|
}
|
|
}
|
|
|
|
uint64_t MetalDrawContext::GetNativeObject(NativeObject obj, void *srcObject) {
|
|
switch (obj) {
|
|
case NativeObject::DEVICE: return (uint64_t)(uintptr_t)(__bridge void *)device_;
|
|
case NativeObject::CONTEXT: return (uint64_t)(uintptr_t)(__bridge void *)rm_->Queue();
|
|
case NativeObject::RENDER_MANAGER: return (uint64_t)(uintptr_t)rm_;
|
|
case NativeObject::TEXTURE_VIEW: return (uint64_t)(uintptr_t)(__bridge void *)((MetalTexture *)srcObject)->texture;
|
|
case NativeObject::NULL_IMAGEVIEW: return (uint64_t)(uintptr_t)(__bridge void *)nullTexture_;
|
|
case NativeObject::BOUND_TEXTURE0_IMAGEVIEW: return (uint64_t)(uintptr_t)(__bridge void *)boundTextures_[0];
|
|
case NativeObject::BOUND_TEXTURE1_IMAGEVIEW: return (uint64_t)(uintptr_t)(__bridge void *)boundTextures_[1];
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
void MetalDrawContext::HandleEvent(Event ev, int width, int height, void *param1, void *param2) {
|
|
switch (ev) {
|
|
case Event::LOST_BACKBUFFER:
|
|
if (rm_->IsBackbufferPass()) {
|
|
rm_->UnbindRenderPass();
|
|
}
|
|
break;
|
|
case Event::GOT_BACKBUFFER:
|
|
rm_->Resize(width, height);
|
|
primarySwapchain_->SetSize(rm_->BackbufferWidth(), rm_->BackbufferHeight());
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::string MetalDrawContext::GetGpuProfileString() const {
|
|
const MetalStats &stats = rm_->Stats();
|
|
return StringFromFormat("Render passes: %d\nBlit passes: %d\nFlushes: %d\n", stats.renderPasses, stats.blitPasses, stats.flushes);
|
|
}
|
|
|
|
DrawContext *T3DCreateMetalContext(MetalRenderManager *renderManager) {
|
|
return new MetalDrawContext(renderManager);
|
|
}
|
|
|
|
} // namespace Draw
|