Allow sparse color attachments

Add implementations and related tests reflecting the spec update.

Bug: dawn:1294
Change-Id: I2c20af313259e1d6d6049189cb8adebe4c2436af
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/81922
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Shrek Shao <shrekshao@google.com>
diff --git a/dawn.json b/dawn.json
index 41b4fdc..0ee7e22 100644
--- a/dawn.json
+++ b/dawn.json
@@ -1802,7 +1802,7 @@
     "render pass color attachment": {
         "category": "structure",
         "members": [
-            {"name": "view", "type": "texture view"},
+            {"name": "view", "type": "texture view", "optional": true},
             {"name": "resolve target", "type": "texture view", "optional": true},
             {"name": "load op", "type": "load op"},
             {"name": "store op", "type": "store op"},
diff --git a/src/dawn/common/ityp_bitset.h b/src/dawn/common/ityp_bitset.h
index 057e54e..d91cb19 100644
--- a/src/dawn/common/ityp_bitset.h
+++ b/src/dawn/common/ityp_bitset.h
@@ -131,4 +131,56 @@
 
 }  // namespace ityp
 
+// Assume we have bitset of at most 64 bits
+// Returns i which is the next integer of the index of the highest bit
+// i == 0 if there is no bit set to true
+// i == 1 if only the least significant bit (at index 0) is the bit set to true with the
+// highest index
+// ...
+// i == 64 if the most significant bit (at index 64) is the bit set to true with the highest
+// index
+template <typename Index, size_t N>
+Index GetHighestBitIndexPlusOne(const ityp::bitset<Index, N>& bitset) {
+    using I = UnderlyingType<Index>;
+#if defined(DAWN_COMPILER_MSVC)
+    if constexpr (N > 32) {
+#    if defined(DAWN_PLATFORM_64_BIT)
+        unsigned long firstBitIndex = 0ul;
+        unsigned char ret = _BitScanReverse64(&firstBitIndex, bitset.to_ullong());
+        if (ret == 0) {
+            return Index(static_cast<I>(0));
+        }
+        return Index(static_cast<I>(firstBitIndex + 1));
+#    else   // defined(DAWN_PLATFORM_64_BIT)
+        if (bitset.none()) {
+            return Index(static_cast<I>(0));
+        }
+        for (size_t i = 0u; i < N; i++) {
+            if (bitset.test(Index(static_cast<I>(N - 1 - i)))) {
+                return Index(static_cast<I>(N - i));
+            }
+        }
+        UNREACHABLE();
+#    endif  // defined(DAWN_PLATFORM_64_BIT)
+    } else {
+        unsigned long firstBitIndex = 0ul;
+        unsigned char ret = _BitScanReverse(&firstBitIndex, bitset.to_ulong());
+        if (ret == 0) {
+            return Index(static_cast<I>(0));
+        }
+        return Index(static_cast<I>(firstBitIndex + 1));
+    }
+#else   // defined(DAWN_COMPILER_MSVC)
+    if (bitset.none()) {
+        return Index(static_cast<I>(0));
+    }
+    if constexpr (N > 32) {
+        return Index(
+            static_cast<I>(64 - static_cast<uint32_t>(__builtin_clzll(bitset.to_ullong()))));
+    } else {
+        return Index(static_cast<I>(32 - static_cast<uint32_t>(__builtin_clz(bitset.to_ulong()))));
+    }
+#endif  // defined(DAWN_COMPILER_MSVC)
+}
+
 #endif  // COMMON_ITYP_BITSET_H_
diff --git a/src/dawn/native/AttachmentState.cpp b/src/dawn/native/AttachmentState.cpp
index c5ac739..1e38d9d 100644
--- a/src/dawn/native/AttachmentState.cpp
+++ b/src/dawn/native/AttachmentState.cpp
@@ -27,8 +27,11 @@
         ASSERT(descriptor->colorFormatsCount <= kMaxColorAttachments);
         for (ColorAttachmentIndex i(uint8_t(0));
              i < ColorAttachmentIndex(static_cast<uint8_t>(descriptor->colorFormatsCount)); ++i) {
-            mColorAttachmentsSet.set(i);
-            mColorFormats[i] = descriptor->colorFormats[static_cast<uint8_t>(i)];
+            wgpu::TextureFormat format = descriptor->colorFormats[static_cast<uint8_t>(i)];
+            if (format != wgpu::TextureFormat::Undefined) {
+                mColorAttachmentsSet.set(i);
+                mColorFormats[i] = format;
+            }
         }
         mDepthStencilFormat = descriptor->depthStencilFormat;
     }
@@ -40,8 +43,12 @@
             for (ColorAttachmentIndex i(uint8_t(0));
                  i < ColorAttachmentIndex(static_cast<uint8_t>(descriptor->fragment->targetCount));
                  ++i) {
-                mColorAttachmentsSet.set(i);
-                mColorFormats[i] = descriptor->fragment->targets[static_cast<uint8_t>(i)].format;
+                wgpu::TextureFormat format =
+                    descriptor->fragment->targets[static_cast<uint8_t>(i)].format;
+                if (format != wgpu::TextureFormat::Undefined) {
+                    mColorAttachmentsSet.set(i);
+                    mColorFormats[i] = format;
+                }
             }
         }
         if (descriptor->depthStencil != nullptr) {
@@ -55,6 +62,9 @@
              ++i) {
             TextureViewBase* attachment =
                 descriptor->colorAttachments[static_cast<uint8_t>(i)].view;
+            if (attachment == nullptr) {
+                continue;
+            }
             mColorAttachmentsSet.set(i);
             mColorFormats[i] = attachment->GetFormat().format;
             if (mSampleCount == 0) {
diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp
index da9ffc0..4c3e714 100644
--- a/src/dawn/native/CommandEncoder.cpp
+++ b/src/dawn/native/CommandEncoder.cpp
@@ -227,6 +227,9 @@
             uint32_t* sampleCount,
             UsageValidationMode usageValidationMode) {
             TextureViewBase* attachment = colorAttachment.view;
+            if (attachment == nullptr) {
+                return {};
+            }
             DAWN_TRY(device->ValidateObject(attachment));
             DAWN_TRY(ValidateCanUseAs(attachment->GetTexture(),
                                       wgpu::TextureUsage::RenderAttachment, usageValidationMode));
@@ -390,11 +393,15 @@
                 "Color attachment count (%u) exceeds the maximum number of color attachments (%u).",
                 descriptor->colorAttachmentCount, kMaxColorAttachments);
 
+            bool isAllColorAttachmentNull = true;
             for (uint32_t i = 0; i < descriptor->colorAttachmentCount; ++i) {
                 DAWN_TRY_CONTEXT(ValidateRenderPassColorAttachment(
                                      device, descriptor->colorAttachments[i], width, height,
                                      sampleCount, usageValidationMode),
                                  "validating colorAttachments[%u].", i);
+                if (descriptor->colorAttachments[i].view) {
+                    isAllColorAttachmentNull = false;
+                }
             }
 
             if (descriptor->depthStencilAttachment != nullptr) {
@@ -402,6 +409,10 @@
                                      device, descriptor->depthStencilAttachment, width, height,
                                      sampleCount, usageValidationMode),
                                  "validating depthStencilAttachment.");
+            } else {
+                DAWN_INVALID_IF(
+                    isAllColorAttachmentNull,
+                    "No color or depthStencil attachments specified. At least one is required.");
             }
 
             if (descriptor->occlusionQuerySet != nullptr) {
diff --git a/src/dawn/native/RenderBundleEncoder.cpp b/src/dawn/native/RenderBundleEncoder.cpp
index 421774c..6d7a2db 100644
--- a/src/dawn/native/RenderBundleEncoder.cpp
+++ b/src/dawn/native/RenderBundleEncoder.cpp
@@ -66,13 +66,14 @@
             "Color formats count (%u) exceeds maximum number of color attachements (%u).",
             descriptor->colorFormatsCount, kMaxColorAttachments);
 
-        DAWN_INVALID_IF(descriptor->colorFormatsCount == 0 &&
-                            descriptor->depthStencilFormat == wgpu::TextureFormat::Undefined,
-                        "No color or depth/stencil attachment formats specified.");
-
+        bool allColorFormatsUndefined = true;
         for (uint32_t i = 0; i < descriptor->colorFormatsCount; ++i) {
-            DAWN_TRY_CONTEXT(ValidateColorAttachmentFormat(device, descriptor->colorFormats[i]),
-                             "validating colorFormats[%u]", i);
+            wgpu::TextureFormat format = descriptor->colorFormats[i];
+            if (format != wgpu::TextureFormat::Undefined) {
+                DAWN_TRY_CONTEXT(ValidateColorAttachmentFormat(device, format),
+                                 "validating colorFormats[%u]", i);
+                allColorFormatsUndefined = false;
+            }
         }
 
         if (descriptor->depthStencilFormat != wgpu::TextureFormat::Undefined) {
@@ -80,6 +81,10 @@
                                  device, descriptor->depthStencilFormat, descriptor->depthReadOnly,
                                  descriptor->stencilReadOnly),
                              "validating depthStencilFormat");
+        } else {
+            DAWN_INVALID_IF(
+                allColorFormatsUndefined,
+                "No color or depthStencil attachments specified. At least one is required.");
         }
 
         return {};
diff --git a/src/dawn/native/RenderPipeline.cpp b/src/dawn/native/RenderPipeline.cpp
index 8cf13b9..b61b192e 100644
--- a/src/dawn/native/RenderPipeline.cpp
+++ b/src/dawn/native/RenderPipeline.cpp
@@ -430,11 +430,22 @@
                 descriptor->module->GetEntryPoint(descriptor->entryPoint);
             for (ColorAttachmentIndex i(uint8_t(0));
                  i < ColorAttachmentIndex(static_cast<uint8_t>(descriptor->targetCount)); ++i) {
-                DAWN_TRY_CONTEXT(
-                    ValidateColorTargetState(device, &descriptor->targets[static_cast<uint8_t>(i)],
-                                             fragmentMetadata.fragmentOutputsWritten[i],
-                                             fragmentMetadata.fragmentOutputVariables[i]),
-                    "validating targets[%u].", static_cast<uint8_t>(i));
+                const ColorTargetState* target = &descriptor->targets[static_cast<uint8_t>(i)];
+                if (target->format != wgpu::TextureFormat::Undefined) {
+                    DAWN_TRY_CONTEXT(ValidateColorTargetState(
+                                         device, target, fragmentMetadata.fragmentOutputsWritten[i],
+                                         fragmentMetadata.fragmentOutputVariables[i]),
+                                     "validating targets[%u].", static_cast<uint8_t>(i));
+                } else {
+                    DAWN_INVALID_IF(
+                        target->blend,
+                        "Color target[%u] blend state is set when the format is undefined.",
+                        static_cast<uint8_t>(i));
+                    DAWN_INVALID_IF(
+                        target->writeMask != wgpu::ColorWriteMask::None,
+                        "Color target[%u] write mask is set to (%s) when the format is undefined.",
+                        static_cast<uint8_t>(i), target->writeMask);
+                }
             }
 
             return {};
diff --git a/src/dawn/native/d3d12/CommandBufferD3D12.cpp b/src/dawn/native/d3d12/CommandBufferD3D12.cpp
index 83efc93..d16c92e 100644
--- a/src/dawn/native/d3d12/CommandBufferD3D12.cpp
+++ b/src/dawn/native/d3d12/CommandBufferD3D12.cpp
@@ -1197,43 +1197,68 @@
                                               RenderPassBuilder* renderPassBuilder) {
         Device* device = ToBackend(GetDevice());
 
-        for (ColorAttachmentIndex i :
-             IterateBitSet(renderPass->attachmentState->GetColorAttachmentsMask())) {
-            RenderPassColorAttachmentInfo& attachmentInfo = renderPass->colorAttachments[i];
-            TextureView* view = ToBackend(attachmentInfo.view.Get());
+        CPUDescriptorHeapAllocation nullRTVAllocation;
+        D3D12_CPU_DESCRIPTOR_HANDLE nullRTV;
 
-            // Set view attachment.
-            CPUDescriptorHeapAllocation rtvAllocation;
-            DAWN_TRY_ASSIGN(
-                rtvAllocation,
-                device->GetRenderTargetViewAllocator()->AllocateTransientCPUDescriptors());
+        const auto& colorAttachmentsMaskBitSet =
+            renderPass->attachmentState->GetColorAttachmentsMask();
+        for (ColorAttachmentIndex i(uint8_t(0)); i < ColorAttachmentIndex(kMaxColorAttachments);
+             i++) {
+            if (colorAttachmentsMaskBitSet.test(i)) {
+                RenderPassColorAttachmentInfo& attachmentInfo = renderPass->colorAttachments[i];
+                TextureView* view = ToBackend(attachmentInfo.view.Get());
 
-            const D3D12_RENDER_TARGET_VIEW_DESC viewDesc = view->GetRTVDescriptor();
-            const D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor = rtvAllocation.GetBaseDescriptor();
+                // Set view attachment.
+                CPUDescriptorHeapAllocation rtvAllocation;
+                DAWN_TRY_ASSIGN(
+                    rtvAllocation,
+                    device->GetRenderTargetViewAllocator()->AllocateTransientCPUDescriptors());
 
-            device->GetD3D12Device()->CreateRenderTargetView(
-                ToBackend(view->GetTexture())->GetD3D12Resource(), &viewDesc, baseDescriptor);
+                const D3D12_RENDER_TARGET_VIEW_DESC viewDesc = view->GetRTVDescriptor();
+                const D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor =
+                    rtvAllocation.GetBaseDescriptor();
 
-            renderPassBuilder->SetRenderTargetView(i, baseDescriptor);
+                device->GetD3D12Device()->CreateRenderTargetView(
+                    ToBackend(view->GetTexture())->GetD3D12Resource(), &viewDesc, baseDescriptor);
 
-            // Set color load operation.
-            renderPassBuilder->SetRenderTargetBeginningAccess(
-                i, attachmentInfo.loadOp, attachmentInfo.clearColor, view->GetD3D12Format());
+                renderPassBuilder->SetRenderTargetView(i, baseDescriptor, false);
 
-            // Set color store operation.
-            if (attachmentInfo.resolveTarget != nullptr) {
-                TextureView* resolveDestinationView = ToBackend(attachmentInfo.resolveTarget.Get());
-                Texture* resolveDestinationTexture =
-                    ToBackend(resolveDestinationView->GetTexture());
+                // Set color load operation.
+                renderPassBuilder->SetRenderTargetBeginningAccess(
+                    i, attachmentInfo.loadOp, attachmentInfo.clearColor, view->GetD3D12Format());
 
-                resolveDestinationTexture->TrackUsageAndTransitionNow(
-                    commandContext, D3D12_RESOURCE_STATE_RESOLVE_DEST,
-                    resolveDestinationView->GetSubresourceRange());
+                // Set color store operation.
+                if (attachmentInfo.resolveTarget != nullptr) {
+                    TextureView* resolveDestinationView =
+                        ToBackend(attachmentInfo.resolveTarget.Get());
+                    Texture* resolveDestinationTexture =
+                        ToBackend(resolveDestinationView->GetTexture());
 
-                renderPassBuilder->SetRenderTargetEndingAccessResolve(i, attachmentInfo.storeOp,
-                                                                      view, resolveDestinationView);
+                    resolveDestinationTexture->TrackUsageAndTransitionNow(
+                        commandContext, D3D12_RESOURCE_STATE_RESOLVE_DEST,
+                        resolveDestinationView->GetSubresourceRange());
+
+                    renderPassBuilder->SetRenderTargetEndingAccessResolve(
+                        i, attachmentInfo.storeOp, view, resolveDestinationView);
+                } else {
+                    renderPassBuilder->SetRenderTargetEndingAccess(i, attachmentInfo.storeOp);
+                }
             } else {
-                renderPassBuilder->SetRenderTargetEndingAccess(i, attachmentInfo.storeOp);
+                if (!nullRTVAllocation.IsValid()) {
+                    DAWN_TRY_ASSIGN(
+                        nullRTVAllocation,
+                        device->GetRenderTargetViewAllocator()->AllocateTransientCPUDescriptors());
+                    nullRTV = nullRTVAllocation.GetBaseDescriptor();
+                    D3D12_RENDER_TARGET_VIEW_DESC nullRTVDesc;
+                    nullRTVDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
+                    nullRTVDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
+                    nullRTVDesc.Texture2D.MipSlice = 0;
+                    nullRTVDesc.Texture2D.PlaneSlice = 0;
+                    device->GetD3D12Device()->CreateRenderTargetView(nullptr, &nullRTVDesc,
+                                                                     nullRTV);
+                }
+
+                renderPassBuilder->SetRenderTargetView(i, nullRTV, true);
             }
         }
 
@@ -1290,15 +1315,14 @@
 
         // Clear framebuffer attachments as needed.
         {
-            for (ColorAttachmentIndex i(uint8_t(0));
-                 i < renderPassBuilder->GetColorAttachmentCount(); i++) {
+            for (const auto& attachment :
+                 renderPassBuilder->GetRenderPassRenderTargetDescriptors()) {
                 // Load op - color
-                if (renderPassBuilder->GetRenderPassRenderTargetDescriptors()[i]
-                        .BeginningAccess.Type == D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR) {
+                if (attachment.cpuDescriptor.ptr != 0 &&
+                    attachment.BeginningAccess.Type ==
+                        D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR) {
                     commandList->ClearRenderTargetView(
-                        renderPassBuilder->GetRenderPassRenderTargetDescriptors()[i].cpuDescriptor,
-                        renderPassBuilder->GetRenderPassRenderTargetDescriptors()[i]
-                            .BeginningAccess.Clear.ClearValue.Color,
+                        attachment.cpuDescriptor, attachment.BeginningAccess.Clear.ClearValue.Color,
                         0, nullptr);
                 }
             }
@@ -1333,7 +1357,7 @@
         }
 
         commandList->OMSetRenderTargets(
-            static_cast<uint8_t>(renderPassBuilder->GetColorAttachmentCount()),
+            static_cast<uint8_t>(renderPassBuilder->GetHighestColorAttachmentIndexPlusOne()),
             renderPassBuilder->GetRenderTargetViews(), FALSE,
             renderPassBuilder->HasDepth()
                 ? &renderPassBuilder->GetRenderPassDepthStencilDescriptor()->cpuDescriptor
@@ -1358,7 +1382,7 @@
         // beginning and ending access operations.
         if (useRenderPass) {
             commandContext->GetCommandList4()->BeginRenderPass(
-                static_cast<uint8_t>(renderPassBuilder.GetColorAttachmentCount()),
+                static_cast<uint8_t>(renderPassBuilder.GetHighestColorAttachmentIndexPlusOne()),
                 renderPassBuilder.GetRenderPassRenderTargetDescriptors().data(),
                 renderPassBuilder.HasDepth()
                     ? renderPassBuilder.GetRenderPassDepthStencilDescriptor()
diff --git a/src/dawn/native/d3d12/RenderPassBuilderD3D12.cpp b/src/dawn/native/d3d12/RenderPassBuilderD3D12.cpp
index a1ce853..247850e 100644
--- a/src/dawn/native/d3d12/RenderPassBuilderD3D12.cpp
+++ b/src/dawn/native/d3d12/RenderPassBuilderD3D12.cpp
@@ -116,19 +116,24 @@
     }
 
     void RenderPassBuilder::SetRenderTargetView(ColorAttachmentIndex attachmentIndex,
-                                                D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor) {
-        ASSERT(mColorAttachmentCount < kMaxColorAttachmentsTyped);
+                                                D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor,
+                                                bool isNullRTV) {
         mRenderTargetViews[attachmentIndex] = baseDescriptor;
         mRenderPassRenderTargetDescriptors[attachmentIndex].cpuDescriptor = baseDescriptor;
-        mColorAttachmentCount++;
+        if (!isNullRTV) {
+            mHighestColorAttachmentIndexPlusOne =
+                std::max(mHighestColorAttachmentIndexPlusOne,
+                         ColorAttachmentIndex{
+                             static_cast<uint8_t>(static_cast<uint8_t>(attachmentIndex) + 1u)});
+        }
     }
 
     void RenderPassBuilder::SetDepthStencilView(D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor) {
         mRenderPassDepthStencilDesc.cpuDescriptor = baseDescriptor;
     }
 
-    ColorAttachmentIndex RenderPassBuilder::GetColorAttachmentCount() const {
-        return mColorAttachmentCount;
+    ColorAttachmentIndex RenderPassBuilder::GetHighestColorAttachmentIndexPlusOne() const {
+        return mHighestColorAttachmentIndexPlusOne;
     }
 
     bool RenderPassBuilder::HasDepth() const {
@@ -137,7 +142,7 @@
 
     ityp::span<ColorAttachmentIndex, const D3D12_RENDER_PASS_RENDER_TARGET_DESC>
     RenderPassBuilder::GetRenderPassRenderTargetDescriptors() const {
-        return {mRenderPassRenderTargetDescriptors.data(), mColorAttachmentCount};
+        return {mRenderPassRenderTargetDescriptors.data(), mHighestColorAttachmentIndexPlusOne};
     }
 
     const D3D12_RENDER_PASS_DEPTH_STENCIL_DESC*
diff --git a/src/dawn/native/d3d12/RenderPassBuilderD3D12.h b/src/dawn/native/d3d12/RenderPassBuilderD3D12.h
index 2b926a7c..cbbc29b 100644
--- a/src/dawn/native/d3d12/RenderPassBuilderD3D12.h
+++ b/src/dawn/native/d3d12/RenderPassBuilderD3D12.h
@@ -37,7 +37,9 @@
       public:
         RenderPassBuilder(bool hasUAV);
 
-        ColorAttachmentIndex GetColorAttachmentCount() const;
+        // Returns the highest color attachment index + 1. If there is no color attachment, returns
+        // 0. Range: [0, kMaxColorAttachments + 1)
+        ColorAttachmentIndex GetHighestColorAttachmentIndexPlusOne() const;
 
         // Returns descriptors that are fed directly to BeginRenderPass, or are used as parameter
         // storage if D3D12 render pass API is unavailable.
@@ -75,11 +77,12 @@
         void SetStencilNoAccess();
 
         void SetRenderTargetView(ColorAttachmentIndex attachmentIndex,
-                                 D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor);
+                                 D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor,
+                                 bool isNullRTV);
         void SetDepthStencilView(D3D12_CPU_DESCRIPTOR_HANDLE baseDescriptor);
 
       private:
-        ColorAttachmentIndex mColorAttachmentCount{uint8_t(0)};
+        ColorAttachmentIndex mHighestColorAttachmentIndexPlusOne{uint8_t(0)};
         bool mHasDepth = false;
         D3D12_RENDER_PASS_FLAGS mRenderPassFlags = D3D12_RENDER_PASS_FLAG_NONE;
         D3D12_RENDER_PASS_DEPTH_STENCIL_DESC mRenderPassDepthStencilDesc;
diff --git a/src/dawn/native/d3d12/RenderPipelineD3D12.cpp b/src/dawn/native/d3d12/RenderPipelineD3D12.cpp
index 1e40bd6..0e0e8f2 100644
--- a/src/dawn/native/d3d12/RenderPipelineD3D12.cpp
+++ b/src/dawn/native/d3d12/RenderPipelineD3D12.cpp
@@ -393,13 +393,24 @@
             descriptorD3D12.DSVFormat = D3D12TextureFormat(GetDepthStencilFormat());
         }
 
+        static_assert(kMaxColorAttachments == 8);
+        for (uint8_t i = 0; i < kMaxColorAttachments; i++) {
+            descriptorD3D12.RTVFormats[i] = DXGI_FORMAT_UNKNOWN;
+            descriptorD3D12.BlendState.RenderTarget[i].BlendEnable = false;
+            descriptorD3D12.BlendState.RenderTarget[i].RenderTargetWriteMask = 0;
+            descriptorD3D12.BlendState.RenderTarget[i].LogicOpEnable = false;
+            descriptorD3D12.BlendState.RenderTarget[i].LogicOp = D3D12_LOGIC_OP_NOOP;
+        }
+        ColorAttachmentIndex highestColorAttachmentIndexPlusOne =
+            GetHighestBitIndexPlusOne(GetColorAttachmentsMask());
         for (ColorAttachmentIndex i : IterateBitSet(GetColorAttachmentsMask())) {
             descriptorD3D12.RTVFormats[static_cast<uint8_t>(i)] =
                 D3D12TextureFormat(GetColorAttachmentFormat(i));
             descriptorD3D12.BlendState.RenderTarget[static_cast<uint8_t>(i)] =
                 ComputeColorDesc(GetColorTargetState(i));
         }
-        descriptorD3D12.NumRenderTargets = static_cast<uint32_t>(GetColorAttachmentsMask().count());
+        ASSERT(highestColorAttachmentIndexPlusOne <= kMaxColorAttachmentsTyped);
+        descriptorD3D12.NumRenderTargets = static_cast<uint8_t>(highestColorAttachmentIndexPlusOne);
 
         descriptorD3D12.BlendState.AlphaToCoverageEnable = IsAlphaToCoverageEnabled();
         descriptorD3D12.BlendState.IndependentBlendEnable = TRUE;
diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index 1fba2b3..8a40d30 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn/native/vulkan/CommandBufferVk.cpp
@@ -246,6 +246,9 @@
                      IterateBitSet(renderPass->attachmentState->GetColorAttachmentsMask())) {
                     auto& attachmentInfo = renderPass->colorAttachments[i];
                     TextureView* view = ToBackend(attachmentInfo.view.Get());
+                    if (view == nullptr) {
+                        continue;
+                    }
 
                     attachments[attachmentCount] = view->GetHandle();
 
diff --git a/src/dawn/native/vulkan/RenderPassCache.cpp b/src/dawn/native/vulkan/RenderPassCache.cpp
index 695dbd9..f1735ee 100644
--- a/src/dawn/native/vulkan/RenderPassCache.cpp
+++ b/src/dawn/native/vulkan/RenderPassCache.cpp
@@ -115,11 +115,23 @@
     ResultOrError<VkRenderPass> RenderPassCache::CreateRenderPassForQuery(
         const RenderPassCacheQuery& query) const {
         // The Vulkan subpasses want to know the layout of the attachments with VkAttachmentRef.
-        // Precompute them as they must be pointer-chained in VkSubpassDescription
-        std::array<VkAttachmentReference, kMaxColorAttachments> colorAttachmentRefs;
-        std::array<VkAttachmentReference, kMaxColorAttachments> resolveAttachmentRefs;
+        // Precompute them as they must be pointer-chained in VkSubpassDescription.
+        // Note that both colorAttachmentRefs and resolveAttachmentRefs can be sparse with holes
+        // filled with VK_ATTACHMENT_UNUSED.
+        ityp::array<ColorAttachmentIndex, VkAttachmentReference, kMaxColorAttachments>
+            colorAttachmentRefs;
+        ityp::array<ColorAttachmentIndex, VkAttachmentReference, kMaxColorAttachments>
+            resolveAttachmentRefs;
         VkAttachmentReference depthStencilAttachmentRef;
 
+        for (ColorAttachmentIndex i(uint8_t(0)); i < kMaxColorAttachmentsTyped; i++) {
+            colorAttachmentRefs[i].attachment = VK_ATTACHMENT_UNUSED;
+            resolveAttachmentRefs[i].attachment = VK_ATTACHMENT_UNUSED;
+            // The Khronos Vulkan validation layer will complain if not set
+            colorAttachmentRefs[i].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+            resolveAttachmentRefs[i].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+        }
+
         // Contains the attachment description that will be chained in the create info
         // The order of all attachments in attachmentDescs is "color-depthstencil-resolve".
         constexpr uint8_t kMaxAttachmentCount = kMaxColorAttachments * 2 + 1;
@@ -127,12 +139,13 @@
 
         VkSampleCountFlagBits vkSampleCount = VulkanSampleCount(query.sampleCount);
 
-        uint32_t colorAttachmentIndex = 0;
+        uint32_t attachmentCount = 0;
+        ColorAttachmentIndex highestColorAttachmentIndexPlusOne(static_cast<uint8_t>(0));
         for (ColorAttachmentIndex i : IterateBitSet(query.colorMask)) {
-            auto& attachmentRef = colorAttachmentRefs[colorAttachmentIndex];
-            auto& attachmentDesc = attachmentDescs[colorAttachmentIndex];
+            auto& attachmentRef = colorAttachmentRefs[i];
+            auto& attachmentDesc = attachmentDescs[attachmentCount];
 
-            attachmentRef.attachment = colorAttachmentIndex;
+            attachmentRef.attachment = attachmentCount;
             attachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
             attachmentDesc.flags = 0;
@@ -143,10 +156,11 @@
             attachmentDesc.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
             attachmentDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
-            ++colorAttachmentIndex;
+            attachmentCount++;
+            highestColorAttachmentIndexPlusOne =
+                ColorAttachmentIndex(static_cast<uint8_t>(static_cast<uint8_t>(i) + 1u));
         }
 
-        uint32_t attachmentCount = colorAttachmentIndex;
         VkAttachmentReference* depthStencilAttachment = nullptr;
         if (query.hasDepthStencil) {
             auto& attachmentDesc = attachmentDescs[attachmentCount];
@@ -172,12 +186,11 @@
             attachmentDesc.initialLayout = depthStencilAttachmentRef.layout;
             attachmentDesc.finalLayout = depthStencilAttachmentRef.layout;
 
-            ++attachmentCount;
+            attachmentCount++;
         }
 
-        uint32_t resolveAttachmentIndex = 0;
         for (ColorAttachmentIndex i : IterateBitSet(query.resolveTargetMask)) {
-            auto& attachmentRef = resolveAttachmentRefs[resolveAttachmentIndex];
+            auto& attachmentRef = resolveAttachmentRefs[i];
             auto& attachmentDesc = attachmentDescs[attachmentCount];
 
             attachmentRef.attachment = attachmentCount;
@@ -191,31 +204,18 @@
             attachmentDesc.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
             attachmentDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
-            ++attachmentCount;
-            ++resolveAttachmentIndex;
+            attachmentCount++;
         }
 
-        // All color attachments without a corresponding resolve attachment must be set to
-        // VK_ATTACHMENT_UNUSED
-        for (; resolveAttachmentIndex < colorAttachmentIndex; resolveAttachmentIndex++) {
-            auto& attachmentRef = resolveAttachmentRefs[resolveAttachmentIndex];
-            attachmentRef.attachment = VK_ATTACHMENT_UNUSED;
-            // The Khronos Vulkan validation layer will complain if not set
-            attachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
-        }
-
-        VkAttachmentReference* resolveTargetAttachmentRefs =
-            query.resolveTargetMask.any() ? resolveAttachmentRefs.data() : nullptr;
-
         // Create the VkSubpassDescription that will be chained in the VkRenderPassCreateInfo
         VkSubpassDescription subpassDesc;
         subpassDesc.flags = 0;
         subpassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
         subpassDesc.inputAttachmentCount = 0;
         subpassDesc.pInputAttachments = nullptr;
-        subpassDesc.colorAttachmentCount = colorAttachmentIndex;
+        subpassDesc.colorAttachmentCount = static_cast<uint8_t>(highestColorAttachmentIndexPlusOne);
         subpassDesc.pColorAttachments = colorAttachmentRefs.data();
-        subpassDesc.pResolveAttachments = resolveTargetAttachmentRefs;
+        subpassDesc.pResolveAttachments = resolveAttachmentRefs.data();
         subpassDesc.pDepthStencilAttachment = depthStencilAttachment;
         subpassDesc.preserveAttachmentCount = 0;
         subpassDesc.pPreserveAttachments = nullptr;
diff --git a/src/dawn/native/vulkan/RenderPipelineVk.cpp b/src/dawn/native/vulkan/RenderPipelineVk.cpp
index 580be6c..4f30496 100644
--- a/src/dawn/native/vulkan/RenderPipelineVk.cpp
+++ b/src/dawn/native/vulkan/RenderPipelineVk.cpp
@@ -457,8 +457,21 @@
         if (GetStageMask() & wgpu::ShaderStage::Fragment) {
             // Initialize the "blend state info" that will be chained in the "create info" from the
             // data pre-computed in the ColorState
+            for (auto& blend : colorBlendAttachments) {
+                blend.blendEnable = VK_FALSE;
+                blend.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
+                blend.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
+                blend.colorBlendOp = VK_BLEND_OP_ADD;
+                blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
+                blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
+                blend.alphaBlendOp = VK_BLEND_OP_ADD;
+                blend.colorWriteMask = 0;
+            }
+
             const auto& fragmentOutputsWritten =
                 GetStage(SingleShaderStage::Fragment).metadata->fragmentOutputsWritten;
+            ColorAttachmentIndex highestColorAttachmentIndexPlusOne =
+                GetHighestBitIndexPlusOne(GetColorAttachmentsMask());
             for (ColorAttachmentIndex i : IterateBitSet(GetColorAttachmentsMask())) {
                 const ColorTargetState* target = GetColorTargetState(i);
                 colorBlendAttachments[i] = ComputeColorDesc(target, fragmentOutputsWritten[i]);
@@ -470,7 +483,7 @@
             // LogicOp isn't supported so we disable it.
             colorBlend.logicOpEnable = VK_FALSE;
             colorBlend.logicOp = VK_LOGIC_OP_CLEAR;
-            colorBlend.attachmentCount = static_cast<uint32_t>(GetColorAttachmentsMask().count());
+            colorBlend.attachmentCount = static_cast<uint8_t>(highestColorAttachmentIndexPlusOne);
             colorBlend.pAttachments = colorBlendAttachments.data();
             // The blend constant is always dynamic so we fill in a dummy value
             colorBlend.blendConstants[0] = 0.0f;
diff --git a/src/dawn/tests/end2end/ColorStateTests.cpp b/src/dawn/tests/end2end/ColorStateTests.cpp
index 6040d61..9560d91 100644
--- a/src/dawn/tests/end2end/ColorStateTests.cpp
+++ b/src/dawn/tests/end2end/ColorStateTests.cpp
@@ -1095,6 +1095,69 @@
     EXPECT_PIXEL_RGBA8_EQ(expected, renderPass.color, kRTSize / 2, kRTSize / 2);
 }
 
+TEST_P(ColorStateTest, SparseAttachmentsDifferentColorMask) {
+    DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("disable_indexed_draw_buffers"));
+
+    wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, R"(
+        struct Outputs {
+            @location(1) o1 : vec4<f32>;
+            @location(3) o3 : vec4<f32>;
+        }
+
+        @stage(fragment) fn main() -> Outputs {
+            return Outputs(vec4<f32>(1.0), vec4<f32>(0.0, 1.0, 1.0, 1.0));
+        }
+    )");
+
+    utils::ComboRenderPipelineDescriptor pipelineDesc;
+    pipelineDesc.vertex.module = vsModule;
+    pipelineDesc.cFragment.module = fsModule;
+    pipelineDesc.cFragment.targetCount = 4;
+    pipelineDesc.cTargets[0].format = wgpu::TextureFormat::Undefined;
+    pipelineDesc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+    pipelineDesc.cTargets[1].format = wgpu::TextureFormat::RGBA8Unorm;
+    pipelineDesc.cTargets[2].format = wgpu::TextureFormat::Undefined;
+    pipelineDesc.cTargets[2].writeMask = wgpu::ColorWriteMask::None;
+    pipelineDesc.cTargets[3].format = wgpu::TextureFormat::RGBA8Unorm;
+    pipelineDesc.cTargets[3].writeMask = wgpu::ColorWriteMask::Green | wgpu::ColorWriteMask::Alpha;
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
+
+    wgpu::TextureDescriptor texDesc;
+    texDesc.dimension = wgpu::TextureDimension::e2D;
+    texDesc.format = wgpu::TextureFormat::RGBA8Unorm;
+    texDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+    texDesc.size = {1, 1};
+    wgpu::Texture attachment1 = device.CreateTexture(&texDesc);
+    wgpu::Texture attachment3 = device.CreateTexture(&texDesc);
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassColorAttachment colorAttachments[4]{};
+        colorAttachments[0].view = nullptr;
+        colorAttachments[1].view = attachment1.CreateView();
+        colorAttachments[1].loadOp = wgpu::LoadOp::Load;
+        colorAttachments[1].storeOp = wgpu::StoreOp::Store;
+        colorAttachments[2].view = nullptr;
+        colorAttachments[3].view = attachment3.CreateView();
+        colorAttachments[3].loadOp = wgpu::LoadOp::Load;
+        colorAttachments[3].storeOp = wgpu::StoreOp::Store;
+
+        wgpu::RenderPassDescriptor rpDesc;
+        rpDesc.colorAttachmentCount = 4;
+        rpDesc.colorAttachments = colorAttachments;
+
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rpDesc);
+        pass.SetPipeline(pipeline);
+        pass.Draw(3);
+        pass.End();
+    }
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kWhite, attachment1, 0, 0);
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kGreen, attachment3, 0, 0);
+}
+
 DAWN_INSTANTIATE_TEST(ColorStateTest,
                       D3D12Backend(),
                       MetalBackend(),
diff --git a/src/dawn/tests/end2end/RenderPassTests.cpp b/src/dawn/tests/end2end/RenderPassTests.cpp
index 0a6d2c6..85d39c0 100644
--- a/src/dawn/tests/end2end/RenderPassTests.cpp
+++ b/src/dawn/tests/end2end/RenderPassTests.cpp
@@ -159,7 +159,7 @@
     wgpu::CommandBuffer commands = encoder.Finish();
     queue.Submit(1, &commands);
 
-    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kBlue, renderTarget, 2, kRTSize - 1);
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kBlue, renderTarget, 1, kRTSize - 1);
     EXPECT_PIXEL_RGBA8_EQ(RGBA8::kRed, renderTarget, kRTSize - 1, 1);
 }
 
diff --git a/src/dawn/tests/unittests/ITypBitsetTests.cpp b/src/dawn/tests/unittests/ITypBitsetTests.cpp
index 8a82e41..6aa7fd2 100644
--- a/src/dawn/tests/unittests/ITypBitsetTests.cpp
+++ b/src/dawn/tests/unittests/ITypBitsetTests.cpp
@@ -23,6 +23,7 @@
   protected:
     using Key = TypedInteger<struct KeyT, size_t>;
     using Bitset = ityp::bitset<Key, 9>;
+    using Bitset40 = ityp::bitset<Key, 40>;
 
     // Test that the expected bitset methods can be constexpr
     struct ConstexprTest {
@@ -176,3 +177,33 @@
     bits ^= Bitset{1 << 1 | 1 << 6};
     ExpectBits(bits, {2, 6, 7});
 }
+
+// Testing the GetHighestBitIndexPlusOne function
+TEST_F(ITypBitsetTest, GetHighestBitIndexPlusOne) {
+    // <= 32 bit
+    EXPECT_EQ(0u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset(0b00))));
+    EXPECT_EQ(1u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset(0b01))));
+    EXPECT_EQ(2u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset(0b10))));
+    EXPECT_EQ(2u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset(0b11))));
+
+    EXPECT_EQ(3u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset{1 << 2})));
+    EXPECT_EQ(9u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset{1 << 8})));
+    EXPECT_EQ(9u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset{1 << 8 | 1 << 2})));
+
+    // > 32 bit
+    EXPECT_EQ(0u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0b00))));
+    EXPECT_EQ(1u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0b01))));
+    EXPECT_EQ(2u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0b10))));
+    EXPECT_EQ(2u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0b11))));
+
+    EXPECT_EQ(5u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0x10))));
+    EXPECT_EQ(5u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0x1F))));
+    EXPECT_EQ(16u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xF000))));
+    EXPECT_EQ(16u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xFFFF))));
+    EXPECT_EQ(32u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xF0000000))));
+    EXPECT_EQ(32u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xFFFFFFFF))));
+    EXPECT_EQ(36u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xF00000000))));
+    EXPECT_EQ(36u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xFFFFFFFFF))));
+    EXPECT_EQ(40u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xF000000000))));
+    EXPECT_EQ(40u, static_cast<size_t>(GetHighestBitIndexPlusOne(Bitset40(0xFFFFFFFFFF))));
+}
\ No newline at end of file
diff --git a/src/dawn/tests/unittests/validation/RenderBundleValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderBundleValidationTests.cpp
index 79f53fd..7474fd3 100644
--- a/src/dawn/tests/unittests/validation/RenderBundleValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderBundleValidationTests.cpp
@@ -635,12 +635,35 @@
     }
 }
 
-// Test that render bundle color formats cannot be set to undefined.
-TEST_F(RenderBundleValidationTest, ColorFormatUndefined) {
-    utils::ComboRenderBundleEncoderDescriptor desc = {};
-    desc.colorFormatsCount = 1;
-    desc.cColorFormats[0] = wgpu::TextureFormat::Undefined;
-    ASSERT_DEVICE_ERROR(device.CreateRenderBundleEncoder(&desc));
+// Test that render bundle sparse color formats.
+TEST_F(RenderBundleValidationTest, SparseColorFormats) {
+    // Sparse color formats is valid.
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 2;
+        desc.cColorFormats[0] = wgpu::TextureFormat::Undefined;
+        desc.cColorFormats[1] = wgpu::TextureFormat::RGBA8Unorm;
+        device.CreateRenderBundleEncoder(&desc);
+    }
+
+    // When all color formats are undefined, depth stencil format must not be undefined.
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 1;
+        desc.cColorFormats[0] = wgpu::TextureFormat::Undefined;
+        desc.depthStencilFormat = wgpu::TextureFormat::Undefined;
+        ASSERT_DEVICE_ERROR(
+            device.CreateRenderBundleEncoder(&desc),
+            testing::HasSubstr(
+                "No color or depthStencil attachments specified. At least one is required."));
+    }
+    {
+        utils::ComboRenderBundleEncoderDescriptor desc = {};
+        desc.colorFormatsCount = 1;
+        desc.cColorFormats[0] = wgpu::TextureFormat::Undefined;
+        desc.depthStencilFormat = wgpu::TextureFormat::Depth24PlusStencil8;
+        device.CreateRenderBundleEncoder(&desc);
+    }
 }
 
 // Test that the render bundle depth stencil format cannot be set to undefined.
diff --git a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
index b687fed..cd10233 100644
--- a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
@@ -130,6 +130,62 @@
         }
     }
 
+    // Test sparse color attachment validations
+    TEST_F(RenderPassDescriptorValidationTest, SparseColorAttachment) {
+        // Having sparse color attachment is valid.
+        {
+            std::array<wgpu::RenderPassColorAttachment, 2> colorAttachments;
+            colorAttachments[0].view = nullptr;
+
+            colorAttachments[1].view =
+                Create2DAttachment(device, 1, 1, wgpu::TextureFormat::RGBA8Unorm);
+            colorAttachments[1].loadOp = wgpu::LoadOp::Load;
+            colorAttachments[1].storeOp = wgpu::StoreOp::Store;
+
+            wgpu::RenderPassDescriptor renderPass;
+            renderPass.colorAttachmentCount = colorAttachments.size();
+            renderPass.colorAttachments = colorAttachments.data();
+            renderPass.depthStencilAttachment = nullptr;
+            AssertBeginRenderPassSuccess(&renderPass);
+        }
+
+        // When all color attachments are null
+        {
+            std::array<wgpu::RenderPassColorAttachment, 2> colorAttachments;
+            colorAttachments[0].view = nullptr;
+            colorAttachments[1].view = nullptr;
+
+            // Control case: depth stencil attachment is not null is valid.
+            {
+                wgpu::TextureView depthStencilView =
+                    Create2DAttachment(device, 1, 1, wgpu::TextureFormat::Depth24PlusStencil8);
+                wgpu::RenderPassDepthStencilAttachment depthStencilAttachment;
+                depthStencilAttachment.view = depthStencilView;
+                depthStencilAttachment.depthClearValue = 1.0f;
+                depthStencilAttachment.stencilClearValue = 0;
+                depthStencilAttachment.depthLoadOp = wgpu::LoadOp::Clear;
+                depthStencilAttachment.depthStoreOp = wgpu::StoreOp::Store;
+                depthStencilAttachment.stencilLoadOp = wgpu::LoadOp::Clear;
+                depthStencilAttachment.stencilStoreOp = wgpu::StoreOp::Store;
+
+                wgpu::RenderPassDescriptor renderPass;
+                renderPass.colorAttachmentCount = colorAttachments.size();
+                renderPass.colorAttachments = colorAttachments.data();
+                renderPass.depthStencilAttachment = &depthStencilAttachment;
+                AssertBeginRenderPassSuccess(&renderPass);
+            }
+
+            // Error case: depth stencil attachment being null is invalid.
+            {
+                wgpu::RenderPassDescriptor renderPass;
+                renderPass.colorAttachmentCount = colorAttachments.size();
+                renderPass.colorAttachments = colorAttachments.data();
+                renderPass.depthStencilAttachment = nullptr;
+                AssertBeginRenderPassError(&renderPass);
+            }
+        }
+    }
+
     // Check that the render pass color attachment must have the RenderAttachment usage.
     TEST_F(RenderPassDescriptorValidationTest, ColorAttachmentInvalidUsage) {
         // Control case: using a texture with RenderAttachment is valid.
diff --git a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
index 856ec2a..9d279bf 100644
--- a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -193,6 +193,48 @@
     }
 }
 
+// Tests that target blend and writeMasks must not be set if the format is undefined.
+TEST_F(RenderPipelineValidationTest, UndefinedColorStateFormatWithBlendOrWriteMask) {
+    {
+        // Control case: Valid undefined format target.
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        descriptor.cFragment.targetCount = 1;
+        descriptor.cTargets[0].format = wgpu::TextureFormat::Undefined;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+
+        device.CreateRenderPipeline(&descriptor);
+    }
+    {
+        // Error case: undefined format target with blend state set.
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        descriptor.cFragment.targetCount = 1;
+        descriptor.cTargets[0].format = wgpu::TextureFormat::Undefined;
+        descriptor.cTargets[0].blend = &descriptor.cBlends[0];
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+
+        ASSERT_DEVICE_ERROR(
+            device.CreateRenderPipeline(&descriptor),
+            testing::HasSubstr("Color target[0] blend state is set when the format is undefined."));
+    }
+    {
+        // Error case: undefined format target with write masking not being none.
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+        descriptor.cFragment.module = fsModule;
+        descriptor.cFragment.targetCount = 1;
+        descriptor.cTargets[0].format = wgpu::TextureFormat::Undefined;
+        descriptor.cTargets[0].blend = nullptr;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::All;
+
+        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor),
+                            testing::HasSubstr("Color target[0] write mask is set to"));
+    }
+}
+
 // Tests that the color formats must be renderable.
 TEST_F(RenderPipelineValidationTest, NonRenderableFormat) {
     {