Implement the webgpu.h swapchains on Metal

The webgpu.h surface-based swapchains are implement on Metal which
required adding the present mode to NewSwapChainBase.

Additional automated tests are added which require getting the Instance
so a new getter is added to DawnTest. Additional some the state tracking
of swapchains is performed in the backend, so the
SwapChainValidationTests are turned into regular DawnTests so they can
check backends do the correct state tracking. To not lose coverage of
the Null backend, a NullBackend() DawnTestParam factory is added.

Finally swapchains cannot be entirely tested in an automated fashion, so
a new example is added called "ManualSwapChainTests" that allows
manually checking a number of properties. Documentation of the controls
and a manual test plan is in a comment at the top of the example's
source.

Bug: dawn:269

Change-Id: If62fffc29a6cefdbec62747d01c523e2a5475715
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/17181
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
This commit is contained in:
Corentin Wallez
2020-03-20 17:07:20 +00:00
committed by Commit Bot service account
parent 527045fe55
commit 11652ff8f8
13 changed files with 775 additions and 38 deletions

View File

@@ -110,6 +110,12 @@ DawnTestParam MetalBackend(std::initializer_list<const char*> forceEnabledWorkar
forceDisabledWorkarounds);
}
DawnTestParam NullBackend(std::initializer_list<const char*> forceEnabledWorkarounds,
std::initializer_list<const char*> forceDisabledWorkarounds) {
return DawnTestParam(wgpu::BackendType::Null, forceEnabledWorkarounds,
forceDisabledWorkarounds);
}
DawnTestParam OpenGLBackend(std::initializer_list<const char*> forceEnabledWorkarounds,
std::initializer_list<const char*> forceDisabledWorkarounds) {
return DawnTestParam(wgpu::BackendType::OpenGL, forceEnabledWorkarounds,
@@ -446,6 +452,10 @@ bool DawnTestBase::IsMetal() const {
return mParam.backendType == wgpu::BackendType::Metal;
}
bool DawnTestBase::IsNull() const {
return mParam.backendType == wgpu::BackendType::Null;
}
bool DawnTestBase::IsOpenGL() const {
return mParam.backendType == wgpu::BackendType::OpenGL;
}
@@ -526,6 +536,14 @@ uint32_t DawnTestBase::GetVendorIdFilter() const {
return gTestEnv->GetVendorIdFilter();
}
wgpu::Instance DawnTestBase::GetInstance() const {
return gTestEnv->GetInstance()->Get();
}
dawn_native::Adapter DawnTestBase::GetAdapter() const {
return mBackendAdapter;
}
std::vector<const char*> DawnTestBase::GetRequiredExtensions() {
return {};
}
@@ -958,6 +976,9 @@ namespace detail {
#if defined(DAWN_ENABLE_BACKEND_METAL)
case wgpu::BackendType::Metal:
#endif
#if defined(DAWN_ENABLE_BACKEND_NULL)
case wgpu::BackendType::Null:
#endif
#if defined(DAWN_ENABLE_BACKEND_OPENGL)
case wgpu::BackendType::OpenGL:
#endif

View File

@@ -104,6 +104,9 @@ DawnTestParam D3D12Backend(std::initializer_list<const char*> forceEnabledWorkar
DawnTestParam MetalBackend(std::initializer_list<const char*> forceEnabledWorkarounds = {},
std::initializer_list<const char*> forceDisabledWorkarounds = {});
DawnTestParam NullBackend(std::initializer_list<const char*> forceEnabledWorkarounds = {},
std::initializer_list<const char*> forceDisabledWorkarounds = {});
DawnTestParam OpenGLBackend(std::initializer_list<const char*> forceEnabledWorkarounds = {},
std::initializer_list<const char*> forceDisabledWorkarounds = {});
@@ -177,6 +180,7 @@ class DawnTestBase {
bool IsD3D12() const;
bool IsMetal() const;
bool IsNull() const;
bool IsOpenGL() const;
bool IsVulkan() const;
@@ -202,6 +206,9 @@ class DawnTestBase {
bool HasVendorIdFilter() const;
uint32_t GetVendorIdFilter() const;
wgpu::Instance GetInstance() const;
dawn_native::Adapter GetAdapter() const;
protected:
wgpu::Device device;
wgpu::Queue queue;

View File

@@ -0,0 +1,219 @@
// Copyright 2020 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/Constants.h"
#include "common/Log.h"
#include "utils/GLFWUtils.h"
#include "utils/WGPUHelpers.h"
#include "GLFW/glfw3.h"
class SwapChainTests : public DawnTest {
public:
void TestSetUp() override {
DAWN_SKIP_TEST_IF(UsesWire());
glfwSetErrorCallback([](int code, const char* message) {
dawn::ErrorLog() << "GLFW error " << code << " " << message;
});
glfwInit();
// The SwapChainTests don't create OpenGL contexts so we don't need to call
// SetupGLFWWindowHintsForBackend. Set GLFW_NO_API anyway to avoid GLFW bringing up a GL
// context that we won't use.
ASSERT_TRUE(!IsOpenGL());
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
window = glfwCreateWindow(400, 400, "SwapChainValidationTests window", nullptr, nullptr);
int width;
int height;
glfwGetFramebufferSize(window, &width, &height);
surface = utils::CreateSurfaceForWindow(GetInstance(), window);
ASSERT_NE(surface, nullptr);
baseDescriptor.width = width;
baseDescriptor.height = height;
baseDescriptor.usage = wgpu::TextureUsage::OutputAttachment;
baseDescriptor.format = wgpu::TextureFormat::BGRA8Unorm;
baseDescriptor.presentMode = wgpu::PresentMode::Mailbox;
}
void TearDown() override {
// Destroy the surface before the window as required by webgpu-native.
surface = wgpu::Surface();
if (window != nullptr) {
glfwDestroyWindow(window);
}
}
void ClearTexture(wgpu::TextureView view, wgpu::Color color) {
utils::ComboRenderPassDescriptor desc({view});
desc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
desc.cColorAttachments[0].clearColor = color;
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc);
pass.EndPass();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
}
protected:
GLFWwindow* window = nullptr;
wgpu::Surface surface;
wgpu::SwapChainDescriptor baseDescriptor;
};
// Basic test for creating a swapchain and presenting one frame.
TEST_P(SwapChainTests, Basic) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &baseDescriptor);
ClearTexture(swapchain.GetCurrentTextureView(), {1.0, 0.0, 0.0, 1.0});
swapchain.Present();
}
// Test replacing the swapchain
TEST_P(SwapChainTests, ReplaceBasic) {
wgpu::SwapChain swapchain1 = device.CreateSwapChain(surface, &baseDescriptor);
ClearTexture(swapchain1.GetCurrentTextureView(), {1.0, 0.0, 0.0, 1.0});
swapchain1.Present();
wgpu::SwapChain swapchain2 = device.CreateSwapChain(surface, &baseDescriptor);
ClearTexture(swapchain2.GetCurrentTextureView(), {0.0, 1.0, 0.0, 1.0});
swapchain2.Present();
}
// Test replacing the swapchain after GetCurrentTextureView
TEST_P(SwapChainTests, ReplaceAfterGet) {
wgpu::SwapChain swapchain1 = device.CreateSwapChain(surface, &baseDescriptor);
ClearTexture(swapchain1.GetCurrentTextureView(), {1.0, 0.0, 0.0, 1.0});
wgpu::SwapChain swapchain2 = device.CreateSwapChain(surface, &baseDescriptor);
ClearTexture(swapchain2.GetCurrentTextureView(), {0.0, 1.0, 0.0, 1.0});
swapchain2.Present();
}
// Test destroying the swapchain after GetCurrentTextureView
TEST_P(SwapChainTests, DestroyAfterGet) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &baseDescriptor);
ClearTexture(swapchain.GetCurrentTextureView(), {1.0, 0.0, 0.0, 1.0});
}
// Test destroying the surface before the swapchain
TEST_P(SwapChainTests, DestroySurface) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &baseDescriptor);
surface = nullptr;
}
// Test destroying the surface before the swapchain but after GetCurrentTextureView
TEST_P(SwapChainTests, DestroySurfaceAfterGet) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &baseDescriptor);
ClearTexture(swapchain.GetCurrentTextureView(), {1.0, 0.0, 0.0, 1.0});
surface = nullptr;
}
// Test switching between present modes.
TEST_P(SwapChainTests, SwitchPresentMode) {
constexpr wgpu::PresentMode kAllPresentModes[] = {
wgpu::PresentMode::Immediate,
wgpu::PresentMode::Fifo,
wgpu::PresentMode::Mailbox,
};
for (wgpu::PresentMode mode1 : kAllPresentModes) {
for (wgpu::PresentMode mode2 : kAllPresentModes) {
wgpu::SwapChainDescriptor desc = baseDescriptor;
desc.presentMode = mode1;
wgpu::SwapChain swapchain1 = device.CreateSwapChain(surface, &desc);
ClearTexture(swapchain1.GetCurrentTextureView(), {0.0, 0.0, 0.0, 1.0});
swapchain1.Present();
desc.presentMode = mode2;
wgpu::SwapChain swapchain2 = device.CreateSwapChain(surface, &desc);
ClearTexture(swapchain2.GetCurrentTextureView(), {0.0, 0.0, 0.0, 1.0});
swapchain2.Present();
}
}
}
// Test resizing the swapchain and without resizing the window.
TEST_P(SwapChainTests, ResizingSwapChainOnly) {
for (int i = 0; i < 10; i++) {
wgpu::SwapChainDescriptor desc = baseDescriptor;
desc.width += i * 10;
desc.height -= i * 10;
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &desc);
ClearTexture(swapchain.GetCurrentTextureView(), {0.05 * i, 0.0, 0.0, 1.0});
swapchain.Present();
}
}
// Test resizing the window but not the swapchain.
TEST_P(SwapChainTests, ResizingWindowOnly) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &baseDescriptor);
for (int i = 0; i < 10; i++) {
glfwSetWindowSize(window, 400 - 10 * i, 400 + 10 * i);
glfwPollEvents();
ClearTexture(swapchain.GetCurrentTextureView(), {0.05 * i, 0.0, 0.0, 1.0});
swapchain.Present();
}
}
// Test resizing both the window and the swapchain at the same time.
TEST_P(SwapChainTests, ResizingWindowAndSwapChain) {
for (int i = 0; i < 10; i++) {
glfwSetWindowSize(window, 400 - 10 * i, 400 + 10 * i);
glfwPollEvents();
int width;
int height;
glfwGetFramebufferSize(window, &width, &height);
wgpu::SwapChainDescriptor desc = baseDescriptor;
desc.width = width;
desc.height = height;
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &desc);
ClearTexture(swapchain.GetCurrentTextureView(), {0.05 * i, 0.0, 0.0, 1.0});
swapchain.Present();
}
}
// Test switching devices on the same adapter.
TEST_P(SwapChainTests, SwitchingDevice) {
wgpu::Device device2 = GetAdapter().CreateDevice();
for (int i = 0; i < 3; i++) {
wgpu::Device deviceToUse;
if (i % 2 == 0) {
deviceToUse = device;
} else {
deviceToUse = device2;
}
wgpu::SwapChain swapchain = deviceToUse.CreateSwapChain(surface, &baseDescriptor);
swapchain.GetCurrentTextureView();
swapchain.Present();
}
}
DAWN_INSTANTIATE_TEST(SwapChainTests, MetalBackend());

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "tests/unittests/validation/ValidationTest.h"
#include "tests/DawnTest.h"
#include "common/Constants.h"
#include "common/Log.h"
@@ -22,21 +22,25 @@
#include "GLFW/glfw3.h"
class SwapChainValidationTests : public ValidationTest {
class SwapChainValidationTests : public DawnTest {
public:
void SetUp() override {
void TestSetUp() override {
DAWN_SKIP_TEST_IF(UsesWire());
DAWN_SKIP_TEST_IF(IsDawnValidationSkipped());
glfwSetErrorCallback([](int code, const char* message) {
dawn::ErrorLog() << "GLFW error " << code << " " << message;
});
glfwInit();
// The SwapChainValidationTests tests don't create devices so we don't need to call
// The SwapChainValidationTests don't create devices so we don't need to call
// SetupGLFWWindowHintsForBackend. Set GLFW_NO_API anyway to avoid GLFW bringing up a GL
// context that we won't use.
ASSERT_TRUE(!IsOpenGL());
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
window = glfwCreateWindow(400, 400, "SwapChainValidationTests window", nullptr, nullptr);
surface = utils::CreateSurfaceForWindow(instance->Get(), window);
surface = utils::CreateSurfaceForWindow(GetInstance(), window);
ASSERT_NE(surface, nullptr);
goodDescriptor.width = 1;
@@ -52,7 +56,9 @@ class SwapChainValidationTests : public ValidationTest {
void TearDown() override {
// Destroy the surface before the window as required by webgpu-native.
surface = wgpu::Surface();
glfwDestroyWindow(window);
if (window != nullptr) {
glfwDestroyWindow(window);
}
}
protected:
@@ -86,14 +92,14 @@ class SwapChainValidationTests : public ValidationTest {
};
// Control case for a successful swapchain creation and presenting.
TEST_F(SwapChainValidationTests, CreationSuccess) {
TEST_P(SwapChainValidationTests, CreationSuccess) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
wgpu::TextureView view = swapchain.GetCurrentTextureView();
swapchain.Present();
}
// Checks that the creation size must be a valid 2D texture size.
TEST_F(SwapChainValidationTests, InvalidCreationSize) {
TEST_P(SwapChainValidationTests, InvalidCreationSize) {
// A width of 0 is invalid.
{
wgpu::SwapChainDescriptor desc = goodDescriptor;
@@ -129,28 +135,28 @@ TEST_F(SwapChainValidationTests, InvalidCreationSize) {
}
// Checks that the creation usage must be OutputAttachment
TEST_F(SwapChainValidationTests, InvalidCreationUsage) {
TEST_P(SwapChainValidationTests, InvalidCreationUsage) {
wgpu::SwapChainDescriptor desc = goodDescriptor;
desc.usage = wgpu::TextureUsage::Sampled;
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
}
// Checks that the creation format must (currently) be BGRA8Unorm
TEST_F(SwapChainValidationTests, InvalidCreationFormat) {
TEST_P(SwapChainValidationTests, InvalidCreationFormat) {
wgpu::SwapChainDescriptor desc = goodDescriptor;
desc.format = wgpu::TextureFormat::RGBA8Unorm;
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
}
// Checks that the implementation must be zero.
TEST_F(SwapChainValidationTests, InvalidWithImplementation) {
TEST_P(SwapChainValidationTests, InvalidWithImplementation) {
wgpu::SwapChainDescriptor desc = goodDescriptor;
desc.implementation = 1;
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
}
// Check swapchain operations with an error swapchain are errors
TEST_F(SwapChainValidationTests, OperationsOnErrorSwapChain) {
TEST_P(SwapChainValidationTests, OperationsOnErrorSwapChain) {
wgpu::SwapChain swapchain;
ASSERT_DEVICE_ERROR(swapchain = device.CreateSwapChain(surface, &badDescriptor));
@@ -162,7 +168,7 @@ TEST_F(SwapChainValidationTests, OperationsOnErrorSwapChain) {
}
// Check it is invalid to call present without getting a current view.
TEST_F(SwapChainValidationTests, PresentWithoutCurrentView) {
TEST_P(SwapChainValidationTests, PresentWithoutCurrentView) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
// Check it is invalid if we never called GetCurrentTextureView
@@ -175,7 +181,7 @@ TEST_F(SwapChainValidationTests, PresentWithoutCurrentView) {
}
// Check that the current view is in the destroyed state after the swapchain is destroyed.
TEST_F(SwapChainValidationTests, ViewDestroyedAfterSwapChainDestruction) {
TEST_P(SwapChainValidationTests, ViewDestroyedAfterSwapChainDestruction) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
wgpu::TextureView view = swapchain.GetCurrentTextureView();
swapchain = nullptr;
@@ -184,7 +190,7 @@ TEST_F(SwapChainValidationTests, ViewDestroyedAfterSwapChainDestruction) {
}
// Check that the current view is the destroyed state after present.
TEST_F(SwapChainValidationTests, ViewDestroyedAfterPresent) {
TEST_P(SwapChainValidationTests, ViewDestroyedAfterPresent) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
wgpu::TextureView view = swapchain.GetCurrentTextureView();
swapchain.Present();
@@ -193,7 +199,7 @@ TEST_F(SwapChainValidationTests, ViewDestroyedAfterPresent) {
}
// Check that returned view is of the current format / usage / dimension / size / sample count
TEST_F(SwapChainValidationTests, ReturnedViewCharacteristics) {
TEST_P(SwapChainValidationTests, ReturnedViewCharacteristics) {
utils::ComboRenderPipelineDescriptor pipelineDesc(device);
pipelineDesc.vertexStage.module =
utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, R"(
@@ -249,7 +255,7 @@ TEST_F(SwapChainValidationTests, ReturnedViewCharacteristics) {
}
// Check that failing to create a new swapchain doesn't replace the previous one.
TEST_F(SwapChainValidationTests, ErrorSwapChainDoesntReplacePreviousOne) {
TEST_P(SwapChainValidationTests, ErrorSwapChainDoesntReplacePreviousOne) {
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &badDescriptor));
@@ -258,7 +264,7 @@ TEST_F(SwapChainValidationTests, ErrorSwapChainDoesntReplacePreviousOne) {
}
// Check that after replacement, all swapchain operations are errors and the view is destroyed.
TEST_F(SwapChainValidationTests, ReplacedSwapChainIsInvalid) {
TEST_P(SwapChainValidationTests, ReplacedSwapChainIsInvalid) {
{
wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor);
device.CreateSwapChain(surface, &goodDescriptor);
@@ -277,12 +283,12 @@ TEST_F(SwapChainValidationTests, ReplacedSwapChainIsInvalid) {
// Check that after surface destruction, all swapchain operations are errors and the view is
// destroyed. The test is split in two to reset the wgpu::Surface in the middle.
TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_GetView) {
TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_GetView) {
wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor);
surface = nullptr;
ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTextureView());
}
TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetView) {
TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetView) {
wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor);
wgpu::TextureView view = replacedSwapChain.GetCurrentTextureView();
surface = nullptr;
@@ -293,12 +299,12 @@ TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_After
// Test that after Device is Lost, all swap chain operations fail
static void ToMockDeviceLostCallback(const char* message, void* userdata) {
ValidationTest* self = static_cast<ValidationTest*>(userdata);
DawnTest* self = static_cast<DawnTest*>(userdata);
self->StartExpectDeviceError();
}
// Test that new swap chain present fails after device is lost
TEST_F(SwapChainValidationTests, NewSwapChainPresentFailsAfterDeviceLost) {
TEST_P(SwapChainValidationTests, NewSwapChainPresentFailsAfterDeviceLost) {
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
wgpu::TextureView view = swapchain.GetCurrentTextureView();
@@ -308,7 +314,7 @@ TEST_F(SwapChainValidationTests, NewSwapChainPresentFailsAfterDeviceLost) {
}
// Test that new swap chain get current texture view fails after device is lost
TEST_F(SwapChainValidationTests, NewSwapChainGetCurrentTextureViewFailsAfterDevLost) {
TEST_P(SwapChainValidationTests, NewSwapChainGetCurrentTextureViewFailsAfterDevLost) {
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
@@ -316,10 +322,12 @@ TEST_F(SwapChainValidationTests, NewSwapChainGetCurrentTextureViewFailsAfterDevL
ASSERT_DEVICE_ERROR(swapchain.GetCurrentTextureView());
}
// Test that creation of a new swapchain fails
TEST_F(SwapChainValidationTests, CreateNewSwapChainFailsAfterDevLost) {
// Test that creation of a new swapchain fails after device is lost
TEST_P(SwapChainValidationTests, CreateNewSwapChainFailsAfterDevLost) {
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
device.LoseForTesting();
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &goodDescriptor));
}
DAWN_INSTANTIATE_TEST(SwapChainValidationTests, MetalBackend(), NullBackend());