// 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 "dawn/tests/DawnTest.h" #include "dawn/common/Constants.h" #include "dawn/common/Log.h" #include "dawn/utils/ComboRenderPipelineDescriptor.h" #include "dawn/utils/WGPUHelpers.h" #include "webgpu/webgpu_glfw.h" #include "GLFW/glfw3.h" namespace dawn { namespace { class SwapChainValidationTests : public DawnTest { public: void SetUp() override { DawnTest::SetUp(); DAWN_TEST_UNSUPPORTED_IF(UsesWire()); DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation")); glfwSetErrorCallback([](int code, const char* message) { ErrorLog() << "GLFW error " << code << " " << message; }); DAWN_TEST_UNSUPPORTED_IF(!glfwInit()); // Set GLFW_NO_API to avoid GLFW bringing up a GL context that we won't use. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); window = glfwCreateWindow(400, 400, "SwapChainValidationTests window", nullptr, nullptr); surface = wgpu::glfw::CreateSurfaceForWindow(GetInstance(), window); ASSERT_NE(surface, nullptr); goodDescriptor.width = 1; goodDescriptor.height = 1; goodDescriptor.usage = wgpu::TextureUsage::RenderAttachment; goodDescriptor.format = wgpu::TextureFormat::BGRA8Unorm; goodDescriptor.presentMode = wgpu::PresentMode::Mailbox; badDescriptor = goodDescriptor; badDescriptor.width = 0; } void TearDown() override { // Destroy the surface before the window as required by webgpu-native. surface = wgpu::Surface(); if (window != nullptr) { glfwDestroyWindow(window); } DawnTest::TearDown(); } protected: GLFWwindow* window = nullptr; wgpu::Surface surface; wgpu::SwapChainDescriptor goodDescriptor; wgpu::SwapChainDescriptor badDescriptor; // Checks that a RenderAttachment view is an error by trying to create a render pass on it. void CheckTextureIsError(wgpu::Texture texture) { CheckTexture(texture, true, false); } // Checks that a RenderAttachment view is an error by trying to submit a render pass on it. void CheckTextureIsDestroyed(wgpu::Texture texture) { CheckTexture(texture, false, true); } // Checks that a RenderAttachment view is valid by submitting a render pass on it. void CheckTextureIsValid(wgpu::Texture texture) { CheckTexture(texture, false, false); } private: void CheckTexture(wgpu::Texture texture, bool error, bool destroyed) { wgpu::TextureView view; if (error) { ASSERT_DEVICE_ERROR(view = texture.CreateView()); } else { view = texture.CreateView(); } utils::ComboRenderPassDescriptor renderPassDesc({view}); wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDesc); pass.End(); if (error) { ASSERT_DEVICE_ERROR(encoder.Finish()); } else { wgpu::CommandBuffer commands = encoder.Finish(); if (destroyed) { ASSERT_DEVICE_ERROR(queue.Submit(1, &commands)); } else { queue.Submit(1, &commands); } } } }; void CheckTextureMatchesDescriptor(const wgpu::Texture& tex, const wgpu::SwapChainDescriptor& desc) { EXPECT_EQ(desc.width, tex.GetWidth()); EXPECT_EQ(desc.height, tex.GetHeight()); EXPECT_EQ(desc.usage, tex.GetUsage()); EXPECT_EQ(desc.format, tex.GetFormat()); EXPECT_EQ(1u, tex.GetDepthOrArrayLayers()); EXPECT_EQ(1u, tex.GetMipLevelCount()); EXPECT_EQ(1u, tex.GetSampleCount()); EXPECT_EQ(wgpu::TextureDimension::e2D, tex.GetDimension()); } // Control case for a successful swapchain creation and presenting. TEST_P(SwapChainValidationTests, CreationSuccess) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); swapchain.GetCurrentTexture(); swapchain.Present(); } // Test that creating a swapchain with an invalid surface is an error. TEST_P(SwapChainValidationTests, InvalidSurface) { wgpu::SurfaceDescriptor surface_desc = {}; wgpu::Surface surface = GetInstance().CreateSurface(&surface_desc); ASSERT_DEVICE_ERROR_MSG(device.CreateSwapChain(surface, &goodDescriptor), testing::HasSubstr("[Surface] is invalid")); } // Checks that the creation size must be a valid 2D texture size. TEST_P(SwapChainValidationTests, InvalidCreationSize) { wgpu::Limits supportedLimits = GetSupportedLimits().limits; // A width of 0 is invalid. { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.width = 0; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // A height of 0 is invalid. { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.height = 0; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // A width of maxTextureDimension2D is valid but maxTextureDimension2D + 1 isn't. { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.width = supportedLimits.maxTextureDimension2D; device.CreateSwapChain(surface, &desc); desc.width = supportedLimits.maxTextureDimension2D + 1; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // A height of maxTextureDimension2D is valid but maxTextureDimension2D + 1 isn't. { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.height = supportedLimits.maxTextureDimension2D; device.CreateSwapChain(surface, &desc); desc.height = supportedLimits.maxTextureDimension2D + 1; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } } // Checks that the creation usage must be RenderAttachment TEST_P(SwapChainValidationTests, InvalidCreationUsage) { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.usage = wgpu::TextureUsage::TextureBinding; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // Checks that the creation format must (currently) be BGRA8Unorm TEST_P(SwapChainValidationTests, InvalidCreationFormat) { wgpu::SwapChainDescriptor desc = goodDescriptor; desc.format = wgpu::TextureFormat::RGBA8Unorm; ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &desc)); } // Check swapchain operations with an error swapchain are errors TEST_P(SwapChainValidationTests, OperationsOnErrorSwapChain) { wgpu::SwapChain swapchain; ASSERT_DEVICE_ERROR(swapchain = device.CreateSwapChain(surface, &badDescriptor)); wgpu::Texture texture; ASSERT_DEVICE_ERROR(texture = swapchain.GetCurrentTexture()); CheckTextureIsError(texture); ASSERT_DEVICE_ERROR(swapchain.Present()); } // Check it is invalid to call present without getting a current texture. TEST_P(SwapChainValidationTests, PresentWithoutCurrentTexture) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); // Check it is invalid if we never called GetCurrentTexture ASSERT_DEVICE_ERROR(swapchain.Present()); // Check it is invalid if we never called since the last present. swapchain.GetCurrentTexture(); swapchain.Present(); ASSERT_DEVICE_ERROR(swapchain.Present()); } // Check that the current texture isn't destroyed when the ref to the swapchain is lost because the // swapchain is kept alive by the surface. Also check after we lose all refs to the surface, the // texture is destroyed. TEST_P(SwapChainValidationTests, TextureValidAfterSwapChainRefLost) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::Texture texture = swapchain.GetCurrentTexture(); swapchain = nullptr; CheckTextureIsValid(texture); surface = nullptr; CheckTextureIsDestroyed(texture); } // Check that the current texture is the destroyed state after present. TEST_P(SwapChainValidationTests, TextureDestroyedAfterPresent) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::Texture texture = swapchain.GetCurrentTexture(); swapchain.Present(); CheckTextureIsDestroyed(texture); } // Check that returned texture is of the current format / usage / dimension / size / sample count TEST_P(SwapChainValidationTests, ReturnedTextureCharacteristics) { utils::ComboRenderPipelineDescriptor pipelineDesc; pipelineDesc.vertex.module = utils::CreateShaderModule(device, R"( @vertex fn main() -> @builtin(position) vec4f { return vec4f(0.0, 0.0, 0.0, 1.0); })"); pipelineDesc.cFragment.module = utils::CreateShaderModule(device, R"( struct FragmentOut { @location(0) target0 : vec4f, @location(1) target1 : f32, } @fragment fn main() -> FragmentOut { var out : FragmentOut; out.target0 = vec4f(0.0, 1.0, 0.0, 1.0); out.target1 = 0.5; return out; })"); // Validation will check that the sample count of the view matches this format. pipelineDesc.multisample.count = 1; pipelineDesc.cFragment.targetCount = 2; // Validation will check that the format of the view matches this format. pipelineDesc.cTargets[0].format = wgpu::TextureFormat::BGRA8Unorm; pipelineDesc.cTargets[1].format = wgpu::TextureFormat::R8Unorm; device.CreateRenderPipeline(&pipelineDesc); // Create a second texture to be used as render pass attachment. Validation will check that the // size of the view matches the size of this texture. wgpu::TextureDescriptor textureDesc; textureDesc.usage = wgpu::TextureUsage::RenderAttachment; textureDesc.dimension = wgpu::TextureDimension::e2D; textureDesc.size = {1, 1, 1}; textureDesc.format = wgpu::TextureFormat::R8Unorm; textureDesc.sampleCount = 1; wgpu::Texture secondTexture = device.CreateTexture(&textureDesc); // Get the swapchain view and try to use it in the render pass to trigger all the validation. wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::TextureView view = swapchain.GetCurrentTexture().CreateView(); // Validation will also check the dimension of the view is 2D, and it's usage contains // RenderAttachment utils::ComboRenderPassDescriptor renderPassDesc({view, secondTexture.CreateView()}); wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDesc); pass.End(); wgpu::CommandBuffer commands = encoder.Finish(); queue.Submit(1, &commands); // Check that view doesn't have an extra Sampled usage. wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout( device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}}); ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, bgl, {{0, view}})); // Check that view doesn't have an extra Storage usage. bgl = utils::MakeBindGroupLayout( device, {{0, wgpu::ShaderStage::Fragment, wgpu::StorageTextureAccess::WriteOnly, wgpu::TextureFormat::R32Uint}}); ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, bgl, {{0, view}})); } // Check the reflection of textures returned by GetCurrentTexture on valid swapchain. TEST_P(SwapChainValidationTests, ReflectionValidGetCurrentTexture) { // Check with the goodDescriptor. { wgpu::SwapChain swapChain = device.CreateSwapChain(surface, &goodDescriptor); CheckTextureMatchesDescriptor(swapChain.GetCurrentTexture(), goodDescriptor); } // Check with properties that can be changed while keeping a valid descriptor. { wgpu::SwapChainDescriptor otherDescriptor = goodDescriptor; otherDescriptor.width = 2; otherDescriptor.height = 7; wgpu::SwapChain swapChain = device.CreateSwapChain(surface, &goodDescriptor); CheckTextureMatchesDescriptor(swapChain.GetCurrentTexture(), goodDescriptor); } } // Check the reflection of textures returned by GetCurrentTexture on valid swapchain. TEST_P(SwapChainValidationTests, ReflectionErrorGetCurrentTexture) { wgpu::SwapChain swapChain; ASSERT_DEVICE_ERROR(swapChain = device.CreateSwapChain(surface, &badDescriptor)); wgpu::Texture texture; ASSERT_DEVICE_ERROR(texture = swapChain.GetCurrentTexture()); CheckTextureMatchesDescriptor(texture, badDescriptor); } // Check that failing to create a new swapchain doesn't replace the previous one. TEST_P(SwapChainValidationTests, ErrorSwapChainDoesntReplacePreviousOne) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &badDescriptor)); swapchain.GetCurrentTexture(); swapchain.Present(); } // Check that after replacement, all swapchain operations are errors and the texture is destroyed. TEST_P(SwapChainValidationTests, ReplacedSwapChainIsInvalid) { { wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor); device.CreateSwapChain(surface, &goodDescriptor); ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTexture()); } { wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::Texture texture = replacedSwapChain.GetCurrentTexture(); device.CreateSwapChain(surface, &goodDescriptor); CheckTextureIsDestroyed(texture); ASSERT_DEVICE_ERROR(replacedSwapChain.Present()); } } // Check that after surface destruction, all swapchain operations are errors and the texture is // destroyed. The test is split in two to reset the wgpu::Surface in the middle. TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_GetTexture) { wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor); surface = nullptr; ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTexture()); } TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetTexture) { wgpu::SwapChain replacedSwapChain = device.CreateSwapChain(surface, &goodDescriptor); wgpu::Texture texture = replacedSwapChain.GetCurrentTexture(); surface = nullptr; CheckTextureIsDestroyed(texture); ASSERT_DEVICE_ERROR(replacedSwapChain.Present()); } // Test that new swap chain present fails after device is lost TEST_P(SwapChainValidationTests, SwapChainPresentFailsAfterDeviceLost) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); swapchain.GetCurrentTexture(); LoseDeviceForTesting(); ASSERT_DEVICE_ERROR(swapchain.Present()); } // Test that new swap chain get current texture fails after device is lost TEST_P(SwapChainValidationTests, SwapChainGetCurrentTextureFailsAfterDevLost) { wgpu::SwapChain swapchain = device.CreateSwapChain(surface, &goodDescriptor); LoseDeviceForTesting(); ASSERT_DEVICE_ERROR(swapchain.GetCurrentTexture()); } // Test that creation of a new swapchain fails after device is lost TEST_P(SwapChainValidationTests, CreateSwapChainFailsAfterDevLost) { LoseDeviceForTesting(); ASSERT_DEVICE_ERROR(device.CreateSwapChain(surface, &goodDescriptor)); } DAWN_INSTANTIATE_TEST(SwapChainValidationTests, MetalBackend(), NullBackend()); } // anonymous namespace } // namespace dawn