Implement drawIndexedIndirect validation

Every render pass which invokes DrawIndexedIndirect, either directly or
through a RenderBundle execution, is now preceded immediately by at
least one validation pass.

All indirect buffer offests used with DII are validated, and their
validated values are copied into a separate scratch buffer (or zeroed
out there, in the case of validation failure). All encoded DII commands
are rewritten to use the validated parameters instead of the original
ones.

Bug: dawn:809
Change-Id: I5eead937f19536f84f89e2c8e6fed7f18f0aee9f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/63461
Commit-Queue: Ken Rockot <rockot@google.com>
Reviewed-by: Austin Eng <enga@chromium.org>
diff --git a/src/common/NonCopyable.h b/src/common/NonCopyable.h
index 61f15ca..2d217df 100644
--- a/src/common/NonCopyable.h
+++ b/src/common/NonCopyable.h
@@ -21,6 +21,9 @@
     constexpr NonCopyable() = default;
     ~NonCopyable() = default;
 
+    NonCopyable(NonCopyable&&) = default;
+    NonCopyable& operator=(NonCopyable&&) = default;
+
   private:
     NonCopyable(const NonCopyable&) = delete;
     void operator=(const NonCopyable&) = delete;
diff --git a/src/dawn_native/BUILD.gn b/src/dawn_native/BUILD.gn
index b3b1f6e..6539d0b 100644
--- a/src/dawn_native/BUILD.gn
+++ b/src/dawn_native/BUILD.gn
@@ -237,9 +237,14 @@
     "Format.cpp",
     "Format.h",
     "Forward.h",
+    "IndirectDrawMetadata.cpp",
+    "IndirectDrawMetadata.h",
+    "IndirectDrawValidationEncoder.cpp",
+    "IndirectDrawValidationEncoder.h",
     "Instance.cpp",
     "Instance.h",
     "IntegerTypes.h",
+    "InternalPipelineStore.cpp",
     "InternalPipelineStore.h",
     "Limits.cpp",
     "Limits.h",
@@ -286,6 +291,8 @@
     "RingBufferAllocator.h",
     "Sampler.cpp",
     "Sampler.h",
+    "ScratchBuffer.cpp",
+    "ScratchBuffer.h",
     "ShaderModule.cpp",
     "ShaderModule.h",
     "StagingBuffer.cpp",
diff --git a/src/dawn_native/Buffer.cpp b/src/dawn_native/Buffer.cpp
index 7698c97..bd70648 100644
--- a/src/dawn_native/Buffer.cpp
+++ b/src/dawn_native/Buffer.cpp
@@ -147,6 +147,12 @@
         if (mUsage & wgpu::BufferUsage::QueryResolve) {
             mUsage |= kInternalStorageBuffer;
         }
+
+        // We also add internal storage usage for Indirect buffers if validation is enabled, since
+        // validation involves binding them as storage buffers for use in a compute pass.
+        if ((mUsage & wgpu::BufferUsage::Indirect) && device->IsValidationEnabled()) {
+            mUsage |= kInternalStorageBuffer;
+        }
     }
 
     BufferBase::BufferBase(DeviceBase* device,
diff --git a/src/dawn_native/CMakeLists.txt b/src/dawn_native/CMakeLists.txt
index fb93f89..8e2b2ce 100644
--- a/src/dawn_native/CMakeLists.txt
+++ b/src/dawn_native/CMakeLists.txt
@@ -98,6 +98,10 @@
     "Extensions.h"
     "ExternalTexture.cpp"
     "ExternalTexture.h"
+    "IndirectDrawMetadata.cpp"
+    "IndirectDrawMetadata.h"
+    "IndirectDrawValidationEncoder.cpp"
+    "IndirectDrawValidationEncoder.h"
     "ObjectContentHasher.cpp"
     "ObjectContentHasher.h"
     "Format.cpp"
@@ -105,6 +109,7 @@
     "Forward.h"
     "Instance.cpp"
     "Instance.h"
+    "InternalPipelineStore.cpp"
     "InternalPipelineStore.h"
     "IntegerTypes.h"
     "Limits.cpp"
@@ -150,6 +155,8 @@
     "RingBufferAllocator.h"
     "Sampler.cpp"
     "Sampler.h"
+    "ScratchBuffer.cpp"
+    "ScratchBuffer.h"
     "ShaderModule.cpp"
     "ShaderModule.h"
     "StagingBuffer.cpp"
diff --git a/src/dawn_native/CommandBuffer.cpp b/src/dawn_native/CommandBuffer.cpp
index 43455c2..1300cd1 100644
--- a/src/dawn_native/CommandBuffer.cpp
+++ b/src/dawn_native/CommandBuffer.cpp
@@ -38,6 +38,14 @@
         Destroy();
     }
 
+    void CommandBufferBase::DoNextSetValidatedBufferLocationsInternal() {
+        SetValidatedBufferLocationsInternalCmd* cmd =
+            mCommands.NextCommand<SetValidatedBufferLocationsInternalCmd>();
+        for (const DeferredBufferLocationUpdate& update : cmd->updates) {
+            update.location->Set(update.buffer.Get(), update.offset);
+        }
+    }
+
     // static
     CommandBufferBase* CommandBufferBase::MakeError(DeviceBase* device) {
         return new CommandBufferBase(device, ObjectBase::kError);
diff --git a/src/dawn_native/CommandBuffer.h b/src/dawn_native/CommandBuffer.h
index e90d320..94159a1 100644
--- a/src/dawn_native/CommandBuffer.h
+++ b/src/dawn_native/CommandBuffer.h
@@ -44,6 +44,8 @@
       protected:
         ~CommandBufferBase();
 
+        void DoNextSetValidatedBufferLocationsInternal();
+
         CommandIterator mCommands;
 
       private:
diff --git a/src/dawn_native/CommandBufferStateTracker.cpp b/src/dawn_native/CommandBufferStateTracker.cpp
index f3a6b47..001be91 100644
--- a/src/dawn_native/CommandBufferStateTracker.cpp
+++ b/src/dawn_native/CommandBufferStateTracker.cpp
@@ -308,4 +308,13 @@
     PipelineLayoutBase* CommandBufferStateTracker::GetPipelineLayout() const {
         return mLastPipelineLayout;
     }
+
+    wgpu::IndexFormat CommandBufferStateTracker::GetIndexFormat() const {
+        return mIndexFormat;
+    }
+
+    uint64_t CommandBufferStateTracker::GetIndexBufferSize() const {
+        return mIndexBufferSize;
+    }
+
 }  // namespace dawn_native
diff --git a/src/dawn_native/CommandBufferStateTracker.h b/src/dawn_native/CommandBufferStateTracker.h
index 805a4fb..0a6c587 100644
--- a/src/dawn_native/CommandBufferStateTracker.h
+++ b/src/dawn_native/CommandBufferStateTracker.h
@@ -47,6 +47,8 @@
 
         BindGroupBase* GetBindGroup(BindGroupIndex index) const;
         PipelineLayoutBase* GetPipelineLayout() const;
+        wgpu::IndexFormat GetIndexFormat() const;
+        uint64_t GetIndexBufferSize() const;
 
       private:
         MaybeError ValidateOperation(ValidationAspects requiredAspects);
diff --git a/src/dawn_native/CommandEncoder.cpp b/src/dawn_native/CommandEncoder.cpp
index a256382..143ba7e 100644
--- a/src/dawn_native/CommandEncoder.cpp
+++ b/src/dawn_native/CommandEncoder.cpp
@@ -508,6 +508,7 @@
         uint32_t width = 0;
         uint32_t height = 0;
         Ref<AttachmentState> attachmentState;
+        mEncodingContext.WillBeginRenderPass();
         bool success =
             mEncodingContext.TryEncode(this, [&](CommandAllocator* allocator) -> MaybeError {
                 uint32_t sampleCount = 0;
@@ -922,6 +923,18 @@
         return commandBuffer.Detach();
     }
 
+    void CommandEncoder::EncodeSetValidatedBufferLocationsInternal(
+        std::vector<DeferredBufferLocationUpdate> updates) {
+        ASSERT(GetDevice()->IsValidationEnabled());
+        mEncodingContext.TryEncode(this, [&](CommandAllocator* allocator) -> MaybeError {
+            SetValidatedBufferLocationsInternalCmd* cmd =
+                allocator->Allocate<SetValidatedBufferLocationsInternalCmd>(
+                    Command::SetValidatedBufferLocationsInternal);
+            cmd->updates = std::move(updates);
+            return {};
+        });
+    }
+
     ResultOrError<Ref<CommandBufferBase>> CommandEncoder::FinishInternal(
         const CommandBufferDescriptor* descriptor) {
         DeviceBase* device = GetDevice();
diff --git a/src/dawn_native/CommandEncoder.h b/src/dawn_native/CommandEncoder.h
index 7eaca04..dbb33cf 100644
--- a/src/dawn_native/CommandEncoder.h
+++ b/src/dawn_native/CommandEncoder.h
@@ -76,6 +76,9 @@
 
         CommandBufferBase* APIFinish(const CommandBufferDescriptor* descriptor = nullptr);
 
+        void EncodeSetValidatedBufferLocationsInternal(
+            std::vector<DeferredBufferLocationUpdate> updates);
+
       private:
         ResultOrError<Ref<CommandBufferBase>> FinishInternal(
             const CommandBufferDescriptor* descriptor);
diff --git a/src/dawn_native/Commands.cpp b/src/dawn_native/Commands.cpp
index c2bd0dd..ea122e7 100644
--- a/src/dawn_native/Commands.cpp
+++ b/src/dawn_native/Commands.cpp
@@ -158,6 +158,12 @@
                     cmd->~SetStencilReferenceCmd();
                     break;
                 }
+                case Command::SetValidatedBufferLocationsInternal: {
+                    SetValidatedBufferLocationsInternalCmd* cmd =
+                        commands->NextCommand<SetValidatedBufferLocationsInternalCmd>();
+                    cmd->~SetValidatedBufferLocationsInternalCmd();
+                    break;
+                }
                 case Command::SetViewport: {
                     SetViewportCmd* cmd = commands->NextCommand<SetViewportCmd>();
                     cmd->~SetViewportCmd();
@@ -313,6 +319,10 @@
                 commands->NextCommand<SetStencilReferenceCmd>();
                 break;
 
+            case Command::SetValidatedBufferLocationsInternal:
+                commands->NextCommand<SetValidatedBufferLocationsInternalCmd>();
+                break;
+
             case Command::SetViewport:
                 commands->NextCommand<SetViewportCmd>();
                 break;
diff --git a/src/dawn_native/Commands.h b/src/dawn_native/Commands.h
index 333d19b..09acd10 100644
--- a/src/dawn_native/Commands.h
+++ b/src/dawn_native/Commands.h
@@ -63,6 +63,7 @@
         SetBlendConstant,
         SetBindGroup,
         SetIndexBuffer,
+        SetValidatedBufferLocationsInternal,
         SetVertexBuffer,
         WriteBuffer,
         WriteTimestamp,
@@ -224,6 +225,16 @@
         uint32_t reference;
     };
 
+    struct DeferredBufferLocationUpdate {
+        Ref<BufferLocation> location;
+        Ref<BufferBase> buffer;
+        uint64_t offset;
+    };
+
+    struct SetValidatedBufferLocationsInternalCmd {
+        std::vector<DeferredBufferLocationUpdate> updates;
+    };
+
     struct SetViewportCmd {
         float x, y, width, height, minDepth, maxDepth;
     };
diff --git a/src/dawn_native/ComputePassEncoder.cpp b/src/dawn_native/ComputePassEncoder.cpp
index dcc5df8..834fa6c 100644
--- a/src/dawn_native/ComputePassEncoder.cpp
+++ b/src/dawn_native/ComputePassEncoder.cpp
@@ -67,7 +67,7 @@
 
                 return {};
             })) {
-            mEncodingContext->ExitPass(this, mUsageTracker.AcquireResourceUsage());
+            mEncodingContext->ExitComputePass(this, mUsageTracker.AcquireResourceUsage());
         }
     }
 
diff --git a/src/dawn_native/Device.cpp b/src/dawn_native/Device.cpp
index c209cd1..44287e0 100644
--- a/src/dawn_native/Device.cpp
+++ b/src/dawn_native/Device.cpp
@@ -225,7 +225,7 @@
         mDynamicUploader = std::make_unique<DynamicUploader>(this);
         mCallbackTaskManager = std::make_unique<CallbackTaskManager>();
         mDeprecationWarnings = std::make_unique<DeprecationWarnings>();
-        mInternalPipelineStore = std::make_unique<InternalPipelineStore>();
+        mInternalPipelineStore = std::make_unique<InternalPipelineStore>(this);
         mPersistentCache = std::make_unique<PersistentCache>(this);
 
         ASSERT(GetPlatform() != nullptr);
diff --git a/src/dawn_native/EncodingContext.cpp b/src/dawn_native/EncodingContext.cpp
index 9e8812d..9e7b960 100644
--- a/src/dawn_native/EncodingContext.cpp
+++ b/src/dawn_native/EncodingContext.cpp
@@ -19,6 +19,7 @@
 #include "dawn_native/Commands.h"
 #include "dawn_native/Device.h"
 #include "dawn_native/ErrorData.h"
+#include "dawn_native/IndirectDrawValidationEncoder.h"
 #include "dawn_native/RenderBundleEncoder.h"
 
 namespace dawn_native {
@@ -47,8 +48,9 @@
     }
 
     void EncodingContext::MoveToIterator() {
+        CommitCommands(std::move(mPendingCommands));
         if (!mWasMovedToIterator) {
-            mIterator = CommandIterator(std::move(mAllocator));
+            mIterator.AcquireCommandBlocks(std::move(mAllocators));
             mWasMovedToIterator = true;
         }
     }
@@ -67,6 +69,18 @@
         }
     }
 
+    void EncodingContext::WillBeginRenderPass() {
+        ASSERT(mCurrentEncoder == mTopLevelEncoder);
+        if (mDevice->IsValidationEnabled()) {
+            // When validation is enabled, we are going to want to capture all commands encoded
+            // between and including BeginRenderPassCmd and EndRenderPassCmd, and defer their
+            // sequencing util after we have a chance to insert any necessary validation
+            // commands. To support this we commit any current commands now, so that the
+            // impending BeginRenderPassCmd starts in a fresh CommandAllocator.
+            CommitCommands(std::move(mPendingCommands));
+        }
+    }
+
     void EncodingContext::EnterPass(const ObjectBase* passEncoder) {
         // Assert we're at the top level.
         ASSERT(mCurrentEncoder == mTopLevelEncoder);
@@ -75,15 +89,34 @@
         mCurrentEncoder = passEncoder;
     }
 
-    void EncodingContext::ExitPass(const ObjectBase* passEncoder, RenderPassResourceUsage usages) {
+    MaybeError EncodingContext::ExitRenderPass(const ObjectBase* passEncoder,
+                                               RenderPassResourceUsageTracker usageTracker,
+                                               CommandEncoder* commandEncoder,
+                                               IndirectDrawMetadata indirectDrawMetadata) {
         ASSERT(mCurrentEncoder != mTopLevelEncoder);
         ASSERT(mCurrentEncoder == passEncoder);
 
         mCurrentEncoder = mTopLevelEncoder;
-        mRenderPassUsages.push_back(std::move(usages));
+
+        if (mDevice->IsValidationEnabled()) {
+            // With validation enabled, commands were committed just before BeginRenderPassCmd was
+            // encoded by our RenderPassEncoder (see WillBeginRenderPass above). This means
+            // 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.
+            CommandAllocator renderCommands = std::move(mPendingCommands);
+            DAWN_TRY(EncodeIndirectDrawValidationCommands(mDevice, commandEncoder, &usageTracker,
+                                                          &indirectDrawMetadata));
+            CommitCommands(std::move(mPendingCommands));
+            CommitCommands(std::move(renderCommands));
+        }
+
+        mRenderPassUsages.push_back(usageTracker.AcquireResourceUsage());
+        return {};
     }
 
-    void EncodingContext::ExitPass(const ObjectBase* passEncoder, ComputePassResourceUsage usages) {
+    void EncodingContext::ExitComputePass(const ObjectBase* passEncoder,
+                                          ComputePassResourceUsage usages) {
         ASSERT(mCurrentEncoder != mTopLevelEncoder);
         ASSERT(mCurrentEncoder == passEncoder);
 
@@ -126,6 +159,7 @@
         // if Finish() has been called.
         mCurrentEncoder = nullptr;
         mTopLevelEncoder = nullptr;
+        CommitCommands(std::move(mPendingCommands));
 
         if (mError != nullptr) {
             return std::move(mError);
@@ -136,6 +170,12 @@
         return {};
     }
 
+    void EncodingContext::CommitCommands(CommandAllocator allocator) {
+        if (!allocator.IsEmpty()) {
+            mAllocators.push_back(std::move(allocator));
+        }
+    }
+
     bool EncodingContext::IsFinished() const {
         return mTopLevelEncoder == nullptr;
     }
diff --git a/src/dawn_native/EncodingContext.h b/src/dawn_native/EncodingContext.h
index b97e317..522d29e 100644
--- a/src/dawn_native/EncodingContext.h
+++ b/src/dawn_native/EncodingContext.h
@@ -18,6 +18,7 @@
 #include "dawn_native/CommandAllocator.h"
 #include "dawn_native/Error.h"
 #include "dawn_native/ErrorData.h"
+#include "dawn_native/IndirectDrawMetadata.h"
 #include "dawn_native/PassResourceUsageTracker.h"
 #include "dawn_native/dawn_platform.h"
 
@@ -25,6 +26,7 @@
 
 namespace dawn_native {
 
+    class CommandEncoder;
     class DeviceBase;
     class ObjectBase;
 
@@ -69,13 +71,21 @@
                 return false;
             }
             ASSERT(!mWasMovedToIterator);
-            return !ConsumedError(encodeFunction(&mAllocator));
+            return !ConsumedError(encodeFunction(&mPendingCommands));
         }
 
+        // Must be called prior to encoding a BeginRenderPassCmd. Note that it's OK to call this
+        // and then not actually call EnterPass+ExitRenderPass, for example if some other pass setup
+        // failed validation before the BeginRenderPassCmd could be encoded.
+        void WillBeginRenderPass();
+
         // Functions to set current encoder state
         void EnterPass(const ObjectBase* passEncoder);
-        void ExitPass(const ObjectBase* passEncoder, RenderPassResourceUsage usages);
-        void ExitPass(const ObjectBase* passEncoder, ComputePassResourceUsage usages);
+        MaybeError ExitRenderPass(const ObjectBase* passEncoder,
+                                  RenderPassResourceUsageTracker usageTracker,
+                                  CommandEncoder* commandEncoder,
+                                  IndirectDrawMetadata indirectDrawMetadata);
+        void ExitComputePass(const ObjectBase* passEncoder, ComputePassResourceUsage usages);
         MaybeError Finish();
 
         const RenderPassUsages& GetRenderPassUsages() const;
@@ -84,6 +94,8 @@
         ComputePassUsages AcquireComputePassUsages();
 
       private:
+        void CommitCommands(CommandAllocator allocator);
+
         bool IsFinished() const;
         void MoveToIterator();
 
@@ -104,7 +116,9 @@
         ComputePassUsages mComputePassUsages;
         bool mWereComputePassUsagesAcquired = false;
 
-        CommandAllocator mAllocator;
+        CommandAllocator mPendingCommands;
+
+        std::vector<CommandAllocator> mAllocators;
         CommandIterator mIterator;
         bool mWasMovedToIterator = false;
         bool mWereCommandsAcquired = false;
diff --git a/src/dawn_native/IndirectDrawMetadata.cpp b/src/dawn_native/IndirectDrawMetadata.cpp
new file mode 100644
index 0000000..235935f
--- /dev/null
+++ b/src/dawn_native/IndirectDrawMetadata.cpp
@@ -0,0 +1,193 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/IndirectDrawMetadata.h"
+
+#include "common/Constants.h"
+#include "common/RefCounted.h"
+#include "dawn_native/IndirectDrawValidationEncoder.h"
+#include "dawn_native/RenderBundle.h"
+
+#include <algorithm>
+#include <utility>
+
+namespace dawn_native {
+
+    namespace {
+
+        // In the unlikely scenario that indirect offsets used over a single buffer span more than
+        // this length of the buffer, we split the validation work into multiple batches.
+        constexpr uint64_t kMaxBatchOffsetRange = kMaxStorageBufferBindingSize -
+                                                  kMinStorageBufferOffsetAlignment -
+                                                  kDrawIndexedIndirectSize;
+
+    }  // namespace
+
+    IndirectDrawMetadata::IndexedIndirectBufferValidationInfo::IndexedIndirectBufferValidationInfo(
+        BufferBase* indirectBuffer)
+        : mIndirectBuffer(indirectBuffer) {
+    }
+
+    void IndirectDrawMetadata::IndexedIndirectBufferValidationInfo::AddIndexedIndirectDraw(
+        IndexedIndirectDraw draw) {
+        const uint64_t newOffset = draw.clientBufferOffset;
+        auto it = mBatches.begin();
+        while (it != mBatches.end()) {
+            IndexedIndirectValidationBatch& batch = *it;
+            if (batch.draws.size() >= kMaxDrawCallsPerIndirectValidationBatch) {
+                // This batch is full. If its minOffset is to the right of the new offset, we can
+                // just insert a new batch here.
+                if (newOffset < batch.minOffset) {
+                    break;
+                }
+
+                // Otherwise keep looking.
+                ++it;
+                continue;
+            }
+
+            if (newOffset >= batch.minOffset && newOffset <= batch.maxOffset) {
+                batch.draws.push_back(std::move(draw));
+                return;
+            }
+
+            if (newOffset < batch.minOffset &&
+                batch.maxOffset - newOffset <= kMaxBatchOffsetRange) {
+                // We can extend this batch to the left in order to fit the new offset.
+                batch.minOffset = newOffset;
+                batch.draws.push_back(std::move(draw));
+                return;
+            }
+
+            if (newOffset > batch.maxOffset &&
+                newOffset - batch.minOffset <= kMaxBatchOffsetRange) {
+                // We can extend this batch to the right in order to fit the new offset.
+                batch.maxOffset = newOffset;
+                batch.draws.push_back(std::move(draw));
+                return;
+            }
+
+            if (newOffset < batch.minOffset) {
+                // We want to insert a new batch just before this one.
+                break;
+            }
+
+            ++it;
+        }
+
+        IndexedIndirectValidationBatch newBatch;
+        newBatch.minOffset = newOffset;
+        newBatch.maxOffset = newOffset;
+        newBatch.draws.push_back(std::move(draw));
+
+        mBatches.insert(it, std::move(newBatch));
+    }
+
+    void IndirectDrawMetadata::IndexedIndirectBufferValidationInfo::AddBatch(
+        const IndexedIndirectValidationBatch& newBatch) {
+        auto it = mBatches.begin();
+        while (it != mBatches.end()) {
+            IndexedIndirectValidationBatch& batch = *it;
+            uint64_t min = std::min(newBatch.minOffset, batch.minOffset);
+            uint64_t max = std::max(newBatch.maxOffset, batch.maxOffset);
+            if (max - min <= kMaxBatchOffsetRange && batch.draws.size() + newBatch.draws.size() <=
+                                                         kMaxDrawCallsPerIndirectValidationBatch) {
+                // This batch fits within the limits of an existing batch. Merge it.
+                batch.minOffset = min;
+                batch.maxOffset = max;
+                batch.draws.insert(batch.draws.end(), newBatch.draws.begin(), newBatch.draws.end());
+                return;
+            }
+
+            if (newBatch.minOffset < batch.minOffset) {
+                break;
+            }
+
+            ++it;
+        }
+        mBatches.push_back(newBatch);
+    }
+
+    const std::vector<IndirectDrawMetadata::IndexedIndirectValidationBatch>&
+    IndirectDrawMetadata::IndexedIndirectBufferValidationInfo::GetBatches() const {
+        return mBatches;
+    }
+
+    IndirectDrawMetadata::IndirectDrawMetadata() = default;
+
+    IndirectDrawMetadata::~IndirectDrawMetadata() = default;
+
+    IndirectDrawMetadata::IndirectDrawMetadata(IndirectDrawMetadata&&) = default;
+
+    IndirectDrawMetadata& IndirectDrawMetadata::operator=(IndirectDrawMetadata&&) = default;
+
+    IndirectDrawMetadata::IndexedIndirectBufferValidationInfoMap*
+    IndirectDrawMetadata::GetIndexedIndirectBufferValidationInfo() {
+        return &mIndexedIndirectBufferValidationInfo;
+    }
+
+    void IndirectDrawMetadata::AddBundle(RenderBundleBase* bundle) {
+        auto result = mAddedBundles.insert(bundle);
+        if (!result.second) {
+            return;
+        }
+
+        for (const auto& entry :
+             bundle->GetIndirectDrawMetadata().mIndexedIndirectBufferValidationInfo) {
+            const IndexedIndirectConfig& config = entry.first;
+            auto it = mIndexedIndirectBufferValidationInfo.lower_bound(config);
+            if (it != mIndexedIndirectBufferValidationInfo.end() && it->first == config) {
+                // We already have batches for the same config. Merge the new ones in.
+                for (const IndexedIndirectValidationBatch& batch : entry.second.GetBatches()) {
+                    it->second.AddBatch(batch);
+                }
+            } else {
+                mIndexedIndirectBufferValidationInfo.emplace_hint(it, config, entry.second);
+            }
+        }
+    }
+
+    void IndirectDrawMetadata::AddIndexedIndirectDraw(
+        wgpu::IndexFormat indexFormat,
+        uint64_t indexBufferSize,
+        BufferBase* indirectBuffer,
+        uint64_t indirectOffset,
+        BufferLocation* drawCmdIndirectBufferLocation) {
+        uint64_t numIndexBufferElements;
+        switch (indexFormat) {
+            case wgpu::IndexFormat::Uint16:
+                numIndexBufferElements = indexBufferSize / 2;
+                break;
+            case wgpu::IndexFormat::Uint32:
+                numIndexBufferElements = indexBufferSize / 4;
+                break;
+            case wgpu::IndexFormat::Undefined:
+                UNREACHABLE();
+        }
+
+        const IndexedIndirectConfig config(indirectBuffer, numIndexBufferElements);
+        auto it = mIndexedIndirectBufferValidationInfo.find(config);
+        if (it == mIndexedIndirectBufferValidationInfo.end()) {
+            auto result = mIndexedIndirectBufferValidationInfo.emplace(
+                config, IndexedIndirectBufferValidationInfo(indirectBuffer));
+            it = result.first;
+        }
+
+        IndexedIndirectDraw draw;
+        draw.clientBufferOffset = indirectOffset;
+        draw.bufferLocation = drawCmdIndirectBufferLocation;
+        it->second.AddIndexedIndirectDraw(std::move(draw));
+    }
+
+}  // namespace dawn_native
diff --git a/src/dawn_native/IndirectDrawMetadata.h b/src/dawn_native/IndirectDrawMetadata.h
new file mode 100644
index 0000000..04c38e3
--- /dev/null
+++ b/src/dawn_native/IndirectDrawMetadata.h
@@ -0,0 +1,112 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWNNATIVE_INDIRECTDRAWMETADATA_H_
+#define DAWNNATIVE_INDIRECTDRAWMETADATA_H_
+
+#include "common/NonCopyable.h"
+#include "common/RefCounted.h"
+#include "dawn_native/Buffer.h"
+#include "dawn_native/BufferLocation.h"
+#include "dawn_native/CommandBufferStateTracker.h"
+#include "dawn_native/Commands.h"
+
+#include <cstdint>
+#include <map>
+#include <set>
+#include <utility>
+#include <vector>
+
+namespace dawn_native {
+
+    class RenderBundleBase;
+
+    // Metadata corresponding to the validation requirements of a single render pass. This metadata
+    // is accumulated while its corresponding render pass is encoded, and is later used to encode
+    // validation commands to be inserted into the command buffer just before the render pass's own
+    // commands.
+    class IndirectDrawMetadata : public NonCopyable {
+      public:
+        struct IndexedIndirectDraw {
+            uint64_t clientBufferOffset;
+            Ref<BufferLocation> bufferLocation;
+        };
+
+        struct IndexedIndirectValidationBatch {
+            uint64_t minOffset;
+            uint64_t maxOffset;
+            std::vector<IndexedIndirectDraw> draws;
+        };
+
+        // Tracks information about every draw call in this render pass which uses the same indirect
+        // buffer and the same-sized index buffer. Calls are grouped by indirect offset ranges so
+        // that validation work can be chunked efficiently if necessary.
+        class IndexedIndirectBufferValidationInfo {
+          public:
+            explicit IndexedIndirectBufferValidationInfo(BufferBase* indirectBuffer);
+
+            // Logs a new drawIndexedIndirect call for the render pass. `cmd` is updated with an
+            // assigned (and deferred) buffer ref and relative offset before returning.
+            void AddIndexedIndirectDraw(IndexedIndirectDraw draw);
+
+            // Adds draw calls from an already-computed batch, e.g. from a previously encoded
+            // RenderBundle. The added batch is merged into an existing batch if possible, otherwise
+            // it's added to mBatch.
+            void AddBatch(const IndexedIndirectValidationBatch& batch);
+
+            const std::vector<IndexedIndirectValidationBatch>& GetBatches() const;
+
+          private:
+            Ref<BufferBase> mIndirectBuffer;
+
+            // A list of information about validation batches that will need to be executed for the
+            // corresponding indirect buffer prior to a single render pass. These are kept sorted by
+            // minOffset and may overlap iff the number of offsets in one batch would otherwise
+            // exceed some large upper bound (roughly ~33M draw calls).
+            //
+            // Since the most common expected cases will overwhelmingly require only a single
+            // validation pass per render pass, this is optimized for efficient updates to a single
+            // batch rather than for efficient manipulation of a large number of batches.
+            std::vector<IndexedIndirectValidationBatch> mBatches;
+        };
+
+        // Combination of an indirect buffer reference, and the number of addressable index buffer
+        // elements at the time of a draw call.
+        using IndexedIndirectConfig = std::pair<BufferBase*, uint64_t>;
+        using IndexedIndirectBufferValidationInfoMap =
+            std::map<IndexedIndirectConfig, IndexedIndirectBufferValidationInfo>;
+
+        IndirectDrawMetadata();
+        ~IndirectDrawMetadata();
+
+        IndirectDrawMetadata(IndirectDrawMetadata&&);
+        IndirectDrawMetadata& operator=(IndirectDrawMetadata&&);
+
+        IndexedIndirectBufferValidationInfoMap* GetIndexedIndirectBufferValidationInfo();
+
+        void AddBundle(RenderBundleBase* bundle);
+        void AddIndexedIndirectDraw(wgpu::IndexFormat indexFormat,
+                                    uint64_t indexBufferSize,
+                                    BufferBase* indirectBuffer,
+                                    uint64_t indirectOffset,
+                                    BufferLocation* drawCmdIndirectBufferLocation);
+
+      private:
+        IndexedIndirectBufferValidationInfoMap mIndexedIndirectBufferValidationInfo;
+        std::set<RenderBundleBase*> mAddedBundles;
+    };
+
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_INDIRECTDRAWMETADATA_H_
diff --git a/src/dawn_native/IndirectDrawValidationEncoder.cpp b/src/dawn_native/IndirectDrawValidationEncoder.cpp
new file mode 100644
index 0000000..c3c2a04
--- /dev/null
+++ b/src/dawn_native/IndirectDrawValidationEncoder.cpp
@@ -0,0 +1,397 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/IndirectDrawValidationEncoder.h"
+
+#include "common/Constants.h"
+#include "common/Math.h"
+#include "dawn_native/BindGroup.h"
+#include "dawn_native/BindGroupLayout.h"
+#include "dawn_native/CommandEncoder.h"
+#include "dawn_native/ComputePassEncoder.h"
+#include "dawn_native/ComputePipeline.h"
+#include "dawn_native/Device.h"
+#include "dawn_native/InternalPipelineStore.h"
+#include "dawn_native/Queue.h"
+
+#include <cstdlib>
+#include <limits>
+
+namespace dawn_native {
+
+    namespace {
+        // NOTE: This must match the workgroup_size attribute on the compute entry point below.
+        constexpr uint64_t kWorkgroupSize = 64;
+
+        // Equivalent to the BatchInfo struct defined in the shader below.
+        struct BatchInfo {
+            uint64_t numIndexBufferElements;
+            uint32_t numDraws;
+            uint32_t padding;
+        };
+
+        // TODO(https://crbug.com/dawn/1108): Propagate validation feedback from this shader in
+        // various failure modes.
+        static const char sRenderValidationShaderSource[] = R"(
+            let kNumIndirectParamsPerDrawCall = 5u;
+
+            let kIndexCountEntry = 0u;
+            let kInstanceCountEntry = 1u;
+            let kFirstIndexEntry = 2u;
+            let kBaseVertexEntry = 3u;
+            let kFirstInstanceEntry = 4u;
+
+            [[block]] struct BatchInfo {
+                numIndexBufferElementsLow: u32;
+                numIndexBufferElementsHigh: u32;
+                numDraws: u32;
+                padding: u32;
+                indirectOffsets: array<u32>;
+            };
+
+            [[block]] struct IndirectParams {
+                data: array<u32>;
+            };
+
+            [[group(0), binding(0)]] var<storage, read> batch: BatchInfo;
+            [[group(0), binding(1)]] var<storage, read_write> clientParams: IndirectParams;
+            [[group(0), binding(2)]] var<storage, write> validatedParams: IndirectParams;
+
+            fn fail(drawIndex: u32) {
+                let index = drawIndex * kNumIndirectParamsPerDrawCall;
+                validatedParams.data[index + kIndexCountEntry] = 0u;
+                validatedParams.data[index + kInstanceCountEntry] = 0u;
+                validatedParams.data[index + kFirstIndexEntry] = 0u;
+                validatedParams.data[index + kBaseVertexEntry] = 0u;
+                validatedParams.data[index + kFirstInstanceEntry] = 0u;
+            }
+
+            fn pass(drawIndex: u32) {
+                let vIndex = drawIndex * kNumIndirectParamsPerDrawCall;
+                let cIndex = batch.indirectOffsets[drawIndex];
+                validatedParams.data[vIndex + kIndexCountEntry] =
+                    clientParams.data[cIndex + kIndexCountEntry];
+                validatedParams.data[vIndex + kInstanceCountEntry] =
+                    clientParams.data[cIndex + kInstanceCountEntry];
+                validatedParams.data[vIndex + kFirstIndexEntry] =
+                    clientParams.data[cIndex + kFirstIndexEntry];
+                validatedParams.data[vIndex + kBaseVertexEntry] =
+                    clientParams.data[cIndex + kBaseVertexEntry];
+                validatedParams.data[vIndex + kFirstInstanceEntry] =
+                    clientParams.data[cIndex + kFirstInstanceEntry];
+            }
+
+            [[stage(compute), workgroup_size(64, 1, 1)]]
+            fn main([[builtin(global_invocation_id)]] id : vec3<u32>) {
+                if (id.x >= batch.numDraws) {
+                    return;
+                }
+
+                let clientIndex = batch.indirectOffsets[id.x];
+                let firstInstance = clientParams.data[clientIndex + kFirstInstanceEntry];
+                if (firstInstance != 0u) {
+                    fail(id.x);
+                    return;
+                }
+
+                if (batch.numIndexBufferElementsHigh >= 2u) {
+                    // firstIndex and indexCount are both u32. The maximum possible sum of these
+                    // values is 0x1fffffffe, which is less than 0x200000000. Nothing to validate.
+                    pass(id.x);
+                    return;
+                }
+
+                let firstIndex = clientParams.data[clientIndex + kFirstIndexEntry];
+                if (batch.numIndexBufferElementsHigh == 0u &&
+                    batch.numIndexBufferElementsLow < firstIndex) {
+                    fail(id.x);
+                    return;
+                }
+
+                // Note that this subtraction may underflow, but only when
+                // numIndexBufferElementsHigh is 1u. The result is still correct in that case.
+                let maxIndexCount = batch.numIndexBufferElementsLow - firstIndex;
+                let indexCount = clientParams.data[clientIndex + kIndexCountEntry];
+                if (indexCount > maxIndexCount) {
+                    fail(id.x);
+                    return;
+                }
+                pass(id.x);
+            }
+        )";
+
+        ResultOrError<ComputePipelineBase*> GetOrCreateRenderValidationPipeline(
+            DeviceBase* device) {
+            InternalPipelineStore* store = device->GetInternalPipelineStore();
+
+            if (store->renderValidationPipeline == nullptr) {
+                // Create compute shader module if not cached before.
+                if (store->renderValidationShader == nullptr) {
+                    ShaderModuleDescriptor descriptor;
+                    ShaderModuleWGSLDescriptor wgslDesc;
+                    wgslDesc.source = sRenderValidationShaderSource;
+                    descriptor.nextInChain = reinterpret_cast<ChainedStruct*>(&wgslDesc);
+                    DAWN_TRY_ASSIGN(store->renderValidationShader,
+                                    device->CreateShaderModule(&descriptor));
+                }
+
+                BindGroupLayoutEntry entries[3];
+                entries[0].binding = 0;
+                entries[0].visibility = wgpu::ShaderStage::Compute;
+                entries[0].buffer.type = wgpu::BufferBindingType::ReadOnlyStorage;
+                entries[1].binding = 1;
+                entries[1].visibility = wgpu::ShaderStage::Compute;
+                entries[1].buffer.type = kInternalStorageBufferBinding;
+                entries[2].binding = 2;
+                entries[2].visibility = wgpu::ShaderStage::Compute;
+                entries[2].buffer.type = wgpu::BufferBindingType::Storage;
+
+                BindGroupLayoutDescriptor bindGroupLayoutDescriptor;
+                bindGroupLayoutDescriptor.entryCount = 3;
+                bindGroupLayoutDescriptor.entries = entries;
+                Ref<BindGroupLayoutBase> bindGroupLayout;
+                DAWN_TRY_ASSIGN(bindGroupLayout,
+                                device->CreateBindGroupLayout(&bindGroupLayoutDescriptor, true));
+
+                PipelineLayoutDescriptor pipelineDescriptor;
+                pipelineDescriptor.bindGroupLayoutCount = 1;
+                pipelineDescriptor.bindGroupLayouts = &bindGroupLayout.Get();
+                Ref<PipelineLayoutBase> pipelineLayout;
+                DAWN_TRY_ASSIGN(pipelineLayout, device->CreatePipelineLayout(&pipelineDescriptor));
+
+                ComputePipelineDescriptor computePipelineDescriptor = {};
+                computePipelineDescriptor.layout = pipelineLayout.Get();
+                computePipelineDescriptor.compute.module = store->renderValidationShader.Get();
+                computePipelineDescriptor.compute.entryPoint = "main";
+
+                DAWN_TRY_ASSIGN(store->renderValidationPipeline,
+                                device->CreateComputePipeline(&computePipelineDescriptor));
+            }
+
+            return store->renderValidationPipeline.Get();
+        }
+
+        size_t GetBatchDataSize(uint32_t numDraws) {
+            return sizeof(BatchInfo) + numDraws * sizeof(uint32_t);
+        }
+
+    }  // namespace
+
+    const uint32_t kBatchDrawCallLimitByDispatchSize =
+        kMaxComputePerDimensionDispatchSize * kWorkgroupSize;
+    const uint32_t kBatchDrawCallLimitByStorageBindingSize =
+        (kMaxStorageBufferBindingSize - sizeof(BatchInfo)) / sizeof(uint32_t);
+    const uint32_t kMaxDrawCallsPerIndirectValidationBatch =
+        std::min(kBatchDrawCallLimitByDispatchSize, kBatchDrawCallLimitByStorageBindingSize);
+
+    MaybeError EncodeIndirectDrawValidationCommands(DeviceBase* device,
+                                                    CommandEncoder* commandEncoder,
+                                                    RenderPassResourceUsageTracker* usageTracker,
+                                                    IndirectDrawMetadata* indirectDrawMetadata) {
+        struct Batch {
+            const IndirectDrawMetadata::IndexedIndirectValidationBatch* metadata;
+            uint64_t numIndexBufferElements;
+            uint64_t dataBufferOffset;
+            uint64_t dataSize;
+            uint64_t clientIndirectOffset;
+            uint64_t clientIndirectSize;
+            uint64_t validatedParamsOffset;
+            uint64_t validatedParamsSize;
+            BatchInfo* batchInfo;
+        };
+
+        struct Pass {
+            BufferBase* clientIndirectBuffer;
+            uint64_t validatedParamsSize = 0;
+            uint64_t batchDataSize = 0;
+            std::unique_ptr<void, void (*)(void*)> batchData{nullptr, std::free};
+            std::vector<Batch> batches;
+        };
+
+        // First stage is grouping all batches into passes. We try to pack as many batches into a
+        // single pass as possible. Batches can be grouped together as long as they're validating
+        // data from the same indirect buffer, but they may still be split into multiple passes if
+        // the number of draw calls in a pass would exceed some (very high) upper bound.
+        uint64_t numTotalDrawCalls = 0;
+        size_t validatedParamsSize = 0;
+        std::vector<Pass> passes;
+        IndirectDrawMetadata::IndexedIndirectBufferValidationInfoMap& bufferInfoMap =
+            *indirectDrawMetadata->GetIndexedIndirectBufferValidationInfo();
+        if (bufferInfoMap.empty()) {
+            return {};
+        }
+
+        for (auto& entry : bufferInfoMap) {
+            const IndirectDrawMetadata::IndexedIndirectConfig& config = entry.first;
+            BufferBase* clientIndirectBuffer = config.first;
+            for (const IndirectDrawMetadata::IndexedIndirectValidationBatch& batch :
+                 entry.second.GetBatches()) {
+                const uint64_t minOffsetFromAlignedBoundary =
+                    batch.minOffset % kMinStorageBufferOffsetAlignment;
+                const uint64_t minOffsetAlignedDown =
+                    batch.minOffset - minOffsetFromAlignedBoundary;
+
+                Batch newBatch;
+                newBatch.metadata = &batch;
+                newBatch.numIndexBufferElements = config.second;
+                newBatch.dataSize = GetBatchDataSize(batch.draws.size());
+                newBatch.clientIndirectOffset = minOffsetAlignedDown;
+                newBatch.clientIndirectSize =
+                    batch.maxOffset + kDrawIndexedIndirectSize - minOffsetAlignedDown;
+                numTotalDrawCalls += batch.draws.size();
+
+                newBatch.validatedParamsSize = batch.draws.size() * kDrawIndexedIndirectSize;
+                newBatch.validatedParamsOffset =
+                    Align(validatedParamsSize, kMinStorageBufferOffsetAlignment);
+                validatedParamsSize = newBatch.validatedParamsOffset + newBatch.validatedParamsSize;
+                if (validatedParamsSize > kMaxStorageBufferBindingSize) {
+                    return DAWN_INTERNAL_ERROR("Too many drawIndexedIndirect calls to validate");
+                }
+
+                Pass* currentPass = passes.empty() ? nullptr : &passes.back();
+                if (currentPass && currentPass->clientIndirectBuffer == clientIndirectBuffer) {
+                    uint64_t nextBatchDataOffset =
+                        Align(currentPass->batchDataSize, kMinStorageBufferOffsetAlignment);
+                    uint64_t newPassBatchDataSize = nextBatchDataOffset + newBatch.dataSize;
+                    if (newPassBatchDataSize <= kMaxStorageBufferBindingSize) {
+                        // We can fit this batch in the current pass.
+                        newBatch.dataBufferOffset = nextBatchDataOffset;
+                        currentPass->batchDataSize = newPassBatchDataSize;
+                        currentPass->batches.push_back(newBatch);
+                        continue;
+                    }
+                }
+
+                // We need to start a new pass for this batch.
+                newBatch.dataBufferOffset = 0;
+
+                Pass newPass;
+                newPass.clientIndirectBuffer = clientIndirectBuffer;
+                newPass.batchDataSize = newBatch.dataSize;
+                newPass.batches.push_back(newBatch);
+                passes.push_back(std::move(newPass));
+            }
+        }
+
+        auto* const store = device->GetInternalPipelineStore();
+        ScratchBuffer& validatedParamsBuffer = store->scratchIndirectStorage;
+        ScratchBuffer& batchDataBuffer = store->scratchStorage;
+
+        uint64_t requiredBatchDataBufferSize = 0;
+        for (const Pass& pass : passes) {
+            requiredBatchDataBufferSize = std::max(requiredBatchDataBufferSize, pass.batchDataSize);
+        }
+        DAWN_TRY(batchDataBuffer.EnsureCapacity(requiredBatchDataBufferSize));
+        usageTracker->BufferUsedAs(batchDataBuffer.GetBuffer(), wgpu::BufferUsage::Storage);
+
+        DAWN_TRY(validatedParamsBuffer.EnsureCapacity(validatedParamsSize));
+        usageTracker->BufferUsedAs(validatedParamsBuffer.GetBuffer(), wgpu::BufferUsage::Indirect);
+
+        // Now we allocate and populate host-side batch data to be copied to the GPU, and prepare to
+        // update all DrawIndexedIndirectCmd buffer references.
+        std::vector<DeferredBufferLocationUpdate> deferredBufferLocationUpdates;
+        deferredBufferLocationUpdates.reserve(numTotalDrawCalls);
+        for (Pass& pass : passes) {
+            // We use std::malloc here because it guarantees maximal scalar alignment.
+            pass.batchData = {std::malloc(pass.batchDataSize), std::free};
+            memset(pass.batchData.get(), 0, pass.batchDataSize);
+            uint8_t* batchData = static_cast<uint8_t*>(pass.batchData.get());
+            for (Batch& batch : pass.batches) {
+                batch.batchInfo = new (&batchData[batch.dataBufferOffset]) BatchInfo();
+                batch.batchInfo->numIndexBufferElements = batch.numIndexBufferElements;
+                batch.batchInfo->numDraws = static_cast<uint32_t>(batch.metadata->draws.size());
+
+                uint32_t* indirectOffsets = reinterpret_cast<uint32_t*>(batch.batchInfo + 1);
+                uint64_t validatedParamsOffset = batch.validatedParamsOffset;
+                for (const auto& draw : batch.metadata->draws) {
+                    // The shader uses this to index an array of u32, hence the division by 4 bytes.
+                    *indirectOffsets++ = static_cast<uint32_t>(
+                        (draw.clientBufferOffset - batch.clientIndirectOffset) / 4);
+
+                    DeferredBufferLocationUpdate deferredUpdate;
+                    deferredUpdate.location = draw.bufferLocation;
+                    deferredUpdate.buffer = validatedParamsBuffer.GetBuffer();
+                    deferredUpdate.offset = validatedParamsOffset;
+                    deferredBufferLocationUpdates.push_back(std::move(deferredUpdate));
+
+                    validatedParamsOffset += kDrawIndexedIndirectSize;
+                }
+            }
+        }
+
+        ComputePipelineBase* pipeline;
+        DAWN_TRY_ASSIGN(pipeline, GetOrCreateRenderValidationPipeline(device));
+
+        Ref<BindGroupLayoutBase> layout;
+        DAWN_TRY_ASSIGN(layout, pipeline->GetBindGroupLayout(0));
+
+        BindGroupEntry bindings[3];
+        BindGroupEntry& bufferDataBinding = bindings[0];
+        bufferDataBinding.binding = 0;
+        bufferDataBinding.buffer = batchDataBuffer.GetBuffer();
+
+        BindGroupEntry& clientIndirectBinding = bindings[1];
+        clientIndirectBinding.binding = 1;
+
+        BindGroupEntry& validatedParamsBinding = bindings[2];
+        validatedParamsBinding.binding = 2;
+        validatedParamsBinding.buffer = validatedParamsBuffer.GetBuffer();
+
+        BindGroupDescriptor bindGroupDescriptor = {};
+        bindGroupDescriptor.layout = layout.Get();
+        bindGroupDescriptor.entryCount = 3;
+        bindGroupDescriptor.entries = bindings;
+
+        // Finally, we can now encode our validation passes. Each pass first does a single
+        // WriteBuffer to get batch data over to the GPU, followed by a single compute pass. The
+        // compute pass encodes a separate SetBindGroup and Dispatch command for each batch.
+        commandEncoder->EncodeSetValidatedBufferLocationsInternal(
+            std::move(deferredBufferLocationUpdates));
+        for (const Pass& pass : passes) {
+            commandEncoder->APIWriteBuffer(batchDataBuffer.GetBuffer(), 0,
+                                           static_cast<const uint8_t*>(pass.batchData.get()),
+                                           pass.batchDataSize);
+
+            // TODO(dawn:723): change to not use AcquireRef for reentrant object creation.
+            ComputePassDescriptor descriptor = {};
+            Ref<ComputePassEncoder> passEncoder =
+                AcquireRef(commandEncoder->APIBeginComputePass(&descriptor));
+            passEncoder->APISetPipeline(pipeline);
+
+            clientIndirectBinding.buffer = pass.clientIndirectBuffer;
+
+            for (const Batch& batch : pass.batches) {
+                bufferDataBinding.offset = batch.dataBufferOffset;
+                bufferDataBinding.size = batch.dataSize;
+                clientIndirectBinding.offset = batch.clientIndirectOffset;
+                clientIndirectBinding.size = batch.clientIndirectSize;
+                validatedParamsBinding.offset = batch.validatedParamsOffset;
+                validatedParamsBinding.size = batch.validatedParamsSize;
+
+                Ref<BindGroupBase> bindGroup;
+                DAWN_TRY_ASSIGN(bindGroup, device->CreateBindGroup(&bindGroupDescriptor));
+
+                const uint32_t numDrawsRoundedUp =
+                    (batch.batchInfo->numDraws + kWorkgroupSize - 1) / kWorkgroupSize;
+                passEncoder->APISetBindGroup(0, bindGroup.Get());
+                passEncoder->APIDispatch(numDrawsRoundedUp);
+            }
+
+            passEncoder->APIEndPass();
+        }
+
+        return {};
+    }
+
+}  // namespace dawn_native
diff --git a/src/dawn_native/IndirectDrawValidationEncoder.h b/src/dawn_native/IndirectDrawValidationEncoder.h
new file mode 100644
index 0000000..bc62bf0
--- /dev/null
+++ b/src/dawn_native/IndirectDrawValidationEncoder.h
@@ -0,0 +1,39 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWNNATIVE_INDIRECTDRAWVALIDATIONENCODER_H_
+#define DAWNNATIVE_INDIRECTDRAWVALIDATIONENCODER_H_
+
+#include "dawn_native/Error.h"
+#include "dawn_native/IndirectDrawMetadata.h"
+
+namespace dawn_native {
+
+    class CommandEncoder;
+    class DeviceBase;
+    class RenderPassResourceUsageTracker;
+
+    // The maximum number of draws call we can fit into a single validation batch. This is
+    // essentially limited by the number of indirect parameter blocks that can fit into the maximum
+    // allowed storage binding size (about 6.7M).
+    extern const uint32_t kMaxDrawCallsPerIndirectValidationBatch;
+
+    MaybeError EncodeIndirectDrawValidationCommands(DeviceBase* device,
+                                                    CommandEncoder* commandEncoder,
+                                                    RenderPassResourceUsageTracker* usageTracker,
+                                                    IndirectDrawMetadata* indirectDrawMetadata);
+
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_INDIRECTDRAWVALIDATIONENCODER_H_
diff --git a/src/dawn_native/InternalPipelineStore.cpp b/src/dawn_native/InternalPipelineStore.cpp
new file mode 100644
index 0000000..edfd115
--- /dev/null
+++ b/src/dawn_native/InternalPipelineStore.cpp
@@ -0,0 +1,38 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/InternalPipelineStore.h"
+
+#include "dawn_native/ComputePipeline.h"
+#include "dawn_native/Device.h"
+#include "dawn_native/RenderPipeline.h"
+#include "dawn_native/ShaderModule.h"
+
+#include <unordered_map>
+
+namespace dawn_native {
+
+    class RenderPipelineBase;
+    class ShaderModuleBase;
+
+    InternalPipelineStore::InternalPipelineStore(DeviceBase* device)
+        : scratchStorage(device, wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Storage),
+          scratchIndirectStorage(device,
+                                 wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Indirect |
+                                     wgpu::BufferUsage::Storage) {
+    }
+
+    InternalPipelineStore::~InternalPipelineStore() = default;
+
+}  // namespace dawn_native
diff --git a/src/dawn_native/InternalPipelineStore.h b/src/dawn_native/InternalPipelineStore.h
index b3a7398..acf3b13 100644
--- a/src/dawn_native/InternalPipelineStore.h
+++ b/src/dawn_native/InternalPipelineStore.h
@@ -16,15 +16,23 @@
 #define DAWNNATIVE_INTERNALPIPELINESTORE_H_
 
 #include "dawn_native/ObjectBase.h"
+#include "dawn_native/ScratchBuffer.h"
 #include "dawn_native/dawn_platform.h"
 
 #include <unordered_map>
 
 namespace dawn_native {
+
+    class DeviceBase;
     class RenderPipelineBase;
     class ShaderModuleBase;
 
+    // Every DeviceBase owns an InternalPipelineStore. This is a general-purpose cache for
+    // long-lived objects scoped to a device and used to support arbitrary pipeline operations.
     struct InternalPipelineStore {
+        explicit InternalPipelineStore(DeviceBase* device);
+        ~InternalPipelineStore();
+
         std::unordered_map<wgpu::TextureFormat, Ref<RenderPipelineBase>>
             copyTextureForBrowserPipelines;
 
@@ -34,7 +42,18 @@
         Ref<ShaderModuleBase> timestampCS;
 
         Ref<ShaderModuleBase> dummyFragmentShader;
+
+        // A scratch buffer suitable for use as a copy destination and storage binding.
+        ScratchBuffer scratchStorage;
+
+        // A scratch buffer suitable for use as a copy destination, storage binding, and indirect
+        // buffer for indirect dispatch or draw calls.
+        ScratchBuffer scratchIndirectStorage;
+
+        Ref<ComputePipelineBase> renderValidationPipeline;
+        Ref<ShaderModuleBase> renderValidationShader;
     };
+
 }  // namespace dawn_native
 
 #endif  // DAWNNATIVE_INTERNALPIPELINESTORE_H_
diff --git a/src/dawn_native/RenderBundle.cpp b/src/dawn_native/RenderBundle.cpp
index 028dde7..b17031b 100644
--- a/src/dawn_native/RenderBundle.cpp
+++ b/src/dawn_native/RenderBundle.cpp
@@ -24,9 +24,11 @@
     RenderBundleBase::RenderBundleBase(RenderBundleEncoder* encoder,
                                        const RenderBundleDescriptor* descriptor,
                                        Ref<AttachmentState> attachmentState,
-                                       RenderPassResourceUsage resourceUsage)
+                                       RenderPassResourceUsage resourceUsage,
+                                       IndirectDrawMetadata indirectDrawMetadata)
         : ObjectBase(encoder->GetDevice(), kLabelNotImplemented),
           mCommands(encoder->AcquireCommands()),
+          mIndirectDrawMetadata(std::move(indirectDrawMetadata)),
           mAttachmentState(std::move(attachmentState)),
           mResourceUsage(std::move(resourceUsage)) {
     }
@@ -58,4 +60,8 @@
         return mResourceUsage;
     }
 
+    const IndirectDrawMetadata& RenderBundleBase::GetIndirectDrawMetadata() {
+        return mIndirectDrawMetadata;
+    }
+
 }  // namespace dawn_native
diff --git a/src/dawn_native/RenderBundle.h b/src/dawn_native/RenderBundle.h
index f971ed6..40db924 100644
--- a/src/dawn_native/RenderBundle.h
+++ b/src/dawn_native/RenderBundle.h
@@ -19,6 +19,7 @@
 #include "dawn_native/AttachmentState.h"
 #include "dawn_native/CommandAllocator.h"
 #include "dawn_native/Error.h"
+#include "dawn_native/IndirectDrawMetadata.h"
 #include "dawn_native/ObjectBase.h"
 #include "dawn_native/PassResourceUsage.h"
 
@@ -36,7 +37,8 @@
         RenderBundleBase(RenderBundleEncoder* encoder,
                          const RenderBundleDescriptor* descriptor,
                          Ref<AttachmentState> attachmentState,
-                         RenderPassResourceUsage resourceUsage);
+                         RenderPassResourceUsage resourceUsage,
+                         IndirectDrawMetadata indirectDrawMetadata);
 
         static RenderBundleBase* MakeError(DeviceBase* device);
 
@@ -44,6 +46,7 @@
 
         const AttachmentState* GetAttachmentState() const;
         const RenderPassResourceUsage& GetResourceUsage() const;
+        const IndirectDrawMetadata& GetIndirectDrawMetadata();
 
       protected:
         ~RenderBundleBase() override;
@@ -52,6 +55,7 @@
         RenderBundleBase(DeviceBase* device, ErrorTag errorTag);
 
         CommandIterator mCommands;
+        IndirectDrawMetadata mIndirectDrawMetadata;
         Ref<AttachmentState> mAttachmentState;
         RenderPassResourceUsage mResourceUsage;
     };
diff --git a/src/dawn_native/RenderBundleEncoder.cpp b/src/dawn_native/RenderBundleEncoder.cpp
index daff3eb..7ddda31 100644
--- a/src/dawn_native/RenderBundleEncoder.cpp
+++ b/src/dawn_native/RenderBundleEncoder.cpp
@@ -130,7 +130,8 @@
             DAWN_TRY(ValidateFinish(usages));
         }
 
-        return new RenderBundleBase(this, descriptor, AcquireAttachmentState(), std::move(usages));
+        return new RenderBundleBase(this, descriptor, AcquireAttachmentState(), std::move(usages),
+                                    std::move(mIndirectDrawMetadata));
     }
 
     MaybeError RenderBundleEncoder::ValidateFinish(const RenderPassResourceUsage& usages) const {
diff --git a/src/dawn_native/RenderEncoderBase.cpp b/src/dawn_native/RenderEncoderBase.cpp
index 06b0f9b..58849cf 100644
--- a/src/dawn_native/RenderEncoderBase.cpp
+++ b/src/dawn_native/RenderEncoderBase.cpp
@@ -157,16 +157,6 @@
                 DAWN_TRY(ValidateCanUseAs(indirectBuffer, wgpu::BufferUsage::Indirect));
                 DAWN_TRY(mCommandBufferState.ValidateCanDrawIndexed());
 
-                // Indexed indirect draws need a compute-shader based validation check that the
-                // range of indices is contained inside the index buffer on Metal. Disallow them as
-                // unsafe until the validation is implemented.
-                if (GetDevice()->IsToggleEnabled(Toggle::DisallowUnsafeAPIs)) {
-                    return DAWN_VALIDATION_ERROR(
-                        "DrawIndexedIndirect is disallowed because it doesn't validate that the "
-                        "index "
-                        "range is valid yet.");
-                }
-
                 if (indirectOffset % 4 != 0) {
                     return DAWN_VALIDATION_ERROR("Indirect offset must be a multiple of 4");
                 }
@@ -179,7 +169,14 @@
 
             DrawIndexedIndirectCmd* cmd =
                 allocator->Allocate<DrawIndexedIndirectCmd>(Command::DrawIndexedIndirect);
-            cmd->indirectBufferLocation = BufferLocation::New(indirectBuffer, indirectOffset);
+            if (IsValidationEnabled()) {
+                cmd->indirectBufferLocation = BufferLocation::New();
+                mIndirectDrawMetadata.AddIndexedIndirectDraw(
+                    mCommandBufferState.GetIndexFormat(), mCommandBufferState.GetIndexBufferSize(),
+                    indirectBuffer, indirectOffset, cmd->indirectBufferLocation.Get());
+            } else {
+                cmd->indirectBufferLocation = BufferLocation::New(indirectBuffer, indirectOffset);
+            }
 
             mUsageTracker.BufferUsedAs(indirectBuffer, wgpu::BufferUsage::Indirect);
 
diff --git a/src/dawn_native/RenderEncoderBase.h b/src/dawn_native/RenderEncoderBase.h
index 4976ee2..30b7a3c 100644
--- a/src/dawn_native/RenderEncoderBase.h
+++ b/src/dawn_native/RenderEncoderBase.h
@@ -18,6 +18,7 @@
 #include "dawn_native/AttachmentState.h"
 #include "dawn_native/CommandBufferStateTracker.h"
 #include "dawn_native/Error.h"
+#include "dawn_native/IndirectDrawMetadata.h"
 #include "dawn_native/PassResourceUsageTracker.h"
 #include "dawn_native/ProgrammablePassEncoder.h"
 
@@ -64,6 +65,7 @@
 
         CommandBufferStateTracker mCommandBufferState;
         RenderPassResourceUsageTracker mUsageTracker;
+        IndirectDrawMetadata mIndirectDrawMetadata;
 
       private:
         Ref<AttachmentState> mAttachmentState;
diff --git a/src/dawn_native/RenderPassEncoder.cpp b/src/dawn_native/RenderPassEncoder.cpp
index b9cee4f..250d064 100644
--- a/src/dawn_native/RenderPassEncoder.cpp
+++ b/src/dawn_native/RenderPassEncoder.cpp
@@ -99,9 +99,11 @@
                 }
 
                 allocator->Allocate<EndRenderPassCmd>(Command::EndRenderPass);
+                DAWN_TRY(mEncodingContext->ExitRenderPass(this, std::move(mUsageTracker),
+                                                          mCommandEncoder.Get(),
+                                                          std::move(mIndirectDrawMetadata)));
                 return {};
             })) {
-            mEncodingContext->ExitPass(this, mUsageTracker.AcquireResourceUsage());
         }
     }
 
@@ -224,6 +226,10 @@
                     mUsageTracker.AddRenderBundleTextureUsage(usages.textures[i],
                                                               usages.textureUsages[i]);
                 }
+
+                if (IsValidationEnabled()) {
+                    mIndirectDrawMetadata.AddBundle(renderBundles[i]);
+                }
             }
 
             return {};
diff --git a/src/dawn_native/ScratchBuffer.cpp b/src/dawn_native/ScratchBuffer.cpp
new file mode 100644
index 0000000..976214c
--- /dev/null
+++ b/src/dawn_native/ScratchBuffer.cpp
@@ -0,0 +1,47 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/ScratchBuffer.h"
+
+#include "dawn_native/Device.h"
+
+namespace dawn_native {
+
+    ScratchBuffer::ScratchBuffer(DeviceBase* device, wgpu::BufferUsage usage)
+        : mDevice(device), mUsage(usage) {
+    }
+
+    ScratchBuffer::~ScratchBuffer() = default;
+
+    void ScratchBuffer::Reset() {
+        mBuffer = nullptr;
+    }
+
+    MaybeError ScratchBuffer::EnsureCapacity(uint64_t capacity) {
+        if (!mBuffer.Get() || mBuffer->GetSize() < capacity) {
+            BufferDescriptor descriptor;
+            descriptor.size = capacity;
+            descriptor.usage = mUsage;
+            DAWN_TRY_ASSIGN(mBuffer, mDevice->CreateBuffer(&descriptor));
+            mBuffer->SetIsDataInitialized();
+        }
+        return {};
+    }
+
+    BufferBase* ScratchBuffer::GetBuffer() const {
+        ASSERT(mBuffer.Get() != nullptr);
+        return mBuffer.Get();
+    }
+
+}  // namespace dawn_native
diff --git a/src/dawn_native/ScratchBuffer.h b/src/dawn_native/ScratchBuffer.h
new file mode 100644
index 0000000..7bb446d
--- /dev/null
+++ b/src/dawn_native/ScratchBuffer.h
@@ -0,0 +1,55 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWNNATIVE_SCRATCHBUFFER_H_
+#define DAWNNATIVE_SCRATCHBUFFER_H_
+
+#include "common/RefCounted.h"
+#include "dawn_native/Buffer.h"
+
+#include <cstdint>
+
+namespace dawn_native {
+
+    class DeviceBase;
+
+    // A ScratchBuffer is a lazily allocated and lazily grown GPU buffer for intermittent use by
+    // commands in the GPU queue. Note that scratch buffers are not zero-initialized, so users must
+    // be careful not to exposed uninitialized bytes to client shaders.
+    class ScratchBuffer {
+      public:
+        // Note that this object does not retain a reference to `device`, so `device` MUST outlive
+        // this object.
+        ScratchBuffer(DeviceBase* device, wgpu::BufferUsage usage);
+        ~ScratchBuffer();
+
+        // Resets this ScratchBuffer, guaranteeing that the next EnsureCapacity call allocates a
+        // fresh buffer.
+        void Reset();
+
+        // Ensures that this ScratchBuffer is backed by a buffer on `device` with at least
+        // `capacity` bytes of storage.
+        MaybeError EnsureCapacity(uint64_t capacity);
+
+        BufferBase* GetBuffer() const;
+
+      private:
+        DeviceBase* const mDevice;
+        const wgpu::BufferUsage mUsage;
+        Ref<BufferBase> mBuffer;
+    };
+
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_SCRATCHBUFFER_H_
diff --git a/src/dawn_native/d3d12/CommandBufferD3D12.cpp b/src/dawn_native/d3d12/CommandBufferD3D12.cpp
index 175e3dd..4bdc3b0 100644
--- a/src/dawn_native/d3d12/CommandBufferD3D12.cpp
+++ b/src/dawn_native/d3d12/CommandBufferD3D12.cpp
@@ -981,6 +981,10 @@
                     break;
                 }
 
+                case Command::SetValidatedBufferLocationsInternal:
+                    DoNextSetValidatedBufferLocationsInternal();
+                    break;
+
                 case Command::WriteBuffer: {
                     WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
                     const uint64_t offset = write->offset;
diff --git a/src/dawn_native/metal/CommandBufferMTL.mm b/src/dawn_native/metal/CommandBufferMTL.mm
index 4dd47a5..eb9892b 100644
--- a/src/dawn_native/metal/CommandBufferMTL.mm
+++ b/src/dawn_native/metal/CommandBufferMTL.mm
@@ -987,6 +987,10 @@
                     break;
                 }
 
+                case Command::SetValidatedBufferLocationsInternal:
+                    DoNextSetValidatedBufferLocationsInternal();
+                    break;
+
                 case Command::WriteBuffer: {
                     WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
                     const uint64_t offset = write->offset;
diff --git a/src/dawn_native/opengl/CommandBufferGL.cpp b/src/dawn_native/opengl/CommandBufferGL.cpp
index 099d590..f4bb61f 100644
--- a/src/dawn_native/opengl/CommandBufferGL.cpp
+++ b/src/dawn_native/opengl/CommandBufferGL.cpp
@@ -843,6 +843,10 @@
                     break;
                 }
 
+                case Command::SetValidatedBufferLocationsInternal:
+                    DoNextSetValidatedBufferLocationsInternal();
+                    break;
+
                 case Command::WriteBuffer: {
                     WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
                     uint64_t offset = write->offset;
diff --git a/src/dawn_native/vulkan/CommandBufferVk.cpp b/src/dawn_native/vulkan/CommandBufferVk.cpp
index cfc2a71..36f6a39 100644
--- a/src/dawn_native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn_native/vulkan/CommandBufferVk.cpp
@@ -824,6 +824,10 @@
                     break;
                 }
 
+                case Command::SetValidatedBufferLocationsInternal:
+                    DoNextSetValidatedBufferLocationsInternal();
+                    break;
+
                 case Command::WriteBuffer: {
                     WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
                     const uint64_t offset = write->offset;
diff --git a/src/tests/end2end/DrawIndexedIndirectTests.cpp b/src/tests/end2end/DrawIndexedIndirectTests.cpp
index 147d512..0a8a83f 100644
--- a/src/tests/end2end/DrawIndexedIndirectTests.cpp
+++ b/src/tests/end2end/DrawIndexedIndirectTests.cpp
@@ -14,6 +14,7 @@
 
 #include "tests/DawnTest.h"
 
+#include "utils/ComboRenderBundleEncoderDescriptor.h"
 #include "utils/ComboRenderPipelineDescriptor.h"
 #include "utils/WGPUHelpers.h"
 
@@ -59,25 +60,26 @@
              // Second quad: the first 3 vertices represent the top right triangle
              -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f,
              0.0f, 1.0f});
-        indexBuffer = utils::CreateBufferFromData<uint32_t>(
-            device, wgpu::BufferUsage::Index,
-            {0, 1, 2, 0, 3, 1,
-             // The indices below are added to test negatve baseVertex
-             0 + 4, 1 + 4, 2 + 4, 0 + 4, 3 + 4, 1 + 4});
     }
 
     utils::BasicRenderPass renderPass;
     wgpu::RenderPipeline pipeline;
     wgpu::Buffer vertexBuffer;
-    wgpu::Buffer indexBuffer;
 
-    void Test(std::initializer_list<uint32_t> bufferList,
-              uint64_t indexOffset,
-              uint64_t indirectOffset,
-              RGBA8 bottomLeftExpected,
-              RGBA8 topRightExpected) {
-        wgpu::Buffer indirectBuffer =
-            utils::CreateBufferFromData<uint32_t>(device, wgpu::BufferUsage::Indirect, bufferList);
+    wgpu::Buffer CreateIndirectBuffer(std::initializer_list<uint32_t> indirectParamList) {
+        return utils::CreateBufferFromData<uint32_t>(
+            device, wgpu::BufferUsage::Indirect | wgpu::BufferUsage::Storage, indirectParamList);
+    }
+
+    wgpu::Buffer CreateIndexBuffer(std::initializer_list<uint32_t> indexList) {
+        return utils::CreateBufferFromData<uint32_t>(device, wgpu::BufferUsage::Index, indexList);
+    }
+
+    wgpu::CommandBuffer EncodeDrawCommands(std::initializer_list<uint32_t> bufferList,
+                                           wgpu::Buffer indexBuffer,
+                                           uint64_t indexOffset,
+                                           uint64_t indirectOffset) {
+        wgpu::Buffer indirectBuffer = CreateIndirectBuffer(bufferList);
 
         wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
         {
@@ -89,12 +91,28 @@
             pass.EndPass();
         }
 
-        wgpu::CommandBuffer commands = encoder.Finish();
+        return encoder.Finish();
+    }
+
+    void TestDraw(wgpu::CommandBuffer commands, RGBA8 bottomLeftExpected, RGBA8 topRightExpected) {
         queue.Submit(1, &commands);
 
         EXPECT_PIXEL_RGBA8_EQ(bottomLeftExpected, renderPass.color, 1, 3);
         EXPECT_PIXEL_RGBA8_EQ(topRightExpected, renderPass.color, 3, 1);
     }
+
+    void Test(std::initializer_list<uint32_t> bufferList,
+              uint64_t indexOffset,
+              uint64_t indirectOffset,
+              RGBA8 bottomLeftExpected,
+              RGBA8 topRightExpected) {
+        wgpu::Buffer indexBuffer =
+            CreateIndexBuffer({0, 1, 2, 0, 3, 1,
+                               // The indices below are added to test negatve baseVertex
+                               0 + 4, 1 + 4, 2 + 4, 0 + 4, 3 + 4, 1 + 4});
+        TestDraw(EncodeDrawCommands(bufferList, indexBuffer, indexOffset, indirectOffset),
+                 bottomLeftExpected, topRightExpected);
+    }
 };
 
 // The most basic DrawIndexed triangle draw.
@@ -172,6 +190,467 @@
     Test({3, 1, 0, 4, 0, 3, 1, 3, 4, 0}, 0, 5 * sizeof(uint32_t), filled, notFilled);
 }
 
+TEST_P(DrawIndexedIndirectTest, BasicValidation) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows only.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1});
+
+    // Test a draw with an excessive indexCount. Should draw nothing.
+    TestDraw(EncodeDrawCommands({7, 1, 0, 0, 0}, indexBuffer, 0, 0), notFilled, notFilled);
+
+    // Test a draw with an excessive firstIndex. Should draw nothing.
+    TestDraw(EncodeDrawCommands({3, 1, 7, 0, 0}, indexBuffer, 0, 0), notFilled, notFilled);
+
+    // Test a valid draw. Should draw only the second triangle.
+    TestDraw(EncodeDrawCommands({3, 1, 3, 0, 0}, indexBuffer, 0, 0), notFilled, filled);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateWithOffsets) {
+    // TODO(crbug.com/dawn/161): The GL/GLES backend doesn't support indirect index buffer offsets
+    // yet.
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() || IsOpenGLES());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 1, 2});
+
+    // Test that validation properly accounts for index buffer offset.
+    TestDraw(EncodeDrawCommands({3, 1, 0, 0, 0}, indexBuffer, 6 * sizeof(uint32_t), 0), filled,
+             notFilled);
+    TestDraw(EncodeDrawCommands({4, 1, 0, 0, 0}, indexBuffer, 6 * sizeof(uint32_t), 0), notFilled,
+             notFilled);
+    TestDraw(EncodeDrawCommands({3, 1, 4, 0, 0}, indexBuffer, 3 * sizeof(uint32_t), 0), notFilled,
+             notFilled);
+
+    // Test that validation properly accounts for indirect buffer offset.
+    TestDraw(
+        EncodeDrawCommands({3, 1, 0, 0, 0, 1000, 1, 0, 0, 0}, indexBuffer, 0, 4 * sizeof(uint32_t)),
+        notFilled, notFilled);
+    TestDraw(EncodeDrawCommands({3, 1, 0, 0, 0, 1000, 1, 0, 0, 0}, indexBuffer, 0, 0), filled,
+             notFilled);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateMultiplePasses) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows only.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 1, 2});
+
+    // Test validation with multiple passes in a row. Namely this is exercising that scratch buffer
+    // data for use with a previous pass's validation commands is not overwritten before it can be
+    // used.
+    TestDraw(EncodeDrawCommands({10, 1, 0, 0, 0}, indexBuffer, 0, 0), notFilled, notFilled);
+    TestDraw(EncodeDrawCommands({6, 1, 0, 0, 0}, indexBuffer, 0, 0), filled, filled);
+    TestDraw(EncodeDrawCommands({4, 1, 6, 0, 0}, indexBuffer, 0, 0), notFilled, notFilled);
+    TestDraw(EncodeDrawCommands({3, 1, 6, 0, 0}, indexBuffer, 0, 0), filled, notFilled);
+    TestDraw(EncodeDrawCommands({3, 1, 3, 0, 0}, indexBuffer, 0, 0), notFilled, filled);
+    TestDraw(EncodeDrawCommands({6, 1, 3, 0, 0}, indexBuffer, 0, 0), filled, filled);
+    TestDraw(EncodeDrawCommands({6, 1, 6, 0, 0}, indexBuffer, 0, 0), notFilled, notFilled);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateMultipleDraws) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows only.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    // Validate multiple draw calls using the same index and indirect buffers as input, but with
+    // different indirect offsets.
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::Buffer indirectBuffer =
+            CreateIndirectBuffer({3, 1, 3, 0, 0, 10, 1, 0, 0, 0, 3, 1, 6, 0, 0});
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.SetPipeline(pipeline);
+        pass.SetVertexBuffer(0, vertexBuffer);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 1, 2, 0, 3, 1}), wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(indirectBuffer, 0);
+        pass.DrawIndexedIndirect(indirectBuffer, 20);
+        pass.DrawIndexedIndirect(indirectBuffer, 40);
+        pass.EndPass();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+
+    queue.Submit(1, &commands);
+    EXPECT_PIXEL_RGBA8_EQ(notFilled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 3, 1);
+
+    // Validate multiple draw calls using the same indirect buffer but different index buffers as
+    // input.
+    encoder = device.CreateCommandEncoder();
+    {
+        wgpu::Buffer indirectBuffer =
+            CreateIndirectBuffer({3, 1, 3, 0, 0, 10, 1, 0, 0, 0, 3, 1, 6, 0, 0});
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.SetPipeline(pipeline);
+        pass.SetVertexBuffer(0, vertexBuffer);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 1, 2, 0, 3, 1}), wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(indirectBuffer, 0);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 3, 1, 0, 2, 1}), wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(indirectBuffer, 20);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 2, 1}),
+                            wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(indirectBuffer, 40);
+        pass.EndPass();
+    }
+    commands = encoder.Finish();
+
+    queue.Submit(1, &commands);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 3, 1);
+
+    // Validate multiple draw calls using the same index buffer but different indirect buffers as
+    // input.
+    encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.SetPipeline(pipeline);
+        pass.SetVertexBuffer(0, vertexBuffer);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 1, 2, 0, 3, 1}), wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(CreateIndirectBuffer({3, 1, 3, 0, 0}), 0);
+        pass.DrawIndexedIndirect(CreateIndirectBuffer({10, 1, 0, 0, 0}), 0);
+        pass.DrawIndexedIndirect(CreateIndirectBuffer({3, 1, 6, 0, 0}), 0);
+        pass.EndPass();
+    }
+    commands = encoder.Finish();
+
+    queue.Submit(1, &commands);
+    EXPECT_PIXEL_RGBA8_EQ(notFilled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 3, 1);
+
+    // Validate multiple draw calls across different index and indirect buffers.
+    encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.SetPipeline(pipeline);
+        pass.SetVertexBuffer(0, vertexBuffer);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 1, 2, 0, 3, 1}), wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(CreateIndirectBuffer({3, 1, 3, 0, 0}), 0);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 1, 2, 0, 3, 1}), wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(CreateIndirectBuffer({10, 1, 0, 0, 0}), 0);
+        pass.SetIndexBuffer(CreateIndexBuffer({0, 3, 1}), wgpu::IndexFormat::Uint32, 0);
+        pass.DrawIndexedIndirect(CreateIndirectBuffer({3, 1, 3, 0, 0}), 0);
+        pass.EndPass();
+    }
+    commands = encoder.Finish();
+
+    queue.Submit(1, &commands);
+    EXPECT_PIXEL_RGBA8_EQ(notFilled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 3, 1);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateEncodeMultipleThenSubmitInOrder) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows only.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 1, 2});
+
+    wgpu::CommandBuffer commands[7];
+    commands[0] = EncodeDrawCommands({10, 1, 0, 0, 0}, indexBuffer, 0, 0);
+    commands[1] = EncodeDrawCommands({6, 1, 0, 0, 0}, indexBuffer, 0, 0);
+    commands[2] = EncodeDrawCommands({4, 1, 6, 0, 0}, indexBuffer, 0, 0);
+    commands[3] = EncodeDrawCommands({3, 1, 6, 0, 0}, indexBuffer, 0, 0);
+    commands[4] = EncodeDrawCommands({3, 1, 3, 0, 0}, indexBuffer, 0, 0);
+    commands[5] = EncodeDrawCommands({6, 1, 3, 0, 0}, indexBuffer, 0, 0);
+    commands[6] = EncodeDrawCommands({6, 1, 6, 0, 0}, indexBuffer, 0, 0);
+
+    TestDraw(commands[0], notFilled, notFilled);
+    TestDraw(commands[1], filled, filled);
+    TestDraw(commands[2], notFilled, notFilled);
+    TestDraw(commands[3], filled, notFilled);
+    TestDraw(commands[4], notFilled, filled);
+    TestDraw(commands[5], filled, filled);
+    TestDraw(commands[6], notFilled, notFilled);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateEncodeMultipleThenSubmitAtOnce) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // TODO(crbug.com/dawn/1124): Fails on Intel+Vulkan+Windows for drivers
+    // older than 27.20.100.8587, which bots are actively using.
+    DAWN_SUPPRESS_TEST_IF(IsIntel() && IsVulkan() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 1, 2});
+
+    wgpu::CommandBuffer commands[5];
+    commands[0] = EncodeDrawCommands({10, 1, 0, 0, 0}, indexBuffer, 0, 0);
+    commands[1] = EncodeDrawCommands({6, 1, 0, 0, 0}, indexBuffer, 0, 0);
+    commands[2] = EncodeDrawCommands({4, 1, 6, 0, 0}, indexBuffer, 0, 0);
+    commands[3] = EncodeDrawCommands({3, 1, 6, 0, 0}, indexBuffer, 0, 0);
+    commands[4] = EncodeDrawCommands({3, 1, 3, 0, 0}, indexBuffer, 0, 0);
+
+    queue.Submit(5, commands);
+    EXPECT_PIXEL_RGBA8_EQ(notFilled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 3, 1);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateEncodeMultipleThenSubmitOutOfOrder) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows only.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 1, 2});
+
+    wgpu::CommandBuffer commands[7];
+    commands[0] = EncodeDrawCommands({10, 1, 0, 0, 0}, indexBuffer, 0, 0);
+    commands[1] = EncodeDrawCommands({6, 1, 0, 0, 0}, indexBuffer, 0, 0);
+    commands[2] = EncodeDrawCommands({4, 1, 6, 0, 0}, indexBuffer, 0, 0);
+    commands[3] = EncodeDrawCommands({3, 1, 6, 0, 0}, indexBuffer, 0, 0);
+    commands[4] = EncodeDrawCommands({3, 1, 3, 0, 0}, indexBuffer, 0, 0);
+    commands[5] = EncodeDrawCommands({6, 1, 3, 0, 0}, indexBuffer, 0, 0);
+    commands[6] = EncodeDrawCommands({6, 1, 6, 0, 0}, indexBuffer, 0, 0);
+
+    TestDraw(commands[6], notFilled, notFilled);
+    TestDraw(commands[5], filled, filled);
+    TestDraw(commands[4], notFilled, filled);
+    TestDraw(commands[3], filled, notFilled);
+    TestDraw(commands[2], notFilled, notFilled);
+    TestDraw(commands[1], filled, filled);
+    TestDraw(commands[0], notFilled, notFilled);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateWithBundlesInSamePass) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows only.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indirectBuffer =
+        CreateIndirectBuffer({3, 1, 3, 0, 0, 10, 1, 0, 0, 0, 3, 1, 6, 0, 0});
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 1, 2});
+
+    std::vector<wgpu::RenderBundle> bundles;
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 1;
+        desc.cColorFormats[0] = wgpu::TextureFormat::RGBA8Unorm;
+        wgpu::RenderBundleEncoder bundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        bundleEncoder.SetPipeline(pipeline);
+        bundleEncoder.SetVertexBuffer(0, vertexBuffer);
+        bundleEncoder.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32, 0);
+        bundleEncoder.DrawIndexedIndirect(indirectBuffer, 20);
+        bundles.push_back(bundleEncoder.Finish());
+    }
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 1;
+        desc.cColorFormats[0] = wgpu::TextureFormat::RGBA8Unorm;
+        wgpu::RenderBundleEncoder bundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        bundleEncoder.SetPipeline(pipeline);
+        bundleEncoder.SetVertexBuffer(0, vertexBuffer);
+        bundleEncoder.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32, 0);
+        bundleEncoder.DrawIndexedIndirect(indirectBuffer, 40);
+        bundles.push_back(bundleEncoder.Finish());
+    }
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(bundles.size(), bundles.data());
+        pass.EndPass();
+    }
+    wgpu::CommandBuffer commands = encoder.Finish();
+
+    queue.Submit(1, &commands);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(notFilled, renderPass.color, 3, 1);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateWithBundlesInDifferentPasses) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows only.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indirectBuffer =
+        CreateIndirectBuffer({3, 1, 3, 0, 0, 10, 1, 0, 0, 0, 3, 1, 6, 0, 0});
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1, 0, 1, 2});
+
+    wgpu::CommandBuffer commands[2];
+    {
+        wgpu::RenderBundle bundle;
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 1;
+        desc.cColorFormats[0] = wgpu::TextureFormat::RGBA8Unorm;
+        wgpu::RenderBundleEncoder bundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        bundleEncoder.SetPipeline(pipeline);
+        bundleEncoder.SetVertexBuffer(0, vertexBuffer);
+        bundleEncoder.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32, 0);
+        bundleEncoder.DrawIndexedIndirect(indirectBuffer, 20);
+        bundle = bundleEncoder.Finish();
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        renderPass.renderPassInfo.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(1, &bundle);
+        pass.EndPass();
+
+        commands[0] = encoder.Finish();
+    }
+
+    {
+        wgpu::RenderBundle bundle;
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 1;
+        desc.cColorFormats[0] = wgpu::TextureFormat::RGBA8Unorm;
+        wgpu::RenderBundleEncoder bundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        bundleEncoder.SetPipeline(pipeline);
+        bundleEncoder.SetVertexBuffer(0, vertexBuffer);
+        bundleEncoder.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32, 0);
+        bundleEncoder.DrawIndexedIndirect(indirectBuffer, 40);
+        bundle = bundleEncoder.Finish();
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        renderPass.renderPassInfo.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(1, &bundle);
+        pass.EndPass();
+
+        commands[1] = encoder.Finish();
+    }
+
+    queue.Submit(1, &commands[1]);
+    queue.Submit(1, &commands[0]);
+
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(notFilled, renderPass.color, 3, 1);
+}
+
+TEST_P(DrawIndexedIndirectTest, ValidateReusedBundleWithChangingParams) {
+    // TODO(crbug.com/dawn/789): Test is failing under SwANGLE on Windows.
+    DAWN_SUPPRESS_TEST_IF(IsANGLE() && IsWindows());
+
+    // TODO(crbug.com/dawn/1124): Fails on Intel+Vulkan+Windows for drivers
+    // older than 27.20.100.8587, which bots are actively using.
+    DAWN_SUPPRESS_TEST_IF(IsIntel() && IsVulkan() && IsWindows());
+
+    // It doesn't make sense to test invalid inputs when validation is disabled.
+    DAWN_SUPPRESS_TEST_IF(HasToggleEnabled("skip_validation"));
+
+    RGBA8 filled(0, 255, 0, 255);
+    // RGBA8 notFilled(0, 0, 0, 0);
+
+    wgpu::Buffer indirectBuffer = CreateIndirectBuffer({0, 0, 0, 0, 0});
+    wgpu::Buffer indexBuffer = CreateIndexBuffer({0, 1, 2, 0, 3, 1});
+
+    // Encode a single bundle that always uses indirectBuffer offset 0 for its params.
+    wgpu::RenderBundle bundle;
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = wgpu::TextureFormat::RGBA8Unorm;
+    wgpu::RenderBundleEncoder bundleEncoder = device.CreateRenderBundleEncoder(&desc);
+    bundleEncoder.SetPipeline(pipeline);
+    bundleEncoder.SetVertexBuffer(0, vertexBuffer);
+    bundleEncoder.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32, 0);
+    bundleEncoder.DrawIndexedIndirect(indirectBuffer, 0);
+    bundle = bundleEncoder.Finish();
+
+    wgpu::ShaderModule paramWriterModule = utils::CreateShaderModule(device,
+                                                                     R"(
+            [[block]] struct Input { firstIndex: u32; };
+            [[block]] struct Params {
+                indexCount: u32;
+                instanceCount: u32;
+                firstIndex: u32;
+            };
+            [[group(0), binding(0)]] var<uniform> input: Input;
+            [[group(0), binding(1)]] var<storage, write> params: Params;
+            [[stage(compute), workgroup_size(1)]] fn main() {
+                params.indexCount = 3u;
+                params.instanceCount = 1u;
+                params.firstIndex = input.firstIndex;
+            }
+        )");
+
+    wgpu::ComputePipelineDescriptor computeDesc;
+    computeDesc.compute.module = paramWriterModule;
+    computeDesc.compute.entryPoint = "main";
+    wgpu::ComputePipeline computePipeline = device.CreateComputePipeline(&computeDesc);
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+    auto encodeComputePassToUpdateFirstIndex = [&](uint32_t newFirstIndex) {
+        wgpu::Buffer input = utils::CreateBufferFromData<uint32_t>(
+            device, wgpu::BufferUsage::Uniform, {newFirstIndex});
+        wgpu::BindGroup bindGroup = utils::MakeBindGroup(
+            device, computePipeline.GetBindGroupLayout(0),
+            {{0, input, 0, sizeof(uint32_t)}, {1, indirectBuffer, 0, 5 * sizeof(uint32_t)}});
+        wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
+        pass.SetPipeline(computePipeline);
+        pass.SetBindGroup(0, bindGroup);
+        pass.Dispatch(1);
+        pass.EndPass();
+    };
+
+    auto encodeRenderPassToExecuteBundle = [&](wgpu::LoadOp colorLoadOp) {
+        renderPass.renderPassInfo.cColorAttachments[0].loadOp = colorLoadOp;
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(1, &bundle);
+        pass.EndPass();
+    };
+
+    encodeComputePassToUpdateFirstIndex(0);
+    encodeRenderPassToExecuteBundle(wgpu::LoadOp::Clear);
+    encodeComputePassToUpdateFirstIndex(3);
+    encodeRenderPassToExecuteBundle(wgpu::LoadOp::Load);
+    encodeComputePassToUpdateFirstIndex(6);
+    encodeRenderPassToExecuteBundle(wgpu::LoadOp::Load);
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 3, 1);
+}
+
 DAWN_INSTANTIATE_TEST(DrawIndexedIndirectTest,
                       D3D12Backend(),
                       MetalBackend(),
diff --git a/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp b/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp
index abac251..ffdb128 100644
--- a/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp
+++ b/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp
@@ -28,87 +28,6 @@
     }
 };
 
-// Check that DrawIndexedIndirect is disallowed as part of unsafe APIs.
-TEST_F(UnsafeAPIValidationTest, DrawIndexedIndirectDisallowed) {
-    // Create the index and indirect buffers.
-    wgpu::BufferDescriptor indexBufferDesc;
-    indexBufferDesc.size = 4;
-    indexBufferDesc.usage = wgpu::BufferUsage::Index;
-    wgpu::Buffer indexBuffer = device.CreateBuffer(&indexBufferDesc);
-
-    wgpu::BufferDescriptor indirectBufferDesc;
-    indirectBufferDesc.size = 64;
-    indirectBufferDesc.usage = wgpu::BufferUsage::Indirect;
-    wgpu::Buffer indirectBuffer = device.CreateBuffer(&indirectBufferDesc);
-
-    // The RenderPassDescriptor, RenderBundleDescriptor and pipeline for all sub-tests below.
-    DummyRenderPass renderPass(device);
-
-    utils::ComboRenderBundleEncoderDescriptor bundleDesc = {};
-    bundleDesc.colorFormatsCount = 1;
-    bundleDesc.cColorFormats[0] = renderPass.attachmentFormat;
-
-    utils::ComboRenderPipelineDescriptor desc;
-    desc.vertex.module = utils::CreateShaderModule(
-        device,
-        R"([[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
-            return vec4<f32>();
-        })");
-    desc.cFragment.module = utils::CreateShaderModule(device, "[[stage(fragment)]] fn main() {}");
-    desc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
-    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
-
-    // Control cases: DrawIndirect and DrawIndexed are allowed inside a render pass.
-    {
-        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
-        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
-        pass.SetPipeline(pipeline);
-
-        pass.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32);
-        pass.DrawIndexed(1);
-
-        pass.DrawIndirect(indirectBuffer, 0);
-        pass.EndPass();
-        encoder.Finish();
-    }
-
-    // Control case: DrawIndirect and DrawIndexed are allowed inside a render bundle.
-    {
-        wgpu::RenderBundleEncoder encoder = device.CreateRenderBundleEncoder(&bundleDesc);
-        encoder.SetPipeline(pipeline);
-
-        encoder.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32);
-        encoder.DrawIndexed(1);
-
-        encoder.DrawIndirect(indirectBuffer, 0);
-        encoder.Finish();
-    }
-
-    // Error case, DrawIndexedIndirect is disallowed inside a render pass.
-    {
-        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
-        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
-
-        pass.SetPipeline(pipeline);
-        pass.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32);
-        pass.DrawIndexedIndirect(indirectBuffer, 0);
-
-        pass.EndPass();
-        ASSERT_DEVICE_ERROR(encoder.Finish());
-    }
-
-    // Error case, DrawIndexedIndirect is disallowed inside a render bundle.
-    {
-        wgpu::RenderBundleEncoder encoder = device.CreateRenderBundleEncoder(&bundleDesc);
-
-        encoder.SetPipeline(pipeline);
-        encoder.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32);
-        encoder.DrawIndexedIndirect(indirectBuffer, 0);
-
-        ASSERT_DEVICE_ERROR(encoder.Finish());
-    }
-}
-
 // Check that DispatchIndirect is disallowed as part of unsafe APIs.
 TEST_F(UnsafeAPIValidationTest, DispatchIndirectDisallowed) {
     // Create the index and indirect buffers.