blob: ef03bf2f89a7a4d8f5a44f470e7468453dedca6b [file] [log] [blame]
// Copyright 2019 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "tests/DawnTest.h"
#include "common/Assert.h"
#include "utils/ComboRenderPipelineDescriptor.h"
#include "utils/WGPUHelpers.h"
class MultisampledRenderingTest : public DawnTest {
protected:
void SetUp() override {
DawnTest::SetUp();
InitTexturesForTest();
}
void InitTexturesForTest() {
mMultisampledColorTexture = CreateTextureForOutputAttachment(kColorFormat, kSampleCount);
mMultisampledColorView = mMultisampledColorTexture.CreateView();
mResolveTexture = CreateTextureForOutputAttachment(kColorFormat, 1);
mResolveView = mResolveTexture.CreateView();
mDepthStencilTexture = CreateTextureForOutputAttachment(kDepthStencilFormat, kSampleCount);
mDepthStencilView = mDepthStencilTexture.CreateView();
}
wgpu::RenderPipeline CreateRenderPipelineWithOneOutputForTest(bool testDepth) {
const char* kFsOneOutputWithDepth =
R"(#version 450
layout(location = 0) out vec4 fragColor;
layout (std140, set = 0, binding = 0) uniform uBuffer {
vec4 color;
float depth;
};
void main() {
fragColor = color;
gl_FragDepth = depth;
})";
const char* kFsOneOutputWithoutDepth =
R"(#version 450
layout(location = 0) out vec4 fragColor;
layout (std140, set = 0, binding = 0) uniform uBuffer {
vec4 color;
};
void main() {
fragColor = color;
})";
const char* fs = testDepth ? kFsOneOutputWithDepth : kFsOneOutputWithoutDepth;
return CreateRenderPipelineForTest(fs, 1, testDepth);
}
wgpu::RenderPipeline CreateRenderPipelineWithTwoOutputsForTest() {
const char* kFsTwoOutputs =
R"(#version 450
layout(location = 0) out vec4 fragColor1;
layout(location = 1) out vec4 fragColor2;
layout (std140, set = 0, binding = 0) uniform uBuffer {
vec4 color1;
vec4 color2;
};
void main() {
fragColor1 = color1;
fragColor2 = color2;
})";
return CreateRenderPipelineForTest(kFsTwoOutputs, 2, false);
}
wgpu::Texture CreateTextureForOutputAttachment(wgpu::TextureFormat format,
uint32_t sampleCount,
uint32_t mipLevelCount = 1,
uint32_t arrayLayerCount = 1) {
wgpu::TextureDescriptor descriptor;
descriptor.dimension = wgpu::TextureDimension::e2D;
descriptor.size.width = kWidth << (mipLevelCount - 1);
descriptor.size.height = kHeight << (mipLevelCount - 1);
descriptor.size.depth = arrayLayerCount;
descriptor.sampleCount = sampleCount;
descriptor.format = format;
descriptor.mipLevelCount = mipLevelCount;
descriptor.usage = wgpu::TextureUsage::OutputAttachment | wgpu::TextureUsage::CopySrc;
return device.CreateTexture(&descriptor);
}
void EncodeRenderPassForTest(wgpu::CommandEncoder commandEncoder,
const wgpu::RenderPassDescriptor& renderPass,
const wgpu::RenderPipeline& pipeline,
const float* uniformData,
uint32_t uniformDataSize) {
wgpu::Buffer uniformBuffer = utils::CreateBufferFromData(
device, uniformData, uniformDataSize, wgpu::BufferUsage::Uniform);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
{{0, uniformBuffer, 0, uniformDataSize}});
wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
renderPassEncoder.SetPipeline(pipeline);
renderPassEncoder.SetBindGroup(0, bindGroup);
renderPassEncoder.Draw(3);
renderPassEncoder.EndPass();
}
utils::ComboRenderPassDescriptor CreateComboRenderPassDescriptorForTest(
std::initializer_list<wgpu::TextureView> colorViews,
std::initializer_list<wgpu::TextureView> resolveTargetViews,
wgpu::LoadOp colorLoadOp,
wgpu::LoadOp depthStencilLoadOp,
bool hasDepthStencilAttachment) {
ASSERT(colorViews.size() == resolveTargetViews.size());
constexpr wgpu::Color kClearColor = {0.0f, 0.0f, 0.0f, 0.0f};
constexpr float kClearDepth = 1.0f;
utils::ComboRenderPassDescriptor renderPass(colorViews);
uint32_t i = 0;
for (const wgpu::TextureView& resolveTargetView : resolveTargetViews) {
renderPass.cColorAttachments[i].loadOp = colorLoadOp;
renderPass.cColorAttachments[i].clearColor = kClearColor;
renderPass.cColorAttachments[i].resolveTarget = resolveTargetView;
++i;
}
renderPass.cDepthStencilAttachmentInfo.clearDepth = kClearDepth;
renderPass.cDepthStencilAttachmentInfo.depthLoadOp = depthStencilLoadOp;
if (hasDepthStencilAttachment) {
renderPass.cDepthStencilAttachmentInfo.attachment = mDepthStencilView;
renderPass.depthStencilAttachment = &renderPass.cDepthStencilAttachmentInfo;
}
return renderPass;
}
void VerifyResolveTarget(const wgpu::Color& inputColor,
wgpu::Texture resolveTexture,
uint32_t mipmapLevel = 0,
uint32_t arrayLayer = 0) {
constexpr float kMSAACoverage = 0.5f;
// In this test we only check the pixel in the middle of the texture.
constexpr uint32_t kMiddleX = (kWidth - 1) / 2;
constexpr uint32_t kMiddleY = (kHeight - 1) / 2;
RGBA8 expectedColor;
expectedColor.r = static_cast<uint8_t>(0xFF * inputColor.r * kMSAACoverage);
expectedColor.g = static_cast<uint8_t>(0xFF * inputColor.g * kMSAACoverage);
expectedColor.b = static_cast<uint8_t>(0xFF * inputColor.b * kMSAACoverage);
expectedColor.a = static_cast<uint8_t>(0xFF * inputColor.a * kMSAACoverage);
EXPECT_TEXTURE_RGBA8_EQ(&expectedColor, resolveTexture, kMiddleX, kMiddleY, 1, 1,
mipmapLevel, arrayLayer);
}
constexpr static uint32_t kWidth = 3;
constexpr static uint32_t kHeight = 3;
constexpr static uint32_t kSampleCount = 4;
constexpr static wgpu::TextureFormat kColorFormat = wgpu::TextureFormat::RGBA8Unorm;
constexpr static wgpu::TextureFormat kDepthStencilFormat =
wgpu::TextureFormat::Depth24PlusStencil8;
wgpu::Texture mMultisampledColorTexture;
wgpu::TextureView mMultisampledColorView;
wgpu::Texture mResolveTexture;
wgpu::TextureView mResolveView;
wgpu::Texture mDepthStencilTexture;
wgpu::TextureView mDepthStencilView;
private:
wgpu::RenderPipeline CreateRenderPipelineForTest(const char* fs,
uint32_t numColorAttachments,
bool hasDepthStencilAttachment) {
utils::ComboRenderPipelineDescriptor pipelineDescriptor(device);
// Draw a bottom-right triangle. In standard 4xMSAA pattern, for the pixels on diagonal,
// only two of the samples will be touched.
const char* vs =
R"(#version 450
const vec2 pos[3] = vec2[3](vec2(-1.f, 1.f), vec2(1.f, 1.f), vec2(1.f, -1.f));
void main() {
gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
})";
pipelineDescriptor.vertexStage.module =
utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, vs);
pipelineDescriptor.cFragmentStage.module =
utils::CreateShaderModule(device, utils::SingleShaderStage::Fragment, fs);
if (hasDepthStencilAttachment) {
pipelineDescriptor.cDepthStencilState.format = kDepthStencilFormat;
pipelineDescriptor.cDepthStencilState.depthWriteEnabled = true;
pipelineDescriptor.cDepthStencilState.depthCompare = wgpu::CompareFunction::Less;
pipelineDescriptor.depthStencilState = &pipelineDescriptor.cDepthStencilState;
}
pipelineDescriptor.sampleCount = kSampleCount;
pipelineDescriptor.colorStateCount = numColorAttachments;
for (uint32_t i = 0; i < numColorAttachments; ++i) {
pipelineDescriptor.cColorStates[i].format = kColorFormat;
}
wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
return pipeline;
}
};
// Test using one multisampled color attachment with resolve target can render correctly.
TEST_P(MultisampledRenderingTest, ResolveInto2DTexture) {
constexpr bool kTestDepth = false;
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(kTestDepth);
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr uint32_t kSize = sizeof(kGreen);
// Draw a green triangle.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView}, {mResolveView}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
kTestDepth);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, &kGreen.r, kSize);
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
VerifyResolveTarget(kGreen, mResolveTexture);
}
// Test that a single-layer multisampled texture view can be created and resolved from.
TEST_P(MultisampledRenderingTest, ResolveFromSingleLayerArrayInto2DTexture) {
constexpr bool kTestDepth = false;
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(kTestDepth);
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr uint32_t kSize = sizeof(kGreen);
// Draw a green triangle.
{
wgpu::TextureViewDescriptor desc = {};
desc.dimension = wgpu::TextureViewDimension::e2DArray;
desc.arrayLayerCount = 1;
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorTexture.CreateView(&desc)}, {mResolveView}, wgpu::LoadOp::Clear,
wgpu::LoadOp::Clear, kTestDepth);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, &kGreen.r, kSize);
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
VerifyResolveTarget(kGreen, mResolveTexture);
}
// Test multisampled rendering with depth test works correctly.
TEST_P(MultisampledRenderingTest, MultisampledRenderingWithDepthTest) {
constexpr bool kTestDepth = true;
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(kTestDepth);
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
// In first render pass we draw a green triangle with depth value == 0.2f.
{
utils::ComboRenderPassDescriptor renderPass =
CreateComboRenderPassDescriptorForTest({mMultisampledColorView}, {mResolveView},
wgpu::LoadOp::Clear, wgpu::LoadOp::Clear, true);
std::array<float, 5> kUniformData = {kGreen.r, kGreen.g, kGreen.b, kGreen.a, // Color
0.2f}; // depth
constexpr uint32_t kSize = sizeof(kUniformData);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
}
// In second render pass we draw a red triangle with depth value == 0.5f.
// This red triangle should not be displayed because it is behind the green one that is drawn in
// the last render pass.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView}, {mResolveView}, wgpu::LoadOp::Load, wgpu::LoadOp::Load,
kTestDepth);
std::array<float, 8> kUniformData = {kRed.r, kRed.g, kRed.b, kRed.a, // color
0.5f}; // depth
constexpr uint32_t kSize = sizeof(kUniformData);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
// The color of the pixel in the middle of mResolveTexture should be green if MSAA resolve runs
// correctly with depth test.
VerifyResolveTarget(kGreen, mResolveTexture);
}
// Test rendering into a multisampled color attachment and doing MSAA resolve in another render pass
// works correctly.
TEST_P(MultisampledRenderingTest, ResolveInAnotherRenderPass) {
constexpr bool kTestDepth = false;
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(kTestDepth);
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr uint32_t kSize = sizeof(kGreen);
// In first render pass we draw a green triangle and do not set the resolve target.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView}, {nullptr}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
kTestDepth);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, &kGreen.r, kSize);
}
// In second render pass we ony do MSAA resolve with no draw call.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView}, {mResolveView}, wgpu::LoadOp::Load, wgpu::LoadOp::Load,
kTestDepth);
wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
renderPassEncoder.EndPass();
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
VerifyResolveTarget(kGreen, mResolveTexture);
}
// Test doing MSAA resolve into multiple resolve targets works correctly.
TEST_P(MultisampledRenderingTest, ResolveIntoMultipleResolveTargets) {
wgpu::TextureView multisampledColorView2 =
CreateTextureForOutputAttachment(kColorFormat, kSampleCount).CreateView();
wgpu::Texture resolveTexture2 = CreateTextureForOutputAttachment(kColorFormat, 1);
wgpu::TextureView resolveView2 = resolveTexture2.CreateView();
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithTwoOutputsForTest();
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
constexpr bool kTestDepth = false;
// Draw a red triangle to the first color attachment, and a blue triangle to the second color
// attachment, and do MSAA resolve on two render targets in one render pass.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView, multisampledColorView2}, {mResolveView, resolveView2},
wgpu::LoadOp::Clear, wgpu::LoadOp::Clear, kTestDepth);
std::array<float, 8> kUniformData = {kRed.r, kRed.g, kRed.b, kRed.a, // color1
kGreen.r, kGreen.g, kGreen.b, kGreen.a}; // color2
constexpr uint32_t kSize = sizeof(kUniformData);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
VerifyResolveTarget(kRed, mResolveTexture);
VerifyResolveTarget(kGreen, resolveTexture2);
}
// Test doing MSAA resolve on one multisampled texture twice works correctly.
TEST_P(MultisampledRenderingTest, ResolveOneMultisampledTextureTwice) {
constexpr bool kTestDepth = false;
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(kTestDepth);
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr uint32_t kSize = sizeof(kGreen);
wgpu::Texture resolveTexture2 = CreateTextureForOutputAttachment(kColorFormat, 1);
// In first render pass we draw a green triangle and specify mResolveView as the resolve target.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView}, {mResolveView}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
kTestDepth);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, &kGreen.r, kSize);
}
// In second render pass we do MSAA resolve into resolveTexture2.
{
wgpu::TextureView resolveView2 = resolveTexture2.CreateView();
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView}, {resolveView2}, wgpu::LoadOp::Load, wgpu::LoadOp::Load,
kTestDepth);
wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
renderPassEncoder.EndPass();
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
VerifyResolveTarget(kGreen, mResolveTexture);
VerifyResolveTarget(kGreen, resolveTexture2);
}
// Test using a layer of a 2D texture as resolve target works correctly.
TEST_P(MultisampledRenderingTest, ResolveIntoOneMipmapLevelOf2DTexture) {
constexpr uint32_t kBaseMipLevel = 2;
wgpu::TextureViewDescriptor textureViewDescriptor;
textureViewDescriptor.dimension = wgpu::TextureViewDimension::e2D;
textureViewDescriptor.format = kColorFormat;
textureViewDescriptor.baseArrayLayer = 0;
textureViewDescriptor.arrayLayerCount = 1;
textureViewDescriptor.mipLevelCount = 1;
textureViewDescriptor.baseMipLevel = kBaseMipLevel;
wgpu::Texture resolveTexture =
CreateTextureForOutputAttachment(kColorFormat, 1, kBaseMipLevel + 1, 1);
wgpu::TextureView resolveView = resolveTexture.CreateView(&textureViewDescriptor);
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr uint32_t kSize = sizeof(kGreen);
constexpr bool kTestDepth = false;
// Draw a green triangle and do MSAA resolve.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView}, {resolveView}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
kTestDepth);
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(kTestDepth);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, &kGreen.r, kSize);
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
VerifyResolveTarget(kGreen, resolveTexture, kBaseMipLevel, 0);
}
// Test using a level or a layer of a 2D array texture as resolve target works correctly.
TEST_P(MultisampledRenderingTest, ResolveInto2DArrayTexture) {
wgpu::TextureView multisampledColorView2 =
CreateTextureForOutputAttachment(kColorFormat, kSampleCount).CreateView();
wgpu::TextureViewDescriptor baseTextureViewDescriptor;
baseTextureViewDescriptor.dimension = wgpu::TextureViewDimension::e2D;
baseTextureViewDescriptor.format = kColorFormat;
baseTextureViewDescriptor.arrayLayerCount = 1;
baseTextureViewDescriptor.mipLevelCount = 1;
// Create resolveTexture1 with only 1 mipmap level.
constexpr uint32_t kBaseArrayLayer1 = 2;
constexpr uint32_t kBaseMipLevel1 = 0;
wgpu::Texture resolveTexture1 =
CreateTextureForOutputAttachment(kColorFormat, 1, kBaseMipLevel1 + 1, kBaseArrayLayer1 + 1);
wgpu::TextureViewDescriptor resolveViewDescriptor1 = baseTextureViewDescriptor;
resolveViewDescriptor1.baseArrayLayer = kBaseArrayLayer1;
resolveViewDescriptor1.baseMipLevel = kBaseMipLevel1;
wgpu::TextureView resolveView1 = resolveTexture1.CreateView(&resolveViewDescriptor1);
// Create resolveTexture2 with (kBaseMipLevel2 + 1) mipmap levels and resolve into its last
// mipmap level.
constexpr uint32_t kBaseArrayLayer2 = 5;
constexpr uint32_t kBaseMipLevel2 = 3;
wgpu::Texture resolveTexture2 =
CreateTextureForOutputAttachment(kColorFormat, 1, kBaseMipLevel2 + 1, kBaseArrayLayer2 + 1);
wgpu::TextureViewDescriptor resolveViewDescriptor2 = baseTextureViewDescriptor;
resolveViewDescriptor2.baseArrayLayer = kBaseArrayLayer2;
resolveViewDescriptor2.baseMipLevel = kBaseMipLevel2;
wgpu::TextureView resolveView2 = resolveTexture2.CreateView(&resolveViewDescriptor2);
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPipeline pipeline = CreateRenderPipelineWithTwoOutputsForTest();
constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
constexpr bool kTestDepth = false;
// Draw a red triangle to the first color attachment, and a green triangle to the second color
// attachment, and do MSAA resolve on two render targets in one render pass.
{
utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
{mMultisampledColorView, multisampledColorView2}, {resolveView1, resolveView2},
wgpu::LoadOp::Clear, wgpu::LoadOp::Clear, kTestDepth);
std::array<float, 8> kUniformData = {kRed.r, kRed.g, kRed.b, kRed.a, // color1
kGreen.r, kGreen.g, kGreen.b, kGreen.a}; // color2
constexpr uint32_t kSize = sizeof(kUniformData);
EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
}
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
queue.Submit(1, &commandBuffer);
VerifyResolveTarget(kRed, resolveTexture1, kBaseMipLevel1, kBaseArrayLayer1);
VerifyResolveTarget(kGreen, resolveTexture2, kBaseMipLevel2, kBaseArrayLayer2);
}
DAWN_INSTANTIATE_TEST(MultisampledRenderingTest,
D3D12Backend(),
D3D12Backend({}, {"use_d3d12_resource_heap_tier2"}),
D3D12Backend({}, {"use_d3d12_render_pass"}),
MetalBackend(),
OpenGLBackend(),
VulkanBackend(),
MetalBackend({"emulate_store_and_msaa_resolve"}),
MetalBackend({"always_resolve_into_zero_level_and_layer"}),
MetalBackend({"always_resolve_into_zero_level_and_layer",
"emulate_store_and_msaa_resolve"}));