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; 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: true, 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: true, 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: true, 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: true, 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') .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions).combine('unmapBeforeResolve', [true, false]) ) .fn(async t => { const { mapMode, unmapBeforeResolve } = t.params; const bufferSize = 8; const buffer = t.createBufferTracked({ usage: GPUBufferUsage.STORAGE, size: bufferSize, }); 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 { // invalid map call 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: true, 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: true, 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: true, 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: true, 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; } });