blob: ea3c2500d515f4382f7e99a12667c8f873d3e1b6 [file]
// Copyright 2025 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// 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 <string>
#include <unordered_set>
#include <vector>
#include "dawn/common/Enumerator.h"
#include "dawn/tests/DawnTest.h"
#include "dawn/utils/ComboRenderPipelineDescriptor.h"
#include "dawn/utils/WGPUHelpers.h"
namespace dawn {
namespace {
class SizedBindingArrayTests : public DawnTest {
public:
void SetUp() override {
DawnTest::SetUp();
// TODO(https://issues.chromium.org/411573959) Fails using WARP but not on real hardware.
// WARP 10.0.19031.4355 samples the wrong textures when indexing while WARP 1.0.12.0 fails
// pipeline creation.
DAWN_SUPPRESS_TEST_IF(IsD3D12() && IsWARP());
}
// A 1x1 texture with a single value to check the correct binding is used.
wgpu::Texture MakeTestR8Texture(uint8_t value) {
wgpu::TextureDescriptor desc;
desc.size = {1, 1};
desc.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding;
desc.format = wgpu::TextureFormat::R8Unorm;
wgpu::Texture texture = device.CreateTexture(&desc);
wgpu::TexelCopyTextureInfo srcInfo = utils::CreateTexelCopyTextureInfo(texture);
wgpu::TexelCopyBufferLayout dstInfo = {};
wgpu::Extent3D copySize = {1, 1, 1};
queue.WriteTexture(&srcInfo, &value, 1, &dstInfo, &copySize);
return texture;
}
// Test textures that can be used that samplers correctly clamp or wrap.
wgpu::Texture MakeTest2x1R8Texture(uint8_t left, uint8_t right) {
wgpu::TextureDescriptor desc;
desc.size = {2, 1};
desc.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding;
desc.format = wgpu::TextureFormat::R8Unorm;
wgpu::Texture texture = device.CreateTexture(&desc);
wgpu::TexelCopyTextureInfo srcInfo = utils::CreateTexelCopyTextureInfo(texture);
wgpu::TexelCopyBufferLayout dstInfo = {};
wgpu::Extent3D copySize = {2, 1, 1};
std::array<uint8_t, 2> data = {left, right};
queue.WriteTexture(&srcInfo, &data, sizeof(data), &dstInfo, &copySize);
return texture;
}
};
// Test accessing a binding_array with constant values.
TEST_P(SizedBindingArrayTests, IndexingWithConstants) {
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var ts : binding_array<texture_2d<f32>, 4>;
@fragment fn fs() -> @location(0) vec4f {
let r = textureLoad(ts[0], vec2(0, 0), 0)[0];
let g = textureLoad(ts[1], vec2(0, 0), 0)[0];
let b = textureLoad(ts[2], vec2(0, 0), 0)[0];
let a = textureLoad(ts[3], vec2(0, 0), 0)[0];
return vec4(r, g, b, a);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Create a bind group with textures of decreasing values
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, testPipeline.GetBindGroupLayout(0),
{
{3, MakeTestR8Texture(0).CreateView()},
{2, MakeTestR8Texture(1).CreateView()},
{1, MakeTestR8Texture(2).CreateView()},
{0, MakeTestR8Texture(3).CreateView()},
});
// Run the test
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(3, 2, 1, 0), rp.color, 0, 0);
}
// Test accessing a binding_array with dynamically uniform values.
TEST_P(SizedBindingArrayTests, IndexingWithDynamicallyUniformValues) {
// Compat should disallow dynamically uniform indexing of binding_array of textures.
DAWN_TEST_UNSUPPORTED_IF(IsCompatibilityMode());
// TODO(https://issues.chromium.org/425328998): D3D11 exposes core when FXC cannot support
// core's uniform indexing of binding_array<texture*>. We should make D3D11 expose only compat
// by default.
DAWN_SUPPRESS_TEST_IF(IsD3D11());
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var<storage> indices : array<u32, 4>;
@group(0) @binding(1) var ts : binding_array<texture_2d<f32>, 4>;
@fragment fn fs() -> @location(0) vec4f {
let r = textureLoad(ts[indices[0]], vec2(0, 0), 0)[0];
let g = textureLoad(ts[indices[1]], vec2(0, 0), 0)[0];
let b = textureLoad(ts[indices[2]], vec2(0, 0), 0)[0];
let a = textureLoad(ts[indices[3]], vec2(0, 0), 0)[0];
return vec4(r, g, b, a);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Create a bind group with the indirection
wgpu::Buffer indices =
utils::CreateBufferFromData<uint32_t>(device, wgpu::BufferUsage::Storage, {2, 3, 1, 0});
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, testPipeline.GetBindGroupLayout(0),
{
{0, indices},
{1, MakeTestR8Texture(0).CreateView()},
{2, MakeTestR8Texture(1).CreateView()},
{3, MakeTestR8Texture(2).CreateView()},
{4, MakeTestR8Texture(3).CreateView()},
});
// Run the test
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(2, 3, 1, 0), rp.color, 0, 0);
}
// Test accessing a binding_array surrounded with other bindings in the layout.
TEST_P(SizedBindingArrayTests, ArrayEntryOversizedAndSurrounded) {
// Create the BGL with an array larger than the declaration in the shader such the trailing
// binding will be unused.
wgpu::BindGroupLayout bgl;
{
std::array<wgpu::BindGroupLayoutEntry, 3> entries;
entries[0].binding = 0;
entries[0].visibility = wgpu::ShaderStage::Fragment;
entries[0].texture.sampleType = wgpu::TextureSampleType::Float;
entries[1].binding = 1;
entries[1].visibility = wgpu::ShaderStage::Fragment;
entries[1].bindingArraySize = 3;
entries[1].texture.sampleType = wgpu::TextureSampleType::Float;
entries[2].binding = 4;
entries[2].visibility = wgpu::ShaderStage::Fragment;
entries[2].texture.sampleType = wgpu::TextureSampleType::Float;
wgpu::BindGroupLayoutDescriptor bglDesc;
bglDesc.entryCount = entries.size();
bglDesc.entries = entries.data();
bgl = device.CreateBindGroupLayout(&bglDesc);
}
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var t0 : texture_2d<f32>;
// Note that the layout will contain one extra entry using @binding(3)
@group(0) @binding(1) var ts : binding_array<texture_2d<f32>, 2>;
@group(0) @binding(4) var t3 : texture_2d<f32>;
@fragment fn fs() -> @location(0) vec4f {
let r = textureLoad(t0, vec2(0, 0), 0)[0];
let g = textureLoad(ts[0], vec2(0, 0), 0)[0];
let b = textureLoad(ts[1], vec2(0, 0), 0)[0];
let a = textureLoad(t3, vec2(0, 0), 0)[0];
return vec4(r, g, b, a);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.layout = utils::MakePipelineLayout(device, {bgl});
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Create a bind group with the explicit layout and the extra array element
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, bgl,
{
{0, MakeTestR8Texture(0).CreateView()},
{1, MakeTestR8Texture(1).CreateView()},
{2, MakeTestR8Texture(2).CreateView()},
{3, MakeTestR8Texture(42).CreateView()},
{4, MakeTestR8Texture(3).CreateView()},
});
// Run the test
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(0, 1, 2, 3), rp.color, 0, 0);
}
// Test that an arrayed BGLEntry of size 2 is compatible with a non-arrayed binding.
TEST_P(SizedBindingArrayTests, NonArrayedBindingCompatibleWithArrayedBGLEntry) {
// Create the BGL with an oversized array
wgpu::BindGroupLayout bgl;
{
wgpu::BindGroupLayoutEntry entry;
entry.binding = 0;
entry.visibility = wgpu::ShaderStage::Fragment;
entry.bindingArraySize = 2;
entry.texture.sampleType = wgpu::TextureSampleType::Float;
wgpu::BindGroupLayoutDescriptor bglDesc;
bglDesc.entryCount = 1;
bglDesc.entries = &entry;
bgl = device.CreateBindGroupLayout(&bglDesc);
}
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var t : texture_2d<f32>;
@fragment fn fs() -> @location(0) vec4f {
return vec4f(textureLoad(t, vec2(0, 0), 0).r, 0, 0, 0);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.layout = utils::MakePipelineLayout(device, {bgl});
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Run the test
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, bgl,
{
{0, MakeTestR8Texture(33).CreateView()},
{1, MakeTestR8Texture(34).CreateView()},
});
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(33, 0, 0, 0), rp.color, 0, 0);
}
// Test that binding_array<T, 1> is compatible with a non-arrayed layout.
TEST_P(SizedBindingArrayTests, BindingArraySize1CompatibleWithNonArrayedBGL) {
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var ts : binding_array<texture_2d<f32>, 1>;
@fragment fn fs() -> @location(0) vec4f {
return vec4f(textureLoad(ts[0], vec2(0, 0), 0).r, 0, 0, 0);
}
)");
wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}});
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.layout = utils::MakePipelineLayout(device, {bgl});
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Run the test
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, bgl,
{
{0, MakeTestR8Texture(42).CreateView()},
});
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(42, 0, 0, 0), rp.color, 0, 0);
}
// Test passing sampled textures of binding_array as function arguments.
TEST_P(SizedBindingArrayTests, BindingArraySampledTextureAsFunctionArgument) {
// TODO(https://issues.chromium.org/411573957) OgenGL requires that indices in arrays of sampler
// be constants but even after the DirectVariableAccess transform, the index is a function
// parameter (that's constant at the callsite) and not a constant.
DAWN_SUPPRESS_TEST_IF(IsOpenGL() || IsOpenGLES());
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
fn load(t : texture_2d<f32>) -> f32 {
return textureLoad(t, vec2(0, 0), 0)[0];
}
@group(0) @binding(0) var ts : binding_array<texture_2d<f32>, 4>;
@fragment fn fs() -> @location(0) vec4f {
let r = load(ts[0]);
let g = load(ts[1]);
let b = load(ts[2]);
let a = load(ts[3]);
return vec4(r, g, b, a);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Create a bind group with textures of decreasing values
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, testPipeline.GetBindGroupLayout(0),
{
{3, MakeTestR8Texture(0).CreateView()},
{2, MakeTestR8Texture(1).CreateView()},
{1, MakeTestR8Texture(2).CreateView()},
{0, MakeTestR8Texture(3).CreateView()},
});
// Run the test
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(3, 2, 1, 0), rp.color, 0, 0);
}
// Test accessing a binding_array of sampled textures passed as function argument.
TEST_P(SizedBindingArrayTests, BindingArrayOfSampledTexturesPassedAsArgument) {
// Crashes on the Intel Windows Vulkan shader compiler.
DAWN_SUPPRESS_TEST_IF(IsVulkan() && IsWindows() && IsIntel());
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var ts : binding_array<texture_2d<f32>, 4>;
fn f(textures : binding_array<texture_2d<f32>, 4>) -> vec4f {
let r = textureLoad(textures[0], vec2(0, 0), 0)[0];
let g = textureLoad(textures[1], vec2(0, 0), 0)[0];
let b = textureLoad(textures[2], vec2(0, 0), 0)[0];
let a = textureLoad(textures[3], vec2(0, 0), 0)[0];
return vec4(r, g, b, a);
}
@fragment fn fs() -> @location(0) vec4f {
return f(ts);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Create a bind group with textures of decreasing values
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, testPipeline.GetBindGroupLayout(0),
{
{3, MakeTestR8Texture(0).CreateView()},
{2, MakeTestR8Texture(1).CreateView()},
{1, MakeTestR8Texture(2).CreateView()},
{0, MakeTestR8Texture(3).CreateView()},
});
// Run the test
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(3, 2, 1, 0), rp.color, 0, 0);
}
// Test that multiple texture and sampler combinations are handled correctly (in particular on GL).
TEST_P(SizedBindingArrayTests, TextureAndSamplerCombination) {
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var ts : binding_array<texture_2d<f32>, 2>;
@group(0) @binding(2) var samplerClamping : sampler;
@group(0) @binding(3) var samplerWrapping : sampler;
@fragment fn fs() -> @location(0) vec4f {
let r = textureSample(ts[0], samplerWrapping, vec2(1.25, 0.5))[0];
let g = textureSample(ts[0], samplerClamping, vec2(1.25, 0.5))[0];
let b = textureSample(ts[1], samplerWrapping, vec2(1.25, 0.5))[0];
let a = textureSample(ts[1], samplerClamping, vec2(1.25, 0.5))[0];
return vec4(r, g, b, a);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Create a bind group with textures of decreasing values
wgpu::SamplerDescriptor sDesc = {};
sDesc.addressModeU = wgpu::AddressMode::ClampToEdge;
wgpu::Sampler samplerClamping = device.CreateSampler(&sDesc);
sDesc.addressModeU = wgpu::AddressMode::Repeat;
wgpu::Sampler samplerWrapping = device.CreateSampler(&sDesc);
wgpu::BindGroup arrayGroup =
utils::MakeBindGroup(device, testPipeline.GetBindGroupLayout(0),
{
{0, MakeTest2x1R8Texture(3, 2).CreateView()},
{1, MakeTest2x1R8Texture(1, 0).CreateView()},
{2, samplerClamping},
{3, samplerWrapping},
});
// Run the test
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(3, 2, 1, 0), rp.color, 0, 0);
}
// Test that calling textureNumLevels on an element of a binding_array returns the correct value,
// this is targeted for the GL backend that has emulation of that builtin with a UBO.
TEST_P(SizedBindingArrayTests, TextureNumLevels) {
// Make the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0, 0, 0.5, 0.5);
}
@group(0) @binding(0) var ts : binding_array<texture_2d<f32>, 3>;
@fragment fn fs() -> @location(0) vec4f {
let r = f32(textureNumLevels(ts[0])) / 255.0;
let g = f32(textureNumLevels(ts[1])) / 255.0;
let b = f32(textureNumLevels(ts[2])) / 255.0;
let a : f32 = 0.0;
return vec4(r, g, b, a);
}
)");
utils::ComboRenderPipelineDescriptor pDesc;
pDesc.vertex.module = module;
pDesc.cFragment.module = module;
pDesc.cFragment.targetCount = 1;
pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
pDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
wgpu::RenderPipeline testPipeline = device.CreateRenderPipeline(&pDesc);
// Create a bind group with textures views of decreasing numlevels.
wgpu::TextureDescriptor tDesc;
tDesc.size = {4, 4};
tDesc.format = wgpu::TextureFormat::RGBA8Unorm;
tDesc.usage = wgpu::TextureUsage::TextureBinding;
tDesc.mipLevelCount = 3;
wgpu::TextureView view3 = device.CreateTexture(&tDesc).CreateView();
tDesc.mipLevelCount = 2;
wgpu::TextureView view2 = device.CreateTexture(&tDesc).CreateView();
tDesc.mipLevelCount = 1;
wgpu::TextureView view1 = device.CreateTexture(&tDesc).CreateView();
wgpu::BindGroup arrayGroup = utils::MakeBindGroup(device, testPipeline.GetBindGroupLayout(0),
{
{0, view3},
{1, view2},
{2, view1},
});
// Run the test
auto rp = utils::CreateBasicRenderPass(device, 1, 1);
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo);
pass.SetPipeline(testPipeline);
pass.SetBindGroup(0, arrayGroup);
pass.Draw(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(3, 2, 1, 0), rp.color, 0, 0);
}
DAWN_INSTANTIATE_TEST(SizedBindingArrayTests,
D3D11Backend(),
D3D12Backend(),
MetalBackend(),
OpenGLBackend(),
OpenGLESBackend(),
VulkanBackend());
class DynamicBindingArrayTests : public DawnTest {
public:
void SetUp() override {
DawnTest::SetUp();
DAWN_TEST_UNSUPPORTED_IF(
!SupportsFeatures({wgpu::FeatureName::ChromiumExperimentalBindless}));
}
std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
if (SupportsFeatures({wgpu::FeatureName::ChromiumExperimentalBindless})) {
return {wgpu::FeatureName::ChromiumExperimentalBindless};
}
return {};
}
// Helper similar to utils::MakeBindGroupLayout but that adds a dynamic array.
wgpu::BindGroupLayout MakeBindGroupLayout(
wgpu::DynamicBindingKind kind,
uint32_t dynamicArrayStart = 0,
std::initializer_list<utils::BindingLayoutEntryInitializationHelper> entriesInitializer =
{}) {
std::vector<wgpu::BindGroupLayoutEntry> entries;
for (const utils::BindingLayoutEntryInitializationHelper& entry : entriesInitializer) {
entries.push_back(entry);
}
wgpu::BindGroupLayoutDynamicBindingArray dynamic;
dynamic.dynamicArray.kind = kind;
dynamic.dynamicArray.start = dynamicArrayStart;
wgpu::BindGroupLayoutDescriptor descriptor;
descriptor.nextInChain = &dynamic;
descriptor.entryCount = entries.size();
descriptor.entries = entries.data();
return device.CreateBindGroupLayout(&descriptor);
}
// Helper similar to utils::MakeBindGroup but that adds a dynamic array.
wgpu::BindGroup MakeBindGroup(
const wgpu::BindGroupLayout& layout,
uint32_t dynamicArraySize,
std::initializer_list<utils::BindingInitializationHelper> entriesInitializer) {
std::vector<wgpu::BindGroupEntry> entries;
for (const utils::BindingInitializationHelper& helper : entriesInitializer) {
entries.push_back(helper.GetAsBinding());
}
wgpu::BindGroupDynamicBindingArray dynamic;
dynamic.dynamicArraySize = dynamicArraySize;
wgpu::BindGroupDescriptor descriptor;
descriptor.nextInChain = &dynamic;
descriptor.layout = layout;
descriptor.entryCount = entries.size();
descriptor.entries = entries.data();
return device.CreateBindGroup(&descriptor);
}
// Test that `dynamicArray` (with layout `bgl` and `dynamicArrayStart`), has bindings of
// `wgslType` in the `expected` slots.
void TestHasBinding(wgpu::BindGroupLayout bgl,
wgpu::BindGroup dynamicArray,
std::vector<bool> expected,
uint32_t dynamicArrayStart = 0,
std::string wgslType = "texture_2d<f32>") {
// Create the test pipeline.
std::array<wgpu::BindGroupLayout, 2> bgls = {
bgl,
utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::Storage}}),
};
wgpu::PipelineLayoutDescriptor plDesc = {
.bindGroupLayoutCount = 2,
.bindGroupLayouts = bgls.data(),
};
wgpu::ShaderModule module =
utils::CreateShaderModule(device, R"(
enable chromium_experimental_dynamic_binding;
@group(0) @binding()" + std::to_string(dynamicArrayStart) +
R"() var bindings : resource_binding;
@group(1) @binding(0) var<storage, read_write> results : array<u32>;
@compute @workgroup_size(1) fn main() {
for (var i = 0u; i < arrayLength(bindings); i++) {
results[i] = u32(hasBinding<)" +
wgslType + R"(>(bindings, i));
}
}
)");
wgpu::ComputePipelineDescriptor csDesc = {.layout = device.CreatePipelineLayout(&plDesc),
.compute = {
.module = module,
}};
wgpu::ComputePipeline testPipeline = device.CreateComputePipeline(&csDesc);
// Create the result buffer.
wgpu::BufferDescriptor bDesc = {
.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc,
.size = sizeof(uint32_t) * expected.size(),
};
wgpu::Buffer resultBuffer = device.CreateBuffer(&bDesc);
wgpu::BindGroup resultBG = utils::MakeBindGroup(device, bgls[1], {{0, resultBuffer}});
// Run the test.
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.SetBindGroup(0, dynamicArray);
pass.SetBindGroup(1, resultBG);
pass.SetPipeline(testPipeline);
pass.DispatchWorkgroups(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
device.GetQueue().Submit(1, &commands);
// Check we have the expected results.
std::vector<uint32_t> expectedU32;
for (bool b : expected) {
expectedU32.push_back(b ? 1u : 0u);
}
EXPECT_BUFFER_U32_RANGE_EQ(expectedU32.data(), resultBuffer, 0, expectedU32.size())
<< " for WGSL type " << wgslType;
}
};
// Tests that creating the bind group that's only a dynamic array doesn't crash in backends.
TEST_P(DynamicBindingArrayTests, BindGroupOnlyDynamicArray) {
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::TextureDescriptor tDesc;
tDesc.format = wgpu::TextureFormat::R32Float;
tDesc.size = {1, 1};
tDesc.usage = wgpu::TextureUsage::TextureBinding;
wgpu::Texture tex = device.CreateTexture(&tDesc);
// Make a dense dynamic array of size 1.
MakeBindGroup(bgl, 1, {{0, tex.CreateView()}});
// Make a dense dynamic array of size 3.
MakeBindGroup(bgl, 3,
{
{0, tex.CreateView()},
{1, tex.CreateView()},
{2, tex.CreateView()},
});
// Make a sparse dynamic array.
MakeBindGroup(bgl, 3, {{1, tex.CreateView()}});
// Make an empty dynamic array.
MakeBindGroup(bgl, 3, {});
}
// Tests that creating the bind group that has static bindings and a dynamic array doesn't crash in
// backends.
TEST_P(DynamicBindingArrayTests, BindGroupDynamicArrayWithStaticBindings) {
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(
wgpu::DynamicBindingKind::SampledTexture, 4,
{
// Buffer sampler storage texture
{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform},
{1, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering},
{2, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float},
});
wgpu::TextureDescriptor tDesc;
tDesc.format = wgpu::TextureFormat::R16Float;
tDesc.size = {1, 1};
tDesc.usage = wgpu::TextureUsage::TextureBinding;
wgpu::Texture tex = device.CreateTexture(&tDesc);
wgpu::Sampler sampler = device.CreateSampler();
wgpu::BufferDescriptor bDesc;
bDesc.size = 4;
bDesc.usage = wgpu::BufferUsage::Uniform;
wgpu::Buffer buffer = device.CreateBuffer(&bDesc);
// Make a dense dynamic array of size 1.
MakeBindGroup(bgl, 1,
{{0, buffer}, {1, sampler}, {2, tex.CreateView()}, {4, tex.CreateView()}});
// Make a dense dynamic array of size 3.
MakeBindGroup(bgl, 3,
{
{0, buffer},
{1, sampler},
{2, tex.CreateView()},
{4, tex.CreateView()},
{5, tex.CreateView()},
{6, tex.CreateView()},
});
// Make a sparse dynamic array.
MakeBindGroup(bgl, 3,
{{0, buffer}, {1, sampler}, {2, tex.CreateView()}, {5, tex.CreateView()}});
// Make an empty dynamic array.
MakeBindGroup(bgl, 3, {{0, buffer}, {1, sampler}, {2, tex.CreateView()}});
}
// Test that creating bind groups of different sizes doesn't end up reuse incorrectly sized
// allocations.
TEST_P(DynamicBindingArrayTests, RecyclingDoesntReuseTooSmallAllocation) {
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
for (uint32_t i = 0; i < 10; i++) {
MakeBindGroup(bgl, i, {});
// Wait to ensure some deallocation happens and has a chance to cause incorrect recycling.
WaitForAllOperations();
}
}
// Tests that pinning / unpinning doesn't crash in backends. TextureVk has ASSERTs that the frontend
// ensures pinning / unpinning is balanced to help backends.
TEST_P(DynamicBindingArrayTests, PinningBalancedInBackends) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R16Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
// Frontend should skip that unpinning as the texture is not pinned.
tex.Unpin();
// Duplicate pinning should be skipped by the frontend.
tex.Pin(wgpu::TextureUsage::TextureBinding);
tex.Pin(wgpu::TextureUsage::TextureBinding);
// Duplicate unpinning should be skipped by the frontend.
tex.Unpin();
tex.Unpin();
// Force a queue submit to flush pending commands and potentially find more issues.
queue.Submit(0, nullptr);
}
// TODO(https://crbug.com/435317394): Once we have support for running shader with dynamic binding
// arrays, add tests that pinning handles lazy clearing:
// - Check that a newly created resource that's pinned samples as zeroes.
// - Likewise for a texture written to, then discarded with a render pass.
// Test that the WGSL `arrayLength` builtin on dynamic binding arrays returns the correct length.
TEST_P(DynamicBindingArrayTests, ArrayLengthBuiltin) {
// Create a compute pipeline that returns the array length of the dynamic binding arrays.
// One of them has a static binding as well so as to check that it doesn't mess up the
// computation of the array length.
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
enable chromium_experimental_dynamic_binding;
@group(0) @binding(0) var<storage, read_write> result : array<u32, 2>;
@group(0) @binding(1) var firstBindings : resource_binding;
@group(1) @binding(0) var secondBindings : resource_binding;
@compute @workgroup_size(1) fn getArrayLengths() {
// Force the defaulted layout to wgpu::DynamicBindingKind::SampledTexture
_ = hasBinding<texture_2d<f32>>(firstBindings, 0);
_ = hasBinding<texture_2d<f32>>(secondBindings, 0);
result[0] = arrayLength(firstBindings);
result[1] = arrayLength(secondBindings);
}
)");
wgpu::ComputePipelineDescriptor csDesc = {.compute = {
.module = module,
}};
wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc);
// Create the dynamic binding arrays and fetch their array length in a buffer.
wgpu::BufferDescriptor bDesc = {
.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc,
.size = 2 * sizeof(uint32_t),
};
wgpu::Buffer resultBuffer = device.CreateBuffer(&bDesc);
wgpu::BindGroup bg0 = MakeBindGroup(pipeline.GetBindGroupLayout(0), 17, {{0, resultBuffer}});
wgpu::BindGroup bg1 = MakeBindGroup(pipeline.GetBindGroupLayout(1), 3, {});
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.SetBindGroup(0, bg0);
pass.SetBindGroup(1, bg1);
pass.SetPipeline(pipeline);
pass.DispatchWorkgroups(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
device.GetQueue().Submit(1, &commands);
// The result buffer should contain the lengths of the dynamic binding arrays.
EXPECT_BUFFER_U32_EQ(17, resultBuffer, 0);
EXPECT_BUFFER_U32_EQ(3, resultBuffer, 4);
}
// Test WGSL `hasBinding` reflects the state of a dynamic binding array.
TEST_P(DynamicBindingArrayTests, HasBindingOneTexturePinUnpin) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::BindGroup bg = MakeBindGroup(bgl, 3, {{1, tex.CreateView()}});
// Before pinning, the bind group has no valid entries.
TestHasBinding(bgl, bg, {false, false, false});
// After pinning it has the one valid entry valid.
tex.Pin(wgpu::TextureUsage::TextureBinding);
TestHasBinding(bgl, bg, {false, true, false});
// After unpinning it has the no more valid entries.
tex.Unpin();
TestHasBinding(bgl, bg, {false, false, false});
}
// Test pin/unpin updating the availability takes into account the static bindings (so even if it
// doesn't start at BindingIndex 0, things still work)
TEST_P(DynamicBindingArrayTests, HasBindingOneTexturePinUnpinWithStaticBindings) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(
wgpu::DynamicBindingKind::SampledTexture, 4,
{{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::UnfilterableFloat}});
wgpu::BindGroup bg = MakeBindGroup(bgl, 3, {{0, tex.CreateView()}, {5, tex.CreateView()}});
// Before pinning, the bind group has no valid entries.
TestHasBinding(bgl, bg, {false, false, false}, 4);
// After pinning it has the one valid entry valid.
tex.Pin(wgpu::TextureUsage::TextureBinding);
TestHasBinding(bgl, bg, {false, true, false}, 4);
// After unpinning it has the no more valid entries.
tex.Unpin();
TestHasBinding(bgl, bg, {false, false, false}, 4);
}
// Test that calling texture.Destroy() implicitly unpins it.
TEST_P(DynamicBindingArrayTests, HasBindingOneTexturePinDestroy) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::BindGroup bg = MakeBindGroup(bgl, 3, {{1, tex.CreateView()}});
// Before pinning, the bind group has no valid entries.
TestHasBinding(bgl, bg, {false, false, false});
// After pinning it has the one valid entry valid.
tex.Pin(wgpu::TextureUsage::TextureBinding);
TestHasBinding(bgl, bg, {false, true, false});
// After texture destruction it has the no more valid entries.
tex.Destroy();
TestHasBinding(bgl, bg, {false, false, false});
}
// Test that a texture used multiple times in the same dynamic binding array has its availability
// correctly updated.
TEST_P(DynamicBindingArrayTests, HasBindingSameTextureMultipleTimesPinUnpin) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::BindGroup bg = MakeBindGroup(bgl, 4, {{1, tex.CreateView()}, {3, tex.CreateView()}});
// Before pinning, the bind group has no valid entries.
TestHasBinding(bgl, bg, {false, false, false, false});
// After pinning it has valid entries.
tex.Pin(wgpu::TextureUsage::TextureBinding);
TestHasBinding(bgl, bg, {false, true, false, true});
// After unpinning it has the no more valid entries.
tex.Unpin();
TestHasBinding(bgl, bg, {false, false, false, false});
}
// Test that creating a dynamic binding array with an already destroyed texture works, but doesn't
// show that entry as available.
TEST_P(DynamicBindingArrayTests, HasBindingDynamicArrayCreatedWithTextureAlreadyDestroyed) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
tex.Destroy();
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::BindGroup bg = MakeBindGroup(bgl, 1, {{0, tex.CreateView()}});
// Before pinning, the bind group has no valid entries.
TestHasBinding(bgl, bg, {false});
}
// Test that a texture used multiple times in the same dynamic binding array has its
// availability correctly updated.
TEST_P(DynamicBindingArrayTests, HasBindingSameTextureMultipleDynamicArrays) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::BindGroup bg1 = MakeBindGroup(bgl, 3, {{1, tex.CreateView()}});
wgpu::BindGroup bg2 = MakeBindGroup(bgl, 1, {{0, tex.CreateView()}});
// Before pinning, the bind group has no valid entries.
TestHasBinding(bgl, bg1, {false, false, false});
TestHasBinding(bgl, bg2, {false});
// After pinning it has valid entries.
tex.Pin(wgpu::TextureUsage::TextureBinding);
TestHasBinding(bgl, bg1, {false, true, false});
TestHasBinding(bgl, bg2, {true});
// After destroying on dynamic binding array, the other still has the texture available.
bg1.Destroy();
TestHasBinding(bgl, bg2, {true});
}
// Test that texture availabililty is controlled per-texture.
TEST_P(DynamicBindingArrayTests, HasBindingMultipleTexturesInDynamicArray) {
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex0 = device.CreateTexture(&tDesc);
wgpu::Texture tex1 = device.CreateTexture(&tDesc);
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::BindGroup bg = MakeBindGroup(bgl, 2, {{0, tex0.CreateView()}, {1, tex1.CreateView()}});
// Before pinning, the bind group has no valid entries.
TestHasBinding(bgl, bg, {false, false});
// After pinning tex0 it has one valid entry.
tex0.Pin(wgpu::TextureUsage::TextureBinding);
TestHasBinding(bgl, bg, {true, false});
// After pinning tex1 it has two valid entries.
tex1.Pin(wgpu::TextureUsage::TextureBinding);
TestHasBinding(bgl, bg, {true, true});
// After unpinning tex0 it has only one valid entry.
tex0.Unpin();
TestHasBinding(bgl, bg, {false, true});
}
constexpr auto kWgslSampledTextureTypes = std::array{
"texture_1d<f32>",
"texture_1d<i32>",
"texture_1d<u32>",
"texture_2d<f32>",
"texture_2d<i32>",
"texture_2d<u32>",
"texture_2d_array<f32>",
"texture_2d_array<i32>",
"texture_2d_array<u32>",
"texture_cube<f32>",
"texture_cube<i32>",
"texture_cube<u32>",
"texture_cube_array<f32>",
"texture_cube_array<i32>",
"texture_cube_array<u32>",
"texture_3d<f32>",
"texture_3d<i32>",
"texture_3d<u32>",
"texture_multisampled_2d<f32>",
"texture_multisampled_2d<i32>",
"texture_multisampled_2d<u32>",
"texture_depth_2d",
"texture_depth_2d_array",
"texture_depth_cube",
"texture_depth_cube_array",
"texture_depth_multisampled_2d",
};
struct TextureDescForTypeIDCase {
std::unordered_set<std::string_view> wgslTypes;
wgpu::TextureFormat format;
wgpu::TextureDimension dimension;
wgpu::TextureViewDimension viewDimension = wgpu::TextureViewDimension::Undefined;
uint32_t sampleCount = 1;
wgpu::TextureAspect viewAspect = wgpu::TextureAspect::All;
// Create a view for a pinned texture for this case.
wgpu::TextureView CreateTestView(const wgpu::Device& device) {
wgpu::TextureDescriptor tDesc = {
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc,
.dimension = dimension,
.size = {1, 1, 1},
.format = format,
.sampleCount = sampleCount,
};
if (viewDimension == wgpu::TextureViewDimension::Cube ||
viewDimension == wgpu::TextureViewDimension::CubeArray) {
tDesc.size.depthOrArrayLayers = 6;
}
if (sampleCount != 1) {
tDesc.usage |= wgpu::TextureUsage::RenderAttachment;
}
wgpu::TextureViewDescriptor vDesc{
.dimension = viewDimension,
.aspect = viewAspect,
.usage = wgpu::TextureUsage::TextureBinding,
};
wgpu::Texture texture = device.CreateTexture(&tDesc);
texture.Pin(wgpu::TextureUsage::TextureBinding);
return texture.CreateView(&vDesc);
}
};
std::vector<TextureDescForTypeIDCase> MakeTextureDescForTypeIDCases() {
std::vector<TextureDescForTypeIDCase> cases;
// TODO(https://crbug.com/435317394): Add tests of filterable vs. unfilterable floats when
// get/hasBinding is able to make the difference.
// Regular 1D textures.
cases.push_back({
.wgslTypes = {{"texture_1d<f32>"}},
.format = wgpu::TextureFormat::RGBA32Float,
.dimension = wgpu::TextureDimension::e1D,
});
cases.push_back({
.wgslTypes = {{"texture_1d<i32>"}},
.format = wgpu::TextureFormat::RGBA32Sint,
.dimension = wgpu::TextureDimension::e1D,
});
cases.push_back({
.wgslTypes = {{"texture_1d<u32>"}},
.format = wgpu::TextureFormat::RGBA32Uint,
.dimension = wgpu::TextureDimension::e1D,
});
// Regular 2D textures.
cases.push_back({
.wgslTypes = {{"texture_2d<f32>"}},
.format = wgpu::TextureFormat::RGBA32Float,
.dimension = wgpu::TextureDimension::e2D,
});
cases.push_back({
.wgslTypes = {{"texture_2d<i32>"}},
.format = wgpu::TextureFormat::RGBA32Sint,
.dimension = wgpu::TextureDimension::e2D,
});
cases.push_back({
.wgslTypes = {{"texture_2d<u32>"}},
.format = wgpu::TextureFormat::RGBA32Uint,
.dimension = wgpu::TextureDimension::e2D,
});
// Regular 2D array textures.
cases.push_back({
.wgslTypes = {{"texture_2d_array<f32>"}},
.format = wgpu::TextureFormat::RGBA32Float,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::e2DArray,
});
cases.push_back({
.wgslTypes = {{"texture_2d_array<i32>"}},
.format = wgpu::TextureFormat::RGBA32Sint,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::e2DArray,
});
cases.push_back({
.wgslTypes = {{"texture_2d_array<u32>"}},
.format = wgpu::TextureFormat::RGBA32Uint,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::e2DArray,
});
// Regular cube textures.
cases.push_back({
.wgslTypes = {{"texture_cube<f32>"}},
.format = wgpu::TextureFormat::RGBA32Float,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::Cube,
});
cases.push_back({
.wgslTypes = {{"texture_cube<i32>"}},
.format = wgpu::TextureFormat::RGBA32Sint,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::Cube,
});
cases.push_back({
.wgslTypes = {{"texture_cube<u32>"}},
.format = wgpu::TextureFormat::RGBA32Uint,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::Cube,
});
// Regular cube array textures.
cases.push_back({
.wgslTypes = {{"texture_cube_array<f32>"}},
.format = wgpu::TextureFormat::RGBA32Float,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::CubeArray,
});
cases.push_back({
.wgslTypes = {{"texture_cube_array<i32>"}},
.format = wgpu::TextureFormat::RGBA32Sint,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::CubeArray,
});
cases.push_back({
.wgslTypes = {{"texture_cube_array<u32>"}},
.format = wgpu::TextureFormat::RGBA32Uint,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::CubeArray,
});
// Regular 3d textures.
cases.push_back({
.wgslTypes = {{"texture_3d<f32>"}},
.format = wgpu::TextureFormat::RGBA32Float,
.dimension = wgpu::TextureDimension::e3D,
});
cases.push_back({
.wgslTypes = {{"texture_3d<i32>"}},
.format = wgpu::TextureFormat::RGBA32Sint,
.dimension = wgpu::TextureDimension::e3D,
});
cases.push_back({
.wgslTypes = {{"texture_3d<u32>"}},
.format = wgpu::TextureFormat::RGBA32Uint,
.dimension = wgpu::TextureDimension::e3D,
});
// Color multisampled textures.
cases.push_back({
.wgslTypes = {{"texture_multisampled_2d<f32>"}},
.format = wgpu::TextureFormat::RGBA16Float,
.dimension = wgpu::TextureDimension::e2D,
.sampleCount = 4,
});
cases.push_back({
.wgslTypes = {{"texture_multisampled_2d<i32>"}},
.format = wgpu::TextureFormat::RGBA16Sint,
.dimension = wgpu::TextureDimension::e2D,
.sampleCount = 4,
});
cases.push_back({
.wgslTypes = {{"texture_multisampled_2d<u32>"}},
.format = wgpu::TextureFormat::RGBA16Uint,
.dimension = wgpu::TextureDimension::e2D,
.sampleCount = 4,
});
// Depth textures (including multisampled).
// TODO(https://crbug.com/435317394): In the future we should allow depth textures to be used as
// texture_*<f32>.
cases.push_back({
.wgslTypes = {{"texture_depth_2d"}},
.format = wgpu::TextureFormat::Depth32Float,
.dimension = wgpu::TextureDimension::e2D,
});
cases.push_back({
.wgslTypes = {{"texture_depth_2d_array"}},
.format = wgpu::TextureFormat::Depth32Float,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::e2DArray,
});
cases.push_back({
.wgslTypes = {{"texture_depth_cube"}},
.format = wgpu::TextureFormat::Depth32Float,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::Cube,
});
cases.push_back({
.wgslTypes = {{"texture_depth_cube_array"}},
.format = wgpu::TextureFormat::Depth32Float,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::CubeArray,
});
cases.push_back({
.wgslTypes = {{"texture_depth_multisampled_2d"}},
.format = wgpu::TextureFormat::Depth32Float,
.dimension = wgpu::TextureDimension::e2D,
.sampleCount = 4,
});
// Stencil textures can be used as 2D.
cases.push_back({
.wgslTypes = {{"texture_2d<u32>"}},
.format = wgpu::TextureFormat::Stencil8,
.dimension = wgpu::TextureDimension::e2D,
});
cases.push_back({
.wgslTypes = {{"texture_2d_array<u32>"}},
.format = wgpu::TextureFormat::Stencil8,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::e2DArray,
});
cases.push_back({
.wgslTypes = {{"texture_cube<u32>"}},
.format = wgpu::TextureFormat::Stencil8,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::Cube,
});
cases.push_back({
.wgslTypes = {{"texture_cube_array<u32>"}},
.format = wgpu::TextureFormat::Stencil8,
.dimension = wgpu::TextureDimension::e2D,
.viewDimension = wgpu::TextureViewDimension::CubeArray,
});
// Depth-stencil textures with only one aspect selected.
cases.push_back({
.wgslTypes = {{"texture_depth_2d"}},
.format = wgpu::TextureFormat::Depth24PlusStencil8,
.dimension = wgpu::TextureDimension::e2D,
.viewAspect = wgpu::TextureAspect::DepthOnly,
});
cases.push_back({
.wgslTypes = {{"texture_2d<u32>"}},
.format = wgpu::TextureFormat::Depth24PlusStencil8,
.dimension = wgpu::TextureDimension::e2D,
.viewAspect = wgpu::TextureAspect::StencilOnly,
});
return cases;
}
// TODO(https://crbug.com/435317394): When wgpu::BindGroup::Update() or equivalent is added, test
// that availability is updated when entries in the dynamic binding array are updated.
// Test that hasBinding() works as expected for all support types in WGSL.
TEST_P(DynamicBindingArrayTests, HasBindingTextureCompatibilityAllTypes) {
auto textureCases = MakeTextureDescForTypeIDCases();
// Make a dynamic binding array with all of our test textures.
std::vector<wgpu::BindGroupEntry> entries;
for (auto [i, textureCase] : Enumerate(textureCases)) {
wgpu::BindGroupEntry entry{
.binding = uint32_t(i),
.textureView = textureCase.CreateTestView(device),
};
entries.push_back(entry);
}
wgpu::BindGroupLayout bgl = MakeBindGroupLayout(wgpu::DynamicBindingKind::SampledTexture);
wgpu::BindGroupDynamicBindingArray dynamic;
dynamic.dynamicArraySize = entries.size();
wgpu::BindGroupDescriptor bgDesc = {
.nextInChain = &dynamic,
.layout = bgl,
.entryCount = entries.size(),
.entries = entries.data(),
};
wgpu::BindGroup bg = device.CreateBindGroup(&bgDesc);
// Test hasBinding returning for each of the supported WGSL types, against each texture.
for (auto wgslType : kWgslSampledTextureTypes) {
std::vector<bool> expected;
for (auto textureCase : textureCases) {
expected.push_back(textureCase.wgslTypes.contains(wgslType));
}
TestHasBinding(bgl, bg, expected, 0, wgslType);
}
}
// Test that calling hasBinding() with values outside of [0, arrayLength) returns 0.
TEST_P(DynamicBindingArrayTests, HasBindingOOBIsFalse) {
// Create the test pipeline
wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
enable chromium_experimental_dynamic_binding;
@group(0) @binding(0) var<storage, read_write> result : array<u32, 4>;
@group(0) @binding(1) var a : resource_binding;
@compute @workgroup_size(1) fn getArrayLengths() {
result[0] = u32(hasBinding<texture_2d<f32>>(a, arrayLength(a) - 1));
result[1] = u32(hasBinding<texture_2d<f32>>(a, arrayLength(a)));
result[2] = u32(hasBinding<texture_2d<f32>>(a, arrayLength(a) + 1)) +
u32(hasBinding<texture_2d<f32>>(a, arrayLength(a) + 2)) +
u32(hasBinding<texture_2d<f32>>(a, arrayLength(a) + 3)) +
u32(hasBinding<texture_2d<f32>>(a, arrayLength(a) + 4));
result[3] = u32(hasBinding<texture_2d<f32>>(a, arrayLength(a) + 10000000));
}
)");
wgpu::ComputePipelineDescriptor csDesc = {.compute = {
.module = module,
}};
wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc);
// Create the test resources.
wgpu::BufferDescriptor bDesc = {
.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc,
.size = 4 * sizeof(uint32_t),
};
wgpu::Buffer resultBuffer = device.CreateBuffer(&bDesc);
wgpu::TextureDescriptor tDesc{
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::R32Float,
};
wgpu::Texture tex = device.CreateTexture(&tDesc);
tex.Pin(wgpu::TextureUsage::TextureBinding);
wgpu::BindGroup bg = MakeBindGroup(pipeline.GetBindGroupLayout(0), 3,
{
{0, resultBuffer},
{1, tex.CreateView()},
{2, tex.CreateView()},
{3, tex.CreateView()},
});
// Run the test and check results are the expected ones.
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.SetBindGroup(0, bg);
pass.SetPipeline(pipeline);
pass.DispatchWorkgroups(1);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
device.GetQueue().Submit(1, &commands);
EXPECT_BUFFER_U32_EQ(1, resultBuffer, 0);
EXPECT_BUFFER_U32_EQ(0, resultBuffer, 4);
EXPECT_BUFFER_U32_EQ(0, resultBuffer, 8);
EXPECT_BUFFER_U32_EQ(0, resultBuffer, 12);
}
DAWN_INSTANTIATE_TEST(DynamicBindingArrayTests, D3D12Backend(), MetalBackend(), VulkanBackend());
} // anonymous namespace
} // namespace dawn