D3D12: Add end2end tests for small shader-visible heaps.

Adds a toggle to force the use of small shader-visible heaps and
whitebox tests to verify bindgroup encoding correctness.

BUG=dawn:155

Change-Id: I4118b850d9f2cb445ae805aa68ebf4fab671261b
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/16960
Commit-Queue: Bryan Bernhart <bryan.bernhart@intel.com>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
diff --git a/src/dawn_native/Toggles.cpp b/src/dawn_native/Toggles.cpp
index 5984095..225918c 100644
--- a/src/dawn_native/Toggles.cpp
+++ b/src/dawn_native/Toggles.cpp
@@ -119,6 +119,10 @@
              {"disable_base_instance",
               "Disables the use of non-zero base instance which is unsupported on some "
               "platforms."}},
+            {Toggle::UseD3D12SmallShaderVisibleHeapForTesting,
+             {"use_d3d12_small_shader_visible_heap",
+              "Enable use of a small D3D12 shader visible heap, instead of using a large one by "
+              "default. This setting is used to test bindgroup encoding."}},
         }};
 
     }  // anonymous namespace
diff --git a/src/dawn_native/Toggles.h b/src/dawn_native/Toggles.h
index c05363b..3cc40ec 100644
--- a/src/dawn_native/Toggles.h
+++ b/src/dawn_native/Toggles.h
@@ -40,6 +40,7 @@
         MetalDisableSamplerCompare,
         DisableBaseVertex,
         DisableBaseInstance,
+        UseD3D12SmallShaderVisibleHeapForTesting,
 
         EnumCount,
         InvalidEnum = EnumCount,
diff --git a/src/dawn_native/d3d12/DeviceD3D12.cpp b/src/dawn_native/d3d12/DeviceD3D12.cpp
index 2558a60..e9fdf87 100644
--- a/src/dawn_native/d3d12/DeviceD3D12.cpp
+++ b/src/dawn_native/d3d12/DeviceD3D12.cpp
@@ -410,6 +410,9 @@
         SetToggle(Toggle::UseD3D12ResourceHeapTier2, useResourceHeapTier2);
         SetToggle(Toggle::UseD3D12RenderPass, GetDeviceInfo().supportsRenderPass);
         SetToggle(Toggle::UseD3D12ResidencyManagement, false);
+
+        // By default use the maximum shader-visible heap size allowed.
+        SetToggle(Toggle::UseD3D12SmallShaderVisibleHeapForTesting, false);
     }
 
     MaybeError Device::WaitForIdleForDestruction() {
diff --git a/src/dawn_native/d3d12/ShaderVisibleDescriptorAllocatorD3D12.cpp b/src/dawn_native/d3d12/ShaderVisibleDescriptorAllocatorD3D12.cpp
index 76e2db2..101ca4b 100644
--- a/src/dawn_native/d3d12/ShaderVisibleDescriptorAllocatorD3D12.cpp
+++ b/src/dawn_native/d3d12/ShaderVisibleDescriptorAllocatorD3D12.cpp
@@ -22,7 +22,14 @@
     static_assert(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV == 0, "");
     static_assert(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER == 1, "");
 
-    uint32_t GetD3D12ShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE heapType) {
+    // Thresholds should be adjusted (lower == faster) to avoid tests taking too long to complete.
+    static constexpr const uint32_t kShaderVisibleSmallHeapSizes[] = {1024, 512};
+
+    uint32_t GetD3D12ShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE heapType, bool useSmallSize) {
+        if (useSmallSize) {
+            return kShaderVisibleSmallHeapSizes[heapType];
+        }
+
         switch (heapType) {
             case D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV:
                 return D3D12_MAX_SHADER_VISIBLE_DESCRIPTOR_HEAP_SIZE_TIER_1;
@@ -144,7 +151,8 @@
         // TODO(bryan.bernhart@intel.com): Allocating to max heap size wastes memory
         // should the developer not allocate any bindings for the heap type.
         // Consider dynamically re-sizing GPU heaps.
-        const uint32_t descriptorCount = GetD3D12ShaderVisibleHeapSize(heapType);
+        const uint32_t descriptorCount = GetD3D12ShaderVisibleHeapSize(
+            heapType, mDevice->IsToggleEnabled(Toggle::UseD3D12SmallShaderVisibleHeapForTesting));
 
         if (heap == nullptr) {
             D3D12_DESCRIPTOR_HEAP_DESC heapDescriptor;
diff --git a/src/tests/white_box/D3D12DescriptorHeapTests.cpp b/src/tests/white_box/D3D12DescriptorHeapTests.cpp
index 7dc0a91..decec9c 100644
--- a/src/tests/white_box/D3D12DescriptorHeapTests.cpp
+++ b/src/tests/white_box/D3D12DescriptorHeapTests.cpp
@@ -14,6 +14,7 @@
 
 #include "tests/DawnTest.h"
 
+#include "dawn_native/Toggles.h"
 #include "dawn_native/d3d12/DeviceD3D12.h"
 #include "dawn_native/d3d12/ShaderVisibleDescriptorAllocatorD3D12.h"
 #include "utils/ComboRenderPipelineDescriptor.h"
@@ -33,9 +34,63 @@
     void TestSetUp() override {
         DAWN_SKIP_TEST_IF(UsesWire());
         mD3DDevice = reinterpret_cast<Device*>(device.Get());
+
+        mSimpleVSModule = utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, R"(
+        #version 450
+        void main() {
+            const vec2 pos[3] = vec2[3](vec2(-1.f, 1.f), vec2(1.f, 1.f), vec2(-1.f, -1.f));
+            gl_Position = vec4(pos[gl_VertexIndex], 0.f, 1.f);
+        })");
+
+        mSimpleFSModule = utils::CreateShaderModule(device, utils::SingleShaderStage::Fragment, R"(
+        #version 450
+        layout (location = 0) out vec4 fragColor;
+        layout (set = 0, binding = 0) uniform colorBuffer {
+            vec4 color;
+        };
+        void main() {
+            fragColor = color;
+        })");
+    }
+
+    utils::BasicRenderPass MakeRenderPass(const wgpu::Device& device,
+                                          uint32_t width,
+                                          uint32_t height,
+                                          wgpu::TextureFormat format) {
+        DAWN_ASSERT(width > 0 && height > 0);
+
+        wgpu::TextureDescriptor descriptor;
+        descriptor.dimension = wgpu::TextureDimension::e2D;
+        descriptor.size.width = width;
+        descriptor.size.height = height;
+        descriptor.size.depth = 1;
+        descriptor.arrayLayerCount = 1;
+        descriptor.sampleCount = 1;
+        descriptor.format = format;
+        descriptor.mipLevelCount = 1;
+        descriptor.usage = wgpu::TextureUsage::OutputAttachment | wgpu::TextureUsage::CopySrc;
+        wgpu::Texture color = device.CreateTexture(&descriptor);
+
+        return utils::BasicRenderPass(width, height, color);
+    }
+
+    uint32_t GetShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE heapType) const {
+        return mD3DDevice->GetShaderVisibleDescriptorAllocator()
+            ->GetShaderVisibleHeapSizeForTesting(heapType);
+    }
+
+    std::array<float, 4> GetSolidColor(uint32_t n) const {
+        ASSERT(n >> 24 == 0);
+        float b = (n & 0xFF) / 255.0f;
+        float g = ((n >> 8) & 0xFF) / 255.0f;
+        float r = ((n >> 16) & 0xFF) / 255.0f;
+        return {r, g, b, 1};
     }
 
     Device* mD3DDevice = nullptr;
+
+    wgpu::ShaderModule mSimpleVSModule;
+    wgpu::ShaderModule mSimpleFSModule;
 };
 
 // Verify the shader visible heaps switch over within a single submit.
@@ -198,4 +253,441 @@
     EXPECT_EQ(allocator->GetShaderVisiblePoolSizeForTesting(heapType), kNumOfSwitches);
 }
 
-DAWN_INSTANTIATE_TEST(D3D12DescriptorHeapTests, D3D12Backend());
+// Verify encoding multiple heaps worth of bindgroups.
+// Shader-visible heaps will switch out |kNumOfHeaps| times.
+TEST_P(D3D12DescriptorHeapTests, EncodeManyUBO) {
+    // This test draws a solid color triangle |heapSize| times. Each draw uses a new bindgroup that
+    // has its own UBO with a "color value" in the range [1... heapSize]. After |heapSize| draws,
+    // the result is the arithmetic sum of the sequence after the framebuffer is blended by
+    // accumulation. By checking for this sum, we ensure each bindgroup was encoded correctly.
+    DAWN_SKIP_TEST_IF(!mD3DDevice->IsToggleEnabled(
+        dawn_native::Toggle::UseD3D12SmallShaderVisibleHeapForTesting));
+
+    utils::BasicRenderPass renderPass =
+        MakeRenderPass(device, kRTSize, kRTSize, wgpu::TextureFormat::R32Float);
+
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor(device);
+    pipelineDescriptor.vertexStage.module = mSimpleVSModule;
+
+    pipelineDescriptor.cFragmentStage.module =
+        utils::CreateShaderModule(device, utils::SingleShaderStage::Fragment, R"(
+        #version 450
+        layout (location = 0) out float fragColor;
+        layout (set = 0, binding = 0) uniform buffer0 {
+            float heapSize;
+        };
+        void main() {
+            fragColor = heapSize;
+        })");
+
+    pipelineDescriptor.cColorStates[0].format = wgpu::TextureFormat::R32Float;
+    pipelineDescriptor.cColorStates[0].colorBlend.operation = wgpu::BlendOperation::Add;
+    pipelineDescriptor.cColorStates[0].colorBlend.srcFactor = wgpu::BlendFactor::One;
+    pipelineDescriptor.cColorStates[0].colorBlend.dstFactor = wgpu::BlendFactor::One;
+    pipelineDescriptor.cColorStates[0].alphaBlend.operation = wgpu::BlendOperation::Add;
+    pipelineDescriptor.cColorStates[0].alphaBlend.srcFactor = wgpu::BlendFactor::One;
+    pipelineDescriptor.cColorStates[0].alphaBlend.dstFactor = wgpu::BlendFactor::One;
+
+    wgpu::RenderPipeline renderPipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+    const uint32_t heapSize = GetShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
+
+    constexpr uint32_t kNumOfHeaps = 2;
+
+    const uint32_t numOfEncodedBindGroups = kNumOfHeaps * heapSize;
+
+    std::vector<wgpu::BindGroup> bindGroups;
+    for (uint32_t i = 0; i < numOfEncodedBindGroups; i++) {
+        const float color = i + 1;
+        wgpu::Buffer uniformBuffer =
+            utils::CreateBufferFromData(device, &color, sizeof(color), wgpu::BufferUsage::Uniform);
+        bindGroups.push_back(utils::MakeBindGroup(device, renderPipeline.GetBindGroupLayout(0),
+                                                  {{0, uniformBuffer}}));
+    }
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+
+        pass.SetPipeline(renderPipeline);
+
+        for (uint32_t i = 0; i < numOfEncodedBindGroups; ++i) {
+            pass.SetBindGroup(0, bindGroups[i]);
+            pass.Draw(3, 1, 0, 0);
+        }
+
+        pass.EndPass();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    float colorSum = numOfEncodedBindGroups * (numOfEncodedBindGroups + 1) / 2;
+    EXPECT_PIXEL_FLOAT_EQ(colorSum, renderPass.color, 0, 0);
+}
+
+// Verify encoding one bindgroup then a heaps worth in different submits.
+// Shader-visible heaps should switch out once upon encoding 1 + |heapSize| descriptors.
+// The first descriptor's memory will be reused when the second submit encodes |heapSize|
+// descriptors.
+TEST_P(D3D12DescriptorHeapTests, EncodeUBOOverflowMultipleSubmit) {
+    DAWN_SKIP_TEST_IF(!mD3DDevice->IsToggleEnabled(
+        dawn_native::Toggle::UseD3D12SmallShaderVisibleHeapForTesting));
+
+    utils::ComboRenderPipelineDescriptor renderPipelineDescriptor(device);
+
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor(device);
+    pipelineDescriptor.vertexStage.module = mSimpleVSModule;
+    pipelineDescriptor.cFragmentStage.module = mSimpleFSModule;
+    pipelineDescriptor.cColorStates[0].format = renderPass.colorFormat;
+
+    wgpu::RenderPipeline renderPipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+    // Encode the first descriptor and submit.
+    {
+        std::array<float, 4> greenColor = {0, 1, 0, 1};
+        wgpu::Buffer uniformBuffer = utils::CreateBufferFromData(
+            device, &greenColor, sizeof(greenColor), wgpu::BufferUsage::Uniform);
+
+        wgpu::BindGroup bindGroup = utils::MakeBindGroup(
+            device, renderPipeline.GetBindGroupLayout(0), {{0, uniformBuffer}});
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        {
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+
+            pass.SetPipeline(renderPipeline);
+            pass.SetBindGroup(0, bindGroup);
+            pass.Draw(3, 1, 0, 0);
+            pass.EndPass();
+        }
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kGreen, renderPass.color, 0, 0);
+
+    // Encode a heap worth of descriptors.
+    {
+        const uint32_t heapSize = GetShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER);
+
+        std::vector<wgpu::BindGroup> bindGroups;
+        for (uint32_t i = 0; i < heapSize - 1; i++) {
+            std::array<float, 4> fillColor = GetSolidColor(i + 1);  // Avoid black
+            wgpu::Buffer uniformBuffer = utils::CreateBufferFromData(
+                device, &fillColor, sizeof(fillColor), wgpu::BufferUsage::Uniform);
+
+            bindGroups.push_back(utils::MakeBindGroup(device, renderPipeline.GetBindGroupLayout(0),
+                                                      {{0, uniformBuffer}}));
+        }
+
+        std::array<float, 4> redColor = {1, 0, 0, 1};
+        wgpu::Buffer lastUniformBuffer = utils::CreateBufferFromData(
+            device, &redColor, sizeof(redColor), wgpu::BufferUsage::Uniform);
+
+        bindGroups.push_back(utils::MakeBindGroup(device, renderPipeline.GetBindGroupLayout(0),
+                                                  {{0, lastUniformBuffer, 0, sizeof(redColor)}}));
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        {
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+
+            pass.SetPipeline(renderPipeline);
+
+            for (uint32_t i = 0; i < heapSize; ++i) {
+                pass.SetBindGroup(0, bindGroups[i]);
+                pass.Draw(3, 1, 0, 0);
+            }
+
+            pass.EndPass();
+        }
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kRed, renderPass.color, 0, 0);
+}
+
+// Verify encoding a heaps worth of bindgroups plus one more then reuse the first
+// bindgroup in the same submit.
+// Shader-visible heaps should switch out once then re-encode the first descriptor at a new offset
+// in the heap.
+TEST_P(D3D12DescriptorHeapTests, EncodeReuseUBOOverflow) {
+    DAWN_SKIP_TEST_IF(!mD3DDevice->IsToggleEnabled(
+        dawn_native::Toggle::UseD3D12SmallShaderVisibleHeapForTesting));
+
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor(device);
+    pipelineDescriptor.vertexStage.module = mSimpleVSModule;
+    pipelineDescriptor.cFragmentStage.module = mSimpleFSModule;
+    pipelineDescriptor.cColorStates[0].format = renderPass.colorFormat;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+    std::array<float, 4> redColor = {1, 0, 0, 1};
+    wgpu::Buffer firstUniformBuffer = utils::CreateBufferFromData(
+        device, &redColor, sizeof(redColor), wgpu::BufferUsage::Uniform);
+
+    std::vector<wgpu::BindGroup> bindGroups = {utils::MakeBindGroup(
+        device, pipeline.GetBindGroupLayout(0), {{0, firstUniformBuffer, 0, sizeof(redColor)}})};
+
+    const uint32_t heapSize = GetShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
+
+    for (uint32_t i = 0; i < heapSize; i++) {
+        const std::array<float, 4>& fillColor = GetSolidColor(i + 1);  // Avoid black
+        wgpu::Buffer uniformBuffer = utils::CreateBufferFromData(
+            device, &fillColor, sizeof(fillColor), wgpu::BufferUsage::Uniform);
+        bindGroups.push_back(utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                  {{0, uniformBuffer, 0, sizeof(fillColor)}}));
+    }
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    {
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+
+        pass.SetPipeline(pipeline);
+
+        // Encode a heap worth of descriptors plus one more.
+        for (uint32_t i = 0; i < heapSize + 1; ++i) {
+            pass.SetBindGroup(0, bindGroups[i]);
+            pass.Draw(3, 1, 0, 0);
+        }
+
+        // Re-encode the first bindgroup again.
+        pass.SetBindGroup(0, bindGroups[0]);
+        pass.Draw(3, 1, 0, 0);
+
+        pass.EndPass();
+    }
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    // Make sure the first bindgroup was encoded correctly.
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kRed, renderPass.color, 0, 0);
+}
+
+// Verify encoding a heaps worth of bindgroups plus one more in the first submit then reuse the
+// first bindgroup again in the second submit.
+// Shader-visible heaps should switch out once then re-encode the
+// first descriptor at the same offset in the heap.
+TEST_P(D3D12DescriptorHeapTests, EncodeReuseUBOMultipleSubmits) {
+    DAWN_SKIP_TEST_IF(!mD3DDevice->IsToggleEnabled(
+        dawn_native::Toggle::UseD3D12SmallShaderVisibleHeapForTesting));
+
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor(device);
+    pipelineDescriptor.vertexStage.module = mSimpleVSModule;
+    pipelineDescriptor.cFragmentStage.module = mSimpleFSModule;
+    pipelineDescriptor.cColorStates[0].format = renderPass.colorFormat;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+    // Encode heap worth of descriptors plus one more.
+    std::array<float, 4> redColor = {1, 0, 0, 1};
+
+    wgpu::Buffer firstUniformBuffer = utils::CreateBufferFromData(
+        device, &redColor, sizeof(redColor), wgpu::BufferUsage::Uniform);
+
+    std::vector<wgpu::BindGroup> bindGroups = {utils::MakeBindGroup(
+        device, pipeline.GetBindGroupLayout(0), {{0, firstUniformBuffer, 0, sizeof(redColor)}})};
+
+    const uint32_t heapSize = GetShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
+
+    for (uint32_t i = 0; i < heapSize; i++) {
+        std::array<float, 4> fillColor = GetSolidColor(i + 1);  // Avoid black
+        wgpu::Buffer uniformBuffer = utils::CreateBufferFromData(
+            device, &fillColor, sizeof(fillColor), wgpu::BufferUsage::Uniform);
+
+        bindGroups.push_back(utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                  {{0, uniformBuffer, 0, sizeof(fillColor)}}));
+    }
+
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        {
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+
+            pass.SetPipeline(pipeline);
+
+            for (uint32_t i = 0; i < heapSize + 1; ++i) {
+                pass.SetBindGroup(0, bindGroups[i]);
+                pass.Draw(3, 1, 0, 0);
+            }
+
+            pass.EndPass();
+        }
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    // Re-encode the first bindgroup again.
+    {
+        std::array<float, 4> greenColor = {0, 1, 0, 1};
+        firstUniformBuffer.SetSubData(0, sizeof(greenColor), &greenColor);
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        {
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+
+            pass.SetPipeline(pipeline);
+
+            pass.SetBindGroup(0, bindGroups[0]);
+            pass.Draw(3, 1, 0, 0);
+
+            pass.EndPass();
+        }
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    // Make sure the first bindgroup was re-encoded correctly.
+    EXPECT_PIXEL_RGBA8_EQ(RGBA8::kGreen, renderPass.color, 0, 0);
+}
+
+// Verify encoding many sampler and ubo worth of bindgroups.
+// Shader-visible heaps should switch out |kNumOfHeaps| times.
+TEST_P(D3D12DescriptorHeapTests, EncodeManyUBOAndSamplers) {
+    // Create a solid filled texture.
+    wgpu::TextureDescriptor descriptor;
+    descriptor.dimension = wgpu::TextureDimension::e2D;
+    descriptor.size.width = kRTSize;
+    descriptor.size.height = kRTSize;
+    descriptor.size.depth = 1;
+    descriptor.arrayLayerCount = 1;
+    descriptor.sampleCount = 1;
+    descriptor.format = wgpu::TextureFormat::RGBA8Unorm;
+    descriptor.mipLevelCount = 1;
+    descriptor.usage = wgpu::TextureUsage::Sampled | wgpu::TextureUsage::OutputAttachment |
+                       wgpu::TextureUsage::CopySrc;
+    wgpu::Texture texture = device.CreateTexture(&descriptor);
+    wgpu::TextureView textureView = texture.CreateView();
+
+    {
+        utils::BasicRenderPass renderPass = utils::BasicRenderPass(kRTSize, kRTSize, texture);
+
+        utils::ComboRenderPassDescriptor renderPassDesc({textureView});
+        renderPassDesc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+        renderPassDesc.cColorAttachments[0].clearColor = {0.0f, 1.0f, 0.0f, 1.0f};
+        renderPass.renderPassInfo.cColorAttachments[0].attachment = textureView;
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        auto pass = encoder.BeginRenderPass(&renderPassDesc);
+        pass.EndPass();
+
+        wgpu::CommandBuffer commandBuffer = encoder.Finish();
+        queue.Submit(1, &commandBuffer);
+
+        RGBA8 filled(0, 255, 0, 255);
+        EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 0, 0);
+    }
+
+    {
+        utils::ComboRenderPipelineDescriptor pipelineDescriptor(device);
+
+        pipelineDescriptor.vertexStage.module =
+            utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, R"(
+        #version 450
+        layout (set = 0, binding = 0) uniform vertexUniformBuffer {
+            mat2 transform;
+        };
+        void main() {
+            const vec2 pos[3] = vec2[3](vec2(-1.f, 1.f), vec2(1.f, 1.f), vec2(-1.f, -1.f));
+            gl_Position = vec4(transform * pos[gl_VertexIndex], 0.f, 1.f);
+        })");
+
+        pipelineDescriptor.cFragmentStage.module =
+            utils::CreateShaderModule(device, utils::SingleShaderStage::Fragment, R"(
+        #version 450
+        layout (set = 0, binding = 1) uniform sampler sampler0;
+        layout (set = 0, binding = 2) uniform texture2D texture0;
+        layout (set = 0, binding = 3) uniform buffer0 {
+            vec4 color;
+        };
+        layout (location = 0) out vec4 fragColor;
+        void main() {
+            fragColor = texture(sampler2D(texture0, sampler0), gl_FragCoord.xy);
+            fragColor += color;
+        })");
+
+        utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+        pipelineDescriptor.cColorStates[0].format = renderPass.colorFormat;
+
+        wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+        // Encode a heap worth of descriptors |kNumOfHeaps| times.
+        constexpr float dummy = 0.0f;
+        constexpr float transform[] = {1.f, 0.f, dummy, dummy, 0.f, 1.f, dummy, dummy};
+        wgpu::Buffer transformBuffer = utils::CreateBufferFromData(
+            device, &transform, sizeof(transform), wgpu::BufferUsage::Uniform);
+
+        wgpu::SamplerDescriptor samplerDescriptor;
+        wgpu::Sampler sampler = device.CreateSampler(&samplerDescriptor);
+
+        constexpr uint32_t kNumOfBindGroups = 4;
+        std::vector<wgpu::BindGroup> bindGroups;
+        for (uint32_t i = 0; i < kNumOfBindGroups - 1; i++) {
+            std::array<float, 4> fillColor = GetSolidColor(i + 1);  // Avoid black
+            wgpu::Buffer uniformBuffer = utils::CreateBufferFromData(
+                device, &fillColor, sizeof(fillColor), wgpu::BufferUsage::Uniform);
+
+            bindGroups.push_back(
+                utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                     {{0, transformBuffer, 0, sizeof(transformBuffer)},
+                                      {1, sampler},
+                                      {2, textureView},
+                                      {3, uniformBuffer, 0, sizeof(fillColor)}}));
+        }
+
+        std::array<float, 4> redColor = {1, 0, 0, 1};
+        wgpu::Buffer lastUniformBuffer = utils::CreateBufferFromData(
+            device, &redColor, sizeof(redColor), wgpu::BufferUsage::Uniform);
+
+        bindGroups.push_back(utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                  {{0, transformBuffer, 0, sizeof(transform)},
+                                                   {1, sampler},
+                                                   {2, textureView},
+                                                   {3, lastUniformBuffer, 0, sizeof(redColor)}}));
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+
+        pass.SetPipeline(pipeline);
+
+        constexpr uint32_t kBindingsPerGroup = 4;
+        constexpr uint32_t kNumOfHeaps = 5;
+
+        const uint32_t heapSize = GetShaderVisibleHeapSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER);
+        const uint32_t bindGroupsPerHeap = heapSize / kBindingsPerGroup;
+
+        ASSERT_TRUE(heapSize % kBindingsPerGroup == 0);
+
+        for (uint32_t i = 0; i < kNumOfHeaps * bindGroupsPerHeap; ++i) {
+            pass.SetBindGroup(0, bindGroups[i % kNumOfBindGroups]);
+            pass.Draw(3, 1, 0, 0);
+        }
+
+        pass.EndPass();
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+
+        // Final accumulated color is result of sampled + UBO color.
+        RGBA8 filled(255, 255, 0, 255);
+        RGBA8 notFilled(0, 0, 0, 0);
+        EXPECT_PIXEL_RGBA8_EQ(filled, renderPass.color, 0, 0);
+        EXPECT_PIXEL_RGBA8_EQ(notFilled, renderPass.color, kRTSize - 1, 0);
+    }
+}
+
+DAWN_INSTANTIATE_TEST(D3D12DescriptorHeapTests,
+                      D3D12Backend(),
+                      D3D12Backend({"use_d3d12_small_shader_visible_heap"}));