Validate that the color write mask is zero if there is no fragment output

Following spec change https://github.com/gpuweb/gpuweb/pull/1918

Fixed: dawn:962
Change-Id: I9d7eaee588301227736cf523bec46e5a60fe861b
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/59042
Commit-Queue: Austin Eng <enga@chromium.org>
Reviewed-by: Jiawei Shao <jiawei.shao@intel.com>
diff --git a/src/dawn_native/RenderPipeline.cpp b/src/dawn_native/RenderPipeline.cpp
index 3831e95..7eff93d 100644
--- a/src/dawn_native/RenderPipeline.cpp
+++ b/src/dawn_native/RenderPipeline.cpp
@@ -277,10 +277,17 @@
                 return DAWN_VALIDATION_ERROR(
                     "Color format must be blendable when blending is enabled");
             }
-            if (fragmentWritten &&
-                fragmentOutputBaseType != format->GetAspectInfo(Aspect::Color).baseType) {
-                return DAWN_VALIDATION_ERROR(
-                    "Color format must match the fragment stage output type");
+            if (fragmentWritten) {
+                if (fragmentOutputBaseType != format->GetAspectInfo(Aspect::Color).baseType) {
+                    return DAWN_VALIDATION_ERROR(
+                        "Color format must match the fragment stage output type");
+                }
+            } else {
+                if (descriptor->writeMask != wgpu::ColorWriteMask::None) {
+                    return DAWN_VALIDATION_ERROR(
+                        "writeMask must be zero for color targets with no corresponding fragment "
+                        "stage output");
+                }
             }
 
             return {};
diff --git a/src/tests/end2end/FirstIndexOffsetTests.cpp b/src/tests/end2end/FirstIndexOffsetTests.cpp
index b3c6481..92ad316 100644
--- a/src/tests/end2end/FirstIndexOffsetTests.cpp
+++ b/src/tests/end2end/FirstIndexOffsetTests.cpp
@@ -155,6 +155,7 @@
     pipelineDesc.cBuffers[0].attributeCount = 1;
     pipelineDesc.cAttributes[0].format = wgpu::VertexFormat::Float32x4;
     pipelineDesc.cTargets[0].format = renderPass.colorFormat;
+    pipelineDesc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
     wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
 
diff --git a/src/tests/end2end/ObjectCachingTests.cpp b/src/tests/end2end/ObjectCachingTests.cpp
index a42e7f0..bc35fdd 100644
--- a/src/tests/end2end/ObjectCachingTests.cpp
+++ b/src/tests/end2end/ObjectCachingTests.cpp
@@ -208,6 +208,7 @@
     EXPECT_EQ(pl.Get() == samePl.Get(), !UsesWire());
 
     utils::ComboRenderPipelineDescriptor desc;
+    desc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
     desc.vertex.module = utils::CreateShaderModule(device, R"(
         [[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
             return vec4<f32>(0.0, 0.0, 0.0, 0.0);
@@ -248,6 +249,7 @@
     EXPECT_EQ(module.Get() == sameModule.Get(), !UsesWire());
 
     utils::ComboRenderPipelineDescriptor desc;
+    desc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
     desc.cFragment.module = utils::CreateShaderModule(device, R"(
             [[stage(fragment)]] fn main() {
             })");
@@ -288,6 +290,7 @@
         })");
 
     desc.cFragment.module = module;
+    desc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
     wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
 
     desc.cFragment.module = sameModule;
diff --git a/src/tests/end2end/RenderPassTests.cpp b/src/tests/end2end/RenderPassTests.cpp
index c522db3..7db2b3d 100644
--- a/src/tests/end2end/RenderPassTests.cpp
+++ b/src/tests/end2end/RenderPassTests.cpp
@@ -145,6 +145,7 @@
         descriptor.cFragment.module = fsModule;
         descriptor.primitive.topology = wgpu::PrimitiveTopology::TriangleList;
         descriptor.cTargets[0].format = kFormat;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
         wgpu::RenderPipeline pipelineWithNoFragmentOutput =
             device.CreateRenderPipeline(&descriptor);
diff --git a/src/tests/end2end/StorageTextureTests.cpp b/src/tests/end2end/StorageTextureTests.cpp
index 47e082c..63f9fad 100644
--- a/src/tests/end2end/StorageTextureTests.cpp
+++ b/src/tests/end2end/StorageTextureTests.cpp
@@ -370,12 +370,17 @@
                 UNREACHABLE();
                 break;
         }
-        auto workgroupSize = !strcmp(stage, "compute") ? ", workgroup_size(1)" : "";
+        const char* workgroupSize = !strcmp(stage, "compute") ? ", workgroup_size(1)" : "";
+        const bool isFragment = strcmp(stage, "fragment") == 0;
 
         std::ostringstream ostream;
         ostream << GetImageDeclaration(format, "write", dimension, 0) << "\n";
         ostream << "[[stage(" << stage << ")" << workgroupSize << "]]\n";
-        ostream << "fn main() {\n";
+        ostream << "fn main() ";
+        if (isFragment) {
+            ostream << "-> [[location(0)]] vec4<f32> ";
+        }
+        ostream << "{\n";
         ostream << "  let size : vec2<i32> = textureDimensions(storageImage0).xy;\n";
         ostream << "  let sliceCount : i32 = " << sliceCount << ";\n";
         ostream << "  for (var slice : i32 = 0; slice < sliceCount; slice = slice + 1) {\n";
@@ -388,6 +393,9 @@
         ostream << "      }\n";
         ostream << "    }\n";
         ostream << "  }\n";
+        if (isFragment) {
+            ostream << "return vec4<f32>();\n";
+        }
         ostream << "}\n";
 
         return ostream.str();
@@ -616,11 +624,11 @@
     }
 
     void WriteIntoStorageTextureInRenderPass(wgpu::Texture writeonlyStorageTexture,
-                                             const char* kVertexShader,
-                                             const char* kFragmentShader) {
+                                             const char* vertexShader,
+                                             const char* fragmentShader) {
         // Create a render pipeline that writes the expected pixel values into the storage texture
         // without fragment shader outputs.
-        wgpu::RenderPipeline pipeline = CreateRenderPipeline(kVertexShader, kFragmentShader);
+        wgpu::RenderPipeline pipeline = CreateRenderPipeline(vertexShader, fragmentShader);
         wgpu::BindGroup bindGroup = utils::MakeBindGroup(
             device, pipeline.GetBindGroupLayout(0), {{0, writeonlyStorageTexture.CreateView()}});
 
@@ -1263,8 +1271,9 @@
     const char* kCommonWriteOnlyZeroInitTestCodeFragment = R"(
 [[group(0), binding(0)]] var dstImage : texture_storage_2d<r32uint, write>;
 
-[[stage(fragment)]] fn main() {
+[[stage(fragment)]] fn main() -> [[location(0)]] vec4<f32> {
   textureStore(dstImage, vec2<i32>(0, 0), vec4<u32>(1u, 0u, 0u, 1u));
+  return vec4<f32>();
 })";
     const char* kCommonWriteOnlyZeroInitTestCodeCompute = R"(
 [[group(0), binding(0)]] var dstImage : texture_storage_2d<r32uint, write>;
diff --git a/src/tests/end2end/SwapChainValidationTests.cpp b/src/tests/end2end/SwapChainValidationTests.cpp
index 045795b..624b09d 100644
--- a/src/tests/end2end/SwapChainValidationTests.cpp
+++ b/src/tests/end2end/SwapChainValidationTests.cpp
@@ -226,8 +226,15 @@
             return vec4<f32>(0.0, 0.0, 0.0, 1.0);
         })");
     pipelineDesc.cFragment.module = utils::CreateShaderModule(device, R"(
-        [[stage(fragment)]] fn main() -> [[location(0)]] vec4<f32> {
-            return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+        struct FragmentOut {
+            [[location(0)]] target0 : vec4<f32>;
+            [[location(1)]] target1 : f32;
+        };
+        [[stage(fragment)]] fn main() -> FragmentOut {
+            var out : FragmentOut;
+            out.target0 = vec4<f32>(0.0, 1.0, 0.0, 1.0);
+            out.target1 = 0.5;
+            return out;
         })");
     // Validation will check that the sample count of the view matches this format.
     pipelineDesc.multisample.count = 1;
diff --git a/src/tests/unittests/validation/BindGroupValidationTests.cpp b/src/tests/unittests/validation/BindGroupValidationTests.cpp
index 32a5a5d..8f6250c 100644
--- a/src/tests/unittests/validation/BindGroupValidationTests.cpp
+++ b/src/tests/unittests/validation/BindGroupValidationTests.cpp
@@ -1359,6 +1359,7 @@
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
         pipelineDescriptor.vertex.module = vsModule;
         pipelineDescriptor.cFragment.module = fsModule;
+        pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
         wgpu::PipelineLayout pipelineLayout =
             utils::MakeBasicPipelineLayout(device, &mBindGroupLayout);
         pipelineDescriptor.layout = pipelineLayout;
@@ -1818,6 +1819,7 @@
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
         pipelineDescriptor.vertex.module = mVsModule;
         pipelineDescriptor.cFragment.module = fsModule;
+        pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
         pipelineDescriptor.layout = pipelineLayout;
         wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
 
@@ -1957,6 +1959,7 @@
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
         pipelineDescriptor.vertex.module = vsModule;
         pipelineDescriptor.cFragment.module = fsModule;
+        pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
         wgpu::PipelineLayout pipelineLayout = device.CreatePipelineLayout(&descriptor);
         pipelineDescriptor.layout = pipelineLayout;
         return device.CreateRenderPipeline(&pipelineDescriptor);
@@ -2350,6 +2353,7 @@
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
         pipelineDescriptor.vertex.module = vsModule;
         pipelineDescriptor.cFragment.module = fsModule;
+        pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
         wgpu::PipelineLayout pipelineLayout =
             utils::MakeBasicPipelineLayout(device, bindGroupLayout);
         pipelineDescriptor.layout = pipelineLayout;
diff --git a/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp b/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp
index 6ea50b6..4515a24 100644
--- a/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp
+++ b/src/tests/unittests/validation/GetBindGroupLayoutValidationTests.cpp
@@ -31,6 +31,7 @@
         descriptor.layout = nullptr;
         descriptor.vertex.module = vsModule;
         descriptor.cFragment.module = fsModule;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
         return device.CreateRenderPipeline(&descriptor);
     }
@@ -77,6 +78,7 @@
     descriptor.layout = nullptr;
     descriptor.vertex.module = vsModule;
     descriptor.cFragment.module = fsModule;
+    descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
     wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&descriptor);
 
@@ -213,6 +215,7 @@
         utils::ComboRenderPipelineDescriptor descriptor;
         descriptor.vertex.module = vertexModule;
         descriptor.cFragment.module = fragmentModule;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
         return device.CreateRenderPipeline(&descriptor).GetBindGroupLayout(0);
     };
 
@@ -637,6 +640,7 @@
     descriptor.layout = nullptr;
     descriptor.vertex.module = vsModule;
     descriptor.cFragment.module = fsModule;
+    descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
     device.CreateRenderPipeline(&descriptor);
 }
@@ -707,6 +711,7 @@
 
     utils::ComboRenderPipelineDescriptor descriptor;
     descriptor.layout = nullptr;
+    descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
     // Check with both stages using 4 bytes.
     {
@@ -773,6 +778,7 @@
 
     utils::ComboRenderPipelineDescriptor descriptor;
     descriptor.layout = nullptr;
+    descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
     // Check with only the vertex shader using the sampler
     {
@@ -999,6 +1005,7 @@
     pipelineDesc.layout = pipelineLayout;
     pipelineDesc.vertex.module = vsModule;
     pipelineDesc.cFragment.module = fsModule;
+    pipelineDesc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
     wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
 
diff --git a/src/tests/unittests/validation/MinimumBufferSizeValidationTests.cpp b/src/tests/unittests/validation/MinimumBufferSizeValidationTests.cpp
index b841501..e673361 100644
--- a/src/tests/unittests/validation/MinimumBufferSizeValidationTests.cpp
+++ b/src/tests/unittests/validation/MinimumBufferSizeValidationTests.cpp
@@ -193,6 +193,7 @@
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
         pipelineDescriptor.vertex.module = vsModule;
         pipelineDescriptor.cFragment.module = fsModule;
+        pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
         pipelineDescriptor.layout = nullptr;
         if (!layouts.empty()) {
             wgpu::PipelineLayoutDescriptor descriptor;
diff --git a/src/tests/unittests/validation/RenderBundleValidationTests.cpp b/src/tests/unittests/validation/RenderBundleValidationTests.cpp
index b6da94e..d5e3fa8 100644
--- a/src/tests/unittests/validation/RenderBundleValidationTests.cpp
+++ b/src/tests/unittests/validation/RenderBundleValidationTests.cpp
@@ -102,6 +102,7 @@
             descriptor->layout = pipelineLayout;
             descriptor->vertex.module = vsModule;
             descriptor->cFragment.module = fsModule;
+            descriptor->cTargets[0].writeMask = wgpu::ColorWriteMask::None;
             descriptor->vertex.bufferCount = 1;
             descriptor->cBuffers[0].arrayStride = 2 * sizeof(float);
             descriptor->cBuffers[0].attributeCount = 1;
@@ -735,6 +736,9 @@
         desc->cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
         desc->cTargets[1].format = wgpu::TextureFormat::RG16Float;
         desc->cTargets[2].format = wgpu::TextureFormat::R16Sint;
+        desc->cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+        desc->cTargets[1].writeMask = wgpu::ColorWriteMask::None;
+        desc->cTargets[2].writeMask = wgpu::ColorWriteMask::None;
     };
 
     // Test the success case.
diff --git a/src/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
index 62371e7..628a37a 100644
--- a/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -456,6 +456,7 @@
                     ignore(textureDimensions(myTexture));
                 })";
             descriptor.cFragment.module = utils::CreateShaderModule(device, stream.str().c_str());
+            descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
             wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
                 device, {{0, wgpu::ShaderStage::Fragment, kTextureComponentTypes[j]}});
@@ -504,6 +505,7 @@
                     ignore(textureDimensions(myTexture));
                 })";
             descriptor.cFragment.module = utils::CreateShaderModule(device, stream.str().c_str());
+            descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
 
             wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
                 device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float,
@@ -753,6 +755,138 @@
     ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor));
 }
 
+// Test that unwritten fragment outputs must have a write mask of 0.
+TEST_F(RenderPipelineValidationTest, UnwrittenFragmentOutputsMask0) {
+    wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
+        [[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
+            return vec4<f32>();
+        }
+    )");
+
+    wgpu::ShaderModule fsModuleWriteNone = utils::CreateShaderModule(device, R"(
+        [[stage(fragment)]] fn main() {}
+    )");
+
+    wgpu::ShaderModule fsModuleWrite0 = utils::CreateShaderModule(device, R"(
+        [[stage(fragment)]] fn main() -> [[location(0)]] vec4<f32> {
+            return vec4<f32>();
+        }
+    )");
+
+    wgpu::ShaderModule fsModuleWrite1 = utils::CreateShaderModule(device, R"(
+        [[stage(fragment)]] fn main() -> [[location(1)]] vec4<f32> {
+            return vec4<f32>();
+        }
+    )");
+
+    wgpu::ShaderModule fsModuleWriteBoth = utils::CreateShaderModule(device, R"(
+        struct FragmentOut {
+            [[location(0)]] target0 : vec4<f32>;
+            [[location(1)]] target1 : vec4<f32>;
+        };
+        [[stage(fragment)]] fn main() -> FragmentOut {
+            var out : FragmentOut;
+            return out;
+        }
+    )");
+
+    // Control case: write to target 0
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 1;
+        descriptor.cFragment.module = fsModuleWrite0;
+        device.CreateRenderPipeline(&descriptor);
+    }
+
+    // Control case: write to target 0 and target 1
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 2;
+        descriptor.cFragment.module = fsModuleWriteBoth;
+        device.CreateRenderPipeline(&descriptor);
+    }
+
+    // Write only target 1 (not in pipeline fragment state).
+    // Errors because target 0 does not have a write mask of 0.
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 1;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::All;
+        descriptor.cFragment.module = fsModuleWrite1;
+        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor));
+    }
+
+    // Write only target 1 (not in pipeline fragment state).
+    // OK because target 0 has a write mask of 0.
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 1;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+        descriptor.cFragment.module = fsModuleWrite1;
+        device.CreateRenderPipeline(&descriptor);
+    }
+
+    // Write only target 0 with two color targets.
+    // Errors because target 1 does not have a write mask of 0.
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 2;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::Red;
+        descriptor.cTargets[1].writeMask = wgpu::ColorWriteMask::Alpha;
+        descriptor.cFragment.module = fsModuleWrite0;
+        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor));
+    }
+
+    // Write only target 0 with two color targets.
+    // OK because target 1 has a write mask of 0.
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 2;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::All;
+        descriptor.cTargets[1].writeMask = wgpu::ColorWriteMask::None;
+        descriptor.cFragment.module = fsModuleWrite0;
+        device.CreateRenderPipeline(&descriptor);
+    }
+
+    // Write nothing with two color targets.
+    // Errors because both target 0 and 1 have nonzero write masks.
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 2;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::Red;
+        descriptor.cTargets[1].writeMask = wgpu::ColorWriteMask::Green;
+        descriptor.cFragment.module = fsModuleWriteNone;
+        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor));
+    }
+
+    // Write nothing with two color targets.
+    // OK because target 0 and 1 have write masks of 0.
+    {
+        utils::ComboRenderPipelineDescriptor descriptor;
+        descriptor.vertex.module = vsModule;
+
+        descriptor.cFragment.targetCount = 2;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+        descriptor.cTargets[1].writeMask = wgpu::ColorWriteMask::None;
+        descriptor.cFragment.module = fsModuleWriteNone;
+        device.CreateRenderPipeline(&descriptor);
+    }
+}
+
 // Test that fragment output validation is for the correct entryPoint
 // TODO(dawn:216): Re-enable when we correctly reflect which bindings are used for an entryPoint.
 TEST_F(RenderPipelineValidationTest, DISABLED_BindingsFromCorrectEntryPoint) {
diff --git a/src/tests/unittests/validation/ResourceUsageTrackingTests.cpp b/src/tests/unittests/validation/ResourceUsageTrackingTests.cpp
index 0cf64d8..8ba045d 100644
--- a/src/tests/unittests/validation/ResourceUsageTrackingTests.cpp
+++ b/src/tests/unittests/validation/ResourceUsageTrackingTests.cpp
@@ -57,6 +57,7 @@
             utils::ComboRenderPipelineDescriptor pipelineDescriptor;
             pipelineDescriptor.vertex.module = vsModule;
             pipelineDescriptor.cFragment.module = fsModule;
+            pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
             pipelineDescriptor.layout = utils::MakeBasicPipelineLayout(device, nullptr);
             return device.CreateRenderPipeline(&pipelineDescriptor);
         }
@@ -768,6 +769,7 @@
             utils::ComboRenderPipelineDescriptor pipelineDescriptor;
             pipelineDescriptor.vertex.module = vsModule;
             pipelineDescriptor.cFragment.module = fsModule;
+            pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
             pipelineDescriptor.layout = utils::MakeBasicPipelineLayout(device, &bgl0);
             wgpu::RenderPipeline rp = device.CreateRenderPipeline(&pipelineDescriptor);
 
@@ -1578,6 +1580,7 @@
             utils::ComboRenderPipelineDescriptor pipelineDescriptor;
             pipelineDescriptor.vertex.module = vsModule;
             pipelineDescriptor.cFragment.module = fsModule;
+            pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
             pipelineDescriptor.layout = utils::MakeBasicPipelineLayout(device, &readBGL);
             wgpu::RenderPipeline rp = device.CreateRenderPipeline(&pipelineDescriptor);
 
diff --git a/src/tests/unittests/validation/StorageTextureValidationTests.cpp b/src/tests/unittests/validation/StorageTextureValidationTests.cpp
index 8a10014..98d9ec4 100644
--- a/src/tests/unittests/validation/StorageTextureValidationTests.cpp
+++ b/src/tests/unittests/validation/StorageTextureValidationTests.cpp
@@ -178,6 +178,7 @@
         descriptor.layout = nullptr;
         descriptor.vertex.module = mDefaultVSModule;
         descriptor.cFragment.module = fsModule;
+        descriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
         device.CreateRenderPipeline(&descriptor);
     }
 }
diff --git a/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp b/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp
index 7504bc9..abac251 100644
--- a/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp
+++ b/src/tests/unittests/validation/UnsafeAPIValidationTests.cpp
@@ -55,6 +55,7 @@
             return vec4<f32>();
         })");
     desc.cFragment.module = utils::CreateShaderModule(device, "[[stage(fragment)]] fn main() {}");
+    desc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
     wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);
 
     // Control cases: DrawIndirect and DrawIndexed are allowed inside a render pass.