import {
Fixture,
FixtureClass,
FixtureClassInterface,
FixtureClassWithMixin,
SkipTestCase,
SubcaseBatchState,
TestCaseRecorder,
TestParams,
} from '../common/framework/fixture.js'; import { globalTestConfig } from '../common/framework/test_config.js'; import { getGPU } from '../common/util/navigator_gpu.js'; import { assert,
makeValueTestVariant,
memcpy,
range,
ValueTestVariant,
TypedArrayBufferView,
TypedArrayBufferViewConstructor,
unreachable,
} from '../common/util/util.js';
import {
getDefaultLimits,
kLimits,
kQueryTypeInfo,
WGSLLanguageFeature,
} from './capability_info.js'; import { InterpolationType, InterpolationSampling } from './constants.js'; import {
kTextureFormatInfo,
kEncodableTextureFormats,
resolvePerAspectFormat,
SizedTextureFormat,
EncodableTextureFormat,
isCompressedTextureFormat,
ColorTextureFormat,
isTextureFormatUsableAsStorageFormat,
} 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 { physicalMipSizeFromTexture, virtualMipSize } from './util/texture/base.js'; import {
bytesInACompleteRow,
getTextureCopyLayout,
getTextureSubCopyLayout,
LayoutOptions as TextureLayoutOptions,
} from './util/texture/layout.js'; import { PerTexelComponent, kTexelRepresentationInfo } from './util/texture/texel_data.js'; import { TexelView } from './util/texture/texel_view.js'; import {
PerPixelComparison,
PixelExpectation,
TexelCompareOptions,
textureContentIsOKByT2B,
} from './util/texture/texture_ok.js'; import { createTextureFromTexelViews } from './util/texture.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();
// Ensure devicePool.release is called for both providers even if one rejects.
await Promise.all([ this.provider?.then(x => devicePool.release(x)), this.mismatchedProvider?.then(x => devicePool.release(x)),
]);
}
/** @internal MAINTENANCE_TODO: Make this not visible to test code? */
acquireProvider(): Promise<DeviceProvider> { if (this.provider === undefined) { this.selectDeviceOrSkipTestCase(undefined);
} assert(this.provider !== undefined); returnthis.provider;
}
get isCompatibility() { return globalTestConfig.compatibility;
}
/** @internal MAINTENANCE_TODO: Make this not visible to test code? */
acquireMismatchedProvider(): Promise<DeviceProvider> | undefined { returnthis.mismatchedProvider;
}
/** Throws an exception marking the subcase as skipped. */
skip(msg: string): never { thrownew SkipTestCase(msg);
}
/** Throws an exception making the subcase as skipped if condition is true */
skipIf(cond: boolean, msg: string | (() => string) = '') { if (cond) { this.skip(typeof msg === 'function' ? msg() : msg);
}
}
/** *Skipstestifanyformatisnotsupported.
*/
skipIfTextureFormatNotSupported(...formats: (GPUTextureFormat | undefined)[]) { if (this.isCompatibility) { for (const format of formats) { if (format === 'bgra8unorm-srgb') { this.skip(`texture format '${format} is not supported`);
}
}
}
}
skipIfCopyTextureToTextureNotSupportedForFormat(...formats: (GPUTextureFormat | undefined)[]) { if (this.isCompatibility) { for (const format of formats) { if (format && isCompressedTextureFormat(format)) { this.skip(`copyTextureToTexture with ${format} is not supported`);
}
}
}
}
skipIfTextureViewDimensionNotSupported(...dimensions: (GPUTextureViewDimension | undefined)[]) { if (this.isCompatibility) { for (const dimension of dimensions) { if (dimension === 'cube-array') { this.skip(`texture view dimension '${dimension}' is not supported`);
}
}
}
}
skipIfTextureFormatNotUsableAsStorageTexture(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (format && !isTextureFormatUsableAsStorageFormat(format, this.isCompatibility)) { this.skip(`Texture with ${format} is not usable as a storage texture`);
}
}
}
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`);
}
}
}
}
/** *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 a depth texture can not be used with a non-comparison sampler. */
skipIfDepthTextureCanNotBeUsedWithNonComparisonSampler() { this.skipIf( this.isCompatibility, 'depth textures are not usable with non-comparison samplers 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;
/** *Skipstestifanyformatisnotsupported.
*/
skipIfTextureFormatNotSupported(...formats: (GPUTextureFormat | undefined)[]) { if (this.isCompatibility) { for (const format of formats) { if (format === 'bgra8unorm-srgb') { this.skip(`texture format '${format} is not supported`);
}
}
}
}
skipIfTextureViewDimensionNotSupported(...dimensions: (GPUTextureViewDimension | undefined)[]) { if (this.isCompatibility) { for (const dimension of dimensions) { if (dimension === 'cube-array') { this.skip(`texture view dimension '${dimension}' is not supported`);
}
}
}
}
skipIfCopyTextureToTextureNotSupportedForFormat(...formats: (GPUTextureFormat | undefined)[]) { if (this.isCompatibility) { for (const format of formats) { if (format && isCompressedTextureFormat(format)) { this.skip(`copyTextureToTexture with ${format} is not supported`);
}
}
}
}
skipIfTextureFormatNotUsableAsStorageTexture(...formats: (GPUTextureFormat | undefined)[]) { for (const format of formats) { if (format && !isTextureFormatUsableAsStorageFormat(format, this.isCompatibility)) { this.skip(`Texture with ${format} is not usable as a storage texture`);
}
}
}
/** 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`);
}
}
// 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({
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({
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({
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 { returnthis.trackForCleanup(this.device.createBuffer(descriptor));
}
/** Create a GPUTexture and track it for cleanup at the end of the test. */
createTextureTracked(descriptor: GPUTextureDescriptor): GPUTexture { returnthis.trackForCleanup(this.device.createTexture(descriptor));
}
/** Create a GPUQuerySet and track it for cleanup at the end of the test. */
createQuerySetTracked(descriptor: GPUQuerySetDescriptor): GPUQuerySet { returnthis.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'); returnthis.provider.adapter;
}
/** *GPUDevicefortestsrequiringaseconddevicedifferentfromthedefaultone, *e.g.forcreatingobjectsforbydevice_mismatchvalidationtests.
*/
get mismatchedDevice(): GPUDevice { assert( this.mismatchedProvider !== undefined, 'selectMismatchedDeviceOrSkipTestCase was not called in beforeAllSubcases'
); returnthis.mismatchedProvider.device;
}
expectSinglePixelComparisonsAreOkInTexture<E extends PixelExpectation>(
src: GPUTexelCopyTextureInfo,
exp: PerPixelComparison<E>[],
comparisonOptions = {
maxIntDiff: 0,
maxDiffULPsForNormFormat: 1,
maxDiffULPsForFloatFormat: 1,
}
): void { assert(exp.length > 0, 'must specify at least one pixel comparison'); assert(
(kEncodableTextureFormats as GPUTextureFormat[]).includes(src.texture.format),
() => `${src.texture.format} is not an encodable format`
); const lowerCorner = [src.texture.width, src.texture.height, src.texture.depthOrArrayLayers]; const upperCorner = [0, 0, 0]; const expMap = new Map<string, E>(); const coords: Required<GPUOrigin3DDict>[] = []; for (const e of exp) { const coord = reifyOrigin3D(e.coord); const coordKey = JSON.stringify(coord);
coords.push(coord);
// Compute the minimum sub-rect that encompasses all the pixel comparisons. The // `lowerCorner` will become the origin, and the `upperCorner` will be used to compute the // size.
lowerCorner[0] = Math.min(lowerCorner[0], coord.x);
lowerCorner[1] = Math.min(lowerCorner[1], coord.y);
lowerCorner[2] = Math.min(lowerCorner[2], coord.z);
upperCorner[0] = Math.max(upperCorner[0], coord.x);
upperCorner[1] = Math.max(upperCorner[1], coord.y);
upperCorner[2] = Math.max(upperCorner[2], coord.z);
// Build a sparse map of the coordinates to the expected colors for the texel view. assert(
!expMap.has(coordKey),
() => `duplicate pixel expectation at coordinate (${coord.x},${coord.y},${coord.z})`
);
expMap.set(coordKey, e.exp);
} const size: GPUExtent3D = [
upperCorner[0] - lowerCorner[0] + 1,
upperCorner[1] - lowerCorner[1] + 1,
upperCorner[2] - lowerCorner[2] + 1,
];
let expTexelView: TexelView; if (Symbol.iterator in exp[0].exp) {
expTexelView = TexelView.fromTexelsAsBytes(
src.texture.format as EncodableTextureFormat,
coord => { const res = expMap.get(JSON.stringify(coord)); assert(
res !== undefined,
() => `invalid coordinate (${coord.x},${coord.y},${coord.z}) in sparse texel view`
); return res as Uint8Array;
}
);
} else {
expTexelView = TexelView.fromTexelsAsColors(
src.texture.format as EncodableTextureFormat,
coord => { const res = expMap.get(JSON.stringify(coord)); assert(
res !== undefined,
() => `invalid coordinate (${coord.x},${coord.y},${coord.z}) in sparse texel view`
); return res as PerTexelComponent<number>;
}
);
} const coordsF = (function* () { for (const coord of coords) {
yield coord;
}
})();
expectTexturesToMatchByRendering(
actualTexture: GPUTexture,
expectedTexture: GPUTexture,
mipLevel: number,
origin: Required<GPUOrigin3DDict>,
size: Required<GPUExtent3DDict>
): void { // Render every layer of both textures at mipLevel to an rgba8unorm texture // that matches the size of the mipLevel. After each render, copy the // result to a buffer and expect the results from both textures to match. const { pipelineType, pipeline } = getPipelineToRenderTextureToRGB8UnormTexture( this.device,
actualTexture, this.isCompatibility
); const readbackPromisesPerTexturePerLayer = [actualTexture, expectedTexture].map(
(texture, ndx) => { const attachmentSize = virtualMipSize('2d', [texture.width, texture.height, 1], mipLevel); const attachment = this.createTextureTracked({
label: `readback${ndx}`,
size: attachmentSize,
format: 'rgba8unorm',
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
// Wait for all buffers to be ready for (const readbackPromises of readbackPromisesPerTexturePerLayer) {
readbacksPerTexturePerLayer.push(await Promise.all(readbackPromises));
}
function arrayNotAllTheSameValue(arr: TypedArrayBufferView | number[], msg?: string) { const first = arr[0]; return arr.length <= 1 || arr.findIndex(v => v !== first) >= 0
? undefined
: Error(`array is entirely ${first} so likely nothing was tested: ${msg || ''}`);
}
*iterateBlockRows(
size: Required<GPUExtent3DDict>,
format: ColorTextureFormat
): Generator<Required<GPUOrigin3DDict>> { if (size.width === 0 || size.height === 0 || size.depthOrArrayLayers === 0) { // do not iterate anything for an empty region return;
} const info = kTextureFormatInfo[format]; assert(size.height % info.blockHeight === 0); // Note: it's important that the order is in increasing memory address order. for (let z = 0; z < size.depthOrArrayLayers; ++z) { for (let y = 0; y < size.height; y += info.blockHeight) {
yield {
x: 0,
y,
z,
};
}
}
}
}
return TextureExpectations as unknown as FixtureClassWithMixin<F, TextureTestMixinType>;
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.48 Sekunden
(vorverarbeitet am 2026-06-10)
¤
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.