| /* | |
| * 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) | |
| const char* name = [[device name] UTF8String]; | |
| if (name) { | |
| this->setDeviceName(name); | |
| } | |
| #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.fStorageBufferBinding = MtlGraphicsPipeline::kStorageBufferIndex; | |
| // 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; | |
| } | |
| fClampToBorderSupport = (this->isMac() || fGPUFamily >= MTLGPUFamilyApple7); | |
| 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); | |
| // Per "Texture capabilities by pixel format" in | |
| // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf: | |
| // All graphics and compute kernels can read or sample a texture with any available pixel | |
| // format (i.e. nearest filtering, texel reads), so by this point, we can claim that the | |
| // read usage must be supported. Note, linear filtering requires the filter feature flag. | |
| supportedUsage |= TextureUsage::kRead; | |
| // 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; | |
| } | |
| 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) { | |
| // If UsageShaderRead is present, we always add the Read and Sample usages, and rely on | |
| // the format's supported flags to mask to the correct capabilities. | |
| usage |= TextureUsage::kRead | 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 | TextureUsage::kRead)) { | |
| 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 |