Add basic supports of readonly and writeonly storage textures on Metal

This patch adds the basic supports of both read-only and write-only
storage textures on Metal with several simple end2end tests that use
read-only or write-only storage textures in every shader stage.

Here are the follow-ups after this patch:
1. test all the texture formats that can be used as both read-only and
write-only storage textures.
2. support using a texture with multiple different binding types in one
rendering or compute encoders.
3. test image2DArray, imageCube and imageCubeArray.

BUG=dawn:267
TEST=dawn_end2end_tests

Change-Id: Id0de623f7c48389b3b1e90b34a34fd16b14e1477
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/19420
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: Jiawei Shao <jiawei.shao@intel.com>
diff --git a/src/dawn_native/metal/CommandBufferMTL.mm b/src/dawn_native/metal/CommandBufferMTL.mm
index e032692..186b1ea 100644
--- a/src/dawn_native/metal/CommandBufferMTL.mm
+++ b/src/dawn_native/metal/CommandBufferMTL.mm
@@ -589,7 +589,9 @@
                             break;
                         }
 
-                        case wgpu::BindingType::SampledTexture: {
+                        case wgpu::BindingType::SampledTexture:
+                        case wgpu::BindingType::ReadonlyStorageTexture:
+                        case wgpu::BindingType::WriteonlyStorageTexture: {
                             auto textureView =
                                 ToBackend(group->GetBindingAsTextureView(bindingIndex));
                             if (hasVertStage) {
@@ -608,8 +610,6 @@
                         }
 
                         case wgpu::BindingType::StorageTexture:
-                        case wgpu::BindingType::ReadonlyStorageTexture:
-                        case wgpu::BindingType::WriteonlyStorageTexture:
                             UNREACHABLE();
                             break;
                     }
diff --git a/src/dawn_native/metal/PipelineLayoutMTL.mm b/src/dawn_native/metal/PipelineLayoutMTL.mm
index 2f5b569..9b2c6b2 100644
--- a/src/dawn_native/metal/PipelineLayoutMTL.mm
+++ b/src/dawn_native/metal/PipelineLayoutMTL.mm
@@ -50,12 +50,12 @@
                             samplerIndex++;
                             break;
                         case wgpu::BindingType::SampledTexture:
+                        case wgpu::BindingType::ReadonlyStorageTexture:
+                        case wgpu::BindingType::WriteonlyStorageTexture:
                             mIndexInfo[stage][group][bindingIndex] = textureIndex;
                             textureIndex++;
                             break;
                         case wgpu::BindingType::StorageTexture:
-                        case wgpu::BindingType::ReadonlyStorageTexture:
-                        case wgpu::BindingType::WriteonlyStorageTexture:
                             UNREACHABLE();
                             break;
                     }
diff --git a/src/tests/DawnTest.cpp b/src/tests/DawnTest.cpp
index bffa776..60fa7be 100644
--- a/src/tests/DawnTest.cpp
+++ b/src/tests/DawnTest.cpp
@@ -532,6 +532,10 @@
     return gTestEnv->IsSpvcBeingUsed();
 }
 
+bool DawnTestBase::IsSpvcParserBeingUsed() const {
+    return gTestEnv->IsSpvcParserBeingUsed();
+}
+
 bool DawnTestBase::HasVendorIdFilter() const {
     return gTestEnv->HasVendorIdFilter();
 }
diff --git a/src/tests/DawnTest.h b/src/tests/DawnTest.h
index 20ca258..d019bfd 100644
--- a/src/tests/DawnTest.h
+++ b/src/tests/DawnTest.h
@@ -204,6 +204,7 @@
     bool IsBackendValidationEnabled() const;
     bool IsDawnValidationSkipped() const;
     bool IsSpvcBeingUsed() const;
+    bool IsSpvcParserBeingUsed() const;
 
     void StartExpectDeviceError();
     bool EndExpectDeviceError();
diff --git a/src/tests/end2end/StorageTextureTests.cpp b/src/tests/end2end/StorageTextureTests.cpp
index c13a3a1..da9d983 100644
--- a/src/tests/end2end/StorageTextureTests.cpp
+++ b/src/tests/end2end/StorageTextureTests.cpp
@@ -14,7 +14,196 @@
 
 #include "tests/DawnTest.h"
 
-class StorageTextureTests : public DawnTest {};
+#include "common/Assert.h"
+#include "common/Constants.h"
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/WGPUHelpers.h"
+
+class StorageTextureTests : public DawnTest {
+  public:
+    // TODO(jiawei.shao@intel.com): support all formats that can be used in storage textures.
+    static std::vector<uint32_t> GetExpectedData() {
+        constexpr size_t kDataCount = kWidth * kHeight;
+        std::vector<uint32_t> outputData(kDataCount);
+        for (size_t i = 0; i < kDataCount; ++i) {
+            outputData[i] = static_cast<uint32_t>(i + 1u);
+        }
+        return outputData;
+    }
+
+    wgpu::Texture CreateTexture(wgpu::TextureFormat format,
+                                wgpu::TextureUsage usage,
+                                uint32_t width = kWidth,
+                                uint32_t height = kHeight) {
+        wgpu::TextureDescriptor descriptor;
+        descriptor.size = {width, height, 1};
+        descriptor.format = format;
+        descriptor.usage = usage;
+        return device.CreateTexture(&descriptor);
+    }
+
+    wgpu::Buffer CreateEmptyBufferForTextureCopy(uint32_t texelSize) {
+        ASSERT(kWidth * texelSize <= kTextureBytesPerRowAlignment);
+        const size_t uploadBufferSize =
+            kTextureBytesPerRowAlignment * (kHeight - 1) + kWidth * texelSize;
+        wgpu::BufferDescriptor descriptor;
+        descriptor.size = uploadBufferSize;
+        descriptor.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
+        return device.CreateBuffer(&descriptor);
+    }
+
+    // TODO(jiawei.shao@intel.com): support all formats that can be used in storage textures.
+    wgpu::Texture CreateTextureWithTestData(const std::vector<uint32_t>& initialTextureData,
+                                            uint32_t texelSize) {
+        ASSERT(kWidth * texelSize <= kTextureBytesPerRowAlignment);
+        const size_t uploadBufferSize =
+            kTextureBytesPerRowAlignment * (kHeight - 1) + kWidth * texelSize;
+        std::vector<uint32_t> uploadBufferData(uploadBufferSize / texelSize);
+
+        const size_t texelCountPerRow = kTextureBytesPerRowAlignment / texelSize;
+        for (size_t y = 0; y < kHeight; ++y) {
+            for (size_t x = 0; x < kWidth; ++x) {
+                uint32_t data = initialTextureData[kWidth * y + x];
+
+                size_t indexInUploadBuffer = y * texelCountPerRow + x;
+                uploadBufferData[indexInUploadBuffer] = data;
+            }
+        }
+        wgpu::Buffer uploadBuffer =
+            utils::CreateBufferFromData(device, uploadBufferData.data(), uploadBufferSize,
+                                        wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst);
+
+        wgpu::Texture outputTexture =
+            CreateTexture(wgpu::TextureFormat::R32Uint,
+                          wgpu::TextureUsage::Storage | wgpu::TextureUsage::CopyDst);
+
+        wgpu::BufferCopyView bufferCopyView =
+            utils::CreateBufferCopyView(uploadBuffer, 0, kTextureBytesPerRowAlignment, 0);
+        wgpu::TextureCopyView textureCopyView;
+        textureCopyView.texture = outputTexture;
+        wgpu::Extent3D copyExtent = {kWidth, kHeight, 1};
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyBufferToTexture(&bufferCopyView, &textureCopyView, &copyExtent);
+        wgpu::CommandBuffer commandBuffer = encoder.Finish();
+        queue.Submit(1, &commandBuffer);
+
+        return outputTexture;
+    }
+
+    wgpu::ComputePipeline CreateComputePipeline(const char* computeShader) {
+        wgpu::ShaderModule csModule =
+            utils::CreateShaderModule(device, utils::SingleShaderStage::Compute, computeShader);
+        wgpu::ComputePipelineDescriptor computeDescriptor;
+        computeDescriptor.layout = nullptr;
+        computeDescriptor.computeStage.module = csModule;
+        computeDescriptor.computeStage.entryPoint = "main";
+        return device.CreateComputePipeline(&computeDescriptor);
+    }
+
+    wgpu::RenderPipeline CreateRenderPipeline(const char* vertexShader,
+                                              const char* fragmentShader) {
+        wgpu::ShaderModule vsModule =
+            utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, vertexShader);
+        wgpu::ShaderModule fsModule =
+            utils::CreateShaderModule(device, utils::SingleShaderStage::Fragment, fragmentShader);
+
+        utils::ComboRenderPipelineDescriptor desc(device);
+        desc.vertexStage.module = vsModule;
+        desc.cFragmentStage.module = fsModule;
+        desc.cColorStates[0].format = kOutputAttachmentFormat;
+        desc.primitiveTopology = wgpu::PrimitiveTopology::PointList;
+        return device.CreateRenderPipeline(&desc);
+    }
+
+    void CheckDrawsGreen(const char* vertexShader,
+                         const char* fragmentShader,
+                         wgpu::Texture readonlyStorageTexture) {
+        wgpu::RenderPipeline pipeline = CreateRenderPipeline(vertexShader, fragmentShader);
+        wgpu::BindGroup bindGroup = utils::MakeBindGroup(
+            device, pipeline.GetBindGroupLayout(0), {{0, readonlyStorageTexture.CreateView()}});
+
+        // Clear the output attachment to red at the beginning of the render pass.
+        wgpu::Texture outputTexture =
+            CreateTexture(kOutputAttachmentFormat,
+                          wgpu::TextureUsage::OutputAttachment | wgpu::TextureUsage::CopySrc, 1, 1);
+        utils::ComboRenderPassDescriptor renderPassDescriptor({outputTexture.CreateView()});
+        renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+        renderPassDescriptor.cColorAttachments[0].clearColor = {1.f, 0.f, 0.f, 1.f};
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder renderPassEncoder = encoder.BeginRenderPass(&renderPassDescriptor);
+        renderPassEncoder.SetBindGroup(0, bindGroup);
+        renderPassEncoder.SetPipeline(pipeline);
+        renderPassEncoder.Draw(1);
+        renderPassEncoder.EndPass();
+
+        wgpu::CommandBuffer commandBuffer = encoder.Finish();
+        queue.Submit(1, &commandBuffer);
+
+        // Check if the contents in the output texture are all as expected (green).
+        EXPECT_PIXEL_RGBA8_EQ(RGBA8::kGreen, outputTexture, 0, 0);
+    }
+
+    void CheckOutputStorageTexture(wgpu::Texture writeonlyStorageTexture, uint32_t texelSize) {
+        // Copy the content from the write-only storage texture to the result buffer.
+        wgpu::Buffer resultBuffer = CreateEmptyBufferForTextureCopy(texelSize);
+        wgpu::BufferCopyView bufferCopyView =
+            utils::CreateBufferCopyView(resultBuffer, 0, kTextureBytesPerRowAlignment, 0);
+        wgpu::TextureCopyView textureCopyView;
+        textureCopyView.texture = writeonlyStorageTexture;
+        wgpu::Extent3D copyExtent = {kWidth, kHeight, 1};
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        encoder.CopyTextureToBuffer(&textureCopyView, &bufferCopyView, &copyExtent);
+
+        wgpu::CommandBuffer commandBuffer = encoder.Finish();
+        queue.Submit(1, &commandBuffer);
+
+        // Check if the contents in the result buffer are what we expect.
+        const std::vector<uint32_t> kInitialTextureData = GetExpectedData();
+        for (size_t y = 0; y < kHeight; ++y) {
+            const size_t resultBufferOffset = kTextureBytesPerRowAlignment * y;
+            EXPECT_BUFFER_U32_RANGE_EQ(kInitialTextureData.data() + kWidth * y, resultBuffer,
+                                       resultBufferOffset, kWidth);
+        }
+    }
+
+    static constexpr size_t kWidth = 4u;
+    static constexpr size_t kHeight = 4u;
+    static constexpr wgpu::TextureFormat kOutputAttachmentFormat = wgpu::TextureFormat::RGBA8Unorm;
+
+    const char* kSimpleVertexShader = R"(
+        #version 450
+        void main() {
+            gl_Position = vec4(0.f, 0.f, 0.f, 1.f);
+        })";
+
+    const char* kCommonReadOnlyTestCode_uimage2D = R"(
+        bool doTest() {
+            for (uint y = 0; y < 4; ++y) {
+                for (uint x = 0; x < 4; ++x) {
+                    uvec4 expected = uvec4(1u + x + y * 4u, 0, 0, 1u);
+                    uvec4 pixel = imageLoad(srcImage, ivec2(x, y));
+                    if (pixel != expected) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        })";
+
+    const char* kCommonWriteOnlyTestCode_uimage2D = R"(
+        #version 450
+        layout(set = 0, binding = 0, r32ui) uniform writeonly uimage2D dstImage;
+        void main() {
+            for (uint y = 0; y < 4; ++y) {
+                for (uint x = 0; x < 4; ++x) {
+                    uvec4 pixel = uvec4(1u + x + y * 4u, 0, 0, 1u);
+                    imageStore(dstImage, ivec2(x, y), pixel);
+                }
+            }
+        })";
+};
 
 // Test that using read-only storage texture and write-only storage texture in BindGroupLayout is
 // valid on all backends. This test is a regression test for chromium:1061156 and passes by not
@@ -45,6 +234,209 @@
     }
 }
 
+// Test that read-only storage textures are supported in compute shader.
+TEST_P(StorageTextureTests, ReadonlyStorageTextureInComputeShader) {
+    // TODO(jiawei.shao@intel.com): support read-only storage texture on D3D12, Vulkan and OpenGL.
+    DAWN_SKIP_TEST_IF(IsD3D12() || IsVulkan() || IsOpenGL());
+
+    // Prepare the read-only storage texture and fill it with the expected data.
+    // TODO(jiawei.shao@intel.com): test more texture formats.
+    constexpr uint32_t kTexelSizeR32Uint = 4u;
+    const std::vector<uint32_t> kInitialTextureData = GetExpectedData();
+    wgpu::Texture readonlyStorageTexture =
+        CreateTextureWithTestData(kInitialTextureData, kTexelSizeR32Uint);
+
+    // Create a compute shader that reads the pixels from the read-only storage texture and writes 1
+    // to DstBuffer if they all have to expected value.
+    const std::string kComputeShader = std::string(R"(
+        #version 450
+        layout (set = 0, binding = 0, r32ui) uniform readonly uimage2D srcImage;
+        layout (set = 0, binding = 1, std430) buffer DstBuffer {
+            uint result;
+        } dstBuffer;)") + kCommonReadOnlyTestCode_uimage2D +
+                                       R"(
+        void main() {
+            if (doTest()) {
+                dstBuffer.result = 1;
+            } else {
+                dstBuffer.result = 0;
+            }
+        })";
+
+    wgpu::ComputePipeline pipeline = CreateComputePipeline(kComputeShader.c_str());
+
+    // Clear the content of the result buffer into 0.
+    constexpr uint32_t kInitialValue = 0;
+    wgpu::Buffer resultBuffer =
+        utils::CreateBufferFromData(device, &kInitialValue, sizeof(kInitialValue),
+                                    wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc);
+    wgpu::BindGroup bindGroup =
+        utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                             {{0, readonlyStorageTexture.CreateView()}, {1, resultBuffer}});
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::ComputePassEncoder computeEncoder = encoder.BeginComputePass();
+    computeEncoder.SetBindGroup(0, bindGroup);
+    computeEncoder.SetPipeline(pipeline);
+    computeEncoder.Dispatch(1);
+    computeEncoder.EndPass();
+
+    wgpu::CommandBuffer commandBuffer = encoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    // Check if the contents in the result buffer are what we expect.
+    constexpr uint32_t kExpectedValue = 1u;
+    EXPECT_BUFFER_U32_RANGE_EQ(&kExpectedValue, resultBuffer, 0, 1u);
+}
+
+// Test that read-only storage textures are supported in vertex shader.
+TEST_P(StorageTextureTests, ReadonlyStorageTextureInVertexShader) {
+    // TODO(jiawei.shao@intel.com): support read-only storage texture on D3D12, Vulkan and OpenGL.
+    DAWN_SKIP_TEST_IF(IsD3D12() || IsVulkan() || IsOpenGL());
+
+    // When we run dawn_end2end_tests with "--use-spvc-parser", extracting the binding type of a
+    // read-only image will always return shaderc_spvc_binding_type_writeonly_storage_texture.
+    // TODO(jiawei.shao@intel.com): enable this test when we specify "--use-spvc-parser" after the
+    // bug in spvc parser is fixed.
+    DAWN_SKIP_TEST_IF(IsSpvcParserBeingUsed());
+
+    // Prepare the read-only storage texture and fill it with the expected data.
+    // TODO(jiawei.shao@intel.com): test more texture formats
+    constexpr uint32_t kTexelSizeR32Uint = 4u;
+    const std::vector<uint32_t> kInitialTextureData = GetExpectedData();
+    wgpu::Texture readonlyStorageTexture =
+        CreateTextureWithTestData(kInitialTextureData, kTexelSizeR32Uint);
+
+    // Create a rendering pipeline that reads the pixels from the read-only storage texture and uses
+    // green as the output color, otherwise uses red instead.
+    const std::string kVertexShader = std::string(R"(
+            #version 450
+            layout(set = 0, binding = 0, r32ui) uniform readonly uimage2D srcImage;
+            layout(location = 0) out vec4 o_color;)") +
+                                      kCommonReadOnlyTestCode_uimage2D + R"(
+            void main() {
+                gl_Position = vec4(0.f, 0.f, 0.f, 1.f);
+                if (doTest()) {
+                    o_color = vec4(0.f, 1.f, 0.f, 1.f);
+                } else {
+                    o_color = vec4(1.f, 0.f, 0.f, 1.f);
+                }
+            })";
+    const char* kFragmentShader = R"(
+            #version 450
+            layout(location = 0) in vec4 o_color;
+            layout(location = 0) out vec4 fragColor;
+            void main() {
+                fragColor = o_color;
+            })";
+    CheckDrawsGreen(kVertexShader.c_str(), kFragmentShader, readonlyStorageTexture);
+}
+
+// Test that read-only storage textures are supported in fragment shader.
+TEST_P(StorageTextureTests, ReadonlyStorageTextureInFragmentShader) {
+    // TODO(jiawei.shao@intel.com): support read-only storage texture on D3D12, Vulkan and OpenGL.
+    DAWN_SKIP_TEST_IF(IsD3D12() || IsVulkan() || IsOpenGL());
+
+    // When we run dawn_end2end_tests with "--use-spvc-parser", extracting the binding type of a
+    // read-only image will always return shaderc_spvc_binding_type_writeonly_storage_texture.
+    // TODO(jiawei.shao@intel.com): enable this test when we specify "--use-spvc-parser" after the
+    // bug in spvc parser is fixed.
+    DAWN_SKIP_TEST_IF(IsSpvcParserBeingUsed());
+
+    // Prepare the read-only storage texture and fill it with the expected data.
+    // TODO(jiawei.shao@intel.com): test more texture formats
+    constexpr uint32_t kTexelSizeR32Uint = 4u;
+    const std::vector<uint32_t> kInitialTextureData = GetExpectedData();
+    wgpu::Texture readonlyStorageTexture =
+        CreateTextureWithTestData(kInitialTextureData, kTexelSizeR32Uint);
+
+    // Create a rendering pipeline that reads the pixels from the read-only storage texture and uses
+    // green as the output color, otherwise uses red instead.
+    const char* kVertexShader = kSimpleVertexShader;
+    const std::string kFragmentShader = std::string(R"(
+            #version 450
+            layout(set = 0, binding = 0, r32ui) uniform readonly uimage2D srcImage;
+            layout(location = 0) out vec4 o_color;)") +
+                                        kCommonReadOnlyTestCode_uimage2D + R"(
+            void main() {
+                if (doTest()) {
+                    o_color = vec4(0.f, 1.f, 0.f, 1.f);
+                } else {
+                    o_color = vec4(1.f, 0.f, 0.f, 1.f);
+                }
+            })";
+    CheckDrawsGreen(kVertexShader, kFragmentShader.c_str(), readonlyStorageTexture);
+}
+
+// Test that write-only storage textures are supported in compute shader.
+TEST_P(StorageTextureTests, WriteonlyStorageTextureInComputeShader) {
+    // TODO(jiawei.shao@intel.com): support read-only storage texture on D3D12, Vulkan and OpenGL.
+    DAWN_SKIP_TEST_IF(IsD3D12() || IsVulkan() || IsOpenGL());
+
+    // Prepare the write-only storage texture.
+    // TODO(jiawei.shao@intel.com): test more texture formats.
+    constexpr uint32_t kTexelSizeR32Uint = 4u;
+    wgpu::Texture writeonlyStorageTexture = CreateTexture(
+        wgpu::TextureFormat::R32Uint, wgpu::TextureUsage::Storage | wgpu::TextureUsage::CopySrc);
+
+    // Create a compute shader that writes the expected pixel values into the storage texture.
+    const char* kComputeShader = kCommonWriteOnlyTestCode_uimage2D;
+
+    wgpu::ComputePipeline pipeline = CreateComputePipeline(kComputeShader);
+    wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                     {{0, writeonlyStorageTexture.CreateView()}});
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::ComputePassEncoder computePassEncoder = encoder.BeginComputePass();
+    computePassEncoder.SetBindGroup(0, bindGroup);
+    computePassEncoder.SetPipeline(pipeline);
+    computePassEncoder.Dispatch(1);
+    computePassEncoder.EndPass();
+    wgpu::CommandBuffer commandBuffer = encoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    CheckOutputStorageTexture(writeonlyStorageTexture, kTexelSizeR32Uint);
+}
+
+// Test that write-only storage textures are supported in fragment shader.
+TEST_P(StorageTextureTests, WriteonlyStorageTextureInFragmentShader) {
+    // TODO(jiawei.shao@intel.com): support read-only storage texture on D3D12, Vulkan and OpenGL.
+    DAWN_SKIP_TEST_IF(IsD3D12() || IsVulkan() || IsOpenGL());
+
+    // Prepare the write-only storage texture.
+    // TODO(jiawei.shao@intel.com): test more texture formats.
+    constexpr uint32_t kTexelSizeR32Uint = 4u;
+    wgpu::Texture writeonlyStorageTexture = CreateTexture(
+        wgpu::TextureFormat::R32Uint, wgpu::TextureUsage::Storage | wgpu::TextureUsage::CopySrc);
+
+    // Create a render pipeline that writes the expected pixel values into the storage texture
+    // without fragment shader outputs.
+    const char* kVertexShader = kSimpleVertexShader;
+    const char* kFragmentShader = kCommonWriteOnlyTestCode_uimage2D;
+
+    wgpu::RenderPipeline pipeline = CreateRenderPipeline(kVertexShader, kFragmentShader);
+    wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                     {{0, writeonlyStorageTexture.CreateView()}});
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+    // TODO(jiawei.shao@intel.com): remove the output attachment when Dawn supports beginning a
+    // render pass with no attachments.
+    wgpu::Texture dummyOutputTexture =
+        CreateTexture(kOutputAttachmentFormat,
+                      wgpu::TextureUsage::OutputAttachment | wgpu::TextureUsage::CopySrc, 1, 1);
+    utils::ComboRenderPassDescriptor renderPassDescriptor({dummyOutputTexture.CreateView()});
+    wgpu::RenderPassEncoder renderPassEncoder = encoder.BeginRenderPass(&renderPassDescriptor);
+    renderPassEncoder.SetBindGroup(0, bindGroup);
+    renderPassEncoder.SetPipeline(pipeline);
+    renderPassEncoder.Draw(1);
+    renderPassEncoder.EndPass();
+    wgpu::CommandBuffer commandBuffer = encoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    CheckOutputStorageTexture(writeonlyStorageTexture, kTexelSizeR32Uint);
+}
+
 DAWN_INSTANTIATE_TEST(StorageTextureTests,
                       D3D12Backend(),
                       MetalBackend(),