diff --git a/BUILD.gn b/BUILD.gn index 6a073a5b36..7bb5b88cef 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -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", diff --git a/examples/CHelloTriangle.cpp b/examples/CHelloTriangle.cpp index 73c3cdf1ca..ffd269c5b1 100644 --- a/examples/CHelloTriangle.cpp +++ b/examples/CHelloTriangle.cpp @@ -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"; diff --git a/examples/ManualSwapChainTest.cpp b/examples/ManualSwapChainTest.cpp new file mode 100644 index 0000000000..bd1a6c953a --- /dev/null +++ b/examples/ManualSwapChainTest.cpp @@ -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 +#include +#include +#include "GLFW/glfw3.h" + +#include +#include + +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> windows; +static uint64_t windowSerial = 0; + +static std::unique_ptr 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 data = std::make_unique(); + 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(); + instance->DiscoverDefaultAdapters(); + + std::vector 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); + } + } +} diff --git a/src/dawn_native/Error.cpp b/src/dawn_native/Error.cpp index d1ca233ae7..79c2024276 100644 --- a/src/dawn_native/Error.cpp +++ b/src/dawn_native/Error.cpp @@ -24,4 +24,4 @@ namespace dawn_native { ASSERT(errorData->GetType() == wgpu::ErrorType::DeviceLost); } } -} // namespace dawn_native \ No newline at end of file +} // namespace dawn_native diff --git a/src/dawn_native/SwapChain.cpp b/src/dawn_native/SwapChain.cpp index 547bd62a29..86a9c89d2f 100644 --- a/src/dawn_native/SwapChain.cpp +++ b/src/dawn_native/SwapChain.cpp @@ -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; } diff --git a/src/dawn_native/SwapChain.h b/src/dawn_native/SwapChain.h index ff16e02c63..d1d0b7f255 100644 --- a/src/dawn_native/SwapChain.h +++ b/src/dawn_native/SwapChain.h @@ -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. diff --git a/src/dawn_native/metal/DeviceMTL.mm b/src/dawn_native/metal/DeviceMTL.mm index c3d2fafb55..12ddac7605 100644 --- a/src/dawn_native/metal/DeviceMTL.mm +++ b/src/dawn_native/metal/DeviceMTL.mm @@ -131,13 +131,13 @@ namespace dawn_native { namespace metal { } ResultOrError Device::CreateSwapChainImpl( const SwapChainDescriptor* descriptor) { - return new SwapChain(this, descriptor); + return new OldSwapChain(this, descriptor); } ResultOrError 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 Device::CreateTextureImpl(const TextureDescriptor* descriptor) { return new Texture(this, descriptor); diff --git a/src/dawn_native/metal/SwapChainMTL.h b/src/dawn_native/metal/SwapChainMTL.h index 933f7dc633..a4cddc1f5a 100644 --- a/src/dawn_native/metal/SwapChainMTL.h +++ b/src/dawn_native/metal/SwapChainMTL.h @@ -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 mCurrentDrawable = nil; + Ref mTexture; + + MaybeError PresentImpl() override; + ResultOrError GetCurrentTextureViewImpl() override; + void DetachFromSurfaceImpl() override; + }; + }} // namespace dawn_native::metal #endif // DAWNNATIVE_METAL_SWAPCHAINMTL_H_ diff --git a/src/dawn_native/metal/SwapChainMTL.mm b/src/dawn_native/metal/SwapChainMTL.mm index d7be554a26..54d5af2142 100644 --- a/src/dawn_native/metal/SwapChainMTL.mm +++ b/src/dawn_native/metal/SwapChainMTL.mm @@ -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 +#import + 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(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 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 diff --git a/src/tests/DawnTest.cpp b/src/tests/DawnTest.cpp index 2837406f80..9a9879692f 100644 --- a/src/tests/DawnTest.cpp +++ b/src/tests/DawnTest.cpp @@ -110,6 +110,12 @@ DawnTestParam MetalBackend(std::initializer_list forceEnabledWorkar forceDisabledWorkarounds); } +DawnTestParam NullBackend(std::initializer_list forceEnabledWorkarounds, + std::initializer_list forceDisabledWorkarounds) { + return DawnTestParam(wgpu::BackendType::Null, forceEnabledWorkarounds, + forceDisabledWorkarounds); +} + DawnTestParam OpenGLBackend(std::initializer_list forceEnabledWorkarounds, std::initializer_list 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 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 diff --git a/src/tests/DawnTest.h b/src/tests/DawnTest.h index 4211d86e8e..13bf44fd28 100644 --- a/src/tests/DawnTest.h +++ b/src/tests/DawnTest.h @@ -104,6 +104,9 @@ DawnTestParam D3D12Backend(std::initializer_list forceEnabledWorkar DawnTestParam MetalBackend(std::initializer_list forceEnabledWorkarounds = {}, std::initializer_list forceDisabledWorkarounds = {}); +DawnTestParam NullBackend(std::initializer_list forceEnabledWorkarounds = {}, + std::initializer_list forceDisabledWorkarounds = {}); + DawnTestParam OpenGLBackend(std::initializer_list forceEnabledWorkarounds = {}, std::initializer_list 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; diff --git a/src/tests/end2end/SwapChainTests.cpp b/src/tests/end2end/SwapChainTests.cpp new file mode 100644 index 0000000000..7cf0d458b1 --- /dev/null +++ b/src/tests/end2end/SwapChainTests.cpp @@ -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()); diff --git a/src/tests/end2end/SwapChainValidationTests.cpp b/src/tests/end2end/SwapChainValidationTests.cpp index 8624deb4bb..444cd359c6 100644 --- a/src/tests/end2end/SwapChainValidationTests.cpp +++ b/src/tests/end2end/SwapChainValidationTests.cpp @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "tests/unittests/validation/ValidationTest.h" +#include "tests/DawnTest.h" #include "common/Constants.h" #include "common/Log.h" @@ -22,21 +22,25 @@ #include "GLFW/glfw3.h" -class SwapChainValidationTests : public ValidationTest { +class SwapChainValidationTests : public DawnTest { public: - void SetUp() override { + void TestSetUp() override { + DAWN_SKIP_TEST_IF(UsesWire()); + DAWN_SKIP_TEST_IF(IsDawnValidationSkipped()); + glfwSetErrorCallback([](int code, const char* message) { dawn::ErrorLog() << "GLFW error " << code << " " << message; }); glfwInit(); - // The SwapChainValidationTests tests don't create devices so we don't need to call + // The SwapChainValidationTests don't create devices so we don't need to call // SetupGLFWWindowHintsForBackend. Set GLFW_NO_API anyway to avoid GLFW bringing up a GL // context that we won't use. + ASSERT_TRUE(!IsOpenGL()); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); window = glfwCreateWindow(400, 400, "SwapChainValidationTests window", nullptr, nullptr); - surface = utils::CreateSurfaceForWindow(instance->Get(), window); + surface = utils::CreateSurfaceForWindow(GetInstance(), window); ASSERT_NE(surface, nullptr); goodDescriptor.width = 1; @@ -52,7 +56,9 @@ class SwapChainValidationTests : public ValidationTest { void TearDown() override { // Destroy the surface before the window as required by webgpu-native. surface = wgpu::Surface(); - glfwDestroyWindow(window); + if (window != nullptr) { + glfwDestroyWindow(window); + } } protected: @@ -86,14 +92,14 @@ class SwapChainValidationTests : public ValidationTest { }; // Control case for a successful swapchain creation and presenting. -TEST_F(SwapChainValidationTests, CreationSuccess) { +TEST_P(SwapChainValidationTests, CreationSuccess) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::TextureView view = swapchain.GetCurrentTextureView(); swapchain.Present(); } // Checks that the creation size must be a valid 2D texture size. -TEST_F(SwapChainValidationTests, InvalidCreationSize) { +TEST_P(SwapChainValidationTests, InvalidCreationSize) { // A width of 0 is invalid. { wgpu::SwapChainDescriptor desc = goodDescriptor; @@ -129,28 +135,28 @@ TEST_F(SwapChainValidationTests, InvalidCreationSize) { } // Checks that the creation usage must be OutputAttachment -TEST_F(SwapChainValidationTests, InvalidCreationUsage) { +TEST_P(SwapChainValidationTests, InvalidCreationUsage) { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.usage = wgpu::TextureUsage::Sampled; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // Checks that the creation format must (currently) be BGRA8Unorm -TEST_F(SwapChainValidationTests, InvalidCreationFormat) { +TEST_P(SwapChainValidationTests, InvalidCreationFormat) { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.format = wgpu::TextureFormat::RGBA8Unorm; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // Checks that the implementation must be zero. -TEST_F(SwapChainValidationTests, InvalidWithImplementation) { +TEST_P(SwapChainValidationTests, InvalidWithImplementation) { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.implementation = 1; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // Check swapchain operations with an error swapchain are errors -TEST_F(SwapChainValidationTests, OperationsOnErrorSwapChain) { +TEST_P(SwapChainValidationTests, OperationsOnErrorSwapChain) { wgpu::SwapChain swapchain; ASSERT_DEVICE_ERROR(swapchain = device.CreateSwapChain(surface, &badDescriptor)); @@ -162,7 +168,7 @@ TEST_F(SwapChainValidationTests, OperationsOnErrorSwapChain) { } // Check it is invalid to call present without getting a current view. -TEST_F(SwapChainValidationTests, PresentWithoutCurrentView) { +TEST_P(SwapChainValidationTests, PresentWithoutCurrentView) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); // Check it is invalid if we never called GetCurrentTextureView @@ -175,7 +181,7 @@ TEST_F(SwapChainValidationTests, PresentWithoutCurrentView) { } // Check that the current view is in the destroyed state after the swapchain is destroyed. -TEST_F(SwapChainValidationTests, ViewDestroyedAfterSwapChainDestruction) { +TEST_P(SwapChainValidationTests, ViewDestroyedAfterSwapChainDestruction) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::TextureView view = swapchain.GetCurrentTextureView(); swapchain = nullptr; @@ -184,7 +190,7 @@ TEST_F(SwapChainValidationTests, ViewDestroyedAfterSwapChainDestruction) { } // Check that the current view is the destroyed state after present. -TEST_F(SwapChainValidationTests, ViewDestroyedAfterPresent) { +TEST_P(SwapChainValidationTests, ViewDestroyedAfterPresent) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::TextureView view = swapchain.GetCurrentTextureView(); swapchain.Present(); @@ -193,7 +199,7 @@ TEST_F(SwapChainValidationTests, ViewDestroyedAfterPresent) { } // Check that returned view is of the current format / usage / dimension / size / sample count -TEST_F(SwapChainValidationTests, ReturnedViewCharacteristics) { +TEST_P(SwapChainValidationTests, ReturnedViewCharacteristics) { utils::ComboRenderPipelineDescriptor pipelineDesc(device); pipelineDesc.vertexStage.module = utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, R"( @@ -249,7 +255,7 @@ TEST_F(SwapChainValidationTests, ReturnedViewCharacteristics) { } // Check that failing to create a new swapchain doesn't replace the previous one. -TEST_F(SwapChainValidationTests, ErrorSwapChainDoesntReplacePreviousOne) { +TEST_P(SwapChainValidationTests, ErrorSwapChainDoesntReplacePreviousOne) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &badDescriptor)); @@ -258,7 +264,7 @@ TEST_F(SwapChainValidationTests, ErrorSwapChainDoesntReplacePreviousOne) { } // Check that after replacement, all swapchain operations are errors and the view is destroyed. -TEST_F(SwapChainValidationTests, ReplacedSwapChainIsInvalid) { +TEST_P(SwapChainValidationTests, ReplacedSwapChainIsInvalid) { { wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor); device.CreateSwapChain(surface, &goodDescriptor); @@ -277,12 +283,12 @@ TEST_F(SwapChainValidationTests, ReplacedSwapChainIsInvalid) { // Check that after surface destruction, all swapchain operations are errors and the view is // destroyed. The test is split in two to reset the wgpu::Surface in the middle. -TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_GetView) { +TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_GetView) { wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor); surface = nullptr; ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTextureView()); } -TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetView) { +TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetView) { wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::TextureView view = replacedSwapChain.GetCurrentTextureView(); surface = nullptr; @@ -293,12 +299,12 @@ TEST_F(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_After // Test that after Device is Lost, all swap chain operations fail static void ToMockDeviceLostCallback(const char* message, void* userdata) { - ValidationTest* self = static_cast(userdata); + DawnTest* self = static_cast(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());