vulkan: Split multi-layer buffer copies on Maleoon

Huawei Maleoon drivers mis-stride multi-layer buffer<->image copies: in
a single VkBufferImageCopy region with layerCount > 1 and a padded
bufferImageHeight, only the first array layer / depth slice lands at the
correct buffer offset; every later layer is read/written at the wrong
offset and comes back corrupted.

Add a VulkanSplitBufferTextureCopyForArrayLayers toggle that emits one
copy region per layer, each with layerCount = 1 and an explicit
per-layer buffer offset, so the driver never strides between layers
itself. The toggle is enabled by default only on Maleoon and applies to
both CopyBufferToTexture and CopyTextureToBuffer.

Fixes the WebGPU CTS image_copy clusters on Mate60 / Maleoon 910:
api,operation,command_buffer,image_copy:mip_levels:*
api,operation,command_buffer,image_copy:rowsPerImage_and_bytesPerRow:*
api,operation,command_buffer,image_copy:rowsPerImage_and_bytesPerRow_depth_stencil:*

Bug: 520126486, 520126487
Change-Id: Ic913e3ca25c84169f9ff19d974e86ecf0b61f650
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/317116
Reviewed-by: Brandon Jones <bajones@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Commit-Queue: Kai Ninomiya <kainino@chromium.org>
diff --git a/src/dawn/native/Toggles.cpp b/src/dawn/native/Toggles.cpp
index 444f39d..5ade3d6 100644
--- a/src/dawn/native/Toggles.cpp
+++ b/src/dawn/native/Toggles.cpp
@@ -496,6 +496,14 @@
       "Use a shader based blit instead of a copy command to copy a buffer to a texture with "
       "supported format.",
       "https://crbug.com/dawn/348653642", ToggleStage::Device}},
+    {Toggle::VulkanSplitBufferTextureCopyForArrayLayers,
+     {"vulkan_split_buffer_texture_copy_for_array_layers",
+      "Split a multi-layer buffer-to-texture / texture-to-buffer copy into one vkCmdCopy*Image "
+      "region per array layer / depth slice, each with an explicit per-layer buffer offset, "
+      "instead of a single region with layerCount > 1. Workaround for Huawei Maleoon GPUs, whose "
+      "drivers mis-stride the buffer between layers when bufferImageHeight is padded, corrupting "
+      "every layer after the first.",
+      "https://issues.chromium.org/issues/520126486", ToggleStage::Device}},
     {Toggle::GLUseArrayLengthFromUniform,
      {"gl_use_array_length_from_uniform",
       "Use arrayLengthFromUniform transform to replace arrayLength() function calls of dynamic "
diff --git a/src/dawn/native/Toggles.h b/src/dawn/native/Toggles.h
index 5434a62..2d1020e 100644
--- a/src/dawn/native/Toggles.h
+++ b/src/dawn/native/Toggles.h
@@ -126,6 +126,7 @@
     UseBlitForFloat32TextureCopy,
     UseBlitForT2B,
     UseBlitForB2T,
+    VulkanSplitBufferTextureCopyForArrayLayers,
     GLUseArrayLengthFromUniform,
     D3D11DisableCPUUploadBuffers,
     UseT2B2TForSRGBTextureCopy,
diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index 382b359..9601d52 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn/native/vulkan/CommandBufferVk.cpp
@@ -85,6 +85,33 @@
     DAWN_UNREACHABLE();
 }
 
+std::vector<VkBufferImageCopy> ComputePerArrayLayerBufferImageCopyRegions(
+    const BufferCopy& bufferCopy,
+    const TextureCopy& textureCopy,
+    const TexelExtent3D& copySize,
+    const TypedTexelBlockInfo& blockInfo) {
+    const uint64_t bytesPerImage =
+        blockInfo.ToBytes(bufferCopy.blocksPerRow) * static_cast<uint64_t>(bufferCopy.rowsPerImage);
+    const uint32_t layerCount =
+        static_cast<uint32_t>(static_cast<uint64_t>(copySize.depthOrArrayLayers));
+
+    TexelExtent3D singleLayerSize = copySize;
+    singleLayerSize.depthOrArrayLayers = TexelCount{1u};
+
+    std::vector<VkBufferImageCopy> regions;
+    regions.reserve(layerCount);
+    for (uint32_t layer = 0; layer < layerCount; ++layer) {
+        TextureCopy layerTextureCopy = textureCopy;
+        layerTextureCopy.origin.z = textureCopy.origin.z + TexelCount{layer};
+
+        VkBufferImageCopy region = ComputeBufferImageCopyRegion(bufferCopy, layerTextureCopy,
+                                                                blockInfo.ToBlock(singleLayerSize));
+        region.bufferOffset = bufferCopy.offset + uint64_t{layer} * bytesPerImage;
+        regions.push_back(region);
+    }
+    return regions;
+}
+
 bool HasSameTextureCopyExtent(const TextureCopy& srcCopy,
                               const TextureCopy& dstCopy,
                               const TexelExtent3D& copySize) {
@@ -1167,15 +1194,21 @@
                 ToBackend(src.buffer)->EnsureDataInitialized(recordingContext);
 
                 const TypedTexelBlockInfo& blockInfo = GetBlockInfo(dst);
-                VkBufferImageCopy region =
-                    ComputeBufferImageCopyRegion(src, dst, blockInfo.ToBlock(copy->copySize));
-                VkImageSubresourceLayers subresource = region.imageSubresource;
+                std::vector<VkBufferImageCopy> regions;
+                if (device->IsToggleEnabled(Toggle::VulkanSplitBufferTextureCopyForArrayLayers) &&
+                    copy->copySize.depthOrArrayLayers > TexelCount{1u}) {
+                    regions = ComputePerArrayLayerBufferImageCopyRegions(src, dst, copy->copySize,
+                                                                         blockInfo);
+                } else {
+                    regions.push_back(
+                        ComputeBufferImageCopyRegion(src, dst, blockInfo.ToBlock(copy->copySize)));
+                }
 
                 SubresourceRange range =
                     GetSubresourcesAffectedByCopy(copy->destination, copy->copySize);
 
-                if (IsCompleteSubresourceCopiedTo(dst.texture.Get(), copy->copySize,
-                                                  subresource.mipLevel, dst.aspect)) {
+                if (IsCompleteSubresourceCopiedTo(dst.texture.Get(), copy->copySize, dst.mipLevel,
+                                                  dst.aspect)) {
                     // Since texture has been overwritten, it has been "initialized"
                     dst.texture->SetIsSubresourceContentInitialized(true, range);
                 } else {
@@ -1194,8 +1227,9 @@
 
                 // Dawn guarantees dstImage be in the TRANSFER_DST_OPTIMAL layout after the
                 // copy command.
-                device->fn.CmdCopyBufferToImage(commands, srcBuffer, dstImage,
-                                                VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
+                device->fn.CmdCopyBufferToImage(
+                    commands, srcBuffer, dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                    static_cast<uint32_t>(regions.size()), regions.data());
                 break;
             }
 
@@ -1211,8 +1245,15 @@
                 ToBackend(dst.buffer)->EnsureDataInitializedAsDestination(recordingContext, copy);
 
                 const TypedTexelBlockInfo& blockInfo = GetBlockInfo(src);
-                VkBufferImageCopy region =
-                    ComputeBufferImageCopyRegion(dst, src, blockInfo.ToBlock(copy->copySize));
+                std::vector<VkBufferImageCopy> regions;
+                if (device->IsToggleEnabled(Toggle::VulkanSplitBufferTextureCopyForArrayLayers) &&
+                    copy->copySize.depthOrArrayLayers > TexelCount{1u}) {
+                    regions = ComputePerArrayLayerBufferImageCopyRegions(dst, src, copy->copySize,
+                                                                         blockInfo);
+                } else {
+                    regions.push_back(
+                        ComputeBufferImageCopyRegion(dst, src, blockInfo.ToBlock(copy->copySize)));
+                }
 
                 SubresourceRange range =
                     GetSubresourcesAffectedByCopy(copy->source, copy->copySize);
@@ -1230,7 +1271,8 @@
                 VkBuffer dstBuffer = ToBackend(dst.buffer)->GetHandle();
                 // The Dawn CopySrc usage is always mapped to GENERAL
                 device->fn.CmdCopyImageToBuffer(commands, srcImage, VK_IMAGE_LAYOUT_GENERAL,
-                                                dstBuffer, 1, &region);
+                                                dstBuffer, static_cast<uint32_t>(regions.size()),
+                                                regions.data());
                 break;
             }
 
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index b3fa6a4..e38794b 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -1089,6 +1089,14 @@
         deviceToggles->Default(Toggle::IgnoreImportedAHardwareBufferVulkanImageSize, true);
     }
 
+    if (gpu_info::IsHuaweiMaleoon(GetVendorId(), GetDeviceId())) {
+        // crbug.com/520126486: Huawei Maleoon drivers mis-stride multi-layer
+        // buffer<->image copies: only the first array layer / depth slice lands at
+        // the correct buffer offset when a copy region has layerCount > 1.
+        // Split such copies into one region per layer.
+        deviceToggles->Default(Toggle::VulkanSplitBufferTextureCopyForArrayLayers, true);
+    }
+
     // Collapse redundant subgroup min and max operations to workaround a driver crash on some AMD
     // GPUs.  Should only affect AMD Windows Driver versions < 31.0.22000.0, but because this is a
     // harmless "optimizing" workaround go ahead enable for all versions. See: