blob: 9c75630c895631363c2324798e17a106e1d6d277 [file] [log] [blame]
/**
* AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
**/ export const description = `
Texture Usages Validation Tests in Render Pass and Compute Pass.
TODO: description per test
Test Coverage:
- For each combination of two texture usages:
- For various subresource ranges (different mip levels or array layers) that overlap a given
subresources or not for color formats:
- For various places that resources are used, for example, used in bundle or used in render
pass directly.
- Check that an error is generated when read-write or write-write usages are binding to the
same texture subresource. Otherwise, no error should be generated. One exception is race
condition upon two writeonly-storage-texture usages, which is valid.
- For each combination of two texture usages:
- For various aspects (all, depth-only, stencil-only) that overlap a given subresources or not
for depth/stencil formats:
- Check that an error is generated when read-write or write-write usages are binding to the
same aspect. Otherwise, no error should be generated.
- Test combinations of two shader stages:
- Texture usages in bindings with invisible shader stages should be validated. Invisible shader
stages include shader stage with visibility none, compute shader stage in render pass, and
vertex/fragment shader stage in compute pass.
- Tests replaced bindings:
- Texture usages via bindings replaced by another setBindGroup() upon the same bindGroup index
in render pass should be validated. However, replaced bindings should not be validated in
compute pass.
- Test texture usages in bundle:
- Texture usages in bundle should be validated if that bundle is executed in the current scope.
- Test texture usages with unused bindings:
- Texture usages should be validated even its bindings is not used in pipeline.
- Test texture usages validation scope:
- Texture usages should be validated per each render pass. And they should be validated per each
dispatch call in compute.
`;
import { pbool, poptions, params } from '../../../../../common/framework/params_builder.js';
import { pp } from '../../../../../common/framework/preprocessor.js';
import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import { assert } from '../../../../../common/framework/util/util.js';
import {
kDepthStencilFormats,
kDepthStencilFormatInfo,
kTextureBindingTypes,
kTextureBindingTypeInfo,
kShaderStages,
} from '../../../../capability_info.js';
import { GPUConst } from '../../../../constants.js';
import { ValidationTest } from '../../validation_test.js';
const SIZE = 32;
class TextureUsageTracking extends ValidationTest {
createTexture(options = {}) {
const {
width = SIZE,
height = SIZE,
arrayLayerCount = 1,
mipLevelCount = 1,
sampleCount = 1,
format = 'rgba8unorm',
usage = GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.SAMPLED,
} = options;
return this.device.createTexture({
size: { width, height, depth: arrayLayerCount },
mipLevelCount,
sampleCount,
dimension: '2d',
format,
usage,
});
}
createBindGroup(index, view, bindingType, dimension, bindingTexFormat) {
return this.device.createBindGroup({
entries: [{ binding: index, resource: view }],
layout: this.device.createBindGroupLayout({
entries: [
{
binding: index,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
type: bindingType,
viewDimension: dimension,
storageTextureFormat: bindingTexFormat,
},
],
}),
});
}
createAndExecuteBundle(index, bindGroup, pass) {
const bundleEncoder = this.device.createRenderBundleEncoder({
colorFormats: ['rgba8unorm'],
});
bundleEncoder.setBindGroup(index, bindGroup);
const bundle = bundleEncoder.finish();
pass.executeBundles([bundle]);
}
beginSimpleRenderPass(encoder, view) {
return encoder.beginRenderPass({
colorAttachments: [
{
attachment: view,
loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
storeOp: 'store',
},
],
});
}
testValidationScope(compute) {
// Create two bind groups. Resource usages conflict between these two bind groups. But resource
// usage inside each bind group doesn't conflict.
const view = this.createTexture({
usage: GPUTextureUsage.STORAGE | GPUTextureUsage.SAMPLED,
}).createView();
const bindGroup0 = this.createBindGroup(0, view, 'sampled-texture', '2d', undefined);
const bindGroup1 = this.createBindGroup(
0,
view,
'writeonly-storage-texture',
'2d',
'rgba8unorm'
);
const encoder = this.device.createCommandEncoder();
const pass = compute
? encoder.beginComputePass()
: this.beginSimpleRenderPass(encoder, this.createTexture().createView());
// Create pipeline. Note that bindings unused in pipeline should be validated too.
const pipeline = compute ? this.createNoOpComputePipeline() : this.createNoOpRenderPipeline();
return {
bindGroup0,
bindGroup1,
encoder,
pass,
pipeline,
};
}
setPipeline(pass, pipeline, compute) {
if (compute) {
pass.setPipeline(pipeline);
} else {
pass.setPipeline(pipeline);
}
}
issueDrawOrDispatch(pass, compute) {
if (compute) {
pass.dispatch(1);
} else {
pass.draw(3, 1, 0, 0);
}
}
setComputePipelineAndCallDispatch(pass) {
const pipeline = this.createNoOpComputePipeline();
pass.setPipeline(pipeline);
pass.dispatch(1);
}
}
export const g = makeTestGroup(TextureUsageTracking);
const BASE_LEVEL = 1;
const TOTAL_LEVELS = 6;
const BASE_LAYER = 1;
const TOTAL_LAYERS = 6;
const SLICE_COUNT = 2;
// For all tests below, we test compute pass if 'compute' is true, and test render pass otherwise.
g.test('subresources_and_binding_types_combination_for_color')
.params(
params()
.combine(pbool('compute'))
.combine(pbool('binding0InBundle'))
.combine(pbool('binding1InBundle'))
.combine([
// Two texture usages are binding to the same texture subresource.
{
levelCount0: 1,
layerCount0: 1,
baseLevel1: BASE_LEVEL,
levelCount1: 1,
baseLayer1: BASE_LAYER,
layerCount1: 1,
_resourceSuccess: false,
},
// Two texture usages are binding to different mip levels of the same texture.
{
levelCount0: 1,
layerCount0: 1,
baseLevel1: BASE_LEVEL + 1,
levelCount1: 1,
baseLayer1: BASE_LAYER,
layerCount1: 1,
_resourceSuccess: true,
},
// Two texture usages are binding to different array layers of the same texture.
{
levelCount0: 1,
layerCount0: 1,
baseLevel1: BASE_LEVEL,
levelCount1: 1,
baseLayer1: BASE_LAYER + 1,
layerCount1: 1,
_resourceSuccess: true,
},
// The second texture usage contains the whole mip chain where the first texture usage is using.
{
levelCount0: 1,
layerCount0: 1,
baseLevel1: 0,
levelCount1: TOTAL_LEVELS,
baseLayer1: BASE_LAYER,
layerCount1: 1,
_resourceSuccess: false,
},
// The second texture usage contains all layers where the first texture usage is using.
{
levelCount0: 1,
layerCount0: 1,
baseLevel1: BASE_LEVEL,
levelCount1: 1,
baseLayer1: 0,
layerCount1: TOTAL_LAYERS,
_resourceSuccess: false,
},
// The second texture usage contains all subresources where the first texture usage is using.
{
levelCount0: 1,
layerCount0: 1,
baseLevel1: 0,
levelCount1: TOTAL_LEVELS,
baseLayer1: 0,
layerCount1: TOTAL_LAYERS,
_resourceSuccess: false,
},
// Both of the two usages access a few mip levels on the same layer but they don't overlap.
{
levelCount0: SLICE_COUNT,
layerCount0: 1,
baseLevel1: BASE_LEVEL + SLICE_COUNT,
levelCount1: 3,
baseLayer1: BASE_LAYER,
layerCount1: 1,
_resourceSuccess: true,
},
// Both of the two usages access a few mip levels on the same layer and they overlap.
{
levelCount0: SLICE_COUNT,
layerCount0: 1,
baseLevel1: BASE_LEVEL + SLICE_COUNT - 1,
levelCount1: 3,
baseLayer1: BASE_LAYER,
layerCount1: 1,
_resourceSuccess: false,
},
// Both of the two usages access a few array layers on the same level but they don't overlap.
{
levelCount0: 1,
layerCount0: SLICE_COUNT,
baseLevel1: BASE_LEVEL,
levelCount1: 1,
baseLayer1: BASE_LAYER + SLICE_COUNT,
layerCount1: 3,
_resourceSuccess: true,
},
// Both of the two usages access a few array layers on the same level and they overlap.
{
levelCount0: 1,
layerCount0: SLICE_COUNT,
baseLevel1: BASE_LEVEL,
levelCount1: 1,
baseLayer1: BASE_LAYER + SLICE_COUNT - 1,
layerCount1: 3,
_resourceSuccess: false,
},
// Both of the two usages access a few array layers and mip levels but they don't overlap.
{
levelCount0: SLICE_COUNT,
layerCount0: SLICE_COUNT,
baseLevel1: BASE_LEVEL + SLICE_COUNT,
levelCount1: 3,
baseLayer1: BASE_LAYER + SLICE_COUNT,
layerCount1: 3,
_resourceSuccess: true,
},
// Both of the two usages access a few array layers and mip levels and they overlap.
{
levelCount0: SLICE_COUNT,
layerCount0: SLICE_COUNT,
baseLevel1: BASE_LEVEL + SLICE_COUNT - 1,
levelCount1: 3,
baseLayer1: BASE_LAYER + SLICE_COUNT - 1,
layerCount1: 3,
_resourceSuccess: false,
},
])
.combine([
{
type0: 'sampled-texture',
type1: 'sampled-texture',
_usageSuccess: true,
},
{
type0: 'sampled-texture',
type1: 'readonly-storage-texture',
_usageSuccess: true,
},
{
type0: 'sampled-texture',
type1: 'writeonly-storage-texture',
_usageSuccess: false,
},
{
type0: 'sampled-texture',
type1: 'render-target',
_usageSuccess: false,
},
{
type0: 'readonly-storage-texture',
type1: 'readonly-storage-texture',
_usageSuccess: true,
},
{
type0: 'readonly-storage-texture',
type1: 'writeonly-storage-texture',
_usageSuccess: false,
},
{
type0: 'readonly-storage-texture',
type1: 'render-target',
_usageSuccess: false,
},
// Race condition upon multiple writable storage texture is valid.
{
type0: 'writeonly-storage-texture',
type1: 'writeonly-storage-texture',
_usageSuccess: true,
},
{
type0: 'writeonly-storage-texture',
type1: 'render-target',
_usageSuccess: false,
},
{
type0: 'render-target',
type1: 'render-target',
_usageSuccess: false,
},
])
// Every color attachment can use only one single subresource.
.unless(
p =>
(p.type0 === 'render-target' && (p.levelCount0 !== 1 || p.layerCount0 !== 1)) ||
(p.type1 === 'render-target' && (p.levelCount1 !== 1 || p.layerCount1 !== 1))
)
// All color attachments' size should be the same.
.unless(
p =>
p.type0 === 'render-target' && p.type1 === 'render-target' && p.baseLevel1 !== BASE_LEVEL
)
.unless(
p =>
// We can't set 'render-target' in bundle, so we need to exclude it from bundle.
(p.binding0InBundle && p.type0 === 'render-target') ||
(p.binding1InBundle && p.type1 === 'render-target')
)
.unless(
p =>
// We can't set 'render-target' or bundle in compute.
p.compute &&
(p.binding0InBundle ||
p.binding1InBundle ||
p.type0 === 'render-target' ||
p.type1 === 'render-target')
)
)
.fn(async t => {
const {
compute,
binding0InBundle,
binding1InBundle,
levelCount0,
layerCount0,
baseLevel1,
baseLayer1,
levelCount1,
layerCount1,
type0,
type1,
_usageSuccess,
_resourceSuccess,
} = t.params;
const texture = t.createTexture({
arrayLayerCount: TOTAL_LAYERS,
mipLevelCount: TOTAL_LEVELS,
usage: GPUTextureUsage.SAMPLED | GPUTextureUsage.STORAGE | GPUTextureUsage.OUTPUT_ATTACHMENT,
});
const dimension0 = layerCount0 !== 1 ? '2d-array' : '2d';
const view0 = texture.createView({
dimension: dimension0,
baseMipLevel: BASE_LEVEL,
mipLevelCount: levelCount0,
baseArrayLayer: BASE_LAYER,
arrayLayerCount: layerCount0,
});
const dimension1 = layerCount1 !== 1 ? '2d-array' : '2d';
const view1 = texture.createView({
dimension: dimension1,
baseMipLevel: baseLevel1,
mipLevelCount: levelCount1,
baseArrayLayer: baseLayer1,
arrayLayerCount: layerCount1,
});
const encoder = t.device.createCommandEncoder();
if (type0 === 'render-target') {
// Note that type1 is 'render-target' too. So we don't need to create bindings.
assert(type1 === 'render-target');
const pass = encoder.beginRenderPass({
colorAttachments: [
{
attachment: view0,
loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
storeOp: 'store',
},
{
attachment: view1,
loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
storeOp: 'store',
},
],
});
pass.endPass();
} else {
const pass = compute
? encoder.beginComputePass()
: t.beginSimpleRenderPass(
encoder,
type1 === 'render-target' ? view1 : t.createTexture().createView()
);
// Create bind groups. Set bind groups in pass directly or set bind groups in bundle.
const storageTextureFormat0 = type0 === 'sampled-texture' ? undefined : 'rgba8unorm';
const bindGroup0 = t.createBindGroup(0, view0, type0, dimension0, storageTextureFormat0);
if (binding0InBundle) {
assert(pass instanceof GPURenderPassEncoder);
t.createAndExecuteBundle(0, bindGroup0, pass);
} else {
pass.setBindGroup(0, bindGroup0);
}
if (type1 !== 'render-target') {
const storageTextureFormat1 = type1 === 'sampled-texture' ? undefined : 'rgba8unorm';
const bindGroup1 = t.createBindGroup(1, view1, type1, dimension1, storageTextureFormat1);
if (binding1InBundle) {
assert(pass instanceof GPURenderPassEncoder);
t.createAndExecuteBundle(1, bindGroup1, pass);
} else {
pass.setBindGroup(1, bindGroup1);
}
}
if (compute) t.setComputePipelineAndCallDispatch(pass);
pass.endPass();
}
const success = _resourceSuccess || _usageSuccess;
t.expectValidationError(() => {
encoder.finish();
}, !success);
});
g.test('subresources_and_binding_types_combination_for_aspect')
.params(
params()
.combine(pbool('compute'))
.combine(pbool('binding0InBundle'))
.combine(pbool('binding1InBundle'))
.combine(poptions('format', kDepthStencilFormats))
.combine([
{
baseLevel: BASE_LEVEL,
baseLayer: BASE_LAYER,
_resourceSuccess: false,
},
{
baseLevel: BASE_LEVEL + 1,
baseLayer: BASE_LAYER,
_resourceSuccess: true,
},
{
baseLevel: BASE_LEVEL,
baseLayer: BASE_LAYER + 1,
_resourceSuccess: true,
},
])
.combine(poptions('aspect0', ['all', 'depth-only', 'stencil-only']))
.combine(poptions('aspect1', ['all', 'depth-only', 'stencil-only']))
.unless(
p =>
(p.aspect0 === 'stencil-only' && !kDepthStencilFormatInfo[p.format].stencil) ||
(p.aspect1 === 'stencil-only' && !kDepthStencilFormatInfo[p.format].stencil)
)
.unless(
p =>
(p.aspect0 === 'depth-only' && !kDepthStencilFormatInfo[p.format].depth) ||
(p.aspect1 === 'depth-only' && !kDepthStencilFormatInfo[p.format].depth)
)
.combine([
{
type0: 'sampled-texture',
type1: 'sampled-texture',
_usageSuccess: true,
},
{
type0: 'sampled-texture',
type1: 'render-target',
_usageSuccess: false,
},
])
.unless(
p =>
// We can't set 'render-target' in bundle, so we need to exclude it from bundle.
p.binding1InBundle && p.type1 === 'render-target'
)
.unless(
p =>
// We can't set 'render-target' or bundle in compute. Note that type0 is definitely not
// 'render-target'
p.compute && (p.binding0InBundle || p.binding1InBundle || p.type1 === 'render-target')
)
)
.fn(async t => {
const {
compute,
binding0InBundle,
binding1InBundle,
format,
baseLevel,
baseLayer,
aspect0,
aspect1,
type0,
type1,
_resourceSuccess,
_usageSuccess,
} = t.params;
const texture = t.createTexture({
arrayLayerCount: TOTAL_LAYERS,
mipLevelCount: TOTAL_LEVELS,
format,
});
const view0 = texture.createView({
baseMipLevel: BASE_LEVEL,
mipLevelCount: 1,
baseArrayLayer: BASE_LAYER,
arrayLayerCount: 1,
aspect: aspect0,
});
const view1 = texture.createView({
baseMipLevel: baseLevel,
mipLevelCount: 1,
baseArrayLayer: baseLayer,
arrayLayerCount: 1,
aspect: aspect1,
});
const encoder = t.device.createCommandEncoder();
// Color attachment's size should match depth/stencil attachment's size. Note that if
// type1 !== 'render-target' then there's no depthStencilAttachment to match anyway.
const size = SIZE >> baseLevel;
const pass = compute
? encoder.beginComputePass()
: encoder.beginRenderPass({
colorAttachments: [
{
attachment: t.createTexture({ width: size, height: size }).createView(),
loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
storeOp: 'store',
},
],
depthStencilAttachment:
type1 !== 'render-target'
? undefined
: {
attachment: view1,
depthStoreOp: 'clear',
depthLoadValue: 'load',
stencilStoreOp: 'clear',
stencilLoadValue: 'load',
},
});
// Create bind groups. Set bind groups in pass directly or set bind groups in bundle.
const bindGroup0 = t.createBindGroup(0, view0, type0, '2d', undefined);
if (binding0InBundle) {
assert(pass instanceof GPURenderPassEncoder);
t.createAndExecuteBundle(0, bindGroup0, pass);
} else {
pass.setBindGroup(0, bindGroup0);
}
if (type1 !== 'render-target') {
const bindGroup1 = t.createBindGroup(1, view1, type1, '2d', undefined);
if (binding1InBundle) {
assert(pass instanceof GPURenderPassEncoder);
t.createAndExecuteBundle(1, bindGroup1, pass);
} else {
pass.setBindGroup(1, bindGroup1);
}
}
if (compute) t.setComputePipelineAndCallDispatch(pass);
pass.endPass();
const disjointAspects =
(aspect0 === 'depth-only' && aspect1 === 'stencil-only') ||
(aspect0 === 'stencil-only' && aspect1 === 'depth-only');
// If subresources' mip/array slices has no overlap, or their binding types don't conflict,
// it will definitely success no matter what aspects they are binding to.
const success = disjointAspects || _resourceSuccess || _usageSuccess;
t.expectValidationError(() => {
encoder.finish();
}, !success);
});
g.test('shader_stages_and_visibility')
.params(
params()
.combine(pbool('compute'))
.combine(poptions('readVisibility', [0, ...kShaderStages]))
.combine(poptions('writeVisibility', [0, ...kShaderStages]))
.unless(
p =>
// Writeonly-storage-texture binding type is not supported in vertex stage. But it is the
// only way to write into texture in compute. So there is no means to successfully create
// a binding which attempt to write into stage(s) with vertex stage in compute pass.
p.compute && Boolean(p.writeVisibility & GPUConst.ShaderStage.VERTEX)
)
)
.fn(async t => {
const { compute, readVisibility, writeVisibility } = t.params;
// writeonly-storage-texture binding type is not supported in vertex stage. So, this test
// uses writeonly-storage-texture binding as writable binding upon the same subresource if
// vertex stage is not included. Otherwise, it uses output attachment instead.
const writeHasVertexStage = Boolean(writeVisibility & GPUShaderStage.VERTEX);
const texUsage = writeHasVertexStage
? GPUTextureUsage.SAMPLED | GPUTextureUsage.OUTPUT_ATTACHMENT
: GPUTextureUsage.SAMPLED | GPUTextureUsage.STORAGE;
const texture = t.createTexture({ usage: texUsage });
const view = texture.createView();
const bglEntries = [{ binding: 0, visibility: readVisibility, type: 'sampled-texture' }];
const bgEntries = [{ binding: 0, resource: view }];
if (!writeHasVertexStage) {
bglEntries.push({
binding: 1,
visibility: writeVisibility,
type: 'writeonly-storage-texture',
storageTextureFormat: 'rgba8unorm',
});
bgEntries.push({ binding: 1, resource: view });
}
const bindGroup = t.device.createBindGroup({
entries: bgEntries,
layout: t.device.createBindGroupLayout({ entries: bglEntries }),
});
const encoder = t.device.createCommandEncoder();
const pass = compute
? encoder.beginComputePass()
: t.beginSimpleRenderPass(
encoder,
writeHasVertexStage ? view : t.createTexture().createView()
);
pass.setBindGroup(0, bindGroup);
if (compute) t.setComputePipelineAndCallDispatch(pass);
pass.endPass();
// Texture usages in bindings with invisible shader stages should be validated. Invisible shader
// stages include shader stage with visibility none, compute shader stage in render pass, and
// vertex/fragment shader stage in compute pass.
t.expectValidationError(() => {
encoder.finish();
});
});
// We should validate the texture usages in bindings which are replaced by another setBindGroup()
// call site upon the same index in the same render pass. However, replaced bindings in compute
// should not be validated.
g.test('replaced_binding')
.params(
params()
.combine(pbool('compute'))
.combine(pbool('callDrawOrDispatch'))
.combine(poptions('bindingType', kTextureBindingTypes))
)
.fn(async t => {
const { compute, callDrawOrDispatch, bindingType } = t.params;
const info = kTextureBindingTypeInfo[bindingType];
const bindingTexFormat = info.resource === 'storageTex' ? 'rgba8unorm' : undefined;
const sampledView = t.createTexture().createView();
const sampledStorageView = t
.createTexture({ usage: GPUTextureUsage.STORAGE | GPUTextureUsage.SAMPLED })
.createView();
// Create bindGroup0. It has two bindings. These two bindings use different views/subresources.
const bglEntries0 = [
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, type: 'sampled-texture' },
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
type: bindingType,
storageTextureFormat: bindingTexFormat,
},
];
const bgEntries0 = [
{ binding: 0, resource: sampledView },
{ binding: 1, resource: sampledStorageView },
];
const bindGroup0 = t.device.createBindGroup({
entries: bgEntries0,
layout: t.device.createBindGroupLayout({ entries: bglEntries0 }),
});
// Create bindGroup1. It has one binding, which use the same view/subresoure of a binding in
// bindGroup0. So it may or may not conflicts with that binding in bindGroup0.
const bindGroup1 = t.createBindGroup(0, sampledStorageView, 'sampled-texture', '2d', undefined);
const encoder = t.device.createCommandEncoder();
const pass = compute
? encoder.beginComputePass()
: t.beginSimpleRenderPass(encoder, t.createTexture().createView());
// Set bindGroup0 and bindGroup1. bindGroup0 is replaced by bindGroup1 in the current pass.
// But bindings in bindGroup0 should be validated too.
pass.setBindGroup(0, bindGroup0);
if (callDrawOrDispatch) {
const pipeline = compute ? t.createNoOpComputePipeline() : t.createNoOpRenderPipeline();
t.setPipeline(pass, pipeline, compute);
t.issueDrawOrDispatch(pass, compute);
}
pass.setBindGroup(0, bindGroup1);
pass.endPass();
// TODO: If the Compatible Usage List (https://gpuweb.github.io/gpuweb/#compatible-usage-list)
// gets programmatically defined in capability_info, use it here, instead of this logic, for clarity.
let success = bindingType !== 'writeonly-storage-texture';
// Replaced bindings should not be validated in compute pass, because validation only occurs
// inside dispatch() which only looks at the current resource usages.
success || (success = compute);
t.expectValidationError(() => {
encoder.finish();
}, !success);
});
g.test('bindings_in_bundle')
.params(
params()
.combine(pbool('binding0InBundle'))
.combine(pbool('binding1InBundle'))
.combine(poptions('type0', ['render-target', ...kTextureBindingTypes]))
.combine(poptions('type1', ['render-target', ...kTextureBindingTypes]))
.unless(
p =>
// We can't set 'render-target' in bundle, so we need to exclude it from bundle.
// In addition, if both bindings are non-bundle, there is no need to test it because
// we have far more comprehensive test cases for that situation in this file.
(p.binding0InBundle && p.type0 === 'render-target') ||
(p.binding1InBundle && p.type1 === 'render-target') ||
(!p.binding0InBundle && !p.binding1InBundle)
)
)
.fn(async t => {
const { binding0InBundle, binding1InBundle, type0, type1 } = t.params;
// Two bindings are attached to the same texture view.
const view = t
.createTexture({
usage:
GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.STORAGE | GPUTextureUsage.SAMPLED,
})
.createView();
const bindGroups = [];
if (type0 !== 'render-target') {
const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'rgba8unorm';
bindGroups[0] = t.createBindGroup(0, view, type0, '2d', binding0TexFormat);
}
if (type1 !== 'render-target') {
const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'rgba8unorm';
bindGroups[1] = t.createBindGroup(1, view, type1, '2d', binding1TexFormat);
}
const encoder = t.device.createCommandEncoder();
// At least one binding is in bundle, which means that its type is not 'render-target'.
// As a result, only one binding's type is 'render-target' at most.
const pass = t.beginSimpleRenderPass(
encoder,
type0 === 'render-target' || type1 === 'render-target' ? view : t.createTexture().createView()
);
const bindingsInBundle = [binding0InBundle, binding1InBundle];
for (let i = 0; i < 2; i++) {
// Create a bundle for each bind group if its bindings is required to be in bundle on purpose.
// Otherwise, call setBindGroup directly in pass if needed (when its binding is not
// 'render-target').
if (bindingsInBundle[i]) {
const bundleEncoder = t.device.createRenderBundleEncoder({
colorFormats: ['rgba8unorm'],
});
bundleEncoder.setBindGroup(i, bindGroups[i]);
const bundleInPass = bundleEncoder.finish();
pass.executeBundles([bundleInPass]);
} else if (bindGroups[i] !== undefined) {
pass.setBindGroup(i, bindGroups[i]);
}
}
pass.endPass();
let success = false;
if (
(type0 === 'sampled-texture' || type0 === 'readonly-storage-texture') &&
(type1 === 'sampled-texture' || type1 === 'readonly-storage-texture')
) {
success = true;
}
if (type0 === 'writeonly-storage-texture' && type1 === 'writeonly-storage-texture') {
success = true;
}
// Resource usages in bundle should be validated.
t.expectValidationError(() => {
encoder.finish();
}, !success);
});
g.test('unused_bindings_in_pipeline')
.params(
params()
.combine(pbool('compute'))
.combine(pbool('useBindGroup0'))
.combine(pbool('useBindGroup1'))
.combine(poptions('setBindGroupsOrder', ['common', 'reversed']))
.combine(poptions('setPipeline', ['before', 'middle', 'after', 'none']))
.combine(pbool('callDrawOrDispatch'))
)
.fn(async t => {
const {
compute,
useBindGroup0,
useBindGroup1,
setBindGroupsOrder,
setPipeline,
callDrawOrDispatch,
} = t.params;
const view = t.createTexture({ usage: GPUTextureUsage.STORAGE }).createView();
const bindGroup0 = t.createBindGroup(0, view, 'readonly-storage-texture', '2d', 'rgba8unorm');
const bindGroup1 = t.createBindGroup(0, view, 'writeonly-storage-texture', '2d', 'rgba8unorm');
const wgslVertex = '[[stage(vertex)]] fn main() -> void {}';
// TODO: revisit the shader code once 'image' can be supported in wgsl.
const wgslFragment = pp`
${pp._if(useBindGroup0)}
[[set(0), binding(0)]] var<image> image0 : [[access(read)]] texture_storage_2d<rgba8unorm>;
${pp._endif}
${pp._if(useBindGroup1)}
[[set(1), binding(0)]] var<image> image1 : [[access(read)]] texture_storage_2d<rgba8unorm>;
${pp._endif}
[[stage(fragment)]] fn main() -> void {}
`;
// TODO: revisit the shader code once 'image' can be supported in wgsl.
const wgslCompute = pp`
${pp._if(useBindGroup0)}
[[set(0), binding(0)]] var<image> image0 : [[access(read)]] texture_storage_2d<rgba8unorm>;
${pp._endif}
${pp._if(useBindGroup1)}
[[set(1), binding(0)]] var<image> image1 : [[access(read)]] texture_storage_2d<rgba8unorm>;
${pp._endif}
[[stage(compute)]] fn main() -> void {}
`;
const pipeline = compute
? t.device.createComputePipeline({
computeStage: {
module: t.device.createShaderModule({
code: wgslCompute,
}),
entryPoint: 'main',
},
})
: t.device.createRenderPipeline({
vertexStage: {
module: t.device.createShaderModule({
code: wgslVertex,
}),
entryPoint: 'main',
},
fragmentStage: {
module: t.device.createShaderModule({
code: wgslFragment,
}),
entryPoint: 'main',
},
primitiveTopology: 'triangle-list',
colorStates: [{ format: 'rgba8unorm' }],
});
const encoder = t.device.createCommandEncoder();
const pass = compute
? encoder.beginComputePass()
: encoder.beginRenderPass({
colorAttachments: [
{
attachment: t.createTexture().createView(),
loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
storeOp: 'store',
},
],
});
const index0 = setBindGroupsOrder === 'common' ? 0 : 1;
const index1 = setBindGroupsOrder === 'common' ? 1 : 0;
if (setPipeline === 'before') t.setPipeline(pass, pipeline, compute);
pass.setBindGroup(index0, bindGroup0);
if (setPipeline === 'middle') t.setPipeline(pass, pipeline, compute);
pass.setBindGroup(index1, bindGroup1);
if (setPipeline === 'after') t.setPipeline(pass, pipeline, compute);
if (callDrawOrDispatch) t.issueDrawOrDispatch(pass, compute);
pass.endPass();
// Resource usage validation scope is defined by dispatch calls. If dispatch is not called,
// we don't need to do resource usage validation and no validation error to be reported.
const success = compute && !callDrawOrDispatch;
t.expectValidationError(() => {
encoder.finish();
}, !success);
});
g.test('validation_scope,no_draw_or_dispatch')
.params(pbool('compute'))
.fn(async t => {
const { compute } = t.params;
const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(compute);
t.setPipeline(pass, pipeline, compute);
pass.setBindGroup(0, bindGroup0);
pass.setBindGroup(1, bindGroup1);
pass.endPass();
// Resource usage validation scope is defined by dispatch calls. If dispatch is not called,
// we don't need to do resource usage validation and no validation error to be reported.
t.expectValidationError(() => {
encoder.finish();
}, !compute);
});
g.test('validation_scope,same_draw_or_dispatch')
.params(pbool('compute'))
.fn(async t => {
const { compute } = t.params;
const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(compute);
t.setPipeline(pass, pipeline, compute);
pass.setBindGroup(0, bindGroup0);
pass.setBindGroup(1, bindGroup1);
t.issueDrawOrDispatch(pass, compute);
pass.endPass();
t.expectValidationError(() => {
encoder.finish();
});
});
g.test('validation_scope,different_draws_or_dispatches')
.params(pbool('compute'))
.fn(async t => {
const { compute } = t.params;
const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(compute);
t.setPipeline(pass, pipeline, compute);
pass.setBindGroup(0, bindGroup0);
t.issueDrawOrDispatch(pass, compute);
pass.setBindGroup(1, bindGroup1);
t.issueDrawOrDispatch(pass, compute);
pass.endPass();
// Note that bindGroup0 will be inherited in the second draw/dispatch.
t.expectValidationError(() => {
encoder.finish();
});
});
g.test('validation_scope,different_passes')
.params(pbool('compute'))
.fn(async t => {
const { compute } = t.params;
const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(compute);
t.setPipeline(pass, pipeline, compute);
pass.setBindGroup(0, bindGroup0);
if (compute) t.setComputePipelineAndCallDispatch(pass);
pass.endPass();
const pass1 = compute
? encoder.beginComputePass()
: t.beginSimpleRenderPass(encoder, t.createTexture().createView());
t.setPipeline(pass1, pipeline, compute);
pass1.setBindGroup(1, bindGroup1);
if (compute) t.setComputePipelineAndCallDispatch(pass);
pass1.endPass();
// No validation error.
encoder.finish();
});