| 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,destroyedDevice') | |
| .desc('Test that mapAsync is an error when called on a destroyed device.') | |
| .params(u => | |
| u.combine('bufferState', ['valid', 'invalid'] as const).combine('awaitLost', [true, false]) | |
| ) | |
| .fn(async t => { | |
| const { awaitLost, bufferState } = t.params; | |
| const buffer = | |
| bufferState === 'valid' ? t.createMappableBuffer(GPUMapMode.READ, 4) : vtu.getErrorBuffer(t); | |
| t.expectDeviceLost('destroyed'); | |
| await t.testMapAsyncCall( | |
| bufferState === 'valid' | |
| ? 'success' | |
| : { validationError: true, earlyRejection: false, rejectName: 'OperationError' }, | |
| buffer, | |
| GPUMapMode.READ | |
| ); | |
| buffer.unmap(); | |
| t.device.destroy(); | |
| if (awaitLost) { | |
| const lostInfo = await t.device.lost; | |
| t.expect(lostInfo.reason === 'destroyed'); | |
| } | |
| await t.testMapAsyncCall( | |
| { validationError: false, earlyRejection: false, rejectName: 'AbortError' }, | |
| buffer, | |
| GPUMapMode.READ | |
| ); | |
| }); | |
| 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; | |
| } | |
| }); |