import {
Fixture,
FixtureClass,
FixtureClassInterface,
FixtureClassWithMixin,
SubcaseBatchState,
TestCaseRecorder,
TestParams,
} from '../common/framework/fixture.js'; import { registerShutdownTask } from '../common/framework/on_shutdown.js'; import { globalTestConfig, isCompatibilityDevice } from '../common/framework/test_config.js'; import { getGPU } from '../common/util/navigator_gpu.js'; import { assert,
makeValueTestVariant,
memcpy,
range,
ValueTestVariant,
TypedArrayBufferView,
TypedArrayBufferViewConstructor,
unreachable,
hasFeature,
} from '../common/util/util.js';
import { kPossibleLimits, kQueryTypeInfo, WGSLLanguageFeature } from './capability_info.js'; import { InterpolationType, InterpolationSampling } from './constants.js'; import {
resolvePerAspectFormat,
SizedTextureFormat,
EncodableTextureFormat,
isCompressedTextureFormat,
getRequiredFeatureForTextureFormat,
isTextureFormatUsableAsRenderAttachment,
isTextureFormatMultisampled,
isTextureFormatResolvable,
isDepthTextureFormat,
isStencilTextureFormat,
textureViewDimensionAndFormatCompatibleForDevice,
textureDimensionAndFormatCompatibleForDevice,
isTextureFormatUsableWithStorageAccessMode,
isTextureFormatUsableWithCopyExternalImageToTexture,
isTextureFormatFilterable,
isTextureFormatBlendable,
} from './format_info.js'; import { checkElementsEqual, checkElementsBetween } from './util/check_contents.js'; import { CommandBufferMaker, EncoderType } from './util/command_buffer_maker.js'; import { ScalarType } from './util/conversion.js'; import {
CanonicalDeviceDescriptor,
DescriptorModifier,
DevicePool,
DeviceProvider,
UncanonicalizedDeviceDescriptor,
} from './util/device_pool.js'; import { align, roundDown } from './util/math.js'; import {
getTextureCopyLayout,
getTextureSubCopyLayout,
LayoutOptions as TextureLayoutOptions,
} from './util/texture/layout.js'; import { PerTexelComponent, kTexelRepresentationInfo } from './util/texture/texel_data.js'; import { reifyExtent3D, reifyOrigin3D } from './util/unions.js';
// Declarations for WebGPU items we want tests for that are not yet officially part of the spec.
declare global { // MAINTENANCE_TODO: remove once added to @webgpu/types interface GPUSupportedLimits {
readonly maxStorageBuffersInFragmentStage?: number;
readonly maxStorageTexturesInFragmentStage?: number;
readonly maxStorageBuffersInVertexStage?: number;
readonly maxStorageTexturesInVertexStage?: number;
}
}
const devicePool = new DevicePool();
// MAINTENANCE_TODO: When DevicePool becomes able to provide multiple devices at once, use the // usual one instead of a new one. const mismatchedDevicePool = new DevicePool();
// On shutdown, try to explicitly destroy() the device pools (and devices) used by GPUTest, // so they don't keep using system resources until they're fully garbage collected.
registerShutdownTask(() => {
devicePool.destroy();
mismatchedDevicePool.destroy();
});
// Ensure devicePool.release is called for both providers even if one rejects // and wait for both of them before proceeding. const results = await Promise.allSettled([ this.provider?.then(x => devicePool.release(x)), this.mismatchedProvider?.then(x => mismatchedDevicePool.release(x)),
]);
// If one of them rejected throw its reason. It should be an `Error`. for (const result of results) { if (result.status === 'rejected') throw result.reason;
}
}
/** @internal MAINTENANCE_TODO: Make this not visible to test code? */
acquireProvider(): Promise<DeviceProvider> { if (this.provider === undefined) { this.requestDeviceWithRequiredParametersOrSkip(this.skipIfRequirements);
} assert(this.provider !== undefined); assert(!this.useMismatchedDevice || this.mismatchedProvider !== undefined);
return this.provider;
}
get isCompatibility() {
return globalTestConfig.compatibility;
}
/** @internal MAINTENANCE_TODO: Make this not visible to test code? */
acquireMismatchedProvider(): Promise<DeviceProvider> | undefined {
return this.mismatchedProvider;
}
skipIfCopyTextureToTextureNotSupportedForFormat(...formats: (GPUTextureFormat | undefined)[]) { if (this.isCompatibility) { for (const format of formats) { if (format && isCompressedTextureFormat(format)) { this.skip(`copyTextureToTexture with ${format} is not supported in compatibility mode`);
}
}
}
}
/** *Skipstestifthegiveninterpolationtypeorsamplingisnotsupported.
*/
skipIfInterpolationTypeOrSamplingNotSupported({
type,
sampling,
}: {
type?: InterpolationType;
sampling?: InterpolationSampling;
}) { if (this.isCompatibility) { this.skipIf(
type === 'linear', 'interpolation type linear is not supported in compatibility mode'
); this.skipIf(
sampling === 'sample', 'interpolation type linear is not supported in compatibility mode'
); this.skipIf(
type === 'flat' && (!sampling || sampling === 'first'), 'interpolation type flat with sampling not set to either is not supported in compatibility mode'
);
}
}
/** Skips this test case if the `langFeature` is *not* supported. */
skipIfLanguageFeatureNotSupported(langFeature: WGSLLanguageFeature) { if (!this.hasLanguageFeature(langFeature)) { this.skip(`WGSL language feature '${langFeature}' is not supported`);
}
}
/** Skips this test case if the `langFeature` is supported. */
skipIfLanguageFeatureSupported(langFeature: WGSLLanguageFeature) { if (this.hasLanguageFeature(langFeature)) { this.skip(`WGSL language feature '${langFeature}' is supported`);
}
}
// 2. Map the staging buffer, and create the TypedArray from it.
await mappable.mapAsync(GPUMapMode.READ, mapOffset, mapSize); const mapped = new type(mappable.getMappedRange(mapOffset, mapSize)); const data = mapped.subarray(subarrayStart, typedLength) as T;
/** *Skipstestifdevicedoesnothavefeature. *Note:TrytouseoneofthemorespecificskipIftestsifpossible.
*/
skipIfDeviceDoesNotHaveFeature(feature: GPUFeatureName) { this.skipIf(
!hasFeature(this.device.features, feature),
`device does not have feature: '${feature}'`
);
}
/** *Skipstestifdevicedesnotsupportquerytype.
*/
skipIfDeviceDoesNotSupportQueryType(...types: GPUQueryType[]) { for (const type of types) { const feature = kQueryTypeInfo[type].feature; if (feature) { this.skipIfDeviceDoesNotHaveFeature(feature);
}
}
}
skipIfDepthTextureCanNotBeUsedWithNonComparisonSampler() { this.skipIf( this.isCompatibility, 'depth textures are not usable with non-comparison samplers in compatibility mode'
);
}
/** *Skipstestifanyformatisnotsupported.
*/
skipIfTextureFormatNotSupported(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (!format) { continue;
} if (format === 'bgra8unorm-srgb') { if (isCompatibilityDevice(this.device)) { this.skip(`texture format '${format}' is not supported`);
}
} const feature = getRequiredFeatureForTextureFormat(format); this.skipIf(
!!feature && !hasFeature(this.device.features, feature),
`texture format '${format}' requires feature: '${feature}'`
);
}
}
skipIfTextureFormatAndViewDimensionNotCompatible(
format: GPUTextureFormat,
viewDimension: GPUTextureViewDimension
) { this.skipIf(
!textureViewDimensionAndFormatCompatibleForDevice( this.device.features,
viewDimension,
format
),
`format: ${format} does not support viewDimension: ${viewDimension}`
);
}
skipIfTextureFormatAndDimensionNotCompatible(
format: GPUTextureFormat,
dimension: GPUTextureDimension | undefined
) { this.skipIf(
!textureDimensionAndFormatCompatibleForDevice(this.device.features, dimension, format),
`format: ${format} does not support dimension: ${dimension}`
);
}
skipIfTextureFormatNotResolvable(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (format === undefined) continue; if (!isTextureFormatResolvable(this.device.features, format)) { this.skip(`texture format '${format}' is not resolvable`);
}
}
}
skipIfTextureViewDimensionNotSupported(...dimensions: (GPUTextureViewDimension | undefined)[]) { if (isCompatibilityDevice(this.device)) { for (const dimension of dimensions) { if (dimension === 'cube-array') { this.skip(`texture view dimension '${dimension}' is not supported`);
}
}
}
}
skipIfCopyTextureToTextureNotSupportedForFormat(...formats: (GPUTextureFormat | undefined)[]) { if (isCompatibilityDevice(this.device)) { for (const format of formats) { if (format && isCompressedTextureFormat(format)) { this.skip(`copyTextureToTexture with ${format} is not supported`);
}
}
}
}
skipIfTextureLoadNotSupportedForTextureType(...types: (string | undefined | null)[]) { if (this.isCompatibility) { for (const type of types) { switch (type) { case'texture_depth_2d': case'texture_depth_2d_array': case'texture_depth_multisampled_2d': this.skip(`${type} is not supported by textureLoad in compatibility mode`);
}
}
}
}
skipIfTextureFormatNotUsableWithStorageAccessMode(
access: GPUStorageTextureAccess | 'read' | 'write' | 'read_write',
...formats: (GPUTextureFormat | undefined)[]
) { for (const format of formats) { if (!format) continue;
if (!isTextureFormatUsableWithStorageAccessMode(this.device.features, format, access)) { this.skip(
`Texture with ${format} is not usable as a storage texture with access ${access}`
);
}
}
}
skipIfTextureFormatNotUsableAsRenderAttachment(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (format && !isTextureFormatUsableAsRenderAttachment(this.device.features, format)) { this.skip(`Texture with ${format} is not usable as a render attachment`);
}
}
}
skipIfTextureFormatNotMultisampled(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (format === undefined) continue; if (!isTextureFormatMultisampled(this.device.features, format)) { this.skip(`texture format '${format}' does not support multisampling`);
}
}
}
skipIfTextureFormatNotBlendable(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (format === undefined) continue; this.skipIf(
!isTextureFormatBlendable(this.device.features, format),
`${format} is not blendable`
);
}
}
skipIfTextureFormatNotFilterable(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (format === undefined) continue; this.skipIf(
!isTextureFormatFilterable(this.device.features, format),
`${format} is not filterable`
);
}
}
skipIfTextureFormatDoesNotSupportUsage(
usage: GPUTextureUsageFlags,
...formats: (GPUTextureFormat | undefined)[]
) { for (const format of formats) { if (!format) continue; if (usage & GPUTextureUsage.RENDER_ATTACHMENT) { this.skipIfTextureFormatNotUsableAsRenderAttachment(format);
} if (usage & GPUTextureUsage.STORAGE_BINDING) { this.skipIfTextureFormatNotUsableWithStorageAccessMode('write-only', format);
}
}
}
skipIfTextureFormatDoesNotSupportCopyTextureToBuffer(format: GPUTextureFormat) { this.skipIf(
!this.canCallCopyTextureToBufferWithTextureFormat(format),
`can not use copyTextureToBuffer with ${format}`
);
}
skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(format: GPUTextureFormat) { this.skipIf(
!isTextureFormatUsableWithCopyExternalImageToTexture(this.device.features, format),
`can not use copyExternalImageToTexture with ${format}`
);
}
/** Skips this test case if the `langFeature` is *not* supported. */
skipIfLanguageFeatureNotSupported(langFeature: WGSLLanguageFeature) { if (!this.hasLanguageFeature(langFeature)) { this.skip(`WGSL language feature '${langFeature}' is not supported`);
}
}
/** Skips this test case if the `langFeature` is supported. */
skipIfLanguageFeatureSupported(langFeature: WGSLLanguageFeature) { if (this.hasLanguageFeature(langFeature)) { this.skip(`WGSL language feature '${langFeature}' is supported`);
}
}
/** returns true if the `langFeature` is supported */
hasLanguageFeature(langFeature: WGSLLanguageFeature) { const lf = getGPU(this.rec).wgslLanguageFeatures;
return lf !== undefined && lf.has(langFeature);
}
/** Skips this test case if the GPUTextureUsage `TRANSIENT_ATTACHMENT` is *not* supported. */ // MAINTENANCE_TODO(#4509): Remove this after all implementations have TRANSIENT_ATTACHMENT.
skipIfTransientAttachmentNotSupported() { const isTransientAttachmentSupported = 'TRANSIENT_ATTACHMENT' in GPUTextureUsage; this.skipIf(
!isTransientAttachmentSupported, 'GPUTextureUsage TRANSIENT_ATTACHMENT is not supported'
);
}
// If the buffer is small enough, just generate the full expected buffer contents and check // against them on the CPU. const kMaxBufferSizeToCheckOnCpu = 256 * 1024; const bufferSize = bytesPerRow * (numRows - 1) + minBytesPerRow; if (bufferSize <= kMaxBufferSizeToCheckOnCpu) { const valueBytes = Array.from(new Uint8Array(expectedValue)); const rowValues = new Array(minBytesPerRow / valueSize).fill(valueBytes); const rowBytes = new Uint8Array([].concat(...rowValues)); const expectedContents = new Uint8Array(bufferSize);
range(numRows, row => expectedContents.set(rowBytes, row * bytesPerRow)); this.expectGPUBufferValuesEqual(buffer, expectedContents);
return;
}
// Copy into a buffer suitable for STORAGE usage. const storageBuffer = this.createBufferTracked({
label: 'expectGPUBufferRepeatsSingleValue:storageBuffer',
size: bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
// This buffer conveys the data we expect to see for a single value read. Since we read 32 bits at // a time, for values smaller than 32 bits we pad this expectation with repeated value data, or // with zeroes if the width of a row in the buffer is less than 4 bytes. For value sizes larger // than 32 bits, we assume they're a multiple of 32 bits and expect to read exact matches of // `expectedValue` as-is. const expectedDataSize = Math.max(4, valueSize); const expectedDataBuffer = this.createBufferTracked({
label: 'expectGPUBufferRepeatsSingleValue:expectedDataBuffer',
size: expectedDataSize,
usage: GPUBufferUsage.STORAGE,
mappedAtCreation: true,
}); const expectedData = new Uint32Array(expectedDataBuffer.getMappedRange()); if (valueSize === 1) { const value = new Uint8Array(expectedValue)[0]; const values = new Array(Math.min(4, minBytesPerRow)).fill(value); const padding = new Array(Math.max(0, 4 - values.length)).fill(0); const expectedBytes = new Uint8Array(expectedData.buffer);
expectedBytes.set([...values, ...padding]);
} elseif (valueSize === 2) { const value = new Uint16Array(expectedValue)[0]; const expectedWords = new Uint16Array(expectedData.buffer);
expectedWords.set([value, minBytesPerRow > 2 ? value : 0]);
} else {
expectedData.set(new Uint32Array(expectedValue));
}
expectedDataBuffer.unmap();
// The output buffer has one 32-bit entry per buffer row. An entry's value will be 1 if every // read from the corresponding row matches the expected data derived above, or 0 otherwise. const resultBuffer = this.createBufferTracked({
label: 'expectGPUBufferRepeatsSingleValue:resultBuffer',
size: numRows * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
format = resolvePerAspectFormat(format, layout?.aspect); const { byteLength, minBytesPerRow, bytesPerRow, rowsPerImage, mipSize } = getTextureCopyLayout(
format,
dimension,
size,
layout
); // MAINTENANCE_TODO: getTextureCopyLayout does not return the proper size for array textures, // i.e. it will leave the z/depth value as is instead of making it 1 when dealing with 2d // texture arrays. Since we are passing in the dimension, we should update it to return the // corrected size. const copySize = [
mipSize[0],
dimension !== '1d' ? mipSize[1] : 1,
dimension === '3d' ? mipSize[2] : 1,
];
const rep = kTexelRepresentationInfo[format as EncodableTextureFormat]; const expectedTexelData = rep.pack(rep.encode(exp));
/** *ExpectthespecifiedWebGPUerrortobegeneratedwhenrunningtheprovidedfunction.
*/
expectGPUError<R>(filter: GPUErrorFilter, fn: () => R, shouldError: boolean = true): R { // If no error is expected, we let the scope surrounding the test catch it. if (!shouldError) {
return fn();
}
/** *Expectavalidationerrorinsidethecallback. * *TestsshouldalwaysdojustoneWebGPUcallinthecallback,tomakesurethat'swhat'stested.
*/
expectValidationError(fn: () => void, shouldError: boolean = true): void { // If no error is expected, we let the scope surrounding the test catch it. if (shouldError) { this.device.pushErrorScope('validation');
}
// Note: A return value is not allowed for the callback function. This is to avoid confusion // about what the actual behavior would be; either of the following could be reasonable: // - Make expectValidationError async, and have it await on fn(). This causes an async split // between pushErrorScope and popErrorScope, so if the caller doesn't `await` on // expectValidationError (either accidentally or because it doesn't care to do so), then // other test code will be (nondeterministically) caught by the error scope. // - Make expectValidationError NOT await fn(), but just execute its first block (until the // first await) and return the return value (a Promise). This would be confusing because it // would look like the error scope includes the whole async function, but doesn't. // If we do decide we need to return a value, we should use the latter semantic. const returnValue = fn() as unknown; assert(
returnValue === undefined, 'expectValidationError callback should not return a value (or be async)'
);
if (shouldError) { const promise = this.device.popErrorScope();
/** Create a GPUBuffer and track it for cleanup at the end of the test. */
createBufferTracked(descriptor: GPUBufferDescriptor): GPUBuffer {
return this.trackForCleanup(this.device.createBuffer(descriptor));
}
/** Create a GPUTexture and track it for cleanup at the end of the test. */
createTextureTracked(descriptor: GPUTextureDescriptor): GPUTexture {
return this.trackForCleanup(this.device.createTexture(descriptor));
}
/** Create a GPUQuerySet and track it for cleanup at the end of the test. */
createQuerySetTracked(descriptor: GPUQuerySetDescriptor): GPUQuerySet {
return this.trackForCleanup(this.device.createQuerySet(descriptor));
}
/** *FixtureforWebGPUteststhatusesaDeviceProvider
*/
export class GPUTest extends GPUTestBase { // Should never be undefined in a test. If it is, init() must not have run/finished. private provider: DeviceProvider | undefined; private mismatchedProvider: DeviceProvider | undefined;
/** GPUAdapter that the device was created from. */
get adapter(): GPUAdapter { assert(this.provider !== undefined, 'internal error: DeviceProvider missing');
return this.provider.adapter;
}
/** *GPUDevicefortestsrequiringaseconddevicedifferentfromthedefaultone, *e.g.forcreatingobjectsforbydevice_mismatchvalidationtests.
*/
get mismatchedDevice(): GPUDevice { assert( this.mismatchedProvider !== undefined, 'usesMismatchedDevice or selectMismatchedDeviceOrSkipTestCase was not called in beforeAllSubcases'
);
return this.mismatchedProvider.device;
}
/** *GetstheadapterlimitsasastandardJavaScriptobject.
*/ function getAdapterLimitsAsDeviceRequiredLimits(adapter: GPUAdapter) { const requiredLimits: Record<string, GPUSize64> = {}; const adapterLimits = adapter.limits as unknown as Record<string, GPUSize64>; for (const key in adapter.limits) { // MAINTENANCE_TODO: Remove this once minSubgroupSize is removed from // chromium. if (key === 'maxSubgroupSize' || key === 'minSubgroupSize') { continue;
}
requiredLimits[key] = adapterLimits[key];
}
return requiredLimits;
}
/** *Removeslimitsthatdon'texistontheadapter. *Atestmightrequestanewlimitthatnotallimplementationssupport.Thetestitself *shouldchecktherequestedlimitusingcodethatexpectsundefined. * *```ts *t.skipIf(limit<2);// BAD! Doesn't skip if unsupported because undefined is never less than 2. *t.skipIf(!(limit>=2));// Good. Skips if limits is not >= 2. undefined is not >= 2. *```
*/ function removeNonExistentLimits(adapter: GPUAdapter, limits: Record<string, GPUSize64>) { const filteredLimits: Record<string, GPUSize64> = {}; const adapterLimits = adapter.limits as unknown as Record<string, GPUSize64>; for (const [limit, value] of Object.entries(limits)) { if (adapterLimits[limit] !== undefined) {
filteredLimits[limit] = value;
}
}
return filteredLimits;
}
function getAdapterFeaturesAsDeviceRequiredFeatures(adapter: GPUAdapter): Iterable<GPUFeatureName> {
return [...adapter.features].filter(
f => f !== 'core-features-and-limits'
) as Iterable<GPUFeatureName>;
}
/** *UseskipIfDeviceDoesNotHaveFeatureorsimilar.Ifyoureallyneedtotest *lackofafeature(forexampletestsunderwebgpu/api/validation/capability_checks) *thenuseUniqueFeaturesOrLimitsGPUTest
*/
override selectDeviceOrSkipTestCase(descriptor: DeviceSelectionDescriptor): void {
unreachable('this function should not be called in AllFeaturesMaxLimitsGPUTest');
}
/** *UseskipIfDeviceDoesNotHaveFeatureorsimilar.
*/
override selectDeviceForQueryTypeOrSkipTestCase(types: GPUQueryType | GPUQueryType[]): void {
unreachable('this function should not be called in AllFeaturesMaxLimitsGPUTest');
}
/** *UseskipIfDeviceDoesNotHaveFeatureorskipIf(device.limits.maxXXX<requiredXXX)etc...
*/
override selectDeviceForTextureFormatOrSkipTestCase(
formats: GPUTextureFormat | undefined | (GPUTextureFormat | undefined)[]
): void {
unreachable('this function should not be called in AllFeaturesMaxLimitsGPUTest');
}
/** *UseskipIfDeviceDoesNotHaveFeatureorskipIf(device.limits.maxXXX<requiredXXX)etc...
*/
selectMismatchedDeviceOrSkipTestCase(descriptor: DeviceSelectionDescriptor): void {
unreachable('this function should not be called in AllFeaturesMaxLimitsGPUTest');
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.