Add cleanup when encoding indirect draw validations.

- Cleanup is necessary because otherwise encoded render commands may be
  leaked if the validation encoding fails. (The leaked render commands
  can then trigger an assert in ~Device::Cache because the commands can
  hold a ref to an AttachmentState that was not destroyed, and hence
  still be in the device cache.
- Added explicit check in EncoderIndirectDrawValidationCommands for
  device 'alive-ness' since it may create new objects later and hit the
  same error later on anyways.
- Added regression test.

Fixed: chromium:1365011
Change-Id: I342479a4227fc43d82ea35f662d049e6db2b1740
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/103340
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Loko Kung <lokokung@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/dawn/native/EncodingContext.cpp b/src/dawn/native/EncodingContext.cpp
index 9b7f367..3f2e796 100644
--- a/src/dawn/native/EncodingContext.cpp
+++ b/src/dawn/native/EncodingContext.cpp
@@ -127,9 +127,12 @@
         // mPendingCommands contains only the commands from BeginRenderPassCmd to
         // EndRenderPassCmd, inclusive. Now we swap out this allocator with a fresh one to give
         // the validation encoder a chance to insert its commands first.
+        // Note: If encoding validation commands fails, no commands should be in mPendingCommands,
+        //       so swap back the renderCommands to ensure that they are not leaked.
         CommandAllocator renderCommands = std::move(mPendingCommands);
-        DAWN_TRY(EncodeIndirectDrawValidationCommands(mDevice, commandEncoder, &usageTracker,
-                                                      &indirectDrawMetadata));
+        DAWN_TRY_WITH_CLEANUP(EncodeIndirectDrawValidationCommands(
+                                  mDevice, commandEncoder, &usageTracker, &indirectDrawMetadata),
+                              { mPendingCommands = std::move(renderCommands); });
         CommitCommands(std::move(mPendingCommands));
         CommitCommands(std::move(renderCommands));
     }
diff --git a/src/dawn/native/IndirectDrawValidationEncoder.cpp b/src/dawn/native/IndirectDrawValidationEncoder.cpp
index db940e7..e8e4e1b 100644
--- a/src/dawn/native/IndirectDrawValidationEncoder.cpp
+++ b/src/dawn/native/IndirectDrawValidationEncoder.cpp
@@ -239,6 +239,12 @@
                                                 CommandEncoder* commandEncoder,
                                                 RenderPassResourceUsageTracker* usageTracker,
                                                 IndirectDrawMetadata* indirectDrawMetadata) {
+    // Since encoding validation commands may create new objects, verify that the device is alive.
+    // TODO(dawn:1199): This check is obsolete if device loss causes device.destroy().
+    //   - This function only happens within the context of a TryEncode which would catch the
+    //     same issue if device loss implied device.destroy().
+    DAWN_TRY(device->ValidateIsAlive());
+
     struct Batch {
         const IndirectDrawMetadata::IndirectValidationBatch* metadata;
         uint64_t numIndexBufferElements;
diff --git a/src/dawn/tests/end2end/DeviceLostTests.cpp b/src/dawn/tests/end2end/DeviceLostTests.cpp
index d9aa3d0..4c2b01ed85 100644
--- a/src/dawn/tests/end2end/DeviceLostTests.cpp
+++ b/src/dawn/tests/end2end/DeviceLostTests.cpp
@@ -475,6 +475,37 @@
     bg = nullptr;
 }
 
+// This is a regression test for crbug.com/1365011 where ending a render pass with an indirect draw
+// in it after the device is lost would cause render commands to be leaked.
+TEST_P(DeviceLostTest, DeviceLostInRenderPassWithDrawIndirect) {
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, 4u, 4u);
+    utils::ComboRenderPipelineDescriptor desc;
+    desc.vertex.module = utils::CreateShaderModule(device, R"(
+        @vertex fn main(@builtin(vertex_index) i : u32) -> @builtin(position) vec4<f32> {
+            var pos = array<vec2<f32>, 3>(
+                vec2<f32>(-1.0, -1.0),
+                vec2<f32>(3.0, -1.0),
+                vec2<f32>(-1.0, 3.0));
+            return vec4<f32>(pos[i], 0.0, 1.0);
+        }
+    )");
+    desc.cFragment.module = utils::CreateShaderModule(device, R"(
+        @fragment fn main() -> @location(0) vec4<f32> {
+            return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+        }
+    )");
+    desc.cTargets[0].format = renderPass.colorFormat;
+    wgpu::Buffer indirectBuffer =
+        utils::CreateBufferFromData<uint32_t>(device, wgpu::BufferUsage::Indirect, {3, 1, 0, 0});
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+    pass.SetPipeline(pipeline);
+    pass.DrawIndirect(indirectBuffer, 0);
+    LoseDeviceForTesting();
+    pass.End();
+}
+
 // Attempting to set an object label after device loss should not cause an error.
 TEST_P(DeviceLostTest, SetLabelAfterDeviceLoss) {
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());