Start introducing a "backend" for vulkan image wrapping tests

Bug: dawn:221

Change-Id: I8077d6a873bbd4f4ed549b386014e10020ffc725
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/75424
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
diff --git a/src/dawn_native/DawnNative.cpp b/src/dawn_native/DawnNative.cpp
index b166c44..498d9b1 100644
--- a/src/dawn_native/DawnNative.cpp
+++ b/src/dawn_native/DawnNative.cpp
@@ -278,12 +278,20 @@
 
     // ExternalImageDescriptor
 
-    ExternalImageDescriptor::ExternalImageDescriptor(ExternalImageType type) : type(type) {
+    ExternalImageDescriptor::ExternalImageDescriptor(ExternalImageType type) : mType(type) {
+    }
+
+    ExternalImageType ExternalImageDescriptor::GetType() const {
+        return mType;
     }
 
     // ExternalImageExportInfo
 
-    ExternalImageExportInfo::ExternalImageExportInfo(ExternalImageType type) : type(type) {
+    ExternalImageExportInfo::ExternalImageExportInfo(ExternalImageType type) : mType(type) {
+    }
+
+    ExternalImageType ExternalImageExportInfo::GetType() const {
+        return mType;
     }
 
     const char* GetObjectLabelForTesting(void* objectHandle) {
diff --git a/src/dawn_native/vulkan/VulkanBackend.cpp b/src/dawn_native/vulkan/VulkanBackend.cpp
index f15cb8a..f7e3560 100644
--- a/src/dawn_native/vulkan/VulkanBackend.cpp
+++ b/src/dawn_native/vulkan/VulkanBackend.cpp
@@ -83,7 +83,7 @@
 
     WGPUTexture WrapVulkanImage(WGPUDevice device, const ExternalImageDescriptorVk* descriptor) {
 #if defined(DAWN_PLATFORM_LINUX)
-        switch (descriptor->type) {
+        switch (descriptor->GetType()) {
             case ExternalImageType::OpaqueFD:
             case ExternalImageType::DmaBuf: {
                 Device* backendDevice = ToBackend(FromAPI(device));
@@ -108,7 +108,7 @@
             return false;
         }
 #if defined(DAWN_PLATFORM_LINUX)
-        switch (info->type) {
+        switch (info->GetType()) {
             case ExternalImageType::OpaqueFD:
             case ExternalImageType::DmaBuf: {
                 Texture* backendTexture = ToBackend(FromAPI(texture));
diff --git a/src/dawn_native/vulkan/external_memory/MemoryServiceDmaBuf.cpp b/src/dawn_native/vulkan/external_memory/MemoryServiceDmaBuf.cpp
index 2bf8874..8cdd5ba 100644
--- a/src/dawn_native/vulkan/external_memory/MemoryServiceDmaBuf.cpp
+++ b/src/dawn_native/vulkan/external_memory/MemoryServiceDmaBuf.cpp
@@ -144,7 +144,7 @@
         if (!mSupported) {
             return false;
         }
-        if (descriptor->type != ExternalImageType::DmaBuf) {
+        if (descriptor->GetType() != ExternalImageType::DmaBuf) {
             return false;
         }
         const ExternalImageDescriptorDmaBuf* dmaBufDescriptor =
@@ -226,7 +226,7 @@
     ResultOrError<MemoryImportParams> Service::GetMemoryImportParams(
         const ExternalImageDescriptor* descriptor,
         VkImage image) {
-        DAWN_INVALID_IF(descriptor->type != ExternalImageType::DmaBuf,
+        DAWN_INVALID_IF(descriptor->GetType() != ExternalImageType::DmaBuf,
                         "ExternalImageDescriptor is not a ExternalImageDescriptorDmaBuf.");
 
         const ExternalImageDescriptorDmaBuf* dmaBufDescriptor =
@@ -290,7 +290,7 @@
 
     ResultOrError<VkImage> Service::CreateImage(const ExternalImageDescriptor* descriptor,
                                                 const VkImageCreateInfo& baseCreateInfo) {
-        DAWN_INVALID_IF(descriptor->type != ExternalImageType::DmaBuf,
+        DAWN_INVALID_IF(descriptor->GetType() != ExternalImageType::DmaBuf,
                         "ExternalImageDescriptor is not a dma-buf descriptor.");
 
         const ExternalImageDescriptorDmaBuf* dmaBufDescriptor =
diff --git a/src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp b/src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp
index e8762d2..181a17b 100644
--- a/src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp
+++ b/src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp
@@ -90,7 +90,7 @@
     ResultOrError<MemoryImportParams> Service::GetMemoryImportParams(
         const ExternalImageDescriptor* descriptor,
         VkImage image) {
-        DAWN_INVALID_IF(descriptor->type != ExternalImageType::OpaqueFD,
+        DAWN_INVALID_IF(descriptor->GetType() != ExternalImageType::OpaqueFD,
                         "ExternalImageDescriptor is not an OpaqueFD descriptor.");
 
         const ExternalImageDescriptorOpaqueFD* opaqueFDDescriptor =
diff --git a/src/include/dawn_native/DawnNative.h b/src/include/dawn_native/DawnNative.h
index e8a6e13..4850d8c 100644
--- a/src/include/dawn_native/DawnNative.h
+++ b/src/include/dawn_native/DawnNative.h
@@ -217,12 +217,15 @@
     // Common properties of external images
     struct DAWN_NATIVE_EXPORT ExternalImageDescriptor {
       public:
-        const ExternalImageType type;
         const WGPUTextureDescriptor* cTextureDescriptor;  // Must match image creation params
         bool isInitialized;  // Whether the texture is initialized on import
+        ExternalImageType GetType() const;
 
       protected:
         ExternalImageDescriptor(ExternalImageType type);
+
+      private:
+        ExternalImageType mType;
     };
 
     struct DAWN_NATIVE_EXPORT ExternalImageAccessDescriptor {
@@ -233,11 +236,14 @@
 
     struct DAWN_NATIVE_EXPORT ExternalImageExportInfo {
       public:
-        const ExternalImageType type;
         bool isInitialized;  // Whether the texture is initialized after export
+        ExternalImageType GetType() const;
 
       protected:
         ExternalImageExportInfo(ExternalImageType type);
+
+      private:
+        ExternalImageType mType;
     };
 
     DAWN_NATIVE_EXPORT const char* GetObjectLabelForTesting(void* objectHandle);
diff --git a/src/tests/BUILD.gn b/src/tests/BUILD.gn
index 6141875..aab52af 100644
--- a/src/tests/BUILD.gn
+++ b/src/tests/BUILD.gn
@@ -494,7 +494,11 @@
     if (is_chromeos) {
       sources += [ "white_box/VulkanImageWrappingTestsDmaBuf.cpp" ]
     } else if (is_linux) {
-      sources += [ "white_box/VulkanImageWrappingTestsOpaqueFD.cpp" ]
+      sources += [
+        "white_box/VulkanImageWrappingTests.cpp",
+        "white_box/VulkanImageWrappingTests.h",
+        "white_box/VulkanImageWrappingTests_OpaqueFD.cpp",
+      ]
     }
 
     if (dawn_enable_error_injection) {
diff --git a/src/tests/white_box/VulkanImageWrappingTests.cpp b/src/tests/white_box/VulkanImageWrappingTests.cpp
new file mode 100644
index 0000000..7cf1df0
--- /dev/null
+++ b/src/tests/white_box/VulkanImageWrappingTests.cpp
@@ -0,0 +1,793 @@
+// Copyright 2021 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/white_box/VulkanImageWrappingTests.h"
+
+#include "common/Math.h"
+#include "dawn_native/vulkan/AdapterVk.h"
+#include "dawn_native/vulkan/DeviceVk.h"
+#include "tests/DawnTest.h"
+#include "utils/WGPUHelpers.h"
+
+namespace dawn::native { namespace vulkan {
+
+    using ExternalTexture = VulkanImageWrappingTestBackend::ExternalTexture;
+    using ExternalSemaphore = VulkanImageWrappingTestBackend::ExternalSemaphore;
+
+    ExternalImageDescriptorVkForTesting::ExternalImageDescriptorVkForTesting()
+        : ExternalImageDescriptorVk(ExternalImageType::OpaqueFD) {
+    }
+    ExternalImageExportInfoVkForTesting::ExternalImageExportInfoVkForTesting()
+        : ExternalImageExportInfoVk(ExternalImageType::OpaqueFD) {
+    }
+
+    namespace {
+
+        class VulkanImageWrappingTestBase : public DawnTest {
+          protected:
+            std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
+                return {wgpu::FeatureName::DawnInternalUsages};
+            }
+
+          public:
+            void SetUp() override {
+                DawnTest::SetUp();
+                DAWN_TEST_UNSUPPORTED_IF(UsesWire());
+
+                mBackend = VulkanImageWrappingTestBackend::Create(device);
+
+                defaultDescriptor.dimension = wgpu::TextureDimension::e2D;
+                defaultDescriptor.format = wgpu::TextureFormat::RGBA8Unorm;
+                defaultDescriptor.size = {1, 1, 1};
+                defaultDescriptor.sampleCount = 1;
+                defaultDescriptor.mipLevelCount = 1;
+                defaultDescriptor.usage = wgpu::TextureUsage::RenderAttachment |
+                                          wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst;
+
+                defaultTexture = mBackend->CreateTexture(1, 1, defaultDescriptor.format,
+                                                         defaultDescriptor.usage);
+            }
+
+            void TearDown() override {
+                if (UsesWire()) {
+                    DawnTest::TearDown();
+                    return;
+                }
+
+                defaultTexture = nullptr;
+                mBackend = nullptr;
+                DawnTest::TearDown();
+            }
+
+            wgpu::Texture WrapVulkanImage(
+                wgpu::Device dawnDevice,
+                const wgpu::TextureDescriptor* textureDescriptor,
+                const ExternalTexture* externalTexture,
+                std::vector<std::unique_ptr<ExternalSemaphore>> semaphores,
+                bool isInitialized = true,
+                bool expectValid = true) {
+                ExternalImageDescriptorVkForTesting descriptor;
+                return WrapVulkanImage(dawnDevice, textureDescriptor, externalTexture,
+                                       std::move(semaphores), descriptor.releasedOldLayout,
+                                       descriptor.releasedNewLayout, isInitialized, expectValid);
+            }
+
+            wgpu::Texture WrapVulkanImage(
+                wgpu::Device dawnDevice,
+                const wgpu::TextureDescriptor* textureDescriptor,
+                const ExternalTexture* externalTexture,
+                std::vector<std::unique_ptr<ExternalSemaphore>> semaphores,
+                VkImageLayout releasedOldLayout,
+                VkImageLayout releasedNewLayout,
+                bool isInitialized = true,
+                bool expectValid = true) {
+                ExternalImageDescriptorVkForTesting descriptor;
+                descriptor.cTextureDescriptor =
+                    reinterpret_cast<const WGPUTextureDescriptor*>(textureDescriptor);
+                descriptor.isInitialized = isInitialized;
+                descriptor.releasedOldLayout = releasedOldLayout;
+                descriptor.releasedNewLayout = releasedNewLayout;
+
+                wgpu::Texture texture = mBackend->WrapImage(dawnDevice, externalTexture, descriptor,
+                                                            std::move(semaphores));
+
+                if (expectValid) {
+                    EXPECT_NE(texture, nullptr) << "Failed to wrap image, are external memory / "
+                                                   "semaphore extensions supported?";
+                } else {
+                    EXPECT_EQ(texture, nullptr);
+                }
+
+                return texture;
+            }
+
+            // Exports the signal from a wrapped texture and ignores it
+            // We have to export the signal before destroying the wrapped texture else it's an
+            // assertion failure
+            void IgnoreSignalSemaphore(wgpu::Texture wrappedTexture) {
+                ExternalImageExportInfoVkForTesting exportInfo;
+                bool result =
+                    mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_GENERAL, &exportInfo);
+                ASSERT(result);
+            }
+
+          protected:
+            std::unique_ptr<VulkanImageWrappingTestBackend> mBackend;
+
+            wgpu::TextureDescriptor defaultDescriptor;
+            std::unique_ptr<ExternalTexture> defaultTexture;
+        };
+
+    }  // anonymous namespace
+
+    using VulkanImageWrappingValidationTests = VulkanImageWrappingTestBase;
+
+    // Test no error occurs if the import is valid
+    TEST_P(VulkanImageWrappingValidationTests, SuccessfulImport) {
+        wgpu::Texture texture =
+            WrapVulkanImage(device, &defaultDescriptor, defaultTexture.get(), {}, true, true);
+        EXPECT_NE(texture.Get(), nullptr);
+        IgnoreSignalSemaphore(texture);
+    }
+
+    // Test no error occurs if the import is valid with DawnTextureInternalUsageDescriptor
+    TEST_P(VulkanImageWrappingValidationTests, SuccessfulImportWithInternalUsageDescriptor) {
+        wgpu::DawnTextureInternalUsageDescriptor internalDesc = {};
+        defaultDescriptor.nextInChain = &internalDesc;
+        internalDesc.internalUsage = wgpu::TextureUsage::CopySrc;
+        internalDesc.sType = wgpu::SType::DawnTextureInternalUsageDescriptor;
+
+        wgpu::Texture texture =
+            WrapVulkanImage(device, &defaultDescriptor, defaultTexture.get(), {}, true, true);
+        EXPECT_NE(texture.Get(), nullptr);
+        IgnoreSignalSemaphore(texture);
+    }
+
+    // Test an error occurs if an invalid sType is the nextInChain
+    TEST_P(VulkanImageWrappingValidationTests, InvalidTextureDescriptor) {
+        wgpu::ChainedStruct chainedDescriptor;
+        chainedDescriptor.sType = wgpu::SType::SurfaceDescriptorFromWindowsSwapChainPanel;
+        defaultDescriptor.nextInChain = &chainedDescriptor;
+
+        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
+                                device, &defaultDescriptor, defaultTexture.get(), {}, true, false));
+        EXPECT_EQ(texture.Get(), nullptr);
+    }
+
+    // Test an error occurs if the descriptor dimension isn't 2D
+    TEST_P(VulkanImageWrappingValidationTests, InvalidTextureDimension) {
+        defaultDescriptor.dimension = wgpu::TextureDimension::e1D;
+
+        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
+                                device, &defaultDescriptor, defaultTexture.get(), {}, true, false));
+        EXPECT_EQ(texture.Get(), nullptr);
+    }
+
+    // Test an error occurs if the descriptor mip level count isn't 1
+    TEST_P(VulkanImageWrappingValidationTests, InvalidMipLevelCount) {
+        defaultDescriptor.mipLevelCount = 2;
+
+        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
+                                device, &defaultDescriptor, defaultTexture.get(), {}, true, false));
+        EXPECT_EQ(texture.Get(), nullptr);
+    }
+
+    // Test an error occurs if the descriptor depth isn't 1
+    TEST_P(VulkanImageWrappingValidationTests, InvalidDepth) {
+        defaultDescriptor.size.depthOrArrayLayers = 2;
+
+        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
+                                device, &defaultDescriptor, defaultTexture.get(), {}, true, false));
+        EXPECT_EQ(texture.Get(), nullptr);
+    }
+
+    // Test an error occurs if the descriptor sample count isn't 1
+    TEST_P(VulkanImageWrappingValidationTests, InvalidSampleCount) {
+        defaultDescriptor.sampleCount = 4;
+
+        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
+                                device, &defaultDescriptor, defaultTexture.get(), {}, true, false));
+        EXPECT_EQ(texture.Get(), nullptr);
+    }
+
+    // Test an error occurs if we try to export the signal semaphore twice
+    TEST_P(VulkanImageWrappingValidationTests, DoubleSignalSemaphoreExport) {
+        wgpu::Texture texture =
+            WrapVulkanImage(device, &defaultDescriptor, defaultTexture.get(), {}, true, true);
+        ASSERT_NE(texture.Get(), nullptr);
+        IgnoreSignalSemaphore(texture);
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_DEVICE_ERROR(
+            bool success = mBackend->ExportImage(texture, VK_IMAGE_LAYOUT_GENERAL, &exportInfo));
+        ASSERT_FALSE(success);
+        ASSERT_EQ(exportInfo.semaphores.size(), 0u);
+    }
+
+    // Test an error occurs if we try to export the signal semaphore from a normal texture
+    TEST_P(VulkanImageWrappingValidationTests, NormalTextureSignalSemaphoreExport) {
+        wgpu::Texture texture = device.CreateTexture(&defaultDescriptor);
+        ASSERT_NE(texture.Get(), nullptr);
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_DEVICE_ERROR(
+            bool success = mBackend->ExportImage(texture, VK_IMAGE_LAYOUT_GENERAL, &exportInfo));
+        ASSERT_FALSE(success);
+        ASSERT_EQ(exportInfo.semaphores.size(), 0u);
+    }
+
+    // Test an error occurs if we try to export the signal semaphore from a destroyed texture
+    TEST_P(VulkanImageWrappingValidationTests, DestroyedTextureSignalSemaphoreExport) {
+        wgpu::Texture texture = device.CreateTexture(&defaultDescriptor);
+        ASSERT_NE(texture.Get(), nullptr);
+        texture.Destroy();
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_DEVICE_ERROR(
+            bool success = mBackend->ExportImage(texture, VK_IMAGE_LAYOUT_GENERAL, &exportInfo));
+        ASSERT_FALSE(success);
+        ASSERT_EQ(exportInfo.semaphores.size(), 0u);
+    }
+
+    // Fixture to test using external memory textures through different usages.
+    // These tests are skipped if the harness is using the wire.
+    class VulkanImageWrappingUsageTests : public VulkanImageWrappingTestBase {
+      public:
+        void SetUp() override {
+            VulkanImageWrappingTestBase::SetUp();
+            if (UsesWire()) {
+                return;
+            }
+
+            // Create another device based on the original
+            backendAdapter =
+                dawn::native::vulkan::ToBackend(dawn::native::FromAPI(device.Get())->GetAdapter());
+            deviceDescriptor.nextInChain = &togglesDesc;
+            togglesDesc.forceEnabledToggles = GetParam().forceEnabledWorkarounds.data();
+            togglesDesc.forceEnabledTogglesCount = GetParam().forceEnabledWorkarounds.size();
+            togglesDesc.forceDisabledToggles = GetParam().forceDisabledWorkarounds.data();
+            togglesDesc.forceDisabledTogglesCount = GetParam().forceDisabledWorkarounds.size();
+
+            secondDeviceVk =
+                dawn::native::vulkan::ToBackend(backendAdapter->APICreateDevice(&deviceDescriptor));
+            secondDevice = wgpu::Device::Acquire(dawn::native::ToAPI(secondDeviceVk));
+        }
+
+      protected:
+        dawn::native::vulkan::Adapter* backendAdapter;
+        dawn::native::DeviceDescriptor deviceDescriptor;
+        dawn::native::DawnTogglesDeviceDescriptor togglesDesc;
+
+        wgpu::Device secondDevice;
+        dawn::native::vulkan::Device* secondDeviceVk;
+
+        // Clear a texture on a given device
+        void ClearImage(wgpu::Device dawnDevice,
+                        wgpu::Texture wrappedTexture,
+                        wgpu::Color clearColor) {
+            wgpu::TextureView wrappedView = wrappedTexture.CreateView();
+
+            // Submit a clear operation
+            utils::ComboRenderPassDescriptor renderPassDescriptor({wrappedView}, {});
+            renderPassDescriptor.cColorAttachments[0].clearColor = clearColor;
+            renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+
+            wgpu::CommandEncoder encoder = dawnDevice.CreateCommandEncoder();
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDescriptor);
+            pass.EndPass();
+
+            wgpu::CommandBuffer commands = encoder.Finish();
+
+            wgpu::Queue queue = dawnDevice.GetQueue();
+            queue.Submit(1, &commands);
+        }
+
+        // Submits a 1x1x1 copy from source to destination
+        void SimpleCopyTextureToTexture(wgpu::Device dawnDevice,
+                                        wgpu::Queue dawnQueue,
+                                        wgpu::Texture source,
+                                        wgpu::Texture destination) {
+            wgpu::ImageCopyTexture copySrc = utils::CreateImageCopyTexture(source, 0, {0, 0, 0});
+            wgpu::ImageCopyTexture copyDst =
+                utils::CreateImageCopyTexture(destination, 0, {0, 0, 0});
+
+            wgpu::Extent3D copySize = {1, 1, 1};
+
+            wgpu::CommandEncoder encoder = dawnDevice.CreateCommandEncoder();
+            encoder.CopyTextureToTexture(&copySrc, &copyDst, &copySize);
+            wgpu::CommandBuffer commands = encoder.Finish();
+
+            dawnQueue.Submit(1, &commands);
+        }
+    };
+
+    // Clear an image in |secondDevice|
+    // Verify clear color is visible in |device|
+    TEST_P(VulkanImageWrappingUsageTests, ClearImageAcrossDevices) {
+        // Import the image on |secondDevice|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        // Clear |wrappedTexture| on |secondDevice|
+        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image to |device|, making sure we wait on signalFd
+        wgpu::Texture nextWrappedTexture = WrapVulkanImage(
+            device, &defaultDescriptor, defaultTexture.get(), std::move(exportInfo.semaphores),
+            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
+
+        // Verify |device| sees the changes from |secondDevice|
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
+
+        IgnoreSignalSemaphore(nextWrappedTexture);
+    }
+
+    // Clear an image in |secondDevice|
+    // Verify clear color is not visible in |device| if we import the texture as not cleared
+    TEST_P(VulkanImageWrappingUsageTests, UninitializedTextureIsCleared) {
+        // Import the image on |secondDevice|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        // Clear |wrappedTexture| on |secondDevice|
+        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image to |device|, making sure we wait on signalFd
+        wgpu::Texture nextWrappedTexture = WrapVulkanImage(
+            device, &defaultDescriptor, defaultTexture.get(), std::move(exportInfo.semaphores),
+            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout, false);
+
+        // Verify |device| doesn't see the changes from |secondDevice|
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(0, 0, 0, 0), nextWrappedTexture, 0, 0);
+
+        IgnoreSignalSemaphore(nextWrappedTexture);
+    }
+
+    // Import a texture into |secondDevice|
+    // Clear the texture on |secondDevice|
+    // Issue a copy of the imported texture inside |device| to |copyDstTexture|
+    // Verify the clear color from |secondDevice| is visible in |copyDstTexture|
+    TEST_P(VulkanImageWrappingUsageTests, CopyTextureToTextureSrcSync) {
+        // Import the image on |secondDevice|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        // Clear |wrappedTexture| on |secondDevice|
+        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image to |device|, making sure we wait on |signalFd|
+        wgpu::Texture deviceWrappedTexture = WrapVulkanImage(
+            device, &defaultDescriptor, defaultTexture.get(), std::move(exportInfo.semaphores),
+            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
+
+        // Create a second texture on |device|
+        wgpu::Texture copyDstTexture = device.CreateTexture(&defaultDescriptor);
+
+        // Copy |deviceWrappedTexture| into |copyDstTexture|
+        SimpleCopyTextureToTexture(device, queue, deviceWrappedTexture, copyDstTexture);
+
+        // Verify |copyDstTexture| sees changes from |secondDevice|
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), copyDstTexture, 0, 0);
+
+        IgnoreSignalSemaphore(deviceWrappedTexture);
+    }
+
+    // Import a texture into |device|
+    // Clear texture with color A on |device|
+    // Import same texture into |secondDevice|, waiting on the copy signal
+    // Clear the new texture with color B on |secondDevice|
+    // Copy color B using Texture to Texture copy on |secondDevice|
+    // Import texture back into |device|, waiting on color B signal
+    // Verify texture contains color B
+    // If texture destination isn't synchronized, |secondDevice| could copy color B
+    // into the texture first, then |device| writes color A
+    TEST_P(VulkanImageWrappingUsageTests, CopyTextureToTextureDstSync) {
+        // Import the image on |device|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(device, &defaultDescriptor, defaultTexture.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        // Clear |wrappedTexture| on |device|
+        ClearImage(device, wrappedTexture, {5 / 255.0f, 6 / 255.0f, 7 / 255.0f, 8 / 255.0f});
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image to |secondDevice|, making sure we wait on |signalFd|
+        wgpu::Texture secondDeviceWrappedTexture =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture.get(),
+                            std::move(exportInfo.semaphores), exportInfo.releasedOldLayout,
+                            exportInfo.releasedNewLayout);
+
+        // Create a texture with color B on |secondDevice|
+        wgpu::Texture copySrcTexture = secondDevice.CreateTexture(&defaultDescriptor);
+        ClearImage(secondDevice, copySrcTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+        // Copy color B on |secondDevice|
+        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
+        SimpleCopyTextureToTexture(secondDevice, secondDeviceQueue, copySrcTexture,
+                                   secondDeviceWrappedTexture);
+
+        // Re-import back into |device|, waiting on |secondDevice|'s signal
+        ExternalImageExportInfoVkForTesting secondExportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(secondDeviceWrappedTexture,
+                                          VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &secondExportInfo));
+
+        wgpu::Texture nextWrappedTexture =
+            WrapVulkanImage(device, &defaultDescriptor, defaultTexture.get(),
+                            std::move(secondExportInfo.semaphores),
+                            secondExportInfo.releasedOldLayout, secondExportInfo.releasedNewLayout);
+
+        // Verify |nextWrappedTexture| contains the color from our copy
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
+
+        IgnoreSignalSemaphore(nextWrappedTexture);
+    }
+
+    // Import a texture from |secondDevice|
+    // Clear the texture on |secondDevice|
+    // Issue a copy of the imported texture inside |device| to |copyDstBuffer|
+    // Verify the clear color from |secondDevice| is visible in |copyDstBuffer|
+    TEST_P(VulkanImageWrappingUsageTests, CopyTextureToBufferSrcSync) {
+        // Import the image on |secondDevice|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        // Clear |wrappedTexture| on |secondDevice|
+        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image to |device|, making sure we wait on |signalFd|
+        wgpu::Texture deviceWrappedTexture = WrapVulkanImage(
+            device, &defaultDescriptor, defaultTexture.get(), std::move(exportInfo.semaphores),
+            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
+
+        // Create a destination buffer on |device|
+        wgpu::BufferDescriptor bufferDesc;
+        bufferDesc.size = 4;
+        bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc;
+        wgpu::Buffer copyDstBuffer = device.CreateBuffer(&bufferDesc);
+
+        // Copy |deviceWrappedTexture| into |copyDstBuffer|
+        wgpu::ImageCopyTexture copySrc =
+            utils::CreateImageCopyTexture(deviceWrappedTexture, 0, {0, 0, 0});
+        wgpu::ImageCopyBuffer copyDst = utils::CreateImageCopyBuffer(copyDstBuffer, 0, 256);
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyTextureToBuffer(&copySrc, &copyDst, &copySize);
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+
+        // Verify |copyDstBuffer| sees changes from |secondDevice|
+        uint32_t expected = 0x04030201;
+        EXPECT_BUFFER_U32_EQ(expected, copyDstBuffer, 0);
+
+        IgnoreSignalSemaphore(deviceWrappedTexture);
+    }
+
+    // Import a texture into |device|
+    // Clear texture with color A on |device|
+    // Import same texture into |secondDevice|, waiting on the copy signal
+    // Copy color B using Buffer to Texture copy on |secondDevice|
+    // Import texture back into |device|, waiting on color B signal
+    // Verify texture contains color B
+    // If texture destination isn't synchronized, |secondDevice| could copy color B
+    // into the texture first, then |device| writes color A
+    TEST_P(VulkanImageWrappingUsageTests, CopyBufferToTextureDstSync) {
+        // Import the image on |device|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(device, &defaultDescriptor, defaultTexture.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        // Clear |wrappedTexture| on |device|
+        ClearImage(device, wrappedTexture, {5 / 255.0f, 6 / 255.0f, 7 / 255.0f, 8 / 255.0f});
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image to |secondDevice|, making sure we wait on |signalFd|
+        wgpu::Texture secondDeviceWrappedTexture =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture.get(),
+                            std::move(exportInfo.semaphores), exportInfo.releasedOldLayout,
+                            exportInfo.releasedNewLayout);
+
+        // Copy color B on |secondDevice|
+        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
+
+        // Create a buffer on |secondDevice|
+        wgpu::Buffer copySrcBuffer =
+            utils::CreateBufferFromData(secondDevice, wgpu::BufferUsage::CopySrc, {0x04030201});
+
+        // Copy |copySrcBuffer| into |secondDeviceWrappedTexture|
+        wgpu::ImageCopyBuffer copySrc = utils::CreateImageCopyBuffer(copySrcBuffer, 0, 256);
+        wgpu::ImageCopyTexture copyDst =
+            utils::CreateImageCopyTexture(secondDeviceWrappedTexture, 0, {0, 0, 0});
+
+        wgpu::Extent3D copySize = {1, 1, 1};
+
+        wgpu::CommandEncoder encoder = secondDevice.CreateCommandEncoder();
+        encoder.CopyBufferToTexture(&copySrc, &copyDst, &copySize);
+        wgpu::CommandBuffer commands = encoder.Finish();
+        secondDeviceQueue.Submit(1, &commands);
+
+        // Re-import back into |device|, waiting on |secondDevice|'s signal
+        ExternalImageExportInfoVkForTesting secondExportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(secondDeviceWrappedTexture,
+                                          VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &secondExportInfo));
+
+        wgpu::Texture nextWrappedTexture =
+            WrapVulkanImage(device, &defaultDescriptor, defaultTexture.get(),
+                            std::move(secondExportInfo.semaphores),
+                            secondExportInfo.releasedOldLayout, secondExportInfo.releasedNewLayout);
+
+        // Verify |nextWrappedTexture| contains the color from our copy
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
+
+        IgnoreSignalSemaphore(nextWrappedTexture);
+    }
+
+    // Import a texture from |secondDevice|
+    // Clear the texture on |secondDevice|
+    // Issue a copy of the imported texture inside |device| to |copyDstTexture|
+    // Issue second copy to |secondCopyDstTexture|
+    // Verify the clear color from |secondDevice| is visible in both copies
+    TEST_P(VulkanImageWrappingUsageTests, DoubleTextureUsage) {
+        // Import the image on |secondDevice|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultTexture.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        // Clear |wrappedTexture| on |secondDevice|
+        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image to |device|, making sure we wait on |signalFd|
+        wgpu::Texture deviceWrappedTexture = WrapVulkanImage(
+            device, &defaultDescriptor, defaultTexture.get(), std::move(exportInfo.semaphores),
+            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
+
+        // Create a second texture on |device|
+        wgpu::Texture copyDstTexture = device.CreateTexture(&defaultDescriptor);
+
+        // Create a third texture on |device|
+        wgpu::Texture secondCopyDstTexture = device.CreateTexture(&defaultDescriptor);
+
+        // Copy |deviceWrappedTexture| into |copyDstTexture|
+        SimpleCopyTextureToTexture(device, queue, deviceWrappedTexture, copyDstTexture);
+
+        // Copy |deviceWrappedTexture| into |secondCopyDstTexture|
+        SimpleCopyTextureToTexture(device, queue, deviceWrappedTexture, secondCopyDstTexture);
+
+        // Verify |copyDstTexture| sees changes from |secondDevice|
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), copyDstTexture, 0, 0);
+
+        // Verify |secondCopyDstTexture| sees changes from |secondDevice|
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), secondCopyDstTexture, 0, 0);
+
+        IgnoreSignalSemaphore(deviceWrappedTexture);
+    }
+
+    // Tex A on device 3 (external export)
+    // Tex B on device 2 (external export)
+    // Tex C on device 1 (external export)
+    // Clear color for A on device 3
+    // Copy A->B on device 3
+    // Copy B->C on device 2 (wait on B from previous op)
+    // Copy C->D on device 1 (wait on C from previous op)
+    // Verify D has same color as A
+    TEST_P(VulkanImageWrappingUsageTests, ChainTextureCopy) {
+        // device 1 = |device|
+        // device 2 = |secondDevice|
+        // Create device 3
+        dawn::native::vulkan::Device* thirdDeviceVk =
+            dawn::native::vulkan::ToBackend(backendAdapter->APICreateDevice(&deviceDescriptor));
+        wgpu::Device thirdDevice = wgpu::Device::Acquire(dawn::native::ToAPI(thirdDeviceVk));
+
+        // Make queue for device 2 and 3
+        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
+        wgpu::Queue thirdDeviceQueue = thirdDevice.GetQueue();
+
+        // Create textures A, B, C
+        std::unique_ptr<ExternalTexture> textureA =
+            mBackend->CreateTexture(1, 1, wgpu::TextureFormat::RGBA8Unorm, defaultDescriptor.usage);
+        std::unique_ptr<ExternalTexture> textureB =
+            mBackend->CreateTexture(1, 1, wgpu::TextureFormat::RGBA8Unorm, defaultDescriptor.usage);
+        std::unique_ptr<ExternalTexture> textureC =
+            mBackend->CreateTexture(1, 1, wgpu::TextureFormat::RGBA8Unorm, defaultDescriptor.usage);
+
+        // Import TexA, TexB on device 3
+        wgpu::Texture wrappedTexADevice3 =
+            WrapVulkanImage(thirdDevice, &defaultDescriptor, textureA.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+        wgpu::Texture wrappedTexBDevice3 =
+            WrapVulkanImage(thirdDevice, &defaultDescriptor, textureB.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
+
+        // Clear TexA
+        ClearImage(thirdDevice, wrappedTexADevice3,
+                   {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+        // Copy A->B
+        SimpleCopyTextureToTexture(thirdDevice, thirdDeviceQueue, wrappedTexADevice3,
+                                   wrappedTexBDevice3);
+
+        ExternalImageExportInfoVkForTesting exportInfoTexBDevice3;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexBDevice3, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfoTexBDevice3));
+        IgnoreSignalSemaphore(wrappedTexADevice3);
+
+        // Import TexB, TexC on device 2
+        wgpu::Texture wrappedTexBDevice2 = WrapVulkanImage(
+            secondDevice, &defaultDescriptor, textureB.get(),
+            std::move(exportInfoTexBDevice3.semaphores), exportInfoTexBDevice3.releasedOldLayout,
+            exportInfoTexBDevice3.releasedNewLayout);
+
+        wgpu::Texture wrappedTexCDevice2 =
+            WrapVulkanImage(secondDevice, &defaultDescriptor, textureC.get(), {},
+                            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
+
+        // Copy B->C on device 2
+        SimpleCopyTextureToTexture(secondDevice, secondDeviceQueue, wrappedTexBDevice2,
+                                   wrappedTexCDevice2);
+
+        ExternalImageExportInfoVkForTesting exportInfoTexCDevice2;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexCDevice2, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfoTexCDevice2));
+        IgnoreSignalSemaphore(wrappedTexBDevice2);
+
+        // Import TexC on device 1
+        wgpu::Texture wrappedTexCDevice1 = WrapVulkanImage(
+            device, &defaultDescriptor, textureC.get(), std::move(exportInfoTexCDevice2.semaphores),
+            exportInfoTexCDevice2.releasedOldLayout, exportInfoTexCDevice2.releasedNewLayout);
+
+        // Create TexD on device 1
+        wgpu::Texture texD = device.CreateTexture(&defaultDescriptor);
+
+        // Copy C->D on device 1
+        SimpleCopyTextureToTexture(device, queue, wrappedTexCDevice1, texD);
+
+        // Verify D matches clear color
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), texD, 0, 0);
+
+        IgnoreSignalSemaphore(wrappedTexCDevice1);
+    }
+
+    // Tests a larger image is preserved when importing
+    TEST_P(VulkanImageWrappingUsageTests, LargerImage) {
+        wgpu::TextureDescriptor descriptor;
+        descriptor.dimension = wgpu::TextureDimension::e2D;
+        descriptor.size.width = 640;
+        descriptor.size.height = 480;
+        descriptor.size.depthOrArrayLayers = 1;
+        descriptor.sampleCount = 1;
+        descriptor.format = wgpu::TextureFormat::RGBA8Unorm;
+        descriptor.mipLevelCount = 1;
+        descriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::CopySrc;
+
+        // Fill memory with textures
+        std::vector<wgpu::Texture> textures;
+        for (int i = 0; i < 20; i++) {
+            textures.push_back(device.CreateTexture(&descriptor));
+        }
+
+        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
+
+        // Make an image on |secondDevice|
+        std::unique_ptr<ExternalTexture> texture = mBackend->CreateTexture(
+            descriptor.size.width, descriptor.size.height, descriptor.format, descriptor.usage);
+
+        // Import the image on |secondDevice|
+        wgpu::Texture wrappedTexture =
+            WrapVulkanImage(secondDevice, &descriptor, texture.get(), {}, VK_IMAGE_LAYOUT_UNDEFINED,
+                            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
+
+        // Draw a non-trivial picture
+        uint32_t width = 640, height = 480, pixelSize = 4;
+        uint32_t bytesPerRow = Align(width * pixelSize, kTextureBytesPerRowAlignment);
+        std::vector<unsigned char> data(bytesPerRow * (height - 1) + width * pixelSize);
+
+        for (uint32_t row = 0; row < height; row++) {
+            for (uint32_t col = 0; col < width; col++) {
+                float normRow = static_cast<float>(row) / height;
+                float normCol = static_cast<float>(col) / width;
+                float dist = sqrt(normRow * normRow + normCol * normCol) * 3;
+                dist = dist - static_cast<int>(dist);
+                data[4 * (row * width + col)] = static_cast<unsigned char>(dist * 255);
+                data[4 * (row * width + col) + 1] = static_cast<unsigned char>(dist * 255);
+                data[4 * (row * width + col) + 2] = static_cast<unsigned char>(dist * 255);
+                data[4 * (row * width + col) + 3] = 255;
+            }
+        }
+
+        // Write the picture
+        {
+            wgpu::Buffer copySrcBuffer = utils::CreateBufferFromData(
+                secondDevice, data.data(), data.size(), wgpu::BufferUsage::CopySrc);
+            wgpu::ImageCopyBuffer copySrc =
+                utils::CreateImageCopyBuffer(copySrcBuffer, 0, bytesPerRow);
+            wgpu::ImageCopyTexture copyDst =
+                utils::CreateImageCopyTexture(wrappedTexture, 0, {0, 0, 0});
+            wgpu::Extent3D copySize = {width, height, 1};
+
+            wgpu::CommandEncoder encoder = secondDevice.CreateCommandEncoder();
+            encoder.CopyBufferToTexture(&copySrc, &copyDst, &copySize);
+            wgpu::CommandBuffer commands = encoder.Finish();
+            secondDeviceQueue.Submit(1, &commands);
+        }
+        ExternalImageExportInfoVkForTesting exportInfo;
+        ASSERT_TRUE(mBackend->ExportImage(wrappedTexture, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                          &exportInfo));
+
+        // Import the image on |device|
+        wgpu::Texture nextWrappedTexture =
+            WrapVulkanImage(device, &descriptor, texture.get(), std::move(exportInfo.semaphores),
+                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
+
+        // Copy the image into a buffer for comparison
+        wgpu::BufferDescriptor copyDesc;
+        copyDesc.size = data.size();
+        copyDesc.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
+        wgpu::Buffer copyDstBuffer = device.CreateBuffer(&copyDesc);
+        {
+            wgpu::ImageCopyTexture copySrc =
+                utils::CreateImageCopyTexture(nextWrappedTexture, 0, {0, 0, 0});
+            wgpu::ImageCopyBuffer copyDst =
+                utils::CreateImageCopyBuffer(copyDstBuffer, 0, bytesPerRow);
+
+            wgpu::Extent3D copySize = {width, height, 1};
+
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyTextureToBuffer(&copySrc, &copyDst, &copySize);
+            wgpu::CommandBuffer commands = encoder.Finish();
+            queue.Submit(1, &commands);
+        }
+
+        // Check the image is not corrupted on |device|
+        EXPECT_BUFFER_U32_RANGE_EQ(reinterpret_cast<uint32_t*>(data.data()), copyDstBuffer, 0,
+                                   data.size() / 4);
+
+        IgnoreSignalSemaphore(nextWrappedTexture);
+    }
+
+    DAWN_INSTANTIATE_TEST(VulkanImageWrappingValidationTests, VulkanBackend());
+    DAWN_INSTANTIATE_TEST(VulkanImageWrappingUsageTests, VulkanBackend());
+
+}}  // namespace dawn::native::vulkan
diff --git a/src/tests/white_box/VulkanImageWrappingTests.h b/src/tests/white_box/VulkanImageWrappingTests.h
new file mode 100644
index 0000000..6c2deb7
--- /dev/null
+++ b/src/tests/white_box/VulkanImageWrappingTests.h
@@ -0,0 +1,76 @@
+// Copyright 2021 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.
+
+#ifndef TESTS_VULKANIMAGEWRAPPINGTESTS_H_
+#define TESTS_VULKANIMAGEWRAPPINGTESTS_H_
+
+// This must be above all other includes otherwise VulkanBackend.h includes vulkan.h before we had
+// time to wrap it with vulkan_platform.h
+#include "common/vulkan_platform.h"
+
+#include "common/NonCopyable.h"
+#include "dawn/webgpu_cpp.h"
+#include "dawn_native/VulkanBackend.h"
+
+#include <memory>
+#include <vector>
+
+namespace dawn::native::vulkan {
+
+    struct ExternalImageDescriptorVkForTesting;
+    struct ExternalImageExportInfoVkForTesting;
+
+    class VulkanImageWrappingTestBackend {
+      public:
+        static std::unique_ptr<VulkanImageWrappingTestBackend> Create(const wgpu::Device& device);
+        virtual ~VulkanImageWrappingTestBackend() = default;
+
+        class ExternalTexture : NonCopyable {
+          public:
+            virtual ~ExternalTexture() = default;
+        };
+        class ExternalSemaphore : NonCopyable {
+          public:
+            virtual ~ExternalSemaphore() = default;
+        };
+
+        virtual std::unique_ptr<ExternalTexture> CreateTexture(uint32_t width,
+                                                               uint32_t height,
+                                                               wgpu::TextureFormat format,
+                                                               wgpu::TextureUsage usage) = 0;
+        virtual wgpu::Texture WrapImage(
+            const wgpu::Device& device,
+            const ExternalTexture* texture,
+            const ExternalImageDescriptorVkForTesting& descriptor,
+            std::vector<std::unique_ptr<ExternalSemaphore>> semaphores) = 0;
+
+        virtual bool ExportImage(const wgpu::Texture& texture,
+                                 VkImageLayout layout,
+                                 ExternalImageExportInfoVkForTesting* exportInfo) = 0;
+    };
+
+    struct ExternalImageDescriptorVkForTesting : public ExternalImageDescriptorVk {
+      public:
+        ExternalImageDescriptorVkForTesting();
+    };
+
+    struct ExternalImageExportInfoVkForTesting : public ExternalImageExportInfoVk {
+      public:
+        ExternalImageExportInfoVkForTesting();
+        std::vector<std::unique_ptr<VulkanImageWrappingTestBackend::ExternalSemaphore>> semaphores;
+    };
+
+}  // namespace dawn::native::vulkan
+
+#endif  // TESTS_VULKANIMAGEWRAPPINGTESTS_H_
diff --git a/src/tests/white_box/VulkanImageWrappingTestsOpaqueFD.cpp b/src/tests/white_box/VulkanImageWrappingTestsOpaqueFD.cpp
deleted file mode 100644
index cb045d5..0000000
--- a/src/tests/white_box/VulkanImageWrappingTestsOpaqueFD.cpp
+++ /dev/null
@@ -1,1019 +0,0 @@
-// Copyright 2019 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 "common/Math.h"
-#include "tests/DawnTest.h"
-
-#include "common/vulkan_platform.h"
-#include "dawn_native/VulkanBackend.h"
-#include "dawn_native/vulkan/AdapterVk.h"
-#include "dawn_native/vulkan/DeviceVk.h"
-#include "dawn_native/vulkan/FencedDeleter.h"
-#include "dawn_native/vulkan/ResourceMemoryAllocatorVk.h"
-#include "dawn_native/vulkan/TextureVk.h"
-#include "utils/SystemUtils.h"
-#include "utils/WGPUHelpers.h"
-
-namespace dawn::native::vulkan {
-
-    namespace {
-
-        class VulkanImageWrappingTestBase : public DawnTest {
-          protected:
-            std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
-                return {wgpu::FeatureName::DawnInternalUsages};
-            }
-
-          public:
-            void SetUp() override {
-                DawnTest::SetUp();
-                DAWN_TEST_UNSUPPORTED_IF(UsesWire());
-
-                deviceVk = dawn::native::vulkan::ToBackend(dawn::native::FromAPI(device.Get()));
-            }
-
-            // Creates a VkImage with external memory
-            ::VkResult CreateImage(dawn::native::vulkan::Device* deviceVk,
-                                   uint32_t width,
-                                   uint32_t height,
-                                   VkFormat format,
-                                   VkImage* image) {
-                VkExternalMemoryImageCreateInfoKHR externalInfo;
-                externalInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO_KHR;
-                externalInfo.pNext = nullptr;
-                externalInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
-
-                auto usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
-                             VK_IMAGE_USAGE_TRANSFER_DST_BIT;
-
-                VkImageCreateInfo createInfo;
-                createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
-                createInfo.pNext = &externalInfo;
-                createInfo.flags = VK_IMAGE_CREATE_ALIAS_BIT_KHR;
-                createInfo.imageType = VK_IMAGE_TYPE_2D;
-                createInfo.format = format;
-                createInfo.extent = {width, height, 1};
-                createInfo.mipLevels = 1;
-                createInfo.arrayLayers = 1;
-                createInfo.samples = VK_SAMPLE_COUNT_1_BIT;
-                createInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
-                createInfo.usage = usage;
-                createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
-                createInfo.queueFamilyIndexCount = 0;
-                createInfo.pQueueFamilyIndices = nullptr;
-                createInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
-
-                return deviceVk->fn.CreateImage(deviceVk->GetVkDevice(), &createInfo, nullptr,
-                                                &**image);
-            }
-
-            // Allocates memory for an image
-            ::VkResult AllocateMemory(dawn::native::vulkan::Device* deviceVk,
-                                      VkImage handle,
-                                      VkDeviceMemory* allocation,
-                                      VkDeviceSize* allocationSize,
-                                      uint32_t* memoryTypeIndex) {
-                // Create the image memory and associate it with the container
-                VkMemoryRequirements requirements;
-                deviceVk->fn.GetImageMemoryRequirements(deviceVk->GetVkDevice(), handle,
-                                                        &requirements);
-
-                // Import memory from file descriptor
-                VkExportMemoryAllocateInfoKHR externalInfo;
-                externalInfo.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO_KHR;
-                externalInfo.pNext = nullptr;
-                externalInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
-
-                int bestType = deviceVk->GetResourceMemoryAllocator()->FindBestTypeIndex(
-                    requirements, MemoryKind::Opaque);
-                VkMemoryAllocateInfo allocateInfo;
-                allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
-                allocateInfo.pNext = &externalInfo;
-                allocateInfo.allocationSize = requirements.size;
-                allocateInfo.memoryTypeIndex = static_cast<uint32_t>(bestType);
-
-                *allocationSize = allocateInfo.allocationSize;
-                *memoryTypeIndex = allocateInfo.memoryTypeIndex;
-
-                return deviceVk->fn.AllocateMemory(deviceVk->GetVkDevice(), &allocateInfo, nullptr,
-                                                   &**allocation);
-            }
-
-            // Binds memory to an image
-            ::VkResult BindMemory(dawn::native::vulkan::Device* deviceVk,
-                                  VkImage handle,
-                                  VkDeviceMemory* memory) {
-                return deviceVk->fn.BindImageMemory(deviceVk->GetVkDevice(), handle, *memory, 0);
-            }
-
-            // Extracts a file descriptor representing memory on a device
-            int GetMemoryFd(dawn::native::vulkan::Device* deviceVk, VkDeviceMemory memory) {
-                VkMemoryGetFdInfoKHR getFdInfo;
-                getFdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR;
-                getFdInfo.pNext = nullptr;
-                getFdInfo.memory = memory;
-                getFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
-
-                int memoryFd = -1;
-                deviceVk->fn.GetMemoryFdKHR(deviceVk->GetVkDevice(), &getFdInfo, &memoryFd);
-
-                EXPECT_GE(memoryFd, 0) << "Failed to get file descriptor for external memory";
-                return memoryFd;
-            }
-
-            // Prepares and exports memory for an image on a given device
-            void CreateBindExportImage(dawn::native::vulkan::Device* deviceVk,
-                                       uint32_t width,
-                                       uint32_t height,
-                                       VkFormat format,
-                                       VkImage* handle,
-                                       VkDeviceMemory* allocation,
-                                       VkDeviceSize* allocationSize,
-                                       uint32_t* memoryTypeIndex,
-                                       int* memoryFd) {
-                ::VkResult result = CreateImage(deviceVk, width, height, format, handle);
-                EXPECT_EQ(result, VK_SUCCESS) << "Failed to create external image";
-
-                ::VkResult resultBool =
-                    AllocateMemory(deviceVk, *handle, allocation, allocationSize, memoryTypeIndex);
-                EXPECT_EQ(resultBool, VK_SUCCESS) << "Failed to allocate external memory";
-
-                result = BindMemory(deviceVk, *handle, allocation);
-                EXPECT_EQ(result, VK_SUCCESS) << "Failed to bind image memory";
-
-                *memoryFd = GetMemoryFd(deviceVk, *allocation);
-            }
-
-            // Wraps a vulkan image from external memory
-            wgpu::Texture WrapVulkanImage(wgpu::Device dawnDevice,
-                                          const wgpu::TextureDescriptor* textureDescriptor,
-                                          int memoryFd,
-                                          VkDeviceSize allocationSize,
-                                          uint32_t memoryTypeIndex,
-                                          std::vector<int> waitFDs,
-                                          bool isInitialized = true,
-                                          bool expectValid = true) {
-                dawn::native::vulkan::ExternalImageDescriptorOpaqueFD descriptor;
-                return WrapVulkanImage(dawnDevice, textureDescriptor, memoryFd, allocationSize,
-                                       memoryTypeIndex, waitFDs, descriptor.releasedOldLayout,
-                                       descriptor.releasedNewLayout, isInitialized, expectValid);
-            }
-
-            wgpu::Texture WrapVulkanImage(wgpu::Device dawnDevice,
-                                          const wgpu::TextureDescriptor* textureDescriptor,
-                                          int memoryFd,
-                                          VkDeviceSize allocationSize,
-                                          uint32_t memoryTypeIndex,
-                                          std::vector<int> waitFDs,
-                                          VkImageLayout releasedOldLayout,
-                                          VkImageLayout releasedNewLayout,
-                                          bool isInitialized = true,
-                                          bool expectValid = true) {
-                dawn::native::vulkan::ExternalImageDescriptorOpaqueFD descriptor;
-                descriptor.cTextureDescriptor =
-                    reinterpret_cast<const WGPUTextureDescriptor*>(textureDescriptor);
-                descriptor.isInitialized = isInitialized;
-                descriptor.allocationSize = allocationSize;
-                descriptor.memoryTypeIndex = memoryTypeIndex;
-                descriptor.memoryFD = memoryFd;
-                descriptor.waitFDs = waitFDs;
-                descriptor.releasedOldLayout = releasedOldLayout;
-                descriptor.releasedNewLayout = releasedNewLayout;
-
-                WGPUTexture texture =
-                    dawn::native::vulkan::WrapVulkanImage(dawnDevice.Get(), &descriptor);
-
-                if (expectValid) {
-                    EXPECT_NE(texture, nullptr) << "Failed to wrap image, are external memory / "
-                                                   "semaphore extensions supported?";
-                } else {
-                    EXPECT_EQ(texture, nullptr);
-                }
-
-                return wgpu::Texture::Acquire(texture);
-            }
-
-            // Exports the signal from a wrapped texture and ignores it
-            // We have to export the signal before destroying the wrapped texture else it's an
-            // assertion failure
-            void IgnoreSignalSemaphore(wgpu::Texture wrappedTexture) {
-                dawn::native::vulkan::ExternalImageExportInfoOpaqueFD info;
-                dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                        VK_IMAGE_LAYOUT_GENERAL, &info);
-                for (int handle : info.semaphoreHandles) {
-                    ASSERT_NE(handle, -1);
-                    close(handle);
-                }
-            }
-
-          protected:
-            dawn::native::vulkan::Device* deviceVk;
-        };
-
-    }  // anonymous namespace
-
-    class VulkanImageWrappingValidationTests : public VulkanImageWrappingTestBase {
-      public:
-        void SetUp() override {
-            VulkanImageWrappingTestBase::SetUp();
-            DAWN_TEST_UNSUPPORTED_IF(UsesWire());
-
-            CreateBindExportImage(deviceVk, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, &defaultImage,
-                                  &defaultAllocation, &defaultAllocationSize,
-                                  &defaultMemoryTypeIndex, &defaultFd);
-            defaultDescriptor.dimension = wgpu::TextureDimension::e2D;
-            defaultDescriptor.format = wgpu::TextureFormat::RGBA8Unorm;
-            defaultDescriptor.size = {1, 1, 1};
-            defaultDescriptor.sampleCount = 1;
-            defaultDescriptor.mipLevelCount = 1;
-            defaultDescriptor.usage = wgpu::TextureUsage::RenderAttachment |
-                                      wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst;
-        }
-
-        void TearDown() override {
-            if (UsesWire()) {
-                VulkanImageWrappingTestBase::TearDown();
-                return;
-            }
-
-            deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultImage);
-            deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultAllocation);
-            VulkanImageWrappingTestBase::TearDown();
-        }
-
-      protected:
-        wgpu::TextureDescriptor defaultDescriptor;
-        VkImage defaultImage;
-        VkDeviceMemory defaultAllocation;
-        VkDeviceSize defaultAllocationSize;
-        uint32_t defaultMemoryTypeIndex;
-        int defaultFd;
-    };
-
-    // Test no error occurs if the import is valid
-    TEST_P(VulkanImageWrappingValidationTests, SuccessfulImport) {
-        wgpu::Texture texture =
-            WrapVulkanImage(device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, true, true);
-        EXPECT_NE(texture.Get(), nullptr);
-        IgnoreSignalSemaphore(texture);
-    }
-
-    // Test no error occurs if the import is valid with DawnTextureInternalUsageDescriptor
-    TEST_P(VulkanImageWrappingValidationTests, SuccessfulImportWithInternalUsageDescriptor) {
-        wgpu::DawnTextureInternalUsageDescriptor internalDesc = {};
-        defaultDescriptor.nextInChain = &internalDesc;
-        internalDesc.internalUsage = wgpu::TextureUsage::CopySrc;
-        internalDesc.sType = wgpu::SType::DawnTextureInternalUsageDescriptor;
-
-        wgpu::Texture texture =
-            WrapVulkanImage(device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, true, true);
-        EXPECT_NE(texture.Get(), nullptr);
-        IgnoreSignalSemaphore(texture);
-    }
-
-    // Test an error occurs if an invalid sType is the nextInChain
-    TEST_P(VulkanImageWrappingValidationTests, InvalidTextureDescriptor) {
-        wgpu::ChainedStruct chainedDescriptor;
-        chainedDescriptor.sType = wgpu::SType::SurfaceDescriptorFromWindowsSwapChainPanel;
-        defaultDescriptor.nextInChain = &chainedDescriptor;
-
-        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
-                                device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                                defaultMemoryTypeIndex, {}, true, false));
-        EXPECT_EQ(texture.Get(), nullptr);
-    }
-
-    // Test an error occurs if the descriptor dimension isn't 2D
-    TEST_P(VulkanImageWrappingValidationTests, InvalidTextureDimension) {
-        defaultDescriptor.dimension = wgpu::TextureDimension::e1D;
-
-        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
-                                device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                                defaultMemoryTypeIndex, {}, true, false));
-        EXPECT_EQ(texture.Get(), nullptr);
-    }
-
-    // Test an error occurs if the descriptor mip level count isn't 1
-    TEST_P(VulkanImageWrappingValidationTests, InvalidMipLevelCount) {
-        defaultDescriptor.mipLevelCount = 2;
-
-        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
-                                device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                                defaultMemoryTypeIndex, {}, true, false));
-        EXPECT_EQ(texture.Get(), nullptr);
-    }
-
-    // Test an error occurs if the descriptor depth isn't 1
-    TEST_P(VulkanImageWrappingValidationTests, InvalidDepth) {
-        defaultDescriptor.size.depthOrArrayLayers = 2;
-
-        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
-                                device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                                defaultMemoryTypeIndex, {}, true, false));
-        EXPECT_EQ(texture.Get(), nullptr);
-    }
-
-    // Test an error occurs if the descriptor sample count isn't 1
-    TEST_P(VulkanImageWrappingValidationTests, InvalidSampleCount) {
-        defaultDescriptor.sampleCount = 4;
-
-        ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapVulkanImage(
-                                device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                                defaultMemoryTypeIndex, {}, true, false));
-        EXPECT_EQ(texture.Get(), nullptr);
-    }
-
-    // Test an error occurs if we try to export the signal semaphore twice
-    TEST_P(VulkanImageWrappingValidationTests, DoubleSignalSemaphoreExport) {
-        wgpu::Texture texture =
-            WrapVulkanImage(device, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, true, true);
-        ASSERT_NE(texture.Get(), nullptr);
-        IgnoreSignalSemaphore(texture);
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        ASSERT_DEVICE_ERROR(bool success = dawn::native::vulkan::ExportVulkanImage(
-                                texture.Get(), VK_IMAGE_LAYOUT_GENERAL, &exportInfo));
-        ASSERT_FALSE(success);
-    }
-
-    // Test an error occurs if we try to export the signal semaphore from a normal texture
-    TEST_P(VulkanImageWrappingValidationTests, NormalTextureSignalSemaphoreExport) {
-        wgpu::Texture texture = device.CreateTexture(&defaultDescriptor);
-        ASSERT_NE(texture.Get(), nullptr);
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        ASSERT_DEVICE_ERROR(bool success = dawn::native::vulkan::ExportVulkanImage(
-                                texture.Get(), VK_IMAGE_LAYOUT_GENERAL, &exportInfo));
-        ASSERT_FALSE(success);
-    }
-
-    // Test an error occurs if we try to export the signal semaphore from a destroyed texture
-    TEST_P(VulkanImageWrappingValidationTests, DestroyedTextureSignalSemaphoreExport) {
-        wgpu::Texture texture = device.CreateTexture(&defaultDescriptor);
-        ASSERT_NE(texture.Get(), nullptr);
-        texture.Destroy();
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        ASSERT_DEVICE_ERROR(bool success = dawn::native::vulkan::ExportVulkanImage(
-                                texture.Get(), VK_IMAGE_LAYOUT_GENERAL, &exportInfo));
-        ASSERT_FALSE(success);
-    }
-
-    // Fixture to test using external memory textures through different usages.
-    // These tests are skipped if the harness is using the wire.
-    class VulkanImageWrappingUsageTests : public VulkanImageWrappingTestBase {
-      public:
-        void SetUp() override {
-            VulkanImageWrappingTestBase::SetUp();
-            DAWN_TEST_UNSUPPORTED_IF(UsesWire());
-
-            // Create another device based on the original
-            backendAdapter = dawn::native::vulkan::ToBackend(deviceVk->GetAdapter());
-            deviceDescriptor.nextInChain = &togglesDesc;
-            togglesDesc.forceEnabledToggles = GetParam().forceEnabledWorkarounds.data();
-            togglesDesc.forceEnabledTogglesCount = GetParam().forceEnabledWorkarounds.size();
-            togglesDesc.forceDisabledToggles = GetParam().forceDisabledWorkarounds.data();
-            togglesDesc.forceDisabledTogglesCount = GetParam().forceDisabledWorkarounds.size();
-
-            secondDeviceVk =
-                dawn::native::vulkan::ToBackend(backendAdapter->APICreateDevice(&deviceDescriptor));
-            secondDevice = wgpu::Device::Acquire(dawn::native::ToAPI(secondDeviceVk));
-
-            CreateBindExportImage(deviceVk, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, &defaultImage,
-                                  &defaultAllocation, &defaultAllocationSize,
-                                  &defaultMemoryTypeIndex, &defaultFd);
-            defaultDescriptor.dimension = wgpu::TextureDimension::e2D;
-            defaultDescriptor.format = wgpu::TextureFormat::RGBA8Unorm;
-            defaultDescriptor.size = {1, 1, 1};
-            defaultDescriptor.sampleCount = 1;
-            defaultDescriptor.mipLevelCount = 1;
-            defaultDescriptor.usage = wgpu::TextureUsage::RenderAttachment |
-                                      wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst;
-        }
-
-        void TearDown() override {
-            if (UsesWire()) {
-                VulkanImageWrappingTestBase::TearDown();
-                return;
-            }
-
-            deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultImage);
-            deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultAllocation);
-            VulkanImageWrappingTestBase::TearDown();
-        }
-
-      protected:
-        wgpu::Device secondDevice;
-        dawn::native::vulkan::Device* secondDeviceVk;
-
-        dawn::native::vulkan::Adapter* backendAdapter;
-        dawn::native::DeviceDescriptor deviceDescriptor;
-        dawn::native::DawnTogglesDeviceDescriptor togglesDesc;
-
-        wgpu::TextureDescriptor defaultDescriptor;
-        VkImage defaultImage;
-        VkDeviceMemory defaultAllocation;
-        VkDeviceSize defaultAllocationSize;
-        uint32_t defaultMemoryTypeIndex;
-        int defaultFd;
-
-        // Clear a texture on a given device
-        void ClearImage(wgpu::Device dawnDevice,
-                        wgpu::Texture wrappedTexture,
-                        wgpu::Color clearColor) {
-            wgpu::TextureView wrappedView = wrappedTexture.CreateView();
-
-            // Submit a clear operation
-            utils::ComboRenderPassDescriptor renderPassDescriptor({wrappedView}, {});
-            renderPassDescriptor.cColorAttachments[0].clearColor = clearColor;
-
-            wgpu::CommandEncoder encoder = dawnDevice.CreateCommandEncoder();
-            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDescriptor);
-            pass.EndPass();
-
-            wgpu::CommandBuffer commands = encoder.Finish();
-
-            wgpu::Queue queue = dawnDevice.GetQueue();
-            queue.Submit(1, &commands);
-        }
-
-        // Submits a 1x1x1 copy from source to destination
-        void SimpleCopyTextureToTexture(wgpu::Device dawnDevice,
-                                        wgpu::Queue dawnQueue,
-                                        wgpu::Texture source,
-                                        wgpu::Texture destination) {
-            wgpu::ImageCopyTexture copySrc;
-            copySrc.texture = source;
-            copySrc.mipLevel = 0;
-            copySrc.origin = {0, 0, 0};
-
-            wgpu::ImageCopyTexture copyDst;
-            copyDst.texture = destination;
-            copyDst.mipLevel = 0;
-            copyDst.origin = {0, 0, 0};
-
-            wgpu::Extent3D copySize = {1, 1, 1};
-
-            wgpu::CommandEncoder encoder = dawnDevice.CreateCommandEncoder();
-            encoder.CopyTextureToTexture(&copySrc, &copyDst, &copySize);
-            wgpu::CommandBuffer commands = encoder.Finish();
-
-            dawnQueue.Submit(1, &commands);
-        }
-    };
-
-    // Clear an image in |secondDevice|
-    // Verify clear color is visible in |device|
-    TEST_P(VulkanImageWrappingUsageTests, ClearImageAcrossDevices) {
-        // Import the image on |secondDevice|
-        wgpu::Texture wrappedTexture =
-            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, VK_IMAGE_LAYOUT_UNDEFINED,
-                            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        // Clear |wrappedTexture| on |secondDevice|
-        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfo);
-
-        // Import the image to |device|, making sure we wait on signalFd
-        int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-        wgpu::Texture nextWrappedTexture =
-            WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, exportInfo.semaphoreHandles,
-                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
-
-        // Verify |device| sees the changes from |secondDevice|
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
-
-        IgnoreSignalSemaphore(nextWrappedTexture);
-    }
-
-    // Clear an image in |secondDevice|
-    // Verify clear color is not visible in |device| if we import the texture as not cleared
-    TEST_P(VulkanImageWrappingUsageTests, UninitializedTextureIsCleared) {
-        // Import the image on |secondDevice|
-        wgpu::Texture wrappedTexture =
-            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, VK_IMAGE_LAYOUT_UNDEFINED,
-                            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        // Clear |wrappedTexture| on |secondDevice|
-        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfo);
-
-        // Import the image to |device|, making sure we wait on the semaphore
-        int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-        wgpu::Texture nextWrappedTexture =
-            WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, exportInfo.semaphoreHandles,
-                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout, false);
-
-        // Verify |device| doesn't see the changes from |secondDevice|
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(0, 0, 0, 0), nextWrappedTexture, 0, 0);
-
-        IgnoreSignalSemaphore(nextWrappedTexture);
-    }
-
-    // Import a texture into |secondDevice|
-    // Issue a copy of the imported texture inside |device| to |copyDstTexture|
-    // Verify the clear color from |secondDevice| is visible in |copyDstTexture|
-    TEST_P(VulkanImageWrappingUsageTests, CopyTextureToTextureSrcSync) {
-        // Import the image on |secondDevice|
-        wgpu::Texture wrappedTexture =
-            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, VK_IMAGE_LAYOUT_UNDEFINED,
-                            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        // Clear |wrappedTexture| on |secondDevice|
-        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfo);
-
-        // Import the image to |device|, making sure we wait on the semaphore
-        int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-        wgpu::Texture deviceWrappedTexture =
-            WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, exportInfo.semaphoreHandles,
-                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
-
-        // Create a second texture on |device|
-        wgpu::Texture copyDstTexture = device.CreateTexture(&defaultDescriptor);
-
-        // Copy |deviceWrappedTexture| into |copyDstTexture|
-        SimpleCopyTextureToTexture(device, queue, deviceWrappedTexture, copyDstTexture);
-
-        // Verify |copyDstTexture| sees changes from |secondDevice|
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), copyDstTexture, 0, 0);
-
-        IgnoreSignalSemaphore(deviceWrappedTexture);
-    }
-
-    // Import a texture into |device|
-    // Copy color A into texture on |device|
-    // Import same texture into |secondDevice|, waiting on the copy signal
-    // Copy color B using Texture to Texture copy on |secondDevice|
-    // Import texture back into |device|, waiting on color B signal
-    // Verify texture contains color B
-    // If texture destination isn't synchronized, |secondDevice| could copy color B
-    // into the texture first, then |device| writes color A
-    TEST_P(VulkanImageWrappingUsageTests, CopyTextureToTextureDstSync) {
-        // Import the image on |device|
-        wgpu::Texture wrappedTexture = WrapVulkanImage(
-            device, &defaultDescriptor, defaultFd, defaultAllocationSize, defaultMemoryTypeIndex,
-            {}, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        // Clear |wrappedTexture| on |device|
-        ClearImage(device, wrappedTexture, {5 / 255.0f, 6 / 255.0f, 7 / 255.0f, 8 / 255.0f});
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &exportInfo);
-
-        // Import the image to |secondDevice|, making sure we wait on the semaphore
-        int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-        wgpu::Texture secondDeviceWrappedTexture =
-            WrapVulkanImage(secondDevice, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, exportInfo.semaphoreHandles,
-                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
-
-        // Create a texture with color B on |secondDevice|
-        wgpu::Texture copySrcTexture = secondDevice.CreateTexture(&defaultDescriptor);
-        ClearImage(secondDevice, copySrcTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
-
-        // Copy color B on |secondDevice|
-        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
-        SimpleCopyTextureToTexture(secondDevice, secondDeviceQueue, copySrcTexture,
-                                   secondDeviceWrappedTexture);
-
-        // Re-import back into |device|, waiting on |secondDevice|'s signal
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD secondExportInfo;
-        dawn::native::vulkan::ExportVulkanImage(secondDeviceWrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-                                                &secondExportInfo);
-        memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-
-        wgpu::Texture nextWrappedTexture =
-            WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, secondExportInfo.semaphoreHandles,
-                            secondExportInfo.releasedOldLayout, secondExportInfo.releasedNewLayout);
-
-        // Verify |nextWrappedTexture| contains the color from our copy
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
-
-        IgnoreSignalSemaphore(nextWrappedTexture);
-    }
-
-    // Import a texture from |secondDevice|
-    // Issue a copy of the imported texture inside |device| to |copyDstBuffer|
-    // Verify the clear color from |secondDevice| is visible in |copyDstBuffer|
-    TEST_P(VulkanImageWrappingUsageTests, CopyTextureToBufferSrcSync) {
-        // Import the image on |secondDevice|
-        wgpu::Texture wrappedTexture =
-            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, VK_IMAGE_LAYOUT_UNDEFINED,
-                            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        // Clear |wrappedTexture| on |secondDevice|
-        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfo);
-
-        // Import the image to |device|, making sure we wait on the semaphore
-        int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-        wgpu::Texture deviceWrappedTexture =
-            WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, exportInfo.semaphoreHandles,
-                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
-
-        // Create a destination buffer on |device|
-        wgpu::BufferDescriptor bufferDesc;
-        bufferDesc.size = 4;
-        bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc;
-        wgpu::Buffer copyDstBuffer = device.CreateBuffer(&bufferDesc);
-
-        // Copy |deviceWrappedTexture| into |copyDstBuffer|
-        wgpu::ImageCopyTexture copySrc =
-            utils::CreateImageCopyTexture(deviceWrappedTexture, 0, {0, 0, 0});
-        wgpu::ImageCopyBuffer copyDst = utils::CreateImageCopyBuffer(copyDstBuffer, 0, 256);
-
-        wgpu::Extent3D copySize = {1, 1, 1};
-
-        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
-        encoder.CopyTextureToBuffer(&copySrc, &copyDst, &copySize);
-        wgpu::CommandBuffer commands = encoder.Finish();
-        queue.Submit(1, &commands);
-
-        // Verify |copyDstBuffer| sees changes from |secondDevice|
-        uint32_t expected = 0x04030201;
-        EXPECT_BUFFER_U32_EQ(expected, copyDstBuffer, 0);
-
-        IgnoreSignalSemaphore(deviceWrappedTexture);
-    }
-
-    // Import a texture into |device|
-    // Copy color A into texture on |device|
-    // Import same texture into |secondDevice|, waiting on the copy signal
-    // Copy color B using Buffer to Texture copy on |secondDevice|
-    // Import texture back into |device|, waiting on color B signal
-    // Verify texture contains color B
-    // If texture destination isn't synchronized, |secondDevice| could copy color B
-    // into the texture first, then |device| writes color A
-    TEST_P(VulkanImageWrappingUsageTests, CopyBufferToTextureDstSync) {
-        // Import the image on |device|
-        wgpu::Texture wrappedTexture = WrapVulkanImage(
-            device, &defaultDescriptor, defaultFd, defaultAllocationSize, defaultMemoryTypeIndex,
-            {}, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        // Clear |wrappedTexture| on |device|
-        ClearImage(device, wrappedTexture, {5 / 255.0f, 6 / 255.0f, 7 / 255.0f, 8 / 255.0f});
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfo);
-
-        // Import the image to |secondDevice|, making sure we wait on |signalFd|
-        int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-        wgpu::Texture secondDeviceWrappedTexture =
-            WrapVulkanImage(secondDevice, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, exportInfo.semaphoreHandles,
-                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
-
-        // Copy color B on |secondDevice|
-        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
-
-        // Create a buffer on |secondDevice|
-        wgpu::Buffer copySrcBuffer =
-            utils::CreateBufferFromData(secondDevice, wgpu::BufferUsage::CopySrc, {0x04030201});
-
-        // Copy |copySrcBuffer| into |secondDeviceWrappedTexture|
-        wgpu::ImageCopyBuffer copySrc = utils::CreateImageCopyBuffer(copySrcBuffer, 0, 256);
-        wgpu::ImageCopyTexture copyDst =
-            utils::CreateImageCopyTexture(secondDeviceWrappedTexture, 0, {0, 0, 0});
-
-        wgpu::Extent3D copySize = {1, 1, 1};
-
-        wgpu::CommandEncoder encoder = secondDevice.CreateCommandEncoder();
-        encoder.CopyBufferToTexture(&copySrc, &copyDst, &copySize);
-        wgpu::CommandBuffer commands = encoder.Finish();
-        secondDeviceQueue.Submit(1, &commands);
-
-        // Re-import back into |device|, waiting on |secondDevice|'s signal
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD secondExportInfo;
-        dawn::native::vulkan::ExportVulkanImage(secondDeviceWrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-                                                &secondExportInfo);
-        memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-
-        wgpu::Texture nextWrappedTexture =
-            WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, secondExportInfo.semaphoreHandles,
-                            secondExportInfo.releasedOldLayout, secondExportInfo.releasedNewLayout);
-
-        // Verify |nextWrappedTexture| contains the color from our copy
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
-
-        IgnoreSignalSemaphore(nextWrappedTexture);
-    }
-
-    // Import a texture from |secondDevice|
-    // Issue a copy of the imported texture inside |device| to |copyDstTexture|
-    // Issue second copy to |secondCopyDstTexture|
-    // Verify the clear color from |secondDevice| is visible in both copies
-    TEST_P(VulkanImageWrappingUsageTests, DoubleTextureUsage) {
-        // Import the image on |secondDevice|
-        wgpu::Texture wrappedTexture =
-            WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, {}, VK_IMAGE_LAYOUT_UNDEFINED,
-                            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        // Clear |wrappedTexture| on |secondDevice|
-        ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfo);
-
-        // Import the image to |device|, making sure we wait on the semaphore
-        int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
-        wgpu::Texture deviceWrappedTexture =
-            WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
-                            defaultMemoryTypeIndex, exportInfo.semaphoreHandles,
-                            exportInfo.releasedOldLayout, exportInfo.releasedNewLayout);
-
-        // Create a second texture on |device|
-        wgpu::Texture copyDstTexture = device.CreateTexture(&defaultDescriptor);
-
-        // Create a third texture on |device|
-        wgpu::Texture secondCopyDstTexture = device.CreateTexture(&defaultDescriptor);
-
-        // Copy |deviceWrappedTexture| into |copyDstTexture|
-        SimpleCopyTextureToTexture(device, queue, deviceWrappedTexture, copyDstTexture);
-
-        // Copy |deviceWrappedTexture| into |secondCopyDstTexture|
-        SimpleCopyTextureToTexture(device, queue, deviceWrappedTexture, secondCopyDstTexture);
-
-        // Verify |copyDstTexture| sees changes from |secondDevice|
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), copyDstTexture, 0, 0);
-
-        // Verify |secondCopyDstTexture| sees changes from |secondDevice|
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), secondCopyDstTexture, 0, 0);
-
-        IgnoreSignalSemaphore(deviceWrappedTexture);
-    }
-
-    // Tex A on device 3 (external export)
-    // Tex B on device 2 (external export)
-    // Tex C on device 1 (external export)
-    // Clear color for A on device 3
-    // Copy A->B on device 3
-    // Copy B->C on device 2 (wait on B from previous op)
-    // Copy C->D on device 1 (wait on C from previous op)
-    // Verify D has same color as A
-    TEST_P(VulkanImageWrappingUsageTests, ChainTextureCopy) {
-        // Close |defaultFd| since this test doesn't import it anywhere
-        close(defaultFd);
-
-        // device 1 = |device|
-        // device 2 = |secondDevice|
-        // Create device 3
-        dawn::native::vulkan::Device* thirdDeviceVk =
-            dawn::native::vulkan::ToBackend(backendAdapter->APICreateDevice(&deviceDescriptor));
-        wgpu::Device thirdDevice = wgpu::Device::Acquire(dawn::native::ToAPI(thirdDeviceVk));
-
-        // Make queue for device 2 and 3
-        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
-        wgpu::Queue thirdDeviceQueue = thirdDevice.GetQueue();
-
-        // Allocate memory for A, B, C
-        VkImage imageA;
-        VkDeviceMemory allocationA;
-        int memoryFdA;
-        VkDeviceSize allocationSizeA;
-        uint32_t memoryTypeIndexA;
-        CreateBindExportImage(thirdDeviceVk, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, &imageA, &allocationA,
-                              &allocationSizeA, &memoryTypeIndexA, &memoryFdA);
-
-        VkImage imageB;
-        VkDeviceMemory allocationB;
-        int memoryFdB;
-        VkDeviceSize allocationSizeB;
-        uint32_t memoryTypeIndexB;
-        CreateBindExportImage(secondDeviceVk, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, &imageB, &allocationB,
-                              &allocationSizeB, &memoryTypeIndexB, &memoryFdB);
-
-        VkImage imageC;
-        VkDeviceMemory allocationC;
-        int memoryFdC;
-        VkDeviceSize allocationSizeC;
-        uint32_t memoryTypeIndexC;
-        CreateBindExportImage(deviceVk, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, &imageC, &allocationC,
-                              &allocationSizeC, &memoryTypeIndexC, &memoryFdC);
-
-        // Import TexA, TexB on device 3
-        wgpu::Texture wrappedTexADevice3 = WrapVulkanImage(
-            thirdDevice, &defaultDescriptor, memoryFdA, allocationSizeA, memoryTypeIndexA, {},
-            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
-
-        wgpu::Texture wrappedTexBDevice3 = WrapVulkanImage(
-            thirdDevice, &defaultDescriptor, memoryFdB, allocationSizeB, memoryTypeIndexB, {},
-            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
-
-        // Clear TexA
-        ClearImage(thirdDevice, wrappedTexADevice3,
-                   {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
-
-        // Copy A->B
-        SimpleCopyTextureToTexture(thirdDevice, thirdDeviceQueue, wrappedTexADevice3,
-                                   wrappedTexBDevice3);
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfoTexBDevice3;
-        dawn::native::vulkan::ExportVulkanImage(
-            wrappedTexBDevice3.Get(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfoTexBDevice3);
-
-        IgnoreSignalSemaphore(wrappedTexADevice3);
-
-        // Import TexB, TexC on device 2
-        memoryFdB = GetMemoryFd(secondDeviceVk, allocationB);
-        wgpu::Texture wrappedTexBDevice2 = WrapVulkanImage(
-            secondDevice, &defaultDescriptor, memoryFdB, allocationSizeB, memoryTypeIndexB,
-            exportInfoTexBDevice3.semaphoreHandles, exportInfoTexBDevice3.releasedOldLayout,
-            exportInfoTexBDevice3.releasedNewLayout);
-
-        wgpu::Texture wrappedTexCDevice2 = WrapVulkanImage(
-            secondDevice, &defaultDescriptor, memoryFdC, allocationSizeC, memoryTypeIndexC, {},
-            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
-
-        // Copy B->C on device 2
-        SimpleCopyTextureToTexture(secondDevice, secondDeviceQueue, wrappedTexBDevice2,
-                                   wrappedTexCDevice2);
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfoTexCDevice2;
-        dawn::native::vulkan::ExportVulkanImage(
-            wrappedTexCDevice2.Get(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfoTexCDevice2);
-
-        IgnoreSignalSemaphore(wrappedTexBDevice2);
-
-        // Import TexC on device 1
-        memoryFdC = GetMemoryFd(deviceVk, allocationC);
-        wgpu::Texture wrappedTexCDevice1 = WrapVulkanImage(
-            device, &defaultDescriptor, memoryFdC, allocationSizeC, memoryTypeIndexC,
-            exportInfoTexCDevice2.semaphoreHandles, exportInfoTexCDevice2.releasedOldLayout,
-            exportInfoTexCDevice2.releasedNewLayout);
-
-        // Create TexD on device 1
-        wgpu::Texture texD = device.CreateTexture(&defaultDescriptor);
-
-        // Copy C->D on device 1
-        SimpleCopyTextureToTexture(device, queue, wrappedTexCDevice1, texD);
-
-        // Verify D matches clear color
-        EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), texD, 0, 0);
-
-        thirdDeviceVk->GetFencedDeleter()->DeleteWhenUnused(imageA);
-        thirdDeviceVk->GetFencedDeleter()->DeleteWhenUnused(allocationA);
-        secondDeviceVk->GetFencedDeleter()->DeleteWhenUnused(imageB);
-        secondDeviceVk->GetFencedDeleter()->DeleteWhenUnused(allocationB);
-        deviceVk->GetFencedDeleter()->DeleteWhenUnused(imageC);
-        deviceVk->GetFencedDeleter()->DeleteWhenUnused(allocationC);
-
-        IgnoreSignalSemaphore(wrappedTexCDevice1);
-    }
-
-    // Tests a larger image is preserved when importing
-    TEST_P(VulkanImageWrappingUsageTests, LargerImage) {
-        close(defaultFd);
-
-        wgpu::TextureDescriptor descriptor;
-        descriptor.dimension = wgpu::TextureDimension::e2D;
-        descriptor.size.width = 640;
-        descriptor.size.height = 480;
-        descriptor.size.depthOrArrayLayers = 1;
-        descriptor.sampleCount = 1;
-        descriptor.format = wgpu::TextureFormat::BGRA8Unorm;
-        descriptor.mipLevelCount = 1;
-        descriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::CopySrc;
-
-        // Fill memory with textures to trigger layout issues on AMD
-        std::vector<wgpu::Texture> textures;
-        for (int i = 0; i < 20; i++) {
-            textures.push_back(device.CreateTexture(&descriptor));
-        }
-
-        wgpu::Queue secondDeviceQueue = secondDevice.GetQueue();
-
-        // Make an image on |secondDevice|
-        VkImage imageA;
-        VkDeviceMemory allocationA;
-        int memoryFdA;
-        VkDeviceSize allocationSizeA;
-        uint32_t memoryTypeIndexA;
-        CreateBindExportImage(secondDeviceVk, 640, 480, VK_FORMAT_R8G8B8A8_UNORM, &imageA,
-                              &allocationA, &allocationSizeA, &memoryTypeIndexA, &memoryFdA);
-
-        // Import the image on |secondDevice|
-        wgpu::Texture wrappedTexture =
-            WrapVulkanImage(secondDevice, &descriptor, memoryFdA, allocationSizeA, memoryTypeIndexA,
-                            {}, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
-
-        // Draw a non-trivial picture
-        uint32_t width = 640, height = 480, pixelSize = 4;
-        uint32_t bytesPerRow = Align(width * pixelSize, kTextureBytesPerRowAlignment);
-        std::vector<unsigned char> data(bytesPerRow * (height - 1) + width * pixelSize);
-
-        for (uint32_t row = 0; row < height; row++) {
-            for (uint32_t col = 0; col < width; col++) {
-                float normRow = static_cast<float>(row) / height;
-                float normCol = static_cast<float>(col) / width;
-                float dist = sqrt(normRow * normRow + normCol * normCol) * 3;
-                dist = dist - static_cast<int>(dist);
-                data[4 * (row * width + col)] = static_cast<unsigned char>(dist * 255);
-                data[4 * (row * width + col) + 1] = static_cast<unsigned char>(dist * 255);
-                data[4 * (row * width + col) + 2] = static_cast<unsigned char>(dist * 255);
-                data[4 * (row * width + col) + 3] = 255;
-            }
-        }
-
-        // Write the picture
-        {
-            wgpu::Buffer copySrcBuffer = utils::CreateBufferFromData(
-                secondDevice, data.data(), data.size(), wgpu::BufferUsage::CopySrc);
-            wgpu::ImageCopyBuffer copySrc =
-                utils::CreateImageCopyBuffer(copySrcBuffer, 0, bytesPerRow);
-            wgpu::ImageCopyTexture copyDst =
-                utils::CreateImageCopyTexture(wrappedTexture, 0, {0, 0, 0});
-            wgpu::Extent3D copySize = {width, height, 1};
-
-            wgpu::CommandEncoder encoder = secondDevice.CreateCommandEncoder();
-            encoder.CopyBufferToTexture(&copySrc, &copyDst, &copySize);
-            wgpu::CommandBuffer commands = encoder.Finish();
-            secondDeviceQueue.Submit(1, &commands);
-        }
-
-        dawn::native::vulkan::ExternalImageExportInfoOpaqueFD exportInfo;
-        dawn::native::vulkan::ExportVulkanImage(wrappedTexture.Get(),
-                                                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, &exportInfo);
-
-        int memoryFd = GetMemoryFd(secondDeviceVk, allocationA);
-
-        // Import the image on |device|
-        wgpu::Texture nextWrappedTexture =
-            WrapVulkanImage(device, &descriptor, memoryFd, allocationSizeA, memoryTypeIndexA,
-                            exportInfo.semaphoreHandles, exportInfo.releasedOldLayout,
-                            exportInfo.releasedNewLayout);
-
-        // Copy the image into a buffer for comparison
-        wgpu::BufferDescriptor copyDesc;
-        copyDesc.size = data.size();
-        copyDesc.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
-        wgpu::Buffer copyDstBuffer = device.CreateBuffer(&copyDesc);
-        {
-            wgpu::ImageCopyTexture copySrc =
-                utils::CreateImageCopyTexture(nextWrappedTexture, 0, {0, 0, 0});
-            wgpu::ImageCopyBuffer copyDst =
-                utils::CreateImageCopyBuffer(copyDstBuffer, 0, bytesPerRow);
-
-            wgpu::Extent3D copySize = {width, height, 1};
-
-            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
-            encoder.CopyTextureToBuffer(&copySrc, &copyDst, &copySize);
-            wgpu::CommandBuffer commands = encoder.Finish();
-            queue.Submit(1, &commands);
-        }
-
-        // Check the image is not corrupted on |device|
-        EXPECT_BUFFER_U32_RANGE_EQ(reinterpret_cast<uint32_t*>(data.data()), copyDstBuffer, 0,
-                                   data.size() / 4);
-
-        IgnoreSignalSemaphore(nextWrappedTexture);
-        secondDeviceVk->GetFencedDeleter()->DeleteWhenUnused(imageA);
-        secondDeviceVk->GetFencedDeleter()->DeleteWhenUnused(allocationA);
-    }
-
-    DAWN_INSTANTIATE_TEST(VulkanImageWrappingValidationTests, VulkanBackend());
-    DAWN_INSTANTIATE_TEST(VulkanImageWrappingUsageTests, VulkanBackend());
-
-}  // namespace dawn::native::vulkan
diff --git a/src/tests/white_box/VulkanImageWrappingTests_OpaqueFD.cpp b/src/tests/white_box/VulkanImageWrappingTests_OpaqueFD.cpp
new file mode 100644
index 0000000..23275dc
--- /dev/null
+++ b/src/tests/white_box/VulkanImageWrappingTests_OpaqueFD.cpp
@@ -0,0 +1,270 @@
+// Copyright 2021 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/white_box/VulkanImageWrappingTests.h"
+
+#include "dawn_native/vulkan/DeviceVk.h"
+#include "dawn_native/vulkan/FencedDeleter.h"
+#include "dawn_native/vulkan/ResourceMemoryAllocatorVk.h"
+
+#include <gtest/gtest.h>
+#include <unistd.h>
+
+namespace dawn::native::vulkan {
+
+    class ExternalSemaphoreOpaqueFD : public VulkanImageWrappingTestBackend::ExternalSemaphore {
+      public:
+        ExternalSemaphoreOpaqueFD(int handle) : mHandle(handle) {
+        }
+        ~ExternalSemaphoreOpaqueFD() override {
+            if (mHandle != -1) {
+                close(mHandle);
+            }
+        }
+        int AcquireHandle() {
+            int handle = mHandle;
+            mHandle = -1;
+            return handle;
+        }
+
+      private:
+        int mHandle = -1;
+    };
+
+    class ExternalTextureOpaqueFD : public VulkanImageWrappingTestBackend::ExternalTexture {
+      public:
+        ExternalTextureOpaqueFD(dawn::native::vulkan::Device* device,
+                                int fd,
+                                VkDeviceMemory allocation,
+                                VkImage handle,
+                                VkDeviceSize allocationSize,
+                                uint32_t memoryTypeIndex)
+            : mDevice(device),
+              mFd(fd),
+              mAllocation(allocation),
+              mHandle(handle),
+              allocationSize(allocationSize),
+              memoryTypeIndex(memoryTypeIndex) {
+        }
+
+        ~ExternalTextureOpaqueFD() override {
+            if (mFd != -1) {
+                close(mFd);
+            }
+            if (mAllocation != VK_NULL_HANDLE) {
+                mDevice->GetFencedDeleter()->DeleteWhenUnused(mAllocation);
+            }
+            if (mHandle != VK_NULL_HANDLE) {
+                mDevice->GetFencedDeleter()->DeleteWhenUnused(mHandle);
+            }
+        }
+
+        int Dup() const {
+            return dup(mFd);
+        }
+
+      private:
+        dawn::native::vulkan::Device* mDevice;
+        int mFd = -1;
+        VkDeviceMemory mAllocation = VK_NULL_HANDLE;
+        VkImage mHandle = VK_NULL_HANDLE;
+
+      public:
+        const VkDeviceSize allocationSize;
+        const uint32_t memoryTypeIndex;
+    };
+
+    class VulkanImageWrappingTestBackendOpaqueFD : public VulkanImageWrappingTestBackend {
+      public:
+        VulkanImageWrappingTestBackendOpaqueFD(const wgpu::Device& device) : mDevice(device) {
+            mDeviceVk = dawn::native::vulkan::ToBackend(dawn::native::FromAPI(device.Get()));
+        }
+
+        std::unique_ptr<ExternalTexture> CreateTexture(uint32_t width,
+                                                       uint32_t height,
+                                                       wgpu::TextureFormat format,
+                                                       wgpu::TextureUsage usage) override {
+            EXPECT_EQ(format, wgpu::TextureFormat::RGBA8Unorm);
+            VkFormat vulkanFormat = VK_FORMAT_R8G8B8A8_UNORM;
+
+            VkImage handle;
+            ::VkResult result = CreateImage(mDeviceVk, width, height, vulkanFormat, &handle);
+            EXPECT_EQ(result, VK_SUCCESS) << "Failed to create external image";
+
+            VkDeviceMemory allocation;
+            VkDeviceSize allocationSize;
+            uint32_t memoryTypeIndex;
+            ::VkResult resultBool =
+                AllocateMemory(mDeviceVk, handle, &allocation, &allocationSize, &memoryTypeIndex);
+            EXPECT_EQ(resultBool, VK_SUCCESS) << "Failed to allocate external memory";
+
+            result = BindMemory(mDeviceVk, handle, allocation);
+            EXPECT_EQ(result, VK_SUCCESS) << "Failed to bind image memory";
+
+            int fd = GetMemoryFd(mDeviceVk, allocation);
+
+            return std::make_unique<ExternalTextureOpaqueFD>(mDeviceVk, fd, allocation, handle,
+                                                             allocationSize, memoryTypeIndex);
+        }
+
+        wgpu::Texture WrapImage(
+            const wgpu::Device& device,
+            const ExternalTexture* texture,
+            const ExternalImageDescriptorVkForTesting& descriptor,
+            std::vector<std::unique_ptr<ExternalSemaphore>> semaphores) override {
+            const ExternalTextureOpaqueFD* textureOpaqueFD =
+                static_cast<const ExternalTextureOpaqueFD*>(texture);
+            std::vector<int> waitFDs;
+            for (auto& semaphore : semaphores) {
+                waitFDs.push_back(
+                    static_cast<ExternalSemaphoreOpaqueFD*>(semaphore.get())->AcquireHandle());
+            }
+
+            ExternalImageDescriptorOpaqueFD descriptorOpaqueFD;
+            *static_cast<ExternalImageDescriptorVk*>(&descriptorOpaqueFD) = descriptor;
+            descriptorOpaqueFD.memoryFD = textureOpaqueFD->Dup();
+            descriptorOpaqueFD.allocationSize = textureOpaqueFD->allocationSize;
+            descriptorOpaqueFD.memoryTypeIndex = textureOpaqueFD->memoryTypeIndex;
+            descriptorOpaqueFD.waitFDs = std::move(waitFDs);
+
+            return dawn::native::vulkan::WrapVulkanImage(device.Get(), &descriptorOpaqueFD);
+        }
+
+        bool ExportImage(const wgpu::Texture& texture,
+                         VkImageLayout layout,
+                         ExternalImageExportInfoVkForTesting* exportInfo) override {
+            ExternalImageExportInfoOpaqueFD infoOpaqueFD;
+            bool success = ExportVulkanImage(texture.Get(), layout, &infoOpaqueFD);
+
+            *static_cast<ExternalImageExportInfoVk*>(exportInfo) = infoOpaqueFD;
+            for (int fd : infoOpaqueFD.semaphoreHandles) {
+                EXPECT_NE(fd, -1);
+                exportInfo->semaphores.push_back(std::make_unique<ExternalSemaphoreOpaqueFD>(fd));
+            }
+
+            return success;
+        }
+
+      private:
+        // Creates a VkImage with external memory
+        ::VkResult CreateImage(dawn::native::vulkan::Device* deviceVk,
+                               uint32_t width,
+                               uint32_t height,
+                               VkFormat format,
+                               VkImage* image) {
+            VkExternalMemoryImageCreateInfoKHR externalInfo;
+            externalInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO_KHR;
+            externalInfo.pNext = nullptr;
+            externalInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
+
+            auto usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+                         VK_IMAGE_USAGE_TRANSFER_DST_BIT;
+
+            VkImageCreateInfo createInfo;
+            createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+            createInfo.pNext = &externalInfo;
+            createInfo.flags = VK_IMAGE_CREATE_ALIAS_BIT_KHR;
+            createInfo.imageType = VK_IMAGE_TYPE_2D;
+            createInfo.format = format;
+            createInfo.extent = {width, height, 1};
+            createInfo.mipLevels = 1;
+            createInfo.arrayLayers = 1;
+            createInfo.samples = VK_SAMPLE_COUNT_1_BIT;
+            createInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
+            createInfo.usage = usage;
+            createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+            createInfo.queueFamilyIndexCount = 0;
+            createInfo.pQueueFamilyIndices = nullptr;
+            createInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+
+            return deviceVk->fn.CreateImage(deviceVk->GetVkDevice(), &createInfo, nullptr,
+                                            &**image);
+        }
+
+        // Allocates memory for an image
+        ::VkResult AllocateMemory(dawn::native::vulkan::Device* deviceVk,
+                                  VkImage handle,
+                                  VkDeviceMemory* allocation,
+                                  VkDeviceSize* allocationSize,
+                                  uint32_t* memoryTypeIndex) {
+            // Create the image memory and associate it with the container
+            VkMemoryRequirements requirements;
+            deviceVk->fn.GetImageMemoryRequirements(deviceVk->GetVkDevice(), handle, &requirements);
+
+            // Import memory from file descriptor
+            VkExportMemoryAllocateInfoKHR externalInfo;
+            externalInfo.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO_KHR;
+            externalInfo.pNext = nullptr;
+            externalInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
+
+            int bestType = deviceVk->GetResourceMemoryAllocator()->FindBestTypeIndex(
+                requirements, MemoryKind::Opaque);
+            VkMemoryAllocateInfo allocateInfo;
+            allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+            allocateInfo.pNext = &externalInfo;
+            allocateInfo.allocationSize = requirements.size;
+            allocateInfo.memoryTypeIndex = static_cast<uint32_t>(bestType);
+
+            *allocationSize = allocateInfo.allocationSize;
+            *memoryTypeIndex = allocateInfo.memoryTypeIndex;
+
+            return deviceVk->fn.AllocateMemory(deviceVk->GetVkDevice(), &allocateInfo, nullptr,
+                                               &**allocation);
+        }
+
+        // Binds memory to an image
+        ::VkResult BindMemory(dawn::native::vulkan::Device* deviceVk,
+                              VkImage handle,
+                              VkDeviceMemory memory) {
+            return deviceVk->fn.BindImageMemory(deviceVk->GetVkDevice(), handle, memory, 0);
+        }
+
+        // Extracts a file descriptor representing memory on a device
+        int GetMemoryFd(dawn::native::vulkan::Device* deviceVk, VkDeviceMemory memory) {
+            VkMemoryGetFdInfoKHR getFdInfo;
+            getFdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR;
+            getFdInfo.pNext = nullptr;
+            getFdInfo.memory = memory;
+            getFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
+
+            int memoryFd = -1;
+            deviceVk->fn.GetMemoryFdKHR(deviceVk->GetVkDevice(), &getFdInfo, &memoryFd);
+
+            EXPECT_GE(memoryFd, 0) << "Failed to get file descriptor for external memory";
+            return memoryFd;
+        }
+
+        // Prepares and exports memory for an image on a given device
+        void CreateBindExportImage(dawn::native::vulkan::Device* deviceVk,
+                                   uint32_t width,
+                                   uint32_t height,
+                                   VkFormat format,
+                                   VkImage* handle,
+                                   VkDeviceMemory* allocation,
+                                   VkDeviceSize* allocationSize,
+                                   uint32_t* memoryTypeIndex,
+                                   int* memoryFd) {
+        }
+
+        wgpu::Device mDevice;
+        dawn::native::vulkan::Device* mDeviceVk;
+    };
+
+    // static
+    std::unique_ptr<VulkanImageWrappingTestBackend> VulkanImageWrappingTestBackend::Create(
+        const wgpu::Device& device) {
+        return std::make_unique<VulkanImageWrappingTestBackendOpaqueFD>(device);
+    }
+
+}  // namespace dawn::native::vulkan