Implement Texture-to-Texture Copies

Implement texture-to-texture copies for D3D12, Vulkan, and Metal.
Includes end2end and unit tests.

Bug: dawn:18
Change-Id: Ib48453704599bee43a76af21e6164aa9b8db7075
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/5620
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
diff --git a/dawn.json b/dawn.json
index b522d7e..2e88953 100644
--- a/dawn.json
+++ b/dawn.json
@@ -319,6 +319,14 @@
                     {"name": "destination", "type": "buffer copy view", "annotation": "const*"},
                     {"name": "copy size", "type": "extent 3D", "annotation": "const*"}
                 ]
+            },
+            {
+                "name": "copy texture to texture",
+                "args": [
+                    {"name": "source", "type": "texture copy view", "annotation": "const*"},
+                    {"name": "destination", "type": "texture copy view", "annotation": "const*"},
+                    {"name": "copy size", "type": "extent 3D", "annotation": "const*"}
+                ]
             }
         ]
     },
diff --git a/src/dawn_native/CommandEncoder.cpp b/src/dawn_native/CommandEncoder.cpp
index feed603..39e05f2 100644
--- a/src/dawn_native/CommandEncoder.cpp
+++ b/src/dawn_native/CommandEncoder.cpp
@@ -141,6 +141,49 @@
             return {};
         }
 
+        MaybeError ValidateEntireSubresourceCopied(const TextureCopy& src,
+                                                   const TextureCopy& dst,
+                                                   const Extent3D& copySize) {
+            Extent3D srcSize = src.texture.Get()->GetSize();
+
+            if (dst.origin.x != 0 || dst.origin.y != 0 || dst.origin.z != 0 ||
+                srcSize.width != copySize.width || srcSize.height != copySize.height ||
+                srcSize.depth != copySize.depth) {
+                return DAWN_VALIDATION_ERROR(
+                    "The entire subresource must be copied when using a depth/stencil texture or "
+                    "when samples are greater than 1.");
+            }
+
+            return {};
+        }
+
+        MaybeError ValidateTextureToTextureCopyRestrictions(const TextureCopy& src,
+                                                            const TextureCopy& dst,
+                                                            const Extent3D& copySize) {
+            const uint32_t srcSamples = src.texture.Get()->GetSampleCount();
+            const uint32_t dstSamples = dst.texture.Get()->GetSampleCount();
+
+            if (srcSamples != dstSamples) {
+                return DAWN_VALIDATION_ERROR(
+                    "Source and destination textures must have matching sample counts.");
+            } else if (srcSamples > 1) {
+                // D3D12 requires entire subresource to be copied when using CopyTextureRegion when
+                // samples > 1.
+                DAWN_TRY(ValidateEntireSubresourceCopied(src, dst, copySize));
+            }
+
+            if (src.texture.Get()->GetFormat() != dst.texture.Get()->GetFormat()) {
+                // Metal requires texture-to-texture copies be the same format
+                return DAWN_VALIDATION_ERROR("Source and destination texture formats must match.");
+            } else if (TextureFormatHasDepthOrStencil(src.texture.Get()->GetFormat())) {
+                // D3D12 requires entire subresource to be copied when using CopyTextureRegion is
+                // used with depth/stencil.
+                DAWN_TRY(ValidateEntireSubresourceCopied(src, dst, copySize));
+            }
+
+            return {};
+        }
+
         MaybeError ComputeTextureCopyBufferSize(const Extent3D& copySize,
                                                 uint32_t rowPitch,
                                                 uint32_t imageHeight,
@@ -734,6 +777,35 @@
         }
     }
 
+    void CommandEncoderBase::CopyTextureToTexture(const TextureCopyView* source,
+                                                  const TextureCopyView* destination,
+                                                  const Extent3D* copySize) {
+        if (ConsumedError(ValidateCanRecordTopLevelCommands())) {
+            return;
+        }
+
+        if (ConsumedError(GetDevice()->ValidateObject(source->texture))) {
+            return;
+        }
+
+        if (ConsumedError(GetDevice()->ValidateObject(destination->texture))) {
+            return;
+        }
+
+        CopyTextureToTextureCmd* copy =
+            mAllocator.Allocate<CopyTextureToTextureCmd>(Command::CopyTextureToTexture);
+        new (copy) CopyTextureToTextureCmd;
+        copy->source.texture = source->texture;
+        copy->source.origin = source->origin;
+        copy->source.level = source->level;
+        copy->source.slice = source->slice;
+        copy->destination.texture = destination->texture;
+        copy->destination.origin = destination->origin;
+        copy->destination.level = destination->level;
+        copy->destination.slice = destination->slice;
+        copy->copySize = *copySize;
+    }
+
     CommandBufferBase* CommandEncoderBase::Finish() {
         if (GetDevice()->ConsumedError(ValidateFinish())) {
             return CommandBufferBase::MakeError(GetDevice());
@@ -870,6 +942,25 @@
                     mResourceUsages.topLevelBuffers.insert(copy->destination.buffer.Get());
                 } break;
 
+                case Command::CopyTextureToTexture: {
+                    CopyTextureToTextureCmd* copy =
+                        mIterator.NextCommand<CopyTextureToTextureCmd>();
+
+                    DAWN_TRY(ValidateTextureToTextureCopyRestrictions(
+                        copy->source, copy->destination, copy->copySize));
+
+                    DAWN_TRY(ValidateCopySizeFitsInTexture(copy->source, copy->copySize));
+                    DAWN_TRY(ValidateCopySizeFitsInTexture(copy->destination, copy->copySize));
+
+                    DAWN_TRY(ValidateCanUseAs(copy->source.texture.Get(),
+                                              dawn::TextureUsageBit::TransferSrc));
+                    DAWN_TRY(ValidateCanUseAs(copy->destination.texture.Get(),
+                                              dawn::TextureUsageBit::TransferDst));
+
+                    mResourceUsages.topLevelTextures.insert(copy->source.texture.Get());
+                    mResourceUsages.topLevelTextures.insert(copy->destination.texture.Get());
+                } break;
+
                 default:
                     return DAWN_VALIDATION_ERROR("Command disallowed outside of a pass");
             }
diff --git a/src/dawn_native/CommandEncoder.h b/src/dawn_native/CommandEncoder.h
index 20d8901..15bb895 100644
--- a/src/dawn_native/CommandEncoder.h
+++ b/src/dawn_native/CommandEncoder.h
@@ -54,6 +54,9 @@
         void CopyTextureToBuffer(const TextureCopyView* source,
                                  const BufferCopyView* destination,
                                  const Extent3D* copySize);
+        void CopyTextureToTexture(const TextureCopyView* source,
+                                  const TextureCopyView* destination,
+                                  const Extent3D* copySize);
         CommandBufferBase* Finish();
 
         // Functions to interact with the encoders
diff --git a/src/dawn_native/Commands.cpp b/src/dawn_native/Commands.cpp
index 816dd39..2524d2c 100644
--- a/src/dawn_native/Commands.cpp
+++ b/src/dawn_native/Commands.cpp
@@ -49,6 +49,11 @@
                     CopyTextureToBufferCmd* copy = commands->NextCommand<CopyTextureToBufferCmd>();
                     copy->~CopyTextureToBufferCmd();
                 } break;
+                case Command::CopyTextureToTexture: {
+                    CopyTextureToTextureCmd* copy =
+                        commands->NextCommand<CopyTextureToTextureCmd>();
+                    copy->~CopyTextureToTextureCmd();
+                } break;
                 case Command::Dispatch: {
                     DispatchCmd* dispatch = commands->NextCommand<DispatchCmd>();
                     dispatch->~DispatchCmd();
@@ -152,6 +157,10 @@
                 commands->NextCommand<CopyTextureToBufferCmd>();
                 break;
 
+            case Command::CopyTextureToTexture:
+                commands->NextCommand<CopyTextureToTextureCmd>();
+                break;
+
             case Command::Dispatch:
                 commands->NextCommand<DispatchCmd>();
                 break;
diff --git a/src/dawn_native/Commands.h b/src/dawn_native/Commands.h
index 9ba5d66..f8950ee 100644
--- a/src/dawn_native/Commands.h
+++ b/src/dawn_native/Commands.h
@@ -36,6 +36,7 @@
         CopyBufferToBuffer,
         CopyBufferToTexture,
         CopyTextureToBuffer,
+        CopyTextureToTexture,
         Dispatch,
         Draw,
         DrawIndexed,
@@ -119,6 +120,12 @@
         Extent3D copySize;  // Texels
     };
 
+    struct CopyTextureToTextureCmd {
+        TextureCopy source;
+        TextureCopy destination;
+        Extent3D copySize;  // Texels
+    };
+
     struct DispatchCmd {
         uint32_t x;
         uint32_t y;
diff --git a/src/dawn_native/d3d12/CommandBufferD3D12.cpp b/src/dawn_native/d3d12/CommandBufferD3D12.cpp
index 7d59f7d..56807ae 100644
--- a/src/dawn_native/d3d12/CommandBufferD3D12.cpp
+++ b/src/dawn_native/d3d12/CommandBufferD3D12.cpp
@@ -44,6 +44,32 @@
                     UNREACHABLE();
             }
         }
+
+        D3D12_TEXTURE_COPY_LOCATION CreateTextureCopyLocationForTexture(const Texture& texture,
+                                                                        uint32_t level,
+                                                                        uint32_t slice) {
+            D3D12_TEXTURE_COPY_LOCATION copyLocation;
+            copyLocation.pResource = texture.GetD3D12Resource();
+            copyLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
+            copyLocation.SubresourceIndex = texture.GetNumMipLevels() * slice + level;
+
+            return copyLocation;
+        }
+
+        bool CanUseCopyResource(const uint32_t sourceNumMipLevels,
+                                const Extent3D& srcSize,
+                                const Extent3D& dstSize,
+                                const Extent3D& copySize) {
+            if (sourceNumMipLevels == 1 && srcSize.width == dstSize.width &&
+                srcSize.height == dstSize.height && srcSize.depth == dstSize.depth &&
+                srcSize.width == copySize.width && srcSize.height == copySize.height &&
+                srcSize.depth == copySize.depth) {
+                return true;
+            }
+
+            return false;
+        }
+
     }  // anonymous namespace
 
     struct BindGroupStateTracker {
@@ -425,12 +451,9 @@
                         static_cast<uint32_t>(TextureFormatPixelSize(texture->GetFormat())),
                         copy->source.offset, copy->source.rowPitch, copy->source.imageHeight);
 
-                    D3D12_TEXTURE_COPY_LOCATION textureLocation;
-                    textureLocation.pResource = texture->GetD3D12Resource();
-                    textureLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
-                    textureLocation.SubresourceIndex =
-                        texture->GetNumMipLevels() * copy->destination.slice +
-                        copy->destination.level;
+                    D3D12_TEXTURE_COPY_LOCATION textureLocation =
+                        CreateTextureCopyLocationForTexture(*texture, copy->destination.level,
+                                                            copy->destination.slice);
 
                     for (uint32_t i = 0; i < copySplit.count; ++i) {
                         auto& info = copySplit.copies[i];
@@ -473,11 +496,9 @@
                         copy->destination.offset, copy->destination.rowPitch,
                         copy->destination.imageHeight);
 
-                    D3D12_TEXTURE_COPY_LOCATION textureLocation;
-                    textureLocation.pResource = texture->GetD3D12Resource();
-                    textureLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
-                    textureLocation.SubresourceIndex =
-                        texture->GetNumMipLevels() * copy->source.slice + copy->source.level;
+                    D3D12_TEXTURE_COPY_LOCATION textureLocation =
+                        CreateTextureCopyLocationForTexture(*texture, copy->source.level,
+                                                            copy->source.slice);
 
                     for (uint32_t i = 0; i < copySplit.count; ++i) {
                         auto& info = copySplit.copies[i];
@@ -507,6 +528,45 @@
                     }
                 } break;
 
+                case Command::CopyTextureToTexture: {
+                    CopyTextureToTextureCmd* copy =
+                        mCommands.NextCommand<CopyTextureToTextureCmd>();
+
+                    Texture* source = ToBackend(copy->source.texture.Get());
+                    Texture* destination = ToBackend(copy->destination.texture.Get());
+
+                    source->TransitionUsageNow(commandList, dawn::TextureUsageBit::TransferSrc);
+                    destination->TransitionUsageNow(commandList,
+                                                    dawn::TextureUsageBit::TransferDst);
+
+                    if (CanUseCopyResource(source->GetNumMipLevels(), source->GetSize(),
+                                           destination->GetSize(), copy->copySize)) {
+                        commandList->CopyResource(destination->GetD3D12Resource(),
+                                                  source->GetD3D12Resource());
+
+                    } else {
+                        D3D12_TEXTURE_COPY_LOCATION srcLocation =
+                            CreateTextureCopyLocationForTexture(*source, copy->source.level,
+                                                                copy->source.slice);
+
+                        D3D12_TEXTURE_COPY_LOCATION dstLocation =
+                            CreateTextureCopyLocationForTexture(
+                                *destination, copy->destination.level, copy->destination.slice);
+
+                        D3D12_BOX sourceRegion;
+                        sourceRegion.left = copy->source.origin.x;
+                        sourceRegion.top = copy->source.origin.y;
+                        sourceRegion.front = copy->source.origin.z;
+                        sourceRegion.right = copy->source.origin.x + copy->copySize.width;
+                        sourceRegion.bottom = copy->source.origin.y + copy->copySize.height;
+                        sourceRegion.back = copy->source.origin.z + copy->copySize.depth;
+
+                        commandList->CopyTextureRegion(
+                            &dstLocation, copy->destination.origin.x, copy->destination.origin.y,
+                            copy->destination.origin.z, &srcLocation, &sourceRegion);
+                    }
+                } break;
+
                 default: { UNREACHABLE(); } break;
             }
         }
diff --git a/src/dawn_native/d3d12/TextureD3D12.cpp b/src/dawn_native/d3d12/TextureD3D12.cpp
index eb4228f..3cfff18 100644
--- a/src/dawn_native/d3d12/TextureD3D12.cpp
+++ b/src/dawn_native/d3d12/TextureD3D12.cpp
@@ -150,7 +150,7 @@
         return D3D12TextureFormat(GetFormat());
     }
 
-    ID3D12Resource* Texture::GetD3D12Resource() {
+    ID3D12Resource* Texture::GetD3D12Resource() const {
         return mResourcePtr;
     }
 
diff --git a/src/dawn_native/d3d12/TextureD3D12.h b/src/dawn_native/d3d12/TextureD3D12.h
index 2edfb7c..5ca9cf3 100644
--- a/src/dawn_native/d3d12/TextureD3D12.h
+++ b/src/dawn_native/d3d12/TextureD3D12.h
@@ -32,7 +32,7 @@
         ~Texture();
 
         DXGI_FORMAT GetD3D12Format() const;
-        ID3D12Resource* GetD3D12Resource();
+        ID3D12Resource* GetD3D12Resource() const;
 
         void TransitionUsageNow(ComPtr<ID3D12GraphicsCommandList> commandList,
                                 dawn::TextureUsageBit usage);
diff --git a/src/dawn_native/metal/CommandBufferMTL.mm b/src/dawn_native/metal/CommandBufferMTL.mm
index 7fc8741..3506fa4 100644
--- a/src/dawn_native/metal/CommandBufferMTL.mm
+++ b/src/dawn_native/metal/CommandBufferMTL.mm
@@ -476,6 +476,40 @@
                         destinationBytesPerImage:lastRowDataSize];
                 } break;
 
+                case Command::CopyTextureToTexture: {
+                    CopyTextureToTextureCmd* copy =
+                        mCommands.NextCommand<CopyTextureToTextureCmd>();
+                    Texture* srcTexture = ToBackend(copy->source.texture.Get());
+                    Texture* dstTexture = ToBackend(copy->destination.texture.Get());
+
+                    MTLOrigin srcOrigin;
+                    srcOrigin.x = copy->source.origin.x;
+                    srcOrigin.y = copy->source.origin.y;
+                    srcOrigin.z = copy->source.origin.z;
+
+                    MTLOrigin dstOrigin;
+                    dstOrigin.x = copy->destination.origin.x;
+                    dstOrigin.y = copy->destination.origin.y;
+                    dstOrigin.z = copy->destination.origin.z;
+
+                    MTLSize size;
+                    size.width = copy->copySize.width;
+                    size.height = copy->copySize.height;
+                    size.depth = copy->copySize.depth;
+
+                    encoders.EnsureBlit(commandBuffer);
+
+                    [encoders.blit copyFromTexture:srcTexture->GetMTLTexture()
+                                       sourceSlice:copy->source.slice
+                                       sourceLevel:copy->source.level
+                                      sourceOrigin:srcOrigin
+                                        sourceSize:size
+                                         toTexture:dstTexture->GetMTLTexture()
+                                  destinationSlice:copy->destination.slice
+                                  destinationLevel:copy->destination.level
+                                 destinationOrigin:dstOrigin];
+                } break;
+
                 default: { UNREACHABLE(); } break;
             }
         }
diff --git a/src/dawn_native/opengl/CommandBufferGL.cpp b/src/dawn_native/opengl/CommandBufferGL.cpp
index f25d303..73a8fd6 100644
--- a/src/dawn_native/opengl/CommandBufferGL.cpp
+++ b/src/dawn_native/opengl/CommandBufferGL.cpp
@@ -443,6 +443,22 @@
                     glDeleteFramebuffers(1, &readFBO);
                 } break;
 
+                case Command::CopyTextureToTexture: {
+                    CopyTextureToTextureCmd* copy =
+                        mCommands.NextCommand<CopyTextureToTextureCmd>();
+                    auto& src = copy->source;
+                    auto& dst = copy->destination;
+                    auto& copySize = copy->copySize;
+                    Texture* srcTexture = ToBackend(src.texture.Get());
+                    Texture* dstTexture = ToBackend(dst.texture.Get());
+
+                    glCopyImageSubData(srcTexture->GetHandle(), srcTexture->GetGLTarget(),
+                                       src.level, src.origin.x, src.origin.y, src.slice,
+                                       dstTexture->GetHandle(), dstTexture->GetGLTarget(),
+                                       dst.level, dst.origin.x, dst.origin.y, dst.slice,
+                                       copySize.width, copySize.height, 1);
+                } break;
+
                 default: { UNREACHABLE(); } break;
             }
         }
diff --git a/src/dawn_native/vulkan/CommandBufferVk.cpp b/src/dawn_native/vulkan/CommandBufferVk.cpp
index 3e8d794..fa9b36c 100644
--- a/src/dawn_native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn_native/vulkan/CommandBufferVk.cpp
@@ -70,6 +70,39 @@
             return region;
         }
 
+        VkImageCopy ComputeImageCopyRegion(const TextureCopy& srcCopy,
+                                           const TextureCopy& dstCopy,
+                                           const Extent3D& copySize) {
+            const Texture* srcTexture = ToBackend(srcCopy.texture.Get());
+            const Texture* dstTexture = ToBackend(dstCopy.texture.Get());
+
+            VkImageCopy region;
+
+            region.srcSubresource.aspectMask = srcTexture->GetVkAspectMask();
+            region.srcSubresource.mipLevel = srcCopy.level;
+            region.srcSubresource.baseArrayLayer = srcCopy.slice;
+            region.srcSubresource.layerCount = 1;
+
+            region.srcOffset.x = srcCopy.origin.x;
+            region.srcOffset.y = srcCopy.origin.y;
+            region.srcOffset.z = srcCopy.origin.z;
+
+            region.dstSubresource.aspectMask = dstTexture->GetVkAspectMask();
+            region.dstSubresource.mipLevel = dstCopy.level;
+            region.dstSubresource.baseArrayLayer = dstCopy.slice;
+            region.dstSubresource.layerCount = 1;
+
+            region.dstOffset.x = dstCopy.origin.x;
+            region.dstOffset.y = dstCopy.origin.y;
+            region.dstOffset.z = dstCopy.origin.z;
+
+            region.extent.width = copySize.width;
+            region.extent.height = copySize.height;
+            region.extent.depth = copySize.depth;
+
+            return region;
+        }
+
         class DescriptorSetTracker {
           public:
             void OnSetBindGroup(uint32_t index, VkDescriptorSet set) {
@@ -299,6 +332,26 @@
                                                     dstBuffer, 1, &region);
                 } break;
 
+                case Command::CopyTextureToTexture: {
+                    CopyTextureToTextureCmd* copy =
+                        mCommands.NextCommand<CopyTextureToTextureCmd>();
+                    TextureCopy& src = copy->source;
+                    TextureCopy& dst = copy->destination;
+
+                    ToBackend(src.texture)
+                        ->TransitionUsageNow(commands, dawn::TextureUsageBit::TransferSrc);
+                    ToBackend(dst.texture)
+                        ->TransitionUsageNow(commands, dawn::TextureUsageBit::TransferDst);
+
+                    VkImage srcImage = ToBackend(src.texture)->GetHandle();
+                    VkImage dstImage = ToBackend(dst.texture)->GetHandle();
+
+                    VkImageCopy region = ComputeImageCopyRegion(src, dst, copy->copySize);
+
+                    device->fn.CmdCopyImage(commands, srcImage, VK_IMAGE_LAYOUT_GENERAL, dstImage,
+                                            VK_IMAGE_LAYOUT_GENERAL, 1, &region);
+                } break;
+
                 case Command::BeginRenderPass: {
                     BeginRenderPassCmd* cmd = mCommands.NextCommand<BeginRenderPassCmd>();
 
diff --git a/src/tests/end2end/CopyTests.cpp b/src/tests/end2end/CopyTests.cpp
index 6f2a243..3ff2ff0 100644
--- a/src/tests/end2end/CopyTests.cpp
+++ b/src/tests/end2end/CopyTests.cpp
@@ -40,6 +40,21 @@
             uint32_t rowPitch;
         };
 
+        static void FillTextureData(uint32_t width,
+                                    uint32_t height,
+                                    uint32_t texelsPerRow,
+                                    uint32_t layer,
+                                    RGBA8* data) {
+            for (uint32_t y = 0; y < height; ++y) {
+                for (uint32_t x = 0; x < width; ++x) {
+                    uint32_t i = x + y * texelsPerRow;
+                    data[i] = RGBA8(static_cast<uint8_t>((x + layer * x) % 256),
+                                    static_cast<uint8_t>((y + layer * y) % 256),
+                                    static_cast<uint8_t>(x / 256), static_cast<uint8_t>(y / 256));
+                }
+            }
+        }
+
         BufferSpec MinimumBufferSpec(uint32_t width, uint32_t height) {
             uint32_t rowPitch = Align(width * kBytesPerTexel, kTextureRowPitchAlignment);
             return { rowPitch * (height - 1) + width * kBytesPerTexel, 0, rowPitch };
@@ -58,18 +73,6 @@
 
 class CopyTests_T2B : public CopyTests {
     protected:
-        static void FillTextureData(uint32_t width, uint32_t height, uint32_t texelsPerRow, RGBA8* data, uint32_t layer) {
-            for (unsigned int y = 0; y < height; ++y) {
-                for (unsigned int x = 0; x < width; ++x) {
-                    unsigned int i = x + y * texelsPerRow;
-                    data[i] = RGBA8(
-                        static_cast<uint8_t>((x + layer * x)% 256),
-                        static_cast<uint8_t>((y + layer * y)% 256),
-                        static_cast<uint8_t>(x / 256),
-                        static_cast<uint8_t>(y / 256));
-                }
-            }
-        }
 
         void DoTest(const TextureSpec& textureSpec, const BufferSpec& bufferSpec) {
             // Create a texture that is `width` x `height` with (`level` + 1) mip levels.
@@ -96,7 +99,8 @@
             std::vector<std::vector<RGBA8>> textureArrayData(textureSpec.arraySize);
             for (uint32_t slice = 0; slice < textureSpec.arraySize; ++slice) {
                 textureArrayData[slice].resize(texelCountPerLayer);
-                FillTextureData(width, height, rowPitch / kBytesPerTexel, textureArrayData[slice].data(), slice);
+                FillTextureData(width, height, rowPitch / kBytesPerTexel, slice,
+                                textureArrayData[slice].data());
 
                 // Create an upload buffer and use it to populate the current slice of the texture in `level` mip level
                 dawn::Buffer uploadBuffer = utils::CreateBufferFromData(device, textureArrayData[slice].data(),
@@ -245,6 +249,134 @@
 
 };
 
+class CopyTests_T2T : public CopyTests {
+    struct TextureSpec {
+        uint32_t width;
+        uint32_t height;
+        uint32_t x;
+        uint32_t y;
+        uint32_t level;
+        uint32_t arraySize = 1u;
+    };
+
+    struct CopySize {
+        uint32_t width;
+        uint32_t height;
+    };
+
+  protected:
+    void DoTest(const TextureSpec& srcSpec, const TextureSpec& dstSpec, const CopySize& copy) {
+        dawn::TextureDescriptor srcDescriptor;
+        srcDescriptor.dimension = dawn::TextureDimension::e2D;
+        srcDescriptor.size.width = srcSpec.width;
+        srcDescriptor.size.height = srcSpec.height;
+        srcDescriptor.size.depth = 1;
+        srcDescriptor.arrayLayerCount = srcSpec.arraySize;
+        srcDescriptor.sampleCount = 1;
+        srcDescriptor.format = dawn::TextureFormat::R8G8B8A8Unorm;
+        srcDescriptor.mipLevelCount = srcSpec.level + 1;
+        srcDescriptor.usage =
+            dawn::TextureUsageBit::TransferSrc | dawn::TextureUsageBit::TransferDst;
+        dawn::Texture srcTexture = device.CreateTexture(&srcDescriptor);
+
+        dawn::TextureDescriptor dstDescriptor;
+        dstDescriptor.dimension = dawn::TextureDimension::e2D;
+        dstDescriptor.size.width = dstSpec.width;
+        dstDescriptor.size.height = dstSpec.height;
+        dstDescriptor.size.depth = 1;
+        dstDescriptor.arrayLayerCount = dstSpec.arraySize;
+        dstDescriptor.sampleCount = 1;
+        dstDescriptor.format = dawn::TextureFormat::R8G8B8A8Unorm;
+        dstDescriptor.mipLevelCount = dstSpec.level + 1;
+        dstDescriptor.usage =
+            dawn::TextureUsageBit::TransferSrc | dawn::TextureUsageBit::TransferDst;
+        dawn::Texture dstTexture = device.CreateTexture(&dstDescriptor);
+
+        dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        // Create an upload buffer and use it to populate the current slice of the texture in
+        // `level` mip level
+        uint32_t width = srcSpec.width >> srcSpec.level;
+        uint32_t height = srcSpec.height >> srcSpec.level;
+        uint32_t rowPitch = Align(kBytesPerTexel * width, kTextureRowPitchAlignment);
+        uint32_t texelsPerRow = rowPitch / kBytesPerTexel;
+        uint32_t texelCountPerLayer = texelsPerRow * (height - 1) + width;
+
+        std::vector<std::vector<RGBA8>> textureArrayData(srcSpec.arraySize);
+        for (uint32_t slice = 0; slice < srcSpec.arraySize; ++slice) {
+            textureArrayData[slice].resize(texelCountPerLayer);
+            FillTextureData(width, height, rowPitch / kBytesPerTexel, slice,
+                            textureArrayData[slice].data());
+
+            dawn::Buffer uploadBuffer = utils::CreateBufferFromData(
+                device, textureArrayData[slice].data(),
+                static_cast<uint32_t>(sizeof(RGBA8) * textureArrayData[slice].size()),
+                dawn::BufferUsageBit::TransferSrc);
+            dawn::BufferCopyView bufferCopyView =
+                utils::CreateBufferCopyView(uploadBuffer, 0, rowPitch, 0);
+            dawn::TextureCopyView textureCopyView =
+                utils::CreateTextureCopyView(srcTexture, srcSpec.level, slice, {0, 0, 0});
+            dawn::Extent3D bufferCopySize = {width, height, 1};
+
+            encoder.CopyBufferToTexture(&bufferCopyView, &textureCopyView, &bufferCopySize);
+        }
+
+        // Create an upload buffer filled with empty data and use it to populate the `level` mip of
+        // the texture. Note: Prepopulating the texture with empty data ensures that there is not
+        // random data in the expectation and helps ensure that the padding due to the row pitch is
+        // not modified by the copy
+        {
+            uint32_t dstWidth = dstSpec.width >> dstSpec.level;
+            uint32_t dstHeight = dstSpec.height >> dstSpec.level;
+            uint32_t dstRowPitch = Align(kBytesPerTexel * dstWidth, kTextureRowPitchAlignment);
+            uint32_t dstTexelsPerRow = dstRowPitch / kBytesPerTexel;
+            uint32_t dstTexelCount = dstTexelsPerRow * (dstHeight - 1) + dstWidth;
+
+            std::vector<RGBA8> emptyData(dstTexelCount);
+            dawn::Buffer uploadBuffer = utils::CreateBufferFromData(
+                device, emptyData.data(), static_cast<uint32_t>(sizeof(RGBA8) * emptyData.size()),
+                dawn::BufferUsageBit::TransferSrc);
+            dawn::BufferCopyView bufferCopyView =
+                utils::CreateBufferCopyView(uploadBuffer, 0, dstRowPitch, 0);
+            dawn::TextureCopyView textureCopyView =
+                utils::CreateTextureCopyView(dstTexture, dstSpec.level, 0, {0, 0, 0});
+            dawn::Extent3D dstCopySize = {dstWidth, dstHeight, 1};
+            encoder.CopyBufferToTexture(&bufferCopyView, &textureCopyView, &dstCopySize);
+        }
+
+        // Perform the texture to texture copy
+        for (uint32_t slice = 0; slice < srcSpec.arraySize; ++slice) {
+            dawn::TextureCopyView srcTextureCopyView = utils::CreateTextureCopyView(
+                srcTexture, srcSpec.level, slice, {srcSpec.x, srcSpec.y, 0});
+            dawn::TextureCopyView dstTextureCopyView = utils::CreateTextureCopyView(
+                dstTexture, dstSpec.level, slice, {dstSpec.x, dstSpec.y, 0});
+            dawn::Extent3D copySize = {copy.width, copy.height, 1};
+            encoder.CopyTextureToTexture(&srcTextureCopyView, &dstTextureCopyView, &copySize);
+        }
+
+        dawn::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+
+        std::vector<RGBA8> expected(rowPitch / kBytesPerTexel * (copy.height - 1) + copy.width);
+        for (uint32_t slice = 0; slice < srcSpec.arraySize; ++slice) {
+            std::fill(expected.begin(), expected.end(), RGBA8());
+            PackTextureData(
+                &textureArrayData[slice][srcSpec.x + srcSpec.y * (rowPitch / kBytesPerTexel)],
+                copy.width, copy.height, texelsPerRow, expected.data(), copy.width);
+
+            EXPECT_TEXTURE_RGBA8_EQ(expected.data(), dstTexture, dstSpec.x, dstSpec.y, copy.width,
+                                    copy.height, dstSpec.level, slice)
+                << "Texture to Texture copy failed copying region [(" << srcSpec.x << ", "
+                << srcSpec.y << "), (" << srcSpec.x + copy.width << ", " << srcSpec.y + copy.height
+                << ")) from " << srcSpec.width << " x " << srcSpec.height
+                << " texture at mip level " << srcSpec.level << " layer " << slice << " to [("
+                << dstSpec.x << ", " << dstSpec.y << "), (" << dstSpec.x + copy.width << ", "
+                << dstSpec.y + copy.height << ")) region of " << dstSpec.width << " x "
+                << dstSpec.height << " texture at mip level " << dstSpec.level << std::endl;
+        }
+    }
+};
+
 // Test that copying an entire texture with 256-byte aligned dimensions works
 TEST_P(CopyTests_T2B, FullTextureAligned) {
     constexpr uint32_t kWidth = 256;
@@ -551,3 +683,51 @@
 }
 
 DAWN_INSTANTIATE_TEST(CopyTests_B2T, D3D12Backend, MetalBackend, OpenGLBackend, VulkanBackend);
+
+TEST_P(CopyTests_T2T, Texture) {
+    constexpr uint32_t kWidth = 256;
+    constexpr uint32_t kHeight = 128;
+    DoTest({kWidth, kHeight, 0, 0, 0}, {kWidth, kHeight, 0, 0, 0}, {kWidth, kHeight});
+}
+
+TEST_P(CopyTests_T2T, TextureRegion) {
+    constexpr uint32_t kWidth = 256;
+    constexpr uint32_t kHeight = 128;
+    for (unsigned int w : {64, 128, 256}) {
+        for (unsigned int h : {16, 32, 48}) {
+            DoTest({kWidth, kHeight, 0, 0, 0, 1}, {kWidth, kHeight, 0, 0, 0, 1}, {w, h});
+        }
+    }
+}
+
+TEST_P(CopyTests_T2T, Texture2DArray) {
+    constexpr uint32_t kWidth = 256;
+    constexpr uint32_t kHeight = 128;
+    constexpr uint32_t kLayers = 6u;
+    DoTest({kWidth, kHeight, 0, 0, 0, kLayers}, {kWidth, kHeight, 0, 0, 0, kLayers},
+           {kWidth, kHeight});
+}
+
+TEST_P(CopyTests_T2T, Texture2DArrayRegion) {
+    constexpr uint32_t kWidth = 256;
+    constexpr uint32_t kHeight = 128;
+    constexpr uint32_t kLayers = 6u;
+    for (unsigned int w : {64, 128, 256}) {
+        for (unsigned int h : {16, 32, 48}) {
+            DoTest({kWidth, kHeight, 0, 0, 0, kLayers}, {kWidth, kHeight, 0, 0, 0, kLayers},
+                   {w, h});
+        }
+    }
+}
+
+TEST_P(CopyTests_T2T, TextureMip) {
+    constexpr uint32_t kWidth = 256;
+    constexpr uint32_t kHeight = 128;
+    for (unsigned int i = 1; i < 4; ++i) {
+        DoTest({kWidth, kHeight, 0, 0, i}, {kWidth, kHeight, 0, 0, i}, {kWidth >> i, kHeight >> i});
+    }
+}
+
+// TODO(brandon1.jones@intel.com) Add test for ensuring blitCommandEncoder on Metal.
+
+DAWN_INSTANTIATE_TEST(CopyTests_T2T, D3D12Backend, MetalBackend, OpenGLBackend, VulkanBackend);
\ No newline at end of file
diff --git a/src/tests/unittests/validation/CopyCommandsValidationTests.cpp b/src/tests/unittests/validation/CopyCommandsValidationTests.cpp
index dbb6836..3fe8ce1 100644
--- a/src/tests/unittests/validation/CopyCommandsValidationTests.cpp
+++ b/src/tests/unittests/validation/CopyCommandsValidationTests.cpp
@@ -49,6 +49,14 @@
             return (rowPitch * (height - 1) + width) * depth;
         }
 
+        void ValidateExpectation(dawn::CommandEncoder encoder, utils::Expectation expectation) {
+            if (expectation == utils::Expectation::Success) {
+                encoder.Finish();
+            } else {
+                ASSERT_DEVICE_ERROR(encoder.Finish());
+            }
+        }
+
         void TestB2TCopy(utils::Expectation expectation,
                          dawn::Buffer srcBuffer,
                          uint32_t srcOffset,
@@ -67,11 +75,7 @@
             dawn::CommandEncoder encoder = device.CreateCommandEncoder();
             encoder.CopyBufferToTexture(&bufferCopyView, &textureCopyView, &extent3D);
 
-            if (expectation == utils::Expectation::Success) {
-                encoder.Finish();
-            } else {
-                ASSERT_DEVICE_ERROR(encoder.Finish());
-            }
+            ValidateExpectation(encoder, expectation);
         }
 
         void TestT2BCopy(utils::Expectation expectation,
@@ -92,11 +96,28 @@
             dawn::CommandEncoder encoder = device.CreateCommandEncoder();
             encoder.CopyTextureToBuffer(&textureCopyView, &bufferCopyView, &extent3D);
 
-            if (expectation == utils::Expectation::Success) {
-                encoder.Finish();
-            } else {
-                ASSERT_DEVICE_ERROR(encoder.Finish());
-            }
+            ValidateExpectation(encoder, expectation);
+        }
+
+        void TestT2TCopy(utils::Expectation expectation,
+                         dawn::Texture srcTexture,
+                         uint32_t srcLevel,
+                         uint32_t srcSlice,
+                         dawn::Origin3D srcOrigin,
+                         dawn::Texture dstTexture,
+                         uint32_t dstLevel,
+                         uint32_t dstSlice,
+                         dawn::Origin3D dstOrigin,
+                         dawn::Extent3D extent3D) {
+            dawn::TextureCopyView srcTextureCopyView =
+                utils::CreateTextureCopyView(srcTexture, srcLevel, srcSlice, srcOrigin);
+            dawn::TextureCopyView dstTextureCopyView =
+                utils::CreateTextureCopyView(dstTexture, dstLevel, dstSlice, dstOrigin);
+
+            dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+            encoder.CopyTextureToTexture(&srcTextureCopyView, &dstTextureCopyView, &extent3D);
+
+            ValidateExpectation(encoder, expectation);
         }
 };
 
@@ -738,3 +759,193 @@
         ASSERT_DEVICE_ERROR(encoder.Finish());
     }
 }
+
+class CopyCommandTest_T2T : public CopyCommandTest {};
+
+TEST_F(CopyCommandTest_T2T, Success) {
+    dawn::Texture source = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                           dawn::TextureUsageBit::TransferSrc);
+    dawn::Texture destination = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                                dawn::TextureUsageBit::TransferDst);
+
+    // Different copies, including some that touch the OOB condition
+    {
+        // Copy a region along top left boundary
+        TestT2TCopy(utils::Expectation::Success, source, 0, 0, {0, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {4, 4, 1});
+
+        // Copy entire texture
+        TestT2TCopy(utils::Expectation::Success, source, 0, 0, {0, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {16, 16, 1});
+
+        // Copy a region along bottom right boundary
+        TestT2TCopy(utils::Expectation::Success, source, 0, 0, {8, 8, 0}, destination, 0, 0,
+                    {8, 8, 0}, {8, 8, 1});
+
+        // Copy region into mip
+        TestT2TCopy(utils::Expectation::Success, source, 0, 0, {0, 0, 0}, destination, 2, 0,
+                    {0, 0, 0}, {4, 4, 1});
+
+        // Copy mip into region
+        TestT2TCopy(utils::Expectation::Success, source, 2, 0, {0, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {4, 4, 1});
+
+        // Copy between slices
+        TestT2TCopy(utils::Expectation::Success, source, 0, 1, {0, 0, 0}, destination, 0, 1,
+                    {0, 0, 0}, {16, 16, 1});
+    }
+
+    // Empty copies are valid
+    {
+        // An empty copy
+        TestT2TCopy(utils::Expectation::Success, source, 0, 0, {0, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {0, 0, 1});
+
+        // An empty copy touching the side of the source texture
+        TestT2TCopy(utils::Expectation::Success, source, 0, 0, {0, 0, 0}, destination, 0, 0,
+                    {16, 16, 0}, {0, 0, 1});
+
+        // An empty copy touching the side of the destination texture
+        TestT2TCopy(utils::Expectation::Success, source, 0, 0, {0, 0, 0}, destination, 0, 0,
+                    {16, 16, 0}, {0, 0, 1});
+    }
+}
+
+TEST_F(CopyCommandTest_T2T, IncorrectUsage) {
+    dawn::Texture source = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                           dawn::TextureUsageBit::TransferSrc);
+    dawn::Texture destination = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                                dawn::TextureUsageBit::TransferDst);
+
+    // Incorrect source usage causes failure
+    TestT2TCopy(utils::Expectation::Failure, destination, 0, 0, {0, 0, 0}, destination, 0, 0,
+                {0, 0, 0}, {16, 16, 1});
+
+    // Incorrect destination usage causes failure
+    TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, source, 0, 0, {0, 0, 0},
+                {16, 16, 1});
+}
+
+TEST_F(CopyCommandTest_T2T, OutOfBounds) {
+    dawn::Texture source = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                           dawn::TextureUsageBit::TransferSrc);
+    dawn::Texture destination = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                                dawn::TextureUsageBit::TransferDst);
+
+    // OOB on source
+    {
+        // x + width overflows
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {1, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {16, 16, 1});
+
+        // y + height overflows
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 1, 0}, destination, 0, 0,
+                    {0, 0, 0}, {16, 16, 1});
+
+        // non-zero mip overflows
+        TestT2TCopy(utils::Expectation::Failure, source, 1, 0, {0, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {9, 9, 1});
+
+        // empty copy on non-existent mip fails
+        TestT2TCopy(utils::Expectation::Failure, source, 6, 0, {0, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {0, 0, 1});
+
+        // empty copy from non-existent slice fails
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 2, {0, 0, 0}, destination, 0, 0,
+                    {0, 0, 0}, {0, 0, 1});
+    }
+
+    // OOB on destination
+    {
+        // x + width overflows
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 0, 0,
+                    {1, 0, 0}, {16, 16, 1});
+
+        // y + height overflows
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 0, 0,
+                    {0, 1, 0}, {16, 16, 1});
+
+        // non-zero mip overflows
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 1, 0,
+                    {0, 0, 0}, {9, 9, 1});
+
+        // empty copy on non-existent mip fails
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 6, 0,
+                    {0, 0, 0}, {0, 0, 1});
+
+        // empty copy on non-existent slice fails
+        TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 0, 2,
+                    {0, 0, 0}, {0, 0, 1});
+    }
+}
+
+TEST_F(CopyCommandTest_T2T, 2DTextureDepthConstraints) {
+    dawn::Texture source = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                           dawn::TextureUsageBit::TransferSrc);
+    dawn::Texture destination = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                                dawn::TextureUsageBit::TransferDst);
+
+    // Empty copy on source with z > 0 fails
+    TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 1}, destination, 0, 0, {0, 0, 0},
+                {0, 0, 1});
+
+    // Empty copy on destination with z > 0 fails
+    TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 0, 0, {0, 0, 1},
+                {0, 0, 1});
+
+    // Empty copy with depth = 0 fails
+    TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 0, 0, {0, 0, 0},
+                {0, 0, 0});
+}
+
+TEST_F(CopyCommandTest_T2T, 2DTextureDepthStencil) {
+    dawn::Texture source = Create2DTexture(16, 16, 1, 1, dawn::TextureFormat::D32FloatS8Uint,
+                                           dawn::TextureUsageBit::TransferSrc);
+    dawn::Texture destination = Create2DTexture(16, 16, 1, 1, dawn::TextureFormat::D32FloatS8Uint,
+                                                dawn::TextureUsageBit::TransferDst);
+
+    // Success when entire depth stencil subresource is copied
+    TestT2TCopy(utils::Expectation::Success, source, 0, 0, {0, 0, 0}, destination, 0, 0, {0, 0, 0},
+                {16, 16, 1});
+
+    // Failure when depth stencil subresource is partially copied
+    TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 0, 0, {0, 0, 0},
+                {15, 15, 1});
+}
+
+TEST_F(CopyCommandTest_T2T, FormatsMismatch) {
+    dawn::Texture source = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Uint,
+                                           dawn::TextureUsageBit::TransferSrc);
+    dawn::Texture destination = Create2DTexture(16, 16, 5, 2, dawn::TextureFormat::R8G8B8A8Unorm,
+                                                dawn::TextureUsageBit::TransferDst);
+
+    // Failure when formats don't match
+    TestT2TCopy(utils::Expectation::Failure, source, 0, 0, {0, 0, 0}, destination, 0, 0, {0, 0, 0},
+                {0, 0, 1});
+}
+
+TEST_F(CopyCommandTest_T2T, MultisampledCopies) {
+    dawn::Texture sourceMultiSampled1x = Create2DTexture(
+        16, 16, 1, 1, dawn::TextureFormat::R8G8B8A8Unorm, dawn::TextureUsageBit::TransferSrc, 1);
+    dawn::Texture sourceMultiSampled4x = Create2DTexture(
+        16, 16, 1, 1, dawn::TextureFormat::R8G8B8A8Unorm, dawn::TextureUsageBit::TransferSrc, 4);
+    dawn::Texture destinationMultiSampled4x = Create2DTexture(
+        16, 16, 1, 1, dawn::TextureFormat::R8G8B8A8Unorm, dawn::TextureUsageBit::TransferDst, 4);
+
+    // Success when entire multisampled subresource is copied
+    {
+        TestT2TCopy(utils::Expectation::Success, sourceMultiSampled4x, 0, 0, {0, 0, 0},
+                    destinationMultiSampled4x, 0, 0, {0, 0, 0}, {16, 16, 1});
+    }
+
+    // Failures
+    {
+        // An empty copy with mismatched samples fails
+        TestT2TCopy(utils::Expectation::Failure, sourceMultiSampled1x, 0, 0, {0, 0, 0},
+                    destinationMultiSampled4x, 0, 0, {0, 0, 0}, {0, 0, 1});
+
+        // A copy fails when samples are greater than 1, and entire subresource isn't copied
+        TestT2TCopy(utils::Expectation::Failure, sourceMultiSampled4x, 0, 0, {0, 0, 0},
+                    destinationMultiSampled4x, 0, 0, {0, 0, 0}, {15, 15, 1});
+    }
+}
\ No newline at end of file