blob: c693306a876c977be9a8a8548783448103c76162 [file] [log] [blame] [edit]
// 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, &copySize));
} else {
device.GetQueue().WriteTexture(&dst, nullptr, 0, &dataLayout, &copySize);
}
}
}
// 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, &copySize);
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