Skip to content
194 changes: 194 additions & 0 deletions src/webgpu/api/validation/encoding/cmds/setImmediates.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
export const description = `
setImmediates validation tests.
`;

import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import {
kTypedArrayBufferViews,
kTypedArrayBufferViewKeys,
} from '../../../../../common/util/util.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../../gpu_test.js';
import { kProgrammableEncoderTypes } from '../../../../util/command_buffer_maker.js';

class SetImmediatesTest extends AllFeaturesMaxLimitsGPUTest {
override async init() {
await super.init();
if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!('setImmediates' in (GPURenderPassEncoder.prototype as any)) ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!('setImmediates' in (GPUComputePassEncoder.prototype as any)) ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!('setImmediates' in (GPURenderBundleEncoder.prototype as any))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fully review tomorrow but one comment.

This will run the tests only if all three are available. I would like to run all of the tests if ANY of these, OR the WGSL feature OR the new limit, are available.

Also these checks need to be packaged up into a "skipIf" helper at some point, so they can also be used in other tests that don't use SetImmediatesTest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will run the tests only if all three are available. I would like to run all of the tests if ANY of these, OR the WGSL feature OR the new limit, are available.

A bit concern about this. I think it faill the scenario that implementation is in middle. For example, when implementation added wgslLanguageFeature but not do API implementation, the case failed.

So my thoughts is that the test should check what it tends to check. For example, for this test case, we want to check all encoder API validation, then we need to check the API is available.(Maybe we can do more detail checking for each encoder). And for other case which test immediate state, it requires both API and wgslFeature.

WDYT?

) {
this.skip('setImmediates not supported');
}
}
}

export const g = makeTestGroup(SetImmediatesTest);

g.test('alignment')
.desc('Tests that rangeOffset and contentSize must align to 4 bytes.')
.params(u =>
u //
.combine('encoderType', kProgrammableEncoderTypes)
.combine('arrayType', kTypedArrayBufferViewKeys)
.filter(p => p.arrayType !== 'Float16Array')
.combineWithParams([
// control case: rangeOffset 4 is aligned. contentByteSize 8 is aligned.
{ rangeOffset: 4, contentByteSize: 8 },
// rangeOffset 5 is unaligned (5 % 4 !== 0).
{ rangeOffset: 5, contentByteSize: 8 },
// contentByteSize 10 is unaligned (10 % 4 !== 0).
// Note: This case will be skipped for types with element size > 2 (e.g. Uint32, Uint64)
// because they cannot form a 10-byte array.
{ rangeOffset: 4, contentByteSize: 10 },
])
.filter(({ arrayType, contentByteSize }) => {
// Skip if the contentByteSize is not a multiple of the element size.
// For example, we can't have 10 bytes if the element size is 4 or 8 bytes.
const arrayConstructor = kTypedArrayBufferViews[arrayType];
return contentByteSize % arrayConstructor.BYTES_PER_ELEMENT === 0;
})
)
.fn(t => {
const { encoderType, arrayType, rangeOffset, contentByteSize } = t.params;
const arrayBufferType = kTypedArrayBufferViews[arrayType];
const elementSize = arrayBufferType.BYTES_PER_ELEMENT;
const elementCount = contentByteSize / elementSize;

const isRangeOffsetAligned = rangeOffset % 4 === 0;
const isContentSizeAligned = contentByteSize % 4 === 0;

const { encoder, validateFinish } = t.createEncoder(encoderType);
const data = new arrayBufferType(elementCount);

t.shouldThrow(isContentSizeAligned ? false : 'RangeError', () => {
// Cast to any to avoid Float16Array issues
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(encoder as any).setImmediates(rangeOffset, data as any, 0, elementCount);
});

validateFinish(isRangeOffsetAligned);
});

g.test('overflow')
.desc(
`
Tests that rangeOffset + contentSize or dataOffset + size is handled correctly if it exceeds limits.
`
)
.params(u =>
u //
.combine('encoderType', kProgrammableEncoderTypes)
.combine('arrayType', kTypedArrayBufferViewKeys)
.filter(p => p.arrayType !== 'Float16Array')
.combineWithParams([
// control case
{ rangeOffset: 0, dataOffset: 0, elementCount: 4, _expectedError: null },
// rangeOffset + contentSize overflows
{
rangeOffset: Math.pow(2, 31) - 8,
dataOffset: 0,
elementCount: 4,
_expectedError: 'validation',
},
{
rangeOffset: Math.pow(2, 32) - 8,
dataOffset: 0,
elementCount: 4,
_expectedError: 'validation',
},
// dataOffset + size overflows
{
rangeOffset: 0,
dataOffset: Math.pow(2, 31) - 1,
elementCount: 4,
_expectedError: 'RangeError',
},
{
rangeOffset: 0,
dataOffset: Math.pow(2, 32) - 1,
elementCount: 4,
_expectedError: 'RangeError',
},
])
)
.fn(t => {
const { encoderType, arrayType, rangeOffset, dataOffset, elementCount, _expectedError } =
t.params;
const arrayBufferType = kTypedArrayBufferViews[arrayType];

const { encoder, validateFinish } = t.createEncoder(encoderType);
const data = new arrayBufferType(elementCount);

const doSetImmediates = () => {
// Cast to any to avoid Float16Array issues
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(encoder as any).setImmediates(rangeOffset, data as any, dataOffset, elementCount);
};

if (_expectedError === 'RangeError') {
t.shouldThrow('RangeError', doSetImmediates);
} else {
doSetImmediates();
validateFinish(_expectedError === null);
}
});

g.test('out_of_bounds')
.desc(
`
Tests that rangeOffset + contentSize is greater than maxImmediateSize (Validation Error)
and contentSize is larger than data size (RangeError).
`
)
.params(u =>
u //
.combine('encoderType', kProgrammableEncoderTypes)
.combine('arrayType', kTypedArrayBufferViewKeys)
.filter(p => p.arrayType !== 'Float16Array')
.combineWithParams([
// control case
{ rangeOffsetDelta: 0, dataLengthDelta: 0 },
// rangeOffset + contentSize > maxImmediateSize
{ rangeOffsetDelta: 4, dataLengthDelta: 0 },
// dataOffset + size > data.length
{ rangeOffsetDelta: 0, dataLengthDelta: -1 },
])
)
.fn(t => {
const { encoderType, arrayType, rangeOffsetDelta, dataLengthDelta } = t.params;
const arrayBufferType = kTypedArrayBufferViews[arrayType];
const elementSize = arrayBufferType.BYTES_PER_ELEMENT;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const maxImmediateSize = (t.device.limits as any).maxImmediateSize;
if (maxImmediateSize === undefined) {
t.skip('maxImmediateSize not found');
}

// We want contentByteSize to be aligned to 4 bytes to avoid alignment errors.
const elementCount = elementSize >= 4 ? 1 : 4 / elementSize;
const contentByteSize = elementCount * elementSize;

const rangeOffset = maxImmediateSize - contentByteSize + rangeOffsetDelta;
const dataLength = elementCount + dataLengthDelta;

const data = new arrayBufferType(dataLength);

const { encoder, validateFinish } = t.createEncoder(encoderType);

const rangeOverLimit = rangeOffset + contentByteSize > maxImmediateSize;
const dataOverLimit = elementCount > dataLength;

t.shouldThrow(dataOverLimit ? 'RangeError' : false, () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(encoder as any).setImmediates(rangeOffset, data as any, 0, elementCount);
});

if (!dataOverLimit) {
validateFinish(!rangeOverLimit);
}
});