Add wrapping for using external vulkan images as textures

This change adds platform-dependent services that handle creating
semaphores and importing image memory. Then, we use them to wrap a
texture from an outside source, and release a signal semaphore back
when we're done with it. This will be used to allow chrome to render
dawn on Vulkan platforms.

Bug: chromium:976495
Change-Id: I9f07eaf436e10aa6bd88cffdc74fd23834d62ee0
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/8340
Commit-Queue: Idan Raiter <idanr@google.com>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index aed63b1..4375a36 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -362,6 +362,7 @@
       "src/dawn_native/vulkan/ComputePipelineVk.h",
       "src/dawn_native/vulkan/DeviceVk.cpp",
       "src/dawn_native/vulkan/DeviceVk.h",
+      "src/dawn_native/vulkan/ExternalHandle.h",
       "src/dawn_native/vulkan/FencedDeleter.cpp",
       "src/dawn_native/vulkan/FencedDeleter.h",
       "src/dawn_native/vulkan/Forward.h",
@@ -395,7 +396,21 @@
       "src/dawn_native/vulkan/VulkanFunctions.h",
       "src/dawn_native/vulkan/VulkanInfo.cpp",
       "src/dawn_native/vulkan/VulkanInfo.h",
+      "src/dawn_native/vulkan/external_memory/MemoryService.h",
+      "src/dawn_native/vulkan/external_semaphore/SemaphoreService.h",
     ]
+
+    if (is_linux) {
+      sources += [
+        "src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp",
+        "src/dawn_native/vulkan/external_semaphore/SemaphoreServiceOpaqueFD.cpp",
+      ]
+    } else {
+      sources += [
+        "src/dawn_native/vulkan/external_memory/MemoryServiceNull.cpp",
+        "src/dawn_native/vulkan/external_semaphore/SemaphoreServiceNull.cpp",
+      ]
+    }
   }
 }
 
@@ -667,8 +682,9 @@
   }
 }
 
-test("dawn_end2end_tests") {
+source_set("dawn_end2end_tests_sources") {
   configs += [ "${dawn_root}/src/common:dawn_internal" ]
+  testonly = true
 
   deps = [
     ":dawn_utils",
@@ -681,7 +697,6 @@
   ]
 
   sources = [
-    "src/tests/DawnTest.cpp",
     "src/tests/DawnTest.h",
     "src/tests/end2end/BasicTests.cpp",
     "src/tests/end2end/BindGroupTests.cpp",
@@ -729,6 +744,59 @@
 
     libs += [ "IOSurface.framework" ]
   }
+}
+
+source_set("dawn_white_box_tests_sources") {
+  configs += [ "${dawn_root}/src/common:dawn_internal" ]
+  testonly = true
+
+  deps = [
+    ":dawn_utils",
+    ":libdawn_native",
+    ":libdawn_native_sources",
+    ":libdawn_wire",
+    "${dawn_root}/src/common",
+    "${dawn_root}/src/dawn:libdawn",
+    "third_party:glfw",
+    "third_party:gmock_and_gtest",
+  ]
+
+  sources = [
+    "src/tests/DawnTest.h",
+  ]
+
+  if (dawn_enable_vulkan) {
+    deps += [ "third_party:vulkan_headers" ]
+
+    if (is_linux) {
+      sources += [ "src/tests/white_box/VulkanImageWrappingTests.cpp" ]
+    }
+  }
+
+  libs = []
+}
+
+test("dawn_end2end_tests") {
+  configs += [ "${dawn_root}/src/common:dawn_internal" ]
+
+  deps = [
+    ":dawn_end2end_tests_sources",
+    ":dawn_utils",
+    ":dawn_white_box_tests_sources",
+    ":libdawn_native",
+    ":libdawn_wire",
+    "${dawn_root}/src/common",
+    "${dawn_root}/src/dawn:libdawn",
+    "third_party:glfw",
+    "third_party:gmock_and_gtest",
+  ]
+
+  sources = [
+    "src/tests/DawnTest.cpp",
+    "src/tests/DawnTest.h",
+  ]
+
+  libs = []
 
   # When building inside Chromium, use their gtest main function because it is
   # needed to run in swarming correctly.
diff --git a/src/dawn_native/Texture.cpp b/src/dawn_native/Texture.cpp
index 224a10d..87433ad 100644
--- a/src/dawn_native/Texture.cpp
+++ b/src/dawn_native/Texture.cpp
@@ -208,6 +208,9 @@
 
     MaybeError ValidateTextureDescriptor(const DeviceBase* device,
                                          const TextureDescriptor* descriptor) {
+        if (descriptor == nullptr) {
+            return DAWN_VALIDATION_ERROR("Texture descriptor is nullptr");
+        }
         if (descriptor->nextInChain != nullptr) {
             return DAWN_VALIDATION_ERROR("nextInChain must be nullptr");
         }
diff --git a/src/dawn_native/vulkan/DeviceVk.cpp b/src/dawn_native/vulkan/DeviceVk.cpp
index fc3b152..354809f 100644
--- a/src/dawn_native/vulkan/DeviceVk.cpp
+++ b/src/dawn_native/vulkan/DeviceVk.cpp
@@ -18,7 +18,9 @@
 #include "dawn_native/BackendConnection.h"
 #include "dawn_native/Commands.h"
 #include "dawn_native/DynamicUploader.h"
+#include "dawn_native/Error.h"
 #include "dawn_native/ErrorData.h"
+#include "dawn_native/VulkanBackend.h"
 #include "dawn_native/vulkan/AdapterVk.h"
 #include "dawn_native/vulkan/BackendVk.h"
 #include "dawn_native/vulkan/BindGroupLayoutVk.h"
@@ -68,6 +70,9 @@
         mMemoryAllocator = std::make_unique<MemoryAllocator>(this);
         mRenderPassCache = std::make_unique<RenderPassCache>(this);
 
+        mExternalMemoryService = std::make_unique<external_memory::Service>(this);
+        mExternalSemaphoreService = std::make_unique<external_semaphore::Service>(this);
+
         return {};
     }
 
@@ -568,4 +573,91 @@
 
         return {};
     }
+
+    MaybeError Device::ImportExternalImage(const ExternalImageDescriptor* descriptor,
+                                           ExternalMemoryHandle memoryHandle,
+                                           const std::vector<ExternalSemaphoreHandle>& waitHandles,
+                                           VkSemaphore* outSignalSemaphore,
+                                           VkDeviceMemory* outAllocation,
+                                           std::vector<VkSemaphore>* outWaitSemaphores) {
+        // Check services support this combination of handle type / image info
+        if (!mExternalSemaphoreService->Supported()) {
+            return DAWN_VALIDATION_ERROR("External semaphore usage not supported");
+        }
+        if (!mExternalMemoryService->Supported()) {
+            return DAWN_VALIDATION_ERROR("External memory usage not supported");
+        }
+
+        // Create an external semaphore to signal when the texture is done being used
+        DAWN_TRY_ASSIGN(*outSignalSemaphore,
+                        mExternalSemaphoreService->CreateExportableSemaphore());
+
+        // Import the external image's memory
+        DAWN_TRY_ASSIGN(*outAllocation,
+                        mExternalMemoryService->ImportMemory(
+                            memoryHandle, descriptor->allocationSize, descriptor->memoryTypeIndex));
+
+        // Import semaphores we have to wait on before using the texture
+        for (const ExternalSemaphoreHandle& handle : waitHandles) {
+            VkSemaphore semaphore = VK_NULL_HANDLE;
+            DAWN_TRY_ASSIGN(semaphore, mExternalSemaphoreService->ImportSemaphore(handle));
+            outWaitSemaphores->push_back(semaphore);
+        }
+
+        return {};
+    }
+
+    MaybeError Device::SignalAndExportExternalTexture(Texture* texture,
+                                                      ExternalSemaphoreHandle* outHandle) {
+        DAWN_TRY(ValidateObject(texture));
+
+        VkSemaphore outSignalSemaphore;
+        DAWN_TRY(texture->SignalAndDestroy(&outSignalSemaphore));
+
+        // This has to happen right after SignalAndDestroy, since the semaphore will be
+        // deleted when the fenced deleter runs after the queue submission
+        DAWN_TRY_ASSIGN(*outHandle, mExternalSemaphoreService->ExportSemaphore(outSignalSemaphore));
+
+        return {};
+    }
+
+    TextureBase* Device::CreateTextureWrappingVulkanImage(
+        const ExternalImageDescriptor* descriptor,
+        ExternalMemoryHandle memoryHandle,
+        const std::vector<ExternalSemaphoreHandle>& waitHandles) {
+        const TextureDescriptor* textureDescriptor =
+            reinterpret_cast<const TextureDescriptor*>(descriptor->cTextureDescriptor);
+
+        // Initial validation
+        if (ConsumedError(ValidateTextureDescriptor(this, textureDescriptor))) {
+            return nullptr;
+        }
+        if (ConsumedError(ValidateVulkanImageCanBeWrapped(this, textureDescriptor))) {
+            return nullptr;
+        }
+
+        VkSemaphore signalSemaphore = VK_NULL_HANDLE;
+        VkDeviceMemory allocation = VK_NULL_HANDLE;
+        std::vector<VkSemaphore> waitSemaphores;
+        waitSemaphores.reserve(waitHandles.size());
+
+        // If failed, cleanup
+        if (ConsumedError(ImportExternalImage(descriptor, memoryHandle, waitHandles,
+                                              &signalSemaphore, &allocation, &waitSemaphores))) {
+            // Clear the signal semaphore
+            fn.DestroySemaphore(GetVkDevice(), signalSemaphore, nullptr);
+
+            // Clear image memory
+            fn.FreeMemory(GetVkDevice(), allocation, nullptr);
+
+            // Clear any wait semaphores we were able to import
+            for (VkSemaphore semaphore : waitSemaphores) {
+                fn.DestroySemaphore(GetVkDevice(), semaphore, nullptr);
+            }
+            return nullptr;
+        }
+
+        return new Texture(this, descriptor, textureDescriptor, signalSemaphore, allocation,
+                           waitSemaphores);
+    }
 }}  // namespace dawn_native::vulkan
diff --git a/src/dawn_native/vulkan/DeviceVk.h b/src/dawn_native/vulkan/DeviceVk.h
index be76c16..2aba428 100644
--- a/src/dawn_native/vulkan/DeviceVk.h
+++ b/src/dawn_native/vulkan/DeviceVk.h
@@ -25,6 +25,9 @@
 #include "dawn_native/vulkan/VulkanFunctions.h"
 #include "dawn_native/vulkan/VulkanInfo.h"
 
+#include "dawn_native/vulkan/external_memory/MemoryService.h"
+#include "dawn_native/vulkan/external_semaphore/SemaphoreService.h"
+
 #include <memory>
 #include <queue>
 
@@ -32,6 +35,7 @@
 
     class Adapter;
     class BufferUploader;
+    struct ExternalImageDescriptor;
     class FencedDeleter;
     class MapRequestTracker;
     class MemoryAllocator;
@@ -64,6 +68,14 @@
         Serial GetPendingCommandSerial() const override;
         void SubmitPendingCommands();
 
+        TextureBase* CreateTextureWrappingVulkanImage(
+            const ExternalImageDescriptor* descriptor,
+            ExternalMemoryHandle memoryHandle,
+            const std::vector<ExternalSemaphoreHandle>& waitHandles);
+
+        MaybeError SignalAndExportExternalTexture(Texture* texture,
+                                                  ExternalSemaphoreHandle* outHandle);
+
         // Dawn API
         CommandBufferBase* CreateCommandBuffer(CommandEncoderBase* encoder,
                                                const CommandBufferDescriptor* descriptor) override;
@@ -119,6 +131,9 @@
         std::unique_ptr<MemoryAllocator> mMemoryAllocator;
         std::unique_ptr<RenderPassCache> mRenderPassCache;
 
+        std::unique_ptr<external_memory::Service> mExternalMemoryService;
+        std::unique_ptr<external_semaphore::Service> mExternalSemaphoreService;
+
         VkFence GetUnusedFence();
         void CheckPassedFences();
 
@@ -143,8 +158,14 @@
         SerialQueue<CommandPoolAndBuffer> mCommandsInFlight;
         std::vector<CommandPoolAndBuffer> mUnusedCommands;
         CommandPoolAndBuffer mPendingCommands;
-
         CommandRecordingContext mRecordingContext;
+
+        MaybeError ImportExternalImage(const ExternalImageDescriptor* descriptor,
+                                       ExternalMemoryHandle memoryHandle,
+                                       const std::vector<ExternalSemaphoreHandle>& waitHandles,
+                                       VkSemaphore* outSignalSemaphore,
+                                       VkDeviceMemory* outAllocation,
+                                       std::vector<VkSemaphore>* outWaitSemaphores);
     };
 
 }}  // namespace dawn_native::vulkan
diff --git a/src/dawn_native/vulkan/ExternalHandle.h b/src/dawn_native/vulkan/ExternalHandle.h
new file mode 100644
index 0000000..37a2e21
--- /dev/null
+++ b/src/dawn_native/vulkan/ExternalHandle.h
@@ -0,0 +1,19 @@
+#ifndef DAWNNATIVE_VULKAN_EXTERNALHANDLE_H_
+#define DAWNNATIVE_VULKAN_EXTERNALHANDLE_H_
+
+namespace dawn_native { namespace vulkan {
+
+#ifdef DAWN_PLATFORM_LINUX
+    // File descriptor
+    using ExternalMemoryHandle = int;
+    // File descriptor
+    using ExternalSemaphoreHandle = int;
+#else
+    // Generic types so that the Null service can compile, not used for real handles
+    using ExternalMemoryHandle = void*;
+    using ExternalSemaphoreHandle = void*;
+#endif
+
+}}  // namespace dawn_native::vulkan
+
+#endif  // DAWNNATIVE_VULKAN_EXTERNALHANDLE_H_
diff --git a/src/dawn_native/vulkan/MemoryAllocator.cpp b/src/dawn_native/vulkan/MemoryAllocator.cpp
index 66a779e..abd53da 100644
--- a/src/dawn_native/vulkan/MemoryAllocator.cpp
+++ b/src/dawn_native/vulkan/MemoryAllocator.cpp
@@ -41,9 +41,7 @@
     MemoryAllocator::~MemoryAllocator() {
     }
 
-    bool MemoryAllocator::Allocate(VkMemoryRequirements requirements,
-                                   bool mappable,
-                                   DeviceMemoryAllocation* allocation) {
+    int MemoryAllocator::FindBestTypeIndex(VkMemoryRequirements requirements, bool mappable) {
         const VulkanDeviceInfo& info = mDevice->GetDeviceInfo();
 
         // Find a suitable memory type for this allocation
@@ -93,6 +91,14 @@
             }
         }
 
+        return bestType;
+    }
+
+    bool MemoryAllocator::Allocate(VkMemoryRequirements requirements,
+                                   bool mappable,
+                                   DeviceMemoryAllocation* allocation) {
+        int bestType = FindBestTypeIndex(requirements, mappable);
+
         // TODO(cwallez@chromium.org): I think the Vulkan spec guarantees this should never happen
         if (bestType == -1) {
             ASSERT(false);
diff --git a/src/dawn_native/vulkan/MemoryAllocator.h b/src/dawn_native/vulkan/MemoryAllocator.h
index afea7af..56d3350 100644
--- a/src/dawn_native/vulkan/MemoryAllocator.h
+++ b/src/dawn_native/vulkan/MemoryAllocator.h
@@ -42,6 +42,7 @@
         MemoryAllocator(Device* device);
         ~MemoryAllocator();
 
+        int FindBestTypeIndex(VkMemoryRequirements requirements, bool mappable);
         bool Allocate(VkMemoryRequirements requirements,
                       bool mappable,
                       DeviceMemoryAllocation* allocation);
diff --git a/src/dawn_native/vulkan/TextureVk.cpp b/src/dawn_native/vulkan/TextureVk.cpp
index 727292c..ad21992 100644
--- a/src/dawn_native/vulkan/TextureVk.cpp
+++ b/src/dawn_native/vulkan/TextureVk.cpp
@@ -14,6 +14,7 @@
 
 #include "dawn_native/vulkan/TextureVk.h"
 
+#include "dawn_native/VulkanBackend.h"
 #include "dawn_native/vulkan/AdapterVk.h"
 #include "dawn_native/vulkan/CommandRecordingContext.h"
 #include "dawn_native/vulkan/DeviceVk.h"
@@ -379,12 +380,33 @@
         }
     }
 
+    MaybeError ValidateVulkanImageCanBeWrapped(const DeviceBase*,
+                                               const TextureDescriptor* descriptor) {
+        if (descriptor->dimension != dawn::TextureDimension::e2D) {
+            return DAWN_VALIDATION_ERROR("Texture must be 2D");
+        }
+
+        if (descriptor->mipLevelCount != 1) {
+            return DAWN_VALIDATION_ERROR("Mip level count must be 1");
+        }
+
+        if (descriptor->arrayLayerCount != 1) {
+            return DAWN_VALIDATION_ERROR("Array layer count must be 1");
+        }
+
+        if (descriptor->sampleCount != 1) {
+            return DAWN_VALIDATION_ERROR("Sample count must be 1");
+        }
+
+        return {};
+    }
+
     Texture::Texture(Device* device, const TextureDescriptor* descriptor)
         : TextureBase(device, descriptor, TextureState::OwnedInternal) {
         // Create the Vulkan image "container". We don't need to check that the format supports the
         // combination of sample, usage etc. because validation should have been done in the Dawn
         // frontend already based on the minimum supported formats in the Vulkan spec
-        VkImageCreateInfo createInfo;
+        VkImageCreateInfo createInfo = {};
         createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
         createInfo.pNext = nullptr;
         createInfo.flags = 0;
@@ -466,6 +488,95 @@
         : TextureBase(device, descriptor, TextureState::OwnedExternal), mHandle(nativeImage) {
     }
 
+    // Internally managed, but imported from file descriptor
+    Texture::Texture(Device* device,
+                     const ExternalImageDescriptor* descriptor,
+                     const TextureDescriptor* textureDescriptor,
+                     VkSemaphore signalSemaphore,
+                     VkDeviceMemory externalMemoryAllocation,
+                     std::vector<VkSemaphore> waitSemaphores)
+        : TextureBase(device, textureDescriptor, TextureState::OwnedInternal),
+          mExternalAllocation(externalMemoryAllocation),
+          mExternalState(ExternalState::PendingAcquire),
+          mSignalSemaphore(signalSemaphore),
+          mWaitRequirements(std::move(waitSemaphores)) {
+        VkImageCreateInfo createInfo = {};
+        createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+        createInfo.pNext = nullptr;
+        createInfo.flags = VK_IMAGE_CREATE_ALIAS_BIT_KHR;
+        createInfo.imageType = VulkanImageType(GetDimension());
+        createInfo.format = VulkanImageFormat(GetFormat().format);
+        createInfo.extent = VulkanExtent3D(GetSize());
+        createInfo.mipLevels = GetNumMipLevels();
+        createInfo.arrayLayers = GetArrayLayers();
+        createInfo.samples = VulkanSampleCount(GetSampleCount());
+        createInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
+        createInfo.usage = VulkanImageUsage(GetUsage(), GetFormat());
+        createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+        createInfo.queueFamilyIndexCount = 0;
+        createInfo.pQueueFamilyIndices = nullptr;
+        createInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+
+        ASSERT(IsSampleCountSupported(device, createInfo));
+
+        // We always set VK_IMAGE_USAGE_TRANSFER_DST_BIT unconditionally beause the Vulkan images
+        // that are used in vkCmdClearColorImage() must have been created with this flag, which is
+        // also required for the implementation of robust resource initialization.
+        createInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
+
+        if (device->fn.CreateImage(device->GetVkDevice(), &createInfo, nullptr, &mHandle) !=
+            VK_SUCCESS) {
+            ASSERT(false);
+        }
+
+        // Create the image memory and associate it with the container
+        VkMemoryRequirements requirements;
+        device->fn.GetImageMemoryRequirements(device->GetVkDevice(), mHandle, &requirements);
+
+        ASSERT(requirements.size <= descriptor->allocationSize);
+
+        if (device->fn.BindImageMemory(device->GetVkDevice(), mHandle, mExternalAllocation, 0) !=
+            VK_SUCCESS) {
+            ASSERT(false);
+        }
+
+        // Don't clear imported texture if already cleared
+        if (descriptor->isCleared) {
+            SetIsSubresourceContentInitialized(0, 1, 0, 1);
+        }
+    }
+
+    MaybeError Texture::SignalAndDestroy(VkSemaphore* outSignalSemaphore) {
+        Device* device = ToBackend(GetDevice());
+
+        if (mExternalState == ExternalState::Released) {
+            return DAWN_VALIDATION_ERROR("Can't export signal semaphore from signaled texture");
+        }
+
+        if (mExternalAllocation == VK_NULL_HANDLE) {
+            return DAWN_VALIDATION_ERROR(
+                "Can't export signal semaphore from destroyed / non-external texture");
+        }
+
+        ASSERT(mSignalSemaphore != VK_NULL_HANDLE);
+
+        // Release the texture
+        mExternalState = ExternalState::PendingRelease;
+        TransitionUsageNow(device->GetPendingRecordingContext(), dawn::TextureUsageBit::None);
+
+        // Queue submit to signal we are done with the texture
+        device->GetPendingRecordingContext()->signalSemaphores.push_back(mSignalSemaphore);
+        device->SubmitPendingCommands();
+
+        // Write out the signal semaphore
+        *outSignalSemaphore = mSignalSemaphore;
+        mSignalSemaphore = VK_NULL_HANDLE;
+
+        // Destroy the texture so it can't be used again
+        DestroyInternal();
+        return {};
+    }
+
     Texture::~Texture() {
         DestroyInternal();
     }
@@ -479,12 +590,20 @@
             // freed after the VkImage is destroyed and this is taken care of by the
             // FencedDeleter.
             device->GetMemoryAllocator()->Free(&mMemoryAllocation);
-
-            if (mHandle != VK_NULL_HANDLE) {
-                device->GetFencedDeleter()->DeleteWhenUnused(mHandle);
-            }
         }
+
+        if (mHandle != VK_NULL_HANDLE) {
+            device->GetFencedDeleter()->DeleteWhenUnused(mHandle);
+        }
+
+        if (mExternalAllocation != VK_NULL_HANDLE) {
+            device->GetFencedDeleter()->DeleteWhenUnused(mExternalAllocation);
+        }
+
         mHandle = VK_NULL_HANDLE;
+        mExternalAllocation = VK_NULL_HANDLE;
+        // If a signal semaphore exists it should be requested before we delete the texture
+        ASSERT(mSignalSemaphore == VK_NULL_HANDLE);
     }
 
     VkImage Texture::GetHandle() const {
@@ -499,7 +618,7 @@
                                      dawn::TextureUsageBit usage) {
         // Avoid encoding barriers when it isn't needed.
         bool lastReadOnly = (mLastUsage & kReadOnlyTextureUsages) == mLastUsage;
-        if (lastReadOnly && mLastUsage == usage) {
+        if (lastReadOnly && mLastUsage == usage && mLastExternalState == mExternalState) {
             return;
         }
 
@@ -515,8 +634,6 @@
         barrier.dstAccessMask = VulkanAccessFlags(usage, format);
         barrier.oldLayout = VulkanImageLayout(mLastUsage, format);
         barrier.newLayout = VulkanImageLayout(usage, format);
-        barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
-        barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
         barrier.image = mHandle;
         // This transitions the whole resource but assumes it is a 2D texture
         ASSERT(GetDimension() == dawn::TextureDimension::e2D);
@@ -526,11 +643,36 @@
         barrier.subresourceRange.baseArrayLayer = 0;
         barrier.subresourceRange.layerCount = GetArrayLayers();
 
+        barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+        barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+
+        if (mExternalState == ExternalState::PendingAcquire) {
+            // Transfer texture from external queue to graphics queue
+            barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL_KHR;
+            barrier.dstQueueFamilyIndex = ToBackend(GetDevice())->GetGraphicsQueueFamily();
+            // Don't override oldLayout to leave it as VK_IMAGE_LAYOUT_UNDEFINED
+            // TODO(http://crbug.com/dawn/200)
+            mExternalState = ExternalState::Acquired;
+
+        } else if (mExternalState == ExternalState::PendingRelease) {
+            // Transfer texture from graphics queue to external queue
+            barrier.srcQueueFamilyIndex = ToBackend(GetDevice())->GetGraphicsQueueFamily();
+            barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL_KHR;
+            barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
+            mExternalState = ExternalState::Released;
+        }
+
+        // Move required semaphores into waitSemaphores
+        recordingContext->waitSemaphores.insert(recordingContext->waitSemaphores.end(),
+                                                mWaitRequirements.begin(), mWaitRequirements.end());
+        mWaitRequirements.clear();
+
         ToBackend(GetDevice())
             ->fn.CmdPipelineBarrier(recordingContext->commandBuffer, srcStages, dstStages, 0, 0,
                                     nullptr, 0, nullptr, 1, &barrier);
 
         mLastUsage = usage;
+        mLastExternalState = mExternalState;
     }
 
     void Texture::ClearTexture(CommandRecordingContext* recordingContext,
diff --git a/src/dawn_native/vulkan/TextureVk.h b/src/dawn_native/vulkan/TextureVk.h
index 9f71692..52236d3 100644
--- a/src/dawn_native/vulkan/TextureVk.h
+++ b/src/dawn_native/vulkan/TextureVk.h
@@ -18,20 +18,39 @@
 #include "dawn_native/Texture.h"
 
 #include "common/vulkan_platform.h"
+#include "dawn_native/vulkan/ExternalHandle.h"
 #include "dawn_native/vulkan/MemoryAllocator.h"
 
 namespace dawn_native { namespace vulkan {
 
     struct CommandRecordingContext;
+    struct ExternalImageDescriptor;
 
     VkFormat VulkanImageFormat(dawn::TextureFormat format);
     VkImageUsageFlags VulkanImageUsage(dawn::TextureUsageBit usage, const Format& format);
     VkSampleCountFlagBits VulkanSampleCount(uint32_t sampleCount);
 
+    MaybeError ValidateVulkanImageCanBeWrapped(const DeviceBase* device,
+                                               const TextureDescriptor* descriptor);
+
     class Texture : public TextureBase {
       public:
+        enum class ExternalState {
+            InternalOnly,
+            PendingAcquire,
+            Acquired,
+            PendingRelease,
+            Released
+        };
+
         Texture(Device* device, const TextureDescriptor* descriptor);
         Texture(Device* device, const TextureDescriptor* descriptor, VkImage nativeImage);
+        Texture(Device* device,
+                const ExternalImageDescriptor* descriptor,
+                const TextureDescriptor* textureDescriptor,
+                VkSemaphore signalSemaphore,
+                VkDeviceMemory externalMemoryAllocation,
+                std::vector<VkSemaphore> waitSemaphores);
         ~Texture();
 
         VkImage GetHandle() const;
@@ -48,6 +67,8 @@
                                                  uint32_t baseArrayLayer,
                                                  uint32_t layerCount);
 
+        MaybeError SignalAndDestroy(VkSemaphore* outSignalSemaphore);
+
       private:
         void DestroyImpl() override;
         void ClearTexture(CommandRecordingContext* recordingContext,
@@ -58,6 +79,12 @@
 
         VkImage mHandle = VK_NULL_HANDLE;
         DeviceMemoryAllocation mMemoryAllocation;
+        VkDeviceMemory mExternalAllocation = VK_NULL_HANDLE;
+
+        ExternalState mExternalState = ExternalState::InternalOnly;
+        ExternalState mLastExternalState = ExternalState::InternalOnly;
+        VkSemaphore mSignalSemaphore = VK_NULL_HANDLE;
+        std::vector<VkSemaphore> mWaitRequirements;
 
         // A usage of none will make sure the texture is transitioned before its first use as
         // required by the spec.
diff --git a/src/dawn_native/vulkan/VulkanBackend.cpp b/src/dawn_native/vulkan/VulkanBackend.cpp
index acc624a..566605e 100644
--- a/src/dawn_native/vulkan/VulkanBackend.cpp
+++ b/src/dawn_native/vulkan/VulkanBackend.cpp
@@ -24,6 +24,7 @@
 #include "common/SwapChainUtils.h"
 #include "dawn_native/vulkan/DeviceVk.h"
 #include "dawn_native/vulkan/NativeSwapChainImplVk.h"
+#include "dawn_native/vulkan/TextureVk.h"
 
 namespace dawn_native { namespace vulkan {
 
@@ -52,4 +53,32 @@
         return static_cast<DawnTextureFormat>(impl->GetPreferredFormat());
     }
 
+#ifdef DAWN_PLATFORM_LINUX
+    DawnTexture WrapVulkanImageOpaqueFD(DawnDevice cDevice,
+                                        const ExternalImageDescriptorOpaqueFD* descriptor) {
+        Device* device = reinterpret_cast<Device*>(cDevice);
+
+        TextureBase* texture = device->CreateTextureWrappingVulkanImage(
+            descriptor, descriptor->memoryFD, descriptor->waitFDs);
+
+        return reinterpret_cast<DawnTexture>(texture);
+    }
+
+    int ExportSignalSemaphoreOpaqueFD(DawnDevice cDevice, DawnTexture cTexture) {
+        Device* device = reinterpret_cast<Device*>(cDevice);
+        Texture* texture = reinterpret_cast<Texture*>(cTexture);
+
+        if (!texture) {
+            return -1;
+        }
+
+        ExternalSemaphoreHandle outHandle;
+        if (device->ConsumedError(device->SignalAndExportExternalTexture(texture, &outHandle))) {
+            return -1;
+        }
+
+        return outHandle;
+    }
+#endif
+
 }}  // namespace dawn_native::vulkan
diff --git a/src/dawn_native/vulkan/external_memory/MemoryService.h b/src/dawn_native/vulkan/external_memory/MemoryService.h
new file mode 100644
index 0000000..8d27db6
--- /dev/null
+++ b/src/dawn_native/vulkan/external_memory/MemoryService.h
@@ -0,0 +1,50 @@
+// 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.
+
+#ifndef DAWNNATIVE_VULKAN_EXTERNALMEMORY_SERVICE_H_
+#define DAWNNATIVE_VULKAN_EXTERNALMEMORY_SERVICE_H_
+
+#include "common/vulkan_platform.h"
+#include "dawn_native/Error.h"
+#include "dawn_native/vulkan/ExternalHandle.h"
+
+namespace dawn_native { namespace vulkan {
+    class Device;
+}}  // namespace dawn_native::vulkan
+
+namespace dawn_native { namespace vulkan { namespace external_memory {
+
+    class Service {
+      public:
+        explicit Service(Device* device);
+        ~Service();
+
+        // True if the device reports it supports this feature
+        bool Supported();
+
+        // Given an external handle pointing to memory, import it into a VkDeviceMemory
+        ResultOrError<VkDeviceMemory> ImportMemory(ExternalMemoryHandle handle,
+                                                   VkDeviceSize allocationSize,
+                                                   uint32_t memoryTypeIndex);
+
+      private:
+        Device* mDevice = nullptr;
+
+        // True if early checks pass that determine if the service is supported
+        bool mSupportedFirstPass = false;
+    };
+
+}}}  // namespace dawn_native::vulkan::external_memory
+
+#endif  // DAWNNATIVE_VULKAN_EXTERNALMEMORY_SERVICE_H_
diff --git a/src/dawn_native/vulkan/external_memory/MemoryServiceNull.cpp b/src/dawn_native/vulkan/external_memory/MemoryServiceNull.cpp
new file mode 100644
index 0000000..d10ddc7
--- /dev/null
+++ b/src/dawn_native/vulkan/external_memory/MemoryServiceNull.cpp
@@ -0,0 +1,37 @@
+// 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 "dawn_native/vulkan/DeviceVk.h"
+#include "dawn_native/vulkan/external_memory/MemoryService.h"
+
+namespace dawn_native { namespace vulkan { namespace external_memory {
+
+    Service::Service(Device* device) : mDevice(device) {
+        DAWN_UNUSED(mDevice);
+        DAWN_UNUSED(mSupportedFirstPass);
+    }
+
+    Service::~Service() = default;
+
+    bool Service::Supported() {
+        return false;
+    }
+
+    ResultOrError<VkDeviceMemory> Service::ImportMemory(ExternalMemoryHandle handle,
+                                                        VkDeviceSize allocationSize,
+                                                        uint32_t memoryTypeIndex) {
+        return DAWN_UNIMPLEMENTED_ERROR("Using null semaphore service to interop inside Vulkan");
+    }
+
+}}}  // namespace dawn_native::vulkan::external_memory
diff --git a/src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp b/src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp
new file mode 100644
index 0000000..e1f38db
--- /dev/null
+++ b/src/dawn_native/vulkan/external_memory/MemoryServiceOpaqueFD.cpp
@@ -0,0 +1,61 @@
+// 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 "dawn_native/vulkan/DeviceVk.h"
+#include "dawn_native/vulkan/VulkanError.h"
+#include "dawn_native/vulkan/external_memory/MemoryService.h"
+
+namespace dawn_native { namespace vulkan { namespace external_memory {
+
+    Service::Service(Device* device) : mDevice(device) {
+        const VulkanDeviceInfo& info = mDevice->GetDeviceInfo();
+        mSupportedFirstPass = info.externalMemory && info.externalMemoryFD;
+    }
+
+    Service::~Service() = default;
+
+    bool Service::Supported() {
+        // TODO(idanr): Query device here for additional support information, decide if supported
+        // This will likely be done through vkGetPhysicalDeviceImageFormatProperties2KHR, where
+        // we give it the intended image properties and handle type and see if it's supported
+        return mSupportedFirstPass;
+    }
+
+    ResultOrError<VkDeviceMemory> Service::ImportMemory(ExternalMemoryHandle handle,
+                                                        VkDeviceSize allocationSize,
+                                                        uint32_t memoryTypeIndex) {
+        if (handle < 0) {
+            return DAWN_VALIDATION_ERROR("Trying to import memory with invalid handle");
+        }
+
+        VkImportMemoryFdInfoKHR importMemoryFdInfo;
+        importMemoryFdInfo.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR;
+        importMemoryFdInfo.pNext = nullptr;
+        importMemoryFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
+        importMemoryFdInfo.fd = handle;
+
+        VkMemoryAllocateInfo allocateInfo;
+        allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+        allocateInfo.pNext = &importMemoryFdInfo;
+        allocateInfo.allocationSize = allocationSize;
+        allocateInfo.memoryTypeIndex = memoryTypeIndex;
+
+        VkDeviceMemory allocatedMemory = VK_NULL_HANDLE;
+        DAWN_TRY(CheckVkSuccess(mDevice->fn.AllocateMemory(mDevice->GetVkDevice(), &allocateInfo,
+                                                           nullptr, &allocatedMemory),
+                                "vkAllocateMemory"));
+        return allocatedMemory;
+    }
+
+}}}  // namespace dawn_native::vulkan::external_memory
diff --git a/src/dawn_native/vulkan/external_semaphore/SemaphoreService.h b/src/dawn_native/vulkan/external_semaphore/SemaphoreService.h
new file mode 100644
index 0000000..bfa72c4
--- /dev/null
+++ b/src/dawn_native/vulkan/external_semaphore/SemaphoreService.h
@@ -0,0 +1,54 @@
+// 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.
+
+#ifndef DAWNNATIVE_VULKAN_EXTERNALSEMAPHORE_SERVICE_H_
+#define DAWNNATIVE_VULKAN_EXTERNALSEMAPHORE_SERVICE_H_
+
+#include "common/vulkan_platform.h"
+#include "dawn_native/Error.h"
+#include "dawn_native/vulkan/ExternalHandle.h"
+
+namespace dawn_native { namespace vulkan {
+    class Device;
+}}  // namespace dawn_native::vulkan
+
+namespace dawn_native { namespace vulkan { namespace external_semaphore {
+
+    class Service {
+      public:
+        explicit Service(Device* device);
+        ~Service();
+
+        // True if the device reports it supports this feature
+        bool Supported();
+
+        // Given an external handle, import it into a VkSemaphore
+        ResultOrError<VkSemaphore> ImportSemaphore(ExternalSemaphoreHandle handle);
+
+        // Create a VkSemaphore that is exportable into an external handle later
+        ResultOrError<VkSemaphore> CreateExportableSemaphore();
+
+        // Export a VkSemaphore into an external handle
+        ResultOrError<ExternalSemaphoreHandle> ExportSemaphore(VkSemaphore semaphore);
+
+      private:
+        Device* mDevice = nullptr;
+
+        // True if early checks pass that determine if the service is supported
+        bool mSupportedFirstPass = false;
+    };
+
+}}}  // namespace dawn_native::vulkan::external_semaphore
+
+#endif  // DAWNNATIVE_VULKAN_EXTERNALSEMAPHORE_SERVICE_H_
diff --git a/src/dawn_native/vulkan/external_semaphore/SemaphoreServiceNull.cpp b/src/dawn_native/vulkan/external_semaphore/SemaphoreServiceNull.cpp
new file mode 100644
index 0000000..e7fd859
--- /dev/null
+++ b/src/dawn_native/vulkan/external_semaphore/SemaphoreServiceNull.cpp
@@ -0,0 +1,43 @@
+// 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 "dawn_native/vulkan/DeviceVk.h"
+#include "dawn_native/vulkan/external_semaphore/SemaphoreService.h"
+
+namespace dawn_native { namespace vulkan { namespace external_semaphore {
+
+    Service::Service(Device* device) : mDevice(device) {
+        DAWN_UNUSED(mDevice);
+        DAWN_UNUSED(mSupportedFirstPass);
+    }
+
+    Service::~Service() = default;
+
+    bool Service::Supported() {
+        return false;
+    }
+
+    ResultOrError<VkSemaphore> Service::ImportSemaphore(ExternalSemaphoreHandle handle) {
+        return DAWN_UNIMPLEMENTED_ERROR("Using null semaphore service to interop inside Vulkan");
+    }
+
+    ResultOrError<VkSemaphore> Service::CreateExportableSemaphore() {
+        return DAWN_UNIMPLEMENTED_ERROR("Using null semaphore service to interop inside Vulkan");
+    }
+
+    ResultOrError<ExternalSemaphoreHandle> Service::ExportSemaphore(VkSemaphore semaphore) {
+        return DAWN_UNIMPLEMENTED_ERROR("Using null semaphore service to interop inside Vulkan");
+    }
+
+}}}  // namespace dawn_native::vulkan::external_semaphore
diff --git a/src/dawn_native/vulkan/external_semaphore/SemaphoreServiceOpaqueFD.cpp b/src/dawn_native/vulkan/external_semaphore/SemaphoreServiceOpaqueFD.cpp
new file mode 100644
index 0000000..7312142
--- /dev/null
+++ b/src/dawn_native/vulkan/external_semaphore/SemaphoreServiceOpaqueFD.cpp
@@ -0,0 +1,105 @@
+// 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 "dawn_native/vulkan/DeviceVk.h"
+#include "dawn_native/vulkan/VulkanError.h"
+#include "dawn_native/vulkan/external_semaphore/SemaphoreService.h"
+
+namespace dawn_native { namespace vulkan { namespace external_semaphore {
+
+    Service::Service(Device* device) : mDevice(device) {
+        const VulkanDeviceInfo& info = mDevice->GetDeviceInfo();
+        mSupportedFirstPass = info.externalSemaphore && info.externalSemaphoreFD;
+        // TODO(idanr): Query device here for additional support information, decide if supported
+        // This will likely be done through vkGetPhysicalDeviceExternalSemaphorePropertiesKHR, where
+        // we give it the intended handle type and see if it's supported
+    }
+
+    Service::~Service() = default;
+
+    bool Service::Supported() {
+        return mSupportedFirstPass;
+    }
+
+    ResultOrError<VkSemaphore> Service::ImportSemaphore(ExternalSemaphoreHandle handle) {
+        if (handle < 0) {
+            return DAWN_VALIDATION_ERROR("Trying to import semaphore with invalid handle");
+        }
+
+        VkSemaphore semaphore = VK_NULL_HANDLE;
+        VkSemaphoreCreateInfo info;
+        info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+        info.pNext = nullptr;
+        info.flags = 0;
+
+        DAWN_TRY(CheckVkSuccess(
+            mDevice->fn.CreateSemaphore(mDevice->GetVkDevice(), &info, nullptr, &semaphore),
+            "vkCreateSemaphore"));
+
+        VkImportSemaphoreFdInfoKHR importSemaphoreFdInfo;
+        importSemaphoreFdInfo.sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR;
+        importSemaphoreFdInfo.pNext = nullptr;
+        importSemaphoreFdInfo.semaphore = semaphore;
+        importSemaphoreFdInfo.flags = 0;
+        importSemaphoreFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
+        importSemaphoreFdInfo.fd = handle;
+
+        MaybeError status = CheckVkSuccess(
+            mDevice->fn.ImportSemaphoreFdKHR(mDevice->GetVkDevice(), &importSemaphoreFdInfo),
+            "vkImportSemaphoreFdKHR");
+
+        if (status.IsError()) {
+            mDevice->fn.DestroySemaphore(mDevice->GetVkDevice(), semaphore, nullptr);
+            DAWN_TRY(std::move(status));
+        }
+
+        return semaphore;
+    }
+
+    ResultOrError<VkSemaphore> Service::CreateExportableSemaphore() {
+        VkExportSemaphoreCreateInfoKHR exportSemaphoreInfo;
+        exportSemaphoreInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO_KHR;
+        exportSemaphoreInfo.pNext = nullptr;
+        exportSemaphoreInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
+
+        VkSemaphoreCreateInfo semaphoreCreateInfo;
+        semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+        semaphoreCreateInfo.pNext = &exportSemaphoreInfo;
+        semaphoreCreateInfo.flags = 0;
+
+        VkSemaphore signalSemaphore;
+        DAWN_TRY(
+            CheckVkSuccess(mDevice->fn.CreateSemaphore(mDevice->GetVkDevice(), &semaphoreCreateInfo,
+                                                       nullptr, &signalSemaphore),
+                           "vkCreateSemaphore"));
+        return signalSemaphore;
+    }
+
+    ResultOrError<ExternalSemaphoreHandle> Service::ExportSemaphore(VkSemaphore semaphore) {
+        VkSemaphoreGetFdInfoKHR semaphoreGetFdInfo;
+        semaphoreGetFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR;
+        semaphoreGetFdInfo.pNext = nullptr;
+        semaphoreGetFdInfo.semaphore = semaphore;
+        semaphoreGetFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
+
+        int fd = -1;
+        DAWN_TRY(CheckVkSuccess(
+            mDevice->fn.GetSemaphoreFdKHR(mDevice->GetVkDevice(), &semaphoreGetFdInfo, &fd),
+            "vkGetSemaphoreFdKHR"));
+
+        ASSERT(fd >= 0);
+        return fd;
+    }
+
+}}}  // namespace dawn_native::vulkan::external_semaphore
diff --git a/src/include/dawn_native/VulkanBackend.h b/src/include/dawn_native/VulkanBackend.h
index eeb2b0b..f874259 100644
--- a/src/include/dawn_native/VulkanBackend.h
+++ b/src/include/dawn_native/VulkanBackend.h
@@ -23,12 +23,44 @@
 #include <vector>
 
 namespace dawn_native { namespace vulkan {
+
+    // Common properties of external images
+    struct ExternalImageDescriptor {
+        const DawnTextureDescriptor* cTextureDescriptor;  // Must match image creation params
+        bool isCleared;               // Sets whether the texture will be cleared before use
+        VkDeviceSize allocationSize;  // Must match VkMemoryAllocateInfo from image creation
+        uint32_t memoryTypeIndex;     // Must match VkMemoryAllocateInfo from image creation
+    };
+
     DAWN_NATIVE_EXPORT VkInstance GetInstance(DawnDevice device);
 
     DAWN_NATIVE_EXPORT DawnSwapChainImplementation CreateNativeSwapChainImpl(DawnDevice device,
                                                                              VkSurfaceKHR surface);
     DAWN_NATIVE_EXPORT DawnTextureFormat
     GetNativeSwapChainPreferredFormat(const DawnSwapChainImplementation* swapChain);
+
+// Can't use DAWN_PLATFORM_LINUX since header included in both dawn and chrome
+#ifdef __linux__
+        // Descriptor for opaque file descriptor image import
+        struct ExternalImageDescriptorOpaqueFD : ExternalImageDescriptor {
+            int memoryFD;  // A file descriptor from an export of the memory of the image
+            std::vector<int> waitFDs;  // File descriptors of semaphores which will be waited on
+        };
+
+        // Imports an external vulkan image from an opaque file descriptor. Internally, this uses
+        // external memory / semaphore extensions to import the image. Then, waits on the provided
+        // |descriptor->waitFDs| before the texture can be used. Finally, a signal semaphore
+        // can be exported, transferring control back to the caller.
+        // On failure, returns a nullptr
+        DAWN_NATIVE_EXPORT DawnTexture
+        WrapVulkanImageOpaqueFD(DawnDevice cDevice,
+                                const ExternalImageDescriptorOpaqueFD* descriptor);
+
+        // Exports a signal semaphore from a wrapped texture. This must be called on wrapped
+        // textures before they are destroyed. On failure, returns -1
+        DAWN_NATIVE_EXPORT int ExportSignalSemaphoreOpaqueFD(DawnDevice cDevice,
+                                                             DawnTexture cTexture);
+#endif  // __linux__
 }}  // namespace dawn_native::vulkan
 
 #endif  // DAWNNATIVE_VULKANBACKEND_H_
diff --git a/src/tests/white_box/VulkanImageWrappingTests.cpp b/src/tests/white_box/VulkanImageWrappingTests.cpp
new file mode 100644
index 0000000..4284a00
--- /dev/null
+++ b/src/tests/white_box/VulkanImageWrappingTests.cpp
@@ -0,0 +1,912 @@
+// 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 "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/MemoryAllocator.h"
+#include "dawn_native/vulkan/TextureVk.h"
+#include "utils/DawnHelpers.h"
+#include "utils/SystemUtils.h"
+
+// TODO(crbug.com/966500): Intel is disabled until upgrade is finished
+
+namespace {
+
+    class VulkanImageWrappingTestBase : public DawnTest {
+      public:
+        void SetUp() override {
+            DawnTest::SetUp();
+            if (UsesWire() || IsIntel()) {
+                return;
+            }
+
+            deviceVk = reinterpret_cast<dawn_native::vulkan::Device*>(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->GetMemoryAllocator()->FindBestTypeIndex(requirements, false);
+            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
+        dawn::Texture WrapVulkanImage(dawn::Device device,
+                                      const dawn::TextureDescriptor* textureDescriptor,
+                                      int memoryFd,
+                                      VkDeviceSize allocationSize,
+                                      uint32_t memoryTypeIndex,
+                                      std::vector<int> waitFDs,
+                                      bool isCleared = true,
+                                      bool expectValid = true) {
+            dawn_native::vulkan::ExternalImageDescriptorOpaqueFD descriptor;
+            descriptor.cTextureDescriptor =
+                reinterpret_cast<const DawnTextureDescriptor*>(textureDescriptor);
+            descriptor.isCleared = isCleared;
+            descriptor.allocationSize = allocationSize;
+            descriptor.memoryTypeIndex = memoryTypeIndex;
+            descriptor.memoryFD = memoryFd;
+            descriptor.waitFDs = waitFDs;
+
+            DawnTexture texture =
+                dawn_native::vulkan::WrapVulkanImageOpaqueFD(device.Get(), &descriptor);
+
+            if (expectValid) {
+                EXPECT_NE(texture, nullptr) << "Failed to wrap image, are external memory / "
+                                               "semaphore extensions supported?";
+            } else {
+                EXPECT_EQ(texture, nullptr);
+            }
+
+            return dawn::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(dawn::Device device, dawn::Texture wrappedTexture) {
+            int fd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(device.Get(),
+                                                                        wrappedTexture.Get());
+            ASSERT_NE(fd, -1);
+            close(fd);
+        }
+
+      protected:
+        dawn_native::vulkan::Device* deviceVk;
+    };
+
+}  // anonymous namespace
+
+class VulkanImageWrappingValidationTests : public VulkanImageWrappingTestBase {
+  public:
+    void SetUp() override {
+        VulkanImageWrappingTestBase::SetUp();
+        if (UsesWire() || IsIntel()) {
+            return;
+        }
+
+        CreateBindExportImage(deviceVk, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, &defaultImage,
+                              &defaultAllocation, &defaultAllocationSize, &defaultMemoryTypeIndex,
+                              &defaultFd);
+        defaultDescriptor.dimension = dawn::TextureDimension::e2D;
+        defaultDescriptor.format = dawn::TextureFormat::RGBA8Unorm;
+        defaultDescriptor.size = {1, 1, 1};
+        defaultDescriptor.sampleCount = 1;
+        defaultDescriptor.arrayLayerCount = 1;
+        defaultDescriptor.mipLevelCount = 1;
+        defaultDescriptor.usage = dawn::TextureUsageBit::OutputAttachment |
+                                  dawn::TextureUsageBit::CopySrc | dawn::TextureUsageBit::CopyDst;
+    }
+
+    void TearDown() override {
+        if (UsesWire() || IsIntel()) {
+            VulkanImageWrappingTestBase::TearDown();
+            return;
+        }
+
+        deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultImage);
+        deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultAllocation);
+        VulkanImageWrappingTestBase::TearDown();
+    }
+
+  protected:
+    dawn::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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    dawn::Texture texture =
+        WrapVulkanImage(device, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {}, true, true);
+    EXPECT_NE(texture.Get(), nullptr);
+    IgnoreSignalSemaphore(device, texture);
+}
+
+// Test an error occurs if the texture descriptor is missing
+TEST_P(VulkanImageWrappingValidationTests, MissingTextureDescriptor) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    ASSERT_DEVICE_ERROR(dawn::Texture texture =
+                            WrapVulkanImage(device, nullptr, defaultFd, defaultAllocationSize,
+                                            defaultMemoryTypeIndex, {}, true, false));
+    EXPECT_EQ(texture.Get(), nullptr);
+}
+
+// Test an error occurs if the texture descriptor is invalid
+TEST_P(VulkanImageWrappingValidationTests, InvalidTextureDescriptor) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    defaultDescriptor.nextInChain = this;
+
+    ASSERT_DEVICE_ERROR(dawn::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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    defaultDescriptor.dimension = dawn::TextureDimension::e1D;
+
+    ASSERT_DEVICE_ERROR(dawn::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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    defaultDescriptor.mipLevelCount = 2;
+
+    ASSERT_DEVICE_ERROR(dawn::Texture texture = WrapVulkanImage(
+                            device, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                            defaultMemoryTypeIndex, {}, true, false));
+    EXPECT_EQ(texture.Get(), nullptr);
+}
+
+// Test an error occurs if the descriptor array layer count isn't 1
+TEST_P(VulkanImageWrappingValidationTests, InvalidArrayLayerCount) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    defaultDescriptor.arrayLayerCount = 2;
+
+    ASSERT_DEVICE_ERROR(dawn::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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    defaultDescriptor.sampleCount = 4;
+
+    ASSERT_DEVICE_ERROR(dawn::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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    dawn::Texture texture =
+        WrapVulkanImage(device, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {}, true, true);
+    ASSERT_NE(texture.Get(), nullptr);
+    IgnoreSignalSemaphore(device, texture);
+    ASSERT_DEVICE_ERROR(
+        int fd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(device.Get(), texture.Get()));
+    ASSERT_EQ(fd, -1);
+}
+
+// Test an error occurs if we try to export the signal semaphore from a normal texture
+TEST_P(VulkanImageWrappingValidationTests, NormalTextureSignalSemaphoreExport) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    dawn::Texture texture = device.CreateTexture(&defaultDescriptor);
+    ASSERT_NE(texture.Get(), nullptr);
+    ASSERT_DEVICE_ERROR(
+        int fd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(device.Get(), texture.Get()));
+    ASSERT_EQ(fd, -1);
+}
+
+// Test an error occurs if we try to export the signal semaphore from a destroyed texture
+TEST_P(VulkanImageWrappingValidationTests, DestroyedTextureSignalSemaphoreExport) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    dawn::Texture texture = device.CreateTexture(&defaultDescriptor);
+    ASSERT_NE(texture.Get(), nullptr);
+    texture.Destroy();
+    ASSERT_DEVICE_ERROR(
+        int fd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(device.Get(), texture.Get()));
+    ASSERT_EQ(fd, -1);
+}
+
+// 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() || IsIntel()) {
+            return;
+        }
+
+        // Create another device based on the original
+        backendAdapter = reinterpret_cast<dawn_native::vulkan::Adapter*>(deviceVk->GetAdapter());
+        deviceDescriptor.forceEnabledToggles = GetParam().forceEnabledWorkarounds;
+        deviceDescriptor.forceDisabledToggles = GetParam().forceDisabledWorkarounds;
+
+        secondDeviceVk = reinterpret_cast<dawn_native::vulkan::Device*>(
+            backendAdapter->CreateDevice(&deviceDescriptor));
+        secondDevice = dawn::Device::Acquire(reinterpret_cast<DawnDevice>(secondDeviceVk));
+
+        CreateBindExportImage(deviceVk, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, &defaultImage,
+                              &defaultAllocation, &defaultAllocationSize, &defaultMemoryTypeIndex,
+                              &defaultFd);
+        defaultDescriptor.dimension = dawn::TextureDimension::e2D;
+        defaultDescriptor.format = dawn::TextureFormat::RGBA8Unorm;
+        defaultDescriptor.size = {1, 1, 1};
+        defaultDescriptor.sampleCount = 1;
+        defaultDescriptor.arrayLayerCount = 1;
+        defaultDescriptor.mipLevelCount = 1;
+        defaultDescriptor.usage = dawn::TextureUsageBit::OutputAttachment |
+                                  dawn::TextureUsageBit::CopySrc | dawn::TextureUsageBit::CopyDst;
+    }
+
+    void TearDown() override {
+        if (UsesWire() || IsIntel()) {
+            VulkanImageWrappingTestBase::TearDown();
+            return;
+        }
+
+        deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultImage);
+        deviceVk->GetFencedDeleter()->DeleteWhenUnused(defaultAllocation);
+        VulkanImageWrappingTestBase::TearDown();
+    }
+
+  protected:
+    dawn::Device secondDevice;
+    dawn_native::vulkan::Device* secondDeviceVk;
+
+    dawn_native::vulkan::Adapter* backendAdapter;
+    dawn_native::DeviceDescriptor deviceDescriptor;
+
+    dawn::TextureDescriptor defaultDescriptor;
+    VkImage defaultImage;
+    VkDeviceMemory defaultAllocation;
+    VkDeviceSize defaultAllocationSize;
+    uint32_t defaultMemoryTypeIndex;
+    int defaultFd;
+
+    // Clear a texture on a given device
+    void ClearImage(dawn::Device device, dawn::Texture wrappedTexture, dawn::Color clearColor) {
+        dawn::TextureView wrappedView = wrappedTexture.CreateDefaultView();
+
+        // Submit a clear operation
+        utils::ComboRenderPassDescriptor renderPassDescriptor({wrappedView}, {});
+        renderPassDescriptor.cColorAttachmentsInfoPtr[0]->clearColor = clearColor;
+
+        dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDescriptor);
+        pass.EndPass();
+
+        dawn::CommandBuffer commands = encoder.Finish();
+
+        dawn::Queue queue = device.CreateQueue();
+        queue.Submit(1, &commands);
+    }
+
+    // Submits a 1x1x1 copy from source to destination
+    void SimpleCopyTextureToTexture(dawn::Device device,
+                                    dawn::Queue queue,
+                                    dawn::Texture source,
+                                    dawn::Texture destination) {
+        dawn::TextureCopyView copySrc;
+        copySrc.texture = source;
+        copySrc.mipLevel = 0;
+        copySrc.arrayLayer = 0;
+        copySrc.origin = {0, 0, 0};
+
+        dawn::TextureCopyView copyDst;
+        copyDst.texture = destination;
+        copyDst.mipLevel = 0;
+        copyDst.arrayLayer = 0;
+        copyDst.origin = {0, 0, 0};
+
+        dawn::Extent3D copySize = {1, 1, 1};
+
+        dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyTextureToTexture(&copySrc, &copyDst, &copySize);
+        dawn::CommandBuffer commands = encoder.Finish();
+
+        queue.Submit(1, &commands);
+    }
+};
+
+// Clear an image in |secondDevice|
+// Verify clear color is visible in |device|
+TEST_P(VulkanImageWrappingUsageTests, ClearImageAcrossDevices) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // Import the image on |secondDevice|
+    dawn::Texture wrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |secondDevice|
+    ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+    int signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                      wrappedTexture.Get());
+
+    // Import the image to |device|, making sure we wait on signalFd
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture nextWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Verify |device| sees the changes from |secondDevice|
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
+
+    IgnoreSignalSemaphore(device, nextWrappedTexture);
+}
+
+// Import texture to |device| and |secondDevice|
+// Clear image in |secondDevice|
+// Verify clear color is visible in |device|
+// Verify the very first import into |device| also sees the change, since it should
+// alias the same memory
+TEST_P(VulkanImageWrappingUsageTests, ClearImageAcrossDevicesAliased) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+    // Import the image on |device
+    dawn::Texture wrappedTextureAlias = WrapVulkanImage(
+        device, &defaultDescriptor, defaultFd, defaultAllocationSize, defaultMemoryTypeIndex, {});
+
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+
+    // Import the image on |secondDevice|
+    dawn::Texture wrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |secondDevice|
+    ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+    int signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                      wrappedTexture.Get());
+
+    // Import the image to |device|, making sure we wait on signalFd
+    memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture nextWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Verify |device| sees the changes from |secondDevice| (waits)
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
+
+    // Verify aliased texture sees changes from |secondDevice| (without waiting!)
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), wrappedTextureAlias, 0, 0);
+
+    IgnoreSignalSemaphore(device, nextWrappedTexture);
+    IgnoreSignalSemaphore(device, wrappedTextureAlias);
+}
+
+// Clear an image in |secondDevice|
+// Verify clear color is not visible in |device| if we import the texture as not cleared
+TEST_P(VulkanImageWrappingUsageTests, UnclearedTextureIsCleared) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // Import the image on |secondDevice|
+    dawn::Texture wrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |secondDevice|
+    ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+    int signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                      wrappedTexture.Get());
+
+    // Import the image to |device|, making sure we wait on signalFd
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture nextWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd}, false);
+
+    // Verify |device| doesn't see the changes from |secondDevice|
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8(0, 0, 0, 0), nextWrappedTexture, 0, 0);
+
+    IgnoreSignalSemaphore(device, 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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // Import the image on |secondDevice|
+    dawn::Texture wrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |secondDevice|
+    ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+    int signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                      wrappedTexture.Get());
+
+    // Import the image to |device|, making sure we wait on |signalFd|
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture deviceWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Create a second texture on |device|
+    dawn::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(device, 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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // Import the image on |device|
+    dawn::Texture wrappedTexture = WrapVulkanImage(
+        device, &defaultDescriptor, defaultFd, defaultAllocationSize, defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |device|
+    ClearImage(device, wrappedTexture, {5 / 255.0f, 6 / 255.0f, 7 / 255.0f, 8 / 255.0f});
+
+    int signalFd =
+        dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(device.Get(), wrappedTexture.Get());
+
+    // Import the image to |secondDevice|, making sure we wait on |signalFd|
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture secondDeviceWrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Create a texture with color B on |secondDevice|
+    dawn::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|
+    dawn::Queue secondDeviceQueue = secondDevice.CreateQueue();
+    SimpleCopyTextureToTexture(secondDevice, secondDeviceQueue, copySrcTexture,
+                               secondDeviceWrappedTexture);
+
+    // Re-import back into |device|, waiting on |secondDevice|'s signal
+    signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                  secondDeviceWrappedTexture.Get());
+    memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+
+    dawn::Texture nextWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Verify |nextWrappedTexture| contains the color from our copy
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
+
+    IgnoreSignalSemaphore(device, 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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // Import the image on |secondDevice|
+    dawn::Texture wrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |secondDevice|
+    ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+    int signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                      wrappedTexture.Get());
+
+    // Import the image to |device|, making sure we wait on |signalFd|
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture deviceWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Create a destination buffer on |device|
+    dawn::BufferDescriptor bufferDesc;
+    bufferDesc.size = 4;
+    bufferDesc.usage = dawn::BufferUsageBit::CopyDst | dawn::BufferUsageBit::CopySrc;
+    dawn::Buffer copyDstBuffer = device.CreateBuffer(&bufferDesc);
+
+    // Copy |deviceWrappedTexture| into |copyDstBuffer|
+    dawn::TextureCopyView copySrc;
+    copySrc.texture = deviceWrappedTexture;
+    copySrc.mipLevel = 0;
+    copySrc.arrayLayer = 0;
+    copySrc.origin = {0, 0, 0};
+
+    dawn::BufferCopyView copyDst;
+    copyDst.buffer = copyDstBuffer;
+    copyDst.offset = 0;
+    copyDst.rowPitch = 256;
+    copyDst.imageHeight = 0;
+
+    dawn::Extent3D copySize = {1, 1, 1};
+
+    dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+    encoder.CopyTextureToBuffer(&copySrc, &copyDst, &copySize);
+    dawn::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(device, 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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // Import the image on |device|
+    dawn::Texture wrappedTexture = WrapVulkanImage(
+        device, &defaultDescriptor, defaultFd, defaultAllocationSize, defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |device|
+    ClearImage(device, wrappedTexture, {5 / 255.0f, 6 / 255.0f, 7 / 255.0f, 8 / 255.0f});
+
+    int signalFd =
+        dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(device.Get(), wrappedTexture.Get());
+
+    // Import the image to |secondDevice|, making sure we wait on |signalFd|
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture secondDeviceWrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Copy color B on |secondDevice|
+    dawn::Queue secondDeviceQueue = secondDevice.CreateQueue();
+
+    // Create a buffer on |secondDevice|
+    dawn::Buffer copySrcBuffer =
+        utils::CreateBufferFromData(secondDevice, dawn::BufferUsageBit::CopySrc, {0x04030201});
+
+    // Copy |copySrcBuffer| into |secondDeviceWrappedTexture|
+    dawn::BufferCopyView copySrc;
+    copySrc.buffer = copySrcBuffer;
+    copySrc.offset = 0;
+    copySrc.rowPitch = 256;
+    copySrc.imageHeight = 0;
+
+    dawn::TextureCopyView copyDst;
+    copyDst.texture = secondDeviceWrappedTexture;
+    copyDst.mipLevel = 0;
+    copyDst.arrayLayer = 0;
+    copyDst.origin = {0, 0, 0};
+
+    dawn::Extent3D copySize = {1, 1, 1};
+
+    dawn::CommandEncoder encoder = secondDevice.CreateCommandEncoder();
+    encoder.CopyBufferToTexture(&copySrc, &copyDst, &copySize);
+    dawn::CommandBuffer commands = encoder.Finish();
+    secondDeviceQueue.Submit(1, &commands);
+
+    // Re-import back into |device|, waiting on |secondDevice|'s signal
+    signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                  secondDeviceWrappedTexture.Get());
+    memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+
+    dawn::Texture nextWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Verify |nextWrappedTexture| contains the color from our copy
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8(1, 2, 3, 4), nextWrappedTexture, 0, 0);
+
+    IgnoreSignalSemaphore(device, 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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // Import the image on |secondDevice|
+    dawn::Texture wrappedTexture =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, defaultFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {});
+
+    // Clear |wrappedTexture| on |secondDevice|
+    ClearImage(secondDevice, wrappedTexture, {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, 4 / 255.0f});
+
+    int signalFd = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(secondDevice.Get(),
+                                                                      wrappedTexture.Get());
+
+    // Import the image to |device|, making sure we wait on |signalFd|
+    int memoryFd = GetMemoryFd(deviceVk, defaultAllocation);
+    dawn::Texture deviceWrappedTexture =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFd, defaultAllocationSize,
+                        defaultMemoryTypeIndex, {signalFd});
+
+    // Create a second texture on |device|
+    dawn::Texture copyDstTexture = device.CreateTexture(&defaultDescriptor);
+
+    // Create a third texture on |device|
+    dawn::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(device, 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) {
+    DAWN_SKIP_TEST_IF(UsesWire() || IsIntel());
+
+    // 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 = reinterpret_cast<dawn_native::vulkan::Device*>(
+        backendAdapter->CreateDevice(&deviceDescriptor));
+    dawn::Device thirdDevice = dawn::Device::Acquire(reinterpret_cast<DawnDevice>(thirdDeviceVk));
+
+    // Make queue for device 2 and 3
+    dawn::Queue secondDeviceQueue = secondDevice.CreateQueue();
+    dawn::Queue thirdDeviceQueue = thirdDevice.CreateQueue();
+
+    // 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
+    dawn::Texture wrappedTexADevice3 = WrapVulkanImage(thirdDevice, &defaultDescriptor, memoryFdA,
+                                                       allocationSizeA, memoryTypeIndexA, {});
+
+    dawn::Texture wrappedTexBDevice3 = WrapVulkanImage(thirdDevice, &defaultDescriptor, memoryFdB,
+                                                       allocationSizeB, memoryTypeIndexB, {});
+
+    // 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);
+
+    int signalFdTexBDevice3 = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(
+        thirdDevice.Get(), wrappedTexBDevice3.Get());
+    IgnoreSignalSemaphore(thirdDevice, wrappedTexADevice3);
+
+    // Import TexB, TexC on device 2
+    memoryFdB = GetMemoryFd(secondDeviceVk, allocationB);
+    dawn::Texture wrappedTexBDevice2 =
+        WrapVulkanImage(secondDevice, &defaultDescriptor, memoryFdB, allocationSizeB,
+                        memoryTypeIndexB, {signalFdTexBDevice3});
+
+    dawn::Texture wrappedTexCDevice2 = WrapVulkanImage(secondDevice, &defaultDescriptor, memoryFdC,
+                                                       allocationSizeC, memoryTypeIndexC, {});
+
+    // Copy B->C on device 2
+    SimpleCopyTextureToTexture(secondDevice, secondDeviceQueue, wrappedTexBDevice2,
+                               wrappedTexCDevice2);
+
+    int signalFdTexCDevice2 = dawn_native::vulkan::ExportSignalSemaphoreOpaqueFD(
+        secondDevice.Get(), wrappedTexCDevice2.Get());
+    IgnoreSignalSemaphore(secondDevice, wrappedTexBDevice2);
+
+    // Import TexC on device 1
+    memoryFdC = GetMemoryFd(deviceVk, allocationC);
+    dawn::Texture wrappedTexCDevice1 =
+        WrapVulkanImage(device, &defaultDescriptor, memoryFdC, allocationSizeC, memoryTypeIndexC,
+                        {signalFdTexCDevice2});
+
+    // Create TexD on device 1
+    dawn::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(device, wrappedTexCDevice1);
+}
+
+DAWN_INSTANTIATE_TEST(VulkanImageWrappingValidationTests, VulkanBackend);
+DAWN_INSTANTIATE_TEST(VulkanImageWrappingUsageTests, VulkanBackend);