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

@ -997,6 +997,7 @@ source_set("dawn_end2end_tests_sources") {
if (supports_glfw_for_windowing) {
sources += [
"src/tests/end2end/SwapChainTests.cpp",
"src/tests/end2end/SwapChainValidationTests.cpp",
"src/tests/end2end/WindowSurfaceTests.cpp",
]
@ -1291,6 +1292,12 @@ if (dawn_standalone) {
]
}
dawn_sample("ManualSwapChainTest") {
sources = [
"examples/ManualSwapChainTest.cpp",
]
}
group("dawn_samples") {
deps = [
":Animometer",

View File

@ -48,7 +48,7 @@ void init() {
const char* fs =
"#version 450\n"
"layout(location = 0) out vec4 fragColor;"
"layout(location = 0) out vec4 fragColor;\n"
"void main() {\n"
" fragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";

View File

@ -0,0 +1,362 @@
// 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.
// This is an example to manually test swapchain code. Controls are the following, scoped to the
// currently focused window:
// - W: creates a new window.
// - L: Latches the current swapchain, to check what happens when the window changes but not the
// swapchain.
// - R: switches the rendering mode, between "The Red Triangle" and color-cycling clears that's
// (WARNING) likely seizure inducing.
// - D: cycles the divisor for the swapchain size.
// - P: switches present modes.
//
// Closing all the windows exits the example. ^C also works.
//
// Things to test manually:
//
// - Basic tests (with the triangle render mode):
// - Check the triangle is red on a black background and with the pointy side up.
// - Cycle render modes a bunch and check that the triangle background is always solid black.
// - Check that rendering triangles to multiple windows works.
//
// - Present mode single-window tests (with cycling color render mode):
// - Check that Fifo cycles at about 1 cycle per second and has no tearing.
// - Check that Mailbox cycles faster than Fifo and has no tearing.
// - Check that Immediate cycles faster than Fifo, it is allowed to have tearing. (dragging
// between two monitors can help see tearing)
//
// - Present mode multi-window tests, it should have the same results as single-window tests when
// all windows are in the same present mode. In mixed present modes only Immediate windows are
// allowed to tear.
//
// - Resizing tests (with the triangle render mode):
// - Check that cycling divisors on the triangle produces lower and lower resolution triangles.
// - Check latching the swapchain config and resizing the window a bunch (smaller, bigger, and
// diagonal aspect ratio).
//
// - Config change tests:
// - Check that cycling between present modes works.
// - TODO can't be tested yet: check cycling the same window over multiple devices.
// - TODO can't be tested yet: check cycling the same window over multiple formats.
#include "common/Assert.h"
#include "common/Log.h"
#include "utils/ComboRenderPipelineDescriptor.h"
#include "utils/GLFWUtils.h"
#include "utils/WGPUHelpers.h"
#include <dawn/dawn_proc.h>
#include <dawn/webgpu_cpp.h>
#include <dawn_native/DawnNative.h>
#include "GLFW/glfw3.h"
#include <memory>
#include <unordered_map>
struct WindowData {
GLFWwindow* window = nullptr;
uint64_t serial = 0;
float clearCycle = 1.0f;
bool latched = false;
bool renderTriangle = true;
uint32_t divisor = 1;
wgpu::Surface surface = nullptr;
wgpu::SwapChain swapchain = nullptr;
wgpu::SwapChainDescriptor currentDesc;
wgpu::SwapChainDescriptor targetDesc;
};
static std::unordered_map<GLFWwindow*, std::unique_ptr<WindowData>> windows;
static uint64_t windowSerial = 0;
static std::unique_ptr<dawn_native::Instance> instance;
static wgpu::Device device;
static wgpu::Queue queue;
static wgpu::RenderPipeline trianglePipeline;
bool IsSameDescriptor(const wgpu::SwapChainDescriptor& a, const wgpu::SwapChainDescriptor& b) {
return a.usage == b.usage && a.format == b.format && a.width == b.width &&
a.height == b.height && a.presentMode == b.presentMode;
}
void OnKeyPress(GLFWwindow* window, int key, int, int action, int);
void SyncFromWindow(WindowData* data) {
int width;
int height;
glfwGetFramebufferSize(data->window, &width, &height);
data->targetDesc.width = std::max(1u, width / data->divisor);
data->targetDesc.height = std::max(1u, height / data->divisor);
}
void AddWindow() {
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window = glfwCreateWindow(400, 400, "", nullptr, nullptr);
glfwSetKeyCallback(window, OnKeyPress);
wgpu::SwapChainDescriptor descriptor;
descriptor.usage = wgpu::TextureUsage::OutputAttachment;
descriptor.format = wgpu::TextureFormat::BGRA8Unorm;
descriptor.width = 0;
descriptor.height = 0;
descriptor.presentMode = wgpu::PresentMode::Fifo;
std::unique_ptr<WindowData> data = std::make_unique<WindowData>();
data->window = window;
data->serial = windowSerial++;
data->surface = utils::CreateSurfaceForWindow(instance->Get(), window);
data->currentDesc = descriptor;
data->targetDesc = descriptor;
SyncFromWindow(data.get());
windows[window] = std::move(data);
}
void DoRender(WindowData* data) {
wgpu::TextureView view = data->swapchain.GetCurrentTextureView();
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
if (data->renderTriangle) {
utils::ComboRenderPassDescriptor desc({view});
// Use Load to check the swapchain is lazy cleared (we shouldn't see garbage from previous
// frames).
desc.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc);
pass.SetPipeline(trianglePipeline);
pass.Draw(3, 1, 0, 0);
pass.EndPass();
} else {
data->clearCycle -= 1.0 / 60.f;
if (data->clearCycle < 0.0) {
data->clearCycle = 1.0f;
}
utils::ComboRenderPassDescriptor desc({view});
desc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
desc.cColorAttachments[0].clearColor = {data->clearCycle, 1.0 - data->clearCycle, 0.0, 1.0};
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc);
pass.EndPass();
}
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
data->swapchain.Present();
}
std::ostream& operator<<(std::ostream& o, const wgpu::SwapChainDescriptor& desc) {
// For now only output attachment is possible.
ASSERT(desc.usage == wgpu::TextureUsage::OutputAttachment);
o << "OutputAttachment ";
o << desc.width << "x" << desc.height << " ";
// For now only BGRA is allowed
ASSERT(desc.format == wgpu::TextureFormat::BGRA8Unorm);
o << "BGRA8Unorm ";
switch (desc.presentMode) {
case wgpu::PresentMode::Immediate:
o << "Immediate";
break;
case wgpu::PresentMode::Fifo:
o << "Fifo";
break;
case wgpu::PresentMode::Mailbox:
o << "Mailbox";
break;
}
return o;
}
void UpdateTitle(WindowData* data) {
std::ostringstream o;
o << data->serial << " ";
if (data->divisor != 1) {
o << "Divisor:" << data->divisor << " ";
}
if (data->latched) {
o << "Latched: (" << data->currentDesc << ") ";
o << "Target: (" << data->targetDesc << ")";
} else {
o << "(" << data->currentDesc << ")";
}
glfwSetWindowTitle(data->window, o.str().c_str());
}
void OnKeyPress(GLFWwindow* window, int key, int, int action, int) {
if (action != GLFW_PRESS) {
return;
}
ASSERT(windows.count(window) == 1);
WindowData* data = windows[window].get();
switch (key) {
case GLFW_KEY_W:
AddWindow();
break;
case GLFW_KEY_L:
data->latched = !data->latched;
UpdateTitle(data);
break;
case GLFW_KEY_R:
data->renderTriangle = !data->renderTriangle;
UpdateTitle(data);
break;
case GLFW_KEY_D:
data->divisor *= 2;
if (data->divisor > 32) {
data->divisor = 1;
}
break;
case GLFW_KEY_P:
switch (data->targetDesc.presentMode) {
case wgpu::PresentMode::Immediate:
data->targetDesc.presentMode = wgpu::PresentMode::Fifo;
break;
case wgpu::PresentMode::Fifo:
data->targetDesc.presentMode = wgpu::PresentMode::Mailbox;
break;
case wgpu::PresentMode::Mailbox:
data->targetDesc.presentMode = wgpu::PresentMode::Immediate;
break;
}
break;
default:
break;
}
}
int main(int argc, const char* argv[]) {
// Setup GLFW
glfwSetErrorCallback([](int code, const char* message) {
dawn::ErrorLog() << "GLFW error " << code << " " << message;
});
if (!glfwInit()) {
return 1;
}
// Choose an adapter we like.
// TODO: allow switching the window between devices.
DawnProcTable procs = dawn_native::GetProcs();
dawnProcSetProcs(&procs);
instance = std::make_unique<dawn_native::Instance>();
instance->DiscoverDefaultAdapters();
std::vector<dawn_native::Adapter> adapters = instance->GetAdapters();
dawn_native::Adapter chosenAdapter;
for (dawn_native::Adapter& adapter : adapters) {
wgpu::AdapterProperties properties;
adapter.GetProperties(&properties);
if (properties.backendType != wgpu::BackendType::Null) {
chosenAdapter = adapter;
break;
}
}
ASSERT(chosenAdapter);
// Setup the device on that adapter.
device = wgpu::Device::Acquire(chosenAdapter.CreateDevice());
device.SetUncapturedErrorCallback(
[](WGPUErrorType errorType, const char* message, void*) {
const char* errorTypeName = "";
switch (errorType) {
case WGPUErrorType_Validation:
errorTypeName = "Validation";
break;
case WGPUErrorType_OutOfMemory:
errorTypeName = "Out of memory";
break;
case WGPUErrorType_Unknown:
errorTypeName = "Unknown";
break;
case WGPUErrorType_DeviceLost:
errorTypeName = "Device lost";
break;
default:
UNREACHABLE();
return;
}
dawn::ErrorLog() << errorTypeName << " error: " << message;
},
nullptr);
queue = device.CreateQueue();
// The hacky pipeline to render a triangle.
utils::ComboRenderPipelineDescriptor pipelineDesc(device);
pipelineDesc.vertexStage.module =
utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, R"(
#version 450
const vec2 pos[3] = vec2[3](vec2(0.0f, 0.5f), vec2(-0.5f, -0.5f), vec2(0.5f, -0.5f));
void main() {
gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
})");
pipelineDesc.cFragmentStage.module =
utils::CreateShaderModule(device, utils::SingleShaderStage::Fragment, R"(
#version 450
layout(location = 0) out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
})");
pipelineDesc.colorStateCount = 1;
// BGRA shouldn't be hardcoded. Consider having a map[format -> pipeline].
pipelineDesc.cColorStates[0].format = wgpu::TextureFormat::BGRA8Unorm;
trianglePipeline = device.CreateRenderPipeline(&pipelineDesc);
// Craete the first window, since the example exits when there are no windows.
AddWindow();
while (windows.size() != 0) {
glfwPollEvents();
for (auto it = windows.begin(); it != windows.end();) {
GLFWwindow* window = it->first;
if (glfwWindowShouldClose(window)) {
glfwDestroyWindow(window);
it = windows.erase(it);
} else {
it++;
}
}
for (auto& it : windows) {
WindowData* data = it.second.get();
SyncFromWindow(data);
if (!IsSameDescriptor(data->currentDesc, data->targetDesc) && !data->latched) {
data->swapchain = device.CreateSwapChain(data->surface, &data->targetDesc);
data->currentDesc = data->targetDesc;
}
UpdateTitle(data);
DoRender(data);
}
}
}

View File

@ -268,6 +268,7 @@ namespace dawn_native {
mHeight(descriptor->height),
mFormat(descriptor->format),
mUsage(descriptor->usage),
mPresentMode(descriptor->presentMode),
mSurface(surface) {
}
@ -359,7 +360,11 @@ namespace dawn_native {
return mUsage;
}
Surface* NewSwapChainBase::GetSurface() {
wgpu::PresentMode NewSwapChainBase::GetPresentMode() const {
return mPresentMode;
}
Surface* NewSwapChainBase::GetSurface() const {
return mSurface;
}

View File

@ -122,7 +122,8 @@ namespace dawn_native {
uint32_t GetHeight() const;
wgpu::TextureFormat GetFormat() const;
wgpu::TextureUsage GetUsage() const;
Surface* GetSurface();
wgpu::PresentMode GetPresentMode() const;
Surface* GetSurface() const;
bool IsAttached() const;
wgpu::BackendType GetBackendType() const;
@ -132,6 +133,7 @@ namespace dawn_native {
uint32_t mHeight;
wgpu::TextureFormat mFormat;
wgpu::TextureUsage mUsage;
wgpu::PresentMode mPresentMode;
// This is a weak reference to the surface. If the surface is destroyed it will call
// DetachFromSurface and mSurface will be updated to nullptr.

View File

@ -131,13 +131,13 @@ namespace dawn_native { namespace metal {
}
ResultOrError<SwapChainBase*> Device::CreateSwapChainImpl(
const SwapChainDescriptor* descriptor) {
return new SwapChain(this, descriptor);
return new OldSwapChain(this, descriptor);
}
ResultOrError<NewSwapChainBase*> Device::CreateSwapChainImpl(
Surface* surface,
NewSwapChainBase* previousSwapChain,
const SwapChainDescriptor* descriptor) {
return DAWN_VALIDATION_ERROR("New swapchains not implemented.");
return new SwapChain(this, surface, previousSwapChain, descriptor);
}
ResultOrError<TextureBase*> Device::CreateTextureImpl(const TextureDescriptor* descriptor) {
return new Texture(this, descriptor);

View File

@ -17,20 +17,43 @@
#include "dawn_native/SwapChain.h"
@class CAMetalLayer;
@protocol CAMetalDrawable;
namespace dawn_native { namespace metal {
class Device;
class Texture;
class SwapChain : public OldSwapChainBase {
class OldSwapChain : public OldSwapChainBase {
public:
SwapChain(Device* device, const SwapChainDescriptor* descriptor);
~SwapChain();
OldSwapChain(Device* device, const SwapChainDescriptor* descriptor);
~OldSwapChain();
protected:
TextureBase* GetNextTextureImpl(const TextureDescriptor* descriptor) override;
MaybeError OnBeforePresent(TextureBase* texture) override;
};
class SwapChain : public NewSwapChainBase {
public:
SwapChain(Device* device,
Surface* surface,
NewSwapChainBase* previousSwapChain,
const SwapChainDescriptor* descriptor);
~SwapChain() override;
private:
CAMetalLayer* mLayer = nullptr;
id<CAMetalDrawable> mCurrentDrawable = nil;
Ref<Texture> mTexture;
MaybeError PresentImpl() override;
ResultOrError<TextureViewBase*> GetCurrentTextureViewImpl() override;
void DetachFromSurfaceImpl() override;
};
}} // namespace dawn_native::metal
#endif // DAWNNATIVE_METAL_SWAPCHAINMTL_H_

View File

@ -14,14 +14,19 @@
#include "dawn_native/metal/SwapChainMTL.h"
#include "dawn_native/Surface.h"
#include "dawn_native/metal/DeviceMTL.h"
#include "dawn_native/metal/TextureMTL.h"
#include <dawn/dawn_wsi.h>
#import <QuartzCore/CAMetalLayer.h>
namespace dawn_native { namespace metal {
SwapChain::SwapChain(Device* device, const SwapChainDescriptor* descriptor)
// OldSwapChain
OldSwapChain::OldSwapChain(Device* device, const SwapChainDescriptor* descriptor)
: OldSwapChainBase(device, descriptor) {
const auto& im = GetImplementation();
DawnWSIContextMetal wsiContext = {};
@ -30,10 +35,10 @@ namespace dawn_native { namespace metal {
im.Init(im.userData, &wsiContext);
}
SwapChain::~SwapChain() {
OldSwapChain::~OldSwapChain() {
}
TextureBase* SwapChain::GetNextTextureImpl(const TextureDescriptor* descriptor) {
TextureBase* OldSwapChain::GetNextTextureImpl(const TextureDescriptor* descriptor) {
const auto& im = GetImplementation();
DawnSwapChainNextTexture next = {};
DawnSwapChainError error = im.GetNextTexture(im.userData, &next);
@ -46,8 +51,86 @@ namespace dawn_native { namespace metal {
return new Texture(ToBackend(GetDevice()), descriptor, nativeTexture);
}
MaybeError SwapChain::OnBeforePresent(TextureBase*) {
MaybeError OldSwapChain::OnBeforePresent(TextureBase*) {
return {};
}
// SwapChain
SwapChain::SwapChain(Device* device,
Surface* surface,
NewSwapChainBase* previousSwapChain,
const SwapChainDescriptor* descriptor)
: NewSwapChainBase(device, surface, descriptor) {
ASSERT(surface->GetType() == Surface::Type::MetalLayer);
if (previousSwapChain != nullptr) {
// TODO(cwallez@chromium.org): figure out what should happen when surfaces are used by
// multiple backends one after the other. It probably needs to block until the backend
// and GPU are completely finished with the previous swapchain.
ASSERT(previousSwapChain->GetBackendType() == wgpu::BackendType::Metal);
previousSwapChain->DetachFromSurface();
}
mLayer = static_cast<CAMetalLayer*>(surface->GetMetalLayer());
ASSERT(mLayer != nullptr);
CGSize size = {};
size.width = GetWidth();
size.height = GetHeight();
[mLayer setDrawableSize:size];
[mLayer setFramebufferOnly:(GetUsage() == wgpu::TextureUsage::OutputAttachment)];
[mLayer setDevice:ToBackend(GetDevice())->GetMTLDevice()];
[mLayer setPixelFormat:MetalPixelFormat(GetFormat())];
if (@available(macos 10.13, ios 11.0, *)) {
[mLayer setDisplaySyncEnabled:(GetPresentMode() != wgpu::PresentMode::Immediate)];
}
// There is no way to control Fifo vs. Mailbox in Metal.
}
SwapChain::~SwapChain() {
DetachFromSurface();
}
MaybeError SwapChain::PresentImpl() {
ASSERT(mCurrentDrawable != nil);
[mCurrentDrawable present];
mTexture->Destroy();
mTexture = nullptr;
[mCurrentDrawable release];
mCurrentDrawable = nil;
return {};
}
ResultOrError<TextureViewBase*> SwapChain::GetCurrentTextureViewImpl() {
ASSERT(mCurrentDrawable == nil);
mCurrentDrawable = [mLayer nextDrawable];
[mCurrentDrawable retain];
TextureDescriptor textureDesc = GetSwapChainBaseTextureDescriptor(this);
// mTexture will add a reference to mCurrentDrawable.texture to keep it alive.
mTexture =
AcquireRef(new Texture(ToBackend(GetDevice()), &textureDesc, mCurrentDrawable.texture));
return mTexture->CreateView(nullptr);
}
void SwapChain::DetachFromSurfaceImpl() {
ASSERT((mTexture.Get() == nullptr) == (mCurrentDrawable == nil));
if (mTexture.Get() != nullptr) {
mTexture->Destroy();
mTexture = nullptr;
[mCurrentDrawable release];
mCurrentDrawable = nil;
}
}
}} // namespace dawn_native::metal

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,8 +56,10 @@ class SwapChainValidationTests : public ValidationTest {
void TearDown() override {
// Destroy the surface before the window as required by webgpu-native.
surface = wgpu::Surface();
if (window != nullptr) {
glfwDestroyWindow(window);
}
}
protected:
GLFWwindow* window = nullptr;
@ -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());