| // 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 <utility> |
| #include <vector> |
| |
| #include "dawn/common/Enumerator.h" |
| #include "dawn/tests/DawnTest.h" |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" |
| #include "dawn/utils/ScopedIgnoreValidationErrors.h" |
| #include "dawn/utils/WGPUHelpers.h" |
| |
| namespace dawn { |
| namespace { |
| |
| class ResourceTableTests : public DawnTest { |
| protected: |
| void SetUp() override { |
| DawnTest::SetUp(); |
| DAWN_TEST_UNSUPPORTED_IF( |
| !SupportsFeatures({wgpu::FeatureName::ChromiumExperimentalSamplingResourceTable})); |
| |
| // TODO(https://issues.chromium.org/435317394): The Subzero compiler used by Swiftshader |
| // produces bad code and crashes on some VK_EXT_descriptor_indexing workloads. Skip tests on |
| // it, but still run them with Swiftshader LLVM 10.0. On ARM64 the only supported compiler |
| // is LLVM10.0 so use that signal to choose when Swiftshader can be tested. |
| DAWN_SUPPRESS_TEST_IF(IsSwiftshader() && !DAWN_PLATFORM_IS(ARM64)); |
| } |
| |
| std::vector<wgpu::FeatureName> GetRequiredFeatures() override { |
| if (SupportsFeatures({wgpu::FeatureName::ChromiumExperimentalSamplingResourceTable})) { |
| return {wgpu::FeatureName::ChromiumExperimentalSamplingResourceTable}; |
| } |
| return {}; |
| } |
| |
| wgpu::ResourceTable MakeResourceTable( |
| uint32_t size, |
| std::vector<std::pair<uint32_t, wgpu::BindingResource>> resources = {}) { |
| wgpu::ResourceTableDescriptor desc; |
| desc.size = size; |
| wgpu::ResourceTable table = device.CreateResourceTable(&desc); |
| |
| for (auto& [slot, resource] : resources) { |
| EXPECT_EQ(wgpu::Status::Success, table.Update(slot, &resource)); |
| } |
| |
| return table; |
| } |
| |
| wgpu::PipelineLayout MakePipelineLayoutWithTable(std::vector<wgpu::BindGroupLayout> bgls = {}, |
| uint32_t immediateSize = 0) { |
| wgpu::PipelineLayoutResourceTable plTable; |
| plTable.usesResourceTable = true; |
| |
| wgpu::PipelineLayoutDescriptor desc{ |
| .nextInChain = &plTable, |
| .bindGroupLayoutCount = bgls.size(), |
| .bindGroupLayouts = bgls.data(), |
| .immediateSize = immediateSize, |
| }; |
| |
| return device.CreatePipelineLayout(&desc); |
| } |
| |
| // Test that the `table`, has resources of `wgslType` in the `expected` slots. |
| void TestHasResource(wgpu::ResourceTable table, |
| std::vector<bool> expected, |
| std::string wgslType = "texture_2d<f32>") { |
| ASSERT_EQ(table.GetSize(), expected.size()); |
| |
| // Create the test pipeline. |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| |
| @group(0) @binding(0) var<storage, read_write> results : array<u32>; |
| var<immediate> resourceCount : u32; |
| @compute @workgroup_size(1) fn main() { |
| for (var i = 0u; i < resourceCount; i++) { |
| results[i] = u32(hasResource<)" + wgslType + R"(>(i)); |
| } |
| } |
| )"); |
| wgpu::ComputePipelineDescriptor csDesc = {.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, testPipeline.GetBindGroupLayout(0), {{0, resultBuffer}}); |
| uint32_t resourceCount = table.GetSize(); |
| |
| // Run the test. |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetImmediates(0, &resourceCount, sizeof(resourceCount)); |
| pass.SetBindGroup(0, 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; |
| } |
| |
| void DoSomeWorkInSubmit() { |
| wgpu::BufferDescriptor bufDesc = { |
| .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc, |
| .size = 4, |
| }; |
| wgpu::Buffer src = device.CreateBuffer(&bufDesc); |
| wgpu::Buffer dst = device.CreateBuffer(&bufDesc); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyBufferToBuffer(src, 0, dst, 0, 4); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| } |
| |
| wgpu::TextureView MakePinnedU8View(uint8_t value) { |
| // Create the texture. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R8Uint, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| // Write the u8 |
| wgpu::TexelCopyTextureInfo srcInfo = utils::CreateTexelCopyTextureInfo(tex); |
| wgpu::TexelCopyBufferLayout dstInfo = {}; |
| wgpu::Extent3D copySize = {1, 1, 1}; |
| queue.WriteTexture(&srcInfo, &value, 1, &dstInfo, ©Size); |
| |
| // Return a view to the pinned texture. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| return tex.CreateView(); |
| } |
| |
| // Test that `table` has a texture_2d<u32> iff the `expected` has a value, and that the textures |
| // have the expected value, if any. |
| void TestHasU8Bindings(wgpu::ResourceTable table, |
| std::vector<std::optional<uint8_t>> expected) { |
| ASSERT_EQ(table.GetSize(), expected.size()); |
| |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| |
| @group(0) @binding(0) var<storage, read_write> results : array<u32>; |
| var<immediate> resourceCount : u32; |
| @compute @workgroup_size(1) fn main() { |
| for (var i = 0u; i < resourceCount; i++) { |
| if !hasResource<texture_2d<u32>>(i) { |
| results[i] = 0xBEEF; |
| } else { |
| let tex = getResource<texture_2d<u32>>(i); |
| results[i] = textureLoad(tex, vec2(0), 0).x; |
| } |
| } |
| } |
| )"); |
| |
| wgpu::ComputePipelineDescriptor csDesc = {.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, testPipeline.GetBindGroupLayout(0), {{0, resultBuffer}}); |
| uint32_t resourceCount = table.GetSize(); |
| |
| // Run the test. |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetImmediates(0, &resourceCount, sizeof(resourceCount)); |
| pass.SetBindGroup(0, 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 (auto optValue : expected) { |
| expectedU32.push_back(optValue ? *optValue : 0xBEEFu); |
| } |
| |
| EXPECT_BUFFER_U32_RANGE_EQ(expectedU32.data(), resultBuffer, 0, expectedU32.size()); |
| } |
| }; |
| |
| // Test that creating resource tables doesn't crash in backends. |
| TEST_P(ResourceTableTests, ResourceTableCreation) { |
| // Creating an empty resource table. |
| MakeResourceTable(0); |
| |
| // Creating a resource table with a few entries. |
| MakeResourceTable(36); |
| |
| // Creating a resource table with the maximum number of entries. |
| MakeResourceTable(kMaxResourceTableSize); |
| } |
| |
| // Test that creating pipeline layouts with resources tables doesn't crash in backends. |
| TEST_P(ResourceTableTests, PipelineLayoutWithResourceTableCreation) { |
| // Make layouts with no BGLs with / without immediates. |
| MakePipelineLayoutWithTable({}, 0); |
| MakePipelineLayoutWithTable({}, 4); |
| |
| // Make layouts with one BGL, with / without immediates. |
| wgpu::BindGroupLayout testBgl = utils::MakeBindGroupLayout( |
| device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform}}); |
| MakePipelineLayoutWithTable({testBgl}, 0); |
| MakePipelineLayoutWithTable({testBgl}, 4); |
| |
| // Make layouts with max BGLs (3 because the resource tables "consumes" one bind group), with / |
| // without immediates. |
| MakePipelineLayoutWithTable({testBgl, testBgl, testBgl}, 0); |
| MakePipelineLayoutWithTable({testBgl, testBgl, testBgl}, 4); |
| } |
| |
| // Test that creating pipelines that use resource tables doesn't crash in backends. |
| TEST_P(ResourceTableTests, ShaderWithResourceTableCreation) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| |
| // Test compiling a pipeline using only the resource table. |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| } |
| )"); |
| device.CreateComputePipeline(&csDesc); |
| |
| // Test compiling a pipeline using the resource table and a bindgroup. |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @group(0) @binding(0) var t0 : texture_2d<f32>; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| _ = t0; |
| } |
| )"); |
| device.CreateComputePipeline(&csDesc); |
| |
| // Test compiling a pipeline using the resource table and many bindgroup. |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @group(0) @binding(0) var t0 : texture_2d<f32>; |
| @group(1) @binding(0) var t1 : texture_2d<f32>; |
| @group(2) @binding(0) var t2 : texture_2d<f32>; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| _ = t0; |
| _ = t1; |
| _ = t2; |
| } |
| )"); |
| device.CreateComputePipeline(&csDesc); |
| } |
| |
| // Test that creating resource tables of different sizes doesn't end up reusing incorrectly sized |
| // allocations. |
| TEST_P(ResourceTableTests, RecyclingDoesntReuseTooSmallAllocation) { |
| for (uint32_t i = 0; i < 10; i++) { |
| MakeResourceTable(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. |
| TEST_P(ResourceTableTests, 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); |
| } |
| |
| // Test WGSL `hasResource` reflects the state of the resource table. |
| TEST_P(ResourceTableTests, HasResourceOneTexturePinUnpin) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| wgpu::ResourceTable table = MakeResourceTable(3, {{1, {.textureView = tex.CreateView()}}}); |
| |
| // Before pinning, the table has no valid entries. |
| TestHasResource(table, {false, false, false}); |
| |
| // After pinning it has the one valid entry valid. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| TestHasResource(table, {false, true, false}); |
| |
| // After unpinning it has the no more valid entries. |
| tex.Unpin(); |
| TestHasResource(table, {false, false, false}); |
| } |
| |
| // Test that calling texture.Destroy() implicitly unpins it. |
| TEST_P(ResourceTableTests, HasResourceOneTexturePinDestroy) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| wgpu::ResourceTable table = MakeResourceTable(3, {{1, {.textureView = tex.CreateView()}}}); |
| |
| // Before pinning, the table has no valid entries. |
| TestHasResource(table, {false, false, false}); |
| |
| // After pinning it has the one valid entry valid. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| TestHasResource(table, {false, true, false}); |
| |
| // After texture destruction it has the no more valid entries. |
| tex.Destroy(); |
| TestHasResource(table, {false, false, false}); |
| } |
| |
| // Test that a texture used multiple times in the same table has its availability correctly updated. |
| TEST_P(ResourceTableTests, HasResourceSameTextureMultipleTimesPinUnpin) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| wgpu::ResourceTable table = MakeResourceTable(4, { |
| {1, {.textureView = tex.CreateView()}}, |
| {3, {.textureView = tex.CreateView()}}, |
| }); |
| |
| // Before pinning, the table has no valid entries. |
| TestHasResource(table, {false, false, false, false}); |
| |
| // After pinning it has valid entries. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| TestHasResource(table, {false, true, false, true}); |
| |
| // After unpinning it has the no more valid entries. |
| tex.Unpin(); |
| TestHasResource(table, {false, false, false, false}); |
| } |
| |
| // Test that updating a table with an already destroyed texture works, but doesn't show that entry |
| // as available. |
| TEST_P(ResourceTableTests, HasResourceUpdateWithTextureAlreadyDestroyed) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| tex.Destroy(); |
| |
| wgpu::ResourceTable table = MakeResourceTable(1, {{0, {.textureView = tex.CreateView()}}}); |
| |
| // Before pinning, the table has no valid entries. |
| TestHasResource(table, {false}); |
| } |
| |
| // Test that a texture used in multiple resource tables has its availability correctly updated. |
| TEST_P(ResourceTableTests, HasResourceSameTextureMultipleTables) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| wgpu::ResourceTable table1 = MakeResourceTable(3, {{1, {.textureView = tex.CreateView()}}}); |
| wgpu::ResourceTable table2 = MakeResourceTable(1, {{0, {.textureView = tex.CreateView()}}}); |
| |
| // Before pinning, the tables have no valid entries. |
| TestHasResource(table1, {false, false, false}); |
| TestHasResource(table2, {false}); |
| |
| // After pinning the texture, they have valid entries. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| TestHasResource(table1, {false, true, false}); |
| TestHasResource(table2, {true}); |
| |
| // After destroying one table, the other still has the texture available. |
| table1.Destroy(); |
| TestHasResource(table2, {true}); |
| } |
| |
| // Test that texture availabililty is controlled per-texture. |
| TEST_P(ResourceTableTests, HasResourceMultipleTexturesTable) { |
| 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::ResourceTable table = MakeResourceTable(2, { |
| {0, {.textureView = tex0.CreateView()}}, |
| {1, {.textureView = tex1.CreateView()}}, |
| }); |
| |
| // Before pinning, the table has no valid entries. |
| TestHasResource(table, {false, false}); |
| |
| // After pinning tex0 it has one valid entry. |
| tex0.Pin(wgpu::TextureUsage::TextureBinding); |
| TestHasResource(table, {true, false}); |
| |
| // After pinning tex1 it has two valid entries. |
| tex1.Pin(wgpu::TextureUsage::TextureBinding); |
| TestHasResource(table, {true, true}); |
| |
| // After unpinning tex0 it has only one valid entry. |
| tex0.Unpin(); |
| TestHasResource(table, {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://issues.chromium.org/473354065): Add tests of filterable vs. unfilterable floats |
| // when get/hasResource 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://issues.chromium.org/473354065): 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; |
| } |
| |
| // Test that hasResource() works as expected for all supported types in WGSL. |
| TEST_P(ResourceTableTests, HasResourceTextureCompatibilityAllTypes) { |
| auto textureCases = MakeTextureDescForTypeIDCases(); |
| |
| // Make a resource table with all of our test texture views. |
| wgpu::ResourceTable table = MakeResourceTable(textureCases.size()); |
| for (auto [i, textureCase] : Enumerate(textureCases)) { |
| wgpu::BindingResource resource = {.textureView = textureCase.CreateTestView(device)}; |
| table.Update(i, &resource); |
| } |
| |
| // Test hasResource 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)); |
| } |
| |
| TestHasResource(table, expected, wgslType); |
| } |
| } |
| |
| // Test that calling hasResource() with values outside of the resource table size returns false. |
| TEST_P(ResourceTableTests, HasResourceOOBIsFalse) { |
| // Create the test pipeline |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| |
| @group(0) @binding(0) var<storage, read_write> result : array<u32, 4>; |
| var<immediate> resourceCount : u32; |
| @compute @workgroup_size(1) fn getArrayLengths() { |
| result[0] = u32(hasResource<texture_2d<f32>>(resourceCount - 1)); |
| result[1] = u32(hasResource<texture_2d<f32>>(resourceCount)); |
| |
| // Check against all the slots where the default resources are. |
| var result2 = 0u; |
| for (var i = 1u; i < 100; i++) { |
| result2 += u32(hasResource<texture_2d<f32>>(resourceCount + i)); |
| } |
| result[2] = result2; |
| |
| result[3] = u32(hasResource<texture_2d<f32>>(resourceCount + 10000000)); |
| } |
| )"); |
| wgpu::ComputePipelineDescriptor csDesc = {.compute = { |
| .module = module, |
| }}; |
| wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc); |
| |
| // Create the test resource table. |
| 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::ResourceTable table = MakeResourceTable(3, { |
| {0, {.textureView = tex.CreateView()}}, |
| {1, {.textureView = tex.CreateView()}}, |
| {2, {.textureView = tex.CreateView()}}, |
| }); |
| |
| // Create the other test resources. |
| wgpu::BufferDescriptor bDesc = { |
| .usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc, |
| .size = 4 * sizeof(uint32_t), |
| }; |
| wgpu::Buffer resultBuffer = device.CreateBuffer(&bDesc); |
| wgpu::BindGroup resultBG = |
| utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), {{0, resultBuffer}}); |
| uint32_t resourceCount = table.GetSize(); |
| |
| // Run the test and check results are the expected ones. |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetImmediates(0, &resourceCount, sizeof(resourceCount)); |
| pass.SetBindGroup(0, resultBG); |
| 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); |
| } |
| |
| // Check that the default bindings are of size 1 and filled with zeroes. This is not an exhaustive |
| // test (that's for the CTS) but tries to check a few different interesting cases (MS, DS, Cube, 2D |
| // array). |
| TEST_P(ResourceTableTests, DefaultBindingsAreZeroAndSizeOne) { |
| // Create the test pipeline |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| |
| @group(0) @binding(0) var<storage, read_write> error : u32; |
| @group(0) @binding(1) var s : sampler; |
| |
| var<private> checkIndex = 0u; |
| fn check(b : bool) { |
| if (!b && error == 0) { |
| error = 1 + checkIndex; |
| } |
| checkIndex++; |
| } |
| |
| @compute @workgroup_size(1) fn checkDefault() { |
| // Default texture_2d<f32> |
| { |
| check(!hasResource<texture_2d<f32>>(0)); |
| let t = getResource<texture_2d<f32>>(0); |
| check(all(textureDimensions(t) == vec2(1))); |
| check(textureNumLevels(t) == 1); |
| check(all(textureLoad(t, vec2(0), 0) == vec4(0, 0, 0, 1))); |
| } |
| |
| // Default texture_multisampled_2d |
| { |
| check(!hasResource<texture_multisampled_2d<u32>>(0)); |
| let t = getResource<texture_multisampled_2d<u32>>(0); |
| check(all(textureDimensions(t) == vec2(1))); |
| check(textureNumSamples(t) == 4); |
| check(all(textureLoad(t, vec2(0), 0) == vec4(0, 0, 0, 1))); |
| } |
| |
| // Default texture_depth_cube |
| { |
| check(!hasResource<texture_depth_cube>(0)); |
| let t = getResource<texture_depth_cube>(0); |
| check(all(textureDimensions(t) == vec2(1))); |
| check(textureNumLevels(t) == 1); |
| check(textureSampleLevel(t, s, vec3(0), 0) == 0); |
| } |
| |
| // Default texture_2d_array<i32> |
| { |
| check(!hasResource<texture_2d_array<i32>>(0)); |
| let t = getResource<texture_2d_array<i32>>(0); |
| check(all(textureDimensions(t) == vec2(1))); |
| check(textureNumLevels(t) == 1); |
| check(textureNumLayers(t) == 1); |
| check(all(textureLoad(t, vec2(0), 0, 0) == vec4(0, 0, 0, 1))); |
| } |
| } |
| )"); |
| 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 = sizeof(uint32_t), |
| }; |
| wgpu::Buffer errorBuffer = device.CreateBuffer(&bDesc); |
| |
| wgpu::BindGroup bg = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), |
| { |
| {0, errorBuffer}, |
| {1, device.CreateSampler()}, |
| }); |
| wgpu::ResourceTable table = MakeResourceTable(0); |
| |
| // Run the test and check results are the expected ones. |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| 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(0, errorBuffer, 0); |
| } |
| |
| // Check that Pin forces zero-initialization of the resources. |
| TEST_P(ResourceTableTests, PinDoesZeroInit) { |
| // Create the pipeline reading back from the texture. |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| |
| @group(0) @binding(0) var<storage, read_write> result : u32; |
| |
| @compute @workgroup_size(1) fn readbackPixel() { |
| let errorIfNotPresent = u32(!hasResource<texture_2d<u32>>(0)); |
| let tex = getResource<texture_2d<u32>>(0); |
| let texel = textureLoad(tex, vec2u(0), 0).r; |
| result = errorIfNotPresent + texel; |
| } |
| )"); |
| |
| wgpu::ComputePipelineDescriptor csDesc = {.compute = { |
| .module = module, |
| }}; |
| wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc); |
| |
| // Create the test resource table. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::RenderAttachment, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Uint, |
| }; |
| wgpu::TextureViewDescriptor vDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| wgpu::ResourceTable table = |
| MakeResourceTable(1, {{0, {.textureView = tex.CreateView(&vDesc)}}}); |
| |
| // Create the other test resources. |
| wgpu::BufferDescriptor bDesc = { |
| .usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc, |
| .size = sizeof(uint32_t), |
| }; |
| wgpu::Buffer resultBuffer = device.CreateBuffer(&bDesc); |
| |
| wgpu::BindGroup bg = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), |
| { |
| {0, resultBuffer}, |
| }); |
| |
| // Check that Pin does the initial zero init. |
| { |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| 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(0, resultBuffer, 0); |
| } |
| |
| // Use a render pass discard to mark the texture as uninitialized again. Use a LoadOp::Clear to |
| // set some non-zero value in the texture which hopefully would tell us if the lazy clear didn't |
| // happen. |
| { |
| tex.Unpin(); |
| |
| wgpu::RenderPassColorAttachment attachment = { |
| .view = tex.CreateView(), |
| .loadOp = wgpu::LoadOp::Clear, |
| .storeOp = wgpu::StoreOp::Discard, |
| .clearValue = {.r = 1.0, .g = 0.0, .b = 0.0, .a = 0.0}, |
| }; |
| wgpu::RenderPassDescriptor rpDesc = { |
| .colorAttachmentCount = 1, |
| .colorAttachments = &attachment, |
| }; |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rpDesc); |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| device.GetQueue().Submit(1, &commands); |
| } |
| |
| // Check that Pin does the zero init after a discard. |
| { |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| 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); |
| tex.Unpin(); |
| |
| EXPECT_BUFFER_U32_EQ(0, resultBuffer, 0); |
| } |
| } |
| |
| // Check that a resource table slot can be updated only after all commands submitted prior to |
| // RemoveBinding are completed. |
| TEST_P(ResourceTableTests, UpdateAfterRemoveRequiresGPUIsFinished) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Uint, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| wgpu::BindingResource resource{.textureView = tex.CreateView()}; |
| |
| wgpu::ResourceTable table = MakeResourceTable(1); |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| |
| // Removing while the table is still potentially in used by the GPU is an error. But immediately |
| // after we know that the GPU is finished, it is valid. |
| bool updateValid = false; |
| DoSomeWorkInSubmit(); |
| queue.OnSubmittedWorkDone( |
| wgpu::CallbackMode::AllowSpontaneous, |
| [&](wgpu::QueueWorkDoneStatus, wgpu::StringView) { updateValid = true; }); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| |
| if (updateValid) { |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| updateValid = false; |
| } else { |
| EXPECT_EQ(wgpu::Status::Error, table.Update(0, &resource)); |
| } |
| |
| WaitForAllOperations(); |
| |
| if (updateValid) { |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| } else { |
| EXPECT_EQ(wgpu::Status::Error, table.Update(0, &resource)); |
| } |
| } |
| |
| // Check that a resource table slot can be updated only after all commands submitted prior to |
| // RemoveBinding are completed. |
| TEST_P(ResourceTableTests, UpdateAfterRemoveRequiresGPUIsFinished_ErrorBindGroup) { |
| DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation")); |
| |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Uint, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| wgpu::BindingResource resource{.textureView = tex.CreateView()}; |
| |
| // Make an error resource table. |
| wgpu::RenderPassMaxDrawCount maxDraw; |
| maxDraw.maxDrawCount = 1000; |
| wgpu::ResourceTableDescriptor desc{ |
| .nextInChain = &maxDraw, |
| .size = 1, |
| }; |
| wgpu::ResourceTable table; |
| ASSERT_DEVICE_ERROR(table = device.CreateResourceTable(&desc)); |
| |
| { |
| // Ignore all validation errors for this test as they are tested in other places, and we're |
| // checking immediate validation returned as a wgpu::Status and supposed to be the same for |
| // valid and invalid objects. |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| |
| // Removing while the table is still potentially in used by the GPU is an error. But |
| // immediately after we know that the GPU is finished, it is valid. |
| bool updateValid = false; |
| DoSomeWorkInSubmit(); |
| queue.OnSubmittedWorkDone( |
| wgpu::CallbackMode::AllowSpontaneous, |
| [&](wgpu::QueueWorkDoneStatus, wgpu::StringView) { updateValid = true; }); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| |
| if (updateValid) { |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| updateValid = false; |
| } else { |
| EXPECT_EQ(wgpu::Status::Error, table.Update(0, &resource)); |
| } |
| |
| WaitForAllOperations(); |
| |
| if (updateValid) { |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| } else { |
| EXPECT_EQ(wgpu::Status::Error, table.Update(0, &resource)); |
| } |
| } |
| } |
| |
| // Check that Update and InsertBinding make the new binding visible in the resource table. |
| TEST_P(ResourceTableTests, UpdateAndInsertBindingMakeBindingVisible) { |
| wgpu::ResourceTable table = MakeResourceTable(2); |
| |
| // Before we do anything, the table has no valid entries. |
| TestHasU8Bindings(table, {{}, {}}); |
| |
| // Update makes the entry visible. |
| wgpu::BindingResource resource0 = {.textureView = MakePinnedU8View(17)}; |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource0)); |
| TestHasU8Bindings(table, {{17}, {}}); |
| |
| // InsertBinding makes the entry visible. |
| wgpu::BindingResource resource1 = {.textureView = MakePinnedU8View(42)}; |
| EXPECT_EQ(1u, table.InsertBinding(&resource1)); |
| TestHasU8Bindings(table, {{17}, {42}}); |
| } |
| |
| // Check that RemoveBinding instantly makes the binding not visible, both for entries added with |
| // Update and InsertBinding. |
| TEST_P(ResourceTableTests, RemoveBindingMakeBindingInvalid) { |
| // Fill a resource table with both Update and InsertBinding. |
| wgpu::ResourceTable table = MakeResourceTable(2); |
| |
| wgpu::BindingResource resource0 = {.textureView = MakePinnedU8View(100)}; |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource0)); |
| |
| wgpu::BindingResource resource1 = {.textureView = MakePinnedU8View(101)}; |
| EXPECT_EQ(1u, table.InsertBinding(&resource1)); |
| |
| // Before we remove bindings, they are all valid. |
| TestHasU8Bindings(table, {{100}, {101}}); |
| |
| // RemoveBinding immediately makes bindings invalid. |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(1)); |
| TestHasU8Bindings(table, {{100}, {}}); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| TestHasU8Bindings(table, {{}, {}}); |
| } |
| |
| // Check that removing a binding and adding a different one works. |
| TEST_P(ResourceTableTests, ReplaceBinding) { |
| // Create the test resource table. |
| wgpu::ResourceTable table = MakeResourceTable(1); |
| wgpu::BindingResource resource = {.textureView = MakePinnedU8View(19)}; |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| |
| // Test removing a binding that was previously there. |
| TestHasU8Bindings(table, {{19}}); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| TestHasU8Bindings(table, {{}}); |
| |
| /// Add it back a new entry, the shader should be seeing the updated entry. |
| WaitForAllOperations(); |
| |
| wgpu::BindingResource newResource = {.textureView = MakePinnedU8View(23)}; |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &newResource)); |
| TestHasU8Bindings(table, {{23}}); |
| } |
| |
| // Check that removing a binding and adding it back works. |
| TEST_P(ResourceTableTests, ReplaceWithSameBinding) { |
| // Create the test resource table. |
| wgpu::ResourceTable table = MakeResourceTable(1); |
| wgpu::BindingResource resource = {.textureView = MakePinnedU8View(19)}; |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| |
| // Test removing a binding that was previously there. |
| TestHasU8Bindings(table, {{19}}); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| TestHasU8Bindings(table, {{}}); |
| |
| /// Add it back a new entry, the shader should be seeing the updated entry. |
| WaitForAllOperations(); |
| |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| TestHasU8Bindings(table, {{19}}); |
| } |
| |
| // Check that logic to dirty or reuse VkDescriptorSet takes into account the resource table in the |
| // Vulkan backend. |
| TEST_P(ResourceTableTests, SwitchUseResourceTableAndNot) { |
| // Swiftshader doesn't support variable count descriptor sets used in draw operations. In |
| // vk::DescriptorSet::ParseDescriptors it iterates over all the descriptors to prep various |
| // things but iterates over the whole size defined in the vkDescriptorSetLayout instead of |
| // taking into account the variable count. |
| DAWN_SUPPRESS_TEST_IF(IsSwiftshader()); |
| |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(0, 0, 0.5, 0.5); |
| } |
| |
| @group(0) @binding(0) var<storage, read_write> results : array<u32>; |
| var<immediate> resultIndex : u32; |
| |
| @fragment fn yes_resource_table() -> @location(0) vec4f { |
| results[resultIndex] = 10 + u32(hasResource<texture_2d<f32>>(resultIndex)); |
| return vec4(); |
| } |
| |
| @fragment fn no_resource_table() -> @location(0) vec4f { |
| results[resultIndex] = 42; |
| return vec4(); |
| } |
| )"); |
| |
| wgpu::BindGroupLayout resultBGL = utils::MakeBindGroupLayout( |
| device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}}); |
| |
| wgpu::RenderPipeline resourceTablePipeline; |
| { |
| utils::ComboRenderPipelineDescriptor desc; |
| desc.layout = MakePipelineLayoutWithTable({resultBGL}, 4); |
| desc.vertex.module = module; |
| desc.cFragment.module = module; |
| desc.cFragment.entryPoint = "yes_resource_table"; |
| desc.primitive.topology = wgpu::PrimitiveTopology::PointList; |
| resourceTablePipeline = device.CreateRenderPipeline(&desc); |
| } |
| |
| wgpu::RenderPipeline noResourceTablePipeline; |
| { |
| utils::ComboRenderPipelineDescriptor desc; |
| desc.layout = utils::MakeBasicPipelineLayout(device, &resultBGL, 4); |
| desc.vertex.module = module; |
| desc.cFragment.module = module; |
| desc.cFragment.entryPoint = "no_resource_table"; |
| desc.primitive.topology = wgpu::PrimitiveTopology::PointList; |
| noResourceTablePipeline = device.CreateRenderPipeline(&desc); |
| } |
| |
| // Create the result buffer resource. |
| wgpu::BufferDescriptor bDesc = { |
| .usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc, |
| .size = sizeof(uint32_t) * 3, |
| }; |
| wgpu::Buffer resultBuffer = device.CreateBuffer(&bDesc); |
| wgpu::BindGroup resultBG = utils::MakeBindGroup(device, resultBGL, {{0, resultBuffer}}); |
| |
| // Create and populate the resource table. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Uint, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| wgpu::ResourceTable table = MakeResourceTable(0); |
| |
| // Encode render commands that switch between the two pipelines. The resultBGL index in the |
| // Vulkan backend will be pushed by 1 if the pipeline uses the resource table, so we check that |
| // the invalidation of VkDescriptorSet inheritance works correctly. |
| uint32_t resultIndex = 0; |
| auto rp = utils::CreateBasicRenderPass(device, 1, 1); |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetBindGroup(0, resultBG); |
| |
| // Start by not using the resource table. |
| pass.SetPipeline(noResourceTablePipeline); |
| pass.SetImmediates(0, &resultIndex, sizeof(resultIndex)); |
| pass.Draw(1); |
| resultIndex++; |
| |
| // Switch to using the resource table. |
| pass.SetPipeline(resourceTablePipeline); |
| pass.SetImmediates(0, &resultIndex, sizeof(resultIndex)); |
| pass.Draw(1); |
| resultIndex++; |
| |
| // And back to not using it. |
| pass.SetPipeline(noResourceTablePipeline); |
| pass.SetImmediates(0, &resultIndex, sizeof(resultIndex)); |
| pass.Draw(1); |
| resultIndex++; |
| |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| EXPECT_BUFFER_U32_EQ(42, resultBuffer, 0); |
| EXPECT_BUFFER_U32_EQ(10, resultBuffer, 4); |
| EXPECT_BUFFER_U32_EQ(42, resultBuffer, 8); |
| } |
| |
| DAWN_INSTANTIATE_TEST(ResourceTableTests, D3D12Backend(), MetalBackend(), VulkanBackend()); |
| |
| } // anonymous namespace |
| } // namespace dawn |