Capture: Capture Render Pass

Bug: 413053623
Change-Id: I6a6a6964acb6cf83a5b62bc65fa0d45eda683c15
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/266277
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Reviewed-by: Shrek Shao <shrekshao@google.com>
Commit-Queue: Gregg Tavares <gman@chromium.org>
diff --git a/src/dawn/native/webgpu/BindGroupWGPU.cpp b/src/dawn/native/webgpu/BindGroupWGPU.cpp
index 139aa6d..a5ceaa0 100644
--- a/src/dawn/native/webgpu/BindGroupWGPU.cpp
+++ b/src/dawn/native/webgpu/BindGroupWGPU.cpp
@@ -37,6 +37,7 @@
 #include "dawn/native/webgpu/CaptureContext.h"
 #include "dawn/native/webgpu/ComputePipelineWGPU.h"
 #include "dawn/native/webgpu/DeviceWGPU.h"
+#include "dawn/native/webgpu/RenderPipelineWGPU.h"
 #include "dawn/native/webgpu/SamplerWGPU.h"
 #include "dawn/native/webgpu/TextureWGPU.h"
 
@@ -116,9 +117,8 @@
 }
 
 MaybeError BindGroup::AddReferenced(CaptureContext& captureContext) {
-    // We shouldn't need to include any referenced bound objects as we only serialize from
-    // setBindGroup in a command buffer and the command buffer itself should already reference all.
-    // We do need to reference the layout though.
+    // We have to include any referenced bound textures views as the front end does
+    // not track texture views.
     //
     // Unfortunately we can't just call `AddResource(layout)` because that would add a call to
     // createBindGroupLayout which we only want if the bindGroupLayout was not implicit. If
@@ -132,6 +132,22 @@
         DAWN_CHECK(false);
     }
 
+    {
+        BindGroupLayoutInternalBase* layout = GetLayout();
+        const auto& bindingMap = layout->GetBindingMap();
+        for (const auto& [bindingNumbers, apiBindingIndex] : bindingMap) {
+            BindingIndex bindingIndex = layout->AsBindingIndex(apiBindingIndex);
+            const auto& bindingInfo = layout->GetAPIBindingInfo(apiBindingIndex);
+
+            DAWN_TRY(MatchVariant(
+                bindingInfo.bindingLayout,
+                [&](const TextureBindingInfo& info) -> MaybeError {
+                    return captureContext.AddResource(GetBindingAsTextureView(bindingIndex));
+                },
+                [&](const auto& info) -> MaybeError { return {}; }));
+        }
+    }
+
     return {};
 }
 
diff --git a/src/dawn/native/webgpu/CaptureContext.cpp b/src/dawn/native/webgpu/CaptureContext.cpp
index fac7831..df07d2d 100644
--- a/src/dawn/native/webgpu/CaptureContext.cpp
+++ b/src/dawn/native/webgpu/CaptureContext.cpp
@@ -179,6 +179,15 @@
     }};
 }
 
+schema::Color ToSchema(const Color& color) {
+    return {{
+        .r = color.r,
+        .g = color.g,
+        .b = color.b,
+        .a = color.a,
+    }};
+}
+
 schema::TexelCopyBufferLayout ToSchema(const BufferCopy& bufferCopy) {
     return {{
         .offset = bufferCopy.offset,
diff --git a/src/dawn/native/webgpu/CaptureContext.h b/src/dawn/native/webgpu/CaptureContext.h
index e92e16e..ab3cae2 100644
--- a/src/dawn/native/webgpu/CaptureContext.h
+++ b/src/dawn/native/webgpu/CaptureContext.h
@@ -42,6 +42,7 @@
 namespace dawn::native {
 
 class BufferBase;
+struct Color;
 struct Origin3D;
 struct Extent3D;
 struct BufferCopy;
@@ -186,6 +187,7 @@
 wgpu::TextureAspect ToDawn(const Aspect aspect);
 schema::Origin3D ToSchema(const Origin3D& origin);
 schema::Extent3D ToSchema(const Extent3D& extent);
+schema::Color ToSchema(const Color& color);
 schema::ProgrammableStage ToSchema(CaptureContext& captureContext, const ProgrammableStage& stage);
 schema::TexelCopyBufferLayout ToSchema(const BufferCopy& bufferCopy);
 schema::TexelCopyBufferInfo ToSchema(CaptureContext& captureContext, const BufferCopy& bufferCopy);
diff --git a/src/dawn/native/webgpu/CommandBufferWGPU.cpp b/src/dawn/native/webgpu/CommandBufferWGPU.cpp
index c3a9a54..5a3c61d 100644
--- a/src/dawn/native/webgpu/CommandBufferWGPU.cpp
+++ b/src/dawn/native/webgpu/CommandBufferWGPU.cpp
@@ -67,6 +67,7 @@
 // does not outlast a CommandBuffer which itself uses Refs.
 struct CommandBufferResourceUsages {
     std::vector<ComputePipelineBase*> computePipelines;
+    std::vector<RenderPipelineBase*> renderPipelines;
     std::vector<BindGroupBase*> bindGroups;
 };
 
@@ -625,6 +626,47 @@
     return {};
 }
 
+MaybeError GatherReferencedResourcesFromRenderPass(CaptureContext& captureContext,
+                                                   CommandIterator& commands,
+                                                   CommandBufferResourceUsages& usedResources) {
+    Command type;
+    while (commands.NextCommandId(&type)) {
+        switch (type) {
+            case Command::EndRenderPass: {
+                commands.NextCommand<EndRenderPassCmd>();
+                return {};
+            }
+            case Command::SetRenderPipeline: {
+                auto cmd = commands.NextCommand<SetRenderPipelineCmd>();
+                usedResources.renderPipelines.push_back(cmd->pipeline.Get());
+                break;
+            }
+            case Command::SetBindGroup: {
+                auto cmd = commands.NextCommand<SetBindGroupCmd>();
+                usedResources.bindGroups.push_back(cmd->group.Get());
+                break;
+            }
+                DAWN_SKIP_COMMAND(Draw)
+                DAWN_SKIP_COMMAND(DrawIndexed)
+                DAWN_SKIP_COMMAND(DrawIndirect)
+                DAWN_SKIP_COMMAND(DrawIndexedIndirect)
+                DAWN_SKIP_COMMAND(InsertDebugMarker)
+                DAWN_SKIP_COMMAND(PopDebugGroup)
+                DAWN_SKIP_COMMAND(PushDebugGroup)
+                DAWN_SKIP_COMMAND(WriteTimestamp)
+                DAWN_SKIP_COMMAND(SetImmediateData)
+            default: {
+                DAWN_CHECK(false);
+                break;
+            }
+        }
+    }
+
+    // EndComputePass should have been called
+    DAWN_UNREACHABLE();
+    return {};
+}
+
 MaybeError CaptureComputePass(CaptureContext& captureContext, CommandIterator& commands) {
     Command type;
     while (commands.NextCommandId(&type)) {
@@ -682,6 +724,63 @@
     return {};
 }
 
+MaybeError CaptureRenderPass(CaptureContext& captureContext, CommandIterator& commands) {
+    Command type;
+    while (commands.NextCommandId(&type)) {
+        switch (type) {
+            case Command::EndRenderPass: {
+                commands.NextCommand<EndRenderPassCmd>();
+                Serialize(captureContext, schema::RenderPassCommand::End);
+                return {};
+            }
+            case Command::SetRenderPipeline: {
+                const auto& cmd = *commands.NextCommand<SetRenderPipelineCmd>();
+                schema::RenderPassCommandSetPipelineCmd data{{
+                    .data = {{
+                        .pipelineId = captureContext.GetId(cmd.pipeline.Get()),
+                    }},
+                }};
+                Serialize(captureContext, data);
+                break;
+            }
+            case Command::SetBindGroup: {
+                const auto& cmd = *commands.NextCommand<SetBindGroupCmd>();
+                const uint32_t* dynamicOffsetsData =
+                    cmd.dynamicOffsetCount > 0 ? commands.NextData<uint32_t>(cmd.dynamicOffsetCount)
+                                               : nullptr;
+                if (ShouldCaptureBindGroup(captureContext, cmd.group.Get())) {
+                    schema::RenderPassCommandSetBindGroupCmd data{{
+                        .data = {{
+                            .index = uint32_t(cmd.index),
+                            .bindGroupId = captureContext.GetId(cmd.group),
+                            .dynamicOffsets = std::vector<uint32_t>(
+                                dynamicOffsetsData, dynamicOffsetsData + cmd.dynamicOffsetCount),
+                        }},
+                    }};
+                    Serialize(captureContext, data);
+                }
+                break;
+            }
+            case Command::Draw: {
+                const auto& cmd = *commands.NextCommand<DrawCmd>();
+                schema::RenderPassCommandDrawCmd data{{
+                    .data = {{
+                        .vertexCount = cmd.vertexCount,
+                        .instanceCount = cmd.instanceCount,
+                        .firstVertex = cmd.firstVertex,
+                        .firstInstance = cmd.firstInstance,
+                    }},
+                }};
+                Serialize(captureContext, data);
+                break;
+            }
+            default:
+                DAWN_CHECK(false);
+        }
+    }
+    return {};
+}
+
 template <typename T>
 MaybeError AddReferencedPassResourceUsages(CaptureContext& captureContext,
                                            const std::vector<T>& syncScopeResourceUsages) {
@@ -748,10 +847,35 @@
             switch (type) {
                 case Command::BeginComputePass: {
                     commands.NextCommand<BeginComputePassCmd>();
+                    // TODO(451389800): Handle QuerySet
+                    // if (cmd.timestampWrites.querySet != nullptr) {
+                    //     DAWN_TRY(captureContext.AddResource(cmd.timestampWrites.querySet.Get()));
+                    // }
                     DAWN_TRY(GatherReferencedResourcesFromComputePass(captureContext, commands,
                                                                       usedResources));
                     break;
                 }
+                case Command::BeginRenderPass: {
+                    const auto& cmd = *commands.NextCommand<BeginRenderPassCmd>();
+                    for (const auto& attachment : cmd.colorAttachments) {
+                        if (attachment.view != nullptr) {
+                            DAWN_TRY(captureContext.AddResource(attachment.view.Get()));
+                        }
+                        if (attachment.resolveTarget != nullptr) {
+                            DAWN_TRY(captureContext.AddResource(attachment.resolveTarget.Get()));
+                        }
+                    }
+                    if (cmd.depthStencilAttachment.view != nullptr) {
+                        DAWN_TRY(captureContext.AddResource(cmd.depthStencilAttachment.view.Get()));
+                    }
+                    // TODO(451389800): Handle QuerySet
+                    // if (cmd.timestampWrites.querySet != nullptr) {
+                    //     DAWN_TRY(captureContext.AddResource(cmd.timestampWrites.querySet.Get()));
+                    // }
+                    DAWN_TRY(GatherReferencedResourcesFromRenderPass(captureContext, commands,
+                                                                     usedResources));
+                    break;
+                }
                     DAWN_SKIP_COMMAND(CopyBufferToBuffer)
                     DAWN_SKIP_COMMAND(CopyBufferToTexture)
                     DAWN_SKIP_COMMAND(CopyTextureToBuffer)
@@ -768,6 +892,9 @@
     for (auto pipeline : usedResources.computePipelines) {
         DAWN_TRY(captureContext.AddResource(pipeline));
     }
+    for (auto pipeline : usedResources.renderPipelines) {
+        DAWN_TRY(captureContext.AddResource(pipeline));
+    }
     for (auto bindGroup : usedResources.bindGroups) {
         if (ShouldCaptureBindGroup(captureContext, bindGroup)) {
             DAWN_TRY(captureContext.AddResource(bindGroup));
@@ -777,6 +904,36 @@
     return {};
 }
 
+schema::ColorAttachment ToSchema(CaptureContext& captureContext,
+                                 const RenderPassColorAttachmentInfo& info) {
+    return {{
+        .viewId = captureContext.GetId(info.view),
+        .depthSlice = info.view->GetDimension() == wgpu::TextureViewDimension::e3D
+                          ? info.depthSlice
+                          : wgpu::kDepthSliceUndefined,
+        .resolveTargetId = captureContext.GetId(info.resolveTarget),
+        .loadOp = info.loadOp,
+        .storeOp = info.storeOp,
+        .clearValue = ToSchema(info.clearColor),
+    }};
+}
+
+schema::RenderPassDepthStencilAttachment ToSchema(
+    CaptureContext& captureContext,
+    const RenderPassDepthStencilAttachmentInfo& info) {
+    return {{
+        .viewId = captureContext.GetId(info.view),
+        .depthLoadOp = info.depthLoadOp,
+        .depthStoreOp = info.depthStoreOp,
+        .depthClearValue = info.clearDepth,
+        .depthReadOnly = info.depthReadOnly,
+        .stencilLoadOp = info.stencilLoadOp,
+        .stencilStoreOp = info.stencilStoreOp,
+        .stencilClearValue = info.clearStencil,
+        .stencilReadOnly = info.stencilReadOnly,
+    }};
+}
+
 MaybeError CommandBuffer::CaptureCreationParameters(CaptureContext& captureContext) {
     return UseCommands([&](CommandIterator& commands) -> MaybeError {
         Command type;
@@ -845,6 +1002,31 @@
                     DAWN_TRY(CaptureComputePass(captureContext, commands));
                     break;
                 }
+                case Command::BeginRenderPass: {
+                    const auto& cmd = *commands.NextCommand<BeginRenderPassCmd>();
+                    std::vector<schema::ColorAttachment> colorAttachments;
+                    for (ColorAttachmentIndex i : cmd.attachmentState->GetColorAttachmentsMask()) {
+                        colorAttachments.push_back(
+                            ToSchema(captureContext, cmd.colorAttachments[i]));
+                    }
+                    schema::EncoderCommandBeginRenderPassCmd data{{
+                        .data = {{
+                            .label = cmd.label,
+                            .colorAttachments = colorAttachments,
+                            .depthStencilAttachment =
+                                ToSchema(captureContext, cmd.depthStencilAttachment),
+                            // TODO(451389800): Handle QuerySet
+                            // .occlusionQuerySetId =
+                            // captureContext.GetId(cmd.occlusionQuerySet.Get()),
+                            .occlusionQuerySetId = 0,
+                            .timestampWrites = ToSchema(captureContext, cmd.timestampWrites),
+                        }},
+                    }};
+                    Serialize(captureContext, data);
+                    // Capture commands inside the compute pass
+                    DAWN_TRY(CaptureRenderPass(captureContext, commands));
+                    break;
+                }
                 default:
                     DAWN_CHECK(false);
             }
diff --git a/src/dawn/native/webgpu/RenderPipelineWGPU.cpp b/src/dawn/native/webgpu/RenderPipelineWGPU.cpp
index 87e1fa6..1307fea 100644
--- a/src/dawn/native/webgpu/RenderPipelineWGPU.cpp
+++ b/src/dawn/native/webgpu/RenderPipelineWGPU.cpp
@@ -30,6 +30,8 @@
 #include <string>
 #include <vector>
 #include "dawn/common/StringViewUtils.h"
+#include "dawn/native/webgpu/BindGroupLayoutWGPU.h"
+#include "dawn/native/webgpu/CaptureContext.h"
 #include "dawn/native/webgpu/DeviceWGPU.h"
 #include "dawn/native/webgpu/PipelineLayoutWGPU.h"
 #include "dawn/native/webgpu/ShaderModuleWGPU.h"
@@ -46,7 +48,9 @@
 
 RenderPipeline::RenderPipeline(Device* device,
                                const UnpackedPtr<RenderPipelineDescriptor>& descriptor)
-    : RenderPipelineBase(device, descriptor), ObjectWGPU(device->wgpu.renderPipelineRelease) {}
+    : RenderPipelineBase(device, descriptor),
+      RecordableObject(schema::ObjectType::RenderPipeline),
+      ObjectWGPU(device->wgpu.renderPipelineRelease) {}
 
 MaybeError RenderPipeline::InitializeImpl() {
     auto device = ToBackend(GetDevice());
@@ -185,4 +189,133 @@
     return {};
 }
 
+MaybeError RenderPipeline::AddReferenced(CaptureContext& captureContext) {
+    DAWN_TRY(captureContext.AddResource(GetStage(SingleShaderStage::Vertex).module.Get()));
+    if (HasStage(SingleShaderStage::Fragment)) {
+        DAWN_TRY(captureContext.AddResource(GetStage(SingleShaderStage::Fragment).module.Get()));
+    }
+    PipelineLayoutBase* pipelineLayout = GetLayout();
+    if (!pipelineLayout->IsImplicit()) {
+        // TODO(452983510): add support for explicit pipeline layout
+        // DAWN_TRY(captureContext.AddResource(pipelineLayout));
+        return DAWN_INTERNAL_ERROR("explicit pipeline layout unsupported");
+    }
+    return {};
+}
+
+schema::BlendComponent ToSchema(const BlendComponent* component) {
+    const BlendComponent& c = component ? *component : BlendComponent();
+    return {{
+        .operation = c.operation,
+        .srcFactor = c.srcFactor,
+        .dstFactor = c.dstFactor,
+    }};
+}
+
+schema::StencilFaceState ToSchema(const StencilFaceState& state) {
+    return {{
+        .compare = state.compare,
+        .failOp = state.failOp,
+        .depthFailOp = state.depthFailOp,
+        .passOp = state.passOp,
+    }};
+}
+
+MaybeError RenderPipeline::CaptureCreationParameters(CaptureContext& captureContext) {
+    schema::ObjectId layoutId = 0;
+    std::vector<schema::BindGroupLayoutIndexIdPair> groupIndexIds;
+
+    PipelineLayoutBase* pipelineLayout = GetLayout();
+    if (pipelineLayout->IsImplicit()) {
+        // If it's an implicit layout then, on playback, we need to add the bind group layouts
+        // to the id to resource map as there is no other connection.
+        for (BindGroupIndex groupIndex : pipelineLayout->GetBindGroupLayoutsMask()) {
+            BindGroupLayoutBase* bgl = pipelineLayout->GetFrontendBindGroupLayout(groupIndex);
+
+            groupIndexIds.push_back(schema::BindGroupLayoutIndexIdPair{{
+                .groupIndex = uint32_t(groupIndex),
+                .bindGroupLayoutId = captureContext.AddAndGetIdForImplicitResource(bgl),
+            }});
+        }
+    } else {
+        layoutId = captureContext.GetId(pipelineLayout);
+    }
+
+    std::vector<schema::VertexBufferLayout> buffers;
+    for (VertexBufferSlot slot : GetVertexBuffersUsed()) {
+        const auto& info = GetVertexBuffer(slot);
+
+        std::vector<schema::VertexAttribute> attributes;
+        // TODO(454365240): handle attributes
+
+        buffers.push_back({{
+            .arrayStride = info.arrayStride,
+            .stepMode = info.stepMode,
+            .attributes = attributes,
+        }});
+    }
+
+    const DepthStencilState defaultDepthStencilState;
+    const DepthStencilState* depthStencilState = GetDepthStencilState();
+    if (!depthStencilState) {
+        depthStencilState = &defaultDepthStencilState;
+    }
+    ProgrammableStage empty;
+    const ProgrammableStage& fragment =
+        HasStage(SingleShaderStage::Fragment) ? GetStage(SingleShaderStage::Fragment) : empty;
+    std::vector<schema::ColorTargetState> targets;
+    if (fragment.module != nullptr) {
+        for (auto slot : GetColorAttachmentsMask()) {
+            const auto& target = *GetColorTargetState(slot);
+            targets.push_back({{
+                .format = target.format,
+                .blend{{
+                    .color = ToSchema(target.blend ? &target.blend->color : nullptr),
+                    .alpha = ToSchema(target.blend ? &target.blend->alpha : nullptr),
+                }},
+                .writeMask = target.writeMask,
+            }});
+        }
+    }
+
+    schema::RenderPipeline data{{
+        .layoutId = layoutId,
+        .vertex{{
+            .program = ToSchema(captureContext, GetStage(SingleShaderStage::Vertex)),
+            .buffers = buffers,
+        }},
+        .primitive{{
+            .topology = GetPrimitiveTopology(),
+            .stripIndexFormat = GetStripIndexFormat(),
+            .frontFace = GetFrontFace(),
+            .cullMode = GetCullMode(),
+            .unclippedDepth = HasUnclippedDepth(),
+        }},
+        .depthStencil{{
+            .format = depthStencilState->format,
+            .depthWriteEnabled = depthStencilState->depthWriteEnabled == wgpu::OptionalBool(true),
+            .depthCompare = depthStencilState->depthCompare,
+            .stencilFront = ToSchema(depthStencilState->stencilFront),
+            .stencilBack = ToSchema(depthStencilState->stencilBack),
+            .stencilReadMask = depthStencilState->stencilReadMask,
+            .stencilWriteMask = depthStencilState->stencilWriteMask,
+            .depthBias = depthStencilState->depthBias,
+            .depthBiasSlopeScale = depthStencilState->depthBiasSlopeScale,
+            .depthBiasClamp = depthStencilState->depthBiasClamp,
+        }},
+        .multisample{{
+            .count = GetSampleCount(),
+            .mask = GetSampleMask(),
+            .alphaToCoverageEnabled = IsAlphaToCoverageEnabled(),
+        }},
+        .fragment{{
+            .program = ToSchema(captureContext, fragment),
+            .targets = targets,
+        }},
+        .groupIndexIds = groupIndexIds,
+    }};
+    Serialize(captureContext, data);
+    return {};
+}
+
 }  // namespace dawn::native::webgpu
diff --git a/src/dawn/native/webgpu/RenderPipelineWGPU.h b/src/dawn/native/webgpu/RenderPipelineWGPU.h
index 4ab966c..081e679 100644
--- a/src/dawn/native/webgpu/RenderPipelineWGPU.h
+++ b/src/dawn/native/webgpu/RenderPipelineWGPU.h
@@ -32,18 +32,24 @@
 #include "dawn/common/Constants.h"
 #include "dawn/native/RenderPipeline.h"
 #include "dawn/native/webgpu/ObjectWGPU.h"
+#include "dawn/native/webgpu/RecordableObject.h"
 
 namespace dawn::native::webgpu {
 
 class Device;
 
-class RenderPipeline final : public RenderPipelineBase, public ObjectWGPU<WGPURenderPipeline> {
+class RenderPipeline final : public RenderPipelineBase,
+                             public RecordableObject,
+                             public ObjectWGPU<WGPURenderPipeline> {
   public:
     static Ref<RenderPipeline> CreateUninitialized(
         Device* device,
         const UnpackedPtr<RenderPipelineDescriptor>& descriptor);
     MaybeError InitializeImpl() override;
 
+    MaybeError AddReferenced(CaptureContext& captureContext) override;
+    MaybeError CaptureCreationParameters(CaptureContext& context) override;
+
   protected:
     RenderPipeline(Device* device, const UnpackedPtr<RenderPipelineDescriptor>& descriptor);
 };
diff --git a/src/dawn/native/webgpu/Serialization.cpp b/src/dawn/native/webgpu/Serialization.cpp
index 04c0b44..4fe0118 100644
--- a/src/dawn/native/webgpu/Serialization.cpp
+++ b/src/dawn/native/webgpu/Serialization.cpp
@@ -49,10 +49,18 @@
     WriteBytes(context, reinterpret_cast<const char*>(&v), sizeof(v));
 }
 
+void Serialize(CaptureContext& context, float v) {
+    WriteBytes(context, reinterpret_cast<const char*>(&v), sizeof(v));
+}
+
 void Serialize(CaptureContext& context, double v) {
     WriteBytes(context, reinterpret_cast<const char*>(&v), sizeof(v));
 }
 
+void Serialize(CaptureContext& context, bool v) {
+    WriteBytes(context, reinterpret_cast<const char*>(&v), sizeof(v));
+}
+
 void Serialize(CaptureContext& context, const std::string& v) {
     Serialize(context, v.size());
     WriteBytes(context, v.data(), v.size());
diff --git a/src/dawn/native/webgpu/Serialization.h b/src/dawn/native/webgpu/Serialization.h
index 14c2d32..27c8d9e 100644
--- a/src/dawn/native/webgpu/Serialization.h
+++ b/src/dawn/native/webgpu/Serialization.h
@@ -33,7 +33,9 @@
 void Serialize(CaptureContext& context, int32_t v);
 void Serialize(CaptureContext& context, uint32_t v);
 void Serialize(CaptureContext& context, uint64_t v);
+void Serialize(CaptureContext& context, float v);
 void Serialize(CaptureContext& context, double v);
+void Serialize(CaptureContext& context, bool v);
 void Serialize(CaptureContext& context, const std::string& v);
 
 template <typename T>
@@ -150,6 +152,10 @@
     struct CmdType##CmdName##Cmd : CmdType##CmdName##Cmd##__Contents,                  \
                                    public Serializable<CmdType##CmdName##Cmd>
 
+// Makes both a CmdData and a Cmd struct for a given render pass command name.
+#define DAWN_REPLAY_MAKE_RENDER_PASS_CMD_AND_CMD_DATA(CmdName, CMD_MEMBERS) \
+    DAWN_REPLAY_MAKE_CMD_AND_CMD_DATA(RenderPassCommand, CmdName, CMD_MEMBERS)
+
 // Makes both a CmdData and a Cmd struct for a given compute pass command name.
 #define DAWN_REPLAY_MAKE_COMPUTE_PASS_CMD_AND_CMD_DATA(CmdName, CMD_MEMBERS) \
     DAWN_REPLAY_MAKE_CMD_AND_CMD_DATA(ComputePassCommand, CmdName, CMD_MEMBERS)
diff --git a/src/dawn/replay/Deserialization.cpp b/src/dawn/replay/Deserialization.cpp
index 04a54ab..694268e 100644
--- a/src/dawn/replay/Deserialization.cpp
+++ b/src/dawn/replay/Deserialization.cpp
@@ -51,10 +51,18 @@
     return ReadBytes(s, reinterpret_cast<char*>(v), sizeof(*v));
 }
 
+MaybeError Deserialize(ReadHead& s, float* v) {
+    return ReadBytes(s, reinterpret_cast<char*>(v), sizeof(*v));
+}
+
 MaybeError Deserialize(ReadHead& s, double* v) {
     return ReadBytes(s, reinterpret_cast<char*>(v), sizeof(*v));
 }
 
+MaybeError Deserialize(ReadHead& s, bool* v) {
+    return ReadBytes(s, reinterpret_cast<char*>(v), sizeof(*v));
+}
+
 MaybeError Deserialize(ReadHead& s, std::string* v) {
     size_t size = 0;
     DAWN_TRY(Deserialize(s, &size));
diff --git a/src/dawn/replay/Deserialization.h b/src/dawn/replay/Deserialization.h
index ec33a92..5579fba 100644
--- a/src/dawn/replay/Deserialization.h
+++ b/src/dawn/replay/Deserialization.h
@@ -34,7 +34,9 @@
 MaybeError Deserialize(ReadHead& s, int32_t* v);
 MaybeError Deserialize(ReadHead& s, uint32_t* v);
 MaybeError Deserialize(ReadHead& s, uint64_t* v);
+MaybeError Deserialize(ReadHead& s, float* v);
 MaybeError Deserialize(ReadHead& s, double* v);
+MaybeError Deserialize(ReadHead& s, bool* v);
 MaybeError Deserialize(ReadHead& s, std::string* v);
 
 template <typename T>
@@ -159,6 +161,10 @@
     struct CmdType##CmdName##Cmd : CmdType##CmdName##Cmd##__Contents,                  \
                                    public ::dawn::replay::Deserializable<CmdType##CmdName##Cmd>
 
+// Makes both a CmdData and a Cmd struct for a given render pass command name.
+#define DAWN_REPLAY_MAKE_RENDER_PASS_CMD_AND_CMD_DATA(CmdName, CMD_MEMBERS) \
+    DAWN_REPLAY_MAKE_CMD_AND_CMD_DATA(RenderPassCommand, CmdName, CMD_MEMBERS)
+
 // Makes both a CmdData and a Cmd struct for a given compute pass command name.
 #define DAWN_REPLAY_MAKE_COMPUTE_PASS_CMD_AND_CMD_DATA(CmdName, CMD_MEMBERS) \
     DAWN_REPLAY_MAKE_CMD_AND_CMD_DATA(ComputePassCommand, CmdName, CMD_MEMBERS)
diff --git a/src/dawn/replay/Replay.cpp b/src/dawn/replay/Replay.cpp
index ac59327..41c52f1 100644
--- a/src/dawn/replay/Replay.cpp
+++ b/src/dawn/replay/Replay.cpp
@@ -51,6 +51,15 @@
     };
 }
 
+wgpu::Color ToWGPU(const schema::Color& color) {
+    return wgpu::Color{
+        .r = color.r,
+        .g = color.g,
+        .b = color.b,
+        .a = color.a,
+    };
+}
+
 wgpu::PassTimestampWrites ToWGPU(const Replay& replay, const schema::TimestampWrites& writes) {
     return wgpu::PassTimestampWrites{
         .nextInChain = nullptr,
@@ -324,6 +333,47 @@
     return DAWN_INTERNAL_ERROR("Missing ComputePass End command");
 }
 
+MaybeError ProcessRenderPassCommands(const Replay& replay,
+                                     ReadHead& readHead,
+                                     wgpu::Device device,
+                                     wgpu::RenderPassEncoder pass) {
+    schema::RenderPassCommand cmd;
+
+    while (!readHead.IsDone()) {
+        DAWN_TRY(Deserialize(readHead, &cmd));
+        switch (cmd) {
+            case schema::RenderPassCommand::End: {
+                pass.End();
+                return {};
+            }
+            case schema::RenderPassCommand::SetPipeline: {
+                schema::RenderPassCommandSetPipelineCmdData data;
+                DAWN_TRY(Deserialize(readHead, &data));
+                pass.SetPipeline(replay.GetObjectById<wgpu::RenderPipeline>(data.pipelineId));
+                break;
+            }
+            case schema::RenderPassCommand::SetBindGroup: {
+                schema::RenderPassCommandSetBindGroupCmdData data;
+                DAWN_TRY(Deserialize(readHead, &data));
+                pass.SetBindGroup(data.index,
+                                  replay.GetObjectById<wgpu::BindGroup>(data.bindGroupId),
+                                  data.dynamicOffsets.size(), data.dynamicOffsets.data());
+                break;
+            }
+            case schema::RenderPassCommand::Draw: {
+                schema::RenderPassCommandDrawCmdData data;
+                DAWN_TRY(Deserialize(readHead, &data));
+                pass.Draw(data.vertexCount, data.instanceCount, data.firstVertex,
+                          data.firstInstance);
+                break;
+            }
+            default:
+                return DAWN_INTERNAL_ERROR("Render Pass Command not implemented");
+        }
+    }
+    return DAWN_INTERNAL_ERROR("Missing RenderPass End command");
+}
+
 MaybeError ProcessEncoderCommands(const Replay& replay,
                                   ReadHead& readHead,
                                   wgpu::Device device,
@@ -345,6 +395,52 @@
                 DAWN_TRY(ProcessComputePassCommands(replay, readHead, device, pass));
                 break;
             }
+            case schema::EncoderCommand::BeginRenderPass: {
+                schema::EncoderCommandBeginRenderPassCmdData data;
+                DAWN_TRY(Deserialize(readHead, &data));
+                wgpu::PassTimestampWrites timestampWrites = ToWGPU(replay, data.timestampWrites);
+                std::vector<wgpu::RenderPassColorAttachment> colorAttachments;
+
+                for (const auto& attachment : data.colorAttachments) {
+                    colorAttachments.push_back(wgpu::RenderPassColorAttachment{
+                        .nextInChain = nullptr,
+                        .view = replay.GetObjectById<wgpu::TextureView>(attachment.viewId),
+                        .depthSlice = attachment.depthSlice,
+                        .resolveTarget =
+                            replay.GetObjectById<wgpu::TextureView>(attachment.resolveTargetId),
+                        .loadOp = attachment.loadOp,
+                        .storeOp = attachment.storeOp,
+                        .clearValue = ToWGPU(attachment.clearValue),
+                    });
+                }
+
+                wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{
+                    .view =
+                        replay.GetObjectById<wgpu::TextureView>(data.depthStencilAttachment.viewId),
+                    .depthLoadOp = data.depthStencilAttachment.depthLoadOp,
+                    .depthStoreOp = data.depthStencilAttachment.depthStoreOp,
+                    .depthClearValue = data.depthStencilAttachment.depthClearValue,
+                    .depthReadOnly = data.depthStencilAttachment.depthReadOnly,
+                    .stencilLoadOp = data.depthStencilAttachment.stencilLoadOp,
+                    .stencilStoreOp = data.depthStencilAttachment.stencilStoreOp,
+                    .stencilClearValue = data.depthStencilAttachment.stencilClearValue,
+                    .stencilReadOnly = data.depthStencilAttachment.stencilReadOnly,
+                };
+
+                wgpu::RenderPassDescriptor desc{
+                    .label = wgpu::StringView(data.label),
+                    .colorAttachmentCount = colorAttachments.size(),
+                    .colorAttachments = colorAttachments.data(),
+                    .depthStencilAttachment =
+                        depthStencilAttachment.view != nullptr ? &depthStencilAttachment : nullptr,
+                    .occlusionQuerySet =
+                        replay.GetObjectById<wgpu::QuerySet>(data.occlusionQuerySetId),
+                    .timestampWrites = timestampWrites.querySet ? &timestampWrites : nullptr,
+                };
+                wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc);
+                DAWN_TRY(ProcessRenderPassCommands(replay, readHead, device, pass));
+                break;
+            }
             case schema::EncoderCommand::CopyBufferToBuffer: {
                 schema::EncoderCommandCopyBufferToBufferCmdData data;
                 DAWN_TRY(Deserialize(readHead, &data));
diff --git a/src/dawn/replay/Replay.h b/src/dawn/replay/Replay.h
index b3d15ca..15a6aae 100644
--- a/src/dawn/replay/Replay.h
+++ b/src/dawn/replay/Replay.h
@@ -49,6 +49,7 @@
                      wgpu::ComputePipeline,
                      wgpu::PipelineLayout,
                      wgpu::QuerySet,
+                     wgpu::RenderPipeline,
                      wgpu::Sampler,
                      wgpu::ShaderModule,
                      wgpu::Texture,
diff --git a/src/dawn/serialization/Schema.h b/src/dawn/serialization/Schema.h
index 9ace6e0..80fbfef 100644
--- a/src/dawn/serialization/Schema.h
+++ b/src/dawn/serialization/Schema.h
@@ -61,6 +61,30 @@
     End,
 };
 
+enum class RenderPassCommand : uint32_t {
+    Invalid = 0,  // 0 is invalid at it's more likely to catch bugs.
+    Draw,
+    DrawIndexed,
+    DrawIndirect,
+    DrawIndexedIndirect,
+    SetPipeline,
+    SetBindGroup,
+    SetIndexBuffer,
+    SetVertexBuffer,
+    SetViewport,
+    SetScissorRect,
+    SetBlendConstant,
+    SetStencilReference,
+    EndRenderPass,
+    ExecuteBundles,
+    WriteTimestamp,
+    InsertDebugMarker,
+    PopDebugGroup,
+    PushDebugGroup,
+    SetImmediateData,
+    End,
+};
+
 enum class EncoderCommand : uint32_t {
     Invalid = 0,  // 0 is invalid at it's more likely to catch bugs.
     BeginComputePass,
@@ -104,6 +128,14 @@
 
 DAWN_REPLAY_SERIALIZABLE(struct, Extent3D, EXTENT3D_MEMBER){};
 
+#define COLOR_MEMBER(X) \
+    X(double, r)        \
+    X(double, g)        \
+    X(double, b)        \
+    X(double, a)
+
+DAWN_REPLAY_SERIALIZABLE(struct, Color, COLOR_MEMBER){};
+
 #define PIPELINE_CONSTANT_MEMBER(X) \
     X(std::string, name)            \
     X(double, value)
@@ -117,6 +149,90 @@
 
 DAWN_REPLAY_SERIALIZABLE(struct, ProgrammableStage, PROGRAMMABLE_STAGE_MEMBER){};
 
+#define VERTEX_ATTRIBUTE_MEMBER(X) \
+    X(wgpu::VertexFormat, format)  \
+    X(uint64_t, offset)            \
+    X(uint32_t, shaderLocation)
+
+DAWN_REPLAY_SERIALIZABLE(struct, VertexAttribute, VERTEX_ATTRIBUTE_MEMBER){};
+
+#define VERTEX_BUFFER_LAYOUT_MEMBER(X) \
+    X(uint64_t, arrayStride)           \
+    X(wgpu::VertexStepMode, stepMode)  \
+    X(std::vector<VertexAttribute>, attributes)
+
+DAWN_REPLAY_SERIALIZABLE(struct, VertexBufferLayout, VERTEX_BUFFER_LAYOUT_MEMBER){};
+
+#define VERTEX_STATE_MEMBER(X)    \
+    X(ProgrammableStage, program) \
+    X(std::vector<VertexBufferLayout>, buffers)
+
+DAWN_REPLAY_SERIALIZABLE(struct, VertexState, VERTEX_STATE_MEMBER){};
+
+#define PRIMITIVE_STATE_MEMBER(X)          \
+    X(wgpu::PrimitiveTopology, topology)   \
+    X(wgpu::IndexFormat, stripIndexFormat) \
+    X(wgpu::FrontFace, frontFace)          \
+    X(wgpu::CullMode, cullMode)            \
+    X(bool, unclippedDepth)
+
+DAWN_REPLAY_SERIALIZABLE(struct, PrimitiveState, PRIMITIVE_STATE_MEMBER){};
+
+#define STENCIL_FACE_STATE_MEMBER(X)       \
+    X(wgpu::CompareFunction, compare)      \
+    X(wgpu::StencilOperation, failOp)      \
+    X(wgpu::StencilOperation, depthFailOp) \
+    X(wgpu::StencilOperation, passOp)
+
+DAWN_REPLAY_SERIALIZABLE(struct, StencilFaceState, STENCIL_FACE_STATE_MEMBER){};
+
+#define DEPTH_STENCIL_STATE_MEMBER(X)      \
+    X(wgpu::TextureFormat, format)         \
+    X(bool, depthWriteEnabled)             \
+    X(wgpu::CompareFunction, depthCompare) \
+    X(StencilFaceState, stencilFront)      \
+    X(StencilFaceState, stencilBack)       \
+    X(uint32_t, stencilReadMask)           \
+    X(uint32_t, stencilWriteMask)          \
+    X(int32_t, depthBias)                  \
+    X(float, depthBiasSlopeScale)          \
+    X(float, depthBiasClamp)
+
+DAWN_REPLAY_SERIALIZABLE(struct, DepthStencilState, DEPTH_STENCIL_STATE_MEMBER){};
+
+#define MULTISAMPLE_STATE_MEMBER(X) \
+    X(uint32_t, count)              \
+    X(uint32_t, mask)               \
+    X(bool, alphaToCoverageEnabled)
+
+DAWN_REPLAY_SERIALIZABLE(struct, MultisampleState, MULTISAMPLE_STATE_MEMBER){};
+
+#define BLEND_COMPONENT_MEMBER(X)      \
+    X(wgpu::BlendOperation, operation) \
+    X(wgpu::BlendFactor, srcFactor)    \
+    X(wgpu::BlendFactor, dstFactor)
+
+DAWN_REPLAY_SERIALIZABLE(struct, BlendComponent, BLEND_COMPONENT_MEMBER){};
+
+#define BLEND_STATE_MEMBER(X) \
+    X(BlendComponent, color)  \
+    X(BlendComponent, alpha)
+
+DAWN_REPLAY_SERIALIZABLE(struct, BlendState, BLEND_STATE_MEMBER){};
+
+#define COLOR_TARGET_STATE_MEMBER(X) \
+    X(wgpu::TextureFormat, format)   \
+    X(BlendState, blend)             \
+    X(wgpu::ColorWriteMask, writeMask)
+
+DAWN_REPLAY_SERIALIZABLE(struct, ColorTargetState, COLOR_TARGET_STATE_MEMBER){};
+
+#define FRAGMENT_STATE_MEMBER(X)  \
+    X(ProgrammableStage, program) \
+    X(std::vector<ColorTargetState>, targets)
+
+DAWN_REPLAY_SERIALIZABLE(struct, FragmentState, FRAGMENT_STATE_MEMBER){};
+
 #define BUFFER_CREATION_MEMBER(X) \
     X(uint64_t, size)             \
     X(wgpu::BufferUsage, usage)
@@ -172,6 +288,17 @@
 
 DAWN_REPLAY_SERIALIZABLE(struct, ComputePipeline, COMPUTE_PIPELINE_CREATION_MEMBER){};
 
+#define RENDER_PIPELINE_CREATION_MEMBER(X) \
+    X(ObjectId, layoutId)                  \
+    X(VertexState, vertex)                 \
+    X(PrimitiveState, primitive)           \
+    X(DepthStencilState, depthStencil)     \
+    X(MultisampleState, multisample)       \
+    X(FragmentState, fragment)             \
+    X(std::vector<BindGroupLayoutIndexIdPair>, groupIndexIds)
+
+DAWN_REPLAY_SERIALIZABLE(struct, RenderPipeline, RENDER_PIPELINE_CREATION_MEMBER){};
+
 #define BIND_GROUP_ENTRY_MEMBER(X) \
     X(uint32_t, binding)           \
     X(ObjectId, bufferId)          \
@@ -223,6 +350,31 @@
 
 DAWN_REPLAY_SERIALIZABLE(struct, TimestampWrites, TIMESTAMP_WRITES_MEMBER){};
 
+#define COLOR_ATTACHMENT_MEMBER(X) \
+    X(ObjectId, viewId)            \
+    X(uint32_t, depthSlice)        \
+    X(ObjectId, resolveTargetId)   \
+    X(wgpu::LoadOp, loadOp)        \
+    X(wgpu::StoreOp, storeOp)      \
+    X(Color, clearValue)
+
+DAWN_REPLAY_SERIALIZABLE(struct, ColorAttachment, COLOR_ATTACHMENT_MEMBER){};
+
+#define RENDER_PASS_DEPTH_STENCIL_ATTACHMENT_MEMBER(X) \
+    X(ObjectId, viewId)                                \
+    X(wgpu::LoadOp, depthLoadOp)                       \
+    X(wgpu::StoreOp, depthStoreOp)                     \
+    X(float, depthClearValue)                          \
+    X(bool, depthReadOnly)                             \
+    X(wgpu::LoadOp, stencilLoadOp)                     \
+    X(wgpu::StoreOp, stencilStoreOp)                   \
+    X(uint32_t, stencilClearValue)                     \
+    X(bool, stencilReadOnly)
+
+DAWN_REPLAY_SERIALIZABLE(struct,
+                         RenderPassDepthStencilAttachment,
+                         RENDER_PASS_DEPTH_STENCIL_ATTACHMENT_MEMBER){};
+
 #define CREATE_RESOURCE_CMD_DATA_MEMBER(X) X(LabeledResource, resource)
 
 DAWN_REPLAY_MAKE_ROOT_CMD_AND_CMD_DATA(CreateResource, CREATE_RESOURCE_CMD_DATA_MEMBER){};
@@ -293,6 +445,16 @@
 
 DAWN_REPLAY_MAKE_ENCODER_CMD_AND_CMD_DATA(BeginComputePass, BEGIN_COMPUTE_PASS_CMD_DATA_MEMBER){};
 
+#define BEGIN_RENDER_PASS_CMD_DATA_MEMBER(X)                    \
+    X(std::string, label)                                       \
+    X(std::vector<ColorAttachment>, colorAttachments)           \
+    X(RenderPassDepthStencilAttachment, depthStencilAttachment) \
+    X(ObjectId, occlusionQuerySetId)                            \
+    X(TimestampWrites, timestampWrites)                         \
+    X(uint64_t, maxDrawCount)
+
+DAWN_REPLAY_MAKE_ENCODER_CMD_AND_CMD_DATA(BeginRenderPass, BEGIN_RENDER_PASS_CMD_DATA_MEMBER){};
+
 #define SET_COMPUTE_PIPELINE_CMD_DATA_MEMBER(X) X(ObjectId, pipelineId)
 
 DAWN_REPLAY_MAKE_COMPUTE_PASS_CMD_AND_CMD_DATA(SetComputePipeline,
@@ -312,6 +474,20 @@
 
 DAWN_REPLAY_MAKE_COMPUTE_PASS_CMD_AND_CMD_DATA(Dispatch, DISPATCH_CMD_DATA_MEMBER){};
 
+#define SET_PIPELINE_CMD_DATA_MEMBER(X) X(ObjectId, pipelineId)
+
+DAWN_REPLAY_MAKE_RENDER_PASS_CMD_AND_CMD_DATA(SetPipeline, SET_PIPELINE_CMD_DATA_MEMBER){};
+
+DAWN_REPLAY_MAKE_RENDER_PASS_CMD_AND_CMD_DATA(SetBindGroup, SET_BIND_GROUP_CMD_DATA_MEMBER){};
+
+#define DRAW_CMD_DATA_MEMBER(X) \
+    X(uint32_t, vertexCount)    \
+    X(uint32_t, instanceCount)  \
+    X(uint32_t, firstVertex)    \
+    X(uint32_t, firstInstance)
+
+DAWN_REPLAY_MAKE_RENDER_PASS_CMD_AND_CMD_DATA(Draw, DRAW_CMD_DATA_MEMBER){};
+
 }  // namespace schema
 
 #endif  // SRC_DAWN_SERIALIZATION_SCHEMA_H_
diff --git a/src/dawn/tests/white_box/CaptureAndReplayTests.cpp b/src/dawn/tests/white_box/CaptureAndReplayTests.cpp
index 7bd1e33..b13007c 100644
--- a/src/dawn/tests/white_box/CaptureAndReplayTests.cpp
+++ b/src/dawn/tests/white_box/CaptureAndReplayTests.cpp
@@ -1182,6 +1182,47 @@
     }
 }
 
+// Capture and replay the simplest render pass.
+// It just starts and ends a render pass and uses the clearValue to set
+// a texture.
+TEST_P(CaptureAndReplayTests, CaptureRenderPassBasic) {
+    wgpu::TextureDescriptor textureDesc;
+    textureDesc.label = "myTexture";
+    textureDesc.size = {1, 1, 1};
+    textureDesc.format = wgpu::TextureFormat::RGBA8Uint;
+    textureDesc.usage = wgpu::TextureUsage::RenderAttachment;
+    wgpu::Texture texture = device.CreateTexture(&textureDesc);
+
+    wgpu::CommandBuffer commands;
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        utils::ComboRenderPassDescriptor passDescriptor({texture.CreateView()});
+        passDescriptor.cColorAttachments[0].clearValue = {0x11, 0x22, 0x33, 0x44};
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDescriptor);
+        pass.End();
+
+        commands = encoder.Finish();
+    }
+
+    // --- capture ---
+    auto recorder = Recorder::CreateAndStart(device);
+
+    queue.Submit(1, &commands);
+
+    // --- replay ---
+    auto capture = recorder.Finish();
+    auto replay = capture.Replay(device);
+
+    {
+        wgpu::Texture texture = replay->GetObjectByLabel<wgpu::Texture>("myTexture");
+        ASSERT_NE(texture, nullptr);
+
+        uint8_t expected[] = {0x11, 0x22, 0x33, 0x44};
+        EXPECT_TEXTURE_EQ(&expected[0], texture, {0, 0}, {1, 1}, 0, wgpu::TextureAspect::All);
+    }
+}
+
 DAWN_INSTANTIATE_TEST(CaptureAndReplayTests, WebGPUBackend());
 
 }  // anonymous namespace