Skip to content

Commit 18183dc

Browse files
Validate setImmediates API in different encoder types (#4515)
* Validate setImmediates API in different encoder types This PR adding validation tests to cover setImmediates API in different encoder types (compute pass, render pass, render bundle) by covering: * Interpretation: - Passing a TypedArray the data offset and size is not given in elements. * Alignment: - rangeOffset is not a multiple of 4 bytes. - content size, converted to bytes, is not a multiple of 4 bytes. * Arithmetic overflow - rangeOffset + contentSize is overflow * Bounds: - dataOffset + size (in bytes) exceeds the content data size. - rangeOffset + size (in bytes) exceeds the maxImmdiateSize. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Avoid allocation issue * Enhance oob cases * Address comments * Fix grammar issue * Address review comments: refactor setImmediates validation tests * Add SetImmediatesTest class to check feature support * Add check for immediate status * fix checks * Address comments * Address comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a660547 commit 18183dc

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
export const description = `
2+
setImmediates validation tests.
3+
TODO(#4297): enable Float16Array
4+
`;
5+
6+
import { makeTestGroup } from '../../../../../common/framework/test_group.js';
7+
import { getGPU } from '../../../../../common/util/navigator_gpu.js';
8+
import {
9+
kTypedArrayBufferViews,
10+
kTypedArrayBufferViewKeys,
11+
} from '../../../../../common/util/util.js';
12+
import { AllFeaturesMaxLimitsGPUTest } from '../../../../gpu_test.js';
13+
import { kProgrammableEncoderTypes } from '../../../../util/command_buffer_maker.js';
14+
15+
class SetImmediatesTest extends AllFeaturesMaxLimitsGPUTest {
16+
override async init() {
17+
await super.init();
18+
if (
19+
!('setImmediates' in GPURenderPassEncoder.prototype) &&
20+
!('setImmediates' in GPUComputePassEncoder.prototype) &&
21+
!('setImmediates' in GPURenderBundleEncoder.prototype) &&
22+
!('maxImmediateSize' in GPUSupportedLimits.prototype) &&
23+
!getGPU(this.rec).wgslLanguageFeatures.has('immediate_address_space')
24+
) {
25+
this.skip('setImmediates not supported');
26+
}
27+
}
28+
}
29+
30+
export const g = makeTestGroup(SetImmediatesTest);
31+
32+
g.test('alignment')
33+
.desc('Tests that rangeOffset and contentSize must align to 4 bytes.')
34+
.params(u =>
35+
u //
36+
.combine('encoderType', kProgrammableEncoderTypes)
37+
.combine('arrayType', kTypedArrayBufferViewKeys)
38+
.filter(p => p.arrayType !== 'Float16Array')
39+
.combineWithParams([
40+
// control case: rangeOffset 4 is aligned. contentByteSize 8 is aligned.
41+
{ rangeOffset: 4, contentByteSize: 8 },
42+
// rangeOffset 6 is unaligned (6 % 4 !== 0).
43+
{ rangeOffset: 6, contentByteSize: 8 },
44+
// contentByteSize 10 is unaligned (10 % 4 !== 0).
45+
// Note: This case will be skipped for types with element size > 2 (e.g. Uint32, Uint64)
46+
// because they cannot form a 10-byte array.
47+
{ rangeOffset: 4, contentByteSize: 10 },
48+
])
49+
.filter(({ arrayType, contentByteSize }) => {
50+
// Skip if the contentByteSize is not a multiple of the element size.
51+
// For example, we can't have 10 bytes if the element size is 4 or 8 bytes.
52+
const arrayConstructor = kTypedArrayBufferViews[arrayType];
53+
return contentByteSize % arrayConstructor.BYTES_PER_ELEMENT === 0;
54+
})
55+
)
56+
.fn(t => {
57+
const { encoderType, arrayType, rangeOffset, contentByteSize } = t.params;
58+
const arrayBufferType = kTypedArrayBufferViews[arrayType];
59+
const elementSize = arrayBufferType.BYTES_PER_ELEMENT;
60+
const elementCount = contentByteSize / elementSize;
61+
62+
const isRangeOffsetAligned = rangeOffset % 4 === 0;
63+
const isContentSizeAligned = contentByteSize % 4 === 0;
64+
65+
const { encoder, validateFinish } = t.createEncoder(encoderType);
66+
const data = new arrayBufferType(elementCount);
67+
68+
t.shouldThrow(isContentSizeAligned ? false : 'RangeError', () => {
69+
// Cast to any to avoid Float16Array issues
70+
// MAINTENANCE_TODO: remove this cast when the types are updated.
71+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
72+
(encoder as any).setImmediates(rangeOffset, data as any, 0, elementCount);
73+
});
74+
75+
validateFinish(isRangeOffsetAligned);
76+
});
77+
78+
g.test('overflow')
79+
.desc(
80+
`
81+
Tests that rangeOffset + contentSize or dataOffset + size is handled correctly if it exceeds limits.
82+
`
83+
)
84+
.params(u =>
85+
u //
86+
.combine('encoderType', kProgrammableEncoderTypes)
87+
.combine('arrayType', kTypedArrayBufferViewKeys)
88+
.filter(p => p.arrayType !== 'Float16Array')
89+
.combineWithParams([
90+
// control case
91+
{ rangeOffset: 0, dataOffset: 0, elementCount: 4, _expectedError: null },
92+
// elementCount 0
93+
{ rangeOffset: 0, dataOffset: 0, elementCount: 0, _expectedError: null },
94+
// rangeOffset + contentSize overflows
95+
{
96+
rangeOffset: 2 ** 31 - 8,
97+
dataOffset: 0,
98+
elementCount: 4,
99+
_expectedError: 'validation',
100+
},
101+
{
102+
rangeOffset: 2 ** 32 - 8,
103+
dataOffset: 0,
104+
elementCount: 4,
105+
_expectedError: 'validation',
106+
},
107+
// dataOffset + size overflows
108+
{
109+
rangeOffset: 0,
110+
dataOffset: 2 ** 31 - 1,
111+
elementCount: 4,
112+
_expectedError: 'RangeError',
113+
},
114+
{
115+
rangeOffset: 0,
116+
dataOffset: 2 ** 32 - 1,
117+
elementCount: 4,
118+
_expectedError: 'RangeError',
119+
},
120+
])
121+
)
122+
.fn(t => {
123+
const { encoderType, arrayType, rangeOffset, dataOffset, elementCount, _expectedError } =
124+
t.params;
125+
const arrayBufferType = kTypedArrayBufferViews[arrayType];
126+
127+
const { encoder, validateFinish } = t.createEncoder(encoderType);
128+
const data = new arrayBufferType(elementCount);
129+
130+
const doSetImmediates = () => {
131+
// Cast to any to avoid Float16Array issues
132+
// MAINTENANCE_TODO: remove this cast when the types are updated.
133+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
134+
(encoder as any).setImmediates(rangeOffset, data as any, dataOffset, elementCount);
135+
};
136+
137+
if (_expectedError === 'RangeError') {
138+
t.shouldThrow('RangeError', doSetImmediates);
139+
} else {
140+
doSetImmediates();
141+
validateFinish(_expectedError === null);
142+
}
143+
});
144+
145+
g.test('out_of_bounds')
146+
.desc(
147+
`
148+
Tests that rangeOffset + contentSize is greater than maxImmediateSize (Validation Error)
149+
and contentSize is larger than data size (RangeError).
150+
`
151+
)
152+
.params(u =>
153+
u //
154+
.combine('encoderType', kProgrammableEncoderTypes)
155+
.combine('arrayType', kTypedArrayBufferViewKeys)
156+
.filter(p => p.arrayType !== 'Float16Array')
157+
.combineWithParams([
158+
// control case
159+
{ rangeOffsetDelta: 0, dataLengthDelta: 0 },
160+
// rangeOffset + contentSize > maxImmediateSize
161+
{ rangeOffsetDelta: 4, dataLengthDelta: 0 },
162+
// dataOffset + size > data.length
163+
{ rangeOffsetDelta: 0, dataLengthDelta: -1 },
164+
])
165+
)
166+
.fn(t => {
167+
const { encoderType, arrayType, rangeOffsetDelta, dataLengthDelta } = t.params;
168+
const arrayBufferType = kTypedArrayBufferViews[arrayType];
169+
const elementSize = arrayBufferType.BYTES_PER_ELEMENT;
170+
171+
// MAINTENANCE_TODO: remove this cast when the types are updated.
172+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
173+
const maxImmediateSize = (t.device.limits as any).maxImmediateSize;
174+
if (maxImmediateSize === undefined) {
175+
t.skip('maxImmediateSize not found');
176+
}
177+
178+
// We want contentByteSize to be aligned to 4 bytes to avoid alignment errors.
179+
// We use 8 bytes to cover all types including BigUint64 (8 bytes).
180+
const elementCount = elementSize >= 8 ? 1 : 8 / elementSize;
181+
const contentByteSize = elementCount * elementSize;
182+
183+
const rangeOffset = maxImmediateSize - contentByteSize + rangeOffsetDelta;
184+
const dataLength = elementCount + dataLengthDelta;
185+
186+
const data = new arrayBufferType(dataLength);
187+
188+
const { encoder, validateFinish } = t.createEncoder(encoderType);
189+
190+
const rangeOverLimit = rangeOffset + contentByteSize > maxImmediateSize;
191+
const dataOverLimit = elementCount > dataLength;
192+
193+
t.shouldThrow(dataOverLimit ? 'RangeError' : false, () => {
194+
// MAINTENANCE_TODO: remove this cast when the types are updated.
195+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
196+
(encoder as any).setImmediates(rangeOffset, data as any, 0, elementCount);
197+
});
198+
199+
if (!dataOverLimit) {
200+
validateFinish(!rangeOverLimit);
201+
}
202+
});

0 commit comments

Comments
 (0)