| // 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, ©Size); | |
| 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, ©Size); | |
| 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 |