blob: eb9c074a394ca63fb76f333e659aa8374b7e1083 [file] [log] [blame]
export const description = `
Validate that various GPU objects behave correctly in JavaScript.
Examples:
- GPUTexture, GPUBuffer, GPUQuerySet, GPUDevice, GPUAdapter, GPUAdapter.limits, GPUDevice.limits
- return nothing for Object.keys()
- adds no properties for {...object}
- iterate over keys for (key in object)
- throws for (key of object)
- return nothing from Object.getOwnPropertyDescriptors
- GPUAdapter.limits, GPUDevice.limits
- can not be passed to requestAdapter as requiredLimits
- GPUAdapter.features, GPUDevice.features
- do spread to array
- can be copied to set
- can be passed to requestAdapter as requiredFeatures
`;
import { makeTestGroup } from '../../common/framework/test_group.js';
import { keysOf } from '../../common/util/data_tables.js';
import { getGPU } from '../../common/util/navigator_gpu.js';
import {
assert,
objectEquals,
raceWithRejectOnTimeout,
unreachable,
} from '../../common/util/util.js';
import { getDefaultLimitsForDevice, kPossibleLimits } from '../capability_info.js';
import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../gpu_test.js';
// MAINTENANCE_TODO: Remove this filter when these limits are added to the spec.
const isUnspecifiedLimit = (limit: string) =>
/maxStorage(Buffer|Texture)sIn(Vertex|Fragment)Stage/.test(limit);
const kSpecifiedLimits = kPossibleLimits.filter(s => !isUnspecifiedLimit(s));
type ResourceInfo = {
create: (t: GPUTest) => Object;
requiredKeys: readonly string[];
getters: readonly string[];
settable: readonly string[];
sameObject: readonly string[];
// Note: some of the tests need to be skipped in compatibility mode related to 'gpu'.
// This is because getGPU() doesn't return the actual `navigator.gpu` in compatibility mode.
// Instead it returns something that's wrapped to request compatibility mode.
skipInCompatibility?: boolean;
};
const kResourceInfo: { [key: string]: ResourceInfo } = {
gpu: {
create(t: GPUTest) {
return getGPU(null);
},
requiredKeys: ['wgslLanguageFeatures', 'requestAdapter'],
getters: ['wgslLanguageFeatures'],
settable: [],
sameObject: ['wgslLanguageFeatures'],
skipInCompatibility: true,
},
buffer: {
create(t: GPUTest) {
return t.createBufferTracked({ size: 16, usage: GPUBufferUsage.UNIFORM });
},
requiredKeys: [
'destroy',
'getMappedRange',
'label',
'mapAsync',
'mapState',
'size',
'unmap',
'usage',
],
getters: ['label', 'mapState', 'size', 'usage'],
settable: ['label'],
sameObject: [],
},
texture: {
create(t: GPUTest) {
return t.createTextureTracked({
size: [2, 3],
format: 'r8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING,
});
},
requiredKeys: [
'createView',
'depthOrArrayLayers',
'destroy',
'dimension',
'format',
'height',
'label',
'mipLevelCount',
'sampleCount',
'usage',
'width',
],
getters: [
'depthOrArrayLayers',
'dimension',
'format',
'height',
'label',
'mipLevelCount',
'sampleCount',
'usage',
'width',
],
settable: ['label'],
sameObject: [],
},
querySet: {
create(t: GPUTest) {
return t.createQuerySetTracked({
type: 'occlusion',
count: 2,
});
},
requiredKeys: ['count', 'destroy', 'label', 'type'],
getters: ['count', 'label', 'type'],
settable: ['label'],
sameObject: [],
},
adapter: {
create(t: GPUTest) {
return t.adapter;
},
requiredKeys: ['features', 'info', 'limits', 'requestDevice'],
getters: ['features', 'info', 'limits'],
settable: [],
sameObject: ['features', 'info', 'limits'],
},
device: {
create(t: GPUTest) {
return t.device;
},
requiredKeys: [
'adapterInfo',
'addEventListener',
'createBindGroup',
'createBindGroupLayout',
'createBuffer',
'createCommandEncoder',
'createComputePipeline',
'createComputePipelineAsync',
'createPipelineLayout',
'createQuerySet',
'createRenderBundleEncoder',
'createRenderPipeline',
'createRenderPipelineAsync',
'createSampler',
'createShaderModule',
'createTexture',
'destroy',
'dispatchEvent',
'features',
'importExternalTexture',
'label',
'limits',
'lost',
'onuncapturederror',
'popErrorScope',
'pushErrorScope',
'queue',
'removeEventListener',
],
getters: ['adapterInfo', 'features', 'label', 'limits', 'lost', 'onuncapturederror', 'queue'],
settable: ['label', 'onuncapturederror'],
sameObject: ['adapterInfo', 'features', 'limits', 'queue'],
},
'adapter.limits': {
create(t: GPUTest) {
return t.adapter.limits;
},
requiredKeys: kSpecifiedLimits,
getters: kSpecifiedLimits,
settable: [],
sameObject: [],
},
'device.limits': {
create(t: GPUTest) {
return t.device.limits;
},
requiredKeys: kSpecifiedLimits,
getters: kSpecifiedLimits,
settable: [],
sameObject: [],
},
} as const;
const kResources = keysOf(kResourceInfo);
type ResourceName = (typeof kResources)[number];
function createResource(t: GPUTest, type: ResourceName) {
return kResourceInfo[type].create(t);
}
type Resource = ReturnType<typeof createResource>;
function forOfIterations(obj: Resource) {
let count = 0;
for (const _ of obj as unknown as []) {
++count;
}
return count;
}
function hasRequiredKeys(t: GPUTest, obj: Resource, requiredKeys: readonly string[]) {
for (const requiredKey of requiredKeys) {
t.expect(requiredKey in obj, `${requiredKey} in ${obj.constructor.name} exists`);
}
}
function aHasBElements(
t: GPUTest,
a: GPUSupportedFeatures | string[],
b: GPUSupportedFeatures | string[]
) {
for (const elem of b) {
if (Array.isArray(a)) {
t.expect(a.includes(elem), `missing ${elem}`);
} else if (a.has) {
t.expect(a.has(elem), `missing ${elem}`);
} else {
unreachable();
}
}
}
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
g.test('obj,Object_keys')
.desc('tests returns nothing for Object.keys()')
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const { skipInCompatibility } = kResourceInfo[type];
t.skipIf(t.isCompatibility && !!skipInCompatibility, 'skipped in compatibility mode');
const obj = createResource(t, type);
t.expect(objectEquals([...Object.keys(obj)], []), `[...Object.keys(${type})] === []`);
});
g.test('obj,spread')
.desc('does not spread')
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const { skipInCompatibility } = kResourceInfo[type];
t.skipIf(t.isCompatibility && !!skipInCompatibility, 'skipped in compatibility mode');
const obj = createResource(t, type);
t.expect(objectEquals({ ...obj }, {}), `{ ...${type} ] === {}`);
});
g.test('obj,for_in')
.desc('iterates over keys - for (key in object)')
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const obj = createResource(t, type);
hasRequiredKeys(t, obj, kResourceInfo[type].requiredKeys);
});
g.test('obj,for_of')
.desc('throws TypeError - for (key of object')
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const obj = createResource(t, type);
t.shouldThrow('TypeError', () => forOfIterations(obj), {
message: `for (const key of ${type} } throws TypeError`,
});
});
g.test('obj,getOwnPropertyDescriptors')
.desc('Object.getOwnPropertyDescriptors returns {}')
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const { skipInCompatibility } = kResourceInfo[type];
t.skipIf(t.isCompatibility && !!skipInCompatibility, 'skipped in compatibility mode');
const obj = createResource(t, type);
t.expect(
objectEquals(Object.getOwnPropertyDescriptors(obj), {}),
`Object.getOwnPropertyDescriptors(${type}} === {}`
);
});
const kSetLikeFeaturesInfo: { [key: string]: (t: GPUTest) => GPUSupportedFeatures } = {
adapterFeatures(t: GPUTest) {
return t.adapter.features;
},
deviceFeatures(t: GPUTest) {
return t.device.features;
},
};
const kSetLikeFeatures = keysOf(kSetLikeFeaturesInfo);
const kSetLikeInfo: { [key: string]: (t: GPUTest) => ReadonlySet<string> } = {
...kSetLikeFeaturesInfo,
wgslLanguageFeatures() {
return getGPU(null).wgslLanguageFeatures;
},
};
const kSetLikes = keysOf(kSetLikeInfo);
g.test('setlike,spread')
.desc('obj spreads')
.params(u => u.combine('type', kSetLikes))
.fn(t => {
const { type } = t.params;
const setLike = kSetLikeInfo[type](t);
const copy = [...setLike];
aHasBElements(t, copy, setLike);
aHasBElements(t, setLike, copy);
});
g.test('setlike,set')
.desc('obj copies to set')
.params(u => u.combine('type', kSetLikes))
.fn(t => {
const { type } = t.params;
const setLike = kSetLikeInfo[type](t);
const copy = new Set(setLike);
aHasBElements(t, copy, setLike);
aHasBElements(t, setLike, copy);
});
g.test('setlike,requiredFeatures')
.desc('can be passed as required features')
.params(u => u.combine('type', kSetLikeFeatures))
.fn(async t => {
const { type } = t.params;
const features = kSetLikeFeaturesInfo[type](t);
const gpu = getGPU(null);
const adapter = await gpu.requestAdapter();
const device = await t.requestDeviceTracked(adapter!, {
requiredFeatures: features as Iterable<GPUFeatureName>,
});
aHasBElements(t, device.features, features);
aHasBElements(t, features, device.features);
});
g.test('limits')
.desc('adapter/device.limits can not be passed as requiredLimits')
.params(u => u.combine('type', ['adapter', 'device'] as const))
.fn(async t => {
const { type } = t.params;
const obj = type === 'adapter' ? t.adapter : t.device;
const gpu = getGPU(null);
const adapter = await gpu.requestAdapter();
assert(!!adapter);
const device = await t.requestDeviceTracked(adapter, {
requiredLimits: obj.limits as unknown as Record<string, number>,
});
const defaultLimits = getDefaultLimitsForDevice(device);
for (const [key, { default: defaultLimit }] of Object.entries(defaultLimits)) {
if (isUnspecifiedLimit(key)) {
continue;
}
const actual = (device.limits as unknown as Record<string, number>)[key];
t.expect(
actual === defaultLimit,
`expected device.limits.${key}(${actual}) === ${defaultLimit}`
);
}
});
g.test('readonly_properties')
.desc(
`
Test that setting a property with no setter throws.
`
)
.params(u => u.combine('type', kResources))
.fn(t => {
'use strict'; // This makes setting a readonly property produce a TypeError.
const { type } = t.params;
const { getters, settable } = kResourceInfo[type];
const obj = createResource(t, type);
for (const getter of getters) {
const origValue = (obj as unknown as Record<string, unknown>)[getter];
// try setting it.
const isSettable = settable.includes(getter);
t.shouldThrow(
isSettable ? false : 'TypeError',
() => {
(obj as unknown as Record<string, string>)[getter] = 'test value';
// If we were able to set it, restore it.
(obj as unknown as Record<string, unknown>)[getter] = origValue;
},
{
message: `instanceof ${type}.${getter} = value ${
isSettable ? 'does not throw' : 'throws'
}`,
}
);
}
});
g.test('getter_replacement')
.desc(
`
Test that replacing getters on class prototypes works
This is a common pattern for shims and debugging libraries so make sure this pattern works.
`
)
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const { getters } = kResourceInfo[type];
const obj = createResource(t, type);
for (const getter of getters) {
// Check it's not 'ownProperty`
const properties = Object.getOwnPropertyDescriptor(obj, getter);
t.expect(
properties === undefined,
`Object.getOwnPropertyDescriptor(instance of ${type}, '${getter}') === undefined`
);
// Check it's actually a getter that returns a non-function value.
const origValue = (obj as unknown as Record<string, () => unknown>)[getter];
t.expect(typeof origValue !== 'function', `instance of ${type}.${getter} !== 'function'`);
// check replacing the getter on constructor works.
const ctorPrototype = obj.constructor.prototype;
const origProperties = Object.getOwnPropertyDescriptor(ctorPrototype, getter);
assert(
!!origProperties,
`Object.getOwnPropertyDescriptor(${type}, '${getter}') !== undefined`
);
try {
Object.defineProperty(ctorPrototype, getter, {
get() {
return 'testGetterValue';
},
});
const value = (obj as unknown as Record<string, string>)[getter];
t.expect(
value === 'testGetterValue',
`replacing getter: '${getter}' on ${type} returns test value`
);
} finally {
Object.defineProperty(ctorPrototype, getter, origProperties);
}
// Check it turns the same value after restoring as before restoring.
const afterValue = (obj as unknown as Record<string, () => unknown>)[getter];
assert(afterValue === origValue, `able to restore getter for instance of ${type}.${getter}`);
// Check getOwnProperty also returns the value we got before.
assert(
Object.getOwnPropertyDescriptor(ctorPrototype, getter)!.get === origProperties.get,
`getOwnPropertyDescriptor(${type}, '${getter}').get is original function`
);
}
});
g.test('method_replacement')
.desc(
`
Test that replacing methods on class prototypes works
This is a common pattern for shims and debugging libraries so make sure this pattern works.
`
)
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const { requiredKeys, getters, skipInCompatibility } = kResourceInfo[type];
t.skipIf(t.isCompatibility && !!skipInCompatibility, 'skipped in compatibility mode');
const gettersSet = new Set<string>(getters);
const methods = requiredKeys.filter(k => !gettersSet.has(k));
const obj = createResource(t, type);
for (const method of methods) {
const ctorPrototype = obj.constructor.prototype;
const origFunc = ctorPrototype[method];
t.expect(typeof origFunc === 'function', `${type}.prototype.${method} is a function`);
// Check the function the prototype and the one on the object are the same
t.expect(
(obj as unknown as Record<string, unknown>)[method] === origFunc,
`instance of ${type}.${method} === ${type}.prototype.${method}`
);
// Check replacing the method on constructor works.
try {
(ctorPrototype as unknown as Record<string, unknown>)[method] = function () {
return 'testMethodValue';
};
const value = (obj as unknown as Record<string, () => string>)[method]();
t.expect(
value === 'testMethodValue',
`replacing method: '${method}' on ${type} returns test value`
);
} finally {
(ctorPrototype as unknown as Record<string, unknown>)[method] = origFunc;
}
// Check the function the prototype and the one on the object are the same after restoring.
assert(
(obj as unknown as Record<string, unknown>)[method] === origFunc,
`instance of ${type}.${method} === ${type}.prototype.${method}`
);
}
});
g.test('sameObject')
.desc(
`
Test that property that are supposed to return the sameObject do.
`
)
.params(u => u.combine('type', kResources))
.fn(t => {
const { type } = t.params;
const { sameObject } = kResourceInfo[type];
const obj = createResource(t, type);
for (const property of sameObject) {
const origValue1 = (obj as unknown as Record<string, Record<string, string>>)[property];
const origValue2 = (obj as unknown as Record<string, Record<string, string>>)[property];
t.expect(typeof origValue1 === 'object');
t.expect(typeof origValue2 === 'object');
// Seems like this should be enough if they are objects.
t.expect(origValue1 === origValue2);
// add a property
origValue1['foo'] = 'test';
// see that it appears on the object.
t.expect(origValue1.foo === 'test');
t.expect(origValue2.foo === 'test');
// Delete the property.
delete origValue2.foo;
// See it was removed.
assert(origValue1.foo === undefined);
assert(origValue2.foo === undefined);
}
});
const kClassInheritanceTests: { [key: string]: () => boolean } = {
GPUDevice: () => GPUDevice.prototype instanceof EventTarget,
GPUPipelineError: () => GPUPipelineError.prototype instanceof DOMException,
GPUValidationError: () => GPUValidationError.prototype instanceof GPUError,
GPUOutOfMemoryError: () => GPUOutOfMemoryError.prototype instanceof GPUError,
GPUInternalError: () => GPUInternalError.prototype instanceof GPUError,
GPUUncapturedErrorEvent: () => GPUUncapturedErrorEvent.prototype instanceof Event,
} as const;
g.test('inheritance')
.desc(
`
Test that objects inherit from the correct base class
This is important because apps might patch the base class
and expect instances of these classes to respond correctly.
`
)
.params(u => u.combine('type', keysOf(kClassInheritanceTests)))
.fn(t => {
const fn = kClassInheritanceTests[t.params.type];
t.expect(fn(), fn.toString());
});
const kDispatchTests = {
async canPassEventThroughDevice(t: GPUTest) {
const result = await raceWithRejectOnTimeout(
new Promise(resolve => {
t.device.addEventListener('foo', resolve, { once: true });
t.device.dispatchEvent(new Event('foo'));
}),
500,
'timeout'
);
const event = result as Event;
t.expect(() => event instanceof Event, 'event');
t.expect(() => event.type === 'foo');
},
async canPassCustomEventThroughDevice(t: GPUTest) {
const result = await raceWithRejectOnTimeout(
new Promise(resolve => {
t.device.addEventListener('bar', resolve, { once: true });
t.device.dispatchEvent(new CustomEvent('bar'));
}),
500,
'timeout'
);
const event = result as CustomEvent;
t.expect(() => event instanceof CustomEvent);
t.expect(() => event instanceof Event);
t.expect(() => event.type === 'bar');
},
async patchingEventTargetAffectsDevice(t: GPUTest) {
let addEventListenerWasCalled = false;
let dispatchEventWasCalled = false;
let removeEventListenerWasCalled = false;
// eslint-disable-next-line @typescript-eslint/unbound-method
const origFnAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (
...args: Parameters<typeof EventTarget.prototype.addEventListener>
) {
addEventListenerWasCalled = true;
return origFnAddEventListener.call(this, ...args);
};
// eslint-disable-next-line @typescript-eslint/unbound-method
const origFnDispatchEvent = EventTarget.prototype.dispatchEvent;
EventTarget.prototype.dispatchEvent = function (event: Event) {
dispatchEventWasCalled = true;
return origFnDispatchEvent.call(this, event);
};
// eslint-disable-next-line @typescript-eslint/unbound-method
const origFnRemoveEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function (
...args: Parameters<typeof EventTarget.prototype.removeEventListener>
) {
removeEventListenerWasCalled = true;
return origFnRemoveEventListener.call(this, ...args);
};
try {
await raceWithRejectOnTimeout(
new Promise(resolve => {
t.device.addEventListener('foo', resolve);
t.device.dispatchEvent(new Event('foo'));
t.device.removeEventListener('foo', resolve);
}),
500,
'timeout'
);
} finally {
EventTarget.prototype.addEventListener = origFnAddEventListener;
EventTarget.prototype.dispatchEvent = origFnDispatchEvent;
EventTarget.prototype.removeEventListener = origFnRemoveEventListener;
}
t.expect(addEventListenerWasCalled, 'overriding EventTarget addEventListener worked');
t.expect(dispatchEventWasCalled, 'overriding EventTarget dispatchEvent worked');
t.expect(removeEventListenerWasCalled, 'overriding EventTarget removeEventListener worked');
},
async passingGPUUncapturedErrorEventWorksThoughEventTarget(t: GPUTest) {
const target = new EventTarget();
const result = await raceWithRejectOnTimeout(
new Promise(resolve => {
target.addEventListener('uncapturederror', resolve, { once: true });
target.dispatchEvent(
new GPUUncapturedErrorEvent('uncapturederror', {
error: new GPUValidationError('test error'),
})
);
}),
500,
'timeout'
);
const event = result as GPUUncapturedErrorEvent;
t.expect(() => event instanceof GPUUncapturedErrorEvent);
t.expect(() => event.error instanceof GPUValidationError);
t.expect(() => event.error.message === 'test error');
},
} as const;
g.test('device,EventTarget')
.desc(
`
Test some repercussions of the fact that GPUDevice extends EventTarget
`
)
.params(u => u.combine('test', keysOf(kDispatchTests)))
.fn(async t => {
await kDispatchTests[t.params.test](t);
});
const kAddEventListenerTests = {
EventHandler: async (t: GPUTest) => {
const result = await raceWithRejectOnTimeout(
new Promise(resolve => {
t.device.addEventListener('foo', resolve, { once: true });
t.device.dispatchEvent(new Event('foo'));
}),
500,
'timeout'
);
const event = result as Event;
t.expect(() => event instanceof Event, 'event');
t.expect(() => event.type === 'foo');
},
EventListener: async (t: GPUTest) => {
const result = await raceWithRejectOnTimeout(
new Promise(resolve => {
t.device.addEventListener(
'foo',
{
handleEvent: resolve,
},
{ once: true }
);
t.device.dispatchEvent(new Event('foo'));
}),
500,
'timeout'
);
const event = result as Event;
t.expect(() => event instanceof Event, 'event');
t.expect(() => event.type === 'foo');
},
} as const;
g.test('device,addEventListener')
.desc(
`
Test that addEventListener works with both an EventListener and an EventHandler
* https://dom.spec.whatwg.org/#interface-eventtarget
* https://html.spec.whatwg.org/multipage/webappapis.html#eventhandler
`
)
.params(u => u.combine('test', keysOf(kAddEventListenerTests)))
.fn(async t => {
await kAddEventListenerTests[t.params.test](t);
});