Source code

Revision control

Copy as Markdown

Other Tools

export const description = `
Stress tests covering robustness when available VRAM is exhausted.
`;
import { makeTestGroup } from '../../common/framework/test_group.js';
import { unreachable } from '../../common/util/util.js';
import { GPUConst } from '../../webgpu/constants.js';
import { GPUTest } from '../../webgpu/gpu_test.js';
import { exhaustVramUntilUnder64MB } from '../../webgpu/util/memory.js';
export const g = makeTestGroup(GPUTest);
function createBufferWithMapState(
t: GPUTest,
size: number,
mapState: GPUBufferMapState,
mode: GPUMapModeFlags,
mappedAtCreation: boolean
) {
const mappable = mapState === 'unmapped';
if (!mappable && !mappedAtCreation) {
return t.createBufferTracked({
size,
usage: GPUBufferUsage.UNIFORM,
mappedAtCreation,
});
}
let buffer: GPUBuffer;
switch (mode) {
case GPUMapMode.READ:
buffer = t.createBufferTracked({
size,
usage: GPUBufferUsage.MAP_READ,
mappedAtCreation,
});
break;
case GPUMapMode.WRITE:
buffer = t.createBufferTracked({
size,
usage: GPUBufferUsage.MAP_WRITE,
mappedAtCreation,
});
break;
default:
unreachable();
}
// If we want the buffer to be mappable and also mappedAtCreation, we call unmap on it now.
if (mappable && mappedAtCreation) {
buffer.unmap();
}
return buffer;
}
g.test('vram_oom')
.desc(`Tests that we can allocate buffers until we run out of VRAM.`)
.fn(async t => {
await exhaustVramUntilUnder64MB(t);
});
g.test('map_after_vram_oom')
.desc(
`Allocates tons of buffers and textures with varying mapping states (unmappable,
mappable, mapAtCreation, mapAtCreation-then-unmapped) until OOM; then attempts
to mapAsync all the mappable objects. The last buffer should be an error buffer so
mapAsync on it should reject and produce a validation error. `
)
.params(u =>
u
.combine('mapState', ['mapped', 'unmapped'] as GPUBufferMapState[])
.combine('mode', [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE])
.combine('mappedAtCreation', [true, false])
.combine('unmapBeforeResolve', [true, false])
)
.fn(async t => {
// Use a relatively large size to quickly hit OOM.
const kSize = 512 * 1024 * 1024;
const { mapState, mode, mappedAtCreation, unmapBeforeResolve } = t.params;
const mappable = mapState === 'unmapped';
const buffers: GPUBuffer[] = [];
// Closure to call map and verify results on all of the buffers.
const finish = async () => {
if (mappable) {
await Promise.all(buffers.map(value => value.mapAsync(mode)));
} else {
buffers.forEach(value => {
t.expectValidationError(() => {
void value.mapAsync(mode);
});
});
}
// Finally, destroy all the buffers to free the resources.
buffers.forEach(buffer => buffer.destroy());
};
let errorBuffer: GPUBuffer;
for (;;) {
if (mappedAtCreation) {
// When mappedAtCreation is true, OOM can happen on the client which throws a RangeError. In
// this case, we don't do any validations on the OOM buffer.
try {
t.device.pushErrorScope('out-of-memory');
const buffer = createBufferWithMapState(t, kSize, mapState, mode, mappedAtCreation);
if (await t.device.popErrorScope()) {
errorBuffer = buffer;
break;
}
buffers.push(buffer);
} catch (ex) {
t.expect(ex instanceof RangeError);
await finish();
return;
}
} else {
t.device.pushErrorScope('out-of-memory');
const buffer = createBufferWithMapState(t, kSize, mapState, mode, mappedAtCreation);
if (await t.device.popErrorScope()) {
errorBuffer = buffer;
break;
}
buffers.push(buffer);
}
}
// Do some validation on the OOM buffer.
let promise: Promise<void>;
t.expectValidationError(() => {
promise = errorBuffer.mapAsync(mode);
});
if (unmapBeforeResolve) {
// Should reject with abort error because buffer will be unmapped
// before validation check finishes.
t.shouldReject('AbortError', promise!);
} else {
// Should also reject in addition to the validation error.
t.shouldReject('OperationError', promise!);
// Wait for validation error before unmap to ensure validation check
// ends before unmap.
try {
await promise!;
throw new Error('The promise should be rejected.');
} catch {
// Should cause an exception because the promise should be rejected.
}
}
// Should throw an OperationError because the buffer is not mapped.
// Note: not a RangeError because the state of the buffer is checked first.
t.shouldThrow('OperationError', () => {
errorBuffer.getMappedRange();
});
// Should't be a validation error even if the buffer failed to be mapped.
errorBuffer.unmap();
errorBuffer.destroy();
// Finish the rest of the test w.r.t the mappable buffers.
await finish();
});
g.test('validation_vs_oom')
.desc(
`Tests that calls affected by both OOM and validation errors expose the
validation error with precedence.`
)
.unimplemented();
g.test('recovery')
.desc(
`Tests that after going VRAM-OOM, destroying allocated resources eventually
allows new resources to be allocated.`
)
.unimplemented();