Implement RenderBundle in the frontend

This CL implements RenderBundle and RenderBundleEncoder in the frontend
and adds unittests for validation.

Bug: dawn:154
Change-Id: Ice5ecd384cd627ad270b73052408f8139d1ea5f4
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/9221
Commit-Queue: Austin Eng <enga@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
diff --git a/src/dawn_native/AttachmentState.cpp b/src/dawn_native/AttachmentState.cpp
index 39ddb4b..00bd10b 100644
--- a/src/dawn_native/AttachmentState.cpp
+++ b/src/dawn_native/AttachmentState.cpp
@@ -21,6 +21,19 @@
 
 namespace dawn_native {
 
+    AttachmentStateBlueprint::AttachmentStateBlueprint(
+        const RenderBundleEncoderDescriptor* descriptor)
+        : mHasDepthStencilAttachment(descriptor->depthStencilFormat != nullptr),
+          mSampleCount(descriptor->sampleCount) {
+        for (uint32_t i = 0; i < descriptor->colorFormatsCount; ++i) {
+            mColorAttachmentsSet.set(i);
+            mColorFormats[i] = descriptor->colorFormats[i];
+        }
+        if (mHasDepthStencilAttachment) {
+            mDepthStencilFormat = *descriptor->depthStencilFormat;
+        }
+    }
+
     AttachmentStateBlueprint::AttachmentStateBlueprint(const RenderPipelineDescriptor* descriptor)
         : mHasDepthStencilAttachment(descriptor->depthStencilState != nullptr),
           mSampleCount(descriptor->sampleCount) {
diff --git a/src/dawn_native/AttachmentState.h b/src/dawn_native/AttachmentState.h
index 34f2c1a..a7202fd 100644
--- a/src/dawn_native/AttachmentState.h
+++ b/src/dawn_native/AttachmentState.h
@@ -33,8 +33,9 @@
     class AttachmentStateBlueprint {
       public:
         // Note: Descriptors must be validated before the AttachmentState is constructed.
-        AttachmentStateBlueprint(const RenderPipelineDescriptor* descriptor);
-        AttachmentStateBlueprint(const RenderPassDescriptor* descriptor);
+        explicit AttachmentStateBlueprint(const RenderBundleEncoderDescriptor* descriptor);
+        explicit AttachmentStateBlueprint(const RenderPipelineDescriptor* descriptor);
+        explicit AttachmentStateBlueprint(const RenderPassDescriptor* descriptor);
 
         AttachmentStateBlueprint(const AttachmentStateBlueprint& rhs);
 
diff --git a/src/dawn_native/CommandValidation.cpp b/src/dawn_native/CommandValidation.cpp
index 9099a9e..78f8949 100644
--- a/src/dawn_native/CommandValidation.cpp
+++ b/src/dawn_native/CommandValidation.cpp
@@ -19,6 +19,7 @@
 #include "dawn_native/CommandBufferStateTracker.h"
 #include "dawn_native/Commands.h"
 #include "dawn_native/PassResourceUsageTracker.h"
+#include "dawn_native/RenderBundle.h"
 #include "dawn_native/RenderPipeline.h"
 
 namespace dawn_native {
@@ -177,6 +178,27 @@
 
     }  // namespace
 
+    MaybeError ValidateRenderBundle(CommandIterator* commands,
+                                    const AttachmentState* attachmentState,
+                                    PassResourceUsage* resourceUsage) {
+        PassResourceUsageTracker usageTracker;
+        CommandBufferStateTracker commandBufferState;
+        unsigned int debugGroupStackSize = 0;
+
+        Command type;
+        while (commands->NextCommandId(&type)) {
+            DAWN_TRY(ValidateRenderBundleCommand(commands, type, &usageTracker, &commandBufferState,
+                                                 attachmentState, &debugGroupStackSize,
+                                                 "Command disallowed inside a render bundle"));
+        }
+
+        DAWN_TRY(usageTracker.ValidateRenderPassUsages());
+        ASSERT(resourceUsage != nullptr);
+        *resourceUsage = usageTracker.AcquireResourceUsage();
+
+        return {};
+    }
+
     MaybeError ValidateRenderPass(CommandIterator* commands,
                                   BeginRenderPassCmd* renderPass,
                                   std::vector<PassResourceUsage>* perPassResourceUsages) {
@@ -217,6 +239,33 @@
                     return {};
                 } break;
 
+                case Command::ExecuteBundles: {
+                    ExecuteBundlesCmd* cmd = commands->NextCommand<ExecuteBundlesCmd>();
+                    auto bundles = commands->NextData<Ref<RenderBundleBase>>(cmd->count);
+                    for (uint32_t i = 0; i < cmd->count; ++i) {
+                        if (DAWN_UNLIKELY(renderPass->attachmentState.Get() !=
+                                          bundles[i]->GetAttachmentState())) {
+                            return DAWN_VALIDATION_ERROR(
+                                "Render bundle is not compatible with render pass");
+                        }
+
+                        const PassResourceUsage& usages = bundles[i]->GetResourceUsage();
+                        for (uint32_t i = 0; i < usages.buffers.size(); ++i) {
+                            usageTracker.BufferUsedAs(usages.buffers[i], usages.bufferUsages[i]);
+                        }
+
+                        for (uint32_t i = 0; i < usages.textures.size(); ++i) {
+                            usageTracker.TextureUsedAs(usages.textures[i], usages.textureUsages[i]);
+                        }
+                    }
+
+                    if (cmd->count > 0) {
+                        // Reset state. It is invalidated after render bundle execution.
+                        commandBufferState = CommandBufferStateTracker{};
+                    }
+
+                } break;
+
                 case Command::SetStencilReference: {
                     commands->NextCommand<SetStencilReferenceCmd>();
                 } break;
diff --git a/src/dawn_native/CommandValidation.h b/src/dawn_native/CommandValidation.h
index 2d29cb3..c90343c 100644
--- a/src/dawn_native/CommandValidation.h
+++ b/src/dawn_native/CommandValidation.h
@@ -22,9 +22,13 @@
 
 namespace dawn_native {
 
+    class AttachmentState;
     struct BeginRenderPassCmd;
     struct PassResourceUsage;
 
+    MaybeError ValidateRenderBundle(CommandIterator* commands,
+                                    const AttachmentState* attachmentState,
+                                    PassResourceUsage* resourceUsage);
     MaybeError ValidateRenderPass(CommandIterator* commands,
                                   BeginRenderPassCmd* renderPass,
                                   std::vector<PassResourceUsage>* perPassResourceUsages);
diff --git a/src/dawn_native/Commands.cpp b/src/dawn_native/Commands.cpp
index fbc9172..eb11791 100644
--- a/src/dawn_native/Commands.cpp
+++ b/src/dawn_native/Commands.cpp
@@ -18,6 +18,7 @@
 #include "dawn_native/Buffer.h"
 #include "dawn_native/CommandAllocator.h"
 #include "dawn_native/ComputePipeline.h"
+#include "dawn_native/RenderBundle.h"
 #include "dawn_native/RenderPipeline.h"
 #include "dawn_native/Texture.h"
 
@@ -86,6 +87,14 @@
                     EndRenderPassCmd* cmd = commands->NextCommand<EndRenderPassCmd>();
                     cmd->~EndRenderPassCmd();
                 } break;
+                case Command::ExecuteBundles: {
+                    ExecuteBundlesCmd* cmd = commands->NextCommand<ExecuteBundlesCmd>();
+                    auto bundles = commands->NextData<Ref<RenderBundleBase>>(cmd->count);
+                    for (size_t i = 0; i < cmd->count; ++i) {
+                        (&bundles[i])->~Ref<RenderBundleBase>();
+                    }
+                    cmd->~ExecuteBundlesCmd();
+                } break;
                 case Command::InsertDebugMarker: {
                     InsertDebugMarkerCmd* cmd = commands->NextCommand<InsertDebugMarkerCmd>();
                     commands->NextData<char>(cmd->length + 1);
@@ -207,6 +216,11 @@
                 commands->NextCommand<EndRenderPassCmd>();
                 break;
 
+            case Command::ExecuteBundles: {
+                auto* cmd = commands->NextCommand<ExecuteBundlesCmd>();
+                commands->NextData<Ref<RenderBundleBase>>(cmd->count);
+            } break;
+
             case Command::InsertDebugMarker: {
                 InsertDebugMarkerCmd* cmd = commands->NextCommand<InsertDebugMarkerCmd>();
                 commands->NextData<char>(cmd->length + 1);
diff --git a/src/dawn_native/Commands.h b/src/dawn_native/Commands.h
index 442cc95..18d834d 100644
--- a/src/dawn_native/Commands.h
+++ b/src/dawn_native/Commands.h
@@ -46,6 +46,7 @@
         DrawIndexedIndirect,
         EndComputePass,
         EndRenderPass,
+        ExecuteBundles,
         InsertDebugMarker,
         PopDebugGroup,
         PushDebugGroup,
@@ -170,6 +171,10 @@
 
     struct EndRenderPassCmd {};
 
+    struct ExecuteBundlesCmd {
+        uint32_t count;
+    };
+
     struct InsertDebugMarkerCmd {
         uint32_t length;
     };
diff --git a/src/dawn_native/Device.cpp b/src/dawn_native/Device.cpp
index 3eef7e3..b02bda1e 100644
--- a/src/dawn_native/Device.cpp
+++ b/src/dawn_native/Device.cpp
@@ -29,6 +29,7 @@
 #include "dawn_native/Instance.h"
 #include "dawn_native/PipelineLayout.h"
 #include "dawn_native/Queue.h"
+#include "dawn_native/RenderBundleEncoder.h"
 #include "dawn_native/RenderPipeline.h"
 #include "dawn_native/Sampler.h"
 #include "dawn_native/ShaderModule.h"
@@ -268,33 +269,34 @@
     }
 
     Ref<AttachmentState> DeviceBase::GetOrCreateAttachmentState(
-        const RenderPipelineDescriptor* descriptor) {
-        AttachmentStateBlueprint blueprint(descriptor);
-
-        auto iter = mCaches->attachmentStates.find(&blueprint);
+        AttachmentStateBlueprint* blueprint) {
+        auto iter = mCaches->attachmentStates.find(blueprint);
         if (iter != mCaches->attachmentStates.end()) {
             return static_cast<AttachmentState*>(*iter);
         }
 
-        Ref<AttachmentState> attachmentState = new AttachmentState(this, blueprint);
+        Ref<AttachmentState> attachmentState = new AttachmentState(this, *blueprint);
         attachmentState->Release();
         mCaches->attachmentStates.insert(attachmentState.Get());
         return attachmentState;
     }
 
     Ref<AttachmentState> DeviceBase::GetOrCreateAttachmentState(
+        const RenderBundleEncoderDescriptor* descriptor) {
+        AttachmentStateBlueprint blueprint(descriptor);
+        return GetOrCreateAttachmentState(&blueprint);
+    }
+
+    Ref<AttachmentState> DeviceBase::GetOrCreateAttachmentState(
+        const RenderPipelineDescriptor* descriptor) {
+        AttachmentStateBlueprint blueprint(descriptor);
+        return GetOrCreateAttachmentState(&blueprint);
+    }
+
+    Ref<AttachmentState> DeviceBase::GetOrCreateAttachmentState(
         const RenderPassDescriptor* descriptor) {
         AttachmentStateBlueprint blueprint(descriptor);
-
-        auto iter = mCaches->attachmentStates.find(&blueprint);
-        if (iter != mCaches->attachmentStates.end()) {
-            return static_cast<AttachmentState*>(*iter);
-        }
-
-        Ref<AttachmentState> attachmentState = new AttachmentState(this, blueprint);
-        attachmentState->Release();
-        mCaches->attachmentStates.insert(attachmentState.Get());
-        return attachmentState;
+        return GetOrCreateAttachmentState(&blueprint);
     }
 
     void DeviceBase::UncacheAttachmentState(AttachmentState* obj) {
@@ -428,6 +430,16 @@
 
         return result;
     }
+    RenderBundleEncoderBase* DeviceBase::CreateRenderBundleEncoder(
+        const RenderBundleEncoderDescriptor* descriptor) {
+        RenderBundleEncoderBase* result = nullptr;
+
+        if (ConsumedError(CreateRenderBundleEncoderInternal(&result, descriptor))) {
+            return RenderBundleEncoderBase::MakeError(this);
+        }
+
+        return result;
+    }
     RenderPipelineBase* DeviceBase::CreateRenderPipeline(
         const RenderPipelineDescriptor* descriptor) {
         RenderPipelineBase* result = nullptr;
@@ -601,6 +613,14 @@
         return {};
     }
 
+    MaybeError DeviceBase::CreateRenderBundleEncoderInternal(
+        RenderBundleEncoderBase** result,
+        const RenderBundleEncoderDescriptor* descriptor) {
+        DAWN_TRY(ValidateRenderBundleEncoderDescriptor(this, descriptor));
+        *result = new RenderBundleEncoderBase(this, descriptor);
+        return {};
+    }
+
     MaybeError DeviceBase::CreateRenderPipelineInternal(
         RenderPipelineBase** result,
         const RenderPipelineDescriptor* descriptor) {
diff --git a/src/dawn_native/Device.h b/src/dawn_native/Device.h
index 8a2b1c8..3ccde37 100644
--- a/src/dawn_native/Device.h
+++ b/src/dawn_native/Device.h
@@ -34,6 +34,7 @@
 
     class AdapterBase;
     class AttachmentState;
+    class AttachmentStateBlueprint;
     class FenceSignalTracker;
     class DynamicUploader;
     class StagingBufferBase;
@@ -116,6 +117,9 @@
             const ShaderModuleDescriptor* descriptor);
         void UncacheShaderModule(ShaderModuleBase* obj);
 
+        Ref<AttachmentState> GetOrCreateAttachmentState(AttachmentStateBlueprint* blueprint);
+        Ref<AttachmentState> GetOrCreateAttachmentState(
+            const RenderBundleEncoderDescriptor* descriptor);
         Ref<AttachmentState> GetOrCreateAttachmentState(const RenderPipelineDescriptor* descriptor);
         Ref<AttachmentState> GetOrCreateAttachmentState(const RenderPassDescriptor* descriptor);
         void UncacheAttachmentState(AttachmentState* obj);
@@ -132,6 +136,8 @@
         ComputePipelineBase* CreateComputePipeline(const ComputePipelineDescriptor* descriptor);
         PipelineLayoutBase* CreatePipelineLayout(const PipelineLayoutDescriptor* descriptor);
         QueueBase* CreateQueue();
+        RenderBundleEncoderBase* CreateRenderBundleEncoder(
+            const RenderBundleEncoderDescriptor* descriptor);
         RenderPipelineBase* CreateRenderPipeline(const RenderPipelineDescriptor* descriptor);
         SamplerBase* CreateSampler(const SamplerDescriptor* descriptor);
         ShaderModuleBase* CreateShaderModule(const ShaderModuleDescriptor* descriptor);
@@ -204,6 +210,9 @@
         MaybeError CreatePipelineLayoutInternal(PipelineLayoutBase** result,
                                                 const PipelineLayoutDescriptor* descriptor);
         MaybeError CreateQueueInternal(QueueBase** result);
+        MaybeError CreateRenderBundleEncoderInternal(
+            RenderBundleEncoderBase** result,
+            const RenderBundleEncoderDescriptor* descriptor);
         MaybeError CreateRenderPipelineInternal(RenderPipelineBase** result,
                                                 const RenderPipelineDescriptor* descriptor);
         MaybeError CreateSamplerInternal(SamplerBase** result, const SamplerDescriptor* descriptor);
diff --git a/src/dawn_native/EncodingContext.cpp b/src/dawn_native/EncodingContext.cpp
index d2c8a75..d36ccef 100644
--- a/src/dawn_native/EncodingContext.cpp
+++ b/src/dawn_native/EncodingContext.cpp
@@ -76,6 +76,10 @@
     }
 
     MaybeError EncodingContext::Finish() {
+        if (IsFinished()) {
+            return DAWN_VALIDATION_ERROR("Command encoding already finished");
+        }
+
         const void* currentEncoder = mCurrentEncoder;
         const void* topLevelEncoder = mTopLevelEncoder;
 
diff --git a/src/dawn_native/Forward.h b/src/dawn_native/Forward.h
index b4c3e83..1ba29da 100644
--- a/src/dawn_native/Forward.h
+++ b/src/dawn_native/Forward.h
@@ -32,6 +32,8 @@
     class PipelineBase;
     class PipelineLayoutBase;
     class QueueBase;
+    class RenderBundleBase;
+    class RenderBundleEncoderBase;
     class RenderPassEncoderBase;
     class RenderPipelineBase;
     class SamplerBase;
diff --git a/src/dawn_native/RenderBundle.cpp b/src/dawn_native/RenderBundle.cpp
new file mode 100644
index 0000000..9cd08ea
--- /dev/null
+++ b/src/dawn_native/RenderBundle.cpp
@@ -0,0 +1,61 @@
+// Copyright 2019 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/RenderBundle.h"
+
+#include "common/BitSetIterator.h"
+#include "dawn_native/Commands.h"
+#include "dawn_native/Device.h"
+#include "dawn_native/RenderBundleEncoder.h"
+
+namespace dawn_native {
+
+    RenderBundleBase::RenderBundleBase(RenderBundleEncoderBase* encoder,
+                                       const RenderBundleDescriptor* descriptor,
+                                       AttachmentState* attachmentState,
+                                       PassResourceUsage resourceUsage)
+        : ObjectBase(encoder->GetDevice()),
+          mCommands(encoder->AcquireCommands()),
+          mAttachmentState(attachmentState),
+          mResourceUsage(std::move(resourceUsage)) {
+    }
+
+    RenderBundleBase::~RenderBundleBase() {
+        FreeCommands(&mCommands);
+    }
+
+    // static
+    RenderBundleBase* RenderBundleBase::MakeError(DeviceBase* device) {
+        return new RenderBundleBase(device, ObjectBase::kError);
+    }
+
+    RenderBundleBase::RenderBundleBase(DeviceBase* device, ErrorTag errorTag)
+        : ObjectBase(device, errorTag) {
+    }
+
+    CommandIterator* RenderBundleBase::GetCommands() {
+        return &mCommands;
+    }
+
+    const AttachmentState* RenderBundleBase::GetAttachmentState() const {
+        ASSERT(!IsError());
+        return mAttachmentState.Get();
+    }
+
+    const PassResourceUsage& RenderBundleBase::GetResourceUsage() const {
+        ASSERT(!IsError());
+        return mResourceUsage;
+    }
+
+}  // namespace dawn_native
diff --git a/src/dawn_native/RenderBundle.h b/src/dawn_native/RenderBundle.h
new file mode 100644
index 0000000..26db850
--- /dev/null
+++ b/src/dawn_native/RenderBundle.h
@@ -0,0 +1,60 @@
+// Copyright 2019 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_RENDERBUNDLE_H_
+#define DAWNNATIVE_RENDERBUNDLE_H_
+
+#include "common/Constants.h"
+#include "dawn_native/AttachmentState.h"
+#include "dawn_native/CommandAllocator.h"
+#include "dawn_native/Error.h"
+#include "dawn_native/ObjectBase.h"
+#include "dawn_native/PassResourceUsage.h"
+
+#include "dawn_native/dawn_platform.h"
+
+#include <bitset>
+
+namespace dawn_native {
+
+    struct BeginRenderPassCmd;
+    struct RenderBundleDescriptor;
+    class RenderBundleEncoderBase;
+
+    class RenderBundleBase : public ObjectBase {
+      public:
+        RenderBundleBase(RenderBundleEncoderBase* encoder,
+                         const RenderBundleDescriptor* descriptor,
+                         AttachmentState* attachmentState,
+                         PassResourceUsage resourceUsage);
+        ~RenderBundleBase() override;
+
+        static RenderBundleBase* MakeError(DeviceBase* device);
+
+        CommandIterator* GetCommands();
+
+        const AttachmentState* GetAttachmentState() const;
+        const PassResourceUsage& GetResourceUsage() const;
+
+      private:
+        RenderBundleBase(DeviceBase* device, ErrorTag errorTag);
+
+        CommandIterator mCommands;
+        Ref<AttachmentState> mAttachmentState;
+        PassResourceUsage mResourceUsage;
+    };
+
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_RENDERBUNDLE_H_
diff --git a/src/dawn_native/RenderBundleEncoder.cpp b/src/dawn_native/RenderBundleEncoder.cpp
new file mode 100644
index 0000000..b4febef
--- /dev/null
+++ b/src/dawn_native/RenderBundleEncoder.cpp
@@ -0,0 +1,99 @@
+// Copyright 2019 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/RenderBundleEncoder.h"
+
+#include "dawn_native/CommandValidation.h"
+#include "dawn_native/Commands.h"
+#include "dawn_native/Device.h"
+#include "dawn_native/RenderPipeline.h"
+#include "dawn_native/ValidationUtils_autogen.h"
+
+namespace dawn_native {
+
+    MaybeError ValidateRenderBundleEncoderDescriptor(
+        const DeviceBase* device,
+        const RenderBundleEncoderDescriptor* descriptor) {
+        if (!IsValidSampleCount(descriptor->sampleCount)) {
+            return DAWN_VALIDATION_ERROR("Sample count is not supported");
+        }
+
+        if (descriptor->colorFormatsCount > kMaxColorAttachments) {
+            return DAWN_VALIDATION_ERROR("Color formats count exceeds maximum");
+        }
+
+        if (descriptor->colorFormatsCount == 0 && !descriptor->depthStencilFormat) {
+            return DAWN_VALIDATION_ERROR("Should have at least one attachment format");
+        }
+
+        for (uint32_t i = 0; i < descriptor->colorFormatsCount; ++i) {
+            DAWN_TRY(ValidateTextureFormat(descriptor->colorFormats[i]));
+        }
+
+        if (descriptor->depthStencilFormat != nullptr) {
+            DAWN_TRY(ValidateTextureFormat(*descriptor->depthStencilFormat));
+        }
+
+        return {};
+    }
+
+    RenderBundleEncoderBase::RenderBundleEncoderBase(
+        DeviceBase* device,
+        const RenderBundleEncoderDescriptor* descriptor)
+        : RenderEncoderBase(device, &mEncodingContext),
+          mEncodingContext(device, this),
+          mAttachmentState(device->GetOrCreateAttachmentState(descriptor)) {
+    }
+
+    RenderBundleEncoderBase::RenderBundleEncoderBase(DeviceBase* device, ErrorTag errorTag)
+        : RenderEncoderBase(device, &mEncodingContext, errorTag), mEncodingContext(device, this) {
+    }
+
+    // static
+    RenderBundleEncoderBase* RenderBundleEncoderBase::MakeError(DeviceBase* device) {
+        return new RenderBundleEncoderBase(device, ObjectBase::kError);
+    }
+
+    const AttachmentState* RenderBundleEncoderBase::GetAttachmentState() const {
+        return mAttachmentState.Get();
+    }
+
+    CommandIterator RenderBundleEncoderBase::AcquireCommands() {
+        return mEncodingContext.AcquireCommands();
+    }
+
+    RenderBundleBase* RenderBundleEncoderBase::Finish(const RenderBundleDescriptor* descriptor) {
+        if (GetDevice()->ConsumedError(ValidateFinish(descriptor))) {
+            return RenderBundleBase::MakeError(GetDevice());
+        }
+        ASSERT(!IsError());
+
+        return new RenderBundleBase(this, descriptor, mAttachmentState.Get(),
+                                    std::move(mResourceUsage));
+    }
+
+    MaybeError RenderBundleEncoderBase::ValidateFinish(const RenderBundleDescriptor* descriptor) {
+        DAWN_TRY(GetDevice()->ValidateObject(this));
+
+        // Even if Finish() validation fails, calling it will mutate the internal state of the
+        // encoding context. Subsequent calls to encode commands will generate errors.
+        DAWN_TRY(mEncodingContext.Finish());
+
+        CommandIterator* commands = mEncodingContext.GetIterator();
+
+        DAWN_TRY(ValidateRenderBundle(commands, mAttachmentState.Get(), &mResourceUsage));
+        return {};
+    }
+
+}  // namespace dawn_native
diff --git a/src/dawn_native/RenderBundleEncoder.h b/src/dawn_native/RenderBundleEncoder.h
new file mode 100644
index 0000000..aa32201
--- /dev/null
+++ b/src/dawn_native/RenderBundleEncoder.h
@@ -0,0 +1,53 @@
+// Copyright 2019 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_RENDERBUNDLEENCODER_H_
+#define DAWNNATIVE_RENDERBUNDLEENCODER_H_
+
+#include "dawn_native/AttachmentState.h"
+#include "dawn_native/EncodingContext.h"
+#include "dawn_native/Error.h"
+#include "dawn_native/RenderBundle.h"
+#include "dawn_native/RenderEncoderBase.h"
+
+namespace dawn_native {
+
+    MaybeError ValidateRenderBundleEncoderDescriptor(
+        const DeviceBase* device,
+        const RenderBundleEncoderDescriptor* descriptor);
+    class RenderBundleEncoderBase : public RenderEncoderBase {
+      public:
+        RenderBundleEncoderBase(DeviceBase* device,
+                                const RenderBundleEncoderDescriptor* descriptor);
+
+        static RenderBundleEncoderBase* MakeError(DeviceBase* device);
+
+        const AttachmentState* GetAttachmentState() const;
+
+        RenderBundleBase* Finish(const RenderBundleDescriptor* descriptor);
+
+        CommandIterator AcquireCommands();
+
+      private:
+        RenderBundleEncoderBase(DeviceBase* device, ErrorTag errorTag);
+
+        MaybeError ValidateFinish(const RenderBundleDescriptor* descriptor);
+
+        EncodingContext mEncodingContext;
+        Ref<AttachmentState> mAttachmentState;
+        PassResourceUsage mResourceUsage;
+    };
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_RENDERBUNDLEENCODER_H_
diff --git a/src/dawn_native/RenderPassEncoder.cpp b/src/dawn_native/RenderPassEncoder.cpp
index 54d5db5..27f5df3 100644
--- a/src/dawn_native/RenderPassEncoder.cpp
+++ b/src/dawn_native/RenderPassEncoder.cpp
@@ -19,6 +19,7 @@
 #include "dawn_native/CommandEncoder.h"
 #include "dawn_native/Commands.h"
 #include "dawn_native/Device.h"
+#include "dawn_native/RenderBundle.h"
 #include "dawn_native/RenderPipeline.h"
 
 #include <math.h>
@@ -130,4 +131,24 @@
         });
     }
 
+    void RenderPassEncoderBase::ExecuteBundles(uint32_t count,
+                                               RenderBundleBase* const* renderBundles) {
+        mEncodingContext->TryEncode(this, [&](CommandAllocator* allocator) -> MaybeError {
+            for (uint32_t i = 0; i < count; ++i) {
+                DAWN_TRY(GetDevice()->ValidateObject(renderBundles[i]));
+            }
+
+            ExecuteBundlesCmd* cmd =
+                allocator->Allocate<ExecuteBundlesCmd>(Command::ExecuteBundles);
+            cmd->count = count;
+
+            Ref<RenderBundleBase>* bundles = allocator->AllocateData<Ref<RenderBundleBase>>(count);
+            for (uint32_t i = 0; i < count; ++i) {
+                bundles[i] = renderBundles[i];
+            }
+
+            return {};
+        });
+    }
+
 }  // namespace dawn_native
diff --git a/src/dawn_native/RenderPassEncoder.h b/src/dawn_native/RenderPassEncoder.h
index b961079..4b7c06d 100644
--- a/src/dawn_native/RenderPassEncoder.h
+++ b/src/dawn_native/RenderPassEncoder.h
@@ -20,10 +20,12 @@
 
 namespace dawn_native {
 
+    class RenderBundleBase;
+
     // This is called RenderPassEncoderBase to match the code generator expectations. Note that it
     // is a pure frontend type to record in its parent CommandEncoder and never has a backend
     // implementation.
-    // TODO(cwallez@chromium.org): Remove that generator limitation and rename to ComputePassEncoder
+    // TODO(cwallez@chromium.org): Remove that generator limitation and rename to RenderPassEncoder
     class RenderPassEncoderBase : public RenderEncoderBase {
       public:
         RenderPassEncoderBase(DeviceBase* device,
@@ -45,6 +47,7 @@
                          float minDepth,
                          float maxDepth);
         void SetScissorRect(uint32_t x, uint32_t y, uint32_t width, uint32_t height);
+        void ExecuteBundles(uint32_t count, RenderBundleBase* const* renderBundles);
 
       protected:
         RenderPassEncoderBase(DeviceBase* device,
diff --git a/src/dawn_native/RenderPipeline.h b/src/dawn_native/RenderPipeline.h
index 7a00e3a..490d178 100644
--- a/src/dawn_native/RenderPipeline.h
+++ b/src/dawn_native/RenderPipeline.h
@@ -28,6 +28,7 @@
     struct BeginRenderPassCmd;
 
     class DeviceBase;
+    class RenderBundleEncoderBase;
 
     MaybeError ValidateRenderPipelineDescriptor(const DeviceBase* device,
                                                 const RenderPipelineDescriptor* descriptor);
diff --git a/src/tests/unittests/validation/RenderBundleValidationTests.cpp b/src/tests/unittests/validation/RenderBundleValidationTests.cpp
new file mode 100644
index 0000000..daac454
--- /dev/null
+++ b/src/tests/unittests/validation/RenderBundleValidationTests.cpp
@@ -0,0 +1,931 @@
+// Copyright 2019 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 "tests/unittests/validation/ValidationTest.h"
+
+#include "common/Constants.h"
+
+#include "utils/ComboRenderBundleEncoderDescriptor.h"
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/DawnHelpers.h"
+
+namespace {
+
+    class RenderBundleValidationTest : public ValidationTest {
+      protected:
+        void SetUp() override {
+            ValidationTest::SetUp();
+
+            vsModule = utils::CreateShaderModule(device, utils::ShaderStage::Vertex, R"(
+              #version 450
+              layout(location = 0) in vec2 pos;
+              layout (set = 0, binding = 0) uniform vertexUniformBuffer {
+                  mat2 transform;
+              };
+              void main() {
+              })");
+
+            fsModule = utils::CreateShaderModule(device, utils::ShaderStage::Fragment, R"(
+              #version 450
+              layout (set = 1, binding = 0) uniform fragmentUniformBuffer {
+                  vec4 color;
+              };
+              layout (set = 1, binding = 1) buffer storageBuffer {
+                  float dummy[];
+              };
+              void main() {
+              })");
+
+            dawn::BindGroupLayout bgls[] = {
+                utils::MakeBindGroupLayout(
+                    device, {{0, dawn::ShaderStageBit::Vertex, dawn::BindingType::UniformBuffer}}),
+                utils::MakeBindGroupLayout(
+                    device,
+                    {
+                        {0, dawn::ShaderStageBit::Fragment, dawn::BindingType::UniformBuffer},
+                        {1, dawn::ShaderStageBit::Fragment, dawn::BindingType::StorageBuffer},
+                    })};
+
+            dawn::PipelineLayoutDescriptor pipelineLayoutDesc;
+            pipelineLayoutDesc.bindGroupLayoutCount = 2;
+            pipelineLayoutDesc.bindGroupLayouts = bgls;
+
+            pipelineLayout = device.CreatePipelineLayout(&pipelineLayoutDesc);
+
+            utils::ComboRenderPipelineDescriptor descriptor = MakeRenderPipelineDescriptor();
+            pipeline = device.CreateRenderPipeline(&descriptor);
+
+            float data[4];
+            dawn::Buffer buffer = utils::CreateBufferFromData(device, data, 4 * sizeof(float),
+                                                              dawn::BufferUsageBit::Uniform);
+
+            constexpr static float kVertices[] = {-1.f, 1.f, 1.f, -1.f, -1.f, 1.f};
+
+            vertexBuffer = utils::CreateBufferFromData(device, kVertices, sizeof(kVertices),
+                                                       dawn::BufferUsageBit::Vertex);
+
+            // Dummy storage buffer.
+            dawn::Buffer storageBuffer = utils::CreateBufferFromData(
+                device, kVertices, sizeof(kVertices), dawn::BufferUsageBit::Storage);
+
+            // Vertex buffer with storage usage for testing read+write error usage.
+            vertexStorageBuffer = utils::CreateBufferFromData(
+                device, kVertices, sizeof(kVertices),
+                dawn::BufferUsageBit::Vertex | dawn::BufferUsageBit::Storage);
+
+            bg0 = utils::MakeBindGroup(device, bgls[0], {{0, buffer, 0, 4 * sizeof(float)}});
+            bg1 = utils::MakeBindGroup(
+                device, bgls[1],
+                {{0, buffer, 0, 4 * sizeof(float)}, {1, storageBuffer, 0, sizeof(kVertices)}});
+
+            bg1Vertex = utils::MakeBindGroup(device, bgls[1],
+                                             {{0, buffer, 0, 4 * sizeof(float)},
+                                              {1, vertexStorageBuffer, 0, sizeof(kVertices)}});
+        }
+
+        utils::ComboRenderPipelineDescriptor MakeRenderPipelineDescriptor() {
+            utils::ComboRenderPipelineDescriptor descriptor(device);
+            descriptor.layout = pipelineLayout;
+            descriptor.cVertexStage.module = vsModule;
+            descriptor.cFragmentStage.module = fsModule;
+            descriptor.cVertexInput.bufferCount = 1;
+            descriptor.cVertexInput.cBuffers[0].stride = 2 * sizeof(float);
+            descriptor.cVertexInput.cBuffers[0].attributeCount = 1;
+            descriptor.cVertexInput.cAttributes[0].format = dawn::VertexFormat::Float2;
+
+            return descriptor;
+        }
+
+        dawn::ShaderModule vsModule;
+        dawn::ShaderModule fsModule;
+        dawn::PipelineLayout pipelineLayout;
+        dawn::RenderPipeline pipeline;
+        dawn::Buffer vertexBuffer;
+        dawn::Buffer vertexStorageBuffer;
+        const uint64_t zeroOffset = 0;
+        dawn::BindGroup bg0;
+        dawn::BindGroup bg1;
+        dawn::BindGroup bg1Vertex;
+    };
+
+}  // anonymous namespace
+
+// Test creating and encoding an empty render bundle.
+TEST_F(RenderBundleValidationTest, Empty) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+    pass.ExecuteBundles(1, &renderBundle);
+    pass.EndPass();
+    commandEncoder.Finish();
+}
+
+// Test executing zero render bundles.
+TEST_F(RenderBundleValidationTest, ZeroBundles) {
+    DummyRenderPass renderPass(device);
+
+    dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+    pass.ExecuteBundles(0, nullptr);
+    pass.EndPass();
+    commandEncoder.Finish();
+}
+
+// Test successfully creating and encoding a render bundle into a command buffer.
+TEST_F(RenderBundleValidationTest, SimpleSuccess) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+    renderBundleEncoder.SetPipeline(pipeline);
+    renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+    renderBundleEncoder.SetBindGroup(1, bg1, 0, nullptr);
+    renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+    renderBundleEncoder.Draw(3, 0, 0, 0);
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+    pass.ExecuteBundles(1, &renderBundle);
+    pass.EndPass();
+    commandEncoder.Finish();
+}
+
+// Test render bundles do not inherit command buffer state
+TEST_F(RenderBundleValidationTest, StateInheritance) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    // Render bundle does not inherit pipeline so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+        pass.SetPipeline(pipeline);
+
+        renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+        renderBundleEncoder.SetBindGroup(1, bg1, 0, nullptr);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        renderBundleEncoder.Draw(3, 0, 0, 0);
+        ASSERT_DEVICE_ERROR(dawn::RenderBundle renderBundle = renderBundleEncoder.Finish());
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle does not inherit bind groups so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        renderBundleEncoder.Draw(3, 0, 0, 0);
+        ASSERT_DEVICE_ERROR(dawn::RenderBundle renderBundle = renderBundleEncoder.Finish());
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle does not inherit pipeline and bind groups so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        renderBundleEncoder.Draw(3, 0, 0, 0);
+        ASSERT_DEVICE_ERROR(dawn::RenderBundle renderBundle = renderBundleEncoder.Finish());
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle does not inherit buffers so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+        renderBundleEncoder.SetBindGroup(1, bg1, 0, nullptr);
+        renderBundleEncoder.Draw(3, 0, 0, 0);
+        ASSERT_DEVICE_ERROR(dawn::RenderBundle renderBundle = renderBundleEncoder.Finish());
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+}
+
+// Test render bundles do not persist command buffer state
+TEST_F(RenderBundleValidationTest, StatePersistence) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    // Render bundle does not persist pipeline so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle does not persist bind groups so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+        renderBundleEncoder.SetBindGroup(1, bg1, 0, nullptr);
+        dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetPipeline(pipeline);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle does not persist pipeline and bind groups so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+        renderBundleEncoder.SetBindGroup(1, bg1, 0, nullptr);
+        dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle does not persist buffers so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+}
+
+// Test executing render bundles clears command buffer state
+TEST_F(RenderBundleValidationTest, ClearsState) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    // Render bundle clears pipeline so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        pass.SetPipeline(pipeline);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle clears bind groups so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetPipeline(pipeline);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle clears pipeline and bind groups so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Render bundle clears buffers so the draw is invalid.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.Draw(3, 0, 0, 0);
+        pass.EndPass();
+
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Test executing 0 bundles does not clear command buffer state.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.ExecuteBundles(0, nullptr);
+        pass.Draw(3, 0, 0, 0);
+
+        pass.EndPass();
+        commandEncoder.Finish();
+    }
+}
+
+// Test creating and encoding multiple render bundles.
+TEST_F(RenderBundleValidationTest, MultipleBundles) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    dawn::RenderBundle renderBundles[2] = {};
+
+    dawn::RenderBundleEncoder renderBundleEncoder0 = device.CreateRenderBundleEncoder(&desc);
+    renderBundleEncoder0.SetPipeline(pipeline);
+    renderBundleEncoder0.SetBindGroup(0, bg0, 0, nullptr);
+    renderBundleEncoder0.SetBindGroup(1, bg1, 0, nullptr);
+    renderBundleEncoder0.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+    renderBundleEncoder0.Draw(3, 1, 0, 0);
+    renderBundles[0] = renderBundleEncoder0.Finish();
+
+    dawn::RenderBundleEncoder renderBundleEncoder1 = device.CreateRenderBundleEncoder(&desc);
+    renderBundleEncoder1.SetPipeline(pipeline);
+    renderBundleEncoder1.SetBindGroup(0, bg0, 0, nullptr);
+    renderBundleEncoder1.SetBindGroup(1, bg1, 0, nullptr);
+    renderBundleEncoder1.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+    renderBundleEncoder1.Draw(3, 1, 0, 0);
+    renderBundles[1] = renderBundleEncoder1.Finish();
+
+    dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+    pass.ExecuteBundles(2, renderBundles);
+    pass.EndPass();
+    commandEncoder.Finish();
+}
+
+// Test that is is valid to execute a render bundle more than once.
+TEST_F(RenderBundleValidationTest, ExecuteMultipleTimes) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+    renderBundleEncoder.SetPipeline(pipeline);
+    renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+    renderBundleEncoder.SetBindGroup(1, bg1, 0, nullptr);
+    renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+    renderBundleEncoder.Draw(3, 1, 0, 0);
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+    pass.ExecuteBundles(1, &renderBundle);
+    pass.ExecuteBundles(1, &renderBundle);
+    pass.ExecuteBundles(1, &renderBundle);
+    pass.EndPass();
+    commandEncoder.Finish();
+}
+
+// Test that it is an error to call Finish() on a render bundle encoder twice.
+TEST_F(RenderBundleValidationTest, FinishTwice) {
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = dawn::TextureFormat::RGBA8Uint;
+
+    dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+    renderBundleEncoder.Finish();
+    ASSERT_DEVICE_ERROR(renderBundleEncoder.Finish());
+}
+
+// Test that it is invalid to create a render bundle with no texture formats
+TEST_F(RenderBundleValidationTest, RequiresAtLeastOneTextureFormat) {
+    // Test failure case.
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        ASSERT_DEVICE_ERROR(device.CreateRenderBundleEncoder(&desc));
+    }
+
+    // Test success with one color format.
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 1;
+        desc.cColorFormats[0] = dawn::TextureFormat::RGBA8Uint;
+        device.CreateRenderBundleEncoder(&desc);
+    }
+
+    // Test success with a depth stencil format.
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.cDepthStencilFormat = dawn::TextureFormat::Depth24PlusStencil8;
+        desc.depthStencilFormat = &desc.cDepthStencilFormat;
+        device.CreateRenderBundleEncoder(&desc);
+    }
+}
+
+// Test that resource usages are validated inside render bundles.
+TEST_F(RenderBundleValidationTest, UsageTracking) {
+    DummyRenderPass renderPass(device);
+
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.attachmentFormat;
+
+    dawn::RenderBundle renderBundle0;
+    dawn::RenderBundle renderBundle1;
+
+    // First base case is successful. |bg1Vertex| does not reference |vertexBuffer|.
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+        renderBundleEncoder.SetBindGroup(1, bg1Vertex, 0, nullptr);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        renderBundleEncoder.Draw(3, 0, 0, 0);
+        renderBundle0 = renderBundleEncoder.Finish();
+    }
+
+    // Second base case is successful. |bg1| does not reference |vertexStorageBuffer|
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+        renderBundleEncoder.SetBindGroup(1, bg1, 0, nullptr);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexStorageBuffer, &zeroOffset);
+        renderBundleEncoder.Draw(3, 0, 0, 0);
+        renderBundle1 = renderBundleEncoder.Finish();
+    }
+
+    // Test that a render bundle which sets a buffer as both vertex and storage is invalid.
+    // |bg1Vertex| references |vertexStorageBuffer|
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bg0, 0, nullptr);
+        renderBundleEncoder.SetBindGroup(1, bg1Vertex, 0, nullptr);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexStorageBuffer, &zeroOffset);
+        renderBundleEncoder.Draw(3, 0, 0, 0);
+        ASSERT_DEVICE_ERROR(renderBundleEncoder.Finish());
+    }
+
+    // When both render bundles are in the same pass, |vertexStorageBuffer| is used
+    // as both read and write usage. This is invalid.
+    // renderBundle0 uses |vertexStorageBuffer| as a storage buffer.
+    // renderBundle1 uses |vertexStorageBuffer| as a vertex buffer.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle0);
+        pass.ExecuteBundles(1, &renderBundle1);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // |vertexStorageBuffer| is used as both read and write usage. This is invalid.
+    // The render pass uses |vertexStorageBuffer| as a storage buffer.
+    // renderBundle1 uses |vertexStorageBuffer| as a vertex buffer.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1Vertex, 0, nullptr);
+        pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+
+        pass.ExecuteBundles(1, &renderBundle1);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // |vertexStorageBuffer| is used as both read and write usage. This is invalid.
+    // renderBundle0 uses |vertexStorageBuffer| as a storage buffer.
+    // The render pass uses |vertexStorageBuffer| as a vertex buffer.
+    {
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+
+        pass.ExecuteBundles(1, &renderBundle0);
+
+        pass.SetPipeline(pipeline);
+        pass.SetBindGroup(0, bg0, 0, nullptr);
+        pass.SetBindGroup(1, bg1, 0, nullptr);
+        pass.SetVertexBuffers(0, 1, &vertexStorageBuffer, &zeroOffset);
+        pass.Draw(3, 0, 0, 0);
+
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+}
+
+// Test that encoding SetPipline with an incompatible color format produces an error.
+TEST_F(RenderBundleValidationTest, PipelineColorFormatMismatch) {
+    utils::ComboRenderBundleEncoderDescriptor renderBundleDesc = {};
+    renderBundleDesc.colorFormatsCount = 3;
+    renderBundleDesc.cColorFormats[0] = dawn::TextureFormat::RGBA8Unorm;
+    renderBundleDesc.cColorFormats[1] = dawn::TextureFormat::RG16Float;
+    renderBundleDesc.cColorFormats[2] = dawn::TextureFormat::R16Sint;
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDesc = MakeRenderPipelineDescriptor();
+    renderPipelineDesc.colorStateCount = 3;
+    renderPipelineDesc.cColorStates[0]->format = dawn::TextureFormat::RGBA8Unorm;
+    renderPipelineDesc.cColorStates[1]->format = dawn::TextureFormat::RG16Float;
+    renderPipelineDesc.cColorStates[2]->format = dawn::TextureFormat::R16Sint;
+
+    // Test the success case.
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&renderPipelineDesc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.Finish();
+    }
+
+    // Test the failure case for mismatched format types.
+    {
+        utils::ComboRenderPipelineDescriptor desc = renderPipelineDesc;
+        desc.cColorStates[1]->format = dawn::TextureFormat::RGBA8Unorm;
+
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        ASSERT_DEVICE_ERROR(renderBundleEncoder.Finish());
+    }
+
+    // Test the failure case for missing format
+    {
+        utils::ComboRenderPipelineDescriptor desc = renderPipelineDesc;
+        desc.colorStateCount = 2;
+
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        ASSERT_DEVICE_ERROR(renderBundleEncoder.Finish());
+    }
+}
+
+// Test that encoding SetPipline with an incompatible depth stencil format produces an error.
+TEST_F(RenderBundleValidationTest, PipelineDepthStencilFormatMismatch) {
+    utils::ComboRenderBundleEncoderDescriptor renderBundleDesc = {};
+    renderBundleDesc.colorFormatsCount = 1;
+    renderBundleDesc.cColorFormats[0] = dawn::TextureFormat::RGBA8Unorm;
+    renderBundleDesc.cDepthStencilFormat = dawn::TextureFormat::Depth24PlusStencil8;
+    renderBundleDesc.depthStencilFormat = &renderBundleDesc.cDepthStencilFormat;
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDesc = MakeRenderPipelineDescriptor();
+    renderPipelineDesc.colorStateCount = 1;
+    renderPipelineDesc.cColorStates[0]->format = dawn::TextureFormat::RGBA8Unorm;
+    renderPipelineDesc.depthStencilState = &renderPipelineDesc.cDepthStencilState;
+    renderPipelineDesc.cDepthStencilState.format = dawn::TextureFormat::Depth24PlusStencil8;
+
+    // Test the success case.
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&renderPipelineDesc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.Finish();
+    }
+
+    // Test the failure case for mismatched format.
+    {
+        utils::ComboRenderPipelineDescriptor desc = renderPipelineDesc;
+        desc.cDepthStencilState.format = dawn::TextureFormat::Depth24Plus;
+        desc.depthStencilState = &desc.cDepthStencilState;
+
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        ASSERT_DEVICE_ERROR(renderBundleEncoder.Finish());
+    }
+
+    // Test the failure case for missing format.
+    {
+        utils::ComboRenderPipelineDescriptor desc = renderPipelineDesc;
+        desc.depthStencilState = nullptr;
+
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        ASSERT_DEVICE_ERROR(renderBundleEncoder.Finish());
+    }
+}
+
+// Test that encoding SetPipline with an incompatible sample count produces an error.
+TEST_F(RenderBundleValidationTest, PipelineSampleCountMismatch) {
+    utils::ComboRenderBundleEncoderDescriptor renderBundleDesc = {};
+    renderBundleDesc.colorFormatsCount = 1;
+    renderBundleDesc.cColorFormats[0] = dawn::TextureFormat::RGBA8Unorm;
+    renderBundleDesc.sampleCount = 4;
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDesc = MakeRenderPipelineDescriptor();
+    renderPipelineDesc.colorStateCount = 1;
+    renderPipelineDesc.cColorStates[0]->format = dawn::TextureFormat::RGBA8Unorm;
+    renderPipelineDesc.sampleCount = 4;
+
+    // Test the success case.
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&renderPipelineDesc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.Finish();
+    }
+
+    // Test the failure case.
+    {
+        renderPipelineDesc.sampleCount = 1;
+
+        dawn::RenderBundleEncoder renderBundleEncoder =
+            device.CreateRenderBundleEncoder(&renderBundleDesc);
+        dawn::RenderPipeline pipeline = device.CreateRenderPipeline(&renderPipelineDesc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        ASSERT_DEVICE_ERROR(renderBundleEncoder.Finish());
+    }
+}
+
+// Test that encoding ExecuteBundles with an incompatible color format produces an error.
+TEST_F(RenderBundleValidationTest, RenderPassColorFormatMismatch) {
+    utils::ComboRenderBundleEncoderDescriptor renderBundleDesc = {};
+    renderBundleDesc.colorFormatsCount = 3;
+    renderBundleDesc.cColorFormats[0] = dawn::TextureFormat::RGBA8Unorm;
+    renderBundleDesc.cColorFormats[1] = dawn::TextureFormat::RG16Float;
+    renderBundleDesc.cColorFormats[2] = dawn::TextureFormat::R16Sint;
+
+    dawn::RenderBundleEncoder renderBundleEncoder =
+        device.CreateRenderBundleEncoder(&renderBundleDesc);
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::TextureDescriptor textureDesc = {};
+    textureDesc.usage = dawn::TextureUsageBit::OutputAttachment;
+    textureDesc.size = dawn::Extent3D({400, 400, 1});
+
+    textureDesc.format = dawn::TextureFormat::RGBA8Unorm;
+    dawn::Texture tex0 = device.CreateTexture(&textureDesc);
+
+    textureDesc.format = dawn::TextureFormat::RG16Float;
+    dawn::Texture tex1 = device.CreateTexture(&textureDesc);
+
+    textureDesc.format = dawn::TextureFormat::R16Sint;
+    dawn::Texture tex2 = device.CreateTexture(&textureDesc);
+
+    // Test the success case
+    {
+        utils::ComboRenderPassDescriptor renderPass({
+            tex0.CreateDefaultView(),
+            tex1.CreateDefaultView(),
+            tex2.CreateDefaultView(),
+        });
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        commandEncoder.Finish();
+    }
+
+    // Test the failure case for mismatched format
+    {
+        utils::ComboRenderPassDescriptor renderPass({
+            tex0.CreateDefaultView(),
+            tex1.CreateDefaultView(),
+            tex0.CreateDefaultView(),
+        });
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Test the failure case for missing format
+    {
+        utils::ComboRenderPassDescriptor renderPass({
+            tex0.CreateDefaultView(),
+            tex1.CreateDefaultView(),
+        });
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+}
+
+// Test that encoding ExecuteBundles with an incompatible depth stencil format produces an
+// error.
+TEST_F(RenderBundleValidationTest, RenderPassDepthStencilFormatMismatch) {
+    utils::ComboRenderBundleEncoderDescriptor renderBundleDesc = {};
+    renderBundleDesc.colorFormatsCount = 1;
+    renderBundleDesc.cColorFormats[0] = dawn::TextureFormat::RGBA8Unorm;
+    renderBundleDesc.cDepthStencilFormat = dawn::TextureFormat::Depth24Plus;
+    renderBundleDesc.depthStencilFormat = &renderBundleDesc.cDepthStencilFormat;
+
+    dawn::RenderBundleEncoder renderBundleEncoder =
+        device.CreateRenderBundleEncoder(&renderBundleDesc);
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::TextureDescriptor textureDesc = {};
+    textureDesc.usage = dawn::TextureUsageBit::OutputAttachment;
+    textureDesc.size = dawn::Extent3D({400, 400, 1});
+
+    textureDesc.format = dawn::TextureFormat::RGBA8Unorm;
+    dawn::Texture tex0 = device.CreateTexture(&textureDesc);
+
+    textureDesc.format = dawn::TextureFormat::Depth24Plus;
+    dawn::Texture tex1 = device.CreateTexture(&textureDesc);
+
+    textureDesc.format = dawn::TextureFormat::Depth32Float;
+    dawn::Texture tex2 = device.CreateTexture(&textureDesc);
+
+    // Test the success case
+    {
+        utils::ComboRenderPassDescriptor renderPass({tex0.CreateDefaultView()},
+                                                    tex1.CreateDefaultView());
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        commandEncoder.Finish();
+    }
+
+    // Test the failure case for mismatched format
+    {
+        utils::ComboRenderPassDescriptor renderPass({tex0.CreateDefaultView()},
+                                                    tex2.CreateDefaultView());
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+
+    // Test the failure case for missing format
+    {
+        utils::ComboRenderPassDescriptor renderPass({tex0.CreateDefaultView()});
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+}
+
+// Test that encoding ExecuteBundles with an incompatible sample count produces an error.
+TEST_F(RenderBundleValidationTest, RenderPassSampleCountMismatch) {
+    utils::ComboRenderBundleEncoderDescriptor renderBundleDesc = {};
+    renderBundleDesc.colorFormatsCount = 1;
+    renderBundleDesc.cColorFormats[0] = dawn::TextureFormat::RGBA8Unorm;
+
+    dawn::RenderBundleEncoder renderBundleEncoder =
+        device.CreateRenderBundleEncoder(&renderBundleDesc);
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::TextureDescriptor textureDesc = {};
+    textureDesc.usage = dawn::TextureUsageBit::OutputAttachment;
+    textureDesc.size = dawn::Extent3D({400, 400, 1});
+
+    textureDesc.format = dawn::TextureFormat::RGBA8Unorm;
+    dawn::Texture tex0 = device.CreateTexture(&textureDesc);
+
+    textureDesc.sampleCount = 4;
+    dawn::Texture tex1 = device.CreateTexture(&textureDesc);
+
+    // Test the success case
+    {
+        utils::ComboRenderPassDescriptor renderPass({tex0.CreateDefaultView()});
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        commandEncoder.Finish();
+    }
+
+    // Test the failure case
+    {
+        utils::ComboRenderPassDescriptor renderPass({tex1.CreateDefaultView()});
+
+        dawn::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+        dawn::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&renderPass);
+        pass.ExecuteBundles(1, &renderBundle);
+        pass.EndPass();
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish());
+    }
+}
diff --git a/src/utils/ComboRenderBundleEncoderDescriptor.cpp b/src/utils/ComboRenderBundleEncoderDescriptor.cpp
new file mode 100644
index 0000000..86e1f26
--- /dev/null
+++ b/src/utils/ComboRenderBundleEncoderDescriptor.cpp
@@ -0,0 +1,30 @@
+// Copyright 2019 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 "utils/ComboRenderBundleEncoderDescriptor.h"
+
+#include "utils/DawnHelpers.h"
+
+namespace utils {
+
+    ComboRenderBundleEncoderDescriptor::ComboRenderBundleEncoderDescriptor() {
+        dawn::RenderBundleEncoderDescriptor* descriptor = this;
+
+        descriptor->sampleCount = 1;
+        descriptor->depthStencilFormat = nullptr;
+        descriptor->colorFormatsCount = 0;
+        descriptor->colorFormats = &cColorFormats[0];
+    }
+
+}  // namespace utils
diff --git a/src/utils/ComboRenderBundleEncoderDescriptor.h b/src/utils/ComboRenderBundleEncoderDescriptor.h
new file mode 100644
index 0000000..e829693
--- /dev/null
+++ b/src/utils/ComboRenderBundleEncoderDescriptor.h
@@ -0,0 +1,36 @@
+// Copyright 2019 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 UTILS_COMBORENDERBUNDLEENCODERDESCRIPTOR_H_
+#define UTILS_COMBORENDERBUNDLEENCODERDESCRIPTOR_H_
+
+#include <dawn/dawncpp.h>
+
+#include "common/Constants.h"
+
+#include <array>
+
+namespace utils {
+
+    class ComboRenderBundleEncoderDescriptor : public dawn::RenderBundleEncoderDescriptor {
+      public:
+        ComboRenderBundleEncoderDescriptor();
+
+        std::array<dawn::TextureFormat, kMaxColorAttachments> cColorFormats;
+        dawn::TextureFormat cDepthStencilFormat;
+    };
+
+}  // namespace utils
+
+#endif  // UTILS_COMBORENDERBUNDLEENCODERDESCRIPTOR_H_