Support separate depth-stencil readonlyness

Implements support for having separate values for depth/stencilReadOnly
in beginRenderPass. This works without any changes in all backends but
the Vulkan backend.

On the Vulkan backend, new internal wgpu::TextureUsages are introduced
that represent mixed readonly/not depth-stencil attachment. They can
also be combined with wgpu::TextureUsage::TextureBinding to signify that
the readonly aspect will be used for sampling.

The ReadOnlyDepthStencil end2end and validation tests are refactored for
easier extensibility, and extended to test the newly allowed mixed
readonlyness render attachments.

Changes are:
 - Fix a typo in IndirectDrawValidationEncoder.
 - Make TextureD3D11 not rely on CombinedDepthStencil as that aspect is
   supposed to be internal to the Vulkan backend.
 - Introduce a kReservedTextureUsage bit and use it for
   kAgainAsRenderAttachment that's not just intneral to
   PassResourceUsageTracker.
 - Change the validation in CommandEncoder to allow mixed readonlyness
   and correctly track such mixed usages.
 - Make the Vulkan backend require VK_KHR_maintenance2 so that we can
   use the new VK_IMAGE_LAYOUTs it introduces.
 - Change the VulkanImageLayout function to take a Format() instead of a
   whole vulkan::Texture object.
 - Make the vulkan::RenderPassCache handle separate readonlyness of
   depth-stencil.
 - Updated the vulkan::Texture logic for barriers to handle separate
   readonlyness (that's the most complicated part of this CL).
 - Rewrite/expand readonly attachment tests.
 - Enable these tests on OpenGL.
 - Add suppressions for the CTS testing the previous behavior.

Bug: dawn:2146

Change-Id: Ic4151efd28f8735bc8a7e5119a72c85c29f7d124
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/155441
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Loko Kung <lokokung@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp
index a25ef7c..bf638d2 100644
--- a/src/dawn/native/CommandEncoder.cpp
+++ b/src/dawn/native/CommandEncoder.cpp
@@ -424,12 +424,14 @@
                     "The depth stencil attachment %s format (%s) is not renderable.", attachment,
                     format.format);
 
-    DAWN_INVALID_IF(
-        attachment->GetAspects() == (Aspect::Depth | Aspect::Stencil) &&
-            depthStencilAttachment->depthReadOnly != depthStencilAttachment->stencilReadOnly,
-        "depthReadOnly (%u) and stencilReadOnly (%u) must be the same when texture aspect "
-        "is 'all'.",
-        depthStencilAttachment->depthReadOnly, depthStencilAttachment->stencilReadOnly);
+    if (!device->IsToggleEnabled(Toggle::AllowUnsafeAPIs)) {
+        DAWN_INVALID_IF(
+            attachment->GetAspects() == (Aspect::Depth | Aspect::Stencil) &&
+                depthStencilAttachment->depthReadOnly != depthStencilAttachment->stencilReadOnly,
+            "depthReadOnly (%u) and stencilReadOnly (%u) must be the same when texture aspect "
+            "is 'all'.",
+            depthStencilAttachment->depthReadOnly, depthStencilAttachment->stencilReadOnly);
+    }
 
     // Read only, or depth doesn't exist.
     if (depthStencilAttachment->depthReadOnly ||
@@ -740,21 +742,6 @@
                                                 paramsBuffer.Get());
 }
 
-bool IsReadOnlyDepthStencilAttachment(
-    const RenderPassDepthStencilAttachment* depthStencilAttachment) {
-    DAWN_ASSERT(depthStencilAttachment != nullptr);
-    Aspect aspects = depthStencilAttachment->view->GetAspects();
-    DAWN_ASSERT(IsSubset(aspects, Aspect::Depth | Aspect::Stencil));
-
-    if ((aspects & Aspect::Depth) && !depthStencilAttachment->depthReadOnly) {
-        return false;
-    }
-    if (aspects & Aspect::Stencil && !depthStencilAttachment->stencilReadOnly) {
-        return false;
-    }
-    return true;
-}
-
 // Load resolve texture to MSAA attachment if needed.
 MaybeError ApplyMSAARenderToSingleSampledLoadOp(DeviceBase* device,
                                                 RenderPassEncoder* renderPassEncoder,
@@ -1086,8 +1073,10 @@
 
             if (cmd->attachmentState->HasDepthStencilAttachment()) {
                 TextureViewBase* view = descriptor->depthStencilAttachment->view;
-
+                TextureBase* attachment = view->GetTexture();
                 cmd->depthStencilAttachment.view = view;
+                // Range that will be modified per aspect to track the usage.
+                SubresourceRange usageRange = view->GetSubresourceRange();
 
                 switch (descriptor->depthStencilAttachment->depthLoadOp) {
                     case wgpu::LoadOp::Clear:
@@ -1101,57 +1090,67 @@
                         cmd->depthStencilAttachment.clearDepth = 0.f;
                         break;
                 }
-                cmd->depthStencilAttachment.clearStencil =
-                    descriptor->depthStencilAttachment->stencilClearValue;
 
-                // Copy parameters for the depth, reyifing the values when it is not present or
-                // readonly.
+                // GPURenderPassDepthStencilAttachment.stencilClearValue will be converted to
+                // the type of the stencil aspect of view by taking the same number of LSBs as
+                // the number of bits in the stencil aspect of one texel block of view.
+                DAWN_ASSERT(!(view->GetFormat().aspects & Aspect::Stencil) ||
+                            view->GetFormat().GetAspectInfo(Aspect::Stencil).block.byteSize == 1u);
+                cmd->depthStencilAttachment.clearStencil =
+                    descriptor->depthStencilAttachment->stencilClearValue & 0xFF;
+
+                // Depth aspect:
+                //  - Copy parameters for the aspect, reyifing the values when it is not present or
+                //  readonly.
+                //  - Export depthReadOnly to the outside of the depth-stencil attachment handling.
+                //  - Track the usage of this aspect.
+                depthReadOnly = descriptor->depthStencilAttachment->depthReadOnly;
+
                 cmd->depthStencilAttachment.depthReadOnly = false;
                 cmd->depthStencilAttachment.depthLoadOp = wgpu::LoadOp::Load;
                 cmd->depthStencilAttachment.depthStoreOp = wgpu::StoreOp::Store;
-                if (view->GetFormat().HasDepth()) {
-                    cmd->depthStencilAttachment.depthReadOnly =
-                        descriptor->depthStencilAttachment->depthReadOnly;
-                    if (!cmd->depthStencilAttachment.depthReadOnly) {
+                if (attachment->GetFormat().HasDepth()) {
+                    cmd->depthStencilAttachment.depthReadOnly = depthReadOnly;
+                    if (!depthReadOnly) {
                         cmd->depthStencilAttachment.depthLoadOp =
                             descriptor->depthStencilAttachment->depthLoadOp;
                         cmd->depthStencilAttachment.depthStoreOp =
                             descriptor->depthStencilAttachment->depthStoreOp;
                     }
+
+                    usageRange.aspects = Aspect::Depth;
+                    usageTracker.TextureRangeUsedAs(attachment, usageRange,
+                                                    depthReadOnly
+                                                        ? kReadOnlyRenderAttachment
+                                                        : wgpu::TextureUsage::RenderAttachment);
                 }
 
-                // Copy parameters for the stencil, reyifing the values when it is not present or
-                // readonly.
+                // Stencil aspect:
+                //  - Copy parameters for the aspect, reyifing the values when it is not present or
+                //  readonly.
+                //  - Export stencilReadOnly to the outside of the depth-stencil attachment
+                //  handling.
+                //  - Track the usage of this aspect.
+                stencilReadOnly = descriptor->depthStencilAttachment->stencilReadOnly;
+
                 cmd->depthStencilAttachment.stencilReadOnly = false;
                 cmd->depthStencilAttachment.stencilLoadOp = wgpu::LoadOp::Load;
                 cmd->depthStencilAttachment.stencilStoreOp = wgpu::StoreOp::Store;
-                if (view->GetFormat().HasStencil()) {
-                    cmd->depthStencilAttachment.stencilReadOnly =
-                        descriptor->depthStencilAttachment->stencilReadOnly;
-                    if (!cmd->depthStencilAttachment.stencilReadOnly) {
+                if (attachment->GetFormat().HasStencil()) {
+                    cmd->depthStencilAttachment.stencilReadOnly = stencilReadOnly;
+                    if (!stencilReadOnly) {
                         cmd->depthStencilAttachment.stencilLoadOp =
                             descriptor->depthStencilAttachment->stencilLoadOp;
                         cmd->depthStencilAttachment.stencilStoreOp =
                             descriptor->depthStencilAttachment->stencilStoreOp;
                     }
 
-                    // GPURenderPassDepthStencilAttachment.stencilClearValue will be converted to
-                    // the type of the stencil aspect of view by taking the same number of LSBs as
-                    // the number of bits in the stencil aspect of one texel block of view.
-                    DAWN_ASSERT(view->GetFormat()
-                                    .GetAspectInfo(dawn::native::Aspect::Stencil)
-                                    .block.byteSize == 1u);
-                    cmd->depthStencilAttachment.clearStencil &= 0xFF;
+                    usageRange.aspects = Aspect::Stencil;
+                    usageTracker.TextureRangeUsedAs(attachment, usageRange,
+                                                    stencilReadOnly
+                                                        ? kReadOnlyRenderAttachment
+                                                        : wgpu::TextureUsage::RenderAttachment);
                 }
-
-                if (IsReadOnlyDepthStencilAttachment(descriptor->depthStencilAttachment)) {
-                    usageTracker.TextureViewUsedAs(view, kReadOnlyRenderAttachment);
-                } else {
-                    usageTracker.TextureViewUsedAs(view, wgpu::TextureUsage::RenderAttachment);
-                }
-
-                depthReadOnly = descriptor->depthStencilAttachment->depthReadOnly;
-                stencilReadOnly = descriptor->depthStencilAttachment->stencilReadOnly;
             }
 
             cmd->width = width;
diff --git a/src/dawn/native/IndirectDrawValidationEncoder.cpp b/src/dawn/native/IndirectDrawValidationEncoder.cpp
index 05da03c..7391461 100644
--- a/src/dawn/native/IndirectDrawValidationEncoder.cpp
+++ b/src/dawn/native/IndirectDrawValidationEncoder.cpp
@@ -107,7 +107,7 @@
 
             fn numIndirectParamsPerDrawCallOutput() -> u32 {
                 var numParams = numIndirectParamsPerDrawCallInput();
-                // 2 extra parameter for duplicated first/baseVexter and firstInstance
+                // 2 extra parameter for duplicated first/baseVertex and firstInstance
                 if (bool(batch.flags & kDuplicateBaseVertexInstance)) {
                     numParams = numParams + 2u;
                 }
diff --git a/src/dawn/native/PassResourceUsageTracker.cpp b/src/dawn/native/PassResourceUsageTracker.cpp
index 4944b67..15d2a7f 100644
--- a/src/dawn/native/PassResourceUsageTracker.cpp
+++ b/src/dawn/native/PassResourceUsageTracker.cpp
@@ -54,9 +54,12 @@
 }
 
 void SyncScopeUsageTracker::TextureViewUsedAs(TextureViewBase* view, wgpu::TextureUsage usage) {
-    TextureBase* texture = view->GetTexture();
-    const SubresourceRange& range = view->GetSubresourceRange();
+    TextureRangeUsedAs(view->GetTexture(), view->GetSubresourceRange(), usage);
+}
 
+void SyncScopeUsageTracker::TextureRangeUsedAs(TextureBase* texture,
+                                               const SubresourceRange& range,
+                                               wgpu::TextureUsage usage) {
     // Get or create a new TextureSubresourceUsage for that texture (initially filled with
     // wgpu::TextureUsage::None)
     auto it = mTextureUsages.emplace(
@@ -69,8 +72,10 @@
         // TODO(crbug.com/dawn/1001): Consider optimizing to have fewer branches.
 
         // Using the same subresource for two different attachments is a write-write or read-write
-        // hazard. Add the internal kAgainAsAttachment usage to fail the later check that a
+        // hazard. Add an internal kAgainAsAttachment usage to fail the later check that a
         // subresource with a writable usage has a single usage.
+        constexpr wgpu::TextureUsage kAgainAsAttachment =
+            kReservedTextureUsage | static_cast<wgpu::TextureUsage>(1);
         constexpr wgpu::TextureUsage kWritableAttachmentUsages =
             wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::StorageAttachment;
         if ((usage & kWritableAttachmentUsages) && (*storedUsage & kWritableAttachmentUsages)) {
diff --git a/src/dawn/native/PassResourceUsageTracker.h b/src/dawn/native/PassResourceUsageTracker.h
index 3487599..9dd9416 100644
--- a/src/dawn/native/PassResourceUsageTracker.h
+++ b/src/dawn/native/PassResourceUsageTracker.h
@@ -56,7 +56,10 @@
     SyncScopeUsageTracker& operator=(SyncScopeUsageTracker&&);
 
     void BufferUsedAs(BufferBase* buffer, wgpu::BufferUsage usage);
-    void TextureViewUsedAs(TextureViewBase* texture, wgpu::TextureUsage usage);
+    void TextureViewUsedAs(TextureViewBase* view, wgpu::TextureUsage usage);
+    void TextureRangeUsedAs(TextureBase* texture,
+                            const SubresourceRange& range,
+                            wgpu::TextureUsage usage);
     void AddRenderBundleTextureUsage(TextureBase* texture,
                                      const TextureSubresourceUsage& textureUsage);
 
diff --git a/src/dawn/native/d3d11/TextureD3D11.cpp b/src/dawn/native/d3d11/TextureD3D11.cpp
index 5cd65d7..2b114da 100644
--- a/src/dawn/native/d3d11/TextureD3D11.cpp
+++ b/src/dawn/native/d3d11/TextureD3D11.cpp
@@ -68,9 +68,10 @@
 Aspect D3D11Aspect(Aspect aspect) {
     // https://learn.microsoft.com/en-us/windows/win32/direct3d12/subresources
     // Planar formats existed in Direct3D 11, but individual planes could not be addressed
-    // individually.
-    if (IsSubset(aspect, Aspect::Depth | Aspect::Stencil)) {
-        return Aspect::CombinedDepthStencil;
+    // individually, so squash stencil into depth.
+    if (aspect & Aspect::Stencil) {
+        DAWN_ASSERT(IsSubset(aspect, Aspect::Depth | Aspect::Stencil));
+        return Aspect::Depth;
     }
 
     DAWN_ASSERT(HasOneBit(aspect));
@@ -653,7 +654,7 @@
                 // Skip lazy clears if already initialized.
                 continue;
             }
-            uint32_t dstSubresource = GetSubresourceIndex(level, layer, D3D11Aspect(range.aspects));
+            uint32_t dstSubresource = GetSubresourceIndex(level, layer, Aspect::Color);
             auto physicalSize = GetMipLevelSingleSubresourcePhysicalSize(level, Aspect::Color);
             // The documentation says D3D11_BOX's coordinates should be in texels for
             // textures. However the validation layer seemingly assumes them to be in
@@ -802,11 +803,11 @@
         copyCmd.source.texture = this;
         copyCmd.source.origin = origin;
         copyCmd.source.mipLevel = subresources.baseMipLevel;
-        copyCmd.source.aspect = Aspect::CombinedDepthStencil;
+        copyCmd.source.aspect = otherAspects;
         copyCmd.destination.texture = stagingTexture.Get();
         copyCmd.destination.origin = {0, 0, 0};
         copyCmd.destination.mipLevel = 0;
-        copyCmd.destination.aspect = Aspect::CombinedDepthStencil;
+        copyCmd.destination.aspect = otherAspects;
         copyCmd.copySize = size;
         DAWN_TRY(Texture::CopyInternal(commandContext, &copyCmd));
     }
@@ -846,11 +847,11 @@
     copyCmd.source.texture = stagingTexture.Get();
     copyCmd.source.origin = {0, 0, 0};
     copyCmd.source.mipLevel = 0;
-    copyCmd.source.aspect = Aspect::CombinedDepthStencil;
+    copyCmd.source.aspect = GetFormat().aspects;
     copyCmd.destination.texture = this;
     copyCmd.destination.origin = origin;
     copyCmd.destination.mipLevel = subresources.baseMipLevel;
-    copyCmd.destination.aspect = Aspect::CombinedDepthStencil;
+    copyCmd.destination.aspect = GetFormat().aspects;
     copyCmd.copySize = size;
     DAWN_TRY(Texture::CopyInternal(commandContext, &copyCmd));
 
diff --git a/src/dawn/native/dawn_platform.h b/src/dawn/native/dawn_platform.h
index 15f101f..5aff6f5 100644
--- a/src/dawn/native/dawn_platform.h
+++ b/src/dawn/native/dawn_platform.h
@@ -55,10 +55,11 @@
     kInternalStorageBuffer | kReadOnlyStorageBuffer;
 
 // Extra texture usages
-// Internal usage to help tracking when a subresource is used as render attachment usage
-// more than once in a render pass.
-static constexpr wgpu::TextureUsage kAgainAsAttachment =
-    static_cast<wgpu::TextureUsage>((1u << 31) + 1);
+// Usage to denote an extra tag value used in system specific ways.
+//  - Used to store that attachments are used more than once in PassResourceUsageTracker.
+//  - Used to store mixed read-only vs. not depth-stencil layouts in Vulkan.
+static constexpr wgpu::TextureUsage kReservedTextureUsage =
+    static_cast<wgpu::TextureUsage>(1u << 31);
 
 // Add an extra texture usage for textures that will be presented, for use in backends
 // that needs to transition to present usage.
diff --git a/src/dawn/native/vulkan/BindGroupVk.cpp b/src/dawn/native/vulkan/BindGroupVk.cpp
index 3d3e030..c619e88 100644
--- a/src/dawn/native/vulkan/BindGroupVk.cpp
+++ b/src/dawn/native/vulkan/BindGroupVk.cpp
@@ -115,7 +115,7 @@
                 }
                 writeImageInfo[numWrites].imageView = handle;
                 writeImageInfo[numWrites].imageLayout = VulkanImageLayout(
-                    ToBackend(view->GetTexture()), wgpu::TextureUsage::TextureBinding);
+                    view->GetTexture()->GetFormat(), wgpu::TextureUsage::TextureBinding);
 
                 write.pImageInfo = &writeImageInfo[numWrites];
                 break;
diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index e73eed3..8dc675e 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn/native/vulkan/CommandBufferVk.cpp
@@ -236,8 +236,8 @@
 
             query.SetDepthStencil(attachmentInfo.view->GetTexture()->GetFormat().format,
                                   attachmentInfo.depthLoadOp, attachmentInfo.depthStoreOp,
-                                  attachmentInfo.stencilLoadOp, attachmentInfo.stencilStoreOp,
-                                  attachmentInfo.depthReadOnly || attachmentInfo.stencilReadOnly);
+                                  attachmentInfo.depthReadOnly, attachmentInfo.stencilLoadOp,
+                                  attachmentInfo.stencilStoreOp, attachmentInfo.stencilReadOnly);
         }
 
         query.SetSampleCount(renderPass->attachmentState->GetSampleCount());
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index 0b97048..51dbb18 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -161,6 +161,11 @@
         return DAWN_INTERNAL_ERROR("Vulkan 1.1 or Vulkan 1.0 with KHR_Maintenance1 required.");
     }
 
+    // Needed for separate depth/stencilReadOnly
+    if (!mDeviceInfo.HasExt(DeviceExt::Maintenance2)) {
+        return DAWN_INTERNAL_ERROR("Vulkan 1.1 or Vulkan 1.0 with KHR_Maintenance2 required.");
+    }
+
     // Needed for security
     if (!mDeviceInfo.features.robustBufferAccess) {
         return DAWN_INTERNAL_ERROR("Vulkan robustBufferAccess feature required.");
diff --git a/src/dawn/native/vulkan/RenderPassCache.cpp b/src/dawn/native/vulkan/RenderPassCache.cpp
index 81dc5af..872b0a1 100644
--- a/src/dawn/native/vulkan/RenderPassCache.cpp
+++ b/src/dawn/native/vulkan/RenderPassCache.cpp
@@ -82,16 +82,18 @@
 void RenderPassCacheQuery::SetDepthStencil(wgpu::TextureFormat format,
                                            wgpu::LoadOp depthLoadOpIn,
                                            wgpu::StoreOp depthStoreOpIn,
+                                           bool depthReadOnlyIn,
                                            wgpu::LoadOp stencilLoadOpIn,
                                            wgpu::StoreOp stencilStoreOpIn,
-                                           bool readOnly) {
+                                           bool stencilReadOnlyIn) {
     hasDepthStencil = true;
     depthStencilFormat = format;
     depthLoadOp = depthLoadOpIn;
     depthStoreOp = depthStoreOpIn;
+    depthReadOnly = depthReadOnlyIn;
     stencilLoadOp = stencilLoadOpIn;
     stencilStoreOp = stencilStoreOpIn;
-    readOnlyDepthStencil = readOnly;
+    stencilReadOnly = stencilReadOnlyIn;
 }
 
 void RenderPassCacheQuery::SetSampleCount(uint32_t sampleCountIn) {
@@ -175,17 +177,17 @@
 
     VkAttachmentReference* depthStencilAttachment = nullptr;
     if (query.hasDepthStencil) {
-        auto& attachmentDesc = attachmentDescs[attachmentCount];
+        const Format& dsFormat = mDevice->GetValidInternalFormat(query.depthStencilFormat);
 
         depthStencilAttachment = &depthStencilAttachmentRef;
-
         depthStencilAttachmentRef.attachment = attachmentCount;
-        depthStencilAttachmentRef.layout = query.readOnlyDepthStencil
-                                               ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
-                                               : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+        depthStencilAttachmentRef.layout = VulkanImageLayoutForDepthStencilAttachment(
+            dsFormat, query.depthReadOnly, query.stencilReadOnly);
 
+        // Build the attachment descriptor.
+        auto& attachmentDesc = attachmentDescs[attachmentCount];
         attachmentDesc.flags = 0;
-        attachmentDesc.format = VulkanImageFormat(mDevice, query.depthStencilFormat);
+        attachmentDesc.format = VulkanImageFormat(mDevice, dsFormat.format);
         attachmentDesc.samples = vkSampleCount;
 
         attachmentDesc.loadOp = VulkanAttachmentLoadOp(query.depthLoadOp);
@@ -264,6 +266,8 @@
 
 // RenderPassCache
 
+// If you change these, remember to also update StreamImplVk.cpp
+
 size_t RenderPassCache::CacheFuncs::operator()(const RenderPassCacheQuery& query) const {
     size_t hash = Hash(query.colorMask);
 
@@ -276,7 +280,8 @@
     HashCombine(&hash, query.hasDepthStencil);
     if (query.hasDepthStencil) {
         HashCombine(&hash, query.depthStencilFormat, query.depthLoadOp, query.depthStoreOp,
-                    query.stencilLoadOp, query.stencilStoreOp, query.readOnlyDepthStencil);
+                    query.depthReadOnly, query.stencilLoadOp, query.stencilStoreOp,
+                    query.stencilReadOnly);
     }
 
     HashCombine(&hash, query.sampleCount);
@@ -312,8 +317,8 @@
     if (a.hasDepthStencil) {
         if ((a.depthStencilFormat != b.depthStencilFormat) || (a.depthLoadOp != b.depthLoadOp) ||
             (a.stencilLoadOp != b.stencilLoadOp) || (a.depthStoreOp != b.depthStoreOp) ||
-            (a.stencilStoreOp != b.stencilStoreOp) ||
-            (a.readOnlyDepthStencil != b.readOnlyDepthStencil)) {
+            (a.depthReadOnly != b.depthReadOnly) || (a.stencilStoreOp != b.stencilStoreOp) ||
+            (a.stencilReadOnly != b.stencilReadOnly)) {
             return false;
         }
     }
diff --git a/src/dawn/native/vulkan/RenderPassCache.h b/src/dawn/native/vulkan/RenderPassCache.h
index 3858199..22a2bc8 100644
--- a/src/dawn/native/vulkan/RenderPassCache.h
+++ b/src/dawn/native/vulkan/RenderPassCache.h
@@ -59,9 +59,10 @@
     void SetDepthStencil(wgpu::TextureFormat format,
                          wgpu::LoadOp depthLoadOp,
                          wgpu::StoreOp depthStoreOp,
+                         bool depthReadOnly,
                          wgpu::LoadOp stencilLoadOp,
                          wgpu::StoreOp stencilStoreOp,
-                         bool readOnly);
+                         bool stencilRendOnly);
     void SetSampleCount(uint32_t sampleCount);
 
     ityp::bitset<ColorAttachmentIndex, kMaxColorAttachments> colorMask;
@@ -74,9 +75,10 @@
     wgpu::TextureFormat depthStencilFormat;
     wgpu::LoadOp depthLoadOp;
     wgpu::StoreOp depthStoreOp;
+    bool depthReadOnly;
     wgpu::LoadOp stencilLoadOp;
     wgpu::StoreOp stencilStoreOp;
-    bool readOnlyDepthStencil;
+    bool stencilReadOnly;
 
     uint32_t sampleCount;
 };
diff --git a/src/dawn/native/vulkan/RenderPipelineVk.cpp b/src/dawn/native/vulkan/RenderPipelineVk.cpp
index 474b29c..d05cfb6 100644
--- a/src/dawn/native/vulkan/RenderPipelineVk.cpp
+++ b/src/dawn/native/vulkan/RenderPipelineVk.cpp
@@ -540,7 +540,7 @@
 
         if (HasDepthStencilAttachment()) {
             query.SetDepthStencil(GetDepthStencilFormat(), wgpu::LoadOp::Load, wgpu::StoreOp::Store,
-                                  wgpu::LoadOp::Load, wgpu::StoreOp::Store, false);
+                                  false, wgpu::LoadOp::Load, wgpu::StoreOp::Store, false);
         }
 
         query.SetSampleCount(GetSampleCount());
diff --git a/src/dawn/native/vulkan/StreamImplVk.cpp b/src/dawn/native/vulkan/StreamImplVk.cpp
index 03fd671..428e822 100644
--- a/src/dawn/native/vulkan/StreamImplVk.cpp
+++ b/src/dawn/native/vulkan/StreamImplVk.cpp
@@ -329,8 +329,8 @@
     // Serialize the depth-stencil toggle bit, and the parameters if applicable.
     StreamIn(sink, t.hasDepthStencil);
     if (t.hasDepthStencil) {
-        StreamIn(sink, t.depthStencilFormat, t.depthLoadOp, t.depthStoreOp, t.stencilLoadOp,
-                 t.stencilStoreOp, t.readOnlyDepthStencil);
+        StreamIn(sink, t.depthStencilFormat, t.depthLoadOp, t.depthStoreOp, t.depthReadOnly,
+                 t.stencilLoadOp, t.stencilStoreOp, t.stencilReadOnly);
     }
 }
 
diff --git a/src/dawn/native/vulkan/TextureVk.cpp b/src/dawn/native/vulkan/TextureVk.cpp
index f5ccaa9..be0ea9e 100644
--- a/src/dawn/native/vulkan/TextureVk.cpp
+++ b/src/dawn/native/vulkan/TextureVk.cpp
@@ -71,12 +71,59 @@
     DAWN_UNREACHABLE();
 }
 
+// Reserved texture usages to represent mixed read-only/writable depth-stencil texture usages
+// when combining the planes of depth-stencil textures. They can be combined with other in-pass
+// readonly usages like wgpu::TextureUsage::TextureBinding.
+// TODO(dawn:2172): Consider making a bespoke enum instead of hackily extending TextureUsage.
+constexpr wgpu::TextureUsage kDepthReadOnlyStencilWritableAttachment =
+    kReservedTextureUsage | static_cast<wgpu::TextureUsage>(1 << 30);
+constexpr wgpu::TextureUsage kDepthWritableStencilReadOnlyAttachment =
+    kReservedTextureUsage | static_cast<wgpu::TextureUsage>(1 << 29);
+
+// Merge two usages for depth and stencil into a single combined usage that uses the reserved
+// texture usages above. This is used to handle combining Aspect::Depth and Aspect::Stencil into a
+// single Aspect::CombinedDepthStencil.
+wgpu::TextureUsage MergeDepthStencilUsage(wgpu::TextureUsage depth, wgpu::TextureUsage stencil) {
+    // Aspects that are RenderAttachment cannot be anything else at the same time. This lets us
+    // check if we are in one of the RenderAttachment + (ReadOnlyAttachment|readonly usage) cases
+    // and know only the aspect with the readonly attachment might contain extra usages like
+    // TextureBinding.
+    DAWN_ASSERT(depth == wgpu::TextureUsage::RenderAttachment ||
+                IsSubset(depth, ~wgpu::TextureUsage::RenderAttachment));
+    DAWN_ASSERT(stencil == wgpu::TextureUsage::RenderAttachment ||
+                IsSubset(stencil, ~wgpu::TextureUsage::RenderAttachment));
+
+    if (depth == wgpu::TextureUsage::RenderAttachment && stencil & kReadOnlyRenderAttachment) {
+        return kDepthWritableStencilReadOnlyAttachment | (stencil & ~kReadOnlyRenderAttachment);
+    } else if (depth & kReadOnlyRenderAttachment &&
+               stencil == wgpu::TextureUsage::RenderAttachment) {
+        return kDepthReadOnlyStencilWritableAttachment | (depth & ~kReadOnlyRenderAttachment);
+    } else {
+        // Not one of the reserved usage special cases, we can just combine the aspect's usage the
+        // simple way!
+        return depth | stencil;
+    }
+}
+
 // Computes which vulkan access type could be required for the given Dawn usage.
 // TODO(crbug.com/dawn/269): We shouldn't need any access usages for srcAccessMask when
 // the previous usage is readonly because an execution dependency is sufficient.
 VkAccessFlags VulkanAccessFlags(wgpu::TextureUsage usage, const Format& format) {
-    VkAccessFlags flags = 0;
+    if (usage & kReservedTextureUsage) {
+        // Handle the special readonly usages for mixed depth-stencil.
+        DAWN_ASSERT(IsSubset(kDepthReadOnlyStencilWritableAttachment, usage) ||
+                    IsSubset(kDepthWritableStencilReadOnlyAttachment, usage));
 
+        // Add any additional access flags for the non-attachment part of the usage.
+        const wgpu::TextureUsage nonAttachmentUsages =
+            usage &
+            ~(kDepthReadOnlyStencilWritableAttachment | kDepthWritableStencilReadOnlyAttachment);
+        return VulkanAccessFlags(nonAttachmentUsages, format) |
+               VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
+               VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+    }
+
+    VkAccessFlags flags = 0;
     if (usage & wgpu::TextureUsage::CopySrc) {
         flags |= VK_ACCESS_TRANSFER_READ_BIT;
     }
@@ -127,6 +174,19 @@
 
 // Computes which Vulkan pipeline stage can access a texture in the given Dawn usage
 VkPipelineStageFlags VulkanPipelineStage(wgpu::TextureUsage usage, const Format& format) {
+    if (usage & kReservedTextureUsage) {
+        // Handle the special readonly usages for mixed depth-stencil.
+        DAWN_ASSERT(IsSubset(kDepthReadOnlyStencilWritableAttachment, usage) ||
+                    IsSubset(kDepthWritableStencilReadOnlyAttachment, usage));
+
+        // Convert all the reserved attachment usages into just RenderAttachment.
+        const wgpu::TextureUsage nonAttachmentUsages =
+            usage &
+            ~(kDepthReadOnlyStencilWritableAttachment | kDepthWritableStencilReadOnlyAttachment);
+        return VulkanPipelineStage(nonAttachmentUsages | wgpu::TextureUsage::RenderAttachment,
+                                   format);
+    }
+
     VkPipelineStageFlags flags = 0;
 
     if (usage == wgpu::TextureUsage::None) {
@@ -183,13 +243,15 @@
                                         wgpu::TextureUsage lastUsage,
                                         wgpu::TextureUsage usage,
                                         const SubresourceRange& range) {
+    const Format& format = texture->GetFormat();
+
     VkImageMemoryBarrier barrier;
     barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
     barrier.pNext = nullptr;
-    barrier.srcAccessMask = VulkanAccessFlags(lastUsage, texture->GetFormat());
-    barrier.dstAccessMask = VulkanAccessFlags(usage, texture->GetFormat());
-    barrier.oldLayout = VulkanImageLayout(texture, lastUsage);
-    barrier.newLayout = VulkanImageLayout(texture, usage);
+    barrier.srcAccessMask = VulkanAccessFlags(lastUsage, format);
+    barrier.dstAccessMask = VulkanAccessFlags(usage, format);
+    barrier.oldLayout = VulkanImageLayout(format, lastUsage);
+    barrier.newLayout = VulkanImageLayout(format, usage);
     barrier.image = texture->GetHandle();
     barrier.subresourceRange.aspectMask = VulkanAspectMask(range.aspects);
     barrier.subresourceRange.baseMipLevel = range.baseMipLevel;
@@ -538,23 +600,27 @@
 // Chooses which Vulkan image layout should be used for the given Dawn usage. Note that this
 // layout must match the layout given to various Vulkan operations as well as the layout given
 // to descriptor set writes.
-VkImageLayout VulkanImageLayout(const Texture* texture, wgpu::TextureUsage usage) {
+VkImageLayout VulkanImageLayout(const Format& format, wgpu::TextureUsage usage) {
     if (usage == wgpu::TextureUsage::None) {
         return VK_IMAGE_LAYOUT_UNDEFINED;
     }
 
     if (!wgpu::HasZeroOrOneBits(usage)) {
-        // Sampled | kReadOnlyRenderAttachment is the only possible multi-bit usage, if more
-        // appear we might need additional special-casing.
-        DAWN_ASSERT(usage == (wgpu::TextureUsage::TextureBinding | kReadOnlyRenderAttachment));
+        // sampled | (some sort of readonly depth-stencil aspect) is the only possible multi-bit
+        // usage, if more appear we will need additional special-casing.
+        DAWN_ASSERT(IsSubset(
+            usage, wgpu::TextureUsage::TextureBinding | kDepthReadOnlyStencilWritableAttachment |
+                       kDepthWritableStencilReadOnlyAttachment | kReadOnlyRenderAttachment));
 
-        // WebGPU requires both aspects to be readonly if the attachment's format does have
-        // both depth and stencil aspects. Vulkan 1.0 supports readonly for both aspects too
-        // via DEPTH_STENCIL_READ_ONLY image layout. Vulkan 1.1 and above can support separate
-        // readonly for a single aspect via DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL and
-        // DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL layouts. But Vulkan 1.0 cannot support
-        // it, and WebGPU doesn't need that currently.
-        return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
+        if (IsSubset(kDepthReadOnlyStencilWritableAttachment, usage)) {
+            return VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL;
+        } else if (IsSubset(kDepthWritableStencilReadOnlyAttachment, usage)) {
+            return VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL;
+        } else {
+            DAWN_ASSERT(
+                IsSubset(usage, kReadOnlyRenderAttachment | wgpu::TextureUsage::TextureBinding));
+            return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
+        }
     }
 
     // Usage has a single bit so we can switch on its value directly.
@@ -567,7 +633,7 @@
             // The sampled image can be used as a readonly depth/stencil attachment at the same
             // time if it is a depth/stencil renderable format, so the image layout need to be
             // VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL.
-            if (texture->GetFormat().HasDepthOrStencil() && texture->GetFormat().isRenderable) {
+            if (format.HasDepthOrStencil() && format.isRenderable) {
                 return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
             }
             return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
@@ -589,7 +655,7 @@
             return VK_IMAGE_LAYOUT_GENERAL;
 
         case wgpu::TextureUsage::RenderAttachment:
-            if (texture->GetFormat().HasDepthOrStencil()) {
+            if (format.HasDepthOrStencil()) {
                 return VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
             } else {
                 return VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
@@ -618,6 +684,23 @@
     DAWN_UNREACHABLE();
 }
 
+VkImageLayout VulkanImageLayoutForDepthStencilAttachment(const Format& format,
+                                                         bool depthReadOnly,
+                                                         bool stencilReadOnly) {
+    wgpu::TextureUsage depth = wgpu::TextureUsage::None;
+    if (format.HasDepth()) {
+        depth = depthReadOnly ? kReadOnlyRenderAttachment : wgpu::TextureUsage::RenderAttachment;
+    }
+
+    wgpu::TextureUsage stencil = wgpu::TextureUsage::None;
+    if (format.HasStencil()) {
+        stencil =
+            stencilReadOnly ? kReadOnlyRenderAttachment : wgpu::TextureUsage::RenderAttachment;
+    }
+
+    return VulkanImageLayout(format, MergeDepthStencilUsage(depth, stencil));
+}
+
 VkSampleCountFlagBits VulkanSampleCount(uint32_t sampleCount) {
     switch (sampleCount) {
         case 1:
@@ -705,6 +788,12 @@
 MaybeError Texture::InitializeAsInternalTexture(VkImageUsageFlags extraUsages) {
     Device* device = ToBackend(GetDevice());
 
+    // If this triggers, it means it's time to add tests and implement support for readonly
+    // depth-stencil attachments that are also used as readonly storage bindings in the pass.
+    // Have fun! :)
+    DAWN_ASSERT(
+        !(GetFormat().HasDepthOrStencil() && (GetUsage() & wgpu::TextureUsage::StorageBinding)));
+
     // Create the Vulkan image "container". We don't need to check that the format supports the
     // combination of sample, usage etc. because validation should have been done in the Dawn
     // frontend already based on the minimum supported formats in the Vulkan spec
@@ -981,7 +1070,7 @@
     // value used to export with whatever the current layout is. However queue transitioning to the
     // UNDEFINED layout is disallowed so we handle the case where currentLayout is UNDEFINED by
     // promoting to GENERAL.
-    VkImageLayout currentLayout = VulkanImageLayout(this, usage);
+    VkImageLayout currentLayout = VulkanImageLayout(GetFormat(), usage);
     VkImageLayout targetLayout;
     if (currentLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
         targetLayout = currentLayout;
@@ -1166,9 +1255,41 @@
                                      std::vector<VkImageMemoryBarrier>* imageBarriers,
                                      VkPipelineStageFlags* srcStages,
                                      VkPipelineStageFlags* dstStages) {
-    if (UseCombinedAspects()) {
-        SubresourceStorage<wgpu::TextureUsage> combinedUsages(mCombinedAspect, GetArrayLayers(),
-                                                              GetNumMipLevels());
+    if (!UseCombinedAspects()) {
+        TransitionUsageForPassImpl(recordingContext, textureUsages, imageBarriers, srcStages,
+                                   dstStages);
+        return;
+    }
+
+    // We need to combine aspects for the transition, use a new subresource storage that will
+    // contain the combined usages for the aspects.
+    SubresourceStorage<wgpu::TextureUsage> combinedUsages(mCombinedAspect, GetArrayLayers(),
+                                                          GetNumMipLevels());
+
+    if (mCombinedAspect == Aspect::CombinedDepthStencil) {
+        // For depth-stencil we can't just combine the aspect with an | operation because there
+        // needs to be special handling for readonly aspects. Instead figure out which aspect is
+        // currently being added (and which one is already present) and call the custom merging
+        // function for depth-stencil.
+        textureUsages.Iterate([&](const SubresourceRange& range, wgpu::TextureUsage usage) {
+            SubresourceRange updateRange = range;
+            updateRange.aspects = mCombinedAspect;
+            Aspect aspectsToMerge = range.aspects;
+
+            combinedUsages.Update(
+                updateRange, [&](const SubresourceRange&, wgpu::TextureUsage* combinedUsage) {
+                    if (aspectsToMerge == Aspect::Depth) {
+                        *combinedUsage = MergeDepthStencilUsage(usage, *combinedUsage);
+                    } else if (aspectsToMerge == Aspect::Stencil) {
+                        *combinedUsage = MergeDepthStencilUsage(*combinedUsage, usage);
+                    } else {
+                        DAWN_ASSERT(aspectsToMerge == (Aspect::Depth | Aspect::Stencil));
+                        *combinedUsage = usage;
+                    }
+                });
+        });
+    } else {
+        // Combine aspect's usages with the | operation.
         textureUsages.Iterate([&](const SubresourceRange& range, wgpu::TextureUsage usage) {
             SubresourceRange updateRange = range;
             updateRange.aspects = mCombinedAspect;
@@ -1178,13 +1299,10 @@
                                       *combinedUsage |= usage;
                                   });
         });
-
-        TransitionUsageForPassImpl(recordingContext, combinedUsages, imageBarriers, srcStages,
-                                   dstStages);
-    } else {
-        TransitionUsageForPassImpl(recordingContext, textureUsages, imageBarriers, srcStages,
-                                   dstStages);
     }
+
+    TransitionUsageForPassImpl(recordingContext, combinedUsages, imageBarriers, srcStages,
+                               dstStages);
 }
 
 void Texture::TransitionUsageForPassImpl(
@@ -1448,7 +1566,7 @@
 
 VkImageLayout Texture::GetCurrentLayoutForSwapChain() const {
     DAWN_ASSERT(GetFormat().aspects == Aspect::Color);
-    return VulkanImageLayout(this, mSubresourceLastUsages.Get(Aspect::Color, 0, 0));
+    return VulkanImageLayout(GetFormat(), mSubresourceLastUsages.Get(Aspect::Color, 0, 0));
 }
 
 bool Texture::UseCombinedAspects() const {
diff --git a/src/dawn/native/vulkan/TextureVk.h b/src/dawn/native/vulkan/TextureVk.h
index b19e228..4b83897 100644
--- a/src/dawn/native/vulkan/TextureVk.h
+++ b/src/dawn/native/vulkan/TextureVk.h
@@ -47,7 +47,10 @@
 
 VkFormat VulkanImageFormat(const Device* device, wgpu::TextureFormat format);
 VkImageUsageFlags VulkanImageUsage(wgpu::TextureUsage usage, const Format& format);
-VkImageLayout VulkanImageLayout(const Texture* texture, wgpu::TextureUsage usage);
+VkImageLayout VulkanImageLayout(const Format& format, wgpu::TextureUsage usage);
+VkImageLayout VulkanImageLayoutForDepthStencilAttachment(const Format& format,
+                                                         bool depthReadOnly,
+                                                         bool stencilReadOnly);
 VkSampleCountFlagBits VulkanSampleCount(uint32_t sampleCount);
 
 MaybeError ValidateVulkanImageCanBeWrapped(const DeviceBase* device,
diff --git a/src/dawn/tests/end2end/ReadOnlyDepthStencilAttachmentTests.cpp b/src/dawn/tests/end2end/ReadOnlyDepthStencilAttachmentTests.cpp
index f726934..3d9d21f 100644
--- a/src/dawn/tests/end2end/ReadOnlyDepthStencilAttachmentTests.cpp
+++ b/src/dawn/tests/end2end/ReadOnlyDepthStencilAttachmentTests.cpp
@@ -25,6 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+#include <optional>
 #include <vector>
 
 #include "dawn/tests/DawnTest.h"
@@ -43,11 +44,10 @@
 class ReadOnlyDepthStencilAttachmentTests
     : public DawnTestWithParams<ReadOnlyDepthStencilAttachmentTestsParams> {
   protected:
-    struct DepthStencilValues {
-        float depthInitValue;
-        uint32_t stencilInitValue;
-        uint32_t stencilRefValue;
-    };
+    void SetUp() override {
+        DawnTestWithParams<ReadOnlyDepthStencilAttachmentTestsParams>::SetUp();
+        DAWN_TEST_UNSUPPORTED_IF(!mIsFormatSupported);
+    }
 
     std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
         switch (GetParam().mTextureFormat) {
@@ -64,11 +64,24 @@
         }
     }
 
-    bool IsFormatSupported() const { return mIsFormatSupported; }
+    struct TestSpec {
+        wgpu::TextureAspect readonlyAspects;
+        std::optional<wgpu::TextureAspect> sampledAspect = std::nullopt;
 
-    wgpu::RenderPipeline CreateRenderPipeline(wgpu::TextureAspect aspect,
-                                              wgpu::TextureFormat format,
-                                              bool sampleFromAttachment) {
+        wgpu::CompareFunction depthCompare = wgpu::CompareFunction::Always;
+        wgpu::CompareFunction stencilCompare = wgpu::CompareFunction::Always;
+        bool depthWriteEnabled = false;
+        bool stencilWriteEnabled = false;
+
+        float depthClearValue = 0.0;
+        uint32_t stencilClearValue = 0;
+        uint32_t stencilRef = 0;
+
+        wgpu::Texture depthStencilTexture;
+    };
+
+    wgpu::RenderPipeline CreateRenderPipeline(wgpu::TextureFormat format,
+                                              const TestSpec& spec) const {
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
 
         // Draw a rectangle via two triangles. The depth value of the top of the rectangle is 0.4.
@@ -90,184 +103,184 @@
                 return vec4f(pos[VertexIndex], 1.0);
             })");
 
-        if (!sampleFromAttachment) {
+        if (!spec.sampledAspect.has_value()) {
             // Draw a solid blue into color buffer if not sample from depth/stencil attachment.
             pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
-            @fragment fn main() -> @location(0) vec4f {
-                return vec4f(0.0, 0.0, 1.0, 0.0);
-            })");
+                @fragment fn main() -> @location(0) vec4f {
+                    return vec4f(0.0, 0.0, 1.0, 0.0);
+                })");
+        } else if (spec.sampledAspect == wgpu::TextureAspect::DepthOnly) {
+            // Sample from depth attachment and draw that sampled texel into color buffer.
+            pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
+                    @group(0) @binding(0) var samp : sampler;
+                    @group(0) @binding(1) var tex : texture_depth_2d;
+
+                    @fragment
+                    fn main(@builtin(position) FragCoord : vec4f) -> @location(0) vec4f {
+                        return vec4f(textureSample(tex, samp, FragCoord.xy), 0.0, 0.0, 0.0);
+                    })");
+
         } else {
-            // Sample from depth/stencil attachment and draw that sampled texel into color buffer.
-            if (aspect == wgpu::TextureAspect::DepthOnly) {
-                pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
-                @group(0) @binding(0) var samp : sampler;
-                @group(0) @binding(1) var tex : texture_depth_2d;
+            DAWN_ASSERT(spec.sampledAspect == wgpu::TextureAspect::StencilOnly);
+            // Sample from stencil attachment and draw that sampled texel into color buffer.
+            pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
+                    @group(0) @binding(0) var samp : sampler;
+                    @group(0) @binding(1) var tex : texture_2d<u32>;
 
-                @fragment
-                fn main(@builtin(position) FragCoord : vec4f) -> @location(0) vec4f {
-                    return vec4f(textureSample(tex, samp, FragCoord.xy), 0.0, 0.0, 0.0);
-                })");
-            } else {
-                DAWN_ASSERT(aspect == wgpu::TextureAspect::StencilOnly);
-                pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
-                @group(0) @binding(0) var tex : texture_2d<u32>;
-
-                @fragment
-                fn main(@builtin(position) FragCoord : vec4f) -> @location(0) vec4f {
-                    var texel = textureLoad(tex, vec2i(FragCoord.xy), 0);
-                    return vec4f(f32(texel[0]) / 255.0, 0.0, 0.0, 0.0);
-                })");
-            }
+                    @fragment
+                    fn main(@builtin(position) FragCoord : vec4f) -> @location(0) vec4f {
+                        _ = samp;
+                        var texel = textureLoad(tex, vec2i(FragCoord.xy), 0);
+                        return vec4f(f32(texel[0]) / 255.0, 0.0, 0.0, 0.0);
+                    })");
         }
 
-        // Enable depth or stencil test. But depth/stencil write is not enabled.
         wgpu::DepthStencilState* depthStencil = pipelineDescriptor.EnableDepthStencil(format);
-        if (aspect == wgpu::TextureAspect::DepthOnly) {
-            depthStencil->depthCompare = wgpu::CompareFunction::LessEqual;
-        } else {
-            depthStencil->stencilFront.compare = wgpu::CompareFunction::LessEqual;
+        depthStencil->depthCompare = spec.depthCompare;
+        depthStencil->depthWriteEnabled = spec.depthWriteEnabled;
+        depthStencil->stencilFront.compare = spec.stencilCompare;
+        if (spec.stencilWriteEnabled) {
+            depthStencil->stencilFront.passOp = wgpu::StencilOperation::Replace;
         }
 
         return device.CreateRenderPipeline(&pipelineDescriptor);
     }
 
-    wgpu::Texture CreateTexture(wgpu::TextureFormat format, wgpu::TextureUsage usage) {
-        wgpu::TextureDescriptor descriptor = {};
-        descriptor.size = {kSize, kSize, 1};
-        descriptor.format = format;
-        descriptor.usage = usage;
-        return device.CreateTexture(&descriptor);
-    }
+    struct RenderResult {
+        wgpu::Texture color;
+        wgpu::Texture depthStencil;
+    };
+    RenderResult DoRender(const TestSpec& spec) const {
+        wgpu::TextureFormat testFormat = GetParam().mTextureFormat;
 
-    void DoTest(wgpu::TextureAspect aspect,
-                wgpu::TextureFormat format,
-                wgpu::Texture colorTexture,
-                DepthStencilValues* values,
-                bool sampleFromAttachment) {
-        wgpu::TextureUsage dsTextureUsage = wgpu::TextureUsage::RenderAttachment;
-        if (sampleFromAttachment) {
-            dsTextureUsage |= wgpu::TextureUsage::TextureBinding;
+        // Create or reuse the test textures.
+        wgpu::Texture depthStencilTexture = spec.depthStencilTexture;
+        if (!depthStencilTexture) {
+            wgpu::TextureDescriptor dsTextureDesc = {};
+            dsTextureDesc.size = {kSize, kSize, 1};
+            dsTextureDesc.format = testFormat;
+            dsTextureDesc.usage =
+                wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding;
+            depthStencilTexture = device.CreateTexture(&dsTextureDesc);
         }
-        wgpu::Texture depthStencilTexture = CreateTexture(format, dsTextureUsage);
+
+        wgpu::TextureDescriptor colorTextureDesc = {};
+        colorTextureDesc.size = {kSize, kSize, 1};
+        colorTextureDesc.format = wgpu::TextureFormat::RGBA8Unorm;
+        colorTextureDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+        wgpu::Texture colorTexture = device.CreateTexture(&colorTextureDesc);
 
         wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
 
-        // Note that we must encompass all aspects for texture view used in attachment.
-        wgpu::TextureView depthStencilViewInAttachment = depthStencilTexture.CreateView();
-        utils::ComboRenderPassDescriptor passDescriptorInit({}, depthStencilViewInAttachment);
-        passDescriptorInit.UnsetDepthStencilLoadStoreOpsForFormat(format);
-        if (aspect == wgpu::TextureAspect::DepthOnly) {
-            passDescriptorInit.cDepthStencilAttachmentInfo.depthClearValue = values->depthInitValue;
-        } else {
-            DAWN_ASSERT(aspect == wgpu::TextureAspect::StencilOnly);
+        // Do a first render pass that writes the initial values to the aspects that will be
+        // readonly.
+        if (!spec.depthStencilTexture) {
+            utils::ComboRenderPassDescriptor passDescriptorInit({},
+                                                                depthStencilTexture.CreateView());
+            passDescriptorInit.UnsetDepthStencilLoadStoreOpsForFormat(testFormat);
+            passDescriptorInit.cDepthStencilAttachmentInfo.depthClearValue = spec.depthClearValue;
             passDescriptorInit.cDepthStencilAttachmentInfo.stencilClearValue =
-                values->stencilInitValue;
+                spec.stencilClearValue;
+
+            wgpu::RenderPassEncoder passInit = commandEncoder.BeginRenderPass(&passDescriptorInit);
+            passInit.End();
         }
-        wgpu::RenderPassEncoder passInit = commandEncoder.BeginRenderPass(&passDescriptorInit);
-        passInit.End();
 
-        // Note that we can only select one single aspect for texture view used in bind group.
-        wgpu::TextureViewDescriptor viewDesc = {};
-        viewDesc.aspect = aspect;
-        wgpu::TextureView depthStencilViewInBindGroup = depthStencilTexture.CreateView(&viewDesc);
-
-        // Create a render pass to initialize the depth/stencil attachment.
+        // Do the render pass with the readonly attachment, that will potentially use the pipeline
+        // to read and/or write attachments.
         utils::ComboRenderPassDescriptor passDescriptor({colorTexture.CreateView()},
-                                                        depthStencilViewInAttachment);
-        // Set both aspects to readonly. We have to do this if the format has both aspects, or
-        // it doesn't impact anything if the format has only one aspect.
-        passDescriptor.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        passDescriptor.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        passDescriptor.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        passDescriptor.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        passDescriptor.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        passDescriptor.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
+                                                        depthStencilTexture.CreateView());
+        passDescriptor.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Load;
+        passDescriptor.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Load;
+        passDescriptor.UnsetDepthStencilLoadStoreOpsForFormat(testFormat);
+
+        if (spec.readonlyAspects != wgpu::TextureAspect::StencilOnly) {
+            passDescriptor.cDepthStencilAttachmentInfo.depthReadOnly = true;
+            passDescriptor.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
+            passDescriptor.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
+        }
+        if (spec.readonlyAspects != wgpu::TextureAspect::DepthOnly) {
+            passDescriptor.cDepthStencilAttachmentInfo.stencilReadOnly = true;
+            passDescriptor.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
+            passDescriptor.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
+        }
 
         // Create a render pass with readonly depth/stencil attachment. The attachment has already
-        // been initialized. The pipeline in this render pass will sample from the attachment.
+        // been initialized. The pipeline in this render pass will sample from the attachment. TODO
         // The pipeline will read from the attachment to do depth/stencil test too.
         wgpu::RenderPassEncoder pass = commandEncoder.BeginRenderPass(&passDescriptor);
-        wgpu::RenderPipeline pipeline = CreateRenderPipeline(aspect, format, sampleFromAttachment);
+        wgpu::RenderPipeline pipeline = CreateRenderPipeline(testFormat, spec);
         pass.SetPipeline(pipeline);
-        if (aspect == wgpu::TextureAspect::DepthOnly) {
-            if (sampleFromAttachment) {
-                wgpu::BindGroup bindGroup = utils::MakeBindGroup(
-                    device, pipeline.GetBindGroupLayout(0),
-                    {{0, device.CreateSampler()}, {1, depthStencilViewInBindGroup}});
-                pass.SetBindGroup(0, bindGroup);
-            }
-        } else {
-            DAWN_ASSERT(aspect == wgpu::TextureAspect::StencilOnly);
-            if (sampleFromAttachment) {
-                wgpu::BindGroup bindGroup = utils::MakeBindGroup(
-                    device, pipeline.GetBindGroupLayout(0), {{0, depthStencilViewInBindGroup}});
-                pass.SetBindGroup(0, bindGroup);
-            }
-            pass.SetStencilReference(values->stencilRefValue);
+        pass.SetStencilReference(spec.stencilRef);
+
+        // Bind the bindgroup is needed.
+        if (spec.sampledAspect.has_value()) {
+            wgpu::TextureViewDescriptor viewDesc = {};
+            viewDesc.aspect = spec.sampledAspect.value();
+            wgpu::TextureView view = depthStencilTexture.CreateView(&viewDesc);
+
+            wgpu::BindGroup bindGroup = utils::MakeBindGroup(
+                device, pipeline.GetBindGroupLayout(0), {{0, device.CreateSampler()}, {1, view}});
+            pass.SetBindGroup(0, bindGroup);
         }
+
         pass.Draw(6);
         pass.End();
 
         wgpu::CommandBuffer commands = commandEncoder.Finish();
         queue.Submit(1, &commands);
+
+        return {colorTexture, depthStencilTexture};
+    }
+
+    void CheckFullColor(wgpu::Texture color, utils::RGBA8 fullColor) {
+        std::vector<utils::RGBA8> expected(kSize * kSize, fullColor);
+        EXPECT_TEXTURE_EQ(expected.data(), color, {0, 0}, {kSize, kSize});
+    }
+
+    void CheckTopBottomColor(wgpu::Texture color, utils::RGBA8 topColor, utils::RGBA8 bottomColor) {
+        std::vector<utils::RGBA8> expectedTop(kSize * kSize / 2, topColor);
+        EXPECT_TEXTURE_EQ(expectedTop.data(), color, {0, 0}, {kSize, kSize / 2});
+        std::vector<utils::RGBA8> expectedBottom(kSize * kSize / 2, bottomColor);
+        EXPECT_TEXTURE_EQ(expectedBottom.data(), color, {0, kSize / 2}, {kSize, kSize / 2});
     }
 
   private:
     bool mIsFormatSupported = false;
 };
 
-class ReadOnlyDepthAttachmentTests : public ReadOnlyDepthStencilAttachmentTests {
-  protected:
-    void SetUp() override {
-        ReadOnlyDepthStencilAttachmentTests::SetUp();
-        DAWN_TEST_UNSUPPORTED_IF(!IsFormatSupported());
-    }
-};
+class ReadOnlyDepthAttachmentTests : public ReadOnlyDepthStencilAttachmentTests {};
 
 TEST_P(ReadOnlyDepthAttachmentTests, SampleFromAttachment) {
-    wgpu::Texture colorTexture =
-        CreateTexture(wgpu::TextureFormat::RGBA8Unorm,
-                      wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc);
+    // TODO(dawn:2163): The texture reads zeroes, maybe ANGLE's TextureStorageD3D11 is missing a
+    // copy between the storages?
+    DAWN_SUPPRESS_TEST_IF(IsANGLED3D11());
 
-    wgpu::TextureFormat depthFormat = GetParam().mTextureFormat;
-
-    DepthStencilValues values;
-    values.depthInitValue = 0.2;
-
-    DoTest(wgpu::TextureAspect::DepthOnly, depthFormat, colorTexture, &values, true);
+    TestSpec spec;
+    spec.readonlyAspects = wgpu::TextureAspect::DepthOnly;
+    spec.sampledAspect = wgpu::TextureAspect::DepthOnly;
+    spec.depthCompare = wgpu::CompareFunction::LessEqual;
+    spec.depthClearValue = 0.2;
+    auto render = DoRender(spec);
 
     // The top part is not rendered by the pipeline. Its color is the default clear color for
     // color attachment.
-    const std::vector<utils::RGBA8> kExpectedTopColors(kSize * kSize / 2, {0, 0, 0, 0});
     // The bottom part is rendered, whose red channel is sampled from depth attachment, which
     // is initialized into 0.2.
-    const std::vector<utils::RGBA8> kExpectedBottomColors(
-        kSize * kSize / 2, {static_cast<uint8_t>(0.2 * 255), 0, 0, 0});
-    EXPECT_TEXTURE_EQ(kExpectedTopColors.data(), colorTexture, {0, 0}, {kSize, kSize / 2});
-    EXPECT_TEXTURE_EQ(kExpectedBottomColors.data(), colorTexture, {0, kSize / 2},
-                      {kSize, kSize / 2});
+    CheckTopBottomColor(render.color, {0, 0, 0, 0}, {static_cast<uint8_t>(0.2 * 255), 0, 0, 0});
 }
 
 TEST_P(ReadOnlyDepthAttachmentTests, NotSampleFromAttachment) {
-    wgpu::Texture colorTexture =
-        CreateTexture(wgpu::TextureFormat::RGBA8Unorm,
-                      wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc);
-
-    wgpu::TextureFormat depthFormat = GetParam().mTextureFormat;
-
-    DepthStencilValues values;
-    values.depthInitValue = 0.2;
-
-    DoTest(wgpu::TextureAspect::DepthOnly, depthFormat, colorTexture, &values, false);
+    TestSpec spec;
+    spec.readonlyAspects = wgpu::TextureAspect::DepthOnly;
+    spec.depthCompare = wgpu::CompareFunction::LessEqual;
+    spec.depthClearValue = 0.2;
+    auto render = DoRender(spec);
 
     // The top part is not rendered by the pipeline. Its color is the default clear color for
     // color attachment.
-    const std::vector<utils::RGBA8> kExpectedTopColors(kSize * kSize / 2, {0, 0, 0, 0});
     // The bottom part is rendered. Its color is set to blue.
-    const std::vector<utils::RGBA8> kExpectedBottomColors(kSize * kSize / 2, {0, 0, 255, 0});
-    EXPECT_TEXTURE_EQ(kExpectedTopColors.data(), colorTexture, {0, 0}, {kSize, kSize / 2});
-    EXPECT_TEXTURE_EQ(kExpectedBottomColors.data(), colorTexture, {0, kSize / 2},
-                      {kSize, kSize / 2});
+    CheckTopBottomColor(render.color, {0, 0, 0, 0}, {0, 0, 255, 0});
 }
 
 // Regression test for crbug.com/dawn/1512 where having aspectReadOnly for an unused aspect of a
@@ -275,7 +288,11 @@
 // mismatch issues.
 TEST_P(ReadOnlyDepthAttachmentTests, UnusedAspectWithReadOnly) {
     wgpu::TextureFormat format = GetParam().mTextureFormat;
-    wgpu::Texture depthStencilTexture = CreateTexture(format, wgpu::TextureUsage::RenderAttachment);
+    wgpu::TextureDescriptor tDesc;
+    tDesc.size = {1, 1};
+    tDesc.format = format;
+    tDesc.usage = wgpu::TextureUsage::RenderAttachment;
+    wgpu::Texture depthStencilTexture = device.CreateTexture(&tDesc);
 
     utils::ComboRenderPassDescriptor passDescriptor({}, depthStencilTexture.CreateView());
     if (utils::IsStencilOnlyFormat(format)) {
@@ -301,74 +318,190 @@
     queue.Submit(1, &commands);
 }
 
-class ReadOnlyStencilAttachmentTests : public ReadOnlyDepthStencilAttachmentTests {
-  protected:
-    void SetUp() override {
-        ReadOnlyDepthStencilAttachmentTests::SetUp();
-        DAWN_TEST_UNSUPPORTED_IF(!IsFormatSupported());
-    }
-};
+class ReadOnlyStencilAttachmentTests : public ReadOnlyDepthStencilAttachmentTests {};
 
 TEST_P(ReadOnlyStencilAttachmentTests, SampleFromAttachment) {
-    wgpu::Texture colorTexture =
-        CreateTexture(wgpu::TextureFormat::RGBA8Unorm,
-                      wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc);
+    // TODO(angleproject:8384): ASSERT is triggered in the ANGLE D3D11 backend likely because of
+    // the usage of the GL_STENCIL_INDEX8 format.
+    DAWN_SUPPRESS_TEST_IF(IsANGLED3D11() &&
+                          GetParam().mTextureFormat == wgpu::TextureFormat::Stencil8);
 
-    wgpu::TextureFormat stencilFormat = GetParam().mTextureFormat;
-
-    DepthStencilValues values;
-    values.stencilInitValue = 3;
-    values.stencilRefValue = 2;
     // stencilRefValue < stencilValue (stencilInitValue), so stencil test passes. The pipeline
     // samples from stencil buffer and writes into color buffer.
-    DoTest(wgpu::TextureAspect::StencilOnly, stencilFormat, colorTexture, &values, true);
-    const std::vector<utils::RGBA8> kSampledColors(kSize * kSize, {3, 0, 0, 0});
-    EXPECT_TEXTURE_EQ(kSampledColors.data(), colorTexture, {0, 0}, {kSize, kSize});
+    {
+        TestSpec spec;
+        spec.readonlyAspects = wgpu::TextureAspect::StencilOnly;
+        spec.sampledAspect = wgpu::TextureAspect::StencilOnly;
+        spec.stencilCompare = wgpu::CompareFunction::LessEqual;
+        spec.stencilClearValue = 3;
+        spec.stencilRef = 2;
+        auto render = DoRender(spec);
+        CheckFullColor(render.color, {3, 0, 0, 0});
+    }
 
-    values.stencilInitValue = 1;
     // stencilRefValue > stencilValue (stencilInitValue), so stencil test fails. The pipeline
     // doesn't change color buffer. Sampled data from stencil buffer is discarded.
-    DoTest(wgpu::TextureAspect::StencilOnly, stencilFormat, colorTexture, &values, true);
-    const std::vector<utils::RGBA8> kInitColors(kSize * kSize, {0, 0, 0, 0});
-    EXPECT_TEXTURE_EQ(kInitColors.data(), colorTexture, {0, 0}, {kSize, kSize});
+    {
+        TestSpec spec;
+        spec.readonlyAspects = wgpu::TextureAspect::StencilOnly;
+        spec.sampledAspect = wgpu::TextureAspect::StencilOnly;
+        spec.stencilCompare = wgpu::CompareFunction::LessEqual;
+        spec.stencilClearValue = 1;
+        spec.stencilRef = 2;
+        auto render = DoRender(spec);
+        CheckFullColor(render.color, {0, 0, 0, 0});
+    }
 }
 
 TEST_P(ReadOnlyStencilAttachmentTests, NotSampleFromAttachment) {
-    wgpu::Texture colorTexture =
-        CreateTexture(wgpu::TextureFormat::RGBA8Unorm,
-                      wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc);
-
-    wgpu::TextureFormat stencilFormat = GetParam().mTextureFormat;
-
-    DepthStencilValues values;
-    values.stencilInitValue = 3;
-    values.stencilRefValue = 2;
     // stencilRefValue < stencilValue (stencilInitValue), so stencil test passes. The pipeline
     // draw solid blue into color buffer.
-    DoTest(wgpu::TextureAspect::StencilOnly, stencilFormat, colorTexture, &values, false);
-    const std::vector<utils::RGBA8> kSampledColors(kSize * kSize, {0, 0, 255, 0});
-    EXPECT_TEXTURE_EQ(kSampledColors.data(), colorTexture, {0, 0}, {kSize, kSize});
+    {
+        TestSpec spec;
+        spec.readonlyAspects = wgpu::TextureAspect::StencilOnly;
+        spec.stencilCompare = wgpu::CompareFunction::LessEqual;
+        spec.stencilClearValue = 3;
+        spec.stencilRef = 2;
+        auto render = DoRender(spec);
+        CheckFullColor(render.color, {0, 0, 255, 0});
+    }
 
-    values.stencilInitValue = 1;
     // stencilRefValue > stencilValue (stencilInitValue), so stencil test fails. The pipeline
     // doesn't change color buffer. drawing data is discarded.
-    DoTest(wgpu::TextureAspect::StencilOnly, stencilFormat, colorTexture, &values, false);
-    const std::vector<utils::RGBA8> kInitColors(kSize * kSize, {0, 0, 0, 0});
-    EXPECT_TEXTURE_EQ(kInitColors.data(), colorTexture, {0, 0}, {kSize, kSize});
+    {
+        TestSpec spec;
+        spec.readonlyAspects = wgpu::TextureAspect::StencilOnly;
+        spec.stencilCompare = wgpu::CompareFunction::LessEqual;
+        spec.stencilClearValue = 1;
+        spec.stencilRef = 2;
+        auto render = DoRender(spec);
+        CheckFullColor(render.color, {0, 0, 0, 0});
+    }
+}
+
+class ReadOnlyDepthAndStencilAttachmentTests : public ReadOnlyDepthStencilAttachmentTests {};
+
+// Test that using stencilReadOnly while modifying the depth aspect works.
+TEST_P(ReadOnlyDepthAndStencilAttachmentTests, ModifyDepthSampleStencil) {
+    // Stencil test is always true but the depth test passes only for the
+    TestSpec spec1;
+    spec1.readonlyAspects = wgpu::TextureAspect::StencilOnly;
+    spec1.sampledAspect = wgpu::TextureAspect::StencilOnly;
+    spec1.stencilClearValue = 42;
+    spec1.depthClearValue = 0.2;
+    spec1.depthCompare = wgpu::CompareFunction::LessEqual;
+    spec1.depthWriteEnabled = true;
+    auto render1 = DoRender(spec1);
+
+    // Stencil was read successfully, but only in the bottom part.
+    CheckTopBottomColor(render1.color, {0, 0, 0, 0}, {42, 0, 0, 0});
+
+    // Check that the depth was written by setting depthCompare equal, and rendering a solid
+    // blue color. The depth was only written on the bottom half due to the depth test in the
+    // first render.
+    TestSpec spec2;
+    spec2.readonlyAspects = wgpu::TextureAspect::StencilOnly;
+    spec2.depthStencilTexture = render1.depthStencil;
+    spec2.depthCompare = wgpu::CompareFunction::Equal;
+    auto render2 = DoRender(spec2);
+    CheckTopBottomColor(render2.color, {0, 0, 0, 0}, {0, 0, 255, 0});
+}
+
+// Test that using depthReadOnly while modifying the stencil aspect works.
+TEST_P(ReadOnlyDepthAndStencilAttachmentTests, SampleDepthModifyStencil) {
+    // TODO(dawn:2163): The texture reads zeroes, maybe ANGLE's TextureStorageD3D11 is missing a
+    // copy between the storages?
+    DAWN_SUPPRESS_TEST_IF(IsANGLED3D11());
+
+    // Depth/stencil tests are true, the depth is correctly sampled from the depthClearValue.
+    // The stencil is written to the value of the stencil ref.
+    TestSpec spec1;
+    spec1.readonlyAspects = wgpu::TextureAspect::DepthOnly;
+    spec1.sampledAspect = wgpu::TextureAspect::DepthOnly;
+    spec1.depthClearValue = 0.2;
+    spec1.stencilWriteEnabled = true;
+    spec1.stencilRef = 42;
+    spec1.stencilClearValue = 0;
+    auto render1 = DoRender(spec1);
+    CheckFullColor(render1.color, {static_cast<uint8_t>(0.2 * 255), 0, 0, 0});
+
+    // The stencil test checks that the stencil ref matches what's in the stencil buffer
+    // so that we know it was correctly written. The whole quad should be drawn.
+    TestSpec spec2;
+    spec2.readonlyAspects = wgpu::TextureAspect::DepthOnly;
+    spec2.depthStencilTexture = render1.depthStencil;
+    spec2.stencilCompare = wgpu::CompareFunction::Equal;
+    spec2.stencilRef = 42;
+    auto render2 = DoRender(spec2);
+    CheckFullColor(render2.color, {0, 0, 255, 0});
+}
+
+// Test sampling depth with both the depth and stencil readonly.
+TEST_P(ReadOnlyDepthAndStencilAttachmentTests, BothReadOnlySampleDepth) {
+    // TODO(dawn:2163): The texture reads zeroes, maybe ANGLE's TextureStorageD3D11 is missing a
+    // copy between the storages?
+    DAWN_SUPPRESS_TEST_IF(IsANGLED3D11());
+
+    // Sample the depth while using both depth an stencil testing.
+
+    // First render: depth test passes only for the bottom half, stencil passes.
+    TestSpec spec;
+    spec.readonlyAspects = wgpu::TextureAspect::All;
+    spec.sampledAspect = wgpu::TextureAspect::DepthOnly;
+    spec.depthCompare = wgpu::CompareFunction::LessEqual;
+    spec.depthClearValue = 0.2;
+    spec.stencilRef = 42;
+    spec.stencilClearValue = 43;
+    spec.stencilCompare = wgpu::CompareFunction::LessEqual;
+    auto render1 = DoRender(spec);
+    CheckTopBottomColor(render1.color, {0, 0, 0, 0}, {static_cast<uint8_t>(0.2 * 255), 0, 0, 0});
+
+    // Second render: stencil test fails.
+    spec.stencilClearValue = 41;
+    auto render2 = DoRender(spec);
+    CheckFullColor(render2.color, {0, 0, 0, 0});
+}
+
+// Test sampling stencil with both the depth and stencil readonly.
+TEST_P(ReadOnlyDepthAndStencilAttachmentTests, BothReadOnlySampleStencil) {
+    // Sample the stencil while using both depth an stencil testing.
+
+    // First render: depth test passes only for the bottom half, stencil passes.
+    TestSpec spec;
+    spec.readonlyAspects = wgpu::TextureAspect::All;
+    spec.sampledAspect = wgpu::TextureAspect::StencilOnly;
+    spec.depthCompare = wgpu::CompareFunction::LessEqual;
+    spec.depthClearValue = 0.2;
+    spec.stencilRef = 42;
+    spec.stencilClearValue = 43;
+    spec.stencilCompare = wgpu::CompareFunction::LessEqual;
+    auto render1 = DoRender(spec);
+    CheckTopBottomColor(render1.color, {0, 0, 0, 0}, {43, 0, 0, 0});
+
+    // Second render: stencil test fails.
+    spec.stencilClearValue = 41;
+    auto render2 = DoRender(spec);
+    CheckFullColor(render2.color, {0, 0, 0, 0});
 }
 
 DAWN_INSTANTIATE_TEST_P(ReadOnlyDepthAttachmentTests,
                         {D3D11Backend(), D3D12Backend(),
                          D3D12Backend({}, {"use_d3d12_render_pass"}), MetalBackend(),
-                         VulkanBackend()},
+                         OpenGLBackend(), OpenGLESBackend(), VulkanBackend()},
                         std::vector<wgpu::TextureFormat>(utils::kDepthFormats.begin(),
                                                          utils::kDepthFormats.end()));
 DAWN_INSTANTIATE_TEST_P(ReadOnlyStencilAttachmentTests,
                         {D3D11Backend(), D3D12Backend(),
                          D3D12Backend({}, {"use_d3d12_render_pass"}), MetalBackend(),
-                         VulkanBackend()},
+                         OpenGLBackend(), OpenGLESBackend(), VulkanBackend()},
                         std::vector<wgpu::TextureFormat>(utils::kStencilFormats.begin(),
                                                          utils::kStencilFormats.end()));
+DAWN_INSTANTIATE_TEST_P(ReadOnlyDepthAndStencilAttachmentTests,
+                        {D3D11Backend(), D3D12Backend(),
+                         D3D12Backend({}, {"use_d3d12_render_pass"}), MetalBackend(),
+                         OpenGLBackend(), OpenGLESBackend(), VulkanBackend()},
+                        std::vector<wgpu::TextureFormat>(utils::kDepthAndStencilFormats.begin(),
+                                                         utils::kDepthAndStencilFormats.end()));
 
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
index 18442ae..cf5812e 100644
--- a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
@@ -1280,188 +1280,104 @@
     AssertBeginRenderPassSuccess(&renderPassDescriptor);
 }
 
+// Check the validation rules around depth/stencilReadOnly
 TEST_F(RenderPassDescriptorValidationTest, ValidateDepthStencilReadOnly) {
     wgpu::TextureView colorView = Create2DAttachment(device, 1, 1, wgpu::TextureFormat::RGBA8Unorm);
     wgpu::TextureView depthStencilView =
         Create2DAttachment(device, 1, 1, wgpu::TextureFormat::Depth24PlusStencil8);
     wgpu::TextureView depthStencilViewNoStencil =
         Create2DAttachment(device, 1, 1, wgpu::TextureFormat::Depth24Plus);
+    wgpu::TextureView stencilView = Create2DAttachment(device, 1, 1, wgpu::TextureFormat::Stencil8);
 
-    // Tests that a read-only pass with depthReadOnly set to true succeeds.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
+    using Aspect = wgpu::TextureAspect;
+    struct TestSpec {
+        wgpu::TextureFormat format;
+        Aspect formatAspects;
+        Aspect testAspect;
+    };
+
+    TestSpec specs[] = {
+        {wgpu::TextureFormat::Depth24PlusStencil8, Aspect::All, Aspect::StencilOnly},
+        {wgpu::TextureFormat::Depth24PlusStencil8, Aspect::All, Aspect::DepthOnly},
+        {wgpu::TextureFormat::Depth24Plus, Aspect::DepthOnly, Aspect::DepthOnly},
+        {wgpu::TextureFormat::Stencil8, Aspect::All, Aspect::StencilOnly},
+    };
+    for (const auto& spec : specs) {
+        wgpu::TextureView depthStencil = Create2DAttachment(device, 1, 1, spec.format);
+        utils::ComboRenderPassDescriptor renderPass({}, depthStencilView);
+
+        Aspect testAspect = spec.testAspect;
+        Aspect otherAspect =
+            testAspect == Aspect::DepthOnly ? Aspect::StencilOnly : Aspect::DepthOnly;
+
+        auto Set = [&](Aspect aspect, wgpu::LoadOp loadOp, wgpu::StoreOp storeOp, bool readonly) {
+            if (aspect == Aspect::DepthOnly) {
+                renderPass.cDepthStencilAttachmentInfo.depthLoadOp = loadOp;
+                renderPass.cDepthStencilAttachmentInfo.depthStoreOp = storeOp;
+                renderPass.cDepthStencilAttachmentInfo.depthReadOnly = readonly;
+            } else {
+                DAWN_ASSERT(aspect == Aspect::StencilOnly);
+                renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = loadOp;
+                renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = storeOp;
+                renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = readonly;
+            }
+        };
+
+        // Tests that a read-only pass with depth/stencilReadOnly both set to true succeeds.
+        Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
+        Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
         AssertBeginRenderPassSuccess(&renderPass);
-    }
 
-    // Tests that a pass with mismatched depthReadOnly and stencilReadOnly values passes when
-    // there is no stencil component in the format (deprecated).
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilViewNoStencil);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = false;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with mismatched depthReadOnly and stencilReadOnly values fails when
-    // there there is no stencil component in the format and stencil loadOp/storeOp are passed.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilViewNoStencil);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Clear;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = false;
+        // Tests that readOnly with LoadOp not undefined is invalid.
+        Set(testAspect, wgpu::LoadOp::Clear, wgpu::StoreOp::Undefined, true);
+        Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
         AssertBeginRenderPassError(&renderPass);
 
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
+        Set(testAspect, wgpu::LoadOp::Load, wgpu::StoreOp::Undefined, true);
+        Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
         AssertBeginRenderPassError(&renderPass);
 
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Clear;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = false;
+        // Tests that readOnly with StoreOp not undefined is invalid.
+        Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Store, true);
+        Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
         AssertBeginRenderPassError(&renderPass);
-    }
 
-    // Tests that a pass with depthReadOnly=true and stencilReadOnly=true can pass
-    // when there is only depth component in the format. We actually enable readonly
-    // depth/stencil attachment in this case.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilViewNoStencil);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
+        Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Discard, true);
+        Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
+        AssertBeginRenderPassError(&renderPass);
+
+        // Test for the aspect's not present in the format, if applicable.
+        if (testAspect != spec.formatAspects) {
+            // Tests that readOnly with LoadOp not undefined is invalid even if the aspect is not in
+            // the format.
+            Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
+            Set(otherAspect, wgpu::LoadOp::Clear, wgpu::StoreOp::Undefined, true);
+            AssertBeginRenderPassError(&renderPass);
+
+            Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
+            Set(otherAspect, wgpu::LoadOp::Load, wgpu::StoreOp::Undefined, true);
+            AssertBeginRenderPassError(&renderPass);
+
+            // Tests that readOnly with StoreOp not undefined is invalid even if the aspect is not
+            // in the format.
+            Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
+            Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Store, true);
+            AssertBeginRenderPassError(&renderPass);
+
+            Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
+            Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Discard, true);
+            AssertBeginRenderPassError(&renderPass);
+        }
+
+        // Test that it is allowed to set only one of the aspects readonly.
+        Set(testAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
+        Set(otherAspect, wgpu::LoadOp::Load, wgpu::StoreOp::Store, false);
         AssertBeginRenderPassSuccess(&renderPass);
-    }
 
-    // Tests that a pass with depthReadOnly=false and stencilReadOnly=true can pass
-    // when there is only depth component in the format. We actually don't enable readonly
-    // depth/stencil attachment in this case.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilViewNoStencil);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = false;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
+        Set(testAspect, wgpu::LoadOp::Load, wgpu::StoreOp::Store, false);
+        Set(otherAspect, wgpu::LoadOp::Undefined, wgpu::StoreOp::Undefined, true);
         AssertBeginRenderPassSuccess(&renderPass);
     }
-
-    // TODO(https://crbug.com/dawn/666): Add a test case for stencil-only once stencil8 is
-    // supported (depthReadOnly and stencilReadOnly mismatch but no depth component).
-
-    // Tests that a pass with mismatched depthReadOnly and stencilReadOnly values fails when
-    // both depth and stencil components exist.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = false;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with loadOp set to clear and readOnly set to true fails.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Clear;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Clear;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with storeOp set to discard and readOnly set to true fails.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Discard;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Discard;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with loadOp set to load, storeOp set to store, and readOnly set to true
-    // fails.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with only depthLoadOp set to load and readOnly set to true fails.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with only depthStoreOp set to store and readOnly set to true fails.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with only stencilLoadOp set to load and readOnly set to true fails.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Load;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        AssertBeginRenderPassError(&renderPass);
-    }
-
-    // Tests that a pass with only stencilStoreOp set to store and readOnly set to true fails.
-    {
-        utils::ComboRenderPassDescriptor renderPass({colorView}, depthStencilView);
-        renderPass.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.depthReadOnly = true;
-        renderPass.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
-        renderPass.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
-        renderPass.cDepthStencilAttachmentInfo.stencilReadOnly = true;
-        AssertBeginRenderPassError(&renderPass);
-    }
 }
 
 // Check that the depth stencil attachment must use all aspects.
diff --git a/src/dawn/tests/unittests/validation/ResourceUsageTrackingTests.cpp b/src/dawn/tests/unittests/validation/ResourceUsageTrackingTests.cpp
index 70c6b06..915655c 100644
--- a/src/dawn/tests/unittests/validation/ResourceUsageTrackingTests.cpp
+++ b/src/dawn/tests/unittests/validation/ResourceUsageTrackingTests.cpp
@@ -936,6 +936,68 @@
     }
 }
 
+// Test that it is valid to use a depth-stencil texture in mixed readonly and writable attachment
+TEST_F(ResourceUsageTrackingTest, MixedReadOnlyAndNotAttachment) {
+    // Create the depth stencil texture and views.
+    wgpu::Texture texture =
+        CreateTexture(wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::RenderAttachment,
+                      wgpu::TextureFormat::Depth24PlusStencil8);
+
+    wgpu::TextureViewDescriptor viewDesc = {};
+
+    viewDesc.aspect = wgpu::TextureAspect::DepthOnly;
+    wgpu::TextureView depthView = texture.CreateView(&viewDesc);
+    viewDesc.aspect = wgpu::TextureAspect::StencilOnly;
+    wgpu::TextureView stencilView = texture.CreateView(&viewDesc);
+    viewDesc.aspect = wgpu::TextureAspect::All;
+    wgpu::TextureView depthStencilView = texture.CreateView(&viewDesc);
+
+    // Create a bind group.
+    wgpu::BindGroupLayout depthBgl = utils::MakeBindGroupLayout(
+        device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth}});
+    wgpu::BindGroup depthBg = utils::MakeBindGroup(device, depthBgl, {{0, depthView}});
+
+    wgpu::BindGroupLayout stencilBgl = utils::MakeBindGroupLayout(
+        device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Uint}});
+    wgpu::BindGroup stencilBg = utils::MakeBindGroup(device, stencilBgl, {{0, stencilView}});
+
+    // It is valid to use attachments with depth readonly+sampled and stencil written.
+    {
+        utils::ComboRenderPassDescriptor passDesc({}, depthStencilView);
+        passDesc.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Undefined;
+        passDesc.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Undefined;
+        passDesc.cDepthStencilAttachmentInfo.depthReadOnly = true;
+
+        passDesc.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Load;
+        passDesc.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Store;
+        passDesc.cDepthStencilAttachmentInfo.stencilReadOnly = false;
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
+        pass.SetBindGroup(0, depthBg);
+        pass.End();
+        encoder.Finish();
+    }
+
+    // It is valid to use attachments with depth written and stencil readonly+sampled.
+    {
+        utils::ComboRenderPassDescriptor passDesc({}, depthStencilView);
+        passDesc.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Load;
+        passDesc.cDepthStencilAttachmentInfo.depthStoreOp = wgpu::StoreOp::Store;
+        passDesc.cDepthStencilAttachmentInfo.depthReadOnly = false;
+
+        passDesc.cDepthStencilAttachmentInfo.stencilLoadOp = wgpu::LoadOp::Undefined;
+        passDesc.cDepthStencilAttachmentInfo.stencilStoreOp = wgpu::StoreOp::Undefined;
+        passDesc.cDepthStencilAttachmentInfo.stencilReadOnly = true;
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
+        pass.SetBindGroup(0, stencilBg);
+        pass.End();
+        encoder.Finish();
+    }
+}
+
 // Test using multiple writable usages on the same texture in a single pass/dispatch
 TEST_F(ResourceUsageTrackingTest, TextureWithMultipleWriteUsage) {
     // Test render pass
diff --git a/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp b/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp
index 88b6b79..bb0e742 100644
--- a/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp
@@ -70,5 +70,50 @@
     )"));
 }
 
+// Check that separate depth-stencil readonlyness is validated as unsafe.
+TEST_F(UnsafeAPIValidationTest, SeparateDepthStencilReadOnlyness) {
+    wgpu::TextureDescriptor tDesc;
+    tDesc.size = {1, 1};
+    tDesc.format = wgpu::TextureFormat::Depth24PlusStencil8;
+    tDesc.usage = wgpu::TextureUsage::RenderAttachment;
+    wgpu::Texture t = device.CreateTexture(&tDesc);
+
+    // Control case: both readonly is valid.
+    {
+        wgpu::RenderPassDepthStencilAttachment ds;
+        ds.view = t.CreateView();
+        ds.depthReadOnly = true;
+        ds.stencilReadOnly = true;
+
+        wgpu::RenderPassDescriptor rp;
+        rp.colorAttachmentCount = 0;
+        rp.depthStencilAttachment = &ds;
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp);
+        pass.End();
+        encoder.Finish();
+    }
+
+    // Error case: only one readonly is valid.
+    {
+        wgpu::RenderPassDepthStencilAttachment ds;
+        ds.view = t.CreateView();
+        ds.depthReadOnly = true;
+        ds.stencilReadOnly = false;
+        ds.stencilLoadOp = wgpu::LoadOp::Load;
+        ds.stencilStoreOp = wgpu::StoreOp::Store;
+
+        wgpu::RenderPassDescriptor rp;
+        rp.colorAttachmentCount = 0;
+        rp.depthStencilAttachment = &ds;
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp);
+        pass.End();
+        ASSERT_DEVICE_ERROR(encoder.Finish());
+    }
+}
+
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/webgpu-cts/expectations.txt b/webgpu-cts/expectations.txt
index c66c0fc..0f1e7f1 100644
--- a/webgpu-cts/expectations.txt
+++ b/webgpu-cts/expectations.txt
@@ -154,6 +154,12 @@
 crbug.com/1469851 [ win ] webgpu:web_platform,canvas,readbackFromWebGPUCanvas:drawTo2DCanvas:* [ Failure ]
 
 ################################################################################
+# Temporary failures while implementing separate depth-stencil readonlyness.
+# KEEP
+################################################################################
+crbug.com/dawn/2146 webgpu:api,validation,render_pass,render_pass_descriptor:depth_stencil_attachment,loadOp_storeOp_match_depthReadOnly_stencilReadOnly:* [ Failure ]
+
+################################################################################
 # Android failures
 # KEEP
 ################################################################################