| // Copyright 2020 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| #include <memory> | |
| #include "GLFW/glfw3.h" | |
| #include "dawn/common/Constants.h" | |
| #include "dawn/common/Log.h" | |
| #include "dawn/tests/DawnTest.h" | |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" | |
| #include "dawn/utils/WGPUHelpers.h" | |
| #include "webgpu/webgpu_glfw.h" | |
| namespace dawn { | |
| namespace { | |
| struct GLFWindowDestroyer { | |
| void operator()(GLFWwindow* ptr) { glfwDestroyWindow(ptr); } | |
| }; | |
| 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.reset( | |
| glfwCreateWindow(400, 400, "SwapChainValidationTests window", nullptr, nullptr)); | |
| surface = wgpu::glfw::CreateSurfaceForWindow(GetInstance(), window.get()); | |
| 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(); | |
| window.reset(); | |
| DawnTest::TearDown(); | |
| } | |
| wgpu::SwapChain CreateSwapChain(wgpu::Surface const& otherSurface, | |
| wgpu::SwapChainDescriptor const* descriptor) { | |
| wgpu::SwapChain swapchain; | |
| EXPECT_DEPRECATION_WARNING(swapchain = device.CreateSwapChain(otherSurface, descriptor)); | |
| return swapchain; | |
| } | |
| protected: | |
| std::unique_ptr<GLFWwindow, GLFWindowDestroyer> 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 = 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(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(CreateSwapChain(surface, &desc)); | |
| } | |
| // A height of 0 is invalid. | |
| { | |
| wgpu::SwapChainDescriptor desc = goodDescriptor; | |
| desc.height = 0; | |
| ASSERT_DEVICE_ERROR(CreateSwapChain(surface, &desc)); | |
| } | |
| // A width of maxTextureDimension2D is valid but maxTextureDimension2D + 1 isn't. | |
| { | |
| wgpu::SwapChainDescriptor desc = goodDescriptor; | |
| desc.width = supportedLimits.maxTextureDimension2D; | |
| CreateSwapChain(surface, &desc); | |
| desc.width = supportedLimits.maxTextureDimension2D + 1; | |
| ASSERT_DEVICE_ERROR(CreateSwapChain(surface, &desc)); | |
| } | |
| // A height of maxTextureDimension2D is valid but maxTextureDimension2D + 1 isn't. | |
| { | |
| wgpu::SwapChainDescriptor desc = goodDescriptor; | |
| desc.height = supportedLimits.maxTextureDimension2D; | |
| CreateSwapChain(surface, &desc); | |
| desc.height = supportedLimits.maxTextureDimension2D + 1; | |
| ASSERT_DEVICE_ERROR(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(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(CreateSwapChain(surface, &desc)); | |
| } | |
| // Check swapchain operations with an error swapchain are errors | |
| TEST_P(SwapChainValidationTests, OperationsOnErrorSwapChain) { | |
| wgpu::SwapChain swapchain; | |
| ASSERT_DEVICE_ERROR(swapchain = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = CreateSwapChain(surface, &goodDescriptor); | |
| ASSERT_DEVICE_ERROR(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 = CreateSwapChain(surface, &goodDescriptor); | |
| CreateSwapChain(surface, &goodDescriptor); | |
| ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTexture()); | |
| } | |
| { | |
| wgpu::SwapChain replacedSwapChain = CreateSwapChain(surface, &goodDescriptor); | |
| wgpu::Texture texture = replacedSwapChain.GetCurrentTexture(); | |
| 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 = CreateSwapChain(surface, &goodDescriptor); | |
| surface = nullptr; | |
| ASSERT_DEVICE_ERROR(replacedSwapChain.GetCurrentTexture()); | |
| } | |
| TEST_P(SwapChainValidationTests, SwapChainIsInvalidAfterSurfaceDestruction_AfterGetTexture) { | |
| wgpu::SwapChain replacedSwapChain = CreateSwapChain(surface, &goodDescriptor); | |
| wgpu::Texture texture = replacedSwapChain.GetCurrentTexture(); | |
| surface = nullptr; | |
| CheckTextureIsDestroyed(texture); | |
| ASSERT_DEVICE_ERROR(replacedSwapChain.Present()); | |
| } | |
| // Test that new swap chain present after device is lost | |
| TEST_P(SwapChainValidationTests, SwapChainPresentAfterDeviceLost) { | |
| wgpu::SwapChain swapchain = CreateSwapChain(surface, &goodDescriptor); | |
| swapchain.GetCurrentTexture(); | |
| LoseDeviceForTesting(); | |
| swapchain.Present(); | |
| } | |
| // Test that new swap chain get current texture fails after device is lost | |
| TEST_P(SwapChainValidationTests, SwapChainGetCurrentTextureFailsAfterDevLost) { | |
| wgpu::SwapChain swapchain = CreateSwapChain(surface, &goodDescriptor); | |
| LoseDeviceForTesting(); | |
| EXPECT_TRUE(dawn::native::CheckIsErrorForTesting(swapchain.GetCurrentTexture().Get())); | |
| } | |
| // Test that creation of a new swapchain fails after device is lost | |
| TEST_P(SwapChainValidationTests, CreateSwapChainFailsAfterDevLost) { | |
| LoseDeviceForTesting(); | |
| EXPECT_TRUE( | |
| dawn::native::CheckIsErrorForTesting(CreateSwapChain(surface, &goodDescriptor).Get())); | |
| } | |
| DAWN_INSTANTIATE_TEST(SwapChainValidationTests, MetalBackend(), NullBackend()); | |
| } // anonymous namespace | |
| } // namespace dawn |