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:
parent
527045fe55
commit
11652ff8f8
7
BUILD.gn
7
BUILD.gn
|
@ -997,6 +997,7 @@ source_set("dawn_end2end_tests_sources") {
|
||||||
|
|
||||||
if (supports_glfw_for_windowing) {
|
if (supports_glfw_for_windowing) {
|
||||||
sources += [
|
sources += [
|
||||||
|
"src/tests/end2end/SwapChainTests.cpp",
|
||||||
"src/tests/end2end/SwapChainValidationTests.cpp",
|
"src/tests/end2end/SwapChainValidationTests.cpp",
|
||||||
"src/tests/end2end/WindowSurfaceTests.cpp",
|
"src/tests/end2end/WindowSurfaceTests.cpp",
|
||||||
]
|
]
|
||||||
|
@ -1291,6 +1292,12 @@ if (dawn_standalone) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dawn_sample("ManualSwapChainTest") {
|
||||||
|
sources = [
|
||||||
|
"examples/ManualSwapChainTest.cpp",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
group("dawn_samples") {
|
group("dawn_samples") {
|
||||||
deps = [
|
deps = [
|
||||||
":Animometer",
|
":Animometer",
|
||||||
|
|
|
@ -48,7 +48,7 @@ void init() {
|
||||||
|
|
||||||
const char* fs =
|
const char* fs =
|
||||||
"#version 450\n"
|
"#version 450\n"
|
||||||
"layout(location = 0) out vec4 fragColor;"
|
"layout(location = 0) out vec4 fragColor;\n"
|
||||||
"void main() {\n"
|
"void main() {\n"
|
||||||
" fragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
|
" fragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
|
||||||
"}\n";
|
"}\n";
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,4 +24,4 @@ namespace dawn_native {
|
||||||
ASSERT(errorData->GetType() == wgpu::ErrorType::DeviceLost);
|
ASSERT(errorData->GetType() == wgpu::ErrorType::DeviceLost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace dawn_native
|
} // namespace dawn_native
|
||||||
|
|
|
@ -268,6 +268,7 @@ namespace dawn_native {
|
||||||
mHeight(descriptor->height),
|
mHeight(descriptor->height),
|
||||||
mFormat(descriptor->format),
|
mFormat(descriptor->format),
|
||||||
mUsage(descriptor->usage),
|
mUsage(descriptor->usage),
|
||||||
|
mPresentMode(descriptor->presentMode),
|
||||||
mSurface(surface) {
|
mSurface(surface) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,7 +360,11 @@ namespace dawn_native {
|
||||||
return mUsage;
|
return mUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
Surface* NewSwapChainBase::GetSurface() {
|
wgpu::PresentMode NewSwapChainBase::GetPresentMode() const {
|
||||||
|
return mPresentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface* NewSwapChainBase::GetSurface() const {
|
||||||
return mSurface;
|
return mSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,8 @@ namespace dawn_native {
|
||||||
uint32_t GetHeight() const;
|
uint32_t GetHeight() const;
|
||||||
wgpu::TextureFormat GetFormat() const;
|
wgpu::TextureFormat GetFormat() const;
|
||||||
wgpu::TextureUsage GetUsage() const;
|
wgpu::TextureUsage GetUsage() const;
|
||||||
Surface* GetSurface();
|
wgpu::PresentMode GetPresentMode() const;
|
||||||
|
Surface* GetSurface() const;
|
||||||
bool IsAttached() const;
|
bool IsAttached() const;
|
||||||
wgpu::BackendType GetBackendType() const;
|
wgpu::BackendType GetBackendType() const;
|
||||||
|
|
||||||
|
@ -132,6 +133,7 @@ namespace dawn_native {
|
||||||
uint32_t mHeight;
|
uint32_t mHeight;
|
||||||
wgpu::TextureFormat mFormat;
|
wgpu::TextureFormat mFormat;
|
||||||
wgpu::TextureUsage mUsage;
|
wgpu::TextureUsage mUsage;
|
||||||
|
wgpu::PresentMode mPresentMode;
|
||||||
|
|
||||||
// This is a weak reference to the surface. If the surface is destroyed it will call
|
// This is a weak reference to the surface. If the surface is destroyed it will call
|
||||||
// DetachFromSurface and mSurface will be updated to nullptr.
|
// DetachFromSurface and mSurface will be updated to nullptr.
|
||||||
|
|
|
@ -131,13 +131,13 @@ namespace dawn_native { namespace metal {
|
||||||
}
|
}
|
||||||
ResultOrError<SwapChainBase*> Device::CreateSwapChainImpl(
|
ResultOrError<SwapChainBase*> Device::CreateSwapChainImpl(
|
||||||
const SwapChainDescriptor* descriptor) {
|
const SwapChainDescriptor* descriptor) {
|
||||||
return new SwapChain(this, descriptor);
|
return new OldSwapChain(this, descriptor);
|
||||||
}
|
}
|
||||||
ResultOrError<NewSwapChainBase*> Device::CreateSwapChainImpl(
|
ResultOrError<NewSwapChainBase*> Device::CreateSwapChainImpl(
|
||||||
Surface* surface,
|
Surface* surface,
|
||||||
NewSwapChainBase* previousSwapChain,
|
NewSwapChainBase* previousSwapChain,
|
||||||
const SwapChainDescriptor* descriptor) {
|
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) {
|
ResultOrError<TextureBase*> Device::CreateTextureImpl(const TextureDescriptor* descriptor) {
|
||||||
return new Texture(this, descriptor);
|
return new Texture(this, descriptor);
|
||||||
|
|
|
@ -17,20 +17,43 @@
|
||||||
|
|
||||||
#include "dawn_native/SwapChain.h"
|
#include "dawn_native/SwapChain.h"
|
||||||
|
|
||||||
|
@class CAMetalLayer;
|
||||||
|
@protocol CAMetalDrawable;
|
||||||
|
|
||||||
namespace dawn_native { namespace metal {
|
namespace dawn_native { namespace metal {
|
||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
|
class Texture;
|
||||||
|
|
||||||
class SwapChain : public OldSwapChainBase {
|
class OldSwapChain : public OldSwapChainBase {
|
||||||
public:
|
public:
|
||||||
SwapChain(Device* device, const SwapChainDescriptor* descriptor);
|
OldSwapChain(Device* device, const SwapChainDescriptor* descriptor);
|
||||||
~SwapChain();
|
~OldSwapChain();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
TextureBase* GetNextTextureImpl(const TextureDescriptor* descriptor) override;
|
TextureBase* GetNextTextureImpl(const TextureDescriptor* descriptor) override;
|
||||||
MaybeError OnBeforePresent(TextureBase* texture) 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
|
}} // namespace dawn_native::metal
|
||||||
|
|
||||||
#endif // DAWNNATIVE_METAL_SWAPCHAINMTL_H_
|
#endif // DAWNNATIVE_METAL_SWAPCHAINMTL_H_
|
||||||
|
|
|
@ -14,14 +14,19 @@
|
||||||
|
|
||||||
#include "dawn_native/metal/SwapChainMTL.h"
|
#include "dawn_native/metal/SwapChainMTL.h"
|
||||||
|
|
||||||
|
#include "dawn_native/Surface.h"
|
||||||
#include "dawn_native/metal/DeviceMTL.h"
|
#include "dawn_native/metal/DeviceMTL.h"
|
||||||
#include "dawn_native/metal/TextureMTL.h"
|
#include "dawn_native/metal/TextureMTL.h"
|
||||||
|
|
||||||
#include <dawn/dawn_wsi.h>
|
#include <dawn/dawn_wsi.h>
|
||||||
|
|
||||||
|
#import <QuartzCore/CAMetalLayer.h>
|
||||||
|
|
||||||
namespace dawn_native { namespace metal {
|
namespace dawn_native { namespace metal {
|
||||||
|
|
||||||
SwapChain::SwapChain(Device* device, const SwapChainDescriptor* descriptor)
|
// OldSwapChain
|
||||||
|
|
||||||
|
OldSwapChain::OldSwapChain(Device* device, const SwapChainDescriptor* descriptor)
|
||||||
: OldSwapChainBase(device, descriptor) {
|
: OldSwapChainBase(device, descriptor) {
|
||||||
const auto& im = GetImplementation();
|
const auto& im = GetImplementation();
|
||||||
DawnWSIContextMetal wsiContext = {};
|
DawnWSIContextMetal wsiContext = {};
|
||||||
|
@ -30,10 +35,10 @@ namespace dawn_native { namespace metal {
|
||||||
im.Init(im.userData, &wsiContext);
|
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();
|
const auto& im = GetImplementation();
|
||||||
DawnSwapChainNextTexture next = {};
|
DawnSwapChainNextTexture next = {};
|
||||||
DawnSwapChainError error = im.GetNextTexture(im.userData, &next);
|
DawnSwapChainError error = im.GetNextTexture(im.userData, &next);
|
||||||
|
@ -46,8 +51,86 @@ namespace dawn_native { namespace metal {
|
||||||
return new Texture(ToBackend(GetDevice()), descriptor, nativeTexture);
|
return new Texture(ToBackend(GetDevice()), descriptor, nativeTexture);
|
||||||
}
|
}
|
||||||
|
|
||||||
MaybeError SwapChain::OnBeforePresent(TextureBase*) {
|
MaybeError OldSwapChain::OnBeforePresent(TextureBase*) {
|
||||||
return {};
|
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
|
}} // namespace dawn_native::metal
|
||||||
|
|
|
@ -110,6 +110,12 @@ DawnTestParam MetalBackend(std::initializer_list<const char*> forceEnabledWorkar
|
||||||
forceDisabledWorkarounds);
|
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,
|
DawnTestParam OpenGLBackend(std::initializer_list<const char*> forceEnabledWorkarounds,
|
||||||
std::initializer_list<const char*> forceDisabledWorkarounds) {
|
std::initializer_list<const char*> forceDisabledWorkarounds) {
|
||||||
return DawnTestParam(wgpu::BackendType::OpenGL, forceEnabledWorkarounds,
|
return DawnTestParam(wgpu::BackendType::OpenGL, forceEnabledWorkarounds,
|
||||||
|
@ -446,6 +452,10 @@ bool DawnTestBase::IsMetal() const {
|
||||||
return mParam.backendType == wgpu::BackendType::Metal;
|
return mParam.backendType == wgpu::BackendType::Metal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DawnTestBase::IsNull() const {
|
||||||
|
return mParam.backendType == wgpu::BackendType::Null;
|
||||||
|
}
|
||||||
|
|
||||||
bool DawnTestBase::IsOpenGL() const {
|
bool DawnTestBase::IsOpenGL() const {
|
||||||
return mParam.backendType == wgpu::BackendType::OpenGL;
|
return mParam.backendType == wgpu::BackendType::OpenGL;
|
||||||
}
|
}
|
||||||
|
@ -526,6 +536,14 @@ uint32_t DawnTestBase::GetVendorIdFilter() const {
|
||||||
return gTestEnv->GetVendorIdFilter();
|
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() {
|
std::vector<const char*> DawnTestBase::GetRequiredExtensions() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -958,6 +976,9 @@ namespace detail {
|
||||||
#if defined(DAWN_ENABLE_BACKEND_METAL)
|
#if defined(DAWN_ENABLE_BACKEND_METAL)
|
||||||
case wgpu::BackendType::Metal:
|
case wgpu::BackendType::Metal:
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(DAWN_ENABLE_BACKEND_NULL)
|
||||||
|
case wgpu::BackendType::Null:
|
||||||
|
#endif
|
||||||
#if defined(DAWN_ENABLE_BACKEND_OPENGL)
|
#if defined(DAWN_ENABLE_BACKEND_OPENGL)
|
||||||
case wgpu::BackendType::OpenGL:
|
case wgpu::BackendType::OpenGL:
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -104,6 +104,9 @@ DawnTestParam D3D12Backend(std::initializer_list<const char*> forceEnabledWorkar
|
||||||
DawnTestParam MetalBackend(std::initializer_list<const char*> forceEnabledWorkarounds = {},
|
DawnTestParam MetalBackend(std::initializer_list<const char*> forceEnabledWorkarounds = {},
|
||||||
std::initializer_list<const char*> forceDisabledWorkarounds = {});
|
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 = {},
|
DawnTestParam OpenGLBackend(std::initializer_list<const char*> forceEnabledWorkarounds = {},
|
||||||
std::initializer_list<const char*> forceDisabledWorkarounds = {});
|
std::initializer_list<const char*> forceDisabledWorkarounds = {});
|
||||||
|
|
||||||
|
@ -177,6 +180,7 @@ class DawnTestBase {
|
||||||
|
|
||||||
bool IsD3D12() const;
|
bool IsD3D12() const;
|
||||||
bool IsMetal() const;
|
bool IsMetal() const;
|
||||||
|
bool IsNull() const;
|
||||||
bool IsOpenGL() const;
|
bool IsOpenGL() const;
|
||||||
bool IsVulkan() const;
|
bool IsVulkan() const;
|
||||||
|
|
||||||
|
@ -202,6 +206,9 @@ class DawnTestBase {
|
||||||
bool HasVendorIdFilter() const;
|
bool HasVendorIdFilter() const;
|
||||||
uint32_t GetVendorIdFilter() const;
|
uint32_t GetVendorIdFilter() const;
|
||||||
|
|
||||||
|
wgpu::Instance GetInstance() const;
|
||||||
|
dawn_native::Adapter GetAdapter() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wgpu::Device device;
|
wgpu::Device device;
|
||||||
wgpu::Queue queue;
|
wgpu::Queue queue;
|
||||||
|
|
|
@ -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());
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
#include "tests/unittests/validation/ValidationTest.h"
|
#include "tests/DawnTest.h"
|
||||||
|
|
||||||
#include "common/Constants.h"
|
#include "common/Constants.h"
|
||||||
#include "common/Log.h"
|
#include "common/Log.h"
|
||||||
|
@ -22,21 +22,25 @@
|
||||||
|
|
||||||
#include "GLFW/glfw3.h"
|
#include "GLFW/glfw3.h"
|
||||||
|
|
||||||
class SwapChainValidationTests : public ValidationTest {
|
class SwapChainValidationTests : public DawnTest {
|
||||||
public:
|
public:
|
||||||
void SetUp() override {
|
void TestSetUp() override {
|
||||||
|
DAWN_SKIP_TEST_IF(UsesWire());
|
||||||
|
DAWN_SKIP_TEST_IF(IsDawnValidationSkipped());
|
||||||
|
|
||||||
glfwSetErrorCallback([](int code, const char* message) {
|
glfwSetErrorCallback([](int code, const char* message) {
|
||||||
dawn::ErrorLog() << "GLFW error " << code << " " << message;
|
dawn::ErrorLog() << "GLFW error " << code << " " << message;
|
||||||
});
|
});
|
||||||
glfwInit();
|
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
|
// SetupGLFWWindowHintsForBackend. Set GLFW_NO_API anyway to avoid GLFW bringing up a GL
|
||||||
// context that we won't use.
|
// context that we won't use.
|
||||||
|
ASSERT_TRUE(!IsOpenGL());
|
||||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
|
||||||
window = glfwCreateWindow(400, 400, "SwapChainValidationTests window", nullptr, nullptr);
|
window = glfwCreateWindow(400, 400, "SwapChainValidationTests window", nullptr, nullptr);
|
||||||
|
|
||||||
surface = utils::CreateSurfaceForWindow(instance->Get(), window);
|
surface = utils::CreateSurfaceForWindow(GetInstance(), window);
|
||||||
ASSERT_NE(surface, nullptr);
|
ASSERT_NE(surface, nullptr);
|
||||||
|
|
||||||
goodDescriptor.width = 1;
|
goodDescriptor.width = 1;
|
||||||
|
@ -52,7 +56,9 @@ class SwapChainValidationTests : public ValidationTest {
|
||||||
void TearDown() override {
|
void TearDown() override {
|
||||||
// Destroy the surface before the window as required by webgpu-native.
|
// Destroy the surface before the window as required by webgpu-native.
|
||||||
surface = wgpu::Surface();
|
surface = wgpu::Surface();
|
||||||
glfwDestroyWindow(window);
|
if (window != nullptr) {
|
||||||
|
glfwDestroyWindow(window);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -86,14 +92,14 @@ class SwapChainValidationTests : public ValidationTest {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Control case for a successful swapchain creation and presenting.
|
// 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::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
wgpu::TextureView view = swapchain.GetCurrentTextureView();
|
wgpu::TextureView view = swapchain.GetCurrentTextureView();
|
||||||
swapchain.Present();
|
swapchain.Present();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the creation size must be a valid 2D texture size.
|
// 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.
|
// A width of 0 is invalid.
|
||||||
{
|
{
|
||||||
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
||||||
|
@ -129,28 +135,28 @@ TEST_F(SwapChainValidationTests, InvalidCreationSize) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the creation usage must be OutputAttachment
|
// Checks that the creation usage must be OutputAttachment
|
||||||
TEST_F(SwapChainValidationTests, InvalidCreationUsage) {
|
TEST_P(SwapChainValidationTests, InvalidCreationUsage) {
|
||||||
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
||||||
desc.usage = wgpu::TextureUsage::Sampled;
|
desc.usage = wgpu::TextureUsage::Sampled;
|
||||||
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
|
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the creation format must (currently) be BGRA8Unorm
|
// Checks that the creation format must (currently) be BGRA8Unorm
|
||||||
TEST_F(SwapChainValidationTests, InvalidCreationFormat) {
|
TEST_P(SwapChainValidationTests, InvalidCreationFormat) {
|
||||||
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
||||||
desc.format = wgpu::TextureFormat::RGBA8Unorm;
|
desc.format = wgpu::TextureFormat::RGBA8Unorm;
|
||||||
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
|
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the implementation must be zero.
|
// Checks that the implementation must be zero.
|
||||||
TEST_F(SwapChainValidationTests, InvalidWithImplementation) {
|
TEST_P(SwapChainValidationTests, InvalidWithImplementation) {
|
||||||
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
wgpu::SwapChainDescriptor desc = goodDescriptor;
|
||||||
desc.implementation = 1;
|
desc.implementation = 1;
|
||||||
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
|
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check swapchain operations with an error swapchain are errors
|
// Check swapchain operations with an error swapchain are errors
|
||||||
TEST_F(SwapChainValidationTests, OperationsOnErrorSwapChain) {
|
TEST_P(SwapChainValidationTests, OperationsOnErrorSwapChain) {
|
||||||
wgpu::SwapChain swapchain;
|
wgpu::SwapChain swapchain;
|
||||||
ASSERT_DEVICE_ERROR(swapchain = device.CreateSwapChain(surface, &badDescriptor));
|
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.
|
// 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);
|
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
|
|
||||||
// Check it is invalid if we never called GetCurrentTextureView
|
// 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.
|
// 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::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
wgpu::TextureView view = swapchain.GetCurrentTextureView();
|
wgpu::TextureView view = swapchain.GetCurrentTextureView();
|
||||||
swapchain = nullptr;
|
swapchain = nullptr;
|
||||||
|
@ -184,7 +190,7 @@ TEST_F(SwapChainValidationTests, ViewDestroyedAfterSwapChainDestruction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the current view is the destroyed state after present.
|
// 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::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
wgpu::TextureView view = swapchain.GetCurrentTextureView();
|
wgpu::TextureView view = swapchain.GetCurrentTextureView();
|
||||||
swapchain.Present();
|
swapchain.Present();
|
||||||
|
@ -193,7 +199,7 @@ TEST_F(SwapChainValidationTests, ViewDestroyedAfterPresent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that returned view is of the current format / usage / dimension / size / sample count
|
// 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);
|
utils::ComboRenderPipelineDescriptor pipelineDesc(device);
|
||||||
pipelineDesc.vertexStage.module =
|
pipelineDesc.vertexStage.module =
|
||||||
utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, R"(
|
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.
|
// 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);
|
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &badDescriptor));
|
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.
|
// 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);
|
wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
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
|
// 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.
|
// 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);
|
wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
surface = nullptr;
|
surface = nullptr;
|
||||||
ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTextureView());
|
ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTextureView());
|
||||||
}
|
}
|
||||||
TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetView) {
|
TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetView) {
|
||||||
wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor);
|
wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
wgpu::TextureView view = replacedSwapChain.GetCurrentTextureView();
|
wgpu::TextureView view = replacedSwapChain.GetCurrentTextureView();
|
||||||
surface = nullptr;
|
surface = nullptr;
|
||||||
|
@ -293,12 +299,12 @@ TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_After
|
||||||
|
|
||||||
// Test that after Device is Lost, all swap chain operations fail
|
// Test that after Device is Lost, all swap chain operations fail
|
||||||
static void ToMockDeviceLostCallback(const char* message, void* userdata) {
|
static void ToMockDeviceLostCallback(const char* message, void* userdata) {
|
||||||
ValidationTest* self = static_cast<ValidationTest*>(userdata);
|
DawnTest* self = static_cast<DawnTest*>(userdata);
|
||||||
self->StartExpectDeviceError();
|
self->StartExpectDeviceError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that new swap chain present fails after device is lost
|
// Test that new swap chain present fails after device is lost
|
||||||
TEST_F(SwapChainValidationTests, NewSwapChainPresentFailsAfterDeviceLost) {
|
TEST_P(SwapChainValidationTests, NewSwapChainPresentFailsAfterDeviceLost) {
|
||||||
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
|
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
|
||||||
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
wgpu::TextureView view = swapchain.GetCurrentTextureView();
|
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 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);
|
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
|
||||||
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor);
|
||||||
|
|
||||||
|
@ -316,10 +322,12 @@ TEST_F(SwapChainValidationTests, NewSwapChainGetCurrentTextureViewFailsAfterDevL
|
||||||
ASSERT_DEVICE_ERROR(swapchain.GetCurrentTextureView());
|
ASSERT_DEVICE_ERROR(swapchain.GetCurrentTextureView());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that creation of a new swapchain fails
|
// Test that creation of a new swapchain fails after device is lost
|
||||||
TEST_F(SwapChainValidationTests, CreateNewSwapChainFailsAfterDevLost) {
|
TEST_P(SwapChainValidationTests, CreateNewSwapChainFailsAfterDevLost) {
|
||||||
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
|
device.SetDeviceLostCallback(ToMockDeviceLostCallback, this);
|
||||||
device.LoseForTesting();
|
device.LoseForTesting();
|
||||||
|
|
||||||
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &goodDescriptor));
|
ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &goodDescriptor));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DAWN_INSTANTIATE_TEST(SwapChainValidationTests, MetalBackend(), NullBackend());
|
||||||
|
|
Loading…
Reference in New Issue