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