blob: d7ed0c98eaa71fce7b0bd6fac5adf5e9057ccef5 [file]
/*
* Copyright 2021 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/graphite/mtl/MtlCaps.h"
#include "include/core/SkTextureCompressionType.h"
#include "include/gpu/graphite/TextureInfo.h"
#include "include/gpu/graphite/mtl/MtlGraphiteTypes.h"
#include "include/private/SkLog.h"
#include "src/gpu/SwizzlePriv.h"
#include "src/gpu/graphite/CommandBuffer.h"
#include "src/gpu/graphite/ComputePipelineDesc.h"
#include "src/gpu/graphite/GraphicsPipelineDesc.h"
#include "src/gpu/graphite/GraphiteResourceKey.h"
#include "src/gpu/graphite/RenderPassDesc.h"
#include "src/gpu/graphite/RendererProvider.h"
#include "src/gpu/graphite/TextureInfoPriv.h"
#include "src/gpu/graphite/TextureProxy.h"
#include "src/gpu/graphite/mtl/MtlGraphicsPipeline.h"
#include "src/gpu/graphite/mtl/MtlGraphiteUtils.h"
#include "src/gpu/mtl/MtlUtilsPriv.h"
#include "src/sksl/SkSLUtil.h"
namespace skgpu::graphite {
MtlCaps::MtlCaps(const id<MTLDevice> device, const ContextOptions& options) : Caps() {
// Metal-specific MtlCaps
this->initGPUFamily(device);
this->initCaps(device);
this->initFormatTable(device);
// Finish base class
this->finishInitialization(options);
}
void MtlCaps::initGPUFamily(id<MTLDevice> device) {
static_assert(SKGPU_GRAPHITE_METAL_SDK_VERSION >= 230);
static constexpr MTLGPUFamily kFamiliesToCheck[] = {
// Apple silicon shared between iOS and Mac
MTLGPUFamilyApple9,
MTLGPUFamilyApple8,
MTLGPUFamilyApple7,
#ifdef SK_BUILD_FOR_IOS // This hardware is iOS exclusive
MTLGPUFamilyApple6,
MTLGPUFamilyApple5,
MTLGPUFamilyApple4,
MTLGPUFamilyApple3,
MTLGPUFamilyApple2,
#endif
// Legacy mac hardware
MTLGPUFamilyMac2
};
for (MTLGPUFamily family : kFamiliesToCheck) {
if ([device supportsFamily:family]) {
fGPUFamily = family;
return;
}
}
// If we've reached here, we didn't find a supported family and nothing can be trusted.
SK_ABORT("Unable to detect supported MTLGPUFamily");
}
void MtlCaps::initCaps(const id<MTLDevice> device) {
#if defined(GPU_TEST_UTILS)
this->setDeviceName([[device name] UTF8String]);
#endif
if (this->isMac() || fGPUFamily >= MTLGPUFamilyApple3) {
fMaxTextureSize = 16384;
} else {
fMaxTextureSize = 8192;
}
// We use constant address space for our uniform buffers which has various alignment
// requirements for the offset when binding the buffer. On MacOS Intel the offset must align
// to 256. On iOS or Apple Silicon we must align to the max of the data type consumed by the
// vertex function or 4 bytes, or we can ignore the data type and just use 16 bytes.
//
// On Mac, all copies must be aligned to at least 4 bytes; on iOS there is no alignment.
if (this->isMac()) {
fRequiredUniformBufferAlignment = 256;
fRequiredTransferBufferAlignment = 4;
} else {
fRequiredUniformBufferAlignment = 16;
fRequiredTransferBufferAlignment = 1;
}
fResourceBindingReqs.fBackendApi = BackendApi::kMetal;
fResourceBindingReqs.fUniformBufferLayout = Layout::kMetal;
fResourceBindingReqs.fStorageBufferLayout = Layout::kMetal;
// Graphite/Metal does not group resources into different sets or bind groups at this time,
// though ResourceBindingRequirements still expects valid assignments of these indices.
// Assigning both to 0 conveys the usage of one single "set" for all resources.
fResourceBindingReqs.fUniformsSetIdx = 0;
fResourceBindingReqs.fTextureSamplerSetIdx = 0;
fResourceBindingReqs.fComputeUsesDistinctIdxRangesForTextures = true;
fResourceBindingReqs.fIntrinsicBufferBinding =
MtlGraphicsPipeline::kIntrinsicUniformBufferIndex;
fResourceBindingReqs.fCombinedUniformBufferBinding = MtlGraphicsPipeline::kCombinedUniformIndex;
fResourceBindingReqs.fGradientBufferBinding = MtlGraphicsPipeline::kGradientBufferIndex;
// Metal does not distinguish between uniform and storage buffers.
fRequiredStorageBufferAlignment = fRequiredUniformBufferAlignment;
fStorageBufferSupport = true;
fComputeSupport = true;
// See https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf, and what Dawn does at
// https://crsrc.org/c/third_party/dawn/src/dawn/native/metal/PhysicalDeviceMTL.mm?q=maxInterStageShaderVariables
if (this->isMac() || fGPUFamily >= MTLGPUFamilyApple4) {
fMaxVaryings = 31;
} else {
fMaxVaryings = 15;
}
if (@available(macOS 10.12, iOS 14.0, tvOS 14.0, *)) {
fClampToBorderSupport = (this->isMac() || fGPUFamily >= MTLGPUFamilyApple7);
} else {
fClampToBorderSupport = false;
}
const bool isIntel = [device.name containsString:@"Intel"];
if (isIntel) {
// All supported Intel Macs are of an old enough Intel generation that we can just assume
// MSAA should be avoided instead of checking its generation.
fAvoidMSAA = true;
}
// ShaderCaps overrides
SkSL::ShaderCaps* shaderCaps = fShaderCaps.get();
// Dual source blending requires Metal 1.2, but our minimum requirements ensure 2.2
shaderCaps->fDualSourceBlendingSupport = true;
shaderCaps->fVectorClampMinMaxSupport = !isIntel;
if (this->isApple()) {
shaderCaps->fFBFetchSupport = true;
shaderCaps->fFBFetchColorName = "sk_LastFragColor";
}
}
void MtlCaps::initFormatTable(const id<MTLDevice> device) {
// Determine supported sample counts (used for all renderable formats that can be resolved).
SkEnumBitMask<SampleCount> deviceSampleCounts = SampleCount::k1;
if (!fAvoidMSAA) {
for (auto sampleCnt : {SampleCount::k2, SampleCount::k4, SampleCount::k8}) {
if ([device supportsTextureSampleCount: (uint8_t) sampleCnt]) {
deviceSampleCounts |= sampleCnt;
}
}
}
for (int i = 0; i < kTextureFormatCount; ++i) {
TextureFormat tf = static_cast<TextureFormat>(i);
MTLPixelFormat format = TextureFormatToMTLPixelFormat(tf);
SkEnumBitMask<MTLFeatureFlag> features = MTLPixelFormatSupport(fGPUFamily, format);
// Check device capabilities to override `features`
#ifdef SK_BUILD_FOR_MAC
if ((format == MTLPixelFormatDepth24Unorm_Stencil8 ||
format == MTLPixelFormatX24_Stencil8) &&
![device isDepth24Stencil8PixelFormatSupported]) {
features = MTLFeatureFlag::NotAvailable;
}
#endif
// The metal backend only supports optimal tiling currently
auto& [supportedUsage, supportedSampleCounts] = fFormatSupport[(int) Tiling::kOptimal][i];
if (features == MTLFeatureFlag::NotAvailable) {
SkASSERT(!SkToBool(supportedUsage) && !SkToBool(supportedSampleCounts));
continue;
}
// Convert from the Metal format feature flags to Graphite's TextureUsage.
SkASSERT(format != MTLPixelFormatInvalid);
// Every available pixel format can be used as a copy src and dst in Metal.
// Graphite chooses to exclude kCopySrc on compressed formats
if (TextureFormatCompressionType(tf) != SkTextureCompressionType::kNone) {
supportedUsage |= TextureUsage::kCopyDst;
} else {
SkASSERT(tf != TextureFormat::kExternal);
supportedUsage |= TextureUsage::kCopySrc | TextureUsage::kCopyDst;
}
// Graphite assumes you can filter in a shader (not just read texels)
if (features & MTLFeatureFlag::Filter) {
supportedUsage |= TextureUsage::kSample;
}
// Graphite requires textures to be writable to be used for compute storage
// (they are always readable).
if (features & MTLFeatureFlag::Write) {
supportedUsage |= TextureUsage::kStorage;
}
SkEnumBitMask<MTLFeatureFlag> msaaFlags = MTLFeatureFlag::MSAA;
if (TextureFormatIsDepthOrStencil(tf)) {
// All available DS formats can be rendered into; Graphite never resolves DS attachments
// so they support MSAA with just that feature flag
supportedUsage |= TextureUsage::kRender;
} else if ((features & MTLFeatureFlag::Color) && (features & MTLFeatureFlag::Blend)) {
// Color formats require both being a color render target feature and having blending to
// be useful.
supportedUsage |= TextureUsage::kRender;
// Graphite will also assume that it can use the texture format as the resolve target
// for color attachments.
msaaFlags |= MTLFeatureFlag::Resolve;
}
if ((features & msaaFlags) == msaaFlags) {
SkASSERT(supportedUsage & TextureUsage::kRender);
supportedSampleCounts = deviceSampleCounts;
} else {
supportedSampleCounts = SampleCount::k1;
}
}
}
std::pair<SkEnumBitMask<TextureUsage>, Tiling> MtlCaps::getTextureUsage(
const TextureInfo& info) const {
const auto& mtlInfo = TextureInfoPriv::Get<MtlTextureInfo>(info);
SkEnumBitMask<TextureUsage> usage;
if (mtlInfo.fUsage & MTLTextureUsageRenderTarget) {
usage |= TextureUsage::kRender;
// NOTE: No support for MSRTSS
}
// Other than rendering, every other usage is blocked if it's framebuffer-only
if (!mtlInfo.fFramebufferOnly) {
if (mtlInfo.fUsage & MTLTextureUsageShaderRead) {
usage |= TextureUsage::kSample;
if (mtlInfo.fUsage & MTLTextureUsageShaderWrite) {
usage |= TextureUsage::kStorage;
}
}
// Always include CopySrc and CopyDst, relying on the format's supported flags to mask
// the final capabilities automatically (e.g. if this were compressed)
usage |= TextureUsage::kCopySrc | TextureUsage::kCopyDst;
// NOTE: No support for TextureUsage::kHostCopy yet
}
return {usage, Tiling::kOptimal};
}
TextureInfo MtlCaps::onGetDefaultTextureInfo(SkEnumBitMask<TextureUsage> usage,
TextureFormat format,
SampleCount sampleCount,
Mipmapped mipmapped,
Protected,
Discardable discardable) const {
MTLPixelFormat mtlFormat = TextureFormatToMTLPixelFormat(format);
SkASSERT(mtlFormat != MTLPixelFormatInvalid); // should have been caught by support check first
// Default to private in the event it's not discardable or memoryless is not available
MTLStorageMode storageMode = MTLStorageModePrivate;
MTLTextureUsage mtlUsage = MTLTextureUsageUnknown;
if (usage & TextureUsage::kSample) {
mtlUsage |= MTLTextureUsageShaderRead;
}
if (usage & TextureUsage::kStorage) {
mtlUsage |= MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite;
}
if (usage & TextureUsage::kRender) {
mtlUsage |= MTLTextureUsageRenderTarget;
// Switch to memoryless for discardable render targets when possible (only on Apple silicon)
if (discardable == Discardable::kYes && this->isApple()) {
storageMode = MTLStorageModeMemoryless;
}
}
// NOTE: CopyDst and CopySrc are not MTLTextureUsages that have to be requested, so ignore them.
// Caps should not be requested anything with HostCopy or MSRTSS since these are not supported.
SkASSERT(!SkToBool(usage & TextureUsage::kHostCopy) &&
!SkToBool(usage & TextureUsage::kMSRTSS));
MtlTextureInfo info;
info.fSampleCount = sampleCount;
info.fMipmapped = mipmapped;
info.fFormat = mtlFormat;
info.fUsage = mtlUsage;
info.fStorageMode = storageMode;
info.fFramebufferOnly = false;
return TextureInfos::MakeMetal(info);
}
namespace {
skgpu::UniqueKey::Domain get_domain() {
static const skgpu::UniqueKey::Domain kMtlGraphicsPipelineDomain =
skgpu::UniqueKey::GenerateDomain();
return kMtlGraphicsPipelineDomain;
}
}
static constexpr uint16_t kMtlGraphicsPipelineKeyData32Count = 4;
UniqueKey MtlCaps::makeGraphicsPipelineKey(const GraphicsPipelineDesc& pipelineDesc,
const RenderPassDesc& renderPassDesc) const {
UniqueKey pipelineKey;
{
// 4 uint32_t's (render step id, paint id, renderpass desc, uint16 write swizzle key)
UniqueKey::Builder builder(&pipelineKey, get_domain(),
kMtlGraphicsPipelineKeyData32Count, "MtlGraphicsPipeline");
// add GraphicsPipelineDesc key
builder[0] = static_cast<uint32_t>(pipelineDesc.renderStepID());
builder[1] = pipelineDesc.paintParamsID().asUInt();
// add RenderPassDesc key
builder[2] = this->getRenderPassDescKey(renderPassDesc);
builder[3] = renderPassDesc.fWriteSwizzle.asKey();
builder.finish();
}
return pipelineKey;
}
bool MtlCaps::extractGraphicsDescs(const UniqueKey& key,
GraphicsPipelineDesc* pipelineDesc,
RenderPassDesc* renderPassDesc,
const RendererProvider* rendererProvider) const {
struct UnpackedKeyData {
// From the GraphicsPipelineDesc
RenderStep::RenderStepID fRenderStepID = RenderStep::RenderStepID::kInvalid;
UniquePaintParamsID fPaintParamsID = UniquePaintParamsID::Invalid();
// From the RenderPassDesc
TextureFormat fColorFormat = TextureFormat::kUnsupported;
SampleCount fColorSampleCount = SampleCount::k1;
TextureFormat fDSFormat = TextureFormat::kUnsupported;
SampleCount fDSSampleCount = SampleCount::k1;
Swizzle fWriteSwizzle;
} keyData;
SkASSERT(key.domain() == get_domain());
SkASSERT(key.dataSize() == 4 * kMtlGraphicsPipelineKeyData32Count);
const uint32_t* rawKeyData = key.data();
SkASSERT(RenderStep::IsValidRenderStepID(rawKeyData[0]));
keyData.fRenderStepID = static_cast<RenderStep::RenderStepID>(rawKeyData[0]);
keyData.fPaintParamsID = rawKeyData[1] ? UniquePaintParamsID(rawKeyData[1])
: UniquePaintParamsID::Invalid();
keyData.fDSFormat = static_cast<TextureFormat>((rawKeyData[2] >> 8) & 0xFF);
keyData.fDSSampleCount = static_cast<SampleCount>(rawKeyData[2] & 0xFF);
keyData.fColorFormat = static_cast<TextureFormat>((rawKeyData[2] >> 24) & 0xFF);
keyData.fColorSampleCount = static_cast<SampleCount>((rawKeyData[2] >> 16) & 0xFF);
keyData.fWriteSwizzle = SwizzleCtorAccessor::Make(rawKeyData[3]);
if ((keyData.fColorSampleCount > SampleCount::k1 && this->avoidMSAA()) ||
(keyData.fDSFormat != TextureFormat::kUnsupported && this->avoidDepthMode())) {
return false;
}
// Recreate the RenderPassDesc, picking arbitrary load/store ops. Since Metal doesn't need
// to include resolve attachment details, assume that if color attachment's sample count is > 1
// that there is a matching resolve attachment (no MSAA-render-to-single-sample support in MTL).
SkASSERT(keyData.fColorSampleCount == keyData.fDSSampleCount ||
keyData.fDSFormat == TextureFormat::kUnsupported);
*renderPassDesc = {};
renderPassDesc->fColorAttachment = {keyData.fColorFormat,
LoadOp::kClear,
StoreOp::kStore,
keyData.fColorSampleCount};
if (!this->avoidDepthMode()) {
renderPassDesc->fDepthStencilAttachment = {keyData.fDSFormat,
LoadOp::kClear,
StoreOp::kDiscard,
keyData.fDSSampleCount};
}
if (keyData.fColorSampleCount > SampleCount::k1) {
renderPassDesc->fColorResolveAttachment = {keyData.fColorFormat,
LoadOp::kClear,
StoreOp::kStore,
SampleCount::k1};
renderPassDesc->fColorAttachment.fStoreOp = StoreOp::kDiscard;
}
renderPassDesc->fSampleCount = keyData.fColorSampleCount;
renderPassDesc->fWriteSwizzle = keyData.fWriteSwizzle;
renderPassDesc->fDstReadStrategy = this->getDstReadStrategy();
// Recreate the GraphicsPipelineDesc
const RenderStep* renderStep = rendererProvider->lookup(keyData.fRenderStepID);
UniquePaintParamsID paintID = renderStep->performsShading() ? keyData.fPaintParamsID
: UniquePaintParamsID::Invalid();
*pipelineDesc = GraphicsPipelineDesc(renderStep->renderStepID(), paintID);
return true;
}
uint32_t MtlCaps::getRenderPassDescKey(const RenderPassDesc& renderPassDesc) const {
static_assert(kTextureFormatCount <= 256);
// Each attachment format + sample count fits in 16-bits. Load/store ops are ignored.
auto attachmentKey = [](AttachmentDesc desc) {
SkASSERT(desc.fFormat != TextureFormat::kUnsupported ||
desc.fSampleCount == SampleCount::k1);
return (static_cast<uint32_t>(desc.fFormat) << 8) |
static_cast<uint32_t>(desc.fSampleCount);
};
// The MtlRenderPassDescriptor requires no information about the resolve attachment
return (attachmentKey(renderPassDesc.fColorAttachment) << 16) |
attachmentKey(renderPassDesc.fDepthStencilAttachment);
}
UniqueKey MtlCaps::makeComputePipelineKey(const ComputePipelineDesc& pipelineDesc) const {
UniqueKey pipelineKey;
{
static const skgpu::UniqueKey::Domain kComputePipelineDomain = UniqueKey::GenerateDomain();
// The key is made up of a single uint32_t corresponding to the compute step ID.
UniqueKey::Builder builder(&pipelineKey, kComputePipelineDomain, 1, "ComputePipeline");
builder[0] = pipelineDesc.computeStep()->uniqueID();
// TODO(b/240615224): The local work group size should factor into the key on platforms
// that don't support specialization constants and require the workgroup/threadgroup size to
// be specified in the shader text (D3D12, Vulkan 1.0, and OpenGL).
builder.finish();
}
return pipelineKey;
}
void MtlCaps::buildKeyForTexture(SkISize dimensions,
const TextureInfo& info,
ResourceType type,
GraphiteResourceKey* key) const {
const auto& mtlInfo = TextureInfoPriv::Get<MtlTextureInfo>(info);
SkASSERT(!dimensions.isEmpty());
// A MTLPixelFormat is an NSUInteger type which is documented to be 32 bits in 32 bit
// applications and 64 bits in 64 bit applications. So it should fit in an uint64_t, but adding
// the assert heere to make sure.
static_assert(sizeof(MTLPixelFormat) <= sizeof(uint64_t));
SkASSERT(mtlInfo.fFormat != MTLPixelFormatInvalid);
uint64_t formatKey = static_cast<uint64_t>(mtlInfo.fFormat);
uint32_t samplesKey = SamplesToKey(info.sampleCount());
// We don't have to key the number of mip levels because it is inherit in the combination of
// isMipped and dimensions.
bool isMipped = mtlInfo.fMipmapped == Mipmapped::kYes;
Protected isProtected = info.isProtected();
bool isFBOnly = mtlInfo.fFramebufferOnly;
// Confirm all the below parts of the key can fit in a single uint32_t. The sum of the shift
// amounts in the asserts must be less than or equal to 32.
SkASSERT(samplesKey < (1u << kNumSampleKeyBits));
SkASSERT(static_cast<uint32_t>(isMipped) < (1u << 1));
SkASSERT(static_cast<uint32_t>(isProtected) < (1u << 1));
SkASSERT(mtlInfo.fUsage < (1u << 5));
SkASSERT(mtlInfo.fStorageMode < (1u << 2));
SkASSERT(static_cast<uint32_t>(isFBOnly) < (1u << 1));
// We need two uint32_ts for dimensions, 2 for format, and 1 for the rest of the key;
static uint16_t kNum32DataCnt = 2 + 2 + 1;
GraphiteResourceKey::Builder builder(key, type, kNum32DataCnt);
builder[0] = dimensions.width();
builder[1] = dimensions.height();
builder[2] = formatKey & 0xFFFFFFFF;
builder[3] = (formatKey >> 32) & 0xFFFFFFFF;
builder[4] = (samplesKey << 0) |
(static_cast<uint32_t>(isMipped) << kNumSampleKeyBits) |
(static_cast<uint32_t>(isProtected) << 4) |
(static_cast<uint32_t>(mtlInfo.fUsage) << 5) |
(static_cast<uint32_t>(mtlInfo.fStorageMode) << 10)|
(static_cast<uint32_t>(isFBOnly) << 12);
}
} // namespace skgpu::graphite