| // Copyright 2017 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/Assert.h" |
| #include "common/Constants.h" |
| #include "common/Math.h" |
| #include "common/Platform.h" |
| #include "dawn_native/DawnNative.h" |
| #include "dawn_wire/WireClient.h" |
| #include "dawn_wire/WireServer.h" |
| #include "utils/BackendBinding.h" |
| #include "utils/DawnHelpers.h" |
| #include "utils/SystemUtils.h" |
| #include "utils/TerribleCommandBuffer.h" |
| |
| #include <algorithm> |
| #include <iostream> |
| #include <unordered_map> |
| #include "GLFW/glfw3.h" |
| |
| namespace { |
| |
| std::string ParamName(dawn_native::BackendType type) { |
| switch (type) { |
| case dawn_native::BackendType::D3D12: |
| return "D3D12"; |
| case dawn_native::BackendType::Metal: |
| return "Metal"; |
| case dawn_native::BackendType::Null: |
| return "Null"; |
| case dawn_native::BackendType::OpenGL: |
| return "OpenGL"; |
| case dawn_native::BackendType::Vulkan: |
| return "Vulkan"; |
| default: |
| UNREACHABLE(); |
| } |
| } |
| |
| // Windows don't usually like to be bound to one API than the other, for example switching |
| // from Vulkan to OpenGL causes crashes on some drivers. Because of this, we lazily created |
| // a window for each backing API. |
| std::unordered_map<dawn_native::BackendType, GLFWwindow*> windows; |
| |
| // Creates a GLFW window set up for use with a given backend. |
| GLFWwindow* GetWindowForBackend(dawn_native::BackendType type) { |
| GLFWwindow** window = &windows[type]; |
| |
| if (*window != nullptr) { |
| return *window; |
| } |
| |
| if (!glfwInit()) { |
| return nullptr; |
| } |
| |
| glfwDefaultWindowHints(); |
| utils::SetupGLFWWindowHintsForBackend(type); |
| |
| std::string windowName = "Dawn " + ParamName(type) + " test window"; |
| *window = glfwCreateWindow(400, 400, windowName.c_str(), nullptr, nullptr); |
| |
| return *window; |
| } |
| |
| // End2end tests should test valid commands produce the expected result so no error |
| // should happen. Failure cases should be tested in the validation tests. |
| void DeviceErrorCauseTestFailure(const char* message, dawnCallbackUserdata) { |
| FAIL() << "Device level failure: " << message; |
| } |
| |
| struct MapReadUserdata { |
| DawnTest* test; |
| size_t slot; |
| }; |
| |
| constexpr uint32_t kVendorID_AMD = 0x1002; |
| constexpr uint32_t kVendorID_ARM = 0x13B5; |
| constexpr uint32_t kVendorID_ImgTec = 0x1010; |
| constexpr uint32_t kVendorID_Intel = 0x8086; |
| constexpr uint32_t kVendorID_Nvidia = 0x10DE; |
| constexpr uint32_t kVendorID_Qualcomm = 0x5143; |
| |
| } // namespace |
| |
| void InitDawnEnd2EndTestEnvironment(int, char**) { |
| } |
| |
| DawnTest::DawnTest() = default; |
| |
| DawnTest::~DawnTest() { |
| // We need to destroy child objects before the Device |
| mReadbackSlots.clear(); |
| queue = dawn::Queue(); |
| swapchain = dawn::SwapChain(); |
| device = dawn::Device(); |
| |
| dawnSetProcs(nullptr); |
| } |
| |
| bool DawnTest::IsD3D12() const { |
| return GetParam() == D3D12Backend; |
| } |
| |
| bool DawnTest::IsMetal() const { |
| return GetParam() == MetalBackend; |
| } |
| |
| bool DawnTest::IsOpenGL() const { |
| return GetParam() == OpenGLBackend; |
| } |
| |
| bool DawnTest::IsVulkan() const { |
| return GetParam() == VulkanBackend; |
| } |
| |
| bool DawnTest::IsAMD() const { |
| return mPCIInfo.vendorId == kVendorID_AMD; |
| } |
| |
| bool DawnTest::IsARM() const { |
| return mPCIInfo.vendorId == kVendorID_ARM; |
| } |
| |
| bool DawnTest::IsImgTec() const { |
| return mPCIInfo.vendorId == kVendorID_ImgTec; |
| } |
| |
| bool DawnTest::IsIntel() const { |
| return mPCIInfo.vendorId == kVendorID_Intel; |
| } |
| |
| bool DawnTest::IsNvidia() const { |
| return mPCIInfo.vendorId == kVendorID_Nvidia; |
| } |
| |
| bool DawnTest::IsQualcomm() const { |
| return mPCIInfo.vendorId == kVendorID_Qualcomm; |
| } |
| |
| bool DawnTest::IsWindows() const { |
| #ifdef DAWN_PLATFORM_WINDOWS |
| return true; |
| #else |
| return false; |
| #endif |
| } |
| |
| bool DawnTest::IsLinux() const { |
| #ifdef DAWN_PLATFORM_LINUX |
| return true; |
| #else |
| return false; |
| #endif |
| } |
| |
| bool DawnTest::IsMacOS() const { |
| #ifdef DAWN_PLATFORM_APPLE |
| return true; |
| #else |
| return false; |
| #endif |
| } |
| |
| bool gTestUsesWire = false; |
| |
| void DawnTest::SetUp() { |
| // Create the test window and discover adapters using it (esp. for OpenGL) |
| GLFWwindow* testWindow = GetWindowForBackend(GetParam()); |
| DAWN_ASSERT(testWindow != nullptr); |
| |
| mInstance = std::make_unique<dawn_native::Instance>(); |
| utils::DiscoverAdapter(mInstance.get(), testWindow, GetParam()); |
| |
| // Get an adapter for the backend to use, and create the device. |
| dawn_native::Adapter backendAdapter; |
| { |
| std::vector<dawn_native::Adapter> adapters = mInstance->GetAdapters(); |
| auto adapterIt = std::find_if(adapters.begin(), adapters.end(), |
| [this](const dawn_native::Adapter adapter) -> bool { |
| // Chromium's GTest harness has GetParam() as a regular |
| // function and not a member function of this. |
| DAWN_UNUSED(this); |
| return adapter.GetBackendType() == GetParam(); |
| }); |
| ASSERT(adapterIt != adapters.end()); |
| backendAdapter = *adapterIt; |
| } |
| |
| mPCIInfo = backendAdapter.GetPCIInfo(); |
| dawnDevice backendDevice = backendAdapter.CreateDevice(); |
| dawnProcTable backendProcs = dawn_native::GetProcs(); |
| |
| mBinding.reset(utils::CreateBinding(GetParam(), testWindow, backendDevice)); |
| DAWN_ASSERT(mBinding != nullptr); |
| |
| // Choose whether to use the backend procs and devices directly, or set up the wire. |
| dawnDevice cDevice = nullptr; |
| dawnProcTable procs; |
| |
| if (gTestUsesWire) { |
| mC2sBuf = std::make_unique<utils::TerribleCommandBuffer>(); |
| mS2cBuf = std::make_unique<utils::TerribleCommandBuffer>(); |
| |
| mWireServer.reset(new dawn_wire::WireServer(backendDevice, backendProcs, mS2cBuf.get())); |
| mC2sBuf->SetHandler(mWireServer.get()); |
| |
| mWireClient.reset(new dawn_wire::WireClient(mC2sBuf.get())); |
| dawnDevice clientDevice = mWireClient->GetDevice(); |
| dawnProcTable clientProcs = mWireClient->GetProcs(); |
| mS2cBuf->SetHandler(mWireClient.get()); |
| |
| procs = clientProcs; |
| cDevice = clientDevice; |
| } else { |
| procs = backendProcs; |
| cDevice = backendDevice; |
| } |
| |
| // Set up the device and queue because all tests need them, and DawnTest needs them too for the |
| // deferred expectations. |
| dawnSetProcs(&procs); |
| device = dawn::Device::Acquire(cDevice); |
| queue = device.CreateQueue(); |
| |
| // The swapchain isn't used by tests but is useful when debugging with graphics debuggers that |
| // capture at frame boundaries. |
| dawn::SwapChainDescriptor swapChainDesc; |
| swapChainDesc.implementation = mBinding->GetSwapChainImplementation(); |
| swapchain = device.CreateSwapChain(&swapChainDesc); |
| swapchain.Configure( |
| static_cast<dawn::TextureFormat>(mBinding->GetPreferredSwapChainTextureFormat()), |
| dawn::TextureUsageBit::OutputAttachment, 400, 400); |
| |
| // The end2end tests should never cause validation errors. These should be tested in unittests. |
| device.SetErrorCallback(DeviceErrorCauseTestFailure, 0); |
| } |
| |
| void DawnTest::TearDown() { |
| FlushWire(); |
| |
| MapSlotsSynchronously(); |
| ResolveExpectations(); |
| |
| for (size_t i = 0; i < mReadbackSlots.size(); ++i) { |
| mReadbackSlots[i].buffer.Unmap(); |
| } |
| } |
| |
| std::ostringstream& DawnTest::AddBufferExpectation(const char* file, |
| int line, |
| const dawn::Buffer& buffer, |
| uint32_t offset, |
| uint32_t size, |
| detail::Expectation* expectation) { |
| auto readback = ReserveReadback(size); |
| |
| // We need to enqueue the copy immediately because by the time we resolve the expectation, |
| // the buffer might have been modified. |
| dawn::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyBufferToBuffer(buffer, offset, readback.buffer, readback.offset, size); |
| |
| dawn::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| DeferredExpectation deferred; |
| deferred.file = file; |
| deferred.line = line; |
| deferred.readbackSlot = readback.slot; |
| deferred.readbackOffset = readback.offset; |
| deferred.size = size; |
| deferred.rowBytes = size; |
| deferred.rowPitch = size; |
| deferred.expectation.reset(expectation); |
| |
| mDeferredExpectations.push_back(std::move(deferred)); |
| mDeferredExpectations.back().message = std::make_unique<std::ostringstream>(); |
| return *(mDeferredExpectations.back().message.get()); |
| } |
| |
| std::ostringstream& DawnTest::AddTextureExpectation(const char* file, |
| int line, |
| const dawn::Texture& texture, |
| uint32_t x, |
| uint32_t y, |
| uint32_t width, |
| uint32_t height, |
| uint32_t level, |
| uint32_t slice, |
| uint32_t pixelSize, |
| detail::Expectation* expectation) { |
| uint32_t rowPitch = Align(width * pixelSize, kTextureRowPitchAlignment); |
| uint32_t size = rowPitch * (height - 1) + width * pixelSize; |
| |
| auto readback = ReserveReadback(size); |
| |
| // We need to enqueue the copy immediately because by the time we resolve the expectation, |
| // the texture might have been modified. |
| dawn::TextureCopyView textureCopyView = |
| utils::CreateTextureCopyView(texture, level, slice, {x, y, 0}); |
| dawn::BufferCopyView bufferCopyView = |
| utils::CreateBufferCopyView(readback.buffer, readback.offset, rowPitch, 0); |
| dawn::Extent3D copySize = {width, height, 1}; |
| |
| dawn::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyTextureToBuffer(&textureCopyView, &bufferCopyView, ©Size); |
| |
| dawn::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| DeferredExpectation deferred; |
| deferred.file = file; |
| deferred.line = line; |
| deferred.readbackSlot = readback.slot; |
| deferred.readbackOffset = readback.offset; |
| deferred.size = size; |
| deferred.rowBytes = width * pixelSize; |
| deferred.rowPitch = rowPitch; |
| deferred.expectation.reset(expectation); |
| |
| mDeferredExpectations.push_back(std::move(deferred)); |
| mDeferredExpectations.back().message = std::make_unique<std::ostringstream>(); |
| return *(mDeferredExpectations.back().message.get()); |
| } |
| |
| void DawnTest::WaitABit() { |
| device.Tick(); |
| FlushWire(); |
| |
| utils::USleep(100); |
| } |
| |
| void DawnTest::SwapBuffersForCapture() { |
| // Insert a frame boundary for API capture tools. |
| dawn::Texture backBuffer = swapchain.GetNextTexture(); |
| swapchain.Present(backBuffer); |
| } |
| |
| void DawnTest::FlushWire() { |
| if (gTestUsesWire) { |
| bool C2SFlushed = mC2sBuf->Flush(); |
| bool S2CFlushed = mS2cBuf->Flush(); |
| ASSERT(C2SFlushed); |
| ASSERT(S2CFlushed); |
| } |
| } |
| |
| DawnTest::ReadbackReservation DawnTest::ReserveReadback(uint32_t readbackSize) { |
| // For now create a new MapRead buffer for each readback |
| // TODO(cwallez@chromium.org): eventually make bigger buffers and allocate linearly? |
| dawn::BufferDescriptor descriptor; |
| descriptor.size = readbackSize; |
| descriptor.usage = dawn::BufferUsageBit::MapRead | dawn::BufferUsageBit::TransferDst; |
| |
| ReadbackSlot slot; |
| slot.bufferSize = readbackSize; |
| slot.buffer = device.CreateBuffer(&descriptor); |
| |
| ReadbackReservation reservation; |
| reservation.buffer = slot.buffer; |
| reservation.slot = mReadbackSlots.size(); |
| reservation.offset = 0; |
| |
| mReadbackSlots.push_back(std::move(slot)); |
| return reservation; |
| } |
| |
| void DawnTest::MapSlotsSynchronously() { |
| // Initialize numPendingMapOperations before mapping, just in case the callback is called |
| // immediately. |
| mNumPendingMapOperations = mReadbackSlots.size(); |
| |
| // Map all readback slots |
| for (size_t i = 0; i < mReadbackSlots.size(); ++i) { |
| auto userdata = new MapReadUserdata{this, i}; |
| |
| auto& slot = mReadbackSlots[i]; |
| slot.buffer.MapReadAsync(SlotMapReadCallback, static_cast<dawn::CallbackUserdata>( |
| reinterpret_cast<uintptr_t>(userdata))); |
| } |
| |
| // Busy wait until all map operations are done. |
| while (mNumPendingMapOperations != 0) { |
| WaitABit(); |
| } |
| } |
| |
| // static |
| void DawnTest::SlotMapReadCallback(dawnBufferMapAsyncStatus status, |
| const void* data, |
| uint32_t, |
| dawnCallbackUserdata userdata_) { |
| DAWN_ASSERT(status == DAWN_BUFFER_MAP_ASYNC_STATUS_SUCCESS); |
| |
| auto userdata = reinterpret_cast<MapReadUserdata*>(static_cast<uintptr_t>(userdata_)); |
| userdata->test->mReadbackSlots[userdata->slot].mappedData = data; |
| userdata->test->mNumPendingMapOperations--; |
| |
| delete userdata; |
| } |
| |
| void DawnTest::ResolveExpectations() { |
| for (const auto& expectation : mDeferredExpectations) { |
| DAWN_ASSERT(mReadbackSlots[expectation.readbackSlot].mappedData != nullptr); |
| |
| // Get a pointer to the mapped copy of the data for the expectation. |
| const char* data = |
| reinterpret_cast<const char*>(mReadbackSlots[expectation.readbackSlot].mappedData); |
| data += expectation.readbackOffset; |
| |
| uint32_t size; |
| std::vector<char> packedData; |
| if (expectation.rowBytes != expectation.rowPitch) { |
| DAWN_ASSERT(expectation.rowPitch > expectation.rowBytes); |
| uint32_t rowCount = |
| (expectation.size + expectation.rowPitch - 1) / expectation.rowPitch; |
| uint32_t packedSize = rowCount * expectation.rowBytes; |
| packedData.resize(packedSize); |
| for (uint32_t r = 0; r < rowCount; ++r) { |
| for (uint32_t i = 0; i < expectation.rowBytes; ++i) { |
| packedData[i + r * expectation.rowBytes] = data[i + r * expectation.rowPitch]; |
| } |
| } |
| data = packedData.data(); |
| size = packedSize; |
| } else { |
| size = expectation.size; |
| } |
| |
| // Get the result for the expectation and add context to failures |
| testing::AssertionResult result = expectation.expectation->Check(data, size); |
| if (!result) { |
| result << " Expectation created at " << expectation.file << ":" << expectation.line |
| << std::endl; |
| result << expectation.message->str(); |
| } |
| |
| EXPECT_TRUE(result); |
| } |
| } |
| |
| bool RGBA8::operator==(const RGBA8& other) const { |
| return r == other.r && g == other.g && b == other.b && a == other.a; |
| } |
| |
| bool RGBA8::operator!=(const RGBA8& other) const { |
| return !(*this == other); |
| } |
| |
| std::ostream& operator<<(std::ostream& stream, const RGBA8& color) { |
| return stream << "RGBA8(" << static_cast<int>(color.r) << ", " << static_cast<int>(color.g) |
| << ", " << static_cast<int>(color.b) << ", " << static_cast<int>(color.a) << ")"; |
| } |
| |
| namespace detail { |
| bool IsBackendAvailable(dawn_native::BackendType type) { |
| switch (type) { |
| #if defined(DAWN_ENABLE_BACKEND_D3D12) |
| case dawn_native::BackendType::D3D12: |
| #endif |
| #if defined(DAWN_ENABLE_BACKEND_METAL) |
| case dawn_native::BackendType::Metal: |
| #endif |
| #if defined(DAWN_ENABLE_BACKEND_OPENGL) |
| case dawn_native::BackendType::OpenGL: |
| #endif |
| #if defined(DAWN_ENABLE_BACKEND_VULKAN) |
| case dawn_native::BackendType::Vulkan: |
| #endif |
| return true; |
| |
| default: |
| return false; |
| } |
| } |
| |
| std::vector<dawn_native::BackendType> FilterBackends(const dawn_native::BackendType* types, |
| size_t numParams) { |
| std::vector<dawn_native::BackendType> backends; |
| |
| for (size_t i = 0; i < numParams; ++i) { |
| if (IsBackendAvailable(types[i])) { |
| backends.push_back(types[i]); |
| } |
| } |
| return backends; |
| } |
| |
| std::string GetParamName(const testing::TestParamInfo<dawn_native::BackendType>& info) { |
| return ParamName(info.param); |
| } |
| |
| // Helper classes to set expectations |
| |
| template <typename T> |
| ExpectEq<T>::ExpectEq(T singleValue) { |
| mExpected.push_back(singleValue); |
| } |
| |
| template <typename T> |
| ExpectEq<T>::ExpectEq(const T* values, const unsigned int count) { |
| mExpected.assign(values, values + count); |
| } |
| |
| template <typename T> |
| testing::AssertionResult ExpectEq<T>::Check(const void* data, size_t size) { |
| DAWN_ASSERT(size == sizeof(T) * mExpected.size()); |
| |
| const T* actual = reinterpret_cast<const T*>(data); |
| |
| testing::AssertionResult failure = testing::AssertionFailure(); |
| for (size_t i = 0; i < mExpected.size(); ++i) { |
| if (actual[i] != mExpected[i]) { |
| testing::AssertionResult result = testing::AssertionFailure() |
| << "Expected data[" << i << "] to be " |
| << mExpected[i] << ", actual " << actual[i] |
| << std::endl; |
| |
| auto printBuffer = [&](const T* buffer) { |
| static constexpr unsigned int kBytes = sizeof(T); |
| |
| for (size_t index = 0; index < mExpected.size(); ++index) { |
| auto byteView = reinterpret_cast<const uint8_t*>(buffer + index); |
| for (unsigned int b = 0; b < kBytes; ++b) { |
| char buf[4]; |
| sprintf(buf, "%02X ", byteView[b]); |
| result << buf; |
| } |
| } |
| result << std::endl; |
| }; |
| |
| if (mExpected.size() <= 1024) { |
| result << "Expected:" << std::endl; |
| printBuffer(mExpected.data()); |
| |
| result << "Actual:" << std::endl; |
| printBuffer(actual); |
| } |
| |
| return result; |
| } |
| } |
| |
| return testing::AssertionSuccess(); |
| } |
| |
| template class ExpectEq<uint8_t>; |
| template class ExpectEq<uint32_t>; |
| template class ExpectEq<RGBA8>; |
| } // namespace detail |