blob: 05d15d766c45befac8e2d2137da4e8eb546f3f83 [file] [log] [blame]
export const description = `
Validation tests for GPUBuffer.mapAsync, GPUBuffer.unmap and GPUBuffer.getMappedRange.
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { attemptGarbageCollection } from '../../../../common/util/collect_garbage.js';
import { assert, unreachable } from '../../../../common/util/util.js';
import { kBufferUsages } from '../../../capability_info.js';
import { GPUConst } from '../../../constants.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as vtu from '../validation_test_utils.js';
class F extends AllFeaturesMaxLimitsGPUTest {
async testMapAsyncCall(
expectation:
| 'success'
| { validationError: boolean; earlyRejection: boolean; rejectName: string },
buffer: GPUBuffer,
mode: GPUMapModeFlags,
offset?: number,
size?: number
) {
if (expectation === 'success') {
const p = buffer.mapAsync(mode, offset, size);
await p;
} else {
let p: Promise<void>;
this.expectValidationError(() => {
p = buffer.mapAsync(mode, offset, size);
}, expectation.validationError);
let caught = false;
let rejectedEarly = false;
let microtaskBRan = false;
// If mapAsync rejected early, microtask A will run before B.
// If not, B will run before A.
p!.catch(() => {
// Microtask A
caught = true;
});
queueMicrotask(() => {
// Microtask B
rejectedEarly = caught;
microtaskBRan = true;
});
// These handlers should always run after microtasks A and B are both done.
await p!.then(
() => {
unreachable('mapAsync unexpectedly passed');
},
ex => {
const suffix = `\n Rejection: ${ex}`;
this.expect(microtaskBRan, 'scheduling problem?: microtaskB has not run yet' + suffix);
assert(ex instanceof Error, 'mapAsync rejected with non-error' + suffix);
this.expect(typeof ex.stack === 'string', 'mapAsync rejected without a stack' + suffix);
this.expect(
expectation.rejectName === ex.name,
'mapAsync rejected with wrong exception name' + suffix
);
if (expectation.earlyRejection) {
this.expect(rejectedEarly, 'expected early mapAsync rejection, got deferred' + suffix);
} else {
this.expect(!rejectedEarly, 'expected deferred mapAsync rejection, got early' + suffix);
}
}
);
}
}
testGetMappedRangeCall(success: boolean, buffer: GPUBuffer, offset?: number, size?: number) {
if (success) {
const data = buffer.getMappedRange(offset, size);
this.expect(data instanceof ArrayBuffer);
if (size !== undefined) {
this.expect(data.byteLength === size);
}
} else {
this.shouldThrow('OperationError', () => {
buffer.getMappedRange(offset, size);
});
}
}
createMappableBuffer(type: GPUMapModeFlags, size: number): GPUBuffer {
switch (type) {
case GPUMapMode.READ:
return this.createBufferTracked({
size,
usage: GPUBufferUsage.MAP_READ,
});
case GPUMapMode.WRITE:
return this.createBufferTracked({
size,
usage: GPUBufferUsage.MAP_WRITE,
});
default:
unreachable();
}
}
}
export const g = makeTestGroup(F);
const kMapModeOptions = [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE];
const kOffsetAlignment = 8;
const kSizeAlignment = 4;
g.test('mapAsync,usage')
.desc(
`Test the usage validation for mapAsync.
For each buffer usage:
For GPUMapMode.READ, GPUMapMode.WRITE, and 0:
Test that the mapAsync call is valid iff the mapping usage is not 0 and the buffer usage
the mapMode flag.`
)
.paramsSubcasesOnly(u =>
u //
.combineWithParams([
{ mapMode: GPUConst.MapMode.READ, validUsage: GPUConst.BufferUsage.MAP_READ },
{ mapMode: GPUConst.MapMode.WRITE, validUsage: GPUConst.BufferUsage.MAP_WRITE },
// Using mapMode 0 is never valid, so there is no validUsage.
{ mapMode: 0, validUsage: null },
])
.combine('usage', kBufferUsages)
)
.fn(async t => {
const { mapMode, validUsage, usage } = t.params;
const buffer = t.createBufferTracked({
size: 16,
usage,
});
const successParam =
usage === validUsage
? 'success'
: {
validationError: true,
earlyRejection: false,
rejectName: 'OperationError',
};
await t.testMapAsyncCall(successParam, buffer, mapMode);
});
g.test('mapAsync,invalidBuffer')
.desc('Test that mapAsync is an error when called on an invalid buffer.')
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = vtu.getErrorBuffer(t);
await t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
});
g.test('mapAsync,state,destroyed')
.desc('Test that mapAsync is an error when called on a destroyed buffer.')
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
// Start mapping the buffer, we are going to destroy it before it resolves so it will reject
// the mapping promise with an AbortError.
const pending = t.testMapAsyncCall(
{ validationError: false, earlyRejection: false, rejectName: 'AbortError' },
buffer,
mapMode
);
buffer.destroy();
await t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
await pending;
});
g.test('mapAsync,state,mappedAtCreation')
.desc(
`Test that mapAsync is an error when called on a buffer mapped at creation,
but succeeds after unmapping it.`
)
.paramsSubcasesOnly([
{ mapMode: GPUConst.MapMode.READ, validUsage: GPUConst.BufferUsage.MAP_READ },
{ mapMode: GPUConst.MapMode.WRITE, validUsage: GPUConst.BufferUsage.MAP_WRITE },
])
.fn(async t => {
const { mapMode, validUsage } = t.params;
const buffer = t.createBufferTracked({
size: 16,
usage: validUsage,
mappedAtCreation: true,
});
await t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
buffer.unmap();
await t.testMapAsyncCall('success', buffer, mapMode);
});
g.test('mapAsync,state,mapped')
.desc(
`Test that mapAsync is an error when called on a mapped buffer, but succeeds
after unmapping it.`
)
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
await t.testMapAsyncCall('success', buffer, mapMode);
await t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
buffer.unmap();
await t.testMapAsyncCall('success', buffer, mapMode);
});
g.test('mapAsync,state,mappingPending')
.desc(
`Test that mapAsync is rejected when called on a buffer that is being mapped,
but succeeds after the previous mapping request is cancelled.`
)
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
// Start mapping the buffer, we are going to unmap it before it resolves so it will reject
// the mapping promise with an AbortError.
const pending0 = t.testMapAsyncCall(
{ validationError: false, earlyRejection: false, rejectName: 'AbortError' },
buffer,
mapMode
);
// Do the test of mapAsync while [[pending_map]] is non-null. It has to be synchronous so
// that we can unmap the previous mapping in the same stack frame and testing this one doesn't
// get canceled, but instead is rejected.
const pending1 = t.testMapAsyncCall(
{ validationError: false, earlyRejection: true, rejectName: 'OperationError' },
buffer,
mapMode
);
// Unmap the first mapping. It should now be possible to successfully call mapAsync
// This unmap should cause the first mapAsync rejection.
buffer.unmap();
await t.testMapAsyncCall('success', buffer, mapMode);
await pending0;
await pending1;
});
g.test('mapAsync,sizeUnspecifiedOOB')
.desc(
`Test that mapAsync with size unspecified rejects if offset > buffer.[[size]],
with various cases at the limits of the buffer size or with a misaligned offset.
Also test for an empty buffer.`
)
.paramsSubcasesOnly(u =>
u //
.combine('mapMode', kMapModeOptions)
.combineWithParams([
// 0 size buffer.
{ bufferSize: 0, offset: 0 },
{ bufferSize: 0, offset: 1 },
{ bufferSize: 0, offset: kOffsetAlignment },
// Test with a buffer that's not empty.
{ bufferSize: 16, offset: 0 },
{ bufferSize: 16, offset: kOffsetAlignment },
{ bufferSize: 16, offset: 16 },
{ bufferSize: 16, offset: 17 },
{ bufferSize: 16, offset: 16 + kOffsetAlignment },
])
)
.fn(async t => {
const { mapMode, bufferSize, offset } = t.params;
const buffer = t.createMappableBuffer(mapMode, bufferSize);
const successParam =
offset <= bufferSize
? 'success'
: {
validationError: true,
earlyRejection: false,
rejectName: 'OperationError',
};
await t.testMapAsyncCall(successParam, buffer, mapMode, offset);
});
g.test('mapAsync,offsetAndSizeAlignment')
.desc("Test that mapAsync fails if the alignment of offset and size isn't correct.")
.paramsSubcasesOnly(u =>
u
.combine('mapMode', kMapModeOptions)
.combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
.combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
)
.fn(async t => {
const { mapMode, offset, size } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
const successParam =
offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0
? 'success'
: {
validationError: true,
earlyRejection: false,
rejectName: 'OperationError',
};
await t.testMapAsyncCall(successParam, buffer, mapMode, offset, size);
});
g.test('mapAsync,offsetAndSizeOOB')
.desc('Test that mapAsync fails if offset + size is larger than the buffer size.')
.paramsSubcasesOnly(u =>
u //
.combine('mapMode', kMapModeOptions)
.combineWithParams([
// For a 0 size buffer
{ bufferSize: 0, offset: 0, size: 0 },
{ bufferSize: 0, offset: 0, size: 4 },
{ bufferSize: 0, offset: 8, size: 0 },
// For a small buffer
{ bufferSize: 16, offset: 0, size: 16 },
{ bufferSize: 16, offset: kOffsetAlignment, size: 16 },
{ bufferSize: 16, offset: 16, size: 0 },
{ bufferSize: 16, offset: 16, size: kSizeAlignment },
{ bufferSize: 16, offset: 8, size: 0 },
{ bufferSize: 16, offset: 8, size: 8 },
{ bufferSize: 16, offset: 8, size: 8 + kSizeAlignment },
// For a larger buffer
{ bufferSize: 1024, offset: 0, size: 1024 },
{ bufferSize: 1024, offset: kOffsetAlignment, size: 1024 },
{ bufferSize: 1024, offset: 1024, size: 0 },
{ bufferSize: 1024, offset: 1024, size: kSizeAlignment },
{ bufferSize: 1024, offset: 512, size: 0 },
{ bufferSize: 1024, offset: 512, size: 512 },
{ bufferSize: 1024, offset: 512, size: 512 + kSizeAlignment },
])
)
.fn(async t => {
const { mapMode, bufferSize, size, offset } = t.params;
const buffer = t.createMappableBuffer(mapMode, bufferSize);
const successParam =
offset + size <= bufferSize
? 'success'
: {
validationError: true,
earlyRejection: false,
rejectName: 'OperationError',
};
await t.testMapAsyncCall(successParam, buffer, mapMode, offset, size);
});
g.test('mapAsync,earlyRejection')
.desc("Test that mapAsync fails immediately if it's pending map.")
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions).combine('offset2', [0, 8]))
.fn(async t => {
const { mapMode, offset2 } = t.params;
const bufferSize = 16;
const mapSize = 8;
const offset1 = 0;
const buffer = t.createMappableBuffer(mapMode, bufferSize);
const p1 = buffer.mapAsync(mapMode, offset1, mapSize); // succeeds
await t.testMapAsyncCall(
{
validationError: false,
earlyRejection: true,
rejectName: 'OperationError',
},
buffer,
mapMode,
offset2,
mapSize
);
await p1; // ensure the original map still succeeds
});
g.test('mapAsync,abort_over_invalid_error')
.desc(
`Test that unmap abort error should have precedence over validation error
TODO
- Add other validation error test (eg. offset is not a multiple of 8)
`
)
.paramsSubcasesOnly(u =>
u.combine('mapMode', kMapModeOptions).combine('unmapBeforeResolve', [true, false])
)
.fn(async t => {
const { mapMode, unmapBeforeResolve } = t.params;
const bufferSize = 8;
const buffer = t.createMappableBuffer(mapMode, bufferSize);
await buffer.mapAsync(mapMode);
if (unmapBeforeResolve) {
// unmap abort error should have precedence over validation error
const pending = t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'AbortError' },
buffer,
mapMode
);
buffer.unmap();
await pending;
} else {
// map on already mapped buffer should cause validation error
await t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
buffer.unmap();
}
});
g.test('getMappedRange,state,mapped')
.desc('Test that it is valid to call getMappedRange in the mapped state')
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const bufferSize = 16;
const buffer = t.createMappableBuffer(mapMode, bufferSize);
await buffer.mapAsync(mapMode);
const data = buffer.getMappedRange();
t.expect(data instanceof ArrayBuffer);
t.expect(data.byteLength === bufferSize);
// map on already mapped buffer should be rejected
const pending = t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
t.expect(data.byteLength === bufferSize);
await pending;
buffer.unmap();
t.expect(data.byteLength === 0);
});
g.test('getMappedRange,state,mappedAtCreation')
.desc(
`Test that, in the mapped-at-creation state, it is valid to call getMappedRange, for all buffer usages,
and invalid to call mapAsync, for all map modes.`
)
.paramsSubcasesOnly(u =>
u.combine('bufferUsage', kBufferUsages).combine('mapMode', kMapModeOptions)
)
.fn(async t => {
const { bufferUsage, mapMode } = t.params;
const bufferSize = 16;
const buffer = t.createBufferTracked({
usage: bufferUsage,
size: bufferSize,
mappedAtCreation: true,
});
const data = buffer.getMappedRange();
t.expect(data instanceof ArrayBuffer);
t.expect(data.byteLength === bufferSize);
// map on already mapped buffer should be rejected
const pending = t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
t.expect(data.byteLength === bufferSize);
await pending;
buffer.unmap();
t.expect(data.byteLength === 0);
});
g.test('getMappedRange,state,invalid_mappedAtCreation')
.desc(
`mappedAtCreation should return a mapped buffer, even if the buffer is invalid.
Like VRAM allocation (see map_oom), validation can be performed asynchronously (in the GPU process)
so the Content process doesn't necessarily know the buffer is invalid.`
)
.fn(t => {
const buffer = t.expectGPUError('validation', () =>
t.createBufferTracked({
mappedAtCreation: true,
size: 16,
usage: 0xffff_ffff, // Invalid usage
})
);
// Should still be valid.
buffer.getMappedRange();
});
g.test('getMappedRange,state,mappedAgain')
.desc(
'Test that it is valid to call getMappedRange in the mapped state, even if there is a duplicate mapAsync before'
)
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
await buffer.mapAsync(mapMode);
// call mapAsync again on already mapped buffer should fail
await t.testMapAsyncCall(
{ validationError: true, earlyRejection: false, rejectName: 'OperationError' },
buffer,
mapMode
);
// getMapppedRange should still success
t.testGetMappedRangeCall(true, buffer);
});
g.test('getMappedRange,state,unmapped')
.desc(
`Test that it is invalid to call getMappedRange in the unmapped state.
Test for various cases of being unmapped: at creation, after a mapAsync call or after being created mapped.`
)
.fn(async t => {
// It is invalid to call getMappedRange when the buffer starts unmapped when created.
{
const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
t.testGetMappedRangeCall(false, buffer);
}
// It is invalid to call getMappedRange when the buffer is unmapped after mapAsync.
{
const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
await buffer.mapAsync(GPUMapMode.READ);
buffer.unmap();
t.testGetMappedRangeCall(false, buffer);
}
// It is invalid to call getMappedRange when the buffer is unmapped after mappedAtCreation.
{
const buffer = t.createBufferTracked({
usage: GPUBufferUsage.MAP_READ,
size: 16,
mappedAtCreation: true,
});
buffer.unmap();
t.testGetMappedRangeCall(false, buffer);
}
});
g.test('getMappedRange,subrange,mapped')
.desc(
`Test that old getMappedRange returned arraybuffer does not exist after unmap, and newly returned
arraybuffer after new map has correct subrange`
)
.params(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const bufferSize = 16;
const offset = 8;
const subrangeSize = bufferSize - offset;
const buffer = t.createMappableBuffer(mapMode, bufferSize);
await buffer.mapAsync(mapMode);
const data0 = buffer.getMappedRange();
t.expect(data0 instanceof ArrayBuffer);
t.expect(data0.byteLength === bufferSize);
buffer.unmap();
t.expect(data0.byteLength === 0);
await buffer.mapAsync(mapMode, offset);
const data1 = buffer.getMappedRange(8);
t.expect(data0.byteLength === 0);
t.expect(data1.byteLength === subrangeSize);
});
g.test('getMappedRange,subrange,mappedAtCreation')
.desc(
`Test that old getMappedRange returned arraybuffer does not exist after unmap and newly returned
arraybuffer after new map has correct subrange`
)
.fn(async t => {
const bufferSize = 16;
const offset = 8;
const subrangeSize = bufferSize - offset;
const buffer = t.createBufferTracked({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
mappedAtCreation: true,
});
const data0 = buffer.getMappedRange();
t.expect(data0 instanceof ArrayBuffer);
t.expect(data0.byteLength === bufferSize);
buffer.unmap();
t.expect(data0.byteLength === 0);
await buffer.mapAsync(GPUMapMode.READ, offset);
const data1 = buffer.getMappedRange(8);
t.expect(data0.byteLength === 0);
t.expect(data1.byteLength === subrangeSize);
});
g.test('getMappedRange,state,destroyed')
.desc(
`Test that it is invalid to call getMappedRange in the destroyed state.
Test for various cases of being destroyed: at creation, after a mapAsync call or after being created mapped.`
)
.fn(async t => {
// It is invalid to call getMappedRange when the buffer is destroyed when unmapped.
{
const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
buffer.destroy();
t.testGetMappedRangeCall(false, buffer);
}
// It is invalid to call getMappedRange when the buffer is destroyed when mapped.
{
const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
await buffer.mapAsync(GPUMapMode.READ);
buffer.destroy();
t.testGetMappedRangeCall(false, buffer);
}
// It is invalid to call getMappedRange when the buffer is destroyed when mapped at creation.
{
const buffer = t.createBufferTracked({
usage: GPUBufferUsage.MAP_READ,
size: 16,
mappedAtCreation: true,
});
buffer.destroy();
t.testGetMappedRangeCall(false, buffer);
}
});
g.test('getMappedRange,state,mappingPending')
.desc(`Test that it is invalid to call getMappedRange in the mappingPending state.`)
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
/* noawait */ const mapping0 = buffer.mapAsync(mapMode);
// seconding mapping should be rejected
const mapping1 = t.testMapAsyncCall(
{ validationError: false, earlyRejection: true, rejectName: 'OperationError' },
buffer,
mapMode
);
// invalid in mappingPending state
t.testGetMappedRangeCall(false, buffer);
await mapping0;
// valid after buffer is mapped
t.testGetMappedRangeCall(true, buffer);
await mapping1;
});
g.test('getMappedRange,offsetAndSizeAlignment,mapped')
.desc(`Test that getMappedRange fails if the alignment of offset and size isn't correct.`)
.params(u =>
u
.combine('mapMode', kMapModeOptions)
.beginSubcases()
.combine('mapOffset', [0, kOffsetAlignment])
.combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
.combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
)
.fn(async t => {
const { mapMode, mapOffset, offset, size } = t.params;
const buffer = t.createMappableBuffer(mapMode, 32);
await buffer.mapAsync(mapMode, mapOffset);
const success = offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0;
t.testGetMappedRangeCall(success, buffer, offset + mapOffset, size);
});
g.test('getMappedRange,offsetAndSizeAlignment,mappedAtCreation')
.desc(`Test that getMappedRange fails if the alignment of offset and size isn't correct.`)
.paramsSubcasesOnly(u =>
u
.combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
.combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
)
.fn(t => {
const { offset, size } = t.params;
const buffer = t.createBufferTracked({
size: 16,
usage: GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
const success = offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0;
t.testGetMappedRangeCall(success, buffer, offset, size);
});
g.test('getMappedRange,sizeAndOffsetOOB,mappedAtCreation')
.desc(
`Test that getMappedRange size + offset must be less than the buffer size for a
buffer mapped at creation. (and offset has not constraints on its own)`
)
.paramsSubcasesOnly([
// Tests for a zero-sized buffer, with and without a size defined.
{ bufferSize: 0, offset: undefined, size: undefined },
{ bufferSize: 0, offset: undefined, size: 0 },
{ bufferSize: 0, offset: undefined, size: kSizeAlignment },
{ bufferSize: 0, offset: 0, size: undefined },
{ bufferSize: 0, offset: 0, size: 0 },
{ bufferSize: 0, offset: kOffsetAlignment, size: undefined },
{ bufferSize: 0, offset: kOffsetAlignment, size: 0 },
// Tests for a non-empty buffer, with an undefined offset.
{ bufferSize: 80, offset: undefined, size: 80 },
{ bufferSize: 80, offset: undefined, size: 80 + kSizeAlignment },
// Tests for a non-empty buffer, with an undefined size.
{ bufferSize: 80, offset: undefined, size: undefined },
{ bufferSize: 80, offset: 0, size: undefined },
{ bufferSize: 80, offset: kOffsetAlignment, size: undefined },
{ bufferSize: 80, offset: 80, size: undefined },
{ bufferSize: 80, offset: 80 + kOffsetAlignment, size: undefined },
// Tests for a non-empty buffer with a size defined.
{ bufferSize: 80, offset: 0, size: 80 },
{ bufferSize: 80, offset: 0, size: 80 + kSizeAlignment },
{ bufferSize: 80, offset: kOffsetAlignment, size: 80 },
{ bufferSize: 80, offset: 40, size: 40 },
{ bufferSize: 80, offset: 40 + kOffsetAlignment, size: 40 },
{ bufferSize: 80, offset: 40, size: 40 + kSizeAlignment },
])
.fn(t => {
const { bufferSize, offset, size } = t.params;
const buffer = t.createBufferTracked({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
const actualOffset = offset ?? 0;
const actualSize = size ?? bufferSize - actualOffset;
const success = actualOffset <= bufferSize && actualOffset + actualSize <= bufferSize;
t.testGetMappedRangeCall(success, buffer, offset, size);
});
g.test('getMappedRange,sizeAndOffsetOOB,mapped')
.desc('Test that getMappedRange size + offset must be less than the mapAsync range.')
.paramsSubcasesOnly(u =>
u //
.combine('mapMode', kMapModeOptions)
.combineWithParams([
// Tests for an empty buffer, and implicit mapAsync size.
{ bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: undefined, size: undefined },
{ bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: undefined, size: 0 },
{
bufferSize: 0,
mapOffset: 0,
mapSize: undefined,
offset: undefined,
size: kSizeAlignment,
},
{ bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: 0, size: undefined },
{ bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: 0, size: 0 },
{
bufferSize: 0,
mapOffset: 0,
mapSize: undefined,
offset: kOffsetAlignment,
size: undefined,
},
{ bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: kOffsetAlignment, size: 0 },
// Tests for an empty buffer, and explicit mapAsync size.
{ bufferSize: 0, mapOffset: 0, mapSize: 0, offset: undefined, size: undefined },
{ bufferSize: 0, mapOffset: 0, mapSize: 0, offset: 0, size: undefined },
{ bufferSize: 0, mapOffset: 0, mapSize: 0, offset: 0, size: 0 },
{ bufferSize: 0, mapOffset: 0, mapSize: 0, offset: kOffsetAlignment, size: undefined },
{ bufferSize: 0, mapOffset: 0, mapSize: 0, offset: kOffsetAlignment, size: 0 },
// Test for a fully implicit mapAsync call
{ bufferSize: 80, mapOffset: undefined, mapSize: undefined, offset: 0, size: 80 },
{
bufferSize: 80,
mapOffset: undefined,
mapSize: undefined,
offset: 0,
size: 80 + kSizeAlignment,
},
{
bufferSize: 80,
mapOffset: undefined,
mapSize: undefined,
offset: kOffsetAlignment,
size: 80,
},
// Test for a mapAsync call with an implicit size
{ bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 24, size: 80 - 24 },
{
bufferSize: 80,
mapOffset: 24,
mapSize: undefined,
offset: 0,
size: 80 - 24 + kSizeAlignment,
},
{
bufferSize: 80,
mapOffset: 24,
mapSize: undefined,
offset: kOffsetAlignment,
size: 80 - 24,
},
// Test for a non-empty buffer fully mapped.
{ bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 0, size: 80 },
{ bufferSize: 80, mapOffset: 0, mapSize: 80, offset: kOffsetAlignment, size: 80 },
{ bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 0, size: 80 + kSizeAlignment },
{ bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40, size: 40 },
{ bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40 + kOffsetAlignment, size: 40 },
{ bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40, size: 40 + kSizeAlignment },
// Test for a buffer partially mapped.
{ bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: 40 },
{ bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24 - kOffsetAlignment, size: 40 },
{ bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24 + kOffsetAlignment, size: 40 },
{ bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: 40 + kSizeAlignment },
// Test for a partially mapped buffer with implicit size and offset for getMappedRange.
// - Buffer partially mapped in the middle
{ bufferSize: 80, mapOffset: 24, mapSize: 40, offset: undefined, size: undefined },
{ bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 0, size: undefined },
{ bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: undefined },
// - Buffer partially mapped to the end
{ bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 24, size: undefined },
{ bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 80, size: undefined },
// - Buffer partially mapped from the start
{ bufferSize: 80, mapOffset: 0, mapSize: 64, offset: undefined, size: undefined },
{ bufferSize: 80, mapOffset: 0, mapSize: 64, offset: undefined, size: 64 },
])
)
.fn(async t => {
const { mapMode, bufferSize, mapOffset, mapSize, offset, size } = t.params;
const buffer = t.createMappableBuffer(mapMode, bufferSize);
await buffer.mapAsync(mapMode, mapOffset, mapSize);
const actualMapOffset = mapOffset ?? 0;
const actualMapSize = mapSize ?? bufferSize - actualMapOffset;
const actualOffset = offset ?? 0;
const actualSize = size ?? bufferSize - actualOffset;
const success =
actualOffset >= actualMapOffset &&
actualOffset <= bufferSize &&
actualOffset + actualSize <= actualMapOffset + actualMapSize;
t.testGetMappedRangeCall(success, buffer, offset, size);
});
g.test('getMappedRange,disjointRanges')
.desc('Test that the ranges asked through getMappedRange must be disjoint.')
.paramsSubcasesOnly(u =>
u //
.combine('remapBetweenCalls', [false, true])
.combineWithParams([
// Disjoint ranges with one that's empty.
{ offset1: 8, size1: 0, offset2: 8, size2: 8 },
{ offset1: 16, size1: 0, offset2: 8, size2: 8 },
{ offset1: 8, size1: 8, offset2: 8, size2: 0 },
{ offset1: 8, size1: 8, offset2: 16, size2: 0 },
// Disjoint ranges with both non-empty.
{ offset1: 0, size1: 8, offset2: 8, size2: 8 },
{ offset1: 16, size1: 8, offset2: 8, size2: 8 },
{ offset1: 8, size1: 8, offset2: 0, size2: 8 },
{ offset1: 8, size1: 8, offset2: 16, size2: 8 },
// Empty range contained inside another one.
{ offset1: 16, size1: 20, offset2: 24, size2: 0 },
{ offset1: 24, size1: 0, offset2: 16, size2: 20 },
// Ranges that overlap only partially.
{ offset1: 16, size1: 20, offset2: 8, size2: 20 },
{ offset1: 16, size1: 20, offset2: 32, size2: 20 },
// Ranges that include one another.
{ offset1: 0, size1: 80, offset2: 16, size2: 20 },
{ offset1: 16, size1: 20, offset2: 0, size2: 80 },
])
)
.fn(async t => {
const { offset1, size1, offset2, size2, remapBetweenCalls } = t.params;
const buffer = t.createBufferTracked({ size: 80, usage: GPUBufferUsage.MAP_READ });
await buffer.mapAsync(GPUMapMode.READ);
t.testGetMappedRangeCall(true, buffer, offset1, size1);
if (remapBetweenCalls) {
buffer.unmap();
await buffer.mapAsync(GPUMapMode.READ);
}
const range1StartsAfter2 = offset1 >= offset2 + size2;
const range2StartsAfter1 = offset2 >= offset1 + size1;
const disjoint = range1StartsAfter2 || range2StartsAfter1;
const success = disjoint || remapBetweenCalls;
t.testGetMappedRangeCall(success, buffer, offset2, size2);
});
g.test('getMappedRange,disjointRanges_many')
.desc('Test getting a lot of small ranges, and that the disjoint check checks them all.')
.fn(async t => {
const kStride = 256;
const kNumStrides = 256;
const buffer = t.createBufferTracked({
size: kStride * kNumStrides,
usage: GPUBufferUsage.MAP_READ,
});
await buffer.mapAsync(GPUMapMode.READ);
// Get a lot of small mapped ranges.
for (let stride = 0; stride < kNumStrides; stride++) {
t.testGetMappedRangeCall(true, buffer, stride * kStride, 8);
}
// Check for each range it is invalid to get a range that overlaps it and check that it is valid
// to get ranges for the rest of the buffer.
for (let stride = 0; stride < kNumStrides; stride++) {
t.testGetMappedRangeCall(false, buffer, stride * kStride, kStride);
t.testGetMappedRangeCall(true, buffer, stride * kStride + 8, kStride - 8);
}
});
g.test('unmap,state,unmapped')
.desc(
`Test it is valid to call unmap on a buffer that is unmapped (at creation, or after
mappedAtCreation or mapAsync)`
)
.fn(async t => {
// It is valid to call unmap after creation of an unmapped buffer.
{
const buffer = t.createBufferTracked({ size: 16, usage: GPUBufferUsage.MAP_READ });
buffer.unmap();
}
// It is valid to call unmap after unmapping a mapAsynced buffer.
{
const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
await buffer.mapAsync(GPUMapMode.READ);
buffer.unmap();
buffer.unmap();
}
// It is valid to call unmap after unmapping a mappedAtCreation buffer.
{
const buffer = t.createBufferTracked({
usage: GPUBufferUsage.MAP_READ,
size: 16,
mappedAtCreation: true,
});
buffer.unmap();
buffer.unmap();
}
});
g.test('unmap,state,destroyed')
.desc(
`Test it is valid to call unmap on a buffer that is destroyed (at creation, or after
mappedAtCreation or mapAsync)`
)
.fn(async t => {
// It is valid to call unmap after destruction of an unmapped buffer.
{
const buffer = t.createBufferTracked({ size: 16, usage: GPUBufferUsage.MAP_READ });
buffer.destroy();
buffer.unmap();
}
// It is valid to call unmap after destroying a mapAsynced buffer.
{
const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
await buffer.mapAsync(GPUMapMode.READ);
buffer.destroy();
buffer.unmap();
}
// It is valid to call unmap after destroying a mappedAtCreation buffer.
{
const buffer = t.createBufferTracked({
usage: GPUBufferUsage.MAP_READ,
size: 16,
mappedAtCreation: true,
});
buffer.destroy();
buffer.unmap();
}
});
g.test('unmap,state,mappedAtCreation')
.desc('Test it is valid to call unmap on a buffer mapped at creation, for various usages')
.paramsSubcasesOnly(u =>
u //
.combine('bufferUsage', kBufferUsages)
)
.fn(t => {
const { bufferUsage } = t.params;
const buffer = t.createBufferTracked({ size: 16, usage: bufferUsage, mappedAtCreation: true });
buffer.unmap();
});
g.test('unmap,state,mapped')
.desc("Test it is valid to call unmap on a buffer that's mapped")
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
await buffer.mapAsync(mapMode);
buffer.unmap();
});
g.test('unmap,state,mappingPending')
.desc("Test it is valid to call unmap on a buffer that's being mapped")
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
const buffer = t.createMappableBuffer(mapMode, 16);
const pending = t.testMapAsyncCall(
{ validationError: false, earlyRejection: false, rejectName: 'AbortError' },
buffer,
mapMode
);
buffer.unmap();
await pending;
});
g.test('gc_behavior,mappedAtCreation')
.desc(
"Test that GCing the buffer while mappings are handed out doesn't invalidate them - mappedAtCreation case"
)
.fn(async t => {
let buffer = null;
buffer = t.createBufferTracked({
size: 256,
usage: GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
// Write some non-zero data to the buffer.
const contents = new Uint32Array(buffer.getMappedRange());
for (let i = 0; i < contents.length; i++) {
contents[i] = i;
}
// Trigger garbage collection that should collect the buffer (or as if it collected it)
// NOTE: This won't fail unless the browser immediately starts reusing the memory, or gives it
// back to the OS. One good option for browsers to check their logic is good is to zero-out the
// memory on GPUBuffer (or internal gpu::Buffer-like object) destruction.
buffer = null;
await attemptGarbageCollection();
// Use the mapping again both for read and write, it should work.
for (let i = 0; i < contents.length; i++) {
t.expect(contents[i] === i);
contents[i] = i + 1;
}
});
g.test('gc_behavior,mapAsync')
.desc(
"Test that GCing the buffer while mappings are handed out doesn't invalidate them - mapAsync case"
)
.paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
.fn(async t => {
const { mapMode } = t.params;
let buffer = null;
buffer = t.createMappableBuffer(mapMode, 256);
await buffer.mapAsync(mapMode);
// Write some non-zero data to the buffer.
const contents = new Uint32Array(buffer.getMappedRange());
for (let i = 0; i < contents.length; i++) {
contents[i] = i;
}
// Trigger garbage collection that should collect the buffer (or as if it collected it)
// NOTE: This won't fail unless the browser immediately starts reusing the memory, or gives it
// back to the OS. One good option for browsers to check their logic is good is to zero-out the
// memory on GPUBuffer (or internal gpu::Buffer-like object) destruction.
buffer = null;
await attemptGarbageCollection();
// Use the mapping again both for read and write, it should work.
for (let i = 0; i < contents.length; i++) {
t.expect(contents[i] === i);
contents[i] = i + 1;
}
});