blob: b1f2e3e375cb35bd96c2a12f3b82964a54add433 [file] [log] [blame]
import { keysOf } from '../../../common/util/data_tables.js';
import {
AllFeaturesMaxLimitsGPUTest,
GPUTest,
UniqueFeaturesOrLimitsGPUTest,
} from '../../gpu_test.js';
const kEnables: Record<string, GPUFeatureName> = {
f16: 'shader-f16',
subgroups: 'subgroups' as GPUFeatureName,
clip_distances: 'clip-distances' as GPUFeatureName,
chromium_experimental_primitive_id: 'chromium-experimental-primitive-id' as GPUFeatureName,
};
/**
* Note: These regular expressions are not meant to be perfect. This is not production code expecting
* to work with any WGSL passed by a user. It's only test code for working with WGSL written
* in the CTS. A CTS test for which these regular expressions don't work should use a different set of
* testing functions or options that don't use these regular expressions.
*/
const kEnableREs = Object.entries(kEnables).map(([enableName, feature]) => {
return {
re: new RegExp(`\\benable\\s+(?:\\s*\\w+\\s*,)*\\s*${enableName}\\s*(?:,\\s*\\w+)*\\s*;`),
feature,
};
});
/**
* Note: This function is not meant to be perfect. This is not production code expecting
* to work with any WGSL passed by a user. It's only test code for working with WGSL written
* in the CTS. A CTS test for which this check doesn't work can choose a different set of
* testing functions or options that don't take this path.
*/
function skipIfCodeNeedsFeatureAndDeviceDoesNotHaveFeature(t: GPUTest, code: string) {
for (const { re, feature } of kEnableREs) {
if (re.test(code)) {
t.skipIfDeviceDoesNotHaveFeature(feature);
}
}
}
/**
* Base fixture for WGSL shader validation tests.
*/
export class ShaderValidationTest extends AllFeaturesMaxLimitsGPUTest {
/**
* Add a test expectation for whether a createShaderModule call succeeds or not.
* Note: skips test if 'enable X' exists in code and X's corresponding feature does not exist on device
* unless you pass in autoSkipIfFeatureNotAvailable: false.
*
* @example
* ```ts
* t.expectCompileResult(true, `wgsl code`); // Expect success
* t.expectCompileResult(false, `wgsl code`); // Expect validation error with any error string
* ```
*/
expectCompileResult(
expectedResult: boolean,
code: string,
options?: { autoSkipIfFeatureNotAvailable?: boolean } // defaults to true
) {
if (options?.autoSkipIfFeatureNotAvailable !== false) {
skipIfCodeNeedsFeatureAndDeviceDoesNotHaveFeature(this, code);
}
let shaderModule: GPUShaderModule;
this.expectGPUError(
'validation',
() => {
shaderModule = this.device.createShaderModule({ code });
},
expectedResult !== true
);
const error = new Error();
this.eventualAsyncExpectation(async () => {
const compilationInfo = await shaderModule!.getCompilationInfo();
// MAINTENANCE_TODO: Pretty-print error messages with source context.
const messagesLog =
compilationInfo.messages
.map(m => `${m.lineNum}:${m.linePos}: ${m.type}: ${m.message}`)
.join('\n') +
'\n\n---- shader ----\n' +
code;
if (compilationInfo.messages.some(m => m.type === 'error')) {
if (expectedResult) {
error.message = `Unexpected compilationInfo 'error' message.\n` + messagesLog;
this.rec.validationFailed(error);
} else {
error.message = `Found expected compilationInfo 'error' message.\n` + messagesLog;
this.rec.debug(error);
}
} else {
if (!expectedResult) {
error.message = `Missing expected compilationInfo 'error' message.\n` + messagesLog;
this.rec.validationFailed(error);
} else {
error.message = `No compilationInfo 'error' messages, as expected.\n` + messagesLog;
this.rec.debug(error);
}
}
});
}
/**
* Add a test expectation for whether a createShaderModule call issues a warning.
*
* @example
* ```ts
* t.expectCompileWarning(true, `wgsl code`); // Expect compile success and any warning message
* t.expectCompileWarning(false, `wgsl code`); // Expect compile success and no warning messages
* ```
*/
expectCompileWarning(expectWarning: boolean, code: string) {
let shaderModule: GPUShaderModule;
this.expectGPUError(
'validation',
() => {
shaderModule = this.device.createShaderModule({ code });
},
false
);
const error = new Error();
this.eventualAsyncExpectation(async () => {
const compilationInfo = await shaderModule!.getCompilationInfo();
// MAINTENANCE_TODO: Pretty-print error messages with source context.
const messagesLog = compilationInfo.messages
.map(m => `${m.lineNum}:${m.linePos}: ${m.type}: ${m.message}`)
.join('\n');
if (compilationInfo.messages.some(m => m.type === 'warning')) {
if (expectWarning) {
error.message = `No 'warning' message as expected.\n` + messagesLog;
this.rec.debug(error);
} else {
error.message = `Missing expected compilationInfo 'warning' message.\n` + messagesLog;
this.rec.validationFailed(error);
}
} else {
if (expectWarning) {
error.message = `Missing expected 'warning' message.\n` + messagesLog;
this.rec.validationFailed(error);
} else {
error.message = `Found a 'warning' message as expected.\n` + messagesLog;
this.rec.debug(error);
}
}
});
}
/**
* Add a test expectation for whether a createComputePipeline call succeeds or not.
* Note: skips test if 'enable X' exists in code and X's corresponding feature does not exist on device
* unless you pass in autoSkipIfFeatureNotAvailable: false
*/
expectPipelineResult(args: {
// True if the pipeline should build without error
expectedResult: boolean;
// The WGSL shader code
code: string;
// Pipeline overridable constants
constants?: Record<string, GPUPipelineConstantValue>;
// List of additional module-scope variable the entrypoint needs to reference
reference?: string[];
// List of additional statements to insert in the entry point.
statements?: string[];
// Skip tests when WGSL code has 'enable X' and feature for 'X' is not available on device
autoSkipIfFeatureNotAvailable?: boolean; // defaults to true. You must set to false to turn this off.
addWorkgroupSize?: boolean; // defaults to true. You must set to false to turn this off.
}) {
const phonies: Array<string> = [];
if (args.statements !== undefined) {
phonies.push(...args.statements);
}
if (args.constants !== undefined) {
phonies.push(...keysOf(args.constants).map(c => `_ = ${c};`));
}
if (args.reference !== undefined) {
phonies.push(...args.reference.map(c => `_ = ${c};`));
}
const code =
args.code +
(args.addWorkgroupSize !== false
? `
@workgroup_size(1)`
: ``) +
`
@compute fn main() {
${phonies.join('\n')}
}`;
if (args.autoSkipIfFeatureNotAvailable !== false) {
skipIfCodeNeedsFeatureAndDeviceDoesNotHaveFeature(this, code);
}
let shaderModule: GPUShaderModule;
this.expectGPUError(
'validation',
() => {
shaderModule = this.device.createShaderModule({ code });
},
false
);
this.expectGPUError(
'validation',
() => {
this.device.createComputePipeline({
layout: 'auto',
compute: { module: shaderModule!, entryPoint: 'main', constants: args.constants },
});
},
!args.expectedResult
);
}
/**
* Wraps the code fragment into an entry point.
*
* @example
* ```ts
* t.wrapInEntryPoint(`var i = 0;`);
* ```
*/
wrapInEntryPoint(code: string, enabledExtensions: string[] = []) {
const enableDirectives = enabledExtensions.map(x => `enable ${x};`).join('\n ');
return `
${enableDirectives}
@compute @workgroup_size(1)
fn main() {
${code}
}`;
}
}
// MAINTENANCE_TODO: Merge this with implementation above.
// NOTE: These things should probably not inherit. There is no relationship between
// these functions and a test. They could just as easily take a GPUTest as the first
// argument and then the all the problems associated with inheritance would disappear.
export class UniqueFeaturesAndLimitsShaderValidationTest extends UniqueFeaturesOrLimitsGPUTest {
/**
* Add a test expectation for whether a createShaderModule call succeeds or not.
*
* @example
* ```ts
* t.expectCompileResult(true, `wgsl code`); // Expect success
* t.expectCompileResult(false, `wgsl code`); // Expect validation error with any error string
* ```
*/
expectCompileResult(expectedResult: boolean, code: string) {
let shaderModule: GPUShaderModule;
this.expectGPUError(
'validation',
() => {
shaderModule = this.device.createShaderModule({ code });
},
expectedResult !== true
);
const error = new Error();
this.eventualAsyncExpectation(async () => {
const compilationInfo = await shaderModule!.getCompilationInfo();
// MAINTENANCE_TODO: Pretty-print error messages with source context.
const messagesLog =
compilationInfo.messages
.map(m => `${m.lineNum}:${m.linePos}: ${m.type}: ${m.message}`)
.join('\n') +
'\n\n---- shader ----\n' +
code;
if (compilationInfo.messages.some(m => m.type === 'error')) {
if (expectedResult) {
error.message = `Unexpected compilationInfo 'error' message.\n` + messagesLog;
this.rec.validationFailed(error);
} else {
error.message = `Found expected compilationInfo 'error' message.\n` + messagesLog;
this.rec.debug(error);
}
} else {
if (!expectedResult) {
error.message = `Missing expected compilationInfo 'error' message.\n` + messagesLog;
this.rec.validationFailed(error);
} else {
error.message = `No compilationInfo 'error' messages, as expected.\n` + messagesLog;
this.rec.debug(error);
}
}
});
}
/**
* Add a test expectation for whether a createShaderModule call issues a warning.
*
* @example
* ```ts
* t.expectCompileWarning(true, `wgsl code`); // Expect compile success and any warning message
* t.expectCompileWarning(false, `wgsl code`); // Expect compile success and no warning messages
* ```
*/
expectCompileWarning(expectWarning: boolean, code: string) {
let shaderModule: GPUShaderModule;
this.expectGPUError(
'validation',
() => {
shaderModule = this.device.createShaderModule({ code });
},
false
);
const error = new Error();
this.eventualAsyncExpectation(async () => {
const compilationInfo = await shaderModule!.getCompilationInfo();
// MAINTENANCE_TODO: Pretty-print error messages with source context.
const messagesLog = compilationInfo.messages
.map(m => `${m.lineNum}:${m.linePos}: ${m.type}: ${m.message}`)
.join('\n');
if (compilationInfo.messages.some(m => m.type === 'warning')) {
if (expectWarning) {
error.message = `No 'warning' message as expected.\n` + messagesLog;
this.rec.debug(error);
} else {
error.message = `Missing expected compilationInfo 'warning' message.\n` + messagesLog;
this.rec.validationFailed(error);
}
} else {
if (expectWarning) {
error.message = `Missing expected 'warning' message.\n` + messagesLog;
this.rec.validationFailed(error);
} else {
error.message = `Found a 'warning' message as expected.\n` + messagesLog;
this.rec.debug(error);
}
}
});
}
/**
* Add a test expectation for whether a createComputePipeline call succeeds or not.
*/
expectPipelineResult(args: {
// True if the pipeline should build without error
expectedResult: boolean;
// The WGSL shader code
code: string;
// Pipeline overridable constants
constants?: Record<string, GPUPipelineConstantValue>;
// List of additional module-scope variable the entrypoint needs to reference
reference?: string[];
// List of additional statements to insert in the entry point.
statements?: string[];
}) {
const phonies: Array<string> = [];
if (args.statements !== undefined) {
phonies.push(...args.statements);
}
if (args.constants !== undefined) {
phonies.push(...keysOf(args.constants).map(c => `_ = ${c};`));
}
if (args.reference !== undefined) {
phonies.push(...args.reference.map(c => `_ = ${c};`));
}
const code =
args.code +
`
@compute @workgroup_size(1)
fn main() {
${phonies.join('\n')}
}`;
let shaderModule: GPUShaderModule;
this.expectGPUError(
'validation',
() => {
shaderModule = this.device.createShaderModule({ code });
},
false
);
this.expectGPUError(
'validation',
() => {
this.device.createComputePipeline({
layout: 'auto',
compute: { module: shaderModule!, entryPoint: 'main', constants: args.constants },
});
},
!args.expectedResult
);
}
/**
* Wraps the code fragment into an entry point.
*
* @example
* ```ts
* t.wrapInEntryPoint(`var i = 0;`);
* ```
*/
wrapInEntryPoint(code: string, enabledExtensions: string[] = []) {
const enableDirectives = enabledExtensions.map(x => `enable ${x};`).join('\n ');
return `
${enableDirectives}
@compute @workgroup_size(1)
fn main() {
${code}
}`;
}
}