Integration tests for CommandEncoder.kt

Bug: b/444152925
Change-Id: I92f8c06471670a6d62f3fac3b0e679d732893648
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/266554
Reviewed-by: Jim Blackler <jimblackler@google.com>
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Tarun Saini <sainitarun@google.com>
diff --git a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/CommandEncoderTest.kt b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/CommandEncoderTest.kt
new file mode 100644
index 0000000..3185dc9
--- /dev/null
+++ b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/CommandEncoderTest.kt
@@ -0,0 +1,312 @@
+package androidx.webgpu
+
+import androidx.test.filters.SmallTest
+import androidx.webgpu.helper.UncapturedErrorException
+import androidx.webgpu.helper.WebGpu
+import androidx.webgpu.helper.createWebGpu
+import java.nio.ByteBuffer
+import junit.framework.TestCase
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class CommandEncoderTest {
+
+  private lateinit var device: Device
+  private lateinit var webGpu: WebGpu
+
+  @Before
+  fun setup() = runBlocking {
+    webGpu = createWebGpu()
+    device = webGpu.device
+  }
+
+  @After
+  fun teardown() {
+    runCatching { device.destroy() }
+    webGpu.close()
+
+  }
+
+  /**
+   * Verifies that a command encoder cannot be used after `finish()` has been called.
+   */
+  @Test
+  fun testFinish() {
+    val encoder = device.createCommandEncoder()
+    encoder.finish()
+
+    val dummyBuffer = device.createBuffer(
+      BufferDescriptor(size = 4, usage = BufferUsage.CopySrc)
+    )
+    assertThrows(
+      "Using a finished encoder should throw an error",
+      UncapturedErrorException::class.java
+    ) {
+      encoder.copyBufferToBuffer(dummyBuffer, 0, dummyBuffer, 0, 4)
+    }
+    dummyBuffer.destroy()
+  }
+
+  /**
+   * Ensures that a balanced `pushDebugGroup`/`popDebugGroup` pair is a valid operation.
+   *
+   * This test uses an error scope to assert that no validation errors occur when pushing
+   * and immediately popping a debug group on a command encoder.
+   */
+  @Test
+  fun testDebugGroups() {
+    val encoder = device.createCommandEncoder()
+
+    device.pushErrorScope(ErrorFilter.Validation)
+    encoder.pushDebugGroup("MyDebugGroup")
+    encoder.popDebugGroup()
+    val error = runBlocking { device.popErrorScope() }
+
+    TestCase.assertTrue(
+      "Expected no error for balanced push/pop debug group",
+      error.status == PopErrorScopeStatus.Success &&
+        error.type == ErrorType.NoError
+    )
+    encoder.finish()
+  }
+
+  /**
+   * Verifies that `finish()` fails if a nested pass was attempted.
+   *
+   * This confirms that attempting to begin a second pass before the first has ended
+   * invalidates the encoder, causing a validation error upon calling `finish()`.
+   */
+  @Test
+  fun finish_failsWhenNestedPassWasAttempted() {
+    val encoder = device.createCommandEncoder()
+    val activePassEncoder = encoder.beginComputePass()
+
+    encoder.beginComputePass()
+
+    activePassEncoder.end()
+
+    device.pushErrorScope(ErrorFilter.Validation)
+    encoder.finish()
+    val error = runBlocking { device.popErrorScope() }
+
+    TestCase.assertTrue(
+      "Expected a validation error on .finish() due to an earlier nested pass attempt",
+      error.type == ErrorType.Validation,
+    )
+  }
+
+  /**
+   * Verifies that `beginRenderPass` successfully creates a `GPURenderPassEncoder`.
+   *
+   * This test provides a valid `RenderPassDescriptor` and asserts that the
+   * returned encoder is not null, confirming the command's successful initiation.
+   */
+  @Test
+  fun testBeginRenderPass() {
+    val texture = device.createTexture(
+      TextureDescriptor(
+        size = Extent3D(1, 1, 1),
+        format = TextureFormat.RGBA8Unorm,
+        usage = TextureUsage.RenderAttachment
+      )
+    )
+    val textureView = texture.createView()
+
+    val encoder = device.createCommandEncoder()
+    val passEncoder = encoder.beginRenderPass(
+      RenderPassDescriptor(
+        colorAttachments = arrayOf(
+          RenderPassColorAttachment(
+            view = textureView,
+            loadOp = LoadOp.Clear,
+            storeOp = StoreOp.Store,
+            clearValue = Color(0.0, 0.0, 0.0, 1.0)
+          )
+        )
+      )
+    )
+    TestCase.assertNotNull(passEncoder)
+    passEncoder.end()
+    encoder.finish()
+  }
+
+  /**
+   * Verifies that a render pass with `LoadOp.Clear` correctly clears a texture to a specific color.
+   */
+  @Test
+  fun testBeginRenderPass_clearsTextureCorrectly() {
+    val queue = device.getQueue()
+
+    val textureWidth = 1
+    val textureHeight = 1
+    val textureFormat = TextureFormat.RGBA8Unorm
+
+    val bytesPerPixel = 4
+    val bufferSize = (textureWidth * textureHeight * bytesPerPixel).toLong()
+
+    val renderTexture = device.createTexture(
+      TextureDescriptor(
+        size = Extent3D(textureWidth, textureHeight, 1),
+        format = textureFormat,
+        usage = TextureUsage.RenderAttachment or TextureUsage.CopySrc
+      )
+    )
+    val textureView = renderTexture.createView()
+
+    val readbackBuffer = device.createBuffer(
+      BufferDescriptor(
+        size = bufferSize,
+        usage = BufferUsage.CopyDst or BufferUsage.MapRead
+      )
+    )
+
+    val clearColor = Color(0.2, 0.8, 0.6, 1.0)
+
+    val encoder = device.createCommandEncoder()
+
+    // This is the operation we are testing.
+    val passEncoder = encoder.beginRenderPass(
+      RenderPassDescriptor(
+        colorAttachments = arrayOf(
+          RenderPassColorAttachment(
+            view = textureView,
+            loadOp = LoadOp.Clear,
+            storeOp = StoreOp.Store,
+            clearValue = clearColor
+          )
+        )
+      )
+    )
+    passEncoder.end()
+
+    encoder.copyTextureToBuffer(
+      source = TexelCopyTextureInfo(texture = renderTexture),
+      destination = TexelCopyBufferInfo(
+        buffer = readbackBuffer,
+        layout = TexelCopyBufferLayout()
+      ),
+      copySize = Extent3D(textureWidth, textureHeight, 1)
+    )
+
+    val commandBuffer = encoder.finish()
+
+    queue.submit(arrayOf(commandBuffer))
+
+    val mapResult = runBlocking { readbackBuffer.mapAsync(MapMode.Read, 0, bufferSize) }
+    TestCase.assertEquals(MapAsyncStatus.Success, mapResult.status)
+
+    val mappedData: ByteBuffer = readbackBuffer.getConstMappedRange(0, bufferSize)
+
+    val expectedBytes = byteArrayOf(
+      (clearColor.r * 255).toInt().toByte(),
+      (clearColor.g * 255).toInt().toByte(),
+      (clearColor.b * 255).toInt().toByte(),
+      (clearColor.a * 255).toInt().toByte()
+    )
+
+    val actualBytes = ByteArray(bytesPerPixel)
+    mappedData.get(actualBytes)
+
+    assertArrayEquals(
+      "The bytes read back from the texture do not match the expected clear color.",
+      expectedBytes,
+      actualBytes
+    )
+
+    readbackBuffer.unmap()
+    readbackBuffer.destroy()
+    renderTexture.destroy()
+  }
+
+  /**
+   * Verifies that the `clearBuffer` command can be successfully encoded without validation errors.
+   *
+   * This test pushes a validation error scope, encodes the `clearBuffer` command for a valid buffer
+   * and range, and then pops the error scope to assert that the operation was valid.
+   */
+  @Test
+  fun testClearBuffer() {
+    val buffer = device.createBuffer(
+      BufferDescriptor(
+        size = 16,
+        usage = BufferUsage.CopyDst
+      )
+    )
+
+    val encoder = device.createCommandEncoder()
+    device.pushErrorScope(ErrorFilter.Validation)
+    encoder.clearBuffer(buffer, 0, 16)
+    val error = runBlocking { device.popErrorScope() }
+
+    TestCase.assertTrue(
+      "Expected clearBuffer to succeed",
+      error.status == PopErrorScopeStatus.Success &&
+        error.type == ErrorType.NoError
+    )
+    encoder.finish()
+  }
+
+  /**
+   * Validates that the `resolveQuerySet` command can be encoded without errors.
+   *
+   * This test creates a `QuerySet` and a destination buffer, then encodes a command to
+   * resolve the query results into the buffer. It uses an error scope to assert that
+   * the command is considered a valid operation by the encoder.
+   */
+  @Test
+  fun testResolveQuerySet() {
+    val querySet = device.createQuerySet(
+      QuerySetDescriptor(
+        type = QueryType.Occlusion,
+        count = 1
+      )
+    )
+
+    val destination = device.createBuffer(
+      BufferDescriptor(
+        size = 8, // Occlusion queries are 64-bit (8 bytes)
+        usage = BufferUsage.QueryResolve
+      )
+    )
+
+    val encoder = device.createCommandEncoder()
+    device.pushErrorScope(ErrorFilter.Validation)
+    encoder.resolveQuerySet(querySet, 0, 1, destination, 0)
+    val error = runBlocking { device.popErrorScope() }
+
+    TestCase.assertTrue(
+      "Expected resolveQuerySet to succeed",
+      error.status == PopErrorScopeStatus.Success &&
+        error.type == ErrorType.NoError
+    )
+    encoder.finish()
+  }
+
+  /**
+   * Ensures that `insertDebugMarker` can be encoded without validation errors.
+   *
+   * This test verifies that adding a debug marker, a non-functional command used for debugging
+   * and profiling, is a valid operation within a command encoder. An error scope is used to
+   * confirm the success of the command encoding.
+   */
+  @Test
+  fun testInsertDebugMarker() {
+    val encoder = device.createCommandEncoder()
+
+    device.pushErrorScope(ErrorFilter.Validation)
+    encoder.insertDebugMarker("MyDebugMarker")
+    val error = runBlocking { device.popErrorScope() }
+
+    TestCase.assertTrue(
+      "Expected insertDebugMarker to succeed",
+      error.status == PopErrorScopeStatus.Success &&
+        error.type == ErrorType.NoError
+    )
+    encoder.finish()
+  }
+}