Implement RenderBundle in the backend

Bug: dawn:154
Change-Id: I45496fb2103150dabe32fbc7cb5856dc40c9339f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/9222
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Commit-Queue: Austin Eng <enga@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 492ea22..fe9b67c 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -769,6 +769,7 @@
     "src/tests/end2end/ObjectCachingTests.cpp",
     "src/tests/end2end/OpArrayLengthTests.cpp",
     "src/tests/end2end/PrimitiveTopologyTests.cpp",
+    "src/tests/end2end/RenderBundleTests.cpp",
     "src/tests/end2end/RenderPassLoadOpTests.cpp",
     "src/tests/end2end/RenderPassTests.cpp",
     "src/tests/end2end/SamplerTests.cpp",
diff --git a/src/dawn_native/d3d12/CommandBufferD3D12.cpp b/src/dawn_native/d3d12/CommandBufferD3D12.cpp
index 095b410..150dd63 100644
--- a/src/dawn_native/d3d12/CommandBufferD3D12.cpp
+++ b/src/dawn_native/d3d12/CommandBufferD3D12.cpp
@@ -17,6 +17,7 @@
 #include "common/Assert.h"
 #include "dawn_native/CommandEncoder.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/RenderBundle.h"
 #include "dawn_native/d3d12/BindGroupD3D12.h"
 #include "dawn_native/d3d12/BindGroupLayoutD3D12.h"
 #include "dawn_native/d3d12/BufferD3D12.h"
@@ -398,7 +399,7 @@
                 Command type;
                 PipelineLayout* lastLayout = nullptr;
 
-                while (commands->NextCommandId(&type)) {
+                auto HandleCommand = [&](CommandIterator* commands, Command type) {
                     switch (type) {
                         case Command::SetComputePipeline: {
                             SetComputePipelineCmd* cmd =
@@ -431,6 +432,26 @@
                         default:
                             SkipCommand(commands, type);
                     }
+                };
+
+                while (commands->NextCommandId(&type)) {
+                    switch (type) {
+                        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) {
+                                CommandIterator* commands = bundles[i]->GetCommands();
+                                commands->Reset();
+                                while (commands->NextCommandId(&type)) {
+                                    HandleCommand(commands, type);
+                                }
+                            }
+                        } break;
+                        default:
+                            HandleCommand(commands, type);
+                            break;
+                    }
                 }
 
                 commands->Reset();
@@ -987,6 +1008,155 @@
         PipelineLayout* lastLayout = nullptr;
         VertexBuffersInfo vertexBuffersInfo = {};
 
+        auto EncodeRenderBundleCommand = [&](CommandIterator* iter, Command type) {
+            switch (type) {
+                case Command::Draw: {
+                    DrawCmd* draw = iter->NextCommand<DrawCmd>();
+
+                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
+                    commandList->DrawInstanced(draw->vertexCount, draw->instanceCount,
+                                               draw->firstVertex, draw->firstInstance);
+                } break;
+
+                case Command::DrawIndexed: {
+                    DrawIndexedCmd* draw = iter->NextCommand<DrawIndexedCmd>();
+
+                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
+                    commandList->DrawIndexedInstanced(draw->indexCount, draw->instanceCount,
+                                                      draw->firstIndex, draw->baseVertex,
+                                                      draw->firstInstance);
+                } break;
+
+                case Command::DrawIndirect: {
+                    DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>();
+
+                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
+                    Buffer* buffer = ToBackend(draw->indirectBuffer.Get());
+                    ComPtr<ID3D12CommandSignature> signature =
+                        ToBackend(GetDevice())->GetDrawIndirectSignature();
+                    commandList->ExecuteIndirect(signature.Get(), 1,
+                                                 buffer->GetD3D12Resource().Get(),
+                                                 draw->indirectOffset, nullptr, 0);
+                } break;
+
+                case Command::DrawIndexedIndirect: {
+                    DrawIndexedIndirectCmd* draw = iter->NextCommand<DrawIndexedIndirectCmd>();
+
+                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
+                    Buffer* buffer = ToBackend(draw->indirectBuffer.Get());
+                    ComPtr<ID3D12CommandSignature> signature =
+                        ToBackend(GetDevice())->GetDrawIndexedIndirectSignature();
+                    commandList->ExecuteIndirect(signature.Get(), 1,
+                                                 buffer->GetD3D12Resource().Get(),
+                                                 draw->indirectOffset, nullptr, 0);
+                } break;
+
+                case Command::InsertDebugMarker: {
+                    InsertDebugMarkerCmd* cmd = iter->NextCommand<InsertDebugMarkerCmd>();
+                    const char* label = iter->NextData<char>(cmd->length + 1);
+
+                    if (ToBackend(GetDevice())->GetFunctions()->IsPIXEventRuntimeLoaded()) {
+                        // PIX color is 1 byte per channel in ARGB format
+                        constexpr uint64_t kPIXBlackColor = 0xff000000;
+                        ToBackend(GetDevice())
+                            ->GetFunctions()
+                            ->pixSetMarkerOnCommandList(commandList.Get(), kPIXBlackColor, label);
+                    }
+                } break;
+
+                case Command::PopDebugGroup: {
+                    iter->NextCommand<PopDebugGroupCmd>();
+
+                    if (ToBackend(GetDevice())->GetFunctions()->IsPIXEventRuntimeLoaded()) {
+                        ToBackend(GetDevice())
+                            ->GetFunctions()
+                            ->pixEndEventOnCommandList(commandList.Get());
+                    }
+                } break;
+
+                case Command::PushDebugGroup: {
+                    PushDebugGroupCmd* cmd = iter->NextCommand<PushDebugGroupCmd>();
+                    const char* label = iter->NextData<char>(cmd->length + 1);
+
+                    if (ToBackend(GetDevice())->GetFunctions()->IsPIXEventRuntimeLoaded()) {
+                        // PIX color is 1 byte per channel in ARGB format
+                        constexpr uint64_t kPIXBlackColor = 0xff000000;
+                        ToBackend(GetDevice())
+                            ->GetFunctions()
+                            ->pixBeginEventOnCommandList(commandList.Get(), kPIXBlackColor, label);
+                    }
+                } break;
+
+                case Command::SetRenderPipeline: {
+                    SetRenderPipelineCmd* cmd = iter->NextCommand<SetRenderPipelineCmd>();
+                    RenderPipeline* pipeline = ToBackend(cmd->pipeline).Get();
+                    PipelineLayout* layout = ToBackend(pipeline->GetLayout());
+
+                    commandList->SetGraphicsRootSignature(layout->GetRootSignature().Get());
+                    commandList->SetPipelineState(pipeline->GetPipelineState().Get());
+                    commandList->IASetPrimitiveTopology(pipeline->GetD3D12PrimitiveTopology());
+
+                    bindingTracker->SetInheritedBindGroups(commandList, lastLayout, layout);
+
+                    lastPipeline = pipeline;
+                    lastLayout = layout;
+                } break;
+
+                case Command::SetBindGroup: {
+                    SetBindGroupCmd* cmd = iter->NextCommand<SetBindGroupCmd>();
+                    BindGroup* group = ToBackend(cmd->group.Get());
+                    uint64_t* dynamicOffsets = nullptr;
+
+                    if (cmd->dynamicOffsetCount > 0) {
+                        dynamicOffsets = iter->NextData<uint64_t>(cmd->dynamicOffsetCount);
+                    }
+
+                    bindingTracker->SetBindGroup(commandList, lastLayout, group, cmd->index,
+                                                 cmd->dynamicOffsetCount, dynamicOffsets);
+                } break;
+
+                case Command::SetIndexBuffer: {
+                    SetIndexBufferCmd* cmd = iter->NextCommand<SetIndexBufferCmd>();
+
+                    Buffer* buffer = ToBackend(cmd->buffer.Get());
+                    D3D12_INDEX_BUFFER_VIEW bufferView;
+                    bufferView.BufferLocation = buffer->GetVA() + cmd->offset;
+                    bufferView.SizeInBytes = buffer->GetSize() - cmd->offset;
+                    // TODO(cwallez@chromium.org): Make index buffers lazily applied, right now
+                    // this will break if the pipeline is changed for one with a different index
+                    // format after SetIndexBuffer
+                    bufferView.Format =
+                        DXGIIndexFormat(lastPipeline->GetVertexInputDescriptor()->indexFormat);
+
+                    commandList->IASetIndexBuffer(&bufferView);
+                } break;
+
+                case Command::SetVertexBuffers: {
+                    SetVertexBuffersCmd* cmd = iter->NextCommand<SetVertexBuffersCmd>();
+                    auto buffers = iter->NextData<Ref<BufferBase>>(cmd->count);
+                    auto offsets = iter->NextData<uint64_t>(cmd->count);
+
+                    vertexBuffersInfo.startSlot =
+                        std::min(vertexBuffersInfo.startSlot, cmd->startSlot);
+                    vertexBuffersInfo.endSlot =
+                        std::max(vertexBuffersInfo.endSlot, cmd->startSlot + cmd->count);
+
+                    for (uint32_t i = 0; i < cmd->count; ++i) {
+                        Buffer* buffer = ToBackend(buffers[i].Get());
+                        auto* d3d12BufferView =
+                            &vertexBuffersInfo.d3d12BufferViews[cmd->startSlot + i];
+                        d3d12BufferView->BufferLocation = buffer->GetVA() + offsets[i];
+                        d3d12BufferView->SizeInBytes = buffer->GetSize() - offsets[i];
+                        // The bufferView stride is set based on the input state before a draw.
+                    }
+                } break;
+
+                default:
+                    UNREACHABLE();
+                    break;
+            }
+        };
+
         Command type;
         while (mCommands.NextCommandId(&type)) {
             switch (type) {
@@ -1001,98 +1171,6 @@
                     return;
                 } break;
 
-                case Command::Draw: {
-                    DrawCmd* draw = mCommands.NextCommand<DrawCmd>();
-
-                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
-                    commandList->DrawInstanced(draw->vertexCount, draw->instanceCount,
-                                               draw->firstVertex, draw->firstInstance);
-                } break;
-
-                case Command::DrawIndexed: {
-                    DrawIndexedCmd* draw = mCommands.NextCommand<DrawIndexedCmd>();
-
-                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
-                    commandList->DrawIndexedInstanced(draw->indexCount, draw->instanceCount,
-                                                      draw->firstIndex, draw->baseVertex,
-                                                      draw->firstInstance);
-                } break;
-
-                case Command::DrawIndirect: {
-                    DrawIndirectCmd* draw = mCommands.NextCommand<DrawIndirectCmd>();
-
-                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
-                    Buffer* buffer = ToBackend(draw->indirectBuffer.Get());
-                    ComPtr<ID3D12CommandSignature> signature =
-                        ToBackend(GetDevice())->GetDrawIndirectSignature();
-                    commandList->ExecuteIndirect(signature.Get(), 1,
-                                                 buffer->GetD3D12Resource().Get(),
-                                                 draw->indirectOffset, nullptr, 0);
-                } break;
-
-                case Command::DrawIndexedIndirect: {
-                    DrawIndexedIndirectCmd* draw = mCommands.NextCommand<DrawIndexedIndirectCmd>();
-
-                    FlushSetVertexBuffers(commandList, &vertexBuffersInfo, lastPipeline);
-                    Buffer* buffer = ToBackend(draw->indirectBuffer.Get());
-                    ComPtr<ID3D12CommandSignature> signature =
-                        ToBackend(GetDevice())->GetDrawIndexedIndirectSignature();
-                    commandList->ExecuteIndirect(signature.Get(), 1,
-                                                 buffer->GetD3D12Resource().Get(),
-                                                 draw->indirectOffset, nullptr, 0);
-                } break;
-
-                case Command::InsertDebugMarker: {
-                    InsertDebugMarkerCmd* cmd = mCommands.NextCommand<InsertDebugMarkerCmd>();
-                    const char* label = mCommands.NextData<char>(cmd->length + 1);
-
-                    if (ToBackend(GetDevice())->GetFunctions()->IsPIXEventRuntimeLoaded()) {
-                        // PIX color is 1 byte per channel in ARGB format
-                        constexpr uint64_t kPIXBlackColor = 0xff000000;
-                        ToBackend(GetDevice())
-                            ->GetFunctions()
-                            ->pixSetMarkerOnCommandList(commandList.Get(), kPIXBlackColor, label);
-                    }
-                } break;
-
-                case Command::PopDebugGroup: {
-                    mCommands.NextCommand<PopDebugGroupCmd>();
-
-                    if (ToBackend(GetDevice())->GetFunctions()->IsPIXEventRuntimeLoaded()) {
-                        ToBackend(GetDevice())
-                            ->GetFunctions()
-                            ->pixEndEventOnCommandList(commandList.Get());
-                    }
-                } break;
-
-                case Command::PushDebugGroup: {
-                    PushDebugGroupCmd* cmd = mCommands.NextCommand<PushDebugGroupCmd>();
-                    const char* label = mCommands.NextData<char>(cmd->length + 1);
-
-                    if (ToBackend(GetDevice())->GetFunctions()->IsPIXEventRuntimeLoaded()) {
-                        // PIX color is 1 byte per channel in ARGB format
-                        constexpr uint64_t kPIXBlackColor = 0xff000000;
-                        ToBackend(GetDevice())
-                            ->GetFunctions()
-                            ->pixBeginEventOnCommandList(commandList.Get(), kPIXBlackColor, label);
-                    }
-                } break;
-
-                case Command::SetRenderPipeline: {
-                    SetRenderPipelineCmd* cmd = mCommands.NextCommand<SetRenderPipelineCmd>();
-                    RenderPipeline* pipeline = ToBackend(cmd->pipeline).Get();
-                    PipelineLayout* layout = ToBackend(pipeline->GetLayout());
-
-                    commandList->SetGraphicsRootSignature(layout->GetRootSignature().Get());
-                    commandList->SetPipelineState(pipeline->GetPipelineState().Get());
-                    commandList->IASetPrimitiveTopology(pipeline->GetD3D12PrimitiveTopology());
-
-                    bindingTracker->SetInheritedBindGroups(commandList, lastLayout, layout);
-
-                    lastPipeline = pipeline;
-                    lastLayout = layout;
-                } break;
-
                 case Command::SetStencilReference: {
                     SetStencilReferenceCmd* cmd = mCommands.NextCommand<SetStencilReferenceCmd>();
 
@@ -1128,56 +1206,20 @@
                     commandList->OMSetBlendFactor(static_cast<const FLOAT*>(&cmd->color.r));
                 } break;
 
-                case Command::SetBindGroup: {
-                    SetBindGroupCmd* cmd = mCommands.NextCommand<SetBindGroupCmd>();
-                    BindGroup* group = ToBackend(cmd->group.Get());
-                    uint64_t* dynamicOffsets = nullptr;
-
-                    if (cmd->dynamicOffsetCount > 0) {
-                        dynamicOffsets = mCommands.NextData<uint64_t>(cmd->dynamicOffsetCount);
-                    }
-
-                    bindingTracker->SetBindGroup(commandList, lastLayout, group, cmd->index,
-                                                 cmd->dynamicOffsetCount, dynamicOffsets);
-                } break;
-
-                case Command::SetIndexBuffer: {
-                    SetIndexBufferCmd* cmd = mCommands.NextCommand<SetIndexBufferCmd>();
-
-                    Buffer* buffer = ToBackend(cmd->buffer.Get());
-                    D3D12_INDEX_BUFFER_VIEW bufferView;
-                    bufferView.BufferLocation = buffer->GetVA() + cmd->offset;
-                    bufferView.SizeInBytes = buffer->GetSize() - cmd->offset;
-                    // TODO(cwallez@chromium.org): Make index buffers lazily applied, right now
-                    // this will break if the pipeline is changed for one with a different index
-                    // format after SetIndexBuffer
-                    bufferView.Format =
-                        DXGIIndexFormat(lastPipeline->GetVertexInputDescriptor()->indexFormat);
-
-                    commandList->IASetIndexBuffer(&bufferView);
-                } break;
-
-                case Command::SetVertexBuffers: {
-                    SetVertexBuffersCmd* cmd = mCommands.NextCommand<SetVertexBuffersCmd>();
-                    auto buffers = mCommands.NextData<Ref<BufferBase>>(cmd->count);
-                    auto offsets = mCommands.NextData<uint64_t>(cmd->count);
-
-                    vertexBuffersInfo.startSlot =
-                        std::min(vertexBuffersInfo.startSlot, cmd->startSlot);
-                    vertexBuffersInfo.endSlot =
-                        std::max(vertexBuffersInfo.endSlot, cmd->startSlot + cmd->count);
+                case Command::ExecuteBundles: {
+                    ExecuteBundlesCmd* cmd = mCommands.NextCommand<ExecuteBundlesCmd>();
+                    auto bundles = mCommands.NextData<Ref<RenderBundleBase>>(cmd->count);
 
                     for (uint32_t i = 0; i < cmd->count; ++i) {
-                        Buffer* buffer = ToBackend(buffers[i].Get());
-                        auto* d3d12BufferView =
-                            &vertexBuffersInfo.d3d12BufferViews[cmd->startSlot + i];
-                        d3d12BufferView->BufferLocation = buffer->GetVA() + offsets[i];
-                        d3d12BufferView->SizeInBytes = buffer->GetSize() - offsets[i];
-                        // The bufferView stride is set based on the input state before a draw.
+                        CommandIterator* iter = bundles[i]->GetCommands();
+                        iter->Reset();
+                        while (iter->NextCommandId(&type)) {
+                            EncodeRenderBundleCommand(iter, type);
+                        }
                     }
                 } break;
 
-                default: { UNREACHABLE(); } break;
+                default: { EncodeRenderBundleCommand(&mCommands, type); } break;
             }
         }
     }
diff --git a/src/dawn_native/metal/CommandBufferMTL.mm b/src/dawn_native/metal/CommandBufferMTL.mm
index a096073..3c79768 100644
--- a/src/dawn_native/metal/CommandBufferMTL.mm
+++ b/src/dawn_native/metal/CommandBufferMTL.mm
@@ -17,6 +17,7 @@
 #include "dawn_native/BindGroup.h"
 #include "dawn_native/CommandEncoder.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/RenderBundle.h"
 #include "dawn_native/metal/BufferMTL.h"
 #include "dawn_native/metal/ComputePipelineMTL.h"
 #include "dawn_native/metal/DeviceMTL.h"
@@ -871,17 +872,10 @@
         id<MTLRenderCommandEncoder> encoder =
             [commandBuffer renderCommandEncoderWithDescriptor:mtlRenderPass];
 
-        Command type;
-        while (mCommands.NextCommandId(&type)) {
+        auto EncodeRenderBundleCommand = [&](CommandIterator* iter, Command type) {
             switch (type) {
-                case Command::EndRenderPass: {
-                    mCommands.NextCommand<EndRenderPassCmd>();
-                    [encoder endEncoding];
-                    return;
-                } break;
-
                 case Command::Draw: {
-                    DrawCmd* draw = mCommands.NextCommand<DrawCmd>();
+                    DrawCmd* draw = iter->NextCommand<DrawCmd>();
 
                     vertexInputBuffers.Apply(encoder, lastPipeline);
                     storageBufferLengths.Apply(lastPipeline, encoder);
@@ -897,7 +891,7 @@
                 } break;
 
                 case Command::DrawIndexed: {
-                    DrawIndexedCmd* draw = mCommands.NextCommand<DrawIndexedCmd>();
+                    DrawIndexedCmd* draw = iter->NextCommand<DrawIndexedCmd>();
                     size_t formatSize =
                         IndexFormatSize(lastPipeline->GetVertexInputDescriptor()->indexFormat);
 
@@ -919,7 +913,7 @@
                 } break;
 
                 case Command::DrawIndirect: {
-                    DrawIndirectCmd* draw = mCommands.NextCommand<DrawIndirectCmd>();
+                    DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>();
 
                     vertexInputBuffers.Apply(encoder, lastPipeline);
                     storageBufferLengths.Apply(lastPipeline, encoder);
@@ -932,7 +926,7 @@
                 } break;
 
                 case Command::DrawIndexedIndirect: {
-                    DrawIndirectCmd* draw = mCommands.NextCommand<DrawIndirectCmd>();
+                    DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>();
 
                     vertexInputBuffers.Apply(encoder, lastPipeline);
                     storageBufferLengths.Apply(lastPipeline, encoder);
@@ -948,8 +942,8 @@
                 } break;
 
                 case Command::InsertDebugMarker: {
-                    InsertDebugMarkerCmd* cmd = mCommands.NextCommand<InsertDebugMarkerCmd>();
-                    char* label = mCommands.NextData<char>(cmd->length + 1);
+                    InsertDebugMarkerCmd* cmd = iter->NextCommand<InsertDebugMarkerCmd>();
+                    char* label = iter->NextData<char>(cmd->length + 1);
                     NSString* mtlLabel = [[NSString alloc] initWithUTF8String:label];
 
                     [encoder insertDebugSignpost:mtlLabel];
@@ -957,14 +951,14 @@
                 } break;
 
                 case Command::PopDebugGroup: {
-                    mCommands.NextCommand<PopDebugGroupCmd>();
+                    iter->NextCommand<PopDebugGroupCmd>();
 
                     [encoder popDebugGroup];
                 } break;
 
                 case Command::PushDebugGroup: {
-                    PushDebugGroupCmd* cmd = mCommands.NextCommand<PushDebugGroupCmd>();
-                    char* label = mCommands.NextData<char>(cmd->length + 1);
+                    PushDebugGroupCmd* cmd = iter->NextCommand<PushDebugGroupCmd>();
+                    char* label = iter->NextData<char>(cmd->length + 1);
                     NSString* mtlLabel = [[NSString alloc] initWithUTF8String:label];
 
                     [encoder pushDebugGroup:mtlLabel];
@@ -972,7 +966,7 @@
                 } break;
 
                 case Command::SetRenderPipeline: {
-                    SetRenderPipelineCmd* cmd = mCommands.NextCommand<SetRenderPipelineCmd>();
+                    SetRenderPipelineCmd* cmd = iter->NextCommand<SetRenderPipelineCmd>();
                     RenderPipeline* newPipeline = ToBackend(cmd->pipeline).Get();
 
                     vertexInputBuffers.OnSetPipeline(lastPipeline, newPipeline);
@@ -984,6 +978,49 @@
                     lastPipeline = newPipeline;
                 } break;
 
+                case Command::SetBindGroup: {
+                    SetBindGroupCmd* cmd = iter->NextCommand<SetBindGroupCmd>();
+                    uint64_t* dynamicOffsets = nullptr;
+                    if (cmd->dynamicOffsetCount > 0) {
+                        dynamicOffsets = iter->NextData<uint64_t>(cmd->dynamicOffsetCount);
+                    }
+
+                    ApplyBindGroup(cmd->index, ToBackend(cmd->group.Get()), cmd->dynamicOffsetCount,
+                                   dynamicOffsets, ToBackend(lastPipeline->GetLayout()),
+                                   &storageBufferLengths, encoder, nil);
+                } break;
+
+                case Command::SetIndexBuffer: {
+                    SetIndexBufferCmd* cmd = iter->NextCommand<SetIndexBufferCmd>();
+                    auto b = ToBackend(cmd->buffer.Get());
+                    indexBuffer = b->GetMTLBuffer();
+                    indexBufferBaseOffset = cmd->offset;
+                } break;
+
+                case Command::SetVertexBuffers: {
+                    SetVertexBuffersCmd* cmd = iter->NextCommand<SetVertexBuffersCmd>();
+                    const Ref<BufferBase>* buffers = iter->NextData<Ref<BufferBase>>(cmd->count);
+                    const uint64_t* offsets = iter->NextData<uint64_t>(cmd->count);
+
+                    vertexInputBuffers.OnSetVertexBuffers(cmd->startSlot, cmd->count, buffers,
+                                                          offsets);
+                } break;
+
+                default:
+                    UNREACHABLE();
+                    break;
+            }
+        };
+
+        Command type;
+        while (mCommands.NextCommandId(&type)) {
+            switch (type) {
+                case Command::EndRenderPass: {
+                    mCommands.NextCommand<EndRenderPassCmd>();
+                    [encoder endEncoding];
+                    return;
+                } break;
+
                 case Command::SetStencilReference: {
                     SetStencilReferenceCmd* cmd = mCommands.NextCommand<SetStencilReferenceCmd>();
                     [encoder setStencilReferenceValue:cmd->reference];
@@ -1030,36 +1067,20 @@
                                         alpha:cmd->color.a];
                 } break;
 
-                case Command::SetBindGroup: {
-                    SetBindGroupCmd* cmd = mCommands.NextCommand<SetBindGroupCmd>();
-                    uint64_t* dynamicOffsets = nullptr;
-                    if (cmd->dynamicOffsetCount > 0) {
-                        dynamicOffsets = mCommands.NextData<uint64_t>(cmd->dynamicOffsetCount);
+                case Command::ExecuteBundles: {
+                    ExecuteBundlesCmd* cmd = mCommands.NextCommand<ExecuteBundlesCmd>();
+                    auto bundles = mCommands.NextData<Ref<RenderBundleBase>>(cmd->count);
+
+                    for (uint32_t i = 0; i < cmd->count; ++i) {
+                        CommandIterator* iter = bundles[i]->GetCommands();
+                        iter->Reset();
+                        while (iter->NextCommandId(&type)) {
+                            EncodeRenderBundleCommand(iter, type);
+                        }
                     }
-
-                    ApplyBindGroup(cmd->index, ToBackend(cmd->group.Get()), cmd->dynamicOffsetCount,
-                                   dynamicOffsets, ToBackend(lastPipeline->GetLayout()),
-                                   &storageBufferLengths, encoder, nil);
                 } break;
 
-                case Command::SetIndexBuffer: {
-                    SetIndexBufferCmd* cmd = mCommands.NextCommand<SetIndexBufferCmd>();
-                    auto b = ToBackend(cmd->buffer.Get());
-                    indexBuffer = b->GetMTLBuffer();
-                    indexBufferBaseOffset = cmd->offset;
-                } break;
-
-                case Command::SetVertexBuffers: {
-                    SetVertexBuffersCmd* cmd = mCommands.NextCommand<SetVertexBuffersCmd>();
-                    const Ref<BufferBase>* buffers =
-                        mCommands.NextData<Ref<BufferBase>>(cmd->count);
-                    const uint64_t* offsets = mCommands.NextData<uint64_t>(cmd->count);
-
-                    vertexInputBuffers.OnSetVertexBuffers(cmd->startSlot, cmd->count, buffers,
-                                                          offsets);
-                } break;
-
-                default: { UNREACHABLE(); } break;
+                default: { EncodeRenderBundleCommand(&mCommands, type); } break;
             }
         }
 
diff --git a/src/dawn_native/opengl/CommandBufferGL.cpp b/src/dawn_native/opengl/CommandBufferGL.cpp
index 32ee0bf..9fafb6f 100644
--- a/src/dawn_native/opengl/CommandBufferGL.cpp
+++ b/src/dawn_native/opengl/CommandBufferGL.cpp
@@ -17,6 +17,7 @@
 #include "dawn_native/BindGroup.h"
 #include "dawn_native/CommandEncoder.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/RenderBundle.h"
 #include "dawn_native/opengl/BufferGL.h"
 #include "dawn_native/opengl/ComputePipelineGL.h"
 #include "dawn_native/opengl/DeviceGL.h"
@@ -735,21 +736,10 @@
 
         InputBufferTracker inputBuffers;
 
-        Command type;
-        while (mCommands.NextCommandId(&type)) {
+        auto DoRenderBundleCommand = [&](CommandIterator* iter, Command type) {
             switch (type) {
-                case Command::EndRenderPass: {
-                    mCommands.NextCommand<EndRenderPassCmd>();
-
-                    if (renderPass->attachmentState->GetSampleCount() > 1) {
-                        ResolveMultisampledRenderTargets(gl, renderPass);
-                    }
-                    gl.DeleteFramebuffers(1, &fbo);
-                    return;
-                } break;
-
                 case Command::Draw: {
-                    DrawCmd* draw = mCommands.NextCommand<DrawCmd>();
+                    DrawCmd* draw = iter->NextCommand<DrawCmd>();
                     inputBuffers.Apply(gl);
 
                     if (draw->firstInstance > 0) {
@@ -765,7 +755,7 @@
                 } break;
 
                 case Command::DrawIndexed: {
-                    DrawIndexedCmd* draw = mCommands.NextCommand<DrawIndexedCmd>();
+                    DrawIndexedCmd* draw = iter->NextCommand<DrawIndexedCmd>();
                     inputBuffers.Apply(gl);
 
                     dawn::IndexFormat indexFormat =
@@ -790,7 +780,7 @@
                 } break;
 
                 case Command::DrawIndirect: {
-                    DrawIndirectCmd* draw = mCommands.NextCommand<DrawIndirectCmd>();
+                    DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>();
                     inputBuffers.Apply(gl);
 
                     uint64_t indirectBufferOffset = draw->indirectOffset;
@@ -803,7 +793,7 @@
                 } break;
 
                 case Command::DrawIndexedIndirect: {
-                    DrawIndexedIndirectCmd* draw = mCommands.NextCommand<DrawIndexedIndirectCmd>();
+                    DrawIndexedIndirectCmd* draw = iter->NextCommand<DrawIndexedIndirectCmd>();
                     inputBuffers.Apply(gl);
 
                     dawn::IndexFormat indexFormat =
@@ -824,17 +814,60 @@
                 case Command::PushDebugGroup: {
                     // Due to lack of linux driver support for GL_EXT_debug_marker
                     // extension these functions are skipped.
-                    SkipCommand(&mCommands, type);
+                    SkipCommand(iter, type);
                 } break;
 
                 case Command::SetRenderPipeline: {
-                    SetRenderPipelineCmd* cmd = mCommands.NextCommand<SetRenderPipelineCmd>();
+                    SetRenderPipelineCmd* cmd = iter->NextCommand<SetRenderPipelineCmd>();
                     lastPipeline = ToBackend(cmd->pipeline).Get();
                     lastPipeline->ApplyNow(persistentPipelineState);
 
                     inputBuffers.OnSetPipeline(lastPipeline);
                 } break;
 
+                case Command::SetBindGroup: {
+                    SetBindGroupCmd* cmd = iter->NextCommand<SetBindGroupCmd>();
+                    uint64_t* dynamicOffsets = nullptr;
+                    if (cmd->dynamicOffsetCount > 0) {
+                        dynamicOffsets = iter->NextData<uint64_t>(cmd->dynamicOffsetCount);
+                    }
+                    ApplyBindGroup(gl, cmd->index, cmd->group.Get(),
+                                   ToBackend(lastPipeline->GetLayout()), lastPipeline,
+                                   cmd->dynamicOffsetCount, dynamicOffsets);
+                } break;
+
+                case Command::SetIndexBuffer: {
+                    SetIndexBufferCmd* cmd = iter->NextCommand<SetIndexBufferCmd>();
+                    indexBufferBaseOffset = cmd->offset;
+                    inputBuffers.OnSetIndexBuffer(cmd->buffer.Get());
+                } break;
+
+                case Command::SetVertexBuffers: {
+                    SetVertexBuffersCmd* cmd = iter->NextCommand<SetVertexBuffersCmd>();
+                    auto buffers = iter->NextData<Ref<BufferBase>>(cmd->count);
+                    auto offsets = iter->NextData<uint64_t>(cmd->count);
+                    inputBuffers.OnSetVertexBuffers(cmd->startSlot, cmd->count, buffers, offsets);
+                } break;
+
+                default:
+                    UNREACHABLE();
+                    break;
+            }
+        };
+
+        Command type;
+        while (mCommands.NextCommandId(&type)) {
+            switch (type) {
+                case Command::EndRenderPass: {
+                    mCommands.NextCommand<EndRenderPassCmd>();
+
+                    if (renderPass->attachmentState->GetSampleCount() > 1) {
+                        ResolveMultisampledRenderTargets(gl, renderPass);
+                    }
+                    gl.DeleteFramebuffers(1, &fbo);
+                    return;
+                } break;
+
                 case Command::SetStencilReference: {
                     SetStencilReferenceCmd* cmd = mCommands.NextCommand<SetStencilReferenceCmd>();
                     persistentPipelineState.SetStencilReference(gl, cmd->reference);
@@ -856,31 +889,20 @@
                     gl.BlendColor(cmd->color.r, cmd->color.g, cmd->color.b, cmd->color.a);
                 } break;
 
-                case Command::SetBindGroup: {
-                    SetBindGroupCmd* cmd = mCommands.NextCommand<SetBindGroupCmd>();
-                    uint64_t* dynamicOffsets = nullptr;
-                    if (cmd->dynamicOffsetCount > 0) {
-                        dynamicOffsets = mCommands.NextData<uint64_t>(cmd->dynamicOffsetCount);
+                case Command::ExecuteBundles: {
+                    ExecuteBundlesCmd* cmd = mCommands.NextCommand<ExecuteBundlesCmd>();
+                    auto bundles = mCommands.NextData<Ref<RenderBundleBase>>(cmd->count);
+
+                    for (uint32_t i = 0; i < cmd->count; ++i) {
+                        CommandIterator* iter = bundles[i]->GetCommands();
+                        iter->Reset();
+                        while (iter->NextCommandId(&type)) {
+                            DoRenderBundleCommand(iter, type);
+                        }
                     }
-                    ApplyBindGroup(gl, cmd->index, cmd->group.Get(),
-                                   ToBackend(lastPipeline->GetLayout()), lastPipeline,
-                                   cmd->dynamicOffsetCount, dynamicOffsets);
                 } break;
 
-                case Command::SetIndexBuffer: {
-                    SetIndexBufferCmd* cmd = mCommands.NextCommand<SetIndexBufferCmd>();
-                    indexBufferBaseOffset = cmd->offset;
-                    inputBuffers.OnSetIndexBuffer(cmd->buffer.Get());
-                } break;
-
-                case Command::SetVertexBuffers: {
-                    SetVertexBuffersCmd* cmd = mCommands.NextCommand<SetVertexBuffersCmd>();
-                    auto buffers = mCommands.NextData<Ref<BufferBase>>(cmd->count);
-                    auto offsets = mCommands.NextData<uint64_t>(cmd->count);
-                    inputBuffers.OnSetVertexBuffers(cmd->startSlot, cmd->count, buffers, offsets);
-                } break;
-
-                default: { UNREACHABLE(); } break;
+                default: { DoRenderBundleCommand(&mCommands, type); } break;
             }
         }
 
diff --git a/src/dawn_native/vulkan/CommandBufferVk.cpp b/src/dawn_native/vulkan/CommandBufferVk.cpp
index 28e9058..7b1c7fe 100644
--- a/src/dawn_native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn_native/vulkan/CommandBufferVk.cpp
@@ -16,6 +16,7 @@
 
 #include "dawn_native/CommandEncoder.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/RenderBundle.h"
 #include "dawn_native/vulkan/BindGroupVk.h"
 #include "dawn_native/vulkan/BufferVk.h"
 #include "dawn_native/vulkan/CommandRecordingContext.h"
@@ -671,17 +672,10 @@
         DescriptorSetTracker descriptorSets;
         RenderPipeline* lastPipeline = nullptr;
 
-        Command type;
-        while (mCommands.NextCommandId(&type)) {
+        auto EncodeRenderBundleCommand = [&](CommandIterator* iter, Command type) {
             switch (type) {
-                case Command::EndRenderPass: {
-                    mCommands.NextCommand<EndRenderPassCmd>();
-                    device->fn.CmdEndRenderPass(commands);
-                    return;
-                } break;
-
                 case Command::Draw: {
-                    DrawCmd* draw = mCommands.NextCommand<DrawCmd>();
+                    DrawCmd* draw = iter->NextCommand<DrawCmd>();
 
                     descriptorSets.Flush(device, commands, VK_PIPELINE_BIND_POINT_GRAPHICS);
                     device->fn.CmdDraw(commands, draw->vertexCount, draw->instanceCount,
@@ -689,7 +683,7 @@
                 } break;
 
                 case Command::DrawIndexed: {
-                    DrawIndexedCmd* draw = mCommands.NextCommand<DrawIndexedCmd>();
+                    DrawIndexedCmd* draw = iter->NextCommand<DrawIndexedCmd>();
 
                     descriptorSets.Flush(device, commands, VK_PIPELINE_BIND_POINT_GRAPHICS);
                     device->fn.CmdDrawIndexed(commands, draw->indexCount, draw->instanceCount,
@@ -698,7 +692,7 @@
                 } break;
 
                 case Command::DrawIndirect: {
-                    DrawIndirectCmd* draw = mCommands.NextCommand<DrawIndirectCmd>();
+                    DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>();
                     VkBuffer indirectBuffer = ToBackend(draw->indirectBuffer)->GetHandle();
 
                     descriptorSets.Flush(device, commands, VK_PIPELINE_BIND_POINT_GRAPHICS);
@@ -708,7 +702,7 @@
                 } break;
 
                 case Command::DrawIndexedIndirect: {
-                    DrawIndirectCmd* draw = mCommands.NextCommand<DrawIndirectCmd>();
+                    DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>();
                     VkBuffer indirectBuffer = ToBackend(draw->indirectBuffer)->GetHandle();
 
                     descriptorSets.Flush(device, commands, VK_PIPELINE_BIND_POINT_GRAPHICS);
@@ -719,8 +713,8 @@
 
                 case Command::InsertDebugMarker: {
                     if (device->GetDeviceInfo().debugMarker) {
-                        InsertDebugMarkerCmd* cmd = mCommands.NextCommand<InsertDebugMarkerCmd>();
-                        const char* label = mCommands.NextData<char>(cmd->length + 1);
+                        InsertDebugMarkerCmd* cmd = iter->NextCommand<InsertDebugMarkerCmd>();
+                        const char* label = iter->NextData<char>(cmd->length + 1);
                         VkDebugMarkerMarkerInfoEXT markerInfo;
                         markerInfo.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_MARKER_INFO_EXT;
                         markerInfo.pNext = nullptr;
@@ -732,23 +726,23 @@
                         markerInfo.color[3] = 1.0;
                         device->fn.CmdDebugMarkerInsertEXT(commands, &markerInfo);
                     } else {
-                        SkipCommand(&mCommands, Command::InsertDebugMarker);
+                        SkipCommand(iter, Command::InsertDebugMarker);
                     }
                 } break;
 
                 case Command::PopDebugGroup: {
                     if (device->GetDeviceInfo().debugMarker) {
-                        mCommands.NextCommand<PopDebugGroupCmd>();
+                        iter->NextCommand<PopDebugGroupCmd>();
                         device->fn.CmdDebugMarkerEndEXT(commands);
                     } else {
-                        SkipCommand(&mCommands, Command::PopDebugGroup);
+                        SkipCommand(iter, Command::PopDebugGroup);
                     }
                 } break;
 
                 case Command::PushDebugGroup: {
                     if (device->GetDeviceInfo().debugMarker) {
-                        PushDebugGroupCmd* cmd = mCommands.NextCommand<PushDebugGroupCmd>();
-                        const char* label = mCommands.NextData<char>(cmd->length + 1);
+                        PushDebugGroupCmd* cmd = iter->NextCommand<PushDebugGroupCmd>();
+                        const char* label = iter->NextData<char>(cmd->length + 1);
                         VkDebugMarkerMarkerInfoEXT markerInfo;
                         markerInfo.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_MARKER_INFO_EXT;
                         markerInfo.pNext = nullptr;
@@ -760,35 +754,24 @@
                         markerInfo.color[3] = 1.0;
                         device->fn.CmdDebugMarkerBeginEXT(commands, &markerInfo);
                     } else {
-                        SkipCommand(&mCommands, Command::PushDebugGroup);
+                        SkipCommand(iter, Command::PushDebugGroup);
                     }
                 } break;
 
                 case Command::SetBindGroup: {
-                    SetBindGroupCmd* cmd = mCommands.NextCommand<SetBindGroupCmd>();
+                    SetBindGroupCmd* cmd = iter->NextCommand<SetBindGroupCmd>();
                     VkDescriptorSet set = ToBackend(cmd->group.Get())->GetHandle();
                     uint64_t* dynamicOffsets = nullptr;
                     if (cmd->dynamicOffsetCount > 0) {
-                        dynamicOffsets = mCommands.NextData<uint64_t>(cmd->dynamicOffsetCount);
+                        dynamicOffsets = iter->NextData<uint64_t>(cmd->dynamicOffsetCount);
                     }
 
                     descriptorSets.OnSetBindGroup(cmd->index, set, cmd->dynamicOffsetCount,
                                                   dynamicOffsets);
                 } break;
 
-                case Command::SetBlendColor: {
-                    SetBlendColorCmd* cmd = mCommands.NextCommand<SetBlendColorCmd>();
-                    float blendConstants[4] = {
-                        cmd->color.r,
-                        cmd->color.g,
-                        cmd->color.b,
-                        cmd->color.a,
-                    };
-                    device->fn.CmdSetBlendConstants(commands, blendConstants);
-                } break;
-
                 case Command::SetIndexBuffer: {
-                    SetIndexBufferCmd* cmd = mCommands.NextCommand<SetIndexBufferCmd>();
+                    SetIndexBufferCmd* cmd = iter->NextCommand<SetIndexBufferCmd>();
                     VkBuffer indexBuffer = ToBackend(cmd->buffer)->GetHandle();
 
                     // TODO(cwallez@chromium.org): get the index type from the last render pipeline
@@ -801,7 +784,7 @@
                 } break;
 
                 case Command::SetRenderPipeline: {
-                    SetRenderPipelineCmd* cmd = mCommands.NextCommand<SetRenderPipelineCmd>();
+                    SetRenderPipelineCmd* cmd = iter->NextCommand<SetRenderPipelineCmd>();
                     RenderPipeline* pipeline = ToBackend(cmd->pipeline).Get();
 
                     device->fn.CmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_GRAPHICS,
@@ -811,6 +794,50 @@
                     descriptorSets.OnPipelineLayoutChange(ToBackend(pipeline->GetLayout()));
                 } break;
 
+                case Command::SetVertexBuffers: {
+                    SetVertexBuffersCmd* cmd = iter->NextCommand<SetVertexBuffersCmd>();
+                    auto buffers = iter->NextData<Ref<BufferBase>>(cmd->count);
+                    auto offsets = iter->NextData<uint64_t>(cmd->count);
+
+                    std::array<VkBuffer, kMaxVertexBuffers> vkBuffers;
+                    std::array<VkDeviceSize, kMaxVertexBuffers> vkOffsets;
+
+                    for (uint32_t i = 0; i < cmd->count; ++i) {
+                        Buffer* buffer = ToBackend(buffers[i].Get());
+                        vkBuffers[i] = buffer->GetHandle();
+                        vkOffsets[i] = static_cast<VkDeviceSize>(offsets[i]);
+                    }
+
+                    device->fn.CmdBindVertexBuffers(commands, cmd->startSlot, cmd->count,
+                                                    vkBuffers.data(), vkOffsets.data());
+                } break;
+
+                default:
+                    UNREACHABLE();
+                    break;
+            }
+        };
+
+        Command type;
+        while (mCommands.NextCommandId(&type)) {
+            switch (type) {
+                case Command::EndRenderPass: {
+                    mCommands.NextCommand<EndRenderPassCmd>();
+                    device->fn.CmdEndRenderPass(commands);
+                    return;
+                } break;
+
+                case Command::SetBlendColor: {
+                    SetBlendColorCmd* cmd = mCommands.NextCommand<SetBlendColorCmd>();
+                    float blendConstants[4] = {
+                        cmd->color.r,
+                        cmd->color.g,
+                        cmd->color.b,
+                        cmd->color.a,
+                    };
+                    device->fn.CmdSetBlendConstants(commands, blendConstants);
+                } break;
+
                 case Command::SetStencilReference: {
                     SetStencilReferenceCmd* cmd = mCommands.NextCommand<SetStencilReferenceCmd>();
                     device->fn.CmdSetStencilReference(commands, VK_STENCIL_FRONT_AND_BACK,
@@ -841,25 +868,20 @@
                     device->fn.CmdSetScissor(commands, 0, 1, &rect);
                 } break;
 
-                case Command::SetVertexBuffers: {
-                    SetVertexBuffersCmd* cmd = mCommands.NextCommand<SetVertexBuffersCmd>();
-                    auto buffers = mCommands.NextData<Ref<BufferBase>>(cmd->count);
-                    auto offsets = mCommands.NextData<uint64_t>(cmd->count);
-
-                    std::array<VkBuffer, kMaxVertexBuffers> vkBuffers;
-                    std::array<VkDeviceSize, kMaxVertexBuffers> vkOffsets;
+                case Command::ExecuteBundles: {
+                    ExecuteBundlesCmd* cmd = mCommands.NextCommand<ExecuteBundlesCmd>();
+                    auto bundles = mCommands.NextData<Ref<RenderBundleBase>>(cmd->count);
 
                     for (uint32_t i = 0; i < cmd->count; ++i) {
-                        Buffer* buffer = ToBackend(buffers[i].Get());
-                        vkBuffers[i] = buffer->GetHandle();
-                        vkOffsets[i] = static_cast<VkDeviceSize>(offsets[i]);
+                        CommandIterator* iter = bundles[i]->GetCommands();
+                        iter->Reset();
+                        while (iter->NextCommandId(&type)) {
+                            EncodeRenderBundleCommand(iter, type);
+                        }
                     }
-
-                    device->fn.CmdBindVertexBuffers(commands, cmd->startSlot, cmd->count,
-                                                    vkBuffers.data(), vkOffsets.data());
                 } break;
 
-                default: { UNREACHABLE(); } break;
+                default: { EncodeRenderBundleCommand(&mCommands, type); } break;
             }
         }
 
diff --git a/src/tests/end2end/RenderBundleTests.cpp b/src/tests/end2end/RenderBundleTests.cpp
new file mode 100644
index 0000000..ad2dd3c
--- /dev/null
+++ b/src/tests/end2end/RenderBundleTests.cpp
@@ -0,0 +1,210 @@
+// 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/DawnTest.h"
+
+#include "utils/ComboRenderBundleEncoderDescriptor.h"
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/DawnHelpers.h"
+
+constexpr uint32_t kRTSize = 4;
+constexpr RGBA8 kColors[2] = {RGBA8(0, 255, 0, 255), RGBA8(0, 0, 255, 255)};
+
+// RenderBundleTest tests simple usage of RenderBundles to draw. The implementaiton
+// of RenderBundle is shared significantly with render pass execution which is
+// tested in all other rendering tests.
+class RenderBundleTest : public DawnTest {
+  protected:
+    void SetUp() override {
+        DawnTest::SetUp();
+
+        renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+
+        dawn::ShaderModule vsModule =
+            utils::CreateShaderModule(device, utils::ShaderStage::Vertex, R"(
+                #version 450
+                layout(location = 0) in vec4 pos;
+                void main() {
+                    gl_Position = pos;
+                })");
+
+        dawn::ShaderModule fsModule =
+            utils::CreateShaderModule(device, utils::ShaderStage::Fragment, R"(
+                #version 450
+                layout(location = 0) out vec4 fragColor;
+                layout (set = 0, binding = 0) uniform fragmentUniformBuffer {
+                    vec4 color;
+                };
+                void main() {
+                    fragColor = color;
+                })");
+
+        dawn::BindGroupLayout bgl = utils::MakeBindGroupLayout(
+            device, {{0, dawn::ShaderStageBit::Fragment, dawn::BindingType::UniformBuffer}});
+
+        float colors0[] = {kColors[0].r / 255.f, kColors[0].g / 255.f, kColors[0].b / 255.f,
+                           kColors[0].a / 255.f};
+        float colors1[] = {kColors[1].r / 255.f, kColors[1].g / 255.f, kColors[1].b / 255.f,
+                           kColors[1].a / 255.f};
+
+        dawn::Buffer buffer0 = utils::CreateBufferFromData(device, colors0, 4 * sizeof(float),
+                                                           dawn::BufferUsageBit::Uniform);
+        dawn::Buffer buffer1 = utils::CreateBufferFromData(device, colors1, 4 * sizeof(float),
+                                                           dawn::BufferUsageBit::Uniform);
+
+        bindGroups[0] = utils::MakeBindGroup(device, bgl, {{0, buffer0, 0, 4 * sizeof(float)}});
+        bindGroups[1] = utils::MakeBindGroup(device, bgl, {{0, buffer1, 0, 4 * sizeof(float)}});
+
+        dawn::PipelineLayoutDescriptor pipelineLayoutDesc;
+        pipelineLayoutDesc.bindGroupLayoutCount = 1;
+        pipelineLayoutDesc.bindGroupLayouts = &bgl;
+
+        dawn::PipelineLayout pipelineLayout = device.CreatePipelineLayout(&pipelineLayoutDesc);
+
+        utils::ComboRenderPipelineDescriptor descriptor(device);
+        descriptor.layout = pipelineLayout;
+        descriptor.cVertexStage.module = vsModule;
+        descriptor.cFragmentStage.module = fsModule;
+        descriptor.primitiveTopology = dawn::PrimitiveTopology::TriangleStrip;
+        descriptor.cVertexInput.bufferCount = 1;
+        descriptor.cVertexInput.cBuffers[0].stride = 4 * sizeof(float);
+        descriptor.cVertexInput.cBuffers[0].attributeCount = 1;
+        descriptor.cVertexInput.cAttributes[0].format = dawn::VertexFormat::Float4;
+        descriptor.cColorStates[0]->format = renderPass.colorFormat;
+
+        pipeline = device.CreateRenderPipeline(&descriptor);
+
+        vertexBuffer = utils::CreateBufferFromData<float>(
+            device, dawn::BufferUsageBit::Vertex,
+            {// The bottom left 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,
+
+             // 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});
+    }
+
+    utils::BasicRenderPass renderPass;
+    dawn::RenderPipeline pipeline;
+    dawn::Buffer vertexBuffer;
+    dawn::BindGroup bindGroups[2];
+};
+
+// Basic test of RenderBundle.
+TEST_P(RenderBundleTest, Basic) {
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.colorFormat;
+
+    dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+    uint64_t zeroOffset = 0;
+    renderBundleEncoder.SetPipeline(pipeline);
+    renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+    renderBundleEncoder.SetBindGroup(0, bindGroups[0], 0, nullptr);
+    renderBundleEncoder.Draw(6, 1, 0, 0);
+
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+
+    dawn::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+    pass.ExecuteBundles(1, &renderBundle);
+    pass.EndPass();
+
+    dawn::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    EXPECT_PIXEL_RGBA8_EQ(kColors[0], renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(kColors[0], renderPass.color, 3, 1);
+}
+
+// Test execution of multiple render bundles
+TEST_P(RenderBundleTest, MultipleBundles) {
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.colorFormat;
+
+    dawn::RenderBundle renderBundles[2];
+    uint64_t zeroOffset = 0;
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        renderBundleEncoder.SetBindGroup(0, bindGroups[0], 0, nullptr);
+        renderBundleEncoder.Draw(3, 1, 0, 0);
+
+        renderBundles[0] = renderBundleEncoder.Finish();
+    }
+    {
+        dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+        renderBundleEncoder.SetBindGroup(0, bindGroups[1], 0, nullptr);
+        renderBundleEncoder.Draw(3, 1, 3, 0);
+
+        renderBundles[1] = renderBundleEncoder.Finish();
+    }
+
+    dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+
+    dawn::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+    pass.ExecuteBundles(2, renderBundles);
+    pass.EndPass();
+
+    dawn::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    EXPECT_PIXEL_RGBA8_EQ(kColors[0], renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(kColors[1], renderPass.color, 3, 1);
+}
+
+// Test execution of a bundle along with render pass commands.
+TEST_P(RenderBundleTest, BundleAndRenderPassCommands) {
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatsCount = 1;
+    desc.cColorFormats[0] = renderPass.colorFormat;
+
+    dawn::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+
+    uint64_t zeroOffset = 0;
+    renderBundleEncoder.SetPipeline(pipeline);
+    renderBundleEncoder.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+    renderBundleEncoder.SetBindGroup(0, bindGroups[0], 0, nullptr);
+    renderBundleEncoder.Draw(3, 1, 0, 0);
+
+    dawn::RenderBundle renderBundle = renderBundleEncoder.Finish();
+
+    dawn::CommandEncoder encoder = device.CreateCommandEncoder();
+
+    dawn::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+    pass.ExecuteBundles(1, &renderBundle);
+
+    pass.SetPipeline(pipeline);
+    pass.SetVertexBuffers(0, 1, &vertexBuffer, &zeroOffset);
+    pass.SetBindGroup(0, bindGroups[1], 0, nullptr);
+    pass.Draw(3, 1, 3, 0);
+
+    pass.ExecuteBundles(1, &renderBundle);
+    pass.EndPass();
+
+    dawn::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    EXPECT_PIXEL_RGBA8_EQ(kColors[0], renderPass.color, 1, 3);
+    EXPECT_PIXEL_RGBA8_EQ(kColors[1], renderPass.color, 3, 1);
+}
+
+DAWN_INSTANTIATE_TEST(RenderBundleTest, D3D12Backend, MetalBackend, OpenGLBackend, VulkanBackend);