| // 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 <utility> |
| #include <vector> |
| |
| #include "dawn/tests/unittests/validation/ValidationTest.h" |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" |
| #include "dawn/utils/ScopedIgnoreValidationErrors.h" |
| #include "dawn/utils/WGPUHelpers.h" |
| |
| namespace dawn { |
| namespace { |
| |
| class ResourceTableValidationTest : public ValidationTest { |
| protected: |
| std::vector<wgpu::FeatureName> GetRequiredFeatures() override { |
| return {wgpu::FeatureName::ChromiumExperimentalSamplingResourceTable}; |
| } |
| |
| wgpu::ResourceTable MakeResourceTable(uint32_t size) { |
| wgpu::ResourceTableDescriptor desc; |
| desc.size = size; |
| return device.CreateResourceTable(&desc); |
| } |
| |
| wgpu::ResourceTable MakeErrorResourceTable(uint32_t size) { |
| wgpu::RenderPassMaxDrawCount maxDraw; |
| maxDraw.maxDrawCount = 1000; |
| wgpu::ResourceTableDescriptor desc{ |
| .nextInChain = &maxDraw, |
| .size = size, |
| }; |
| |
| wgpu::ResourceTable table; |
| ASSERT_DEVICE_ERROR(table = device.CreateResourceTable(&desc)); |
| return table; |
| } |
| |
| enum class Mutator : uint8_t { |
| Update, |
| InsertBinding, |
| }; |
| void TestMutator(Mutator mutator, const wgpu::BindingResource* resource, bool success) { |
| wgpu::ResourceTable table = MakeResourceTable(1); |
| |
| switch (mutator) { |
| case Mutator::Update: { |
| wgpu::Status status; |
| if (success) { |
| status = table.Update(0, resource); |
| } else { |
| ASSERT_DEVICE_ERROR(status = table.Update(0, resource)); |
| } |
| EXPECT_EQ(status, wgpu::Status::Success); |
| break; |
| } |
| |
| case Mutator::InsertBinding: { |
| uint32_t slot = wgpu::kInvalidBinding; |
| if (success) { |
| slot = table.InsertBinding(resource); |
| } else { |
| ASSERT_DEVICE_ERROR(slot = table.InsertBinding(resource)); |
| } |
| EXPECT_EQ(slot, 0u); |
| break; |
| } |
| } |
| } |
| |
| // Helper to make sure that the resource table is marked as used. Even if internally Dawn |
| // doesn't track this, it makes tests more clearly correct. |
| void UseResourceTableInSubmit(wgpu::ResourceTable table) { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(table); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| device.GetQueue().Submit(1, &commands); |
| } |
| }; |
| |
| class ResourceTableValidationTestDisabled : public ValidationTest { |
| std::vector<wgpu::FeatureName> GetRequiredFeatures() override { return {}; } |
| }; |
| |
| // Test that validates that the feature must be enabled |
| TEST_F(ResourceTableValidationTestDisabled, FeatureNotEnabled) { |
| wgpu::ResourceTableDescriptor descriptor; |
| ASSERT_DEVICE_ERROR(device.CreateResourceTable(&descriptor)); |
| } |
| |
| // Test that setting invalid size is an error |
| TEST_F(ResourceTableValidationTest, InvalidSize) { |
| wgpu::ResourceTableDescriptor descriptor; |
| |
| // Size 0 is valid |
| descriptor.size = 0u; |
| device.CreateResourceTable(&descriptor); |
| |
| // Size of 1 is valid |
| descriptor.size = 1u; |
| device.CreateResourceTable(&descriptor); |
| |
| // Size of maxResourceTableSize is valid |
| descriptor.size = kMaxResourceTableSize; |
| device.CreateResourceTable(&descriptor); |
| |
| // Size > limits is invalid |
| descriptor.size = kMaxResourceTableSize + 1u; |
| ASSERT_DEVICE_ERROR(device.CreateResourceTable(&descriptor)); |
| } |
| |
| // Test that setting nextInChain to anything is an error |
| TEST_F(ResourceTableValidationTest, NextInChain) { |
| // Control case, nextInChain = nullptr is valid. |
| { |
| wgpu::ResourceTableDescriptor descriptor{ |
| .nextInChain = nullptr, |
| .size = 3, |
| }; |
| device.CreateResourceTable(&descriptor); |
| } |
| |
| // Control case, nextInChain = non null is invalid. |
| { |
| wgpu::RenderPassMaxDrawCount maxDraw; |
| maxDraw.maxDrawCount = 1000; |
| wgpu::ResourceTableDescriptor descriptor{ |
| .nextInChain = &maxDraw, |
| .size = 3, |
| }; |
| ASSERT_DEVICE_ERROR(device.CreateResourceTable(&descriptor)); |
| } |
| } |
| |
| // Test the Destroy call on a ResourceTable |
| TEST_F(ResourceTableValidationTest, Destroy) { |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1u; |
| wgpu::ResourceTable resourceTable = device.CreateResourceTable(&descriptor); |
| |
| // Calling destroy is valid |
| resourceTable.Destroy(); |
| |
| // Calling it multiple times is valid |
| resourceTable.Destroy(); |
| } |
| |
| // Control case where enabling use of a resource table with the feature enabled is valid. |
| TEST_F(ResourceTableValidationTest, PipelineLayoutCreation_SuccessWithFeatureEnabled) { |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = 0; |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| resourceTable.usesResourceTable = true; |
| pipelineLayoutDescriptor.nextInChain = &resourceTable; |
| device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| } |
| |
| // Error case where enabling use of a resource table with the feature disabled is an error. |
| TEST_F(ResourceTableValidationTestDisabled, PipelineLayoutCreation_FailureWithFeatureDisabled) { |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = 0; |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| pipelineLayoutDescriptor.nextInChain = &resourceTable; |
| |
| // Failure case |
| resourceTable.usesResourceTable = true; |
| ASSERT_DEVICE_ERROR(device.CreatePipelineLayout(&pipelineLayoutDescriptor)); |
| |
| // Success case |
| resourceTable.usesResourceTable = false; |
| device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| } |
| |
| // Error case where compiling a shader using the resource table with the extension disabled is an |
| // error. |
| TEST_F(ResourceTableValidationTestDisabled, WGSLEnableNotAllowed) { |
| ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| } |
| )")); |
| } |
| |
| // Test that a shader using a resource table requires a layout with one. |
| TEST_F(ResourceTableValidationTest, PipelineCreation_ShaderRequiresLayoutWithResourceTable) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| } |
| )"); |
| |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = 0; |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| pipelineLayoutDescriptor.nextInChain = &resourceTable; |
| |
| // Success case, the layout uses a resource table |
| resourceTable.usesResourceTable = true; |
| csDesc.layout = device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| device.CreateComputePipeline(&csDesc); |
| |
| // Failure case, the layout does not use a resource table |
| resourceTable.usesResourceTable = false; |
| csDesc.layout = device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| ASSERT_DEVICE_ERROR(device.CreateComputePipeline(&csDesc)); |
| } |
| |
| // Test that it is valid to have a layout specifying a resource table with a shader that |
| // doesn't have one. |
| TEST_F(ResourceTableValidationTest, PipelineCreation_ShaderNoResourceTableWithLayoutThatHasOne) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| @compute @workgroup_size(1) fn main() { |
| } |
| )"); |
| |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = 0; |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| pipelineLayoutDescriptor.nextInChain = &resourceTable; |
| |
| resourceTable.usesResourceTable = true; |
| csDesc.layout = device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| device.CreateComputePipeline(&csDesc); |
| } |
| |
| // Test that an defaulted pipeline layout with a shader that uses a resource table has a |
| // PipelineLayoutResourceTable with usesResourceTable == true. |
| TEST_F(ResourceTableValidationTest, PipelineCreation_DefaultedLayoutWithResourceTable) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| } |
| )"); |
| |
| csDesc.layout = nullptr; // Auto |
| device.CreateComputePipeline(&csDesc); |
| } |
| |
| // Test that an defaulted pipeline layout with a multi-stage shader where only one stage uses a |
| // resource table has a PipelineLayoutResourceTable with usesResourceTable == true. |
| TEST_F(ResourceTableValidationTest, PipelineCreation_OneShaderDefaultedLayoutWithResourceTable) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(0, 0, 0.5, 0.5); |
| } |
| @compute @workgroup_size(1) fn compute_main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| } |
| @fragment fn fs() -> @location(0) vec4f { |
| return vec4f(1.0, 0.0, 0.0, 1.0); |
| } |
| )"); |
| |
| csDesc.layout = nullptr; // Auto |
| device.CreateComputePipeline(&csDesc); |
| } |
| |
| // Test that a resource table uses up a BindGroupLayout slot |
| TEST_F(ResourceTableValidationTest, PipelineLayoutCreation_ResourceTableUsesBindGroupLayoutSlot) { |
| // Control case: max bgls, no resource table |
| { |
| std::vector bgLayout(kMaxBindGroups, utils::MakeBindGroupLayout(device, {})); |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = bgLayout.size(); |
| pipelineLayoutDescriptor.bindGroupLayouts = bgLayout.data(); |
| device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| } |
| |
| // Failure case: not enough room for bgls and a resource table |
| { |
| std::vector bgLayout(kMaxBindGroups, utils::MakeBindGroupLayout(device, {})); |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = bgLayout.size(); |
| pipelineLayoutDescriptor.bindGroupLayouts = bgLayout.data(); |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| resourceTable.usesResourceTable = true; |
| pipelineLayoutDescriptor.nextInChain = &resourceTable; |
| ASSERT_DEVICE_ERROR(device.CreatePipelineLayout(&pipelineLayoutDescriptor)); |
| } |
| |
| // Success case: enough room for bgls and a resource table |
| { |
| std::vector bgLayout(kMaxBindGroups - 1, utils::MakeBindGroupLayout(device, {})); |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = bgLayout.size(); |
| pipelineLayoutDescriptor.bindGroupLayouts = bgLayout.data(); |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| resourceTable.usesResourceTable = true; |
| pipelineLayoutDescriptor.nextInChain = &resourceTable; |
| device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| } |
| } |
| |
| // Test that a resource table uses up a storage buffer binding |
| TEST_F(ResourceTableValidationTest, PipelineLayoutCreation_ResourceTableUsesOneStorageBuffer) { |
| const uint32_t maxStorageBuffers = deviceLimits.maxStorageBuffersPerShaderStage; |
| std::vector<wgpu::BindGroupLayoutEntry> storageBufferEntries(maxStorageBuffers); |
| for (size_t i = 0; i < storageBufferEntries.size(); i++) { |
| storageBufferEntries[i].buffer.type = wgpu::BufferBindingType::ReadOnlyStorage; |
| storageBufferEntries[i].visibility = |
| wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment | wgpu::ShaderStage::Compute; |
| storageBufferEntries[i].binding = i; |
| } |
| |
| // Success case: exactly maxStorageBuffers are used (1 for the resource table, max - 1 for BGL |
| // entries). |
| { |
| wgpu::BindGroupLayoutDescriptor bglDesc = { |
| .entryCount = maxStorageBuffers - 1, |
| .entries = storageBufferEntries.data(), |
| }; |
| wgpu::BindGroupLayout bgl = device.CreateBindGroupLayout(&bglDesc); |
| |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| resourceTable.usesResourceTable = true; |
| wgpu::PipelineLayoutDescriptor plDesc = { |
| .nextInChain = &resourceTable, |
| .bindGroupLayoutCount = 1, |
| .bindGroupLayouts = &bgl, |
| }; |
| device.CreatePipelineLayout(&plDesc); |
| } |
| |
| // Error case: the resource table additional storage buffer make the layout go over the limit. |
| { |
| wgpu::BindGroupLayoutDescriptor bglDesc = { |
| .entryCount = maxStorageBuffers, |
| .entries = storageBufferEntries.data(), |
| }; |
| wgpu::BindGroupLayout bgl = device.CreateBindGroupLayout(&bglDesc); |
| |
| wgpu::PipelineLayoutResourceTable resourceTable; |
| resourceTable.usesResourceTable = true; |
| wgpu::PipelineLayoutDescriptor plDesc = { |
| .nextInChain = &resourceTable, |
| .bindGroupLayoutCount = 1, |
| .bindGroupLayouts = &bgl, |
| }; |
| ASSERT_DEVICE_ERROR(device.CreatePipelineLayout(&plDesc)); |
| } |
| } |
| |
| // Test that an defaulted pipeline layout with a resource table uses up a BindGroupLayout slot |
| TEST_F(ResourceTableValidationTest, |
| PipelineCreation_DefaultedLayoutWithResourceTableUsesBindGroupLayoutSlot) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.layout = nullptr; // Auto |
| |
| // Control case: max bgls, no resource table |
| { |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @group(0) @binding(0) var<uniform> a : u32; |
| @group(1) @binding(0) var<uniform> b : u32; |
| @group(2) @binding(0) var<uniform> c : u32; |
| @group(3) @binding(0) var<uniform> d : u32; |
| @compute @workgroup_size(1) fn main() { |
| // _ = hasResource<texture_2d<f32>>(0); |
| _ = a; |
| _ = b; |
| _ = c; |
| _ = d; |
| } |
| )"); |
| device.CreateComputePipeline(&csDesc); |
| } |
| |
| // Failure case: not enough room for bgls and a resource table |
| { |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @group(0) @binding(0) var<uniform> a : u32; |
| @group(1) @binding(0) var<uniform> b : u32; |
| @group(2) @binding(0) var<uniform> c : u32; |
| @group(3) @binding(0) var<uniform> d : u32; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| _ = a; |
| _ = b; |
| _ = c; |
| _ = d; |
| } |
| )"); |
| ASSERT_DEVICE_ERROR(device.CreateComputePipeline(&csDesc)); |
| } |
| |
| // Success case: enough room for bgls and a resource table |
| { |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @group(0) @binding(0) var<uniform> a : u32; |
| @group(1) @binding(0) var<uniform> b : u32; |
| @group(2) @binding(0) var<uniform> c : u32; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| _ = a; |
| _ = b; |
| _ = c; |
| } |
| )"); |
| device.CreateComputePipeline(&csDesc); |
| } |
| } |
| |
| // Test that GetBindGroupLayout is valid for one less BGL if resource tables are used. |
| TEST_F(ResourceTableValidationTest, GetBindGroupLayoutValidForOneLessIndex) { |
| // Default behavior case: GetBGL is valid until kMaxBindGroups - 1 when no resource table is |
| // used. |
| { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.layout = nullptr; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @compute @workgroup_size(1) fn main() { |
| } |
| )"); |
| wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc); |
| |
| pipeline.GetBindGroupLayout(kMaxBindGroups - 1); |
| ASSERT_DEVICE_ERROR(pipeline.GetBindGroupLayout(kMaxBindGroups)); |
| } |
| |
| // Resource table case: GetBGL is valid until kMaxBindGroups - 2. |
| { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.layout = nullptr; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| } |
| )"); |
| wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc); |
| |
| pipeline.GetBindGroupLayout(kMaxBindGroups - 2); |
| ASSERT_DEVICE_ERROR(pipeline.GetBindGroupLayout(kMaxBindGroups - 1)); |
| } |
| } |
| |
| // Tests calling CommandEncoder::SetResourceTable |
| TEST_F(ResourceTableValidationTest, CommandEncoder_SetResourceTable) { |
| // Failure case: invalid encoder state |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.Finish(); |
| ASSERT_DEVICE_ERROR(encoder.SetResourceTable(nullptr)); |
| } |
| |
| // Failure case: invalid resource table |
| { |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = kMaxResourceTableSize + 1u; // Invalid size |
| wgpu::ResourceTable resourceTable; |
| ASSERT_DEVICE_ERROR(resourceTable = device.CreateResourceTable(&descriptor)); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); |
| ASSERT_DEVICE_ERROR(encoder.Finish()); |
| } |
| |
| // Success case: valid resource table |
| { |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1; |
| wgpu::ResourceTable resourceTable = device.CreateResourceTable(&descriptor); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); |
| encoder.Finish(); |
| } |
| |
| // Success case: null resource table |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(nullptr); |
| encoder.Finish(); |
| } |
| } |
| |
| // Tests calling CommandEncoder::SetResourceTable when the feature is disabled |
| TEST_F(ResourceTableValidationTestDisabled, CommandEncoder_SetResourceTable) { |
| // Failure case: feature is disabled |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(nullptr); |
| ASSERT_DEVICE_ERROR(encoder.Finish()); |
| } |
| |
| // Tests that the resource table can be used in submit |
| TEST_F(ResourceTableValidationTest, Submit_CanUseInSubmit) { |
| // Success case: resource table can be used in submit |
| { |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1u; |
| wgpu::ResourceTable resourceTable = device.CreateResourceTable(&descriptor); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| device.GetQueue().Submit(1, &commands); |
| } |
| |
| // Failure case: resource table has been destroyed |
| { |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1u; |
| wgpu::ResourceTable resourceTable = device.CreateResourceTable(&descriptor); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| resourceTable.Destroy(); // Destroy it |
| ASSERT_DEVICE_ERROR(device.GetQueue().Submit(1, &commands)); |
| } |
| |
| // Failure case: one of multiple resource tables has been destroyed |
| { |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1u; |
| wgpu::ResourceTable resourceTable1 = device.CreateResourceTable(&descriptor); |
| wgpu::ResourceTable resourceTable2 = device.CreateResourceTable(&descriptor); |
| wgpu::ResourceTable resourceTable3 = device.CreateResourceTable(&descriptor); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable1); |
| encoder.SetResourceTable(resourceTable2); |
| encoder.SetResourceTable(resourceTable3); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| resourceTable2.Destroy(); // Destroy one |
| ASSERT_DEVICE_ERROR(device.GetQueue().Submit(1, &commands)); |
| } |
| |
| // Failure case: resource table must still be valid if set, then nullptr is set |
| { |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1u; |
| wgpu::ResourceTable resourceTable = device.CreateResourceTable(&descriptor); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); |
| encoder.SetResourceTable(nullptr); // Clear it |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| resourceTable.Destroy(); // Destroy it |
| ASSERT_DEVICE_ERROR(device.GetQueue().Submit(1, &commands)); |
| } |
| } |
| |
| // Tests that the resource table can be used in dispatch |
| TEST_F(ResourceTableValidationTest, Submit_DispatchRequiresResourceTable) { |
| for (bool defaulted : {true, false}) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @compute @workgroup_size(1) fn main() { |
| _ = hasResource<texture_2d<f32>>(0); |
| } |
| )"); |
| |
| wgpu::ComputePipeline pipeline; |
| if (defaulted) { |
| csDesc.layout = nullptr; |
| pipeline = device.CreateComputePipeline(&csDesc); |
| } else { |
| wgpu::PipelineLayoutResourceTable plResourceTable; |
| plResourceTable.usesResourceTable = true; |
| |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = 0; |
| pipelineLayoutDescriptor.nextInChain = &plResourceTable; |
| |
| csDesc.layout = device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| pipeline = device.CreateComputePipeline(&csDesc); |
| } |
| |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1u; |
| wgpu::ResourceTable resourceTable = device.CreateResourceTable(&descriptor); |
| wgpu::ResourceTable resourceTable2 = device.CreateResourceTable(&descriptor); |
| |
| // Success case: `usesResourceTable` is enabled, and one has been set on the encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetPipeline(pipeline); |
| pass.DispatchWorkgroups(1); |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| device.GetQueue().Submit(1, &commands); |
| } |
| |
| // Failure case: `usesResourceTable` is enabled, but none has been set on the encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetPipeline(pipeline); |
| pass.DispatchWorkgroups(1); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(wgpu::CommandBuffer commands = encoder.Finish()); |
| } |
| |
| // Failure case: `usesResourceTable` is enabled, one then nullptr set on the encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); // Set a valid one |
| encoder.SetResourceTable(nullptr); // Then clear it |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetPipeline(pipeline); |
| pass.DispatchWorkgroups(1); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(wgpu::CommandBuffer commands = encoder.Finish()); |
| } |
| |
| // Success case: `usesResourceTable` is enabled, one then nullptr then another set on the |
| // encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); // Set a valid one |
| encoder.SetResourceTable(nullptr); // Then clear it |
| encoder.SetResourceTable(resourceTable2); // Then set another valid one |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetPipeline(pipeline); |
| pass.DispatchWorkgroups(1); |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| device.GetQueue().Submit(1, &commands); |
| } |
| } |
| } |
| |
| // Tests that the resource table can be used in draw |
| TEST_F(ResourceTableValidationTest, Submit_DrawRequiresResourceTable) { |
| for (bool defaulted : {true, false}) { |
| utils::ComboRenderPipelineDescriptor pDesc; |
| pDesc.vertex.module = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(); |
| } |
| )"); |
| pDesc.cFragment.module = utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_resource_table; |
| @fragment fn fs() -> @location(0) vec4f { |
| _ = hasResource<texture_2d<f32>>(0); |
| return vec4f(); |
| } |
| )"); |
| |
| wgpu::RenderPipeline pipeline; |
| if (defaulted) { |
| pDesc.layout = nullptr; |
| pipeline = device.CreateRenderPipeline(&pDesc); |
| } else { |
| wgpu::PipelineLayoutResourceTable plResourceTable; |
| plResourceTable.usesResourceTable = true; |
| |
| wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor; |
| pipelineLayoutDescriptor.bindGroupLayoutCount = 0; |
| pipelineLayoutDescriptor.nextInChain = &plResourceTable; |
| |
| pDesc.layout = device.CreatePipelineLayout(&pipelineLayoutDescriptor); |
| pipeline = device.CreateRenderPipeline(&pDesc); |
| } |
| |
| wgpu::ResourceTableDescriptor descriptor; |
| descriptor.size = 1u; |
| wgpu::ResourceTable resourceTable = device.CreateResourceTable(&descriptor); |
| wgpu::ResourceTable resourceTable2 = device.CreateResourceTable(&descriptor); |
| auto rp = utils::CreateBasicRenderPass(device, 1, 1, wgpu::TextureFormat::RGBA8Unorm); |
| |
| // Success case: `usesResourceTable` is enabled, and one has been set on the encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.Draw(1); |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| device.GetQueue().Submit(1, &commands); |
| } |
| |
| // Failure case: `usesResourceTable` is enabled, but none has been set on the encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.Draw(1); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(wgpu::CommandBuffer commands = encoder.Finish()); |
| } |
| |
| // Failure case: `usesResourceTable` is enabled, one then nullptr set on the encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); // Set a valid one |
| encoder.SetResourceTable(nullptr); // Then clear it |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.Draw(1); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(wgpu::CommandBuffer commands = encoder.Finish()); |
| } |
| |
| // Success case: `usesResourceTable` is enabled, one then nullptr then another set on the |
| // encoder |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.SetResourceTable(resourceTable); // Set a valid one |
| encoder.SetResourceTable(nullptr); // Then clear it |
| encoder.SetResourceTable(resourceTable2); // Then set another valid one |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.Draw(1); |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| device.GetQueue().Submit(1, &commands); |
| } |
| } |
| } |
| |
| // Test that pinning / unpinning is valid for a simple case. This is a control for the test that |
| // errors are produced when the feature is not enabled. |
| TEST_F(ResourceTableValidationTest, PinUnpinTextureSuccess) { |
| wgpu::TextureDescriptor desc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&desc); |
| |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| tex.Unpin(); |
| } |
| |
| // Test that calling pin/unpin is an error when the feature is not enabled. |
| TEST_F(ResourceTableValidationTestDisabled, PinUnpinTextureSuccess) { |
| wgpu::TextureDescriptor desc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&desc); |
| |
| ASSERT_DEVICE_ERROR(tex.Pin(wgpu::TextureUsage::TextureBinding)); |
| ASSERT_DEVICE_ERROR(tex.Unpin()); |
| } |
| |
| // Test the validation of the usage parameter of Pin. |
| TEST_F(ResourceTableValidationTest, PinUnpinTextureUsageConstraint) { |
| wgpu::TextureDescriptor desc{ |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| |
| desc.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | |
| wgpu::TextureUsage::StorageBinding; |
| wgpu::Texture testTexture = device.CreateTexture(&desc); |
| |
| desc.usage = wgpu::TextureUsage::RenderAttachment; |
| wgpu::Texture renderOnlyTexture = device.CreateTexture(&desc); |
| |
| // Control case, pinning the sampled texture to TextureBinding is valid. |
| testTexture.Pin(wgpu::TextureUsage::TextureBinding); |
| |
| // Error case, pinning to a usage not in the texture is invalid. |
| ASSERT_DEVICE_ERROR(renderOnlyTexture.Pin(wgpu::TextureUsage::TextureBinding)); |
| |
| // Error case, pinning to an invalid usage is invalid. |
| ASSERT_DEVICE_ERROR(testTexture.Pin(static_cast<wgpu::TextureUsage>(0x8000'0000))); |
| |
| // Error case, pinning must be to a shader usage. |
| ASSERT_DEVICE_ERROR(testTexture.Pin(wgpu::TextureUsage::CopySrc)); |
| |
| // Error case, pinning must be to a shader usage. |
| // TODO(https://issues.chromium.org/473459218): Lift this constraint and allow other shader |
| // usages. |
| ASSERT_DEVICE_ERROR(testTexture.Pin(wgpu::TextureUsage::StorageBinding)); |
| } |
| |
| // Test that pinning / unpinning don't need to be balanced. |
| TEST_F(ResourceTableValidationTest, PinUnpinUnbalancedIsValid) { |
| wgpu::TextureDescriptor desc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&desc); |
| |
| // Pinning right after creation is valid. |
| tex.Unpin(); |
| |
| // Pinning twice is valid. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| // TODO(https://issues.chromium.org/473459218): Use a different usage here when another is |
| // valid. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| |
| // Unpinning twice (plus one more to make sure we are unbalanced) is valid. |
| tex.Unpin(); |
| tex.Unpin(); |
| tex.Unpin(); |
| } |
| |
| // Test that pinning is not allowed on a destroyed texture. |
| TEST_F(ResourceTableValidationTest, PinDestroyedTextureInvalid) { |
| wgpu::TextureDescriptor desc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&desc); |
| |
| // Success case, pinning before Destroy() is valid. |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| tex.Unpin(); |
| |
| // Error case, pinning a destroyed texture is not allowed. |
| tex.Destroy(); |
| ASSERT_DEVICE_ERROR(tex.Pin(wgpu::TextureUsage::TextureBinding)); |
| } |
| |
| enum class TestPinState { Default, Pinned, Unpinned }; |
| std::array<TestPinState, 3> kAllTestPinStates = {TestPinState::Default, TestPinState::Pinned, |
| TestPinState::Unpinned}; |
| wgpu::Texture CreateTextureWithPinState(const wgpu::Device& device, |
| TestPinState pin, |
| wgpu::TextureUsage usage) { |
| wgpu::TextureDescriptor desc{ |
| .usage = usage, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&desc); |
| |
| switch (pin) { |
| case TestPinState::Default: |
| break; |
| case TestPinState::Pinned: |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| break; |
| case TestPinState::Unpinned: |
| tex.Pin(wgpu::TextureUsage::TextureBinding); |
| tex.Unpin(); |
| break; |
| } |
| |
| return tex; |
| } |
| |
| // Test that pinning prevents usage in WriteTexture |
| TEST_F(ResourceTableValidationTest, PinValidationUsageWriteTexture) { |
| for (auto pin : kAllTestPinStates) { |
| wgpu::Texture tex = CreateTextureWithPinState( |
| device, pin, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst); |
| |
| wgpu::TexelCopyTextureInfo dst = { |
| .texture = tex, |
| }; |
| wgpu::TexelCopyBufferLayout dataLayout = {}; |
| wgpu::Extent3D copySize = {0, 0, 0}; |
| |
| if (pin == TestPinState::Pinned) { |
| ASSERT_DEVICE_ERROR( |
| device.GetQueue().WriteTexture(&dst, nullptr, 0, &dataLayout, ©Size)); |
| } else { |
| device.GetQueue().WriteTexture(&dst, nullptr, 0, &dataLayout, ©Size); |
| } |
| } |
| } |
| |
| // Test that pinning prevents usage in an encoder copy command |
| TEST_F(ResourceTableValidationTest, PinValidationUsageEncoderCopy) { |
| wgpu::TextureDescriptor desc{ |
| .usage = wgpu::TextureUsage::CopyDst, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Float, |
| }; |
| wgpu::Texture texDst = device.CreateTexture(&desc); |
| |
| for (auto pin : kAllTestPinStates) { |
| wgpu::Texture tex = CreateTextureWithPinState( |
| device, pin, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc); |
| |
| wgpu::TexelCopyTextureInfo src = { |
| .texture = tex, |
| }; |
| wgpu::TexelCopyTextureInfo dst = { |
| .texture = texDst, |
| }; |
| wgpu::Extent3D copySize = {0, 0, 0}; |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyTextureToTexture(&src, &dst, ©Size); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| |
| if (pin == TestPinState::Pinned) { |
| ASSERT_DEVICE_ERROR(device.GetQueue().Submit(1, &commands)); |
| } else { |
| device.GetQueue().Submit(1, &commands); |
| } |
| } |
| } |
| |
| // Test that pinning prevents usage in a dispatch if it is not the pinned usage. |
| TEST_F(ResourceTableValidationTest, PinValidationUsageDispatch) { |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.compute.module = utils::CreateShaderModule(device, R"( |
| @group(0) @binding(0) var t_sampled : texture_2d<f32>; |
| @compute @workgroup_size(1) fn sample() { |
| _ = t_sampled; |
| } |
| |
| @group(0) @binding(0) var t_ro_storage : texture_storage_2d<r32float, read>; |
| @compute @workgroup_size(1) fn ro_storage() { |
| _ = t_ro_storage; |
| } |
| )"); |
| |
| csDesc.compute.entryPoint = "sample"; |
| wgpu::ComputePipeline samplePipeline = device.CreateComputePipeline(&csDesc); |
| csDesc.compute.entryPoint = "ro_storage"; |
| wgpu::ComputePipeline storagePipeline = device.CreateComputePipeline(&csDesc); |
| |
| for (auto pin : kAllTestPinStates) { |
| wgpu::Texture tex = CreateTextureWithPinState( |
| device, pin, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::StorageBinding); |
| |
| for (bool sample : {false, true}) { |
| wgpu::ComputePipeline pipeline = sample ? samplePipeline : storagePipeline; |
| wgpu::BindGroup bg = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), |
| { |
| {0, tex.CreateView()}, |
| }); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bg); |
| pass.DispatchWorkgroups(1); |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| |
| if (pin == TestPinState::Pinned && !sample) { |
| ASSERT_DEVICE_ERROR(device.GetQueue().Submit(1, &commands)); |
| } else { |
| device.GetQueue().Submit(1, &commands); |
| } |
| } |
| } |
| } |
| |
| // Test that pinning prevents usage in a render pass if it is not the pinned usage. |
| TEST_F(ResourceTableValidationTest, PinValidationUsageRenderPass) { |
| wgpu::BindGroupLayout sampleLayout = utils::MakeBindGroupLayout( |
| device, { |
| {0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::UnfilterableFloat}, |
| }); |
| wgpu::BindGroupLayout storageLayout = utils::MakeBindGroupLayout( |
| device, { |
| {0, wgpu::ShaderStage::Fragment, wgpu::StorageTextureAccess::ReadOnly, |
| wgpu::TextureFormat::R32Float}, |
| }); |
| |
| for (auto pin : kAllTestPinStates) { |
| wgpu::Texture tex = CreateTextureWithPinState( |
| device, pin, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::StorageBinding); |
| |
| for (bool sample : {false, true}) { |
| wgpu::BindGroupLayout bgl = sample ? sampleLayout : storageLayout; |
| wgpu::BindGroup bg = utils::MakeBindGroup(device, bgl, |
| { |
| {0, tex.CreateView()}, |
| }); |
| |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 1, 1); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetBindGroup(0, bg); |
| pass.End(); |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| |
| if (pin == TestPinState::Pinned && !sample) { |
| ASSERT_DEVICE_ERROR(device.GetQueue().Submit(1, &commands)); |
| } else { |
| device.GetQueue().Submit(1, &commands); |
| } |
| } |
| } |
| } |
| |
| // Checks that only texture views are allowed as resources in mutators for SamplingResourceTable. |
| // TODO(https://issues.chromium.org/473354063): Support samplers in SamplingResourceTable. |
| TEST_F(ResourceTableValidationTest, MutatorBindingKindValidation) { |
| // Create the texture to put in the table. |
| wgpu::TextureDescriptor tDesc = { |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::Texture texture = device.CreateTexture(&tDesc); |
| |
| // Create the buffer to put in the table. |
| wgpu::BufferDescriptor bDesc{ |
| .usage = wgpu::BufferUsage::Storage, |
| .size = 4, |
| }; |
| wgpu::Buffer buffer = device.CreateBuffer(&bDesc); |
| |
| // Create the sampler to put in the table. |
| wgpu::Sampler sampler = device.CreateSampler(); |
| |
| for (auto mutator : {Mutator::Update, Mutator::InsertBinding}) { |
| // Control case: putting only a texture is valid. |
| { |
| wgpu::BindingResource resource = {.textureView = texture.CreateView()}; |
| TestMutator(mutator, &resource, true); |
| } |
| |
| // Error case: a buffer is an error. |
| { |
| wgpu::BindingResource resource = {.buffer = buffer}; |
| TestMutator(mutator, &resource, false); |
| } |
| |
| // Error case: a sampler is an error. |
| // TODO(https://issues.chromium.org/473354063): Support samplers in SamplingResourceTable. |
| { |
| wgpu::BindingResource resource = {.sampler = sampler}; |
| TestMutator(mutator, &resource, false); |
| } |
| |
| // Error case: both a sampler and a texture at the same time is an error. |
| { |
| wgpu::BindingResource resource = {.sampler = sampler, |
| .textureView = texture.CreateView()}; |
| TestMutator(mutator, &resource, false); |
| } |
| } |
| } |
| |
| // Check that the view must have only the TextureBinding usage for SamplingResourceTable. |
| // TODO(https://issues.chromium.org/473444515): Support storage textures in FullResourceTable |
| // TODO(https://issues.chromium.org/382544164): Support texel buffers in FullResourceTable |
| TEST_F(ResourceTableValidationTest, MutatorTextureViewMustBeOnlyTextureBinding) { |
| // Create the texture to put in the table. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding | |
| wgpu::TextureUsage::StorageBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::R32Uint, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| for (auto mutator : {Mutator::Update, Mutator::InsertBinding}) { |
| // Control case: limiting the usage to TextureBinding is valid. |
| { |
| wgpu::TextureViewDescriptor vDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| }; |
| wgpu::BindingResource resource = {.textureView = tex.CreateView(&vDesc)}; |
| TestMutator(mutator, &resource, true); |
| } |
| |
| // Error case: having unrelated usages in the view is not allowed. RenderAttachment case. |
| { |
| wgpu::TextureViewDescriptor vDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::RenderAttachment, |
| }; |
| wgpu::BindingResource resource = {.textureView = tex.CreateView(&vDesc)}; |
| TestMutator(mutator, &resource, false); |
| } |
| |
| // Error case: having unrelated usages in the view is not allowed. StorageBinding case. |
| { |
| wgpu::TextureViewDescriptor vDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::StorageBinding, |
| }; |
| wgpu::BindingResource resource = {.textureView = tex.CreateView(&vDesc)}; |
| TestMutator(mutator, &resource, false); |
| } |
| |
| // Error case: the defaulted texture usages don't contain TextureBinding. |
| { |
| wgpu::TextureDescriptor tDesc2 = tDesc; |
| tDesc2.usage = wgpu::TextureUsage::CopyDst; |
| wgpu::BindingResource resource = {.textureView = |
| device.CreateTexture(&tDesc2).CreateView()}; |
| TestMutator(mutator, &resource, false); |
| } |
| } |
| } |
| |
| // Check that the texture view must have a single aspect for mutators. |
| TEST_F(ResourceTableValidationTest, MutatorTextureViewMustBeSingleAspect) { |
| // Create the texture to put in the table. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::Depth24PlusStencil8, |
| }; |
| wgpu::Texture tex = device.CreateTexture(&tDesc); |
| |
| for (auto mutator : {Mutator::Update, Mutator::InsertBinding}) { |
| // Success case, only the depth aspect is selected. |
| { |
| wgpu::TextureViewDescriptor vDesc{ |
| .aspect = wgpu::TextureAspect::DepthOnly, |
| }; |
| wgpu::BindingResource resource = {.textureView = tex.CreateView(&vDesc)}; |
| TestMutator(mutator, &resource, true); |
| } |
| |
| // Success case, only the stencil aspect is selected. |
| { |
| wgpu::TextureViewDescriptor vDesc{ |
| .aspect = wgpu::TextureAspect::StencilOnly, |
| }; |
| wgpu::BindingResource resource = {.textureView = tex.CreateView(&vDesc)}; |
| TestMutator(mutator, &resource, true); |
| } |
| |
| // Error case: both aspects are selected. |
| { |
| wgpu::BindingResource resource = {.textureView = tex.CreateView()}; |
| TestMutator(mutator, &resource, false); |
| } |
| } |
| } |
| |
| // Test that it is not allowed to call Update, RemoveBinding or InsertBinding after the table is |
| // destroyed. |
| TEST_F(ResourceTableValidationTest, MutatorsAfterDestroy) { |
| // Create the texture to put in the table. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(7), MakeErrorResourceTable(7)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| |
| // Add a few bindings just to test RemoveBinding |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| EXPECT_EQ(wgpu::Status::Success, table.Update(1, &resource)); |
| |
| // Success cases, calling mutators before destroying is valid. |
| EXPECT_EQ(wgpu::Status::Success, table.Update(2, &resource)); |
| EXPECT_NE(wgpu::kInvalidBinding, table.InsertBinding(&resource)); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| |
| // Error case, after destruction all mutators return errors. |
| table.Destroy(); |
| EXPECT_EQ(wgpu::Status::Error, table.Update(6, &resource)); |
| EXPECT_EQ(wgpu::kInvalidBinding, table.InsertBinding(&resource)); |
| EXPECT_EQ(wgpu::Status::Error, table.RemoveBinding(1)); |
| } |
| } |
| |
| // Test that it is not allowed to call Update, RemoveBinding with slots past the end. |
| TEST_F(ResourceTableValidationTest, MutatorsAfterTableEnd) { |
| // Create the texture to put in the table. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(42), MakeErrorResourceTable(42)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| |
| // Success cases, calling mutators with slots in bounds. |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| EXPECT_EQ(wgpu::Status::Success, table.Update(41, &resource)); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(41)); |
| |
| // Error case, calling mutators with out of bounds slots. |
| EXPECT_EQ(wgpu::Status::Error, table.Update(42, &resource)); |
| EXPECT_EQ(wgpu::Status::Error, table.RemoveBinding(42)); |
| } |
| } |
| |
| // Test that Update/RemoveBinding return success but generates a validation error when used on an |
| // invalid table. |
| TEST_F(ResourceTableValidationTest, MutatorsOnInvalidTable) { |
| // Create the texture to put in the table. |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // Test on a valid table. |
| { |
| wgpu::ResourceTable table = MakeResourceTable(3); |
| |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| EXPECT_NE(wgpu::kInvalidBinding, table.InsertBinding(&resource)); |
| } |
| |
| // Test on an invalid table. |
| { |
| wgpu::ResourceTable table = MakeErrorResourceTable(3); |
| |
| ASSERT_DEVICE_ERROR(EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource))); |
| ASSERT_DEVICE_ERROR(EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0))); |
| ASSERT_DEVICE_ERROR(EXPECT_NE(wgpu::kInvalidBinding, table.InsertBinding(&resource))); |
| } |
| |
| // Test on an invalid table due to being too large. |
| { |
| wgpu::ResourceTable table; |
| ASSERT_DEVICE_ERROR(table = MakeResourceTable(kMaxResourceTableSize + 1)); |
| |
| EXPECT_EQ(wgpu::Status::Error, table.Update(0, &resource)); |
| EXPECT_EQ(wgpu::Status::Error, table.RemoveBinding(0)); |
| EXPECT_EQ(wgpu::kInvalidBinding, table.InsertBinding(&resource)); |
| } |
| } |
| |
| // Test that Update() can be called on a table slot if it has never been used before. |
| TEST_F(ResourceTableValidationTest, UpdateBindingWhenNeverUsed) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(3), MakeErrorResourceTable(3)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| |
| // Updating slot 0 when it has never been used is valid, but a second time is an error. |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| EXPECT_EQ(wgpu::Status::Error, table.Update(0, &resource)); |
| |
| // Even after using the table, a previously unused entry is valid to update. |
| UseResourceTableInSubmit(table); |
| EXPECT_EQ(wgpu::Status::Success, table.Update(1, &resource)); |
| } |
| } |
| |
| // Test that Remove() can be called on a table slot even when it was never used. |
| TEST_F(ResourceTableValidationTest, RemoveBindingWhenNeverUsed) { |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(3), MakeErrorResourceTable(3)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| } |
| } |
| |
| // Check that a table slot can be updated only after all commands submitted prior to RemoveBinding |
| // are completed. |
| TEST_F(ResourceTableValidationTest, UpdateAfterRemoveRequiresGPUIsFinished) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(1), MakeErrorResourceTable(1)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| |
| // 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. |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| |
| bool updateValid = false; |
| UseResourceTableInSubmit(table); |
| device.GetQueue().OnSubmittedWorkDone( |
| wgpu::CallbackMode::AllowSpontaneous, |
| [&](wgpu::QueueWorkDoneStatus, wgpu::StringView) { updateValid = true; }); |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| |
| // The null backend happens to call OnSubmittedWorkDone immediately because commands take 0 |
| // time. This test is duplicated in the end2end tests where OnSubmittedWorkDone won't fire |
| // immediately. |
| 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 trying to insert bindings fail when no more are available. |
| TEST_F(ResourceTableValidationTest, InsertBindingFailWhenNoMoreSpace) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(3), MakeErrorResourceTable(3)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| |
| // There is space for only three resources. |
| EXPECT_EQ(0u, table.InsertBinding(&resource)); |
| EXPECT_EQ(1u, table.InsertBinding(&resource)); |
| EXPECT_EQ(2u, table.InsertBinding(&resource)); |
| EXPECT_EQ(wgpu::kInvalidBinding, table.InsertBinding(&resource)); |
| |
| // Remove one binding (and wait for it to be recycled), it will be available for |
| // InsertBinding after which a new InsertBinding will still run out of space. |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(1)); |
| UseResourceTableInSubmit(table); |
| WaitForAllOperations(); |
| |
| EXPECT_EQ(1u, table.InsertBinding(&resource)); |
| EXPECT_EQ(wgpu::kInvalidBinding, table.InsertBinding(&resource)); |
| } |
| } |
| |
| // Check that bindings that are inserted are unavailable for Update() until RemoveBinding. |
| TEST_F(ResourceTableValidationTest, InsertBindingPreventsUpdate) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(1), MakeErrorResourceTable(1)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| |
| EXPECT_EQ(0u, table.InsertBinding(&resource)); |
| EXPECT_EQ(wgpu::Status::Error, table.Update(0, &resource)); |
| |
| // Remove one binding (and wait for it to be recycled), it will be available for Update. |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(0)); |
| UseResourceTableInSubmit(table); |
| WaitForAllOperations(); |
| |
| EXPECT_EQ(wgpu::Status::Success, table.Update(0, &resource)); |
| } |
| } |
| |
| // Check that InsertBinding skips over used slots. |
| TEST_F(ResourceTableValidationTest, InsertBindingSkipsOverUsedSlots) { |
| wgpu::TextureDescriptor tDesc{ |
| .usage = wgpu::TextureUsage::TextureBinding, |
| .size = {1, 1}, |
| .format = wgpu::TextureFormat::RGBA8Unorm, |
| }; |
| wgpu::BindingResource resource = {.textureView = device.CreateTexture(&tDesc).CreateView()}; |
| |
| // This is "content timeline" validation so it works the same on error tables and valid tables, |
| // and we ignore device-timeline validation errors, they are not what we are testing here. |
| for (auto table : {MakeResourceTable(5), MakeErrorResourceTable(5)}) { |
| utils::ScopedIgnoreValidationErrors ignoreErrors(device); |
| EXPECT_EQ(wgpu::Status::Success, table.Update(1, &resource)); |
| EXPECT_EQ(wgpu::Status::Success, table.Update(3, &resource)); |
| |
| // InsertBinding skips over entries used by Update() |
| EXPECT_EQ(0u, table.InsertBinding(&resource)); |
| EXPECT_EQ(2u, table.InsertBinding(&resource)); |
| EXPECT_EQ(4u, table.InsertBinding(&resource)); |
| |
| // Remove bindings in inverse order. |
| for (uint32_t i : {4, 3, 2}) { |
| EXPECT_EQ(wgpu::Status::Success, table.RemoveBinding(i)); |
| } |
| UseResourceTableInSubmit(table); |
| WaitForAllOperations(); |
| |
| // InsertBinding should still return the min available slot. |
| EXPECT_EQ(2u, table.InsertBinding(&resource)); |
| EXPECT_EQ(3u, table.InsertBinding(&resource)); |
| EXPECT_EQ(4u, table.InsertBinding(&resource)); |
| } |
| } |
| |
| // Test the value returned by GetSize right after creating the table. |
| TEST_F(ResourceTableValidationTest, GetSizeAfterCreation) { |
| // Valid resource tables of varying size. |
| { |
| EXPECT_EQ(0u, MakeResourceTable(0).GetSize()); |
| EXPECT_EQ(42u, MakeResourceTable(42).GetSize()); |
| EXPECT_EQ(kMaxResourceTableSize, MakeResourceTable(kMaxResourceTableSize).GetSize()); |
| } |
| |
| // Invalid resource tables of varying size under the limit. |
| { |
| EXPECT_EQ(0u, MakeErrorResourceTable(0).GetSize()); |
| EXPECT_EQ(42u, MakeErrorResourceTable(42).GetSize()); |
| EXPECT_EQ(kMaxResourceTableSize, MakeErrorResourceTable(kMaxResourceTableSize).GetSize()); |
| } |
| |
| // Invalid resource table with a size above the limit is a special case that doesn't allocate |
| // state tracking. |
| { |
| wgpu::ResourceTable table; |
| ASSERT_DEVICE_ERROR(table = MakeResourceTable(kMaxResourceTableSize + 1)); |
| EXPECT_EQ(0u, table.GetSize()); |
| } |
| } |
| |
| // Test the value returned by GetSize after calling Destroy() should return the same value. |
| TEST_F(ResourceTableValidationTest, GetSizeAfterDestroy) { |
| // Valid resource table. |
| { |
| wgpu::ResourceTable table = MakeResourceTable(42); |
| EXPECT_EQ(42u, table.GetSize()); |
| table.Destroy(); |
| EXPECT_EQ(42u, table.GetSize()); |
| } |
| |
| // Invalid resource table. |
| { |
| wgpu::ResourceTable table = MakeResourceTable(42); |
| EXPECT_EQ(42u, table.GetSize()); |
| table.Destroy(); |
| EXPECT_EQ(42u, table.GetSize()); |
| } |
| } |
| |
| } // namespace |
| } // namespace dawn |