Skip to content

Commit 92e6d7e

Browse files
committed
Add tests for mapping behavior around device.destroy()
1 parent 8e05682 commit 92e6d7e

File tree

2 files changed

+110
-3
lines changed

2 files changed

+110
-3
lines changed

src/webgpu/api/operation/buffers/map_detach.spec.ts

+4
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ g.test('while_mapped')
6262

6363
if (mapMode !== undefined) {
6464
if (mappedAtCreation) {
65+
t.expect(buffer.mapState === 'mapped');
6566
buffer.unmap();
6667
}
68+
t.expect(buffer.mapState === 'unmapped');
6769
await buffer.mapAsync(mapMode);
6870
}
71+
t.expect(buffer.mapState === 'mapped');
6972

7073
const arrayBuffer = buffer.getMappedRange();
7174
const view = new Uint8Array(arrayBuffer);
@@ -78,4 +81,5 @@ g.test('while_mapped')
7881

7982
t.expect(arrayBuffer.byteLength === 0, 'ArrayBuffer should be detached');
8083
t.expect(view.byteLength === 0, 'ArrayBufferView should be detached');
84+
t.expect(buffer.mapState === 'unmapped');
8185
});

src/webgpu/api/validation/state/device_lost/destroy.spec.ts

+106-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
kTextureUsageCopyInfo,
2222
kShaderStageKeys,
2323
} from '../../../../capability_info.js';
24+
import { GPUConst } from '../../../../constants.js';
2425
import {
2526
getBlockInfoForColorTextureFormat,
2627
kCompressedTextureFormats,
@@ -52,16 +53,15 @@ class DeviceDestroyTests extends AllFeaturesMaxLimitsValidationTest {
5253
* `fn` after the device is destroyed without any specific expectation. If `awaitLost` is true, we
5354
* also wait for device.lost to resolve before executing `fn` in the destroy case.
5455
*/
55-
async executeAfterDestroy(fn: () => void, awaitLost: boolean): Promise<void> {
56+
async executeAfterDestroy(fn: () => void | Promise<void>, awaitLost: boolean): Promise<void> {
5657
this.expectDeviceLost('destroyed');
5758

58-
this.expectValidationError(fn, false);
5959
this.device.destroy();
6060
if (awaitLost) {
6161
const lostInfo = await this.device.lost;
6262
this.expect(lostInfo.reason === 'destroyed');
6363
}
64-
fn();
64+
await fn();
6565
}
6666

6767
/**
@@ -146,6 +146,109 @@ Tests creating buffers on destroyed device. Tests valid combinations of:
146146
}, awaitLost);
147147
});
148148

149+
g.test('mapping,mappedAtCreation')
150+
.desc(
151+
`
152+
Tests behavior of mappedAtCreation buffers when destroying the device (multiple times).
153+
- Various usages
154+
- Wait for .lost or not
155+
`
156+
)
157+
.params(u =>
158+
u
159+
.combine('usage', [
160+
GPUConst.BufferUsage.MAP_READ,
161+
GPUConst.BufferUsage.MAP_WRITE,
162+
GPUConst.BufferUsage.COPY_SRC,
163+
])
164+
.combine('awaitLost', [true, false])
165+
)
166+
.fn(async t => {
167+
const { awaitLost, usage } = t.params;
168+
169+
const b1 = t.createBufferTracked({ size: 16, usage, mappedAtCreation: true });
170+
t.expect(b1.mapState === 'mapped', 'b1 before destroy 1');
171+
172+
await t.executeAfterDestroy(() => {
173+
// Destroy should have unmapped everything.
174+
t.expect(b1.mapState === 'unmapped', 'b1 after destroy 1');
175+
// But unmap just in case, to reset state before continuing.
176+
b1.unmap();
177+
178+
// mappedAtCreation should still work.
179+
const b2 = t.createBufferTracked({ size: 16, usage, mappedAtCreation: true });
180+
t.expect(b2.mapState === 'mapped', 'b2 at creation after destroy 1');
181+
182+
// Destroying again should unmap.
183+
t.device.destroy();
184+
t.expect(b2.mapState === 'unmapped', 'b2 after destroy 2');
185+
}, awaitLost);
186+
});
187+
188+
g.test('mapping,mapAsync')
189+
.desc(
190+
`
191+
Tests behavior of mapAsync'd buffers when destroying the device.
192+
- Various usages
193+
- Wait for .lost or not
194+
195+
TODO(https://github.com/gpuweb/gpuweb/issues/5101): Test which error we got at [1].
196+
`
197+
)
198+
.params(u =>
199+
u
200+
.combine('usage', [
201+
GPUConst.BufferUsage.MAP_READ,
202+
GPUConst.BufferUsage.MAP_WRITE,
203+
GPUConst.BufferUsage.COPY_SRC,
204+
])
205+
.combine('awaitLost', [true, false])
206+
)
207+
.fn(async t => {
208+
const { awaitLost, usage } = t.params;
209+
210+
const b = t.createBufferTracked({ size: 16, usage });
211+
const mode =
212+
usage === GPUBufferUsage.MAP_READ
213+
? GPUMapMode.READ
214+
: usage === GPUBufferUsage.MAP_WRITE
215+
? GPUMapMode.WRITE
216+
: 0;
217+
if (mode) {
218+
await b.mapAsync(mode);
219+
t.expect(b.mapState === 'mapped', 'bAsync before destroy 1');
220+
} else {
221+
t.expect(b.mapState === 'unmapped', 'bAsync before destroy 1');
222+
}
223+
224+
await t.executeAfterDestroy(async () => {
225+
// Destroy should have unmapped everything.
226+
t.expect(b.mapState === 'unmapped', 'bAsync after destroy 1');
227+
// But unmap just in case, to reset state before continuing.
228+
b.unmap();
229+
230+
// Check that mapping after destroy fails.
231+
// Also check that if mapAsync fails validation, it still produces an OperationError.
232+
try {
233+
await b.mapAsync(mode);
234+
t.expect(false, 'bAsync mapAsync after destroy 1 should reject');
235+
} catch (ex) {
236+
if (mode) {
237+
// The mapAsync call is valid except for the fact that the device is lost.
238+
t.expect(ex instanceof DOMException && ex.name === 'AbortError');
239+
} else {
240+
// The mapAsync call is also invalid for other reasons.
241+
// [1] Test which error type we got. (And maybe switch to shouldReject().)
242+
t.expect(ex instanceof DOMException);
243+
}
244+
}
245+
const mapPromise = b.mapAsync(mode);
246+
t.shouldReject('AbortError', mapPromise);
247+
await mapPromise.catch(() => {});
248+
t.expect(b.mapState === 'unmapped', 'bAsync after mapAsync after destroy 1');
249+
}, awaitLost);
250+
});
251+
149252
g.test('createTexture,2d,uncompressed_format')
150253
.desc(
151254
`

0 commit comments

Comments
 (0)