| // 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 |