[WebGPU] Handle aligned copies within the same buffer (#25193)

WebGPU doesn't allow copying within the same buffer, even when the
ranges don't overlap. We can use a temporary buffer for these aligned
copies and clean it up through the existing path.

---------

Signed-off-by: anutosh491 <andersonbhat491@gmail.com>
diff --git a/runtime/src/iree/hal/cts/command_buffer/copy_buffer_test.cc b/runtime/src/iree/hal/cts/command_buffer/copy_buffer_test.cc
index 96f3208..f90665a 100644
--- a/runtime/src/iree/hal/cts/command_buffer/copy_buffer_test.cc
+++ b/runtime/src/iree/hal/cts/command_buffer/copy_buffer_test.cc
@@ -264,6 +264,40 @@
   RunCopyBufferTest(/*source_offset=*/0, /*target_offset=*/0, 1024 * 1024);
 }
 
+TEST_P(CommandBufferCopyBufferTest, CopyAlignedDisjointRangesWithinSameBuffer) {
+  for (iree_device_size_t length : {4, 16, 256, 1024}) {
+    for (iree_device_size_t source_offset : {iree_device_size_t{0}, length}) {
+      iree_device_size_t target_offset = length - source_offset;
+      SCOPED_TRACE(::testing::Message()
+                   << "length=" << length << " source_offset=" << source_offset
+                   << " target_offset=" << target_offset);
+      std::vector<uint8_t> expected = MakeDeterministicBytes(2 * length + 16);
+      Ref<iree_hal_buffer_t> buffer;
+      IREE_ASSERT_OK(CreateDeviceBufferWithData(expected.data(),
+                                                expected.size(), buffer.out()));
+
+      Ref<iree_hal_command_buffer_t> command_buffer;
+      IREE_ASSERT_OK(iree_hal_command_buffer_create(
+          device_, IREE_HAL_COMMAND_BUFFER_MODE_ONE_SHOT,
+          IREE_HAL_COMMAND_CATEGORY_TRANSFER, IREE_HAL_QUEUE_AFFINITY_ANY,
+          binding_capacity(), command_buffer.out()));
+      IREE_ASSERT_OK(iree_hal_command_buffer_begin(command_buffer));
+      IREE_ASSERT_OK(iree_hal_command_buffer_copy_buffer(
+          command_buffer,
+          MakeRef(buffer, kSourceBufferSlot, source_offset, length),
+          MakeRef(buffer, kTargetBufferSlot, target_offset, length),
+          IREE_HAL_COPY_FLAG_NONE));
+      IREE_ASSERT_OK(iree_hal_command_buffer_end(command_buffer));
+      IREE_ASSERT_OK(SubmitWithBindings(command_buffer, buffer, buffer));
+
+      std::memcpy(expected.data() + target_offset,
+                  expected.data() + source_offset, length);
+      EXPECT_THAT(ReadBufferBytes(buffer, 0, expected.size()),
+                  ContainerEq(expected));
+    }
+  }
+}
+
 CTS_REGISTER_COMMAND_BUFFER_TEST_SUITE(CommandBufferCopyBufferTest);
 
 }  // namespace iree::hal::cts
diff --git a/runtime/src/iree/hal/drivers/webgpu/webgpu_imports.mjs b/runtime/src/iree/hal/drivers/webgpu/webgpu_imports.mjs
index f8e4ba8..0e94286 100644
--- a/runtime/src/iree/hal/drivers/webgpu/webgpu_imports.mjs
+++ b/runtime/src/iree/hal/drivers/webgpu/webgpu_imports.mjs
@@ -270,10 +270,21 @@
 
           if ((effectiveSrc & 3) === 0 && (effectiveDst & 3) === 0 &&
               (length & 3) === 0) {
-            // Aligned: use native copyBufferToBuffer.
-            encoder.copyBufferToBuffer(
-                buffers[srcSlot], effectiveSrc, buffers[dstSlot], effectiveDst,
-                length);
+            if (buffers[srcSlot] === buffers[dstSlot]) {
+              const scratch = device.createBuffer({
+                usage: 0x0004 | 0x0008,  // COPY_SRC | COPY_DST
+                size: length,
+              });
+              temporaryBuffers.push(scratch);
+              encoder.copyBufferToBuffer(
+                  buffers[srcSlot], effectiveSrc, scratch, 0, length);
+              encoder.copyBufferToBuffer(
+                  scratch, 0, buffers[dstSlot], effectiveDst, length);
+            } else {
+              encoder.copyBufferToBuffer(
+                  buffers[srcSlot], effectiveSrc, buffers[dstSlot],
+                  effectiveDst, length);
+            }
           } else {
             // Unaligned: dispatch copy builtin shader.
             const params = device.createBuffer({