blob: d96ec5543c90dc0ee5f04676e710feb6a229c356 [file]
/*
* Copyright 2022 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/vk/VulkanGraphiteUtils.h"
#include "include/core/SkContext.h"
#include "include/core/SkStream.h"
#include "include/gpu/ShaderErrorHandler.h"
#include "include/gpu/graphite/Context.h"
#include "include/gpu/graphite/ContextOptions.h"
#include "include/gpu/graphite/vk/VulkanGraphiteContext.h"
#include "include/gpu/vk/VulkanBackendContext.h"
#include "src/core/SkContextPriv.h"
#include "src/core/SkTraceEvent.h"
#include "src/gpu/graphite/ContextPriv.h"
#include "src/gpu/graphite/RenderPassDesc.h"
#include "src/gpu/graphite/TextureFormat.h"
#include "src/gpu/graphite/vk/VulkanCaps.h"
#include "src/gpu/graphite/vk/VulkanQueueManager.h"
#include "src/gpu/graphite/vk/VulkanSampler.h"
#include "src/gpu/graphite/vk/VulkanSharedContext.h"
#include "src/sksl/SkSLProgramSettings.h"
namespace SkContexts {
// Creates a context wrapping a Graphite GPU backend with Vulkan
std::unique_ptr<SkContext> MakeGraphite(const skgpu::VulkanBackendContext& vkContext,
const SkContextOptions& contextOptions,
const skgpu::graphite::ContextOptions& graphiteOptions) {
auto context = skgpu::graphite::ContextFactory::MakeVulkan(vkContext, graphiteOptions);
if (!context) {
return nullptr;
}
return SkContextCtorAccessor::MakeContext(
contextOptions, skgpu::graphite::ContextPriv::AsGPUContext(std::move(context)));
}
} // namespace SkContexts
namespace skgpu::graphite::ContextFactory {
std::unique_ptr<Context> MakeVulkan(const VulkanBackendContext& backendContext,
const ContextOptions& options) {
sk_sp<SharedContext> sharedContext = VulkanSharedContext::Make(backendContext, options);
if (!sharedContext) {
return nullptr;
}
std::unique_ptr<QueueManager> queueManager(new VulkanQueueManager(backendContext.fQueue,
sharedContext.get()));
if (!queueManager) {
return nullptr;
}
return ContextCtorAccessor::MakeContext(std::move(sharedContext),
std::move(queueManager),
options);
}
} // namespace skgpu::graphite::ContextFactory
namespace skgpu::graphite {
VkShaderModule CreateVulkanShaderModule(const VulkanSharedContext* context,
const SkSL::NativeShader& spirv,
VkShaderStageFlagBits stage) {
TRACE_EVENT0("skia.shaders", "InstallVkShaderModule");
VkShaderModuleCreateInfo moduleCreateInfo = {};
moduleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
moduleCreateInfo.codeSize = spirv.fBinary.size() * sizeof(uint32_t);
moduleCreateInfo.pCode = spirv.fBinary.data();
VkShaderModule shaderModule;
VkResult result;
VULKAN_CALL_RESULT(context,
result,
CreateShaderModule(context->device(),
&moduleCreateInfo,
/*const VkAllocationCallbacks*=*/nullptr,
&shaderModule));
if (result != VK_SUCCESS) {
SKIA_LOG_E("Failed to create VkShaderModule");
return VK_NULL_HANDLE;
}
return shaderModule;
}
std::optional<VkImageMemoryBarrier> CreateVulkanImageMemoryBarrier(
const VulkanSharedContext& sharedContext,
VkImage image,
const VulkanTextureInfo& textureInfo,
VkImageLayout currentLayout,
uint32_t currentQueueIndex,
VkImageLayout newLayout,
VkAccessFlags dstAccessMask,
uint32_t newQueueFamilyIndex) {
SkASSERT(newLayout == currentLayout ||
(VK_IMAGE_LAYOUT_UNDEFINED != newLayout &&
VK_IMAGE_LAYOUT_PREINITIALIZED != newLayout));
#ifdef SK_DEBUG
if (textureInfo.fSharingMode == VK_SHARING_MODE_CONCURRENT) {
if (newQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED) {
SkASSERT(currentQueueIndex == VK_QUEUE_FAMILY_IGNORED ||
currentQueueIndex == VK_QUEUE_FAMILY_EXTERNAL ||
currentQueueIndex == VK_QUEUE_FAMILY_FOREIGN_EXT);
} else {
SkASSERT(newQueueFamilyIndex == VK_QUEUE_FAMILY_EXTERNAL ||
newQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT);
SkASSERT(currentQueueIndex == VK_QUEUE_FAMILY_IGNORED);
}
} else {
SkASSERT(textureInfo.fSharingMode == VK_SHARING_MODE_EXCLUSIVE);
if (newQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED ||
currentQueueIndex == sharedContext.queueIndex()) {
SkASSERT(currentQueueIndex == VK_QUEUE_FAMILY_IGNORED ||
currentQueueIndex == VK_QUEUE_FAMILY_EXTERNAL ||
currentQueueIndex == VK_QUEUE_FAMILY_FOREIGN_EXT ||
currentQueueIndex == sharedContext.queueIndex());
} else if (newQueueFamilyIndex == VK_QUEUE_FAMILY_EXTERNAL ||
newQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT) {
SkASSERT(currentQueueIndex == VK_QUEUE_FAMILY_IGNORED ||
currentQueueIndex == sharedContext.queueIndex());
}
}
#endif
if (textureInfo.fSharingMode == VK_SHARING_MODE_EXCLUSIVE) {
if (newQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED) {
newQueueFamilyIndex = sharedContext.queueIndex();
}
if (currentQueueIndex == VK_QUEUE_FAMILY_IGNORED) {
currentQueueIndex = sharedContext.queueIndex();
}
}
// If the old and new layout are the same and the layout is a read only layout, there is no need
// to put in a barrier unless we also need to switch queues.
if (newLayout == currentLayout && currentQueueIndex == newQueueFamilyIndex &&
(VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL == currentLayout ||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL == currentLayout ||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL == currentLayout)) {
return std::nullopt;
}
VkAccessFlags srcAccessMask =
VkImageLayoutToSrcAccessMask(currentLayout, textureInfo.fImageUsageFlags);
return VkImageMemoryBarrier{
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, // sType
nullptr, // pNext
srcAccessMask, // srcAccessMask
dstAccessMask, // dstAccessMask
currentLayout, // oldLayout
newLayout, // newLayout
currentQueueIndex, // srcQueueFamilyIndex
newQueueFamilyIndex, // dstQueueFamilyIndex
image, // image
{ textureInfo.fAspectMask, 0, VK_REMAINING_MIP_LEVELS, 0, 1 } // subresourceRange
};
}
VkPipelineStageFlags VkImageLayoutToPipelineSrcStageFlags(const VkImageLayout layout,
const VulkanCaps& caps) {
if (VK_IMAGE_LAYOUT_GENERAL == layout) {
return VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
} else if (VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL == layout ||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL == layout) {
return VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL == layout) {
return VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
} else if (VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL == layout ||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL == layout) {
return VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
} else if (VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL == layout) {
return caps.shaderReadOnlySrcStageMask();
} else if (VK_IMAGE_LAYOUT_PREINITIALIZED == layout) {
return VK_PIPELINE_STAGE_HOST_BIT;
} else if (VK_IMAGE_LAYOUT_PRESENT_SRC_KHR == layout) {
return VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
}
SkASSERT(VK_IMAGE_LAYOUT_UNDEFINED == layout);
return VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
}
VkAccessFlags VkImageLayoutToSrcAccessMask(const VkImageLayout layout,
VkImageUsageFlags usageFlags) {
// We can only directly access the host memory if we are in preinitialized or general layout,
// and the image is linear. However, device access to images written by the host happens after
// vkQueueSubmit, which implicitly makes host writes _visible_ to the device, i.e.
// VK_ACCESS_HOST_WRITE_BIT is unnecessary. Host data is made _available_ to the device via
// vkFlushMappedMemoryRanges.
VkAccessFlags flags = 0;
if (VK_IMAGE_LAYOUT_GENERAL == layout) {
flags = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_TRANSFER_WRITE_BIT;
if (usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) {
flags |= VK_ACCESS_SHADER_WRITE_BIT;
}
} else if (VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL == layout) {
flags = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
} else if (VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL == layout) {
flags = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
} else if (VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL == layout) {
flags = VK_ACCESS_TRANSFER_WRITE_BIT;
} else if (VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL == layout ||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL == layout ||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR == layout ||
VK_IMAGE_LAYOUT_PREINITIALIZED == layout) {
// There are no writes that need to be made available
flags = 0;
}
return flags;
}
void DescriptorDataToVkDescSetLayout(const VulkanSharedContext* ctxt,
const SkSpan<const DescriptorData>& requestedDescriptors,
VkDescriptorSetLayout* outLayout) {
// If requestedDescriptors is empty, that simply means we should create an empty placeholder
// layout that doesn't actually contain any descriptors.
constexpr int32_t kInlineCount = 16;
skia_private::STArray<kInlineCount, VkDescriptorSetLayoutBinding> bindingLayouts;
for (size_t i = 0; i < requestedDescriptors.size(); i++) {
if (requestedDescriptors[i].fCount != 0) {
const DescriptorData& currDescriptor = requestedDescriptors[i];
VkDescriptorSetLayoutBinding& layoutBinding = bindingLayouts.push_back();
layoutBinding = {};
layoutBinding.binding = currDescriptor.fBindingIndex;
layoutBinding.descriptorType = DsTypeEnumToVkDs(currDescriptor.fType);
layoutBinding.descriptorCount = currDescriptor.fCount;
layoutBinding.stageFlags =
PipelineStageFlagsToVkShaderStageFlags(currDescriptor.fPipelineStageFlags);
layoutBinding.pImmutableSamplers = currDescriptor.fImmutableSampler
? (static_cast<const VulkanSampler*>(
currDescriptor.fImmutableSampler))->constVkSamplerPtr()
: nullptr;
}
}
VkDescriptorSetLayoutCreateInfo layoutCreateInfo = {};
layoutCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutCreateInfo.bindingCount = bindingLayouts.size();
layoutCreateInfo.pBindings = bindingLayouts.data();
VkResult result;
VULKAN_CALL_RESULT(
ctxt,
result,
CreateDescriptorSetLayout(ctxt->device(), &layoutCreateInfo, nullptr, outLayout));
if (result != VK_SUCCESS) {
SkDebugf("Failed to create VkDescriptorSetLayout\n");
outLayout = VK_NULL_HANDLE;
}
}
VkDescriptorType DsTypeEnumToVkDs(DescriptorType type) {
switch (type) {
case DescriptorType::kUniformBuffer:
return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
case DescriptorType::kUniformBufferDynamic:
return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
case DescriptorType::kTextureSampler:
return VK_DESCRIPTOR_TYPE_SAMPLER;
case DescriptorType::kTexture:
return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
case DescriptorType::kCombinedTextureSampler:
return VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
case DescriptorType::kStorageBuffer:
return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
case DescriptorType::kStorageBufferDynamic:
return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
case DescriptorType::kInputAttachment:
return VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
case DescriptorType::kStorageTexture:
return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
}
SkUNREACHABLE;
}
#define VK_FORMAT_MAPPING(M) \
M(TextureFormat::kR8, VK_FORMAT_R8_UNORM) \
M(TextureFormat::kR16, VK_FORMAT_R16_UNORM) \
M(TextureFormat::kR16F, VK_FORMAT_R16_SFLOAT) \
M(TextureFormat::kR32F, VK_FORMAT_R32_SFLOAT) \
/*TextureFormat::kA8 unsupported */ \
M(TextureFormat::kRG8, VK_FORMAT_R8G8_UNORM) \
M(TextureFormat::kRG16, VK_FORMAT_R16G16_UNORM) \
M(TextureFormat::kRG16F, VK_FORMAT_R16G16_SFLOAT) \
M(TextureFormat::kRG32F, VK_FORMAT_R32G32_SFLOAT) \
M(TextureFormat::kRGB8, VK_FORMAT_R8G8B8_UNORM) \
M(TextureFormat::kBGR8, VK_FORMAT_B8G8R8_UNORM) \
M(TextureFormat::kB5_G6_R5, VK_FORMAT_R5G6B5_UNORM_PACK16) \
M(TextureFormat::kR5_G6_B5, VK_FORMAT_B5G6R5_UNORM_PACK16) \
M(TextureFormat::kRGB16, VK_FORMAT_R16G16B16_UNORM) \
M(TextureFormat::kRGB16F, VK_FORMAT_R16G16B16_SFLOAT) \
M(TextureFormat::kRGB32F, VK_FORMAT_R32G32B32_SFLOAT) \
M(TextureFormat::kRGB8_sRGB, VK_FORMAT_R8G8B8_SRGB) \
/*TextureFormat::kBGR10_XR unsupported */ \
M(TextureFormat::kRGBA8, VK_FORMAT_R8G8B8A8_UNORM) \
M(TextureFormat::kRGBA16, VK_FORMAT_R16G16B16A16_UNORM) \
M(TextureFormat::kRGBA16F, VK_FORMAT_R16G16B16A16_SFLOAT) \
M(TextureFormat::kRGBA32F, VK_FORMAT_R32G32B32A32_SFLOAT) \
M(TextureFormat::kRGB10_A2, VK_FORMAT_A2B10G10R10_UNORM_PACK32) \
M(TextureFormat::kRGBA10x6, VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16) \
M(TextureFormat::kRGBA8_sRGB, VK_FORMAT_R8G8B8A8_SRGB) \
M(TextureFormat::kBGRA8, VK_FORMAT_B8G8R8A8_UNORM) \
M(TextureFormat::kBGR10_A2, VK_FORMAT_A2R10G10B10_UNORM_PACK32) \
M(TextureFormat::kBGRA8_sRGB, VK_FORMAT_B8G8R8A8_SRGB) \
M(TextureFormat::kABGR4, VK_FORMAT_R4G4B4A4_UNORM_PACK16) \
M(TextureFormat::kARGB4, VK_FORMAT_B4G4R4A4_UNORM_PACK16) \
/*TextureFormat::kBGRA10x6_XR unsupported */ \
M(TextureFormat::kRGB8_ETC2, VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK) \
M(TextureFormat::kRGB8_ETC2_sRGB, VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK) \
M(TextureFormat::kRGB8_BC1, VK_FORMAT_BC1_RGB_UNORM_BLOCK) \
M(TextureFormat::kRGBA8_BC1, VK_FORMAT_BC1_RGBA_UNORM_BLOCK) \
M(TextureFormat::kRGBA8_BC1_sRGB, VK_FORMAT_BC1_RGBA_SRGB_BLOCK) \
M(TextureFormat::kYUV8_P2_420, VK_FORMAT_G8_B8R8_2PLANE_420_UNORM) \
M(TextureFormat::kYUV8_P3_420, VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM) \
M(TextureFormat::kYUV10x6_P2_420, VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16) \
M(TextureFormat::kYUV8_P2_422, VK_FORMAT_G8_B8R8_2PLANE_422_UNORM) \
M(TextureFormat::kYUV8_P3_422, VK_FORMAT_G8_B8_R8_3PLANE_422_UNORM) \
M(TextureFormat::kYUV10x6_P2_422, VK_FORMAT_G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16) \
M(TextureFormat::kYUV8_P2_444, VK_FORMAT_G8_B8R8_2PLANE_444_UNORM) \
M(TextureFormat::kYUV8_P3_444, VK_FORMAT_G8_B8_R8_3PLANE_444_UNORM) \
M(TextureFormat::kYUV10x6_P2_444, VK_FORMAT_G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16) \
/*TextureFormat::kExternal VK_FORMAT_UNDEFINED w/ uint64_t sidecar */ \
M(TextureFormat::kS8, VK_FORMAT_S8_UINT) \
M(TextureFormat::kD16, VK_FORMAT_D16_UNORM) \
M(TextureFormat::kD32F, VK_FORMAT_D32_SFLOAT) \
M(TextureFormat::kD24_S8, VK_FORMAT_D24_UNORM_S8_UINT) \
M(TextureFormat::kD32F_S8, VK_FORMAT_D32_SFLOAT_S8_UINT)
// NOTE: For external formats, Vulkan stores the value outside of VkFormat and expects the VkFormat
// to be VK_FORMAT_UNDEFINED. The mapping functions below are unaware of this side car, so it
// maps VK_FORMAT_UNDEFINED to TextureFormat::kUnsupported (this is disambiguated explicitly by
// VulkanTextureInfo which has the full context). The reverse maps TextureFormat::kExternal to
// VK_FORMAT_UNDEFINED, as expected, although callers are responsible for passing along the actual
// external format separately.
TextureFormat VkFormatToTextureFormat(VkFormat format) {
#define M(TF, VK) case VK: return TF;
switch(format) {
VK_FORMAT_MAPPING(M)
default: return TextureFormat::kUnsupported;
}
#undef M
}
VkFormat TextureFormatToVkFormat(TextureFormat format) {
#define M(TF, VK) case TF: return VK;
switch(format) {
VK_FORMAT_MAPPING(M)
default: return VK_FORMAT_UNDEFINED;
}
#undef M
}
VkImageAspectFlags GetVkImageAspectFlags(TextureFormat format) {
switch (format) {
case TextureFormat::kS8:
return VK_IMAGE_ASPECT_STENCIL_BIT;
case TextureFormat::kD16:
[[fallthrough]];
case TextureFormat::kD32F:
return VK_IMAGE_ASPECT_DEPTH_BIT;
case TextureFormat::kD24_S8:
[[fallthrough]];
case TextureFormat::kD32F_S8:
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
default:
return VK_IMAGE_ASPECT_COLOR_BIT;
}
}
VkShaderStageFlags PipelineStageFlagsToVkShaderStageFlags(
SkEnumBitMask<PipelineStageFlags> stageFlags) {
VkShaderStageFlags vkStageFlags = 0;
if (stageFlags & PipelineStageFlags::kVertexShader) {
vkStageFlags |= VK_SHADER_STAGE_VERTEX_BIT;
}
if (stageFlags & PipelineStageFlags::kFragmentShader) {
vkStageFlags |= VK_SHADER_STAGE_FRAGMENT_BIT;
}
if (stageFlags & PipelineStageFlags::kCompute) {
vkStageFlags |= VK_SHADER_STAGE_COMPUTE_BIT;
}
return vkStageFlags;
}
VkPipelineStageFlags PipelineStageFlagsToVkPipelineStageFlags(
SkEnumBitMask<PipelineStageFlags> stageFlags) {
VkPipelineStageFlags vkStageFlags = 0;
if (stageFlags & PipelineStageFlags::kVertexShader) {
vkStageFlags |= VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;
}
if (stageFlags & PipelineStageFlags::kFragmentShader) {
vkStageFlags |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
}
if (stageFlags & PipelineStageFlags::kCompute) {
vkStageFlags |= VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
}
return vkStageFlags;
}
bool RenderPassDescWillLoadMSAAFromResolve(const RenderPassDesc& renderPassDesc) {
return renderPassDesc.fColorResolveAttachment.fFormat != TextureFormat::kUnsupported &&
renderPassDesc.fColorResolveAttachment.fLoadOp == LoadOp::kLoad;
}
bool RenderPassDescWillImplicitlyLoadMSAA(const RenderPassDesc& renderPassDesc) {
SkASSERT(renderPassDesc.fColorAttachment.fFormat != TextureFormat::kUnsupported);
SkASSERT(renderPassDesc.fColorAttachment.fSampleCount > SampleCount::k1 ||
renderPassDesc.fColorResolveAttachment.fFormat == TextureFormat::kUnsupported);
return renderPassDesc.fColorAttachment.fSampleCount == SampleCount::k1 &&
renderPassDesc.fSampleCount > SampleCount::k1;
}
RenderPassDesc MakePipelineCompatibleRenderPass(const RenderPassDesc& renderPassDesc) {
RenderPassDesc compatible = renderPassDesc;
// Reset all load/store ops on the attachments since those do not affect compatibility.
// Choose a combination that is most likely to be used later by the real render pass.
const bool hasResolve =
renderPassDesc.fColorResolveAttachment.fFormat != TextureFormat::kUnsupported;
const bool needLoadMSAAFromResolveSubpass =
RenderPassDescWillLoadMSAAFromResolve(renderPassDesc);
compatible.fColorAttachment.fLoadOp =
needLoadMSAAFromResolveSubpass ? LoadOp::kDiscard : LoadOp::kClear;
compatible.fColorAttachment.fStoreOp = hasResolve ? StoreOp::kDiscard : StoreOp::kStore;
compatible.fColorResolveAttachment.fLoadOp =
needLoadMSAAFromResolveSubpass ? LoadOp::kLoad : LoadOp::kDiscard;
compatible.fColorResolveAttachment.fStoreOp = hasResolve ? StoreOp::kStore : StoreOp::kDiscard;
compatible.fDepthStencilAttachment.fLoadOp = LoadOp::kClear;
compatible.fDepthStencilAttachment.fStoreOp = StoreOp::kDiscard;
return compatible;
}
} // namespace skgpu::graphite