blob: 16302b9e9045b5ceee848e73af88d5f2926b57a2 [file] [log] [blame] [edit]
// Copyright 2017 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "tests/unittests/validation/ValidationTest.h"
#include "common/Assert.h"
#include "common/Constants.h"
#include "utils/ComboRenderPipelineDescriptor.h"
#include "utils/WGPUHelpers.h"
class BindGroupValidationTest : public ValidationTest {
public:
wgpu::Texture CreateTexture(wgpu::TextureUsage usage,
wgpu::TextureFormat format,
uint32_t layerCount) {
wgpu::TextureDescriptor descriptor;
descriptor.dimension = wgpu::TextureDimension::e2D;
descriptor.size = {16, 16, layerCount};
descriptor.sampleCount = 1;
descriptor.mipLevelCount = 1;
descriptor.usage = usage;
descriptor.format = format;
return device.CreateTexture(&descriptor);
}
void SetUp() override {
ValidationTest::SetUp();
// Create objects to use as resources inside test bind groups.
{
wgpu::BufferDescriptor descriptor;
descriptor.size = 1024;
descriptor.usage = wgpu::BufferUsage::Uniform;
mUBO = device.CreateBuffer(&descriptor);
}
{
wgpu::BufferDescriptor descriptor;
descriptor.size = 1024;
descriptor.usage = wgpu::BufferUsage::Storage;
mSSBO = device.CreateBuffer(&descriptor);
}
{ mSampler = device.CreateSampler(); }
{
mSampledTexture =
CreateTexture(wgpu::TextureUsage::TextureBinding, kDefaultTextureFormat, 1);
mSampledTextureView = mSampledTexture.CreateView();
wgpu::ExternalTextureDescriptor externalTextureDesc;
externalTextureDesc.format = kDefaultTextureFormat;
externalTextureDesc.plane0 = mSampledTextureView;
mExternalTexture = device.CreateExternalTexture(&externalTextureDesc);
mExternalTextureBindingEntry.externalTexture = mExternalTexture;
}
}
protected:
wgpu::Buffer mUBO;
wgpu::Buffer mSSBO;
wgpu::Sampler mSampler;
wgpu::Texture mSampledTexture;
wgpu::TextureView mSampledTextureView;
wgpu::ExternalTextureBindingEntry mExternalTextureBindingEntry;
static constexpr wgpu::TextureFormat kDefaultTextureFormat = wgpu::TextureFormat::RGBA8Unorm;
private:
wgpu::ExternalTexture mExternalTexture;
};
// Test the validation of BindGroupDescriptor::nextInChain
TEST_F(BindGroupValidationTest, NextInChainNullptr) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(device, {});
wgpu::BindGroupDescriptor descriptor;
descriptor.layout = layout;
descriptor.entryCount = 0;
descriptor.entries = nullptr;
// Control case: check that nextInChain = nullptr is valid
descriptor.nextInChain = nullptr;
device.CreateBindGroup(&descriptor);
// Check that nextInChain != nullptr is an error.
wgpu::ChainedStruct chainedDescriptor;
chainedDescriptor.sType = wgpu::SType::Invalid;
descriptor.nextInChain = &chainedDescriptor;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
}
// Check constraints on entryCount
TEST_F(BindGroupValidationTest, EntryCountMismatch) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
// Control case: check that a descriptor with one binding is ok
utils::MakeBindGroup(device, layout, {{0, mSampler}});
// Check that entryCount != layout.entryCount fails.
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {}));
}
// Check constraints on BindGroupEntry::binding
TEST_F(BindGroupValidationTest, WrongBindings) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
// Control case: check that a descriptor with a binding matching the layout's is ok
utils::MakeBindGroup(device, layout, {{0, mSampler}});
// Check that binding must be present in the layout
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{1, mSampler}}));
}
// Check that the same binding cannot be set twice
TEST_F(BindGroupValidationTest, BindingSetTwice) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering},
{1, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
// Control case: check that different bindings work
utils::MakeBindGroup(device, layout, {{0, mSampler}, {1, mSampler}});
// Check that setting the same binding twice is invalid
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, mSampler}, {0, mSampler}}));
}
// Check that a sampler binding must contain exactly one sampler
TEST_F(BindGroupValidationTest, SamplerBindingType) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
wgpu::BindGroupEntry binding;
binding.binding = 0;
binding.sampler = nullptr;
binding.textureView = nullptr;
binding.buffer = nullptr;
binding.offset = 0;
binding.size = 0;
wgpu::BindGroupDescriptor descriptor;
descriptor.layout = layout;
descriptor.entryCount = 1;
descriptor.entries = &binding;
// Not setting anything fails
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
// Control case: setting just the sampler works
binding.sampler = mSampler;
device.CreateBindGroup(&descriptor);
// Setting the texture view as well is an error
binding.textureView = mSampledTextureView;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.textureView = nullptr;
// Setting the buffer as well is an error
binding.buffer = mUBO;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.buffer = nullptr;
// Setting the external texture view as well is an error
binding.nextInChain = &mExternalTextureBindingEntry;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.nextInChain = nullptr;
// Setting the sampler to an error sampler is an error.
{
wgpu::SamplerDescriptor samplerDesc;
samplerDesc.minFilter = static_cast<wgpu::FilterMode>(0xFFFFFFFF);
wgpu::Sampler errorSampler;
ASSERT_DEVICE_ERROR(errorSampler = device.CreateSampler(&samplerDesc));
binding.sampler = errorSampler;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.sampler = nullptr;
}
}
// Check that a texture binding must contain exactly a texture view
TEST_F(BindGroupValidationTest, TextureBindingType) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}});
wgpu::BindGroupEntry binding;
binding.binding = 0;
binding.sampler = nullptr;
binding.textureView = nullptr;
binding.buffer = nullptr;
binding.offset = 0;
binding.size = 0;
wgpu::BindGroupDescriptor descriptor;
descriptor.layout = layout;
descriptor.entryCount = 1;
descriptor.entries = &binding;
// Not setting anything fails
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
// Control case: setting just the texture view works
binding.textureView = mSampledTextureView;
device.CreateBindGroup(&descriptor);
// Setting the sampler as well is an error
binding.sampler = mSampler;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.sampler = nullptr;
// Setting the buffer as well is an error
binding.buffer = mUBO;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.buffer = nullptr;
// Setting the external texture view as well is an error
binding.nextInChain = &mExternalTextureBindingEntry;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.nextInChain = nullptr;
// Setting the texture view to an error texture view is an error.
{
wgpu::TextureViewDescriptor viewDesc;
viewDesc.format = kDefaultTextureFormat;
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
viewDesc.baseMipLevel = 0;
viewDesc.mipLevelCount = 0;
viewDesc.baseArrayLayer = 0;
viewDesc.arrayLayerCount = 1000;
wgpu::TextureView errorView;
ASSERT_DEVICE_ERROR(errorView = mSampledTexture.CreateView(&viewDesc));
binding.textureView = errorView;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.textureView = nullptr;
}
}
// Check that a buffer binding must contain exactly a buffer
TEST_F(BindGroupValidationTest, BufferBindingType) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform}});
wgpu::BindGroupEntry binding;
binding.binding = 0;
binding.sampler = nullptr;
binding.textureView = nullptr;
binding.buffer = nullptr;
binding.offset = 0;
binding.size = 1024;
wgpu::BindGroupDescriptor descriptor;
descriptor.layout = layout;
descriptor.entryCount = 1;
descriptor.entries = &binding;
// Not setting anything fails
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
// Control case: setting just the buffer works
binding.buffer = mUBO;
device.CreateBindGroup(&descriptor);
// Setting the texture view as well is an error
binding.textureView = mSampledTextureView;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.textureView = nullptr;
// Setting the sampler as well is an error
binding.sampler = mSampler;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.sampler = nullptr;
// Setting the external texture view as well is an error
binding.nextInChain = &mExternalTextureBindingEntry;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.nextInChain = nullptr;
// Setting the buffer to an error buffer is an error.
{
wgpu::BufferDescriptor bufferDesc;
bufferDesc.size = 1024;
bufferDesc.usage = static_cast<wgpu::BufferUsage>(0xFFFFFFFF);
wgpu::Buffer errorBuffer;
ASSERT_DEVICE_ERROR(errorBuffer = device.CreateBuffer(&bufferDesc));
binding.buffer = errorBuffer;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.buffer = nullptr;
}
}
// Check that an external texture binding must contain exactly an external texture
TEST_F(BindGroupValidationTest, ExternalTextureBindingType) {
// Create an external texture
wgpu::Texture texture =
CreateTexture(wgpu::TextureUsage::TextureBinding, kDefaultTextureFormat, 1);
wgpu::ExternalTextureDescriptor externalDesc;
externalDesc.plane0 = texture.CreateView();
externalDesc.format = kDefaultTextureFormat;
wgpu::ExternalTexture externalTexture = device.CreateExternalTexture(&externalDesc);
// Create a bind group layout for a single external texture
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, &utils::kExternalTextureBindingLayout}});
wgpu::BindGroupEntry binding;
binding.binding = 0;
binding.sampler = nullptr;
binding.textureView = nullptr;
binding.buffer = nullptr;
binding.offset = 0;
binding.size = 0;
wgpu::BindGroupDescriptor descriptor;
descriptor.layout = layout;
descriptor.entryCount = 1;
descriptor.entries = &binding;
// Not setting anything fails
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
// Control case: setting just the external texture works
wgpu::ExternalTextureBindingEntry externalBindingEntry;
externalBindingEntry.externalTexture = externalTexture;
binding.nextInChain = &externalBindingEntry;
device.CreateBindGroup(&descriptor);
// Setting the texture view as well is an error
binding.textureView = mSampledTextureView;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.textureView = nullptr;
// Setting the sampler as well is an error
binding.sampler = mSampler;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.sampler = nullptr;
// Setting the buffer as well is an error
binding.buffer = mUBO;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.buffer = nullptr;
// Setting the external texture to an error external texture is an error.
{
wgpu::ExternalTextureDescriptor errorExternalDesciptor;
errorExternalDesciptor.plane0 = texture.CreateView();
errorExternalDesciptor.format = wgpu::TextureFormat::R8Uint;
wgpu::ExternalTexture errorExternalTexture;
ASSERT_DEVICE_ERROR(errorExternalTexture =
device.CreateExternalTexture(&errorExternalDesciptor));
wgpu::ExternalTextureBindingEntry errorExternalBindingEntry;
errorExternalBindingEntry.externalTexture = errorExternalTexture;
binding.nextInChain = &errorExternalBindingEntry;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.nextInChain = nullptr;
}
// Setting an external texture with another external texture chained is an error.
{
wgpu::ExternalTexture externalTexture2 = device.CreateExternalTexture(&externalDesc);
wgpu::ExternalTextureBindingEntry externalBindingEntry2;
externalBindingEntry2.externalTexture = externalTexture2;
externalBindingEntry.nextInChain = &externalBindingEntry2;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
}
// Chaining a struct that isn't an external texture binding entry is an error.
{
wgpu::ExternalTextureBindingLayout externalBindingLayout;
binding.nextInChain = &externalBindingLayout;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
}
}
// Check that a texture binding must have the correct usage
TEST_F(BindGroupValidationTest, TextureUsage) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}});
// Control case: setting a sampleable texture view works.
utils::MakeBindGroup(device, layout, {{0, mSampledTextureView}});
// Make an render attachment texture and try to set it for a SampledTexture binding
wgpu::Texture outputTexture =
CreateTexture(wgpu::TextureUsage::RenderAttachment, wgpu::TextureFormat::RGBA8Unorm, 1);
wgpu::TextureView outputTextureView = outputTexture.CreateView();
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, outputTextureView}}));
}
// Check that a storage texture binding must have the correct usage
TEST_F(BindGroupValidationTest, StorageTextureUsage) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute, wgpu::StorageTextureAccess::WriteOnly,
wgpu::TextureFormat::RGBA8Uint}});
wgpu::TextureDescriptor descriptor;
descriptor.dimension = wgpu::TextureDimension::e2D;
descriptor.size = {16, 16, 1};
descriptor.sampleCount = 1;
descriptor.mipLevelCount = 1;
descriptor.usage = wgpu::TextureUsage::StorageBinding;
descriptor.format = wgpu::TextureFormat::RGBA8Uint;
wgpu::TextureView view = device.CreateTexture(&descriptor).CreateView();
// Control case: setting a storage texture view works.
utils::MakeBindGroup(device, layout, {{0, view}});
// Sampled texture is invalid with storage buffer binding
descriptor.usage = wgpu::TextureUsage::TextureBinding;
descriptor.format = wgpu::TextureFormat::RGBA8Unorm;
view = device.CreateTexture(&descriptor).CreateView();
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, view}}));
// Multisampled texture is invalid with storage buffer binding
// Regression case for crbug.com/dawn/614 where this hit an ASSERT.
descriptor.sampleCount = 4;
view = device.CreateTexture(&descriptor).CreateView();
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, view}}));
}
// Check that a texture must have the correct sample type
TEST_F(BindGroupValidationTest, TextureSampleType) {
auto DoTest = [this](bool success, wgpu::TextureFormat format,
wgpu::TextureSampleType sampleType) {
wgpu::BindGroupLayout layout =
utils::MakeBindGroupLayout(device, {{0, wgpu::ShaderStage::Fragment, sampleType}});
wgpu::TextureDescriptor descriptor;
descriptor.size = {4, 4, 1};
descriptor.usage = wgpu::TextureUsage::TextureBinding;
descriptor.format = format;
wgpu::TextureView view = device.CreateTexture(&descriptor).CreateView();
if (success) {
utils::MakeBindGroup(device, layout, {{0, view}});
} else {
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, view}}));
}
};
// Test that RGBA8Unorm is only compatible with float/unfilterable-float
DoTest(true, wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureSampleType::Float);
DoTest(true, wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureSampleType::UnfilterableFloat);
DoTest(false, wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureSampleType::Depth);
DoTest(false, wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureSampleType::Uint);
DoTest(false, wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureSampleType::Sint);
// Test that R32Float is only compatible with unfilterable-float
DoTest(false, wgpu::TextureFormat::R32Float, wgpu::TextureSampleType::Float);
DoTest(true, wgpu::TextureFormat::R32Float, wgpu::TextureSampleType::UnfilterableFloat);
DoTest(false, wgpu::TextureFormat::R32Float, wgpu::TextureSampleType::Depth);
DoTest(false, wgpu::TextureFormat::R32Float, wgpu::TextureSampleType::Uint);
DoTest(false, wgpu::TextureFormat::R32Float, wgpu::TextureSampleType::Sint);
// Test that Depth32Float is only compatible with depth.
DoTest(false, wgpu::TextureFormat::Depth32Float, wgpu::TextureSampleType::Float);
DoTest(false, wgpu::TextureFormat::Depth32Float, wgpu::TextureSampleType::UnfilterableFloat);
DoTest(true, wgpu::TextureFormat::Depth32Float, wgpu::TextureSampleType::Depth);
DoTest(false, wgpu::TextureFormat::Depth32Float, wgpu::TextureSampleType::Uint);
DoTest(false, wgpu::TextureFormat::Depth32Float, wgpu::TextureSampleType::Sint);
// Test that RG8Uint is only compatible with uint
DoTest(false, wgpu::TextureFormat::RG8Uint, wgpu::TextureSampleType::Float);
DoTest(false, wgpu::TextureFormat::RG8Uint, wgpu::TextureSampleType::UnfilterableFloat);
DoTest(false, wgpu::TextureFormat::RG8Uint, wgpu::TextureSampleType::Depth);
DoTest(true, wgpu::TextureFormat::RG8Uint, wgpu::TextureSampleType::Uint);
DoTest(false, wgpu::TextureFormat::RG8Uint, wgpu::TextureSampleType::Sint);
// Test that R16Sint is only compatible with sint
DoTest(false, wgpu::TextureFormat::R16Sint, wgpu::TextureSampleType::Float);
DoTest(false, wgpu::TextureFormat::R16Sint, wgpu::TextureSampleType::UnfilterableFloat);
DoTest(false, wgpu::TextureFormat::R16Sint, wgpu::TextureSampleType::Depth);
DoTest(false, wgpu::TextureFormat::R16Sint, wgpu::TextureSampleType::Uint);
DoTest(true, wgpu::TextureFormat::R16Sint, wgpu::TextureSampleType::Sint);
}
// Test which depth-stencil formats are allowed to be sampled (all).
TEST_F(BindGroupValidationTest, SamplingDepthStencilTexture) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth}});
wgpu::TextureDescriptor desc;
desc.size = {1, 1, 1};
desc.usage = wgpu::TextureUsage::TextureBinding;
// Depth32Float is allowed to be sampled.
{
desc.format = wgpu::TextureFormat::Depth32Float;
wgpu::Texture texture = device.CreateTexture(&desc);
utils::MakeBindGroup(device, layout, {{0, texture.CreateView()}});
}
// Depth24Plus is allowed to be sampled.
{
desc.format = wgpu::TextureFormat::Depth24Plus;
wgpu::Texture texture = device.CreateTexture(&desc);
utils::MakeBindGroup(device, layout, {{0, texture.CreateView()}});
}
// Depth24PlusStencil8 is allowed to be sampled, if the depth or stencil aspect is selected.
{
desc.format = wgpu::TextureFormat::Depth24PlusStencil8;
wgpu::Texture texture = device.CreateTexture(&desc);
wgpu::TextureViewDescriptor viewDesc = {};
viewDesc.aspect = wgpu::TextureAspect::DepthOnly;
utils::MakeBindGroup(device, layout, {{0, texture.CreateView(&viewDesc)}});
layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Uint}});
viewDesc.aspect = wgpu::TextureAspect::StencilOnly;
utils::MakeBindGroup(device, layout, {{0, texture.CreateView(&viewDesc)}});
}
}
// Check that a texture must have the correct dimension
TEST_F(BindGroupValidationTest, TextureDimension) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}});
// Control case: setting a 2D texture view works.
utils::MakeBindGroup(device, layout, {{0, mSampledTextureView}});
// Make a 2DArray texture and try to set it to a 2D binding.
wgpu::Texture arrayTexture =
CreateTexture(wgpu::TextureUsage::TextureBinding, wgpu::TextureFormat::RGBA8Uint, 2);
wgpu::TextureView arrayTextureView = arrayTexture.CreateView();
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, arrayTextureView}}));
}
// Check that a storage texture binding must have a texture view with a mipLevelCount of 1
TEST_F(BindGroupValidationTest, StorageTextureViewLayerCount) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute, wgpu::StorageTextureAccess::WriteOnly,
wgpu::TextureFormat::RGBA8Uint}});
wgpu::TextureDescriptor descriptor;
descriptor.dimension = wgpu::TextureDimension::e2D;
descriptor.size = {16, 16, 1};
descriptor.sampleCount = 1;
descriptor.mipLevelCount = 1;
descriptor.usage = wgpu::TextureUsage::StorageBinding;
descriptor.format = wgpu::TextureFormat::RGBA8Uint;
wgpu::Texture textureNoMip = device.CreateTexture(&descriptor);
descriptor.mipLevelCount = 3;
wgpu::Texture textureMip = device.CreateTexture(&descriptor);
// Control case: setting a storage texture view on a texture with only one mip level works
{
wgpu::TextureView view = textureNoMip.CreateView();
utils::MakeBindGroup(device, layout, {{0, view}});
}
// Setting a storage texture view with mipLevelCount=1 on a texture of multiple mip levels is
// valid
{
wgpu::TextureViewDescriptor viewDesc = {};
viewDesc.aspect = wgpu::TextureAspect::All;
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
viewDesc.format = wgpu::TextureFormat::RGBA8Uint;
viewDesc.baseMipLevel = 0;
viewDesc.mipLevelCount = 1;
// Setting texture view with lod 0 is valid
wgpu::TextureView view = textureMip.CreateView(&viewDesc);
utils::MakeBindGroup(device, layout, {{0, view}});
// Setting texture view with other lod is also valid
viewDesc.baseMipLevel = 2;
view = textureMip.CreateView(&viewDesc);
utils::MakeBindGroup(device, layout, {{0, view}});
}
// Texture view with mipLevelCount > 1 is invalid
{
wgpu::TextureViewDescriptor viewDesc = {};
viewDesc.aspect = wgpu::TextureAspect::All;
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
viewDesc.format = wgpu::TextureFormat::RGBA8Uint;
viewDesc.baseMipLevel = 0;
viewDesc.mipLevelCount = 2;
// Setting texture view with lod 0 and 1 is invalid
wgpu::TextureView view = textureMip.CreateView(&viewDesc);
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, view}}));
// Setting texture view with lod 1 and 2 is invalid
viewDesc.baseMipLevel = 1;
view = textureMip.CreateView(&viewDesc);
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, view}}));
}
}
// Check that a UBO must have the correct usage
TEST_F(BindGroupValidationTest, BufferUsageUBO) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform}});
// Control case: using a buffer with the uniform usage works
utils::MakeBindGroup(device, layout, {{0, mUBO, 0, 256}});
// Using a buffer without the uniform usage fails
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, mSSBO, 0, 256}}));
}
// Check that a SSBO must have the correct usage
TEST_F(BindGroupValidationTest, BufferUsageSSBO) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});
// Control case: using a buffer with the storage usage works
utils::MakeBindGroup(device, layout, {{0, mSSBO, 0, 256}});
// Using a buffer without the storage usage fails
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, mUBO, 0, 256}}));
}
// Check that a readonly SSBO must have the correct usage
TEST_F(BindGroupValidationTest, BufferUsageReadonlySSBO) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage}});
// Control case: using a buffer with the storage usage works
utils::MakeBindGroup(device, layout, {{0, mSSBO, 0, 256}});
// Using a buffer without the storage usage fails
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, mUBO, 0, 256}}));
}
// Check that a resolve buffer with internal storge usage cannot be used as SSBO
TEST_F(BindGroupValidationTest, BufferUsageQueryResolve) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});
// Control case: using a buffer with the storage usage works
utils::MakeBindGroup(device, layout, {{0, mSSBO, 0, 256}});
// Using a resolve buffer with the internal storage usage fails
wgpu::BufferDescriptor descriptor;
descriptor.size = 1024;
descriptor.usage = wgpu::BufferUsage::QueryResolve;
wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, buffer, 0, 256}}));
}
// Tests constraints on the buffer offset for bind groups.
TEST_F(BindGroupValidationTest, BufferOffsetAlignment) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
});
// Check that offset 0 is valid
utils::MakeBindGroup(device, layout, {{0, mUBO, 0, 512}});
// Check that offset 256 (aligned) is valid
utils::MakeBindGroup(device, layout, {{0, mUBO, 256, 256}});
// Check cases where unaligned buffer offset is invalid
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, mUBO, 1, 256}}));
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, mUBO, 128, 256}}));
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, mUBO, 255, 256}}));
}
// Tests constraints on the texture for MultisampledTexture bindings
TEST_F(BindGroupValidationTest, MultisampledTexture) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2D, true}});
wgpu::BindGroupEntry binding;
binding.binding = 0;
binding.sampler = nullptr;
binding.textureView = nullptr;
binding.buffer = nullptr;
binding.offset = 0;
binding.size = 0;
wgpu::BindGroupDescriptor descriptor;
descriptor.layout = layout;
descriptor.entryCount = 1;
descriptor.entries = &binding;
// Not setting anything fails
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
// Control case: setting a multisampled 2D texture works
wgpu::TextureDescriptor textureDesc;
textureDesc.sampleCount = 4;
textureDesc.usage = wgpu::TextureUsage::TextureBinding;
textureDesc.dimension = wgpu::TextureDimension::e2D;
textureDesc.format = wgpu::TextureFormat::RGBA8Unorm;
textureDesc.size = {1, 1, 1};
wgpu::Texture msTexture = device.CreateTexture(&textureDesc);
binding.textureView = msTexture.CreateView();
device.CreateBindGroup(&descriptor);
binding.textureView = nullptr;
// Error case: setting a single sampled 2D texture is an error.
binding.textureView = mSampledTextureView;
ASSERT_DEVICE_ERROR(device.CreateBindGroup(&descriptor));
binding.textureView = nullptr;
}
// Tests constraints to be sure the buffer binding fits in the buffer
TEST_F(BindGroupValidationTest, BufferBindingOOB) {
wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
});
wgpu::BufferDescriptor descriptor;
descriptor.size = 1024;
descriptor.usage = wgpu::BufferUsage::Uniform;
wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
// Success case, touching the start of the buffer works
utils::MakeBindGroup(device, layout, {{0, buffer, 0, 256}});
// Success case, touching the end of the buffer works
utils::MakeBindGroup(device, layout, {{0, buffer, 3 * 256, 256}});
// Error case, zero size is invalid.
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, buffer, 1024, 0}}));
// Success case, touching the full buffer works
utils::MakeBindGroup(device, layout, {{0, buffer, 0, 1024}});
utils::MakeBindGroup(device, layout, {{0, buffer, 0, wgpu::kWholeSize}});
// Success case, whole size causes the rest of the buffer to be used but not beyond.
utils::MakeBindGroup(device, layout, {{0, buffer, 256, wgpu::kWholeSize}});
// Error case, offset is OOB
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, buffer, 256 * 5, 0}}));
// Error case, size is OOB
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, buffer, 0, 256 * 5}}));
// Error case, offset+size is OOB
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, layout, {{0, buffer, 1024, 256}}));
// Error case, offset+size overflows to be 0
ASSERT_DEVICE_ERROR(
utils::MakeBindGroup(device, layout, {{0, buffer, 256, uint32_t(0) - uint32_t(256)}}));
}
// Tests constraints to be sure the uniform buffer binding isn't too large
TEST_F(BindGroupValidationTest, MaxUniformBufferBindingSize) {
wgpu::Limits supportedLimits = GetSupportedLimits().limits;
wgpu::BufferDescriptor descriptor;
descriptor.size = 2 * supportedLimits.maxUniformBufferBindingSize;
descriptor.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::Storage;
wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
wgpu::BindGroupLayout uniformLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform}});
// Success case, this is exactly the limit
utils::MakeBindGroup(device, uniformLayout,
{{0, buffer, 0, supportedLimits.maxUniformBufferBindingSize}});
wgpu::BindGroupLayout doubleUniformLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
{1, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform}});
// Success case, individual bindings don't exceed the limit
utils::MakeBindGroup(device, doubleUniformLayout,
{{0, buffer, 0, supportedLimits.maxUniformBufferBindingSize},
{1, buffer, supportedLimits.maxUniformBufferBindingSize,
supportedLimits.maxUniformBufferBindingSize}});
// Error case, this is above the limit
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(
device, uniformLayout, {{0, buffer, 0, supportedLimits.maxUniformBufferBindingSize + 1}}));
// Making sure the constraint doesn't apply to storage buffers
wgpu::BindGroupLayout readonlyStorageLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage}});
wgpu::BindGroupLayout storageLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});
// Success case, storage buffer can still be created.
utils::MakeBindGroup(device, readonlyStorageLayout,
{{0, buffer, 0, 2 * supportedLimits.maxUniformBufferBindingSize}});
utils::MakeBindGroup(device, storageLayout,
{{0, buffer, 0, 2 * supportedLimits.maxUniformBufferBindingSize}});
}
// Tests constraints to be sure the storage buffer binding isn't too large
TEST_F(BindGroupValidationTest, MaxStorageBufferBindingSize) {
wgpu::Limits supportedLimits = GetSupportedLimits().limits;
wgpu::BufferDescriptor descriptor;
descriptor.size = 2 * supportedLimits.maxStorageBufferBindingSize;
descriptor.usage = wgpu::BufferUsage::Storage;
wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
wgpu::BindGroupLayout uniformLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});
// Success case, this is exactly the limit
utils::MakeBindGroup(device, uniformLayout,
{{0, buffer, 0, supportedLimits.maxStorageBufferBindingSize}});
// Success case, this is one less than the limit (check it is not an alignment constraint)
utils::MakeBindGroup(device, uniformLayout,
{{0, buffer, 0, supportedLimits.maxStorageBufferBindingSize - 1}});
wgpu::BindGroupLayout doubleUniformLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage},
{1, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});
// Success case, individual bindings don't exceed the limit
utils::MakeBindGroup(device, doubleUniformLayout,
{{0, buffer, 0, supportedLimits.maxStorageBufferBindingSize},
{1, buffer, supportedLimits.maxStorageBufferBindingSize,
supportedLimits.maxStorageBufferBindingSize}});
// Error case, this is above the limit
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(
device, uniformLayout, {{0, buffer, 0, supportedLimits.maxStorageBufferBindingSize + 1}}));
}
// Test what happens when the layout is an error.
TEST_F(BindGroupValidationTest, ErrorLayout) {
wgpu::BindGroupLayout goodLayout = utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
});
wgpu::BindGroupLayout errorLayout;
ASSERT_DEVICE_ERROR(
errorLayout = utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
}));
// Control case, creating with the good layout works
utils::MakeBindGroup(device, goodLayout, {{0, mUBO, 0, 256}});
// Creating with an error layout fails
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, errorLayout, {{0, mUBO, 0, 256}}));
}
class BindGroupLayoutValidationTest : public ValidationTest {
public:
wgpu::BindGroupLayout MakeBindGroupLayout(wgpu::BindGroupLayoutEntry* binding, uint32_t count) {
wgpu::BindGroupLayoutDescriptor descriptor;
descriptor.entryCount = count;
descriptor.entries = binding;
return device.CreateBindGroupLayout(&descriptor);
}
void TestCreateBindGroupLayout(wgpu::BindGroupLayoutEntry* binding,
uint32_t count,
bool expected) {
wgpu::BindGroupLayoutDescriptor descriptor;
descriptor.entryCount = count;
descriptor.entries = binding;
if (!expected) {
ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&descriptor));
} else {
device.CreateBindGroupLayout(&descriptor);
}
}
void TestCreatePipelineLayout(wgpu::BindGroupLayout* bgl, uint32_t count, bool expected) {
wgpu::PipelineLayoutDescriptor descriptor;
descriptor.bindGroupLayoutCount = count;
descriptor.bindGroupLayouts = bgl;
if (!expected) {
ASSERT_DEVICE_ERROR(device.CreatePipelineLayout(&descriptor));
} else {
device.CreatePipelineLayout(&descriptor);
}
}
};
// Tests setting storage buffer and readonly storage buffer bindings in vertex and fragment shader.
TEST_F(BindGroupLayoutValidationTest, BindGroupLayoutStorageBindingsInVertexShader) {
// Checks that storage buffer binding is not supported in vertex shader.
ASSERT_DEVICE_ERROR(utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Storage}}));
utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::ReadOnlyStorage}});
utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});
utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage}});
}
// Tests setting that bind group layout bindings numbers may be very large.
TEST_F(BindGroupLayoutValidationTest, BindGroupLayoutEntryNumberLarge) {
// Checks that uint32_t max is valid.
utils::MakeBindGroupLayout(device,
{{std::numeric_limits<uint32_t>::max(), wgpu::ShaderStage::Vertex,
wgpu::BufferBindingType::Uniform}});
}
// This test verifies that the BindGroupLayout bindings are correctly validated, even if the
// binding ids are out-of-order.
TEST_F(BindGroupLayoutValidationTest, BindGroupEntry) {
utils::MakeBindGroupLayout(device,
{
{1, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
});
}
// Check that dynamic = true is only allowed buffer bindings.
TEST_F(BindGroupLayoutValidationTest, DynamicAndTypeCompatibility) {
utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::Uniform, true},
});
utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::Storage, true},
});
utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::ReadOnlyStorage, true},
});
}
// This test verifies that visibility of bindings in BindGroupLayout can be none
TEST_F(BindGroupLayoutValidationTest, BindGroupLayoutVisibilityNone) {
utils::MakeBindGroupLayout(device,
{
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
});
wgpu::BindGroupLayoutEntry entry;
entry.binding = 0;
entry.visibility = wgpu::ShaderStage::None;
entry.buffer.type = wgpu::BufferBindingType::Uniform;
wgpu::BindGroupLayoutDescriptor descriptor;
descriptor.entryCount = 1;
descriptor.entries = &entry;
device.CreateBindGroupLayout(&descriptor);
}
// This test verifies that binding with none visibility in bind group layout can be supported in
// bind group
TEST_F(BindGroupLayoutValidationTest, BindGroupLayoutVisibilityNoneExpectsBindGroupEntry) {
wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform},
{1, wgpu::ShaderStage::None, wgpu::BufferBindingType::Uniform},
});
wgpu::BufferDescriptor descriptor;
descriptor.size = 4;
descriptor.usage = wgpu::BufferUsage::Uniform;
wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
utils::MakeBindGroup(device, bgl, {{0, buffer}, {1, buffer}});
ASSERT_DEVICE_ERROR(utils::MakeBindGroup(device, bgl, {{0, buffer}}));
}
#define BGLEntryType(...) \
utils::BindingLayoutEntryInitializationHelper(0, wgpu::ShaderStage::Compute, __VA_ARGS__)
TEST_F(BindGroupLayoutValidationTest, PerStageLimits) {
struct TestInfo {
uint32_t maxCount;
wgpu::BindGroupLayoutEntry entry;
wgpu::BindGroupLayoutEntry otherEntry;
};
std::array<TestInfo, 7> kTestInfos = {
TestInfo{kMaxSampledTexturesPerShaderStage, BGLEntryType(wgpu::TextureSampleType::Float),
BGLEntryType(wgpu::BufferBindingType::Uniform)},
TestInfo{kMaxSamplersPerShaderStage, BGLEntryType(wgpu::SamplerBindingType::Filtering),
BGLEntryType(wgpu::BufferBindingType::Uniform)},
TestInfo{kMaxSamplersPerShaderStage, BGLEntryType(wgpu::SamplerBindingType::Comparison),
BGLEntryType(wgpu::BufferBindingType::Uniform)},
TestInfo{kMaxStorageBuffersPerShaderStage, BGLEntryType(wgpu::BufferBindingType::Storage),
BGLEntryType(wgpu::BufferBindingType::Uniform)},
TestInfo{
kMaxStorageTexturesPerShaderStage,
BGLEntryType(wgpu::StorageTextureAccess::WriteOnly, wgpu::TextureFormat::RGBA8Unorm),
BGLEntryType(wgpu::BufferBindingType::Uniform)},
TestInfo{kMaxUniformBuffersPerShaderStage, BGLEntryType(wgpu::BufferBindingType::Uniform),
BGLEntryType(wgpu::TextureSampleType::Float)},
// External textures use multiple bindings (3 sampled textures, 1 sampler, 1 uniform buffer)
// that count towards the per stage binding limits. The number of external textures are
// currently restricted by the maximum number of sampled textures.
TestInfo{kMaxSampledTexturesPerShaderStage / kSampledTexturesPerExternalTexture,
BGLEntryType(&utils::kExternalTextureBindingLayout),
BGLEntryType(wgpu::BufferBindingType::Uniform)}};
for (TestInfo info : kTestInfos) {
wgpu::BindGroupLayout bgl[2];
std::vector<utils::BindingLayoutEntryInitializationHelper> maxBindings;
for (uint32_t i = 0; i < info.maxCount; ++i) {
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.binding = i;
maxBindings.push_back(entry);
}
// Creating with the maxes works.
bgl[0] = MakeBindGroupLayout(maxBindings.data(), maxBindings.size());
// Adding an extra binding of a different type works.
{
std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
wgpu::BindGroupLayoutEntry entry = info.otherEntry;
entry.binding = info.maxCount;
bindings.push_back(entry);
MakeBindGroupLayout(bindings.data(), bindings.size());
}
// Adding an extra binding of the maxed type in a different stage works
{
std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.binding = info.maxCount;
entry.visibility = wgpu::ShaderStage::Fragment;
bindings.push_back(entry);
MakeBindGroupLayout(bindings.data(), bindings.size());
}
// Adding an extra binding of the maxed type and stage exceeds the per stage limit.
{
std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.binding = info.maxCount;
bindings.push_back(entry);
ASSERT_DEVICE_ERROR(MakeBindGroupLayout(bindings.data(), bindings.size()));
}
// Creating a pipeline layout from the valid BGL works.
TestCreatePipelineLayout(bgl, 1, true);
// Adding an extra binding of a different type in a different BGL works
bgl[1] = utils::MakeBindGroupLayout(device, {info.otherEntry});
TestCreatePipelineLayout(bgl, 2, true);
{
// Adding an extra binding of the maxed type in a different stage works
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.visibility = wgpu::ShaderStage::Fragment;
bgl[1] = utils::MakeBindGroupLayout(device, {entry});
TestCreatePipelineLayout(bgl, 2, true);
}
// Adding an extra binding of the maxed type in a different BGL exceeds the per stage limit.
bgl[1] = utils::MakeBindGroupLayout(device, {info.entry});
TestCreatePipelineLayout(bgl, 2, false);
}
}
// External textures require multiple binding slots (3 sampled texture, 1 uniform buffer, 1
// sampler), so ensure that these count towards the limit when combined non-external texture
// bindings.
TEST_F(BindGroupLayoutValidationTest, PerStageLimitsWithExternalTexture) {
struct TestInfo {
uint32_t maxCount;
uint32_t bindingsPerExternalTexture;
wgpu::BindGroupLayoutEntry entry;
wgpu::BindGroupLayoutEntry otherEntry;
};
std::array<TestInfo, 3> kTestInfos = {
TestInfo{kMaxSampledTexturesPerShaderStage, kSampledTexturesPerExternalTexture,
BGLEntryType(wgpu::TextureSampleType::Float),
BGLEntryType(wgpu::BufferBindingType::Uniform)},
TestInfo{kMaxSamplersPerShaderStage, kSamplersPerExternalTexture,
BGLEntryType(wgpu::SamplerBindingType::Filtering),
BGLEntryType(wgpu::BufferBindingType::Uniform)},
TestInfo{kMaxUniformBuffersPerShaderStage, kUniformsPerExternalTexture,
BGLEntryType(wgpu::BufferBindingType::Uniform),
BGLEntryType(wgpu::TextureSampleType::Float)},
};
for (TestInfo info : kTestInfos) {
wgpu::BindGroupLayout bgl[2];
std::vector<utils::BindingLayoutEntryInitializationHelper> maxBindings;
// Create an external texture binding layout entry
wgpu::BindGroupLayoutEntry entry = BGLEntryType(&utils::kExternalTextureBindingLayout);
entry.binding = 0;
maxBindings.push_back(entry);
// Create the other bindings such that we reach the max bindings per stage when including
// the external texture.
for (uint32_t i = 1; i <= info.maxCount - info.bindingsPerExternalTexture; ++i) {
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.binding = i;
maxBindings.push_back(entry);
}
// Ensure that creation without the external texture works.
bgl[0] = MakeBindGroupLayout(maxBindings.data(), maxBindings.size());
// Adding an extra binding of a different type works.
{
std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
wgpu::BindGroupLayoutEntry entry = info.otherEntry;
entry.binding = info.maxCount;
bindings.push_back(entry);
MakeBindGroupLayout(bindings.data(), bindings.size());
}
// Adding an extra binding of the maxed type in a different stage works
{
std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.binding = info.maxCount;
entry.visibility = wgpu::ShaderStage::Fragment;
bindings.push_back(entry);
MakeBindGroupLayout(bindings.data(), bindings.size());
}
// Adding an extra binding of the maxed type and stage exceeds the per stage limit.
{
std::vector<utils::BindingLayoutEntryInitializationHelper> bindings = maxBindings;
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.binding = info.maxCount;
bindings.push_back(entry);
ASSERT_DEVICE_ERROR(MakeBindGroupLayout(bindings.data(), bindings.size()));
}
// Creating a pipeline layout from the valid BGL works.
TestCreatePipelineLayout(bgl, 1, true);
// Adding an extra binding of a different type in a different BGL works
bgl[1] = utils::MakeBindGroupLayout(device, {info.otherEntry});
TestCreatePipelineLayout(bgl, 2, true);
{
// Adding an extra binding of the maxed type in a different stage works
wgpu::BindGroupLayoutEntry entry = info.entry;
entry.visibility = wgpu::ShaderStage::Fragment;
bgl[1] = utils::MakeBindGroupLayout(device, {entry});
TestCreatePipelineLayout(bgl, 2, true);
}
// Adding an extra binding of the maxed type in a different BGL exceeds the per stage limit.
bgl[1] = utils::MakeBindGroupLayout(device, {info.entry});
TestCreatePipelineLayout(bgl, 2, false);
}
}
// Check that dynamic buffer numbers exceed maximum value in one bind group layout.
TEST_F(BindGroupLayoutValidationTest, DynamicBufferNumberLimit) {
wgpu::BindGroupLayout bgl[2];
std::vector<wgpu::BindGroupLayoutEntry> maxUniformDB;
std::vector<wgpu::BindGroupLayoutEntry> maxStorageDB;
std::vector<wgpu::BindGroupLayoutEntry> maxReadonlyStorageDB;
// In this test, we use all the same shader stage. Ensure that this does not exceed the
// per-stage limit.
static_assert(kMaxDynamicUniformBuffersPerPipelineLayout <= kMaxUniformBuffersPerShaderStage,
"");
static_assert(kMaxDynamicStorageBuffersPerPipelineLayout <= kMaxStorageBuffersPerShaderStage,
"");
for (uint32_t i = 0; i < kMaxDynamicUniformBuffersPerPipelineLayout; ++i) {
maxUniformDB.push_back(utils::BindingLayoutEntryInitializationHelper(
i, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::Uniform, true));
}
for (uint32_t i = 0; i < kMaxDynamicStorageBuffersPerPipelineLayout; ++i) {
maxStorageDB.push_back(utils::BindingLayoutEntryInitializationHelper(
i, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::Storage, true));
}
for (uint32_t i = 0; i < kMaxDynamicStorageBuffersPerPipelineLayout; ++i) {
maxReadonlyStorageDB.push_back(utils::BindingLayoutEntryInitializationHelper(
i, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::ReadOnlyStorage, true));
}
// Test creating with the maxes works
{
bgl[0] = MakeBindGroupLayout(maxUniformDB.data(), maxUniformDB.size());
TestCreatePipelineLayout(bgl, 1, true);
bgl[0] = MakeBindGroupLayout(maxStorageDB.data(), maxStorageDB.size());
TestCreatePipelineLayout(bgl, 1, true);
bgl[0] = MakeBindGroupLayout(maxReadonlyStorageDB.data(), maxReadonlyStorageDB.size());
TestCreatePipelineLayout(bgl, 1, true);
}
// The following tests exceed the per-pipeline layout limits. We use the Fragment stage to
// ensure we don't hit the per-stage limit.
// Check dynamic uniform buffers exceed maximum in pipeline layout.
{
bgl[0] = MakeBindGroupLayout(maxUniformDB.data(), maxUniformDB.size());
bgl[1] = utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true},
});
TestCreatePipelineLayout(bgl, 2, false);
}
// Check dynamic storage buffers exceed maximum in pipeline layout
{
bgl[0] = MakeBindGroupLayout(maxStorageDB.data(), maxStorageDB.size());
bgl[1] = utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage, true},
});
TestCreatePipelineLayout(bgl, 2, false);
}
// Check dynamic readonly storage buffers exceed maximum in pipeline layout
{
bgl[0] = MakeBindGroupLayout(maxReadonlyStorageDB.data(), maxReadonlyStorageDB.size());
bgl[1] = utils::MakeBindGroupLayout(
device,
{
{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, true},
});
TestCreatePipelineLayout(bgl, 2, false);
}
// Check dynamic storage buffers + dynamic readonly storage buffers exceed maximum storage
// buffers in pipeline layout
{
bgl[0] = MakeBindGroupLayout(maxStorageDB.data(), maxStorageDB.size());
bgl[1] = utils::MakeBindGroupLayout(
device,
{
{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, true},
});
TestCreatePipelineLayout(bgl, 2, false);
}
// Check dynamic uniform buffers exceed maximum in bind group layout.
{
maxUniformDB.push_back(utils::BindingLayoutEntryInitializationHelper(
kMaxDynamicUniformBuffersPerPipelineLayout, wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Uniform, true));
TestCreateBindGroupLayout(maxUniformDB.data(), maxUniformDB.size(), false);
}
// Check dynamic storage buffers exceed maximum in bind group layout.
{
maxStorageDB.push_back(utils::BindingLayoutEntryInitializationHelper(
kMaxDynamicStorageBuffersPerPipelineLayout, wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage, true));
TestCreateBindGroupLayout(maxStorageDB.data(), maxStorageDB.size(), false);
}
// Check dynamic readonly storage buffers exceed maximum in bind group layout.
{
maxReadonlyStorageDB.push_back(utils::BindingLayoutEntryInitializationHelper(
kMaxDynamicStorageBuffersPerPipelineLayout, wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::ReadOnlyStorage, true));
TestCreateBindGroupLayout(maxReadonlyStorageDB.data(), maxReadonlyStorageDB.size(), false);
}
}
// Test that multisampled textures must be 2D sampled textures
TEST_F(BindGroupLayoutValidationTest, MultisampledTextureViewDimension) {
// Multisampled 2D texture works.
utils::MakeBindGroupLayout(device,
{
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2D, true},
});
// Multisampled 2D (defaulted) texture works.
utils::MakeBindGroupLayout(device,
{
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::Undefined, true},
});
// Multisampled 2D array texture is invalid.
ASSERT_DEVICE_ERROR(utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2DArray, true},
}));
// Multisampled cube texture is invalid.
ASSERT_DEVICE_ERROR(utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::Cube, true},
}));
// Multisampled cube array texture is invalid.
ASSERT_DEVICE_ERROR(utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::CubeArray, true},
}));
// Multisampled 3D texture is invalid.
ASSERT_DEVICE_ERROR(utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e3D, true},
}));
// Multisampled 1D texture is invalid.
ASSERT_DEVICE_ERROR(utils::MakeBindGroupLayout(
device, {
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e1D, true},
}));
}
// Test that multisampled texture bindings are valid
TEST_F(BindGroupLayoutValidationTest, MultisampledTextureSampleType) {
// Multisampled float sample type works.
utils::MakeBindGroupLayout(device,
{
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2D, true},
});
// Multisampled uint sample type works.
utils::MakeBindGroupLayout(device,
{
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Uint,
wgpu::TextureViewDimension::e2D, true},
});
// Multisampled sint sample type works.
utils::MakeBindGroupLayout(device,
{
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Sint,
wgpu::TextureViewDimension::e2D, true},
});
// Multisampled depth sample type works.
utils::MakeBindGroupLayout(device,
{
{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Depth,
wgpu::TextureViewDimension::e2D, true},
});
}
constexpr uint32_t kBindingSize = 9;
class SetBindGroupValidationTest : public ValidationTest {
public:
void SetUp() override {
ValidationTest::SetUp();
mBindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Uniform, true},
{1, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Uniform, false},
{2, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage, true},
{3, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::ReadOnlyStorage, true}});
mMinUniformBufferOffsetAlignment =
GetSupportedLimits().limits.minUniformBufferOffsetAlignment;
mBufferSize = 3 * mMinUniformBufferOffsetAlignment + 8;
}
wgpu::Buffer CreateBuffer(uint64_t bufferSize, wgpu::BufferUsage usage) {
wgpu::BufferDescriptor bufferDescriptor;
bufferDescriptor.size = bufferSize;
bufferDescriptor.usage = usage;
return device.CreateBuffer(&bufferDescriptor);
}
wgpu::BindGroupLayout mBindGroupLayout;
wgpu::RenderPipeline CreateRenderPipeline() {
wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
[[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
return vec4<f32>();
})");
wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, R"(
struct S {
value : vec2<f32>;
};
[[group(0), binding(0)]] var<uniform> uBufferDynamic : S;
[[group(0), binding(1)]] var<uniform> uBuffer : S;
[[group(0), binding(2)]] var<storage, read_write> sBufferDynamic : S;
[[group(0), binding(3)]] var<storage, read> sReadonlyBufferDynamic : S;
[[stage(fragment)]] fn main() {
})");
utils::ComboRenderPipelineDescriptor pipelineDescriptor;
pipelineDescriptor.vertex.module = vsModule;
pipelineDescriptor.cFragment.module = fsModule;
pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
wgpu::PipelineLayout pipelineLayout =
utils::MakeBasicPipelineLayout(device, &mBindGroupLayout);
pipelineDescriptor.layout = pipelineLayout;
return device.CreateRenderPipeline(&pipelineDescriptor);
}
wgpu::ComputePipeline CreateComputePipeline() {
wgpu::ShaderModule csModule = utils::CreateShaderModule(device, R"(
struct S {
value : vec2<f32>;
};
[[group(0), binding(0)]] var<uniform> uBufferDynamic : S;
[[group(0), binding(1)]] var<uniform> uBuffer : S;
[[group(0), binding(2)]] var<storage, read_write> sBufferDynamic : S;
[[group(0), binding(3)]] var<storage, read> sReadonlyBufferDynamic : S;
[[stage(compute), workgroup_size(4, 4, 1)]] fn main() {
})");
wgpu::PipelineLayout pipelineLayout =
utils::MakeBasicPipelineLayout(device, &mBindGroupLayout);
wgpu::ComputePipelineDescriptor csDesc;
csDesc.layout = pipelineLayout;
csDesc.compute.module = csModule;
csDesc.compute.entryPoint = "main";
return device.CreateComputePipeline(&csDesc);
}
void TestRenderPassBindGroup(wgpu::BindGroup bindGroup,
uint32_t* offsets,
uint32_t count,
bool expectation) {
wgpu::RenderPipeline renderPipeline = CreateRenderPipeline();
DummyRenderPass renderPass(device);
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
renderPassEncoder.SetPipeline(renderPipeline);
if (bindGroup != nullptr) {
renderPassEncoder.SetBindGroup(0, bindGroup, count, offsets);
}
renderPassEncoder.Draw(3);
renderPassEncoder.EndPass();
if (!expectation) {
ASSERT_DEVICE_ERROR(commandEncoder.Finish());
} else {
commandEncoder.Finish();
}
}
void TestComputePassBindGroup(wgpu::BindGroup bindGroup,
uint32_t* offsets,
uint32_t count,
bool expectation) {
wgpu::ComputePipeline computePipeline = CreateComputePipeline();
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder computePassEncoder = commandEncoder.BeginComputePass();
computePassEncoder.SetPipeline(computePipeline);
if (bindGroup != nullptr) {
computePassEncoder.SetBindGroup(0, bindGroup, count, offsets);
}
computePassEncoder.Dispatch(1);
computePassEncoder.EndPass();
if (!expectation) {
ASSERT_DEVICE_ERROR(commandEncoder.Finish());
} else {
commandEncoder.Finish();
}
}
protected:
uint32_t mMinUniformBufferOffsetAlignment;
uint64_t mBufferSize;
};
// This is the test case that should work.
TEST_F(SetBindGroupValidationTest, Basic) {
// Set up the bind group.
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
std::array<uint32_t, 3> offsets = {512, 256, 0};
TestRenderPassBindGroup(bindGroup, offsets.data(), 3, true);
TestComputePassBindGroup(bindGroup, offsets.data(), 3, true);
}
// Draw/dispatch with a bind group missing is invalid
TEST_F(SetBindGroupValidationTest, MissingBindGroup) {
TestRenderPassBindGroup(nullptr, nullptr, 0, false);
TestComputePassBindGroup(nullptr, nullptr, 0, false);
}
// Setting bind group after a draw / dispatch should re-verify the layout is compatible
TEST_F(SetBindGroupValidationTest, VerifyGroupIfChangedAfterAction) {
// Set up the bind group
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
std::array<uint32_t, 3> offsets = {512, 256, 0};
// Set up bind group that is incompatible
wgpu::BindGroupLayout invalidLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage}});
wgpu::BindGroup invalidGroup =
utils::MakeBindGroup(device, invalidLayout, {{0, storageBuffer, 0, kBindingSize}});
{
wgpu::ComputePipeline computePipeline = CreateComputePipeline();
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder computePassEncoder = commandEncoder.BeginComputePass();
computePassEncoder.SetPipeline(computePipeline);
computePassEncoder.SetBindGroup(0, bindGroup, 3, offsets.data());
computePassEncoder.Dispatch(1);
computePassEncoder.SetBindGroup(0, invalidGroup, 0, nullptr);
computePassEncoder.Dispatch(1);
computePassEncoder.EndPass();
ASSERT_DEVICE_ERROR(commandEncoder.Finish());
}
{
wgpu::RenderPipeline renderPipeline = CreateRenderPipeline();
DummyRenderPass renderPass(device);
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
renderPassEncoder.SetPipeline(renderPipeline);
renderPassEncoder.SetBindGroup(0, bindGroup, 3, offsets.data());
renderPassEncoder.Draw(3);
renderPassEncoder.SetBindGroup(0, invalidGroup, 0, nullptr);
renderPassEncoder.Draw(3);
renderPassEncoder.EndPass();
ASSERT_DEVICE_ERROR(commandEncoder.Finish());
}
}
// Test cases that test dynamic offsets count mismatch with bind group layout.
TEST_F(SetBindGroupValidationTest, DynamicOffsetsMismatch) {
// Set up bind group.
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
// Number of offsets mismatch.
std::array<uint32_t, 4> mismatchOffsets = {768, 512, 256, 0};
TestRenderPassBindGroup(bindGroup, mismatchOffsets.data(), 1, false);
TestRenderPassBindGroup(bindGroup, mismatchOffsets.data(), 2, false);
TestRenderPassBindGroup(bindGroup, mismatchOffsets.data(), 4, false);
TestComputePassBindGroup(bindGroup, mismatchOffsets.data(), 1, false);
TestComputePassBindGroup(bindGroup, mismatchOffsets.data(), 2, false);
TestComputePassBindGroup(bindGroup, mismatchOffsets.data(), 4, false);
}
// Test cases that test dynamic offsets not aligned
TEST_F(SetBindGroupValidationTest, DynamicOffsetsNotAligned) {
// Set up bind group.
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
// Dynamic offsets are not aligned.
std::array<uint32_t, 3> notAlignedOffsets = {512, 128, 0};
TestRenderPassBindGroup(bindGroup, notAlignedOffsets.data(), 3, false);
TestComputePassBindGroup(bindGroup, notAlignedOffsets.data(), 3, false);
}
// Test cases that test dynamic uniform buffer out of bound situation.
TEST_F(SetBindGroupValidationTest, OffsetOutOfBoundDynamicUniformBuffer) {
// Set up bind group.
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
// Dynamic offset + offset is larger than buffer size.
std::array<uint32_t, 3> overFlowOffsets = {1024, 256, 0};
TestRenderPassBindGroup(bindGroup, overFlowOffsets.data(), 3, false);
TestComputePassBindGroup(bindGroup, overFlowOffsets.data(), 3, false);
}
// Test cases that test dynamic storage buffer out of bound situation.
TEST_F(SetBindGroupValidationTest, OffsetOutOfBoundDynamicStorageBuffer) {
// Set up bind group.
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
// Dynamic offset + offset is larger than buffer size.
std::array<uint32_t, 3> overFlowOffsets = {0, 256, 1024};
TestRenderPassBindGroup(bindGroup, overFlowOffsets.data(), 3, false);
TestComputePassBindGroup(bindGroup, overFlowOffsets.data(), 3, false);
}
// Test cases that test dynamic uniform buffer out of bound situation because of binding size.
TEST_F(SetBindGroupValidationTest, BindingSizeOutOfBoundDynamicUniformBuffer) {
// Set up bind group, but binding size is larger than
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
// Dynamic offset + offset isn't larger than buffer size.
// But with binding size, it will trigger OOB error.
std::array<uint32_t, 3> offsets = {768, 256, 0};
TestRenderPassBindGroup(bindGroup, offsets.data(), 3, false);
TestComputePassBindGroup(bindGroup, offsets.data(), 3, false);
}
// Test cases that test dynamic storage buffer out of bound situation because of binding size.
TEST_F(SetBindGroupValidationTest, BindingSizeOutOfBoundDynamicStorageBuffer) {
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::Buffer readonlyStorageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mBindGroupLayout,
{{0, uniformBuffer, 0, kBindingSize},
{1, uniformBuffer, 0, kBindingSize},
{2, storageBuffer, 0, kBindingSize},
{3, readonlyStorageBuffer, 0, kBindingSize}});
// Dynamic offset + offset isn't larger than buffer size.
// But with binding size, it will trigger OOB error.
std::array<uint32_t, 3> offsets = {0, 256, 768};
TestRenderPassBindGroup(bindGroup, offsets.data(), 3, false);
TestComputePassBindGroup(bindGroup, offsets.data(), 3, false);
}
// Regression test for crbug.com/dawn/408 where dynamic offsets were applied in the wrong order.
// Dynamic offsets should be applied in increasing order of binding number.
TEST_F(SetBindGroupValidationTest, DynamicOffsetOrder) {
// Note: The order of the binding numbers of the bind group and bind group layout are
// intentionally different and not in increasing order.
// This test uses both storage and uniform buffers to ensure buffer bindings are sorted first by
// binding number before type.
wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
device, {
{3, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::ReadOnlyStorage, true},
{0, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::ReadOnlyStorage, true},
{2, wgpu::ShaderStage::Compute, wgpu::BufferBindingType::Uniform, true},
});
// Create buffers which are 3x, 2x, and 1x the size of the minimum buffer offset, plus 4 bytes
// to spare (to avoid zero-sized bindings). We will offset the bindings so they reach the very
// end of the buffer. Any mismatch applying too-large of an offset to a smaller buffer will hit
// the out-of-bounds condition during validation.
wgpu::Buffer buffer3x =
CreateBuffer(3 * mMinUniformBufferOffsetAlignment + 4, wgpu::BufferUsage::Storage);
wgpu::Buffer buffer2x =
CreateBuffer(2 * mMinUniformBufferOffsetAlignment + 4, wgpu::BufferUsage::Storage);
wgpu::Buffer buffer1x =
CreateBuffer(1 * mMinUniformBufferOffsetAlignment + 4, wgpu::BufferUsage::Uniform);
wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, bgl,
{
{0, buffer3x, 0, 4},
{3, buffer2x, 0, 4},
{2, buffer1x, 0, 4},
});
std::array<uint32_t, 3> offsets;
{
// Base case works.
offsets = {/* binding 0 */ 0,
/* binding 2 */ 0,
/* binding 3 */ 0};
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder computePassEncoder = commandEncoder.BeginComputePass();
computePassEncoder.SetBindGroup(0, bindGroup, offsets.size(), offsets.data());
computePassEncoder.EndPass();
commandEncoder.Finish();
}
{
// Offset the first binding to touch the end of the buffer. Should succeed.
// Will fail if the offset is applied to the first or second bindings since their buffers
// are too small.
offsets = {/* binding 0 */ 3 * mMinUniformBufferOffsetAlignment,
/* binding 2 */ 0,
/* binding 3 */ 0};
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder computePassEncoder = commandEncoder.BeginComputePass();
computePassEncoder.SetBindGroup(0, bindGroup, offsets.size(), offsets.data());
computePassEncoder.EndPass();
commandEncoder.Finish();
}
{
// Offset the second binding to touch the end of the buffer. Should succeed.
offsets = {/* binding 0 */ 0,
/* binding 2 */ 1 * mMinUniformBufferOffsetAlignment,
/* binding 3 */ 0};
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder computePassEncoder = commandEncoder.BeginComputePass();
computePassEncoder.SetBindGroup(0, bindGroup, offsets.size(), offsets.data());
computePassEncoder.EndPass();
commandEncoder.Finish();
}
{
// Offset the third binding to touch the end of the buffer. Should succeed.
// Will fail if the offset is applied to the second binding since its buffer
// is too small.
offsets = {/* binding 0 */ 0,
/* binding 2 */ 0,
/* binding 3 */ 2 * mMinUniformBufferOffsetAlignment};
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder computePassEncoder = commandEncoder.BeginComputePass();
computePassEncoder.SetBindGroup(0, bindGroup, offsets.size(), offsets.data());
computePassEncoder.EndPass();
commandEncoder.Finish();
}
{
// Offset each binding to touch the end of their buffer. Should succeed.
offsets = {/* binding 0 */ 3 * mMinUniformBufferOffsetAlignment,
/* binding 2 */ 1 * mMinUniformBufferOffsetAlignment,
/* binding 3 */ 2 * mMinUniformBufferOffsetAlignment};
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder computePassEncoder = commandEncoder.BeginComputePass();
computePassEncoder.SetBindGroup(0, bindGroup, offsets.size(), offsets.data());
computePassEncoder.EndPass();
commandEncoder.Finish();
}
}
// Test that an error is produced (and no ASSERTs fired) when using an error bindgroup in
// SetBindGroup
TEST_F(SetBindGroupValidationTest, ErrorBindGroup) {
// Bindgroup creation fails because not all bindings are specified.
wgpu::BindGroup bindGroup;
ASSERT_DEVICE_ERROR(bindGroup = utils::MakeBindGroup(device, mBindGroupLayout, {}));
TestRenderPassBindGroup(bindGroup, nullptr, 0, false);
TestComputePassBindGroup(bindGroup, nullptr, 0, false);
}
class SetBindGroupPersistenceValidationTest : public ValidationTest {
protected:
void SetUp() override {
ValidationTest::SetUp();
mVsModule = utils::CreateShaderModule(device, R"(
[[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
return vec4<f32>();
})");
mBufferSize = 3 * GetSupportedLimits().limits.minUniformBufferOffsetAlignment + 8;
}
wgpu::Buffer CreateBuffer(uint64_t bufferSize, wgpu::BufferUsage usage) {
wgpu::BufferDescriptor bufferDescriptor;
bufferDescriptor.size = bufferSize;
bufferDescriptor.usage = usage;
return device.CreateBuffer(&bufferDescriptor);
}
// Generates bind group layouts and a pipeline from a 2D list of binding types.
std::tuple<std::vector<wgpu::BindGroupLayout>, wgpu::RenderPipeline> SetUpLayoutsAndPipeline(
std::vector<std::vector<wgpu::BufferBindingType>> layouts) {
std::vector<wgpu::BindGroupLayout> bindGroupLayouts(layouts.size());
// Iterate through the desired bind group layouts.
for (uint32_t l = 0; l < layouts.size(); ++l) {
const auto& layout = layouts[l];
std::vector<wgpu::BindGroupLayoutEntry> bindings(layout.size());
// Iterate through binding types and populate a list of BindGroupLayoutEntrys.
for (uint32_t b = 0; b < layout.size(); ++b) {
bindings[b] = utils::BindingLayoutEntryInitializationHelper(
b, wgpu::ShaderStage::Fragment, layout[b]);
}
// Create the bind group layout.
wgpu::BindGroupLayoutDescriptor bglDescriptor;
bglDescriptor.entryCount = static_cast<uint32_t>(bindings.size());
bglDescriptor.entries = bindings.data();
bindGroupLayouts[l] = device.CreateBindGroupLayout(&bglDescriptor);
}
// Create a pipeline layout from the list of bind group layouts.
wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor;
pipelineLayoutDescriptor.bindGroupLayoutCount =
static_cast<uint32_t>(bindGroupLayouts.size());
pipelineLayoutDescriptor.bindGroupLayouts = bindGroupLayouts.data();
wgpu::PipelineLayout pipelineLayout =
device.CreatePipelineLayout(&pipelineLayoutDescriptor);
std::stringstream ss;
ss << "struct S { value : vec2<f32>; };";
// Build a shader which has bindings that match the pipeline layout.
for (uint32_t l = 0; l < layouts.size(); ++l) {
const auto& layout = layouts[l];
for (uint32_t b = 0; b < layout.size(); ++b) {
wgpu::BufferBindingType binding = layout[b];
ss << "[[group(" << l << "), binding(" << b << ")]] ";
switch (binding) {
case wgpu::BufferBindingType::Storage:
ss << "var<storage, read_write> set" << l << "_binding" << b << " : S;";
break;
case wgpu::BufferBindingType::Uniform:
ss << "var<uniform> set" << l << "_binding" << b << " : S;";
break;
default:
UNREACHABLE();
}
}
}
ss << "[[stage(fragment)]] fn main() {}";
wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, ss.str().c_str());
utils::ComboRenderPipelineDescriptor pipelineDescriptor;
pipelineDescriptor.vertex.module = mVsModule;
pipelineDescriptor.cFragment.module = fsModule;
pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
pipelineDescriptor.layout = pipelineLayout;
wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
return std::make_tuple(bindGroupLayouts, pipeline);
}
protected:
uint32_t mBufferSize;
private:
wgpu::ShaderModule mVsModule;
};
// Test it is valid to set bind groups before setting the pipeline.
TEST_F(SetBindGroupPersistenceValidationTest, BindGroupBeforePipeline) {
auto [bindGroupLayouts, pipeline] = SetUpLayoutsAndPipeline({{
{{
wgpu::BufferBindingType::Uniform,
wgpu::BufferBindingType::Uniform,
}},
{{
wgpu::BufferBindingType::Storage,
wgpu::BufferBindingType::Uniform,
}},
}});
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroup0 = utils::MakeBindGroup(
device, bindGroupLayouts[0],
{{0, uniformBuffer, 0, kBindingSize}, {1, uniformBuffer, 0, kBindingSize}});
wgpu::BindGroup bindGroup1 = utils::MakeBindGroup(
device, bindGroupLayouts[1],
{{0, storageBuffer, 0, kBindingSize}, {1, uniformBuffer, 0, kBindingSize}});
DummyRenderPass renderPass(device);
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
renderPassEncoder.SetBindGroup(0, bindGroup0);
renderPassEncoder.SetBindGroup(1, bindGroup1);
renderPassEncoder.SetPipeline(pipeline);
renderPassEncoder.Draw(3);
renderPassEncoder.EndPass();
commandEncoder.Finish();
}
// Dawn does not have a concept of bind group inheritance though the backing APIs may.
// Test that it is valid to draw with bind groups that are not "inherited". They persist
// after a pipeline change.
TEST_F(SetBindGroupPersistenceValidationTest, NotVulkanInheritance) {
auto [bindGroupLayoutsA, pipelineA] = SetUpLayoutsAndPipeline({{
{{
wgpu::BufferBindingType::Uniform,
wgpu::BufferBindingType::Storage,
}},
{{
wgpu::BufferBindingType::Uniform,
wgpu::BufferBindingType::Uniform,
}},
}});
auto [bindGroupLayoutsB, pipelineB] = SetUpLayoutsAndPipeline({{
{{
wgpu::BufferBindingType::Storage,
wgpu::BufferBindingType::Uniform,
}},
{{
wgpu::BufferBindingType::Uniform,
wgpu::BufferBindingType::Uniform,
}},
}});
wgpu::Buffer uniformBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
wgpu::Buffer storageBuffer = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
wgpu::BindGroup bindGroupA0 = utils::MakeBindGroup(
device, bindGroupLayoutsA[0],
{{0, uniformBuffer, 0, kBindingSize}, {1, storageBuffer, 0, kBindingSize}});
wgpu::BindGroup bindGroupA1 = utils::MakeBindGroup(
device, bindGroupLayoutsA[1],
{{0, uniformBuffer, 0, kBindingSize}, {1, uniformBuffer, 0, kBindingSize}});
wgpu::BindGroup bindGroupB0 = utils::MakeBindGroup(
device, bindGroupLayoutsB[0],
{{0, storageBuffer, 0, kBindingSize}, {1, uniformBuffer, 0, kBindingSize}});
DummyRenderPass renderPass(device);
wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
renderPassEncoder.SetPipeline(pipelineA);
renderPassEncoder.SetBindGroup(0, bindGroupA0);
renderPassEncoder.SetBindGroup(1, bindGroupA1);
renderPassEncoder.Draw(3);
renderPassEncoder.SetPipeline(pipelineB);
renderPassEncoder.SetBindGroup(0, bindGroupB0);
// This draw is valid.
// Bind group 1 persists even though it is not "inherited".
renderPassEncoder.Draw(3);
renderPassEncoder.EndPass();
commandEncoder.Finish();
}
class BindGroupLayoutCompatibilityTest : public ValidationTest {
public:
wgpu::Buffer CreateBuffer(uint64_t bufferSize, wgpu::BufferUsage usage) {
wgpu::BufferDescriptor bufferDescriptor;
bufferDescriptor.size = bufferSize;
bufferDescriptor.usage = usage;
return device.CreateBuffer(&bufferDescriptor);
}
wgpu::RenderPipeline CreateFSRenderPipeline(
const char* fsShader,
std::vector<wgpu::BindGroupLayout> bindGroupLayout) {
wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
[[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
return vec4<f32>();
})");
wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, fsShader);
wgpu::PipelineLayoutDescriptor descriptor;
descriptor.bindGroupLayoutCount = bindGroupLayout.size();
descriptor.bindGroupLayouts = bindGroupLayout.data();
utils::ComboRenderPipelineDescriptor pipelineDescriptor;
pipelineDescriptor.vertex.module = vsModule;
pipelineDescriptor.cFragment.module = fsModule;
pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
wgpu::PipelineLayout pipelineLayout = device.CreatePipelineLayout(&descriptor);
pipelineDescriptor.layout = pipelineLayout;
return device.CreateRenderPipeline(&pipelineDescriptor);
}
wgpu::RenderPipeline CreateRenderPipeline(std::vector<wgpu::BindGroupLayout> bindGroupLayouts) {
return CreateFSRenderPipeline(R"(
struct S {
value : vec2<f32>;
};
[[group(0), binding(0)]] var<storage, read_write> sBufferDynamic : S;
[[group(1), binding(0)]] var<storage, read> sReadonlyBufferDynamic : S;
[[stage(fragment)]] fn main() {
var val : vec2<f32> = sBufferDynamic.value;
val = sReadonlyBufferDynamic.value;
})",
std::move(bindGroupLayouts));
}
wgpu::ComputePipeline CreateComputePipeline(
const char* shader,
std::vector<wgpu::BindGroupLayout> bindGroupLayout) {
wgpu::ShaderModule csModule = utils::CreateShaderModule(device, shader);
wgpu::PipelineLayoutDescriptor descriptor;
descriptor.bindGroupLayoutCount = bindGroupLayout.size();
descriptor.bindGroupLayouts = bindGroupLayout.data();
wgpu::PipelineLayout pipelineLayout = device.CreatePipelineLayout(&descriptor);
wgpu::ComputePipelineDescriptor csDesc;
csDesc.layout = pipelineLayout;
csDesc.compute.module = csModule;
csDesc.compute.entryPoint = "main";
return device.CreateComputePipeline(&csDesc);
}
wgpu::ComputePipeline CreateComputePipeline(
std::vector<wgpu::BindGroupLayout> bindGroupLayouts) {
return CreateComputePipeline(R"(
struct S {
value : vec2<f32>;
};
[[group(0), binding(0)]] var<storage, read_write> sBufferDynamic : S;
[[group(1), binding(0)]] var<storage, read> sReadonlyBufferDynamic : S;
[[stage(compute), workgroup_size(4, 4, 1)]] fn main() {
var val : vec2<f32> = sBufferDynamic.value;
val = sReadonlyBufferDynamic.value;
})",
std::move(bindGroupLayouts));
}
};
// Test that it is valid to pass a writable storage buffer in the pipeline layout when the shader
// uses the binding as a readonly storage buffer.
TEST_F(BindGroupLayoutCompatibilityTest, RWStorageInBGLWithROStorageInShader) {
// Set up the bind group layout.
wgpu::BindGroupLayout bgl0 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage}});
wgpu::BindGroupLayout bgl1 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage}});
CreateRenderPipeline({bgl0, bgl1});
CreateComputePipeline({bgl0, bgl1});
}
// Test that it is invalid to pass a readonly storage buffer in the pipeline layout when the shader
// uses the binding as a writable storage buffer.
TEST_F(BindGroupLayoutCompatibilityTest, ROStorageInBGLWithRWStorageInShader) {
// Set up the bind group layout.
wgpu::BindGroupLayout bgl0 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::ReadOnlyStorage}});
wgpu::BindGroupLayout bgl1 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::ReadOnlyStorage}});
ASSERT_DEVICE_ERROR(CreateRenderPipeline({bgl0, bgl1}));
ASSERT_DEVICE_ERROR(CreateComputePipeline({bgl0, bgl1}));
}
TEST_F(BindGroupLayoutCompatibilityTest, TextureViewDimension) {
constexpr char kTexture2DShaderFS[] = R"(
[[group(0), binding(0)]] var myTexture : texture_2d<f32>;
[[stage(fragment)]] fn main() {
textureDimensions(myTexture);
})";
constexpr char kTexture2DShaderCS[] = R"(
[[group(0), binding(0)]] var myTexture : texture_2d<f32>;
[[stage(compute), workgroup_size(1)]] fn main() {
textureDimensions(myTexture);
})";
// Render: Test that 2D texture with 2D view dimension works
CreateFSRenderPipeline(
kTexture2DShaderFS,
{utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2D}})});
// Render: Test that 2D texture with 2D array view dimension is invalid
ASSERT_DEVICE_ERROR(CreateFSRenderPipeline(
kTexture2DShaderFS,
{utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2DArray}})}));
// Compute: Test that 2D texture with 2D view dimension works
CreateComputePipeline(
kTexture2DShaderCS,
{utils::MakeBindGroupLayout(device,
{{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2D}})});
// Compute: Test that 2D texture with 2D array view dimension is invalid
ASSERT_DEVICE_ERROR(CreateComputePipeline(
kTexture2DShaderCS,
{utils::MakeBindGroupLayout(device,
{{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2DArray}})}));
constexpr char kTexture2DArrayShaderFS[] = R"(
[[group(0), binding(0)]] var myTexture : texture_2d_array<f32>;
[[stage(fragment)]] fn main() {
textureDimensions(myTexture);
})";
constexpr char kTexture2DArrayShaderCS[] = R"(
[[group(0), binding(0)]] var myTexture : texture_2d_array<f32>;
[[stage(compute), workgroup_size(1)]] fn main() {
textureDimensions(myTexture);
})";
// Render: Test that 2D texture array with 2D array view dimension works
CreateFSRenderPipeline(
kTexture2DArrayShaderFS,
{utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2DArray}})});
// Render: Test that 2D texture array with 2D view dimension is invalid
ASSERT_DEVICE_ERROR(CreateFSRenderPipeline(
kTexture2DArrayShaderFS,
{utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2D}})}));
// Compute: Test that 2D texture array with 2D array view dimension works
CreateComputePipeline(
kTexture2DArrayShaderCS,
{utils::MakeBindGroupLayout(device,
{{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2DArray}})});
// Compute: Test that 2D texture array with 2D view dimension is invalid
ASSERT_DEVICE_ERROR(CreateComputePipeline(
kTexture2DArrayShaderCS,
{utils::MakeBindGroupLayout(device,
{{0, wgpu::ShaderStage::Compute, wgpu::TextureSampleType::Float,
wgpu::TextureViewDimension::e2D}})}));
}
// Test that a bgl with an external texture is compatible with texture_external in a shader and that
// an error is returned when the binding in the shader does not match.
TEST_F(BindGroupLayoutCompatibilityTest, ExternalTextureBindGroupLayoutCompatibility) {
wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, &utils::kExternalTextureBindingLayout}});
// Test that an external texture binding works with a texture_external in the shader.
CreateFSRenderPipeline(R"(
[[group(0), binding(0)]] var myExternalTexture: texture_external;
[[stage(fragment)]] fn main() {
_ = myExternalTexture;
})",
{bgl});
// Test that an external texture binding doesn't work with a texture_2d<f32> in the shader.
ASSERT_DEVICE_ERROR(CreateFSRenderPipeline(R"(
[[group(0), binding(0)]] var myTexture: texture_2d<f32>;
[[stage(fragment)]] fn main() {
_ = myTexture;
})",
{bgl}));
}
class BindingsValidationTest : public BindGroupLayoutCompatibilityTest {
public:
void SetUp() override {
BindGroupLayoutCompatibilityTest::SetUp();
mBufferSize = 3 * GetSupportedLimits().limits.minUniformBufferOffsetAlignment + 8;
}
void TestRenderPassBindings(const wgpu::BindGroup* bg,
uint32_t count,
wgpu::RenderPipeline pipeline,
bool expectation) {
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
DummyRenderPass dummyRenderPass(device);
wgpu::RenderPassEncoder rp = encoder.BeginRenderPass(&dummyRenderPass);
for (uint32_t i = 0; i < count; ++i) {
rp.SetBindGroup(i, bg[i]);
}
rp.SetPipeline(pipeline);
rp.Draw(3);
rp.EndPass();
if (!expectation) {
ASSERT_DEVICE_ERROR(encoder.Finish());
} else {
encoder.Finish();
}
}
void TestComputePassBindings(const wgpu::BindGroup* bg,
uint32_t count,
wgpu::ComputePipeline pipeline,
bool expectation) {
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder cp = encoder.BeginComputePass();
for (uint32_t i = 0; i < count; ++i) {
cp.SetBindGroup(i, bg[i]);
}
cp.SetPipeline(pipeline);
cp.Dispatch(1);
cp.EndPass();
if (!expectation) {
ASSERT_DEVICE_ERROR(encoder.Finish());
} else {
encoder.Finish();
}
}
uint32_t mBufferSize;
static constexpr uint32_t kBindingNum = 3;
};
// Test that it is valid to set a pipeline layout with bindings unused by the pipeline.
TEST_F(BindingsValidationTest, PipelineLayoutWithMoreBindingsThanPipeline) {
// Set up bind group layouts.
wgpu::BindGroupLayout bgl0 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage},
{1, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Uniform}});
wgpu::BindGroupLayout bgl1 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::ReadOnlyStorage}});
wgpu::BindGroupLayout bgl2 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage}});
// pipelineLayout has unused binding set (bgl2) and unused entry in a binding set (bgl0).
CreateRenderPipeline({bgl0, bgl1, bgl2});
CreateComputePipeline({bgl0, bgl1, bgl2});
}
// Test that it is invalid to set a pipeline layout that doesn't have all necessary bindings
// required by the pipeline.
TEST_F(BindingsValidationTest, PipelineLayoutWithLessBindingsThanPipeline) {
// Set up bind group layout.
wgpu::BindGroupLayout bgl0 = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage}});
// missing a binding set (bgl1) in pipeline layout
{
ASSERT_DEVICE_ERROR(CreateRenderPipeline({bgl0}));
ASSERT_DEVICE_ERROR(CreateComputePipeline({bgl0}));
}
// bgl1 is not missing, but it is empty
{
wgpu::BindGroupLayout bgl1 = utils::MakeBindGroupLayout(device, {});
ASSERT_DEVICE_ERROR(CreateRenderPipeline({bgl0, bgl1}));
ASSERT_DEVICE_ERROR(CreateComputePipeline({bgl0, bgl1}));
}
// bgl1 is neither missing nor empty, but it doesn't contain the necessary binding
{
wgpu::BindGroupLayout bgl1 = utils::MakeBindGroupLayout(
device, {{1, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Uniform}});
ASSERT_DEVICE_ERROR(CreateRenderPipeline({bgl0, bgl1}));
ASSERT_DEVICE_ERROR(CreateComputePipeline({bgl0, bgl1}));
}
}
// Test that it is valid to set bind groups whose layout is not set in the pipeline layout.
// But it's invalid to set extra entry for a given bind group's layout if that layout is set in
// the pipeline layout.
TEST_F(BindingsValidationTest, BindGroupsWithMoreBindingsThanPipelineLayout) {
// Set up bind group layouts, buffers, bind groups, pipeline layouts and pipelines.
std::array<wgpu::BindGroupLayout, kBindingNum + 1> bgl;
std::array<wgpu::BindGroup, kBindingNum + 1> bg;
std::array<wgpu::Buffer, kBindingNum + 1> buffer;
for (uint32_t i = 0; i < kBindingNum + 1; ++i) {
bgl[i] = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage}});
buffer[i] = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
bg[i] = utils::MakeBindGroup(device, bgl[i], {{0, buffer[i]}});
}
// Set 3 bindings (and 3 pipeline layouts) in pipeline.
wgpu::RenderPipeline renderPipeline = CreateRenderPipeline({bgl[0], bgl[1], bgl[2]});
wgpu::ComputePipeline computePipeline = CreateComputePipeline({bgl[0], bgl[1], bgl[2]});
// Comprared to pipeline layout, there is an extra bind group (bg[3])
TestRenderPassBindings(bg.data(), kBindingNum + 1, renderPipeline, true);
TestComputePassBindings(bg.data(), kBindingNum + 1, computePipeline, true);
// If a bind group has entry (like bgl1_1 below) unused by the pipeline layout, it is invalid.
// Bind groups associated layout should exactly match bind group layout if that layout is
// set in pipeline layout.
bgl[1] = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::ReadOnlyStorage},
{1, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Uniform}});
buffer[1] = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage | wgpu::BufferUsage::Uniform);
bg[1] = utils::MakeBindGroup(device, bgl[1], {{0, buffer[1]}, {1, buffer[1]}});
TestRenderPassBindings(bg.data(), kBindingNum, renderPipeline, false);
TestComputePassBindings(bg.data(), kBindingNum, computePipeline, false);
}
// Test that it is invalid to set bind groups that don't have all necessary bindings required
// by the pipeline layout. Note that both pipeline layout and bind group have enough bindings for
// pipeline in the following test.
TEST_F(BindingsValidationTest, BindGroupsWithLessBindingsThanPipelineLayout) {
// Set up bind group layouts, buffers, bind groups, pipeline layouts and pipelines.
std::array<wgpu::BindGroupLayout, kBindingNum> bgl;
std::array<wgpu::BindGroup, kBindingNum> bg;
std::array<wgpu::Buffer, kBindingNum> buffer;
for (uint32_t i = 0; i < kBindingNum; ++i) {
bgl[i] = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Storage}});
buffer[i] = CreateBuffer(mBufferSize, wgpu::BufferUsage::Storage);
bg[i] = utils::MakeBindGroup(device, bgl[i], {{0, buffer[i]}});
}
wgpu::RenderPipeline renderPipeline = CreateRenderPipeline({bgl[0], bgl[1], bgl[2]});
wgpu::ComputePipeline computePipeline = CreateComputePipeline({bgl[0], bgl[1], bgl[2]});
// Compared to pipeline layout, a binding set (bgl2) related bind group is missing
TestRenderPassBindings(bg.data(), kBindingNum - 1, renderPipeline, false);
TestComputePassBindings(bg.data(), kBindingNum - 1, computePipeline, false);
// bgl[2] related bind group is not missing, but its bind group is empty
bgl[2] = utils::MakeBindGroupLayout(device, {});
bg[2] = utils::MakeBindGroup(device, bgl[2], {});
TestRenderPassBindings(bg.data(), kBindingNum, renderPipeline, false);
TestComputePassBindings(bg.data(), kBindingNum, computePipeline, false);
// bgl[2] related bind group is neither missing nor empty, but it doesn't contain the necessary
// binding
bgl[2] = utils::MakeBindGroupLayout(
device, {{1, wgpu::ShaderStage::Compute | wgpu::ShaderStage::Fragment,
wgpu::BufferBindingType::Uniform}});
buffer[2] = CreateBuffer(mBufferSize, wgpu::BufferUsage::Uniform);
bg[2] = utils::MakeBindGroup(device, bgl[2], {{1, buffer[2]}});
TestRenderPassBindings(bg.data(), kBindingNum, renderPipeline, false);
TestComputePassBindings(bg.data(), kBindingNum, computePipeline, false);
}
class SamplerTypeBindingTest : public ValidationTest {
protected:
wgpu::RenderPipeline CreateFragmentPipeline(wgpu::BindGroupLayout* bindGroupLayout,
const char* fragmentSource) {
wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
[[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
return vec4<f32>();
})");
wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, fragmentSource);
utils::ComboRenderPipelineDescriptor pipelineDescriptor;
pipelineDescriptor.vertex.module = vsModule;
pipelineDescriptor.cFragment.module = fsModule;
pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
wgpu::PipelineLayout pipelineLayout =
utils::MakeBasicPipelineLayout(device, bindGroupLayout);
pipelineDescriptor.layout = pipelineLayout;
return device.CreateRenderPipeline(&pipelineDescriptor);
}
};
// Test that the use of sampler and comparison_sampler in the shader must match the bind group
// layout.
TEST_F(SamplerTypeBindingTest, ShaderAndBGLMatches) {
// Test that a filtering sampler binding works with normal sampler in the shader.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[stage(fragment)]] fn main() {
_ = mySampler;
})");
}
// Test that a non-filtering sampler binding works with normal sampler in the shader.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::NonFiltering}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[stage(fragment)]] fn main() {
_ = mySampler;
})");
}
// Test that comparison sampler binding works with comparison sampler in the shader.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler_comparison;
[[stage(fragment)]] fn main() {
_ = mySampler;
})");
}
// Test that filtering sampler binding does not work with comparison sampler in the shader.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
ASSERT_DEVICE_ERROR(CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler_comparison;
[[stage(fragment)]] fn main() {
_ = mySampler;
})"));
}
// Test that non-filtering sampler binding does not work with comparison sampler in the shader.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::NonFiltering}});
ASSERT_DEVICE_ERROR(CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler_comparison;
[[stage(fragment)]] fn main() {
_ = mySampler;
})"));
}
// Test that comparison sampler binding does not work with normal sampler in the shader.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison}});
ASSERT_DEVICE_ERROR(CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[stage(fragment)]] fn main() {
_ = mySampler;
})"));
}
// Test that a filtering sampler can be used to sample a float texture.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering},
{1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[group(0), binding(1)]] var myTexture: texture_2d<f32>;
[[stage(fragment)]] fn main() {
textureSample(myTexture, mySampler, vec2<f32>(0.0, 0.0));
})");
}
// Test that a non-filtering sampler can be used to sample a float texture.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::NonFiltering},
{1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[group(0), binding(1)]] var myTexture: texture_2d<f32>;
[[stage(fragment)]] fn main() {
textureSample(myTexture, mySampler, vec2<f32>(0.0, 0.0));
})");
}
// Test that a filtering sampler can be used to sample a depth texture.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering},
{1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[group(0), binding(1)]] var myTexture: texture_depth_2d;
[[stage(fragment)]] fn main() {
textureSample(myTexture, mySampler, vec2<f32>(0.0, 0.0));
})");
}
// Test that a non-filtering sampler can be used to sample a depth texture.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::NonFiltering},
{1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[group(0), binding(1)]] var myTexture: texture_depth_2d;
[[stage(fragment)]] fn main() {
textureSample(myTexture, mySampler, vec2<f32>(0.0, 0.0));
})");
}
// Test that a comparison sampler can be used to sample a depth texture.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison},
{1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler_comparison;
[[group(0), binding(1)]] var myTexture: texture_depth_2d;
[[stage(fragment)]] fn main() {
textureSampleCompare(myTexture, mySampler, vec2<f32>(0.0, 0.0), 0.0);
})");
}
// Test that a filtering sampler cannot be used to sample an unfilterable-float texture.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering},
{1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::UnfilterableFloat}});
ASSERT_DEVICE_ERROR(CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[group(0), binding(1)]] var myTexture: texture_2d<f32>;
[[stage(fragment)]] fn main() {
textureSample(myTexture, mySampler, vec2<f32>(0.0, 0.0));
})"));
}
// Test that a non-filtering sampler can be used to sample an unfilterable-float texture.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::NonFiltering},
{1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::UnfilterableFloat}});
CreateFragmentPipeline(&bindGroupLayout, R"(
[[group(0), binding(0)]] var mySampler: sampler;
[[group(0), binding(1)]] var myTexture: texture_2d<f32>;
[[stage(fragment)]] fn main() {
textureSample(myTexture, mySampler, vec2<f32>(0.0, 0.0));
})");
}
}
TEST_F(SamplerTypeBindingTest, SamplerAndBindGroupMatches) {
// Test that sampler binding works with normal sampler.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler()}});
}
// Test that comparison sampler binding works with sampler w/ compare function.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison}});
wgpu::SamplerDescriptor desc = {};
desc.compare = wgpu::CompareFunction::Never;
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}});
}
// Test that sampler binding does not work with sampler w/ compare function.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
wgpu::SamplerDescriptor desc;
desc.compare = wgpu::CompareFunction::Never;
ASSERT_DEVICE_ERROR(
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}}));
}
// Test that comparison sampler binding does not work with normal sampler.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison}});
wgpu::SamplerDescriptor desc = {};
ASSERT_DEVICE_ERROR(
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}}));
}
// Test that filtering sampler binding works with a filtering or non-filtering sampler.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering}});
// Test each filter member
{
wgpu::SamplerDescriptor desc;
desc.minFilter = wgpu::FilterMode::Linear;
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}});
}
{
wgpu::SamplerDescriptor desc;
desc.magFilter = wgpu::FilterMode::Linear;
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}});
}
{
wgpu::SamplerDescriptor desc;
desc.mipmapFilter = wgpu::FilterMode::Linear;
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}});
}
// Test non-filtering sampler
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler()}});
}
// Test that non-filtering sampler binding does not work with a filtering sampler.
{
wgpu::BindGroupLayout bindGroupLayout = utils::MakeBindGroupLayout(
device, {{0, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::NonFiltering}});
// Test each filter member
{
wgpu::SamplerDescriptor desc;
desc.minFilter = wgpu::FilterMode::Linear;
ASSERT_DEVICE_ERROR(
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}}));
}
{
wgpu::SamplerDescriptor desc;
desc.magFilter = wgpu::FilterMode::Linear;
ASSERT_DEVICE_ERROR(
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}}));
}
{
wgpu::SamplerDescriptor desc;
desc.mipmapFilter = wgpu::FilterMode::Linear;
ASSERT_DEVICE_ERROR(
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler(&desc)}}));
}
// Test non-filtering sampler
utils::MakeBindGroup(device, bindGroupLayout, {{0, device.CreateSampler()}});
}
}