diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src')
748 files changed, 56555 insertions, 14044 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts index 77875e047d..616023e20c 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts @@ -15,7 +15,8 @@ export type TestParams = { type DestroyableObject = | { destroy(): void } | { close(): void } - | { getExtension(extensionName: 'WEBGL_lose_context'): WEBGL_lose_context }; + | { getExtension(extensionName: 'WEBGL_lose_context'): WEBGL_lose_context } + | HTMLVideoElement; export class SubcaseBatchState { constructor( @@ -124,8 +125,12 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> { if (WEBGL_lose_context) WEBGL_lose_context.loseContext(); } else if ('destroy' in o) { o.destroy(); - } else { + } else if ('close' in o) { o.close(); + } else { + // HTMLVideoElement + o.src = ''; + o.srcObject = null; } } } @@ -161,6 +166,14 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> { this.rec.debug(new Error(msg)); } + /** + * Log an info message. + * **Use sparingly. Use `debug()` instead if logs are only needed with debug logging enabled.** + */ + info(msg: string): void { + this.rec.info(new Error(msg)); + } + /** Throws an exception marking the subcase as skipped. */ skip(msg: string): never { throw new SkipTestCase(msg); diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts index 2575418299..e6624ae120 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts @@ -1,4 +1,9 @@ export type TestConfig = { + /** + * Enable debug-level logs (normally logged via `Fixture.debug()`). + */ + enableDebugLogs: boolean; + maxSubcasesInFlight: number; testHeartbeatCallback: () => void; noRaceWithRejectOnTimeout: boolean; @@ -21,12 +26,25 @@ export type TestConfig = { * Whether or not we're running in compatibility mode. */ compatibility: boolean; + + /** + * Whether or not to request a fallback adapter. + */ + forceFallbackAdapter: boolean; + + /** + * Whether to enable the `logToWebSocket` function used for out-of-band test logging. + */ + logToWebSocket: boolean; }; export const globalTestConfig: TestConfig = { + enableDebugLogs: false, maxSubcasesInFlight: 500, testHeartbeatCallback: () => {}, noRaceWithRejectOnTimeout: false, unrollConstEvalLoops: false, compatibility: false, + forceFallbackAdapter: false, + logToWebSocket: false, }; diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts index b5e1b1a446..aae4b87995 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts @@ -73,8 +73,9 @@ export abstract class TestFileLoader extends EventTarget { query: TestQuery, { subqueriesToExpand = [], + fullyExpandSubtrees = [], maxChunkTime = Infinity, - }: { subqueriesToExpand?: string[]; maxChunkTime?: number } = {} + }: { subqueriesToExpand?: string[]; fullyExpandSubtrees?: string[]; maxChunkTime?: number } = {} ): Promise<TestTree> { const tree = await loadTreeForQuery(this, query, { subqueriesToExpand: subqueriesToExpand.map(s => { @@ -82,6 +83,7 @@ export abstract class TestFileLoader extends EventTarget { assert(q.level >= 2, () => `subqueriesToExpand entries should not be multi-file:\n ${q}`); return q; }), + fullyExpandSubtrees: fullyExpandSubtrees.map(s => parseQuery(s)), maxChunkTime, }); this.dispatchEvent(new MessageEvent<void>('finish')); diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts index ee006cdeb3..b01c08b56e 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts @@ -1,19 +1,36 @@ import { ErrorWithExtra } from '../../util/util.js'; import { extractImportantStackTrace } from '../stack.js'; +import { LogMessageRawData } from './result.js'; + export class LogMessageWithStack extends Error { readonly extra: unknown; private stackHiddenMessage: string | undefined = undefined; - constructor(name: string, ex: Error | ErrorWithExtra) { - super(ex.message); + /** + * Wrap an Error (which was created to capture the stack at that point) into a + * LogMessageWithStack (which has extra stuff for good log messages). + * + * The original `ex.name` is ignored. Inclued it in the `name` parameter if it + * needs to be preserved. + */ + static wrapError(name: string, ex: Error | ErrorWithExtra) { + return new LogMessageWithStack({ + name, + message: ex.message, + stackHiddenMessage: undefined, + stack: ex.stack, + extra: 'extra' in ex ? ex.extra : undefined, + }); + } - this.name = name; - this.stack = ex.stack; - if ('extra' in ex) { - this.extra = ex.extra; - } + constructor(o: LogMessageRawData) { + super(o.message); + this.name = o.name; + this.stackHiddenMessage = o.stackHiddenMessage; + this.stack = o.stack; + this.extra = o.extra; } /** Set a flag so the stack is not printed in toJSON(). */ @@ -21,6 +38,11 @@ export class LogMessageWithStack extends Error { this.stackHiddenMessage ??= stackHiddenMessage; } + /** + * Print the message for display. + * + * Note: This is toJSON instead of toString to make it easy to save logs using JSON.stringify. + */ toJSON(): string { let m = this.name; if (this.message) m += ': ' + this.message; @@ -33,6 +55,21 @@ export class LogMessageWithStack extends Error { } return m; } + + /** + * Flatten the message for sending over a message channel. + * + * Note `extra` may get mangled by postMessage. + */ + toRawData(): LogMessageRawData { + return { + name: this.name, + message: this.message, + stackHiddenMessage: this.stackHiddenMessage, + stack: this.stack, + extra: this.extra, + }; + } } /** diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts index e4526cff54..6b95f48b74 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts @@ -1,3 +1,4 @@ +import { globalTestConfig } from '../../framework/test_config.js'; import { version } from '../version.js'; import { LiveTestCaseResult } from './result.js'; @@ -6,8 +7,6 @@ import { TestCaseRecorder } from './test_case_recorder.js'; export type LogResults = Map<string, LiveTestCaseResult>; export class Logger { - static globalDebugMode: boolean = false; - readonly overriddenDebugMode: boolean | undefined; readonly results: LogResults = new Map(); @@ -19,7 +18,7 @@ export class Logger { const result: LiveTestCaseResult = { status: 'running', timems: -1 }; this.results.set(name, result); return [ - new TestCaseRecorder(result, this.overriddenDebugMode ?? Logger.globalDebugMode), + new TestCaseRecorder(result, this.overriddenDebugMode ?? globalTestConfig.enableDebugLogs), result, ]; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts index 3318e8c937..9968f3d359 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts @@ -14,8 +14,24 @@ export interface LiveTestCaseResult extends TestCaseResult { logs?: LogMessageWithStack[]; } +/** + * Raw data for a test log message. + * + * This form is sendable over a message channel, except `extra` may get mangled. + */ +export interface LogMessageRawData { + name: string; + message: string; + stackHiddenMessage: string | undefined; + stack: string | undefined; + extra: unknown; +} + +/** + * Test case results in a form sendable over a message channel. + * + * Note `extra` may get mangled by postMessage. + */ export interface TransferredTestCaseResult extends TestCaseResult { - // When transferred from a worker, a LogMessageWithStack turns into a generic Error - // (its prototype gets lost and replaced with Error). - logs?: Error[]; + logs?: LogMessageRawData[]; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts index f5c3252b5c..78f625269e 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts @@ -45,8 +45,6 @@ export class TestCaseRecorder { private logs: LogMessageWithStack[] = []; private logLinesAtCurrentSeverity = 0; private debugging = false; - /** Used to dedup log messages which have identical stacks. */ - private messagesForPreviouslySeenStacks = new Map<string, LogMessageWithStack>(); constructor(result: LiveTestCaseResult, debugging: boolean) { this.result = result; @@ -143,13 +141,15 @@ export class TestCaseRecorder { this.skipped(ex); return; } - this.logImpl(LogSeverity.ThrewException, 'EXCEPTION', ex); + // logImpl will discard the original error's ex.name. Preserve it here. + const name = ex instanceof Error ? `EXCEPTION: ${ex.name}` : 'EXCEPTION'; + this.logImpl(LogSeverity.ThrewException, name, ex); } private logImpl(level: LogSeverity, name: string, baseException: unknown): void { assert(baseException instanceof Error, 'test threw a non-Error object'); globalTestConfig.testHeartbeatCallback(); - const logMessage = new LogMessageWithStack(name, baseException); + const logMessage = LogMessageWithStack.wrapError(name, baseException); // Final case status should be the "worst" of all log entries. if (this.inSubCase) { diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts index a9419b87c1..f49833f5a2 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts @@ -58,7 +58,10 @@ function compareOneLevel(ordering: Ordering, aIsBig: boolean, bIsBig: boolean): return Ordering.Unordered; } -function comparePaths(a: readonly string[], b: readonly string[]): Ordering { +/** + * Compare two file paths, or file-local test paths, returning an Ordering between the two. + */ +export function comparePaths(a: readonly string[], b: readonly string[]): Ordering { const shorter = Math.min(a.length, b.length); for (let i = 0; i < shorter; ++i) { diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts index 996835b0ec..0a9b355804 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts @@ -17,12 +17,49 @@ import { import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './separators.js'; import { validQueryPart } from './validQueryPart.js'; -export function parseQuery(s: string): TestQuery { +/** + * converts foo/bar/src/webgpu/this/that/file.spec.ts to webgpu:this,that,file,* + */ +function convertPathToQuery(path: string) { + // removes .spec.ts and splits by directory separators. + const parts = path.substring(0, path.length - 8).split(/\/|\\/g); + // Gets parts only after the last `src`. Example: returns ['webgpu', 'foo', 'bar', 'test'] + // for ['Users', 'me', 'src', 'cts', 'src', 'webgpu', 'foo', 'bar', 'test'] + const partsAfterSrc = parts.slice(parts.lastIndexOf('src') + 1); + const suite = partsAfterSrc.shift(); + return `${suite}:${partsAfterSrc.join(',')},*`; +} + +/** + * If a query looks like a path (ends in .spec.ts and has directory separators) + * then convert try to convert it to a query. + */ +function convertPathLikeToQuery(queryOrPath: string) { + return queryOrPath.endsWith('.spec.ts') && + (queryOrPath.includes('/') || queryOrPath.includes('\\')) + ? convertPathToQuery(queryOrPath) + : queryOrPath; +} + +/** + * Convert long suite names (the part before the first colon) to the + * shortest last word + * foo.bar.moo:test,subtest,foo -> moo:test,subtest,foo + */ +function shortenSuiteName(query: string) { + const parts = query.split(':'); + // converts foo.bar.moo to moo + const suite = parts.shift()?.replace(/.*\.(\w+)$/, '$1'); + return [suite, ...parts].join(':'); +} + +export function parseQuery(queryLike: string): TestQuery { try { - return parseQueryImpl(s); + const query = shortenSuiteName(convertPathLikeToQuery(queryLike)); + return parseQueryImpl(query); } catch (ex) { if (ex instanceof Error) { - ex.message += '\n on: ' + s; + ex.message += `\n on: ${queryLike}`; } throw ex; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts index 7c72a62f88..676ac46d38 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts @@ -1,5 +1,5 @@ import { TestParams } from '../../framework/fixture.js'; -import { optionEnabled } from '../../runtime/helper/options.js'; +import { optionWorkerMode } from '../../runtime/helper/options.js'; import { assert, unreachable } from '../../util/util.js'; import { Expectation } from '../logging/result.js'; @@ -188,12 +188,12 @@ export function parseExpectationsForTestQuery( assert( expectationURL.pathname === wptURL.pathname, `Invalid expectation path ${expectationURL.pathname} -Expectation should be of the form path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;... +Expectation should be of the form path/to/cts.https.html?debug=0&q=suite:test_path:test_name:foo=1;bar=2;... ` ); const params = expectationURL.searchParams; - if (optionEnabled('worker', params) !== optionEnabled('worker', wptURL.searchParams)) { + if (optionWorkerMode('worker', params) !== optionWorkerMode('worker', wptURL.searchParams)) { continue; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts index 632a822ef1..e1d0cde12d 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts @@ -34,7 +34,7 @@ import { validQueryPart } from '../internal/query/validQueryPart.js'; import { DeepReadonly } from '../util/types.js'; import { assert, unreachable } from '../util/util.js'; -import { logToWebsocket } from './websocket_logger.js'; +import { logToWebSocket } from './websocket_logger.js'; export type RunFn = ( rec: TestCaseRecorder, @@ -294,9 +294,11 @@ class TestBuilder<S extends SubcaseBatchState, F extends Fixture> { (this.description ? this.description + '\n\n' : '') + 'TODO: .unimplemented()'; this.isUnimplemented = true; - this.testFn = () => { + // Use the beforeFn to skip the test, so we don't have to iterate the subcases. + this.beforeFn = () => { throw new SkipTestCase('test unimplemented'); }; + this.testFn = () => {}; } /** Perform various validation/"lint" chenks. */ @@ -350,7 +352,7 @@ class TestBuilder<S extends SubcaseBatchState, F extends Fixture> { const testcaseStringUnique = stringifyPublicParamsUniquely(params); assert( !seen.has(testcaseStringUnique), - `Duplicate public test case+subcase params for test ${testPathString}: ${testcaseString}` + `Duplicate public test case+subcase params for test ${testPathString}: ${testcaseString} (${caseQuery})` ); seen.add(testcaseStringUnique); } @@ -737,7 +739,7 @@ class RunCaseSpecific implements RunCase { timems: rec.result.timems, nonskippedSubcaseCount: rec.nonskippedSubcaseCount, }; - logToWebsocket(JSON.stringify(msg)); + logToWebSocket(JSON.stringify(msg)); } } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts index 2d2b555366..c5a0e11448 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts @@ -1,6 +1,6 @@ // A listing of all specs within a single suite. This is the (awaited) type of // `groups` in '{cts,unittests}/listing.ts' and `listing` in the auto-generated -// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings). +// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings_and_webworkers). export type TestSuiteListing = TestSuiteListingEntry[]; export type TestSuiteListingEntry = TestSuiteListingEntrySpec | TestSuiteListingEntryReadme; diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts index 594837059c..f2fad59037 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts @@ -286,8 +286,9 @@ export async function loadTreeForQuery( queryToLoad: TestQuery, { subqueriesToExpand, + fullyExpandSubtrees = [], maxChunkTime = Infinity, - }: { subqueriesToExpand: TestQuery[]; maxChunkTime?: number } + }: { subqueriesToExpand: TestQuery[]; fullyExpandSubtrees?: TestQuery[]; maxChunkTime?: number } ): Promise<TestTree> { const suite = queryToLoad.suite; const specs = await loader.listing(suite); @@ -303,6 +304,10 @@ export async function loadTreeForQuery( // If toExpand == subquery, no expansion is needed (but it's still "seen"). if (ordering === Ordering.Equal) seenSubqueriesToExpand[i] = true; return ordering !== Ordering.StrictSubset; + }) && + fullyExpandSubtrees.every(toExpand => { + const ordering = compareQueries(toExpand, subquery); + return ordering === Ordering.Unordered; }); // L0 = suite-level, e.g. suite:* diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts index 30246df843..373378e7c2 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts @@ -1,3 +1,5 @@ +import { globalTestConfig } from '../framework/test_config.js'; + /** * - 'uninitialized' means we haven't tried to connect yet * - Promise means it's pending @@ -8,12 +10,15 @@ let connection: Promise<WebSocket | 'failed'> | WebSocket | 'failed' | 'uninitia 'uninitialized'; /** - * Log a string to a websocket at `localhost:59497`. See `tools/websocket-logger`. + * If the logToWebSocket option is enabled (?log_to_web_socket=1 in browser, + * --log-to-web-socket on command line, or enable it by default in options.ts), + * log a string to a websocket at `localhost:59497`. See `tools/websocket-logger`. * - * This does nothing if a connection couldn't be established on the first call. + * This does nothing if a logToWebSocket is not enabled, or if a connection + * couldn't be established on the first call. */ -export function logToWebsocket(msg: string) { - if (connection === 'failed') { +export function logToWebSocket(msg: string) { + if (!globalTestConfig.logToWebSocket || connection === 'failed') { return; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts index 44a73fb38b..2a00640f0e 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import { dataCache } from '../framework/data_cache.js'; +import { getResourcePath, setBaseResourcePath } from '../framework/resources.js'; import { globalTestConfig } from '../framework/test_config.js'; import { DefaultTestFileLoader } from '../internal/file_loader.js'; import { prettyPrintLog } from '../internal/logging/log_message.js'; @@ -37,6 +38,12 @@ Options: return sys.exit(rc); } +if (!sys.existsSync('src/common/runtime/cmdline.ts')) { + console.log('Must be run from repository root'); + usage(1); +} +setBaseResourcePath('out-node/resources'); + // The interface that exposes creation of the GPU, and optional interface to code coverage. interface GPUProviderModule { // @returns a GPU with the given flags @@ -60,12 +67,10 @@ Colors.enabled = false; let verbose = false; let emitCoverage = false; let listMode: listModes = 'none'; -let debug = false; let printJSON = false; let quiet = false; let loadWebGPUExpectations: Promise<unknown> | undefined = undefined; let gpuProviderModule: GPUProviderModule | undefined = undefined; -let dataPath: string | undefined = undefined; const queries: string[] = []; const gpuProviderFlags: string[] = []; @@ -83,9 +88,7 @@ for (let i = 0; i < sys.args.length; ++i) { } else if (a === '--list-unimplemented') { listMode = 'unimplemented'; } else if (a === '--debug') { - debug = true; - } else if (a === '--data') { - dataPath = sys.args[++i]; + globalTestConfig.enableDebugLogs = true; } else if (a === '--print-json') { printJSON = true; } else if (a === '--expectations') { @@ -102,6 +105,10 @@ for (let i = 0; i < sys.args.length; ++i) { globalTestConfig.unrollConstEvalLoops = true; } else if (a === '--compat') { globalTestConfig.compatibility = true; + } else if (a === '--force-fallback-adapter') { + globalTestConfig.forceFallbackAdapter = true; + } else if (a === '--log-to-websocket') { + globalTestConfig.logToWebSocket = true; } else { console.log('unrecognized flag: ', a); usage(1); @@ -113,9 +120,12 @@ for (let i = 0; i < sys.args.length; ++i) { let codeCoverage: CodeCoverageProvider | undefined = undefined; -if (globalTestConfig.compatibility) { +if (globalTestConfig.compatibility || globalTestConfig.forceFallbackAdapter) { // MAINTENANCE_TODO: remove the cast once compatibilityMode is officially added - setDefaultRequestAdapterOptions({ compatibilityMode: true } as GPURequestAdapterOptions); + setDefaultRequestAdapterOptions({ + compatibilityMode: globalTestConfig.compatibility, + forceFallbackAdapter: globalTestConfig.forceFallbackAdapter, + } as GPURequestAdapterOptions); } if (gpuProviderModule) { @@ -132,21 +142,20 @@ Did you remember to build with code coverage instrumentation enabled?` } } -if (dataPath !== undefined) { - dataCache.setStore({ - load: (path: string) => { - return new Promise<Uint8Array>((resolve, reject) => { - fs.readFile(`${dataPath}/${path}`, (err, data) => { - if (err !== null) { - reject(err.message); - } else { - resolve(data); - } - }); +dataCache.setStore({ + load: (path: string) => { + return new Promise<Uint8Array>((resolve, reject) => { + fs.readFile(getResourcePath(`cache/${path}`), (err, data) => { + if (err !== null) { + reject(err.message); + } else { + resolve(data); + } }); - }, - }); -} + }); + }, +}); + if (verbose) { dataCache.setDebugLogger(console.log); } @@ -166,7 +175,6 @@ if (queries.length === 0) { filterQuery ); - Logger.globalDebugMode = debug; const log = new Logger(); const failed: Array<[string, LiveTestCaseResult]> = []; diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts index 38974b803f..4a82c7d292 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts @@ -1,3 +1,5 @@ +import { unreachable } from '../../util/util.js'; + let windowURL: URL | undefined = undefined; function getWindowURL() { if (windowURL === undefined) { @@ -6,6 +8,7 @@ function getWindowURL() { return windowURL; } +/** Parse a runner option that is always boolean-typed. False if missing or '0'. */ export function optionEnabled( opt: string, searchParams: URLSearchParams = getWindowURL().searchParams @@ -14,30 +17,55 @@ export function optionEnabled( return val !== null && val !== '0'; } +/** Parse a runner option that is string-typed. If the option is missing, returns `null`. */ export function optionString( opt: string, searchParams: URLSearchParams = getWindowURL().searchParams -): string { - return searchParams.get(opt) || ''; +): string | null { + return searchParams.get(opt); +} + +/** Runtime modes for running tests in different types of workers. */ +export type WorkerMode = 'dedicated' | 'service' | 'shared'; +/** Parse a runner option for different worker modes (as in `?worker=shared`). Null if no worker. */ +export function optionWorkerMode( + opt: string, + searchParams: URLSearchParams = getWindowURL().searchParams +): WorkerMode | null { + const value = searchParams.get(opt); + if (value === null || value === '0') { + return null; + } else if (value === 'service') { + return 'service'; + } else if (value === 'shared') { + return 'shared'; + } else if (value === '' || value === '1' || value === 'dedicated') { + return 'dedicated'; + } + unreachable('invalid worker= option value'); } /** * The possible options for the tests. */ export interface CTSOptions { - worker: boolean; + worker: WorkerMode | null; debug: boolean; compatibility: boolean; + forceFallbackAdapter: boolean; unrollConstEvalLoops: boolean; - powerPreference?: GPUPowerPreference | ''; + powerPreference: GPUPowerPreference | null; + logToWebSocket: boolean; } export const kDefaultCTSOptions: CTSOptions = { - worker: false, + worker: null, debug: true, compatibility: false, + forceFallbackAdapter: false, unrollConstEvalLoops: false, - powerPreference: '', + powerPreference: null, + logToWebSocket: false, }; /** @@ -45,8 +73,8 @@ export const kDefaultCTSOptions: CTSOptions = { */ export interface OptionInfo { description: string; - parser?: (key: string, searchParams?: URLSearchParams) => boolean | string; - selectValueDescriptions?: { value: string; description: string }[]; + parser?: (key: string, searchParams?: URLSearchParams) => boolean | string | null; + selectValueDescriptions?: { value: string | null; description: string }[]; } /** @@ -59,19 +87,30 @@ export type OptionsInfos<Type> = Record<keyof Type, OptionInfo>; * Options to the CTS. */ export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = { - worker: { description: 'run in a worker' }, + worker: { + description: 'run in a worker', + parser: optionWorkerMode, + selectValueDescriptions: [ + { value: null, description: 'no worker' }, + { value: 'dedicated', description: 'dedicated worker' }, + { value: 'shared', description: 'shared worker' }, + { value: 'service', description: 'service worker' }, + ], + }, debug: { description: 'show more info' }, compatibility: { description: 'run in compatibility mode' }, + forceFallbackAdapter: { description: 'pass forceFallbackAdapter: true to requestAdapter' }, unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' }, powerPreference: { description: 'set default powerPreference for some tests', parser: optionString, selectValueDescriptions: [ - { value: '', description: 'default' }, + { value: null, description: 'default' }, { value: 'low-power', description: 'low-power' }, { value: 'high-performance', description: 'high-performance' }, ], }, + logToWebSocket: { description: 'send some logs to ws://localhost:59497/' }, }; /** @@ -95,7 +134,7 @@ function getOptionsInfoFromSearchString<Type extends CTSOptions>( searchString: string ): Type { const searchParams = new URLSearchParams(searchString); - const optionValues: Record<string, boolean | string> = {}; + const optionValues: Record<string, boolean | string | null> = {}; for (const [optionName, info] of Object.entries(optionsInfos)) { const parser = info.parser || optionEnabled; optionValues[optionName] = parser(camelCaseToSnakeCase(optionName), searchParams); diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts index e8d187ea7e..ebc206c3b2 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts @@ -1,15 +1,11 @@ import { setBaseResourcePath } from '../../framework/resources.js'; -import { globalTestConfig } from '../../framework/test_config.js'; import { DefaultTestFileLoader } from '../../internal/file_loader.js'; -import { Logger } from '../../internal/logging/logger.js'; import { parseQuery } from '../../internal/query/parseQuery.js'; -import { TestQueryWithExpectation } from '../../internal/query/query.js'; -import { setDefaultRequestAdapterOptions } from '../../util/navigator_gpu.js'; import { assert } from '../../util/util.js'; -import { CTSOptions } from './options.js'; +import { setupWorkerEnvironment, WorkerTestRunRequest } from './utils_worker.js'; -// Should be DedicatedWorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom". +// Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom". /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ declare const self: any; @@ -17,25 +13,10 @@ const loader = new DefaultTestFileLoader(); setBaseResourcePath('../../../resources'); -self.onmessage = async (ev: MessageEvent) => { - const query: string = ev.data.query; - const expectations: TestQueryWithExpectation[] = ev.data.expectations; - const ctsOptions: CTSOptions = ev.data.ctsOptions; +async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) { + const { query, expectations, ctsOptions } = ev.data as WorkerTestRunRequest; - const { debug, unrollConstEvalLoops, powerPreference, compatibility } = ctsOptions; - globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops; - globalTestConfig.compatibility = compatibility; - - Logger.globalDebugMode = debug; - const log = new Logger(); - - if (powerPreference || compatibility) { - setDefaultRequestAdapterOptions({ - ...(powerPreference && { powerPreference }), - // MAINTENANCE_TODO: Change this to whatever the option ends up being - ...(compatibility && { compatibilityMode: true }), - }); - } + const log = setupWorkerEnvironment(ctsOptions); const testcases = Array.from(await loader.loadCases(parseQuery(query))); assert(testcases.length === 1, 'worker query resulted in != 1 cases'); @@ -44,5 +25,23 @@ self.onmessage = async (ev: MessageEvent) => { const [rec, result] = log.record(testcase.query.toString()); await testcase.run(rec, expectations); - self.postMessage({ query, result }); + this.postMessage({ + query, + result: { + ...result, + logs: result.logs?.map(l => l.toRawData()), + }, + }); +} + +self.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(ev.source || self, ev); +}; + +self.onconnect = (event: MessageEvent) => { + const port = event.ports[0]; + + port.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(port, ev); + }; }; diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts index 9bbcab0946..f9a44bb7bc 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts @@ -2,48 +2,190 @@ import { LogMessageWithStack } from '../../internal/logging/log_message.js'; import { TransferredTestCaseResult, LiveTestCaseResult } from '../../internal/logging/result.js'; import { TestCaseRecorder } from '../../internal/logging/test_case_recorder.js'; import { TestQueryWithExpectation } from '../../internal/query/query.js'; +import { timeout } from '../../util/timeout.js'; +import { assert } from '../../util/util.js'; -import { CTSOptions, kDefaultCTSOptions } from './options.js'; +import { CTSOptions, WorkerMode, kDefaultCTSOptions } from './options.js'; +import { WorkerTestRunRequest } from './utils_worker.js'; -export class TestWorker { - private readonly ctsOptions: CTSOptions; - private readonly worker: Worker; - private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>(); +/** Query all currently-registered service workers, and unregister them. */ +function unregisterAllServiceWorkers() { + void navigator.serviceWorker.getRegistrations().then(registrations => { + for (const registration of registrations) { + void registration.unregister(); + } + }); +} - constructor(ctsOptions?: CTSOptions) { - this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: true } }; - const selfPath = import.meta.url; - const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); - const workerPath = selfPathDir + '/test_worker-worker.js'; - this.worker = new Worker(workerPath, { type: 'module' }); - this.worker.onmessage = ev => { - const query: string = ev.data.query; - const result: TransferredTestCaseResult = ev.data.result; - if (result.logs) { - for (const l of result.logs) { - Object.setPrototypeOf(l, LogMessageWithStack.prototype); - } - } - this.resolvers.get(query)!(result as LiveTestCaseResult); +// NOTE: This code runs on startup for any runtime with worker support. Here, we use that chance to +// delete any leaked service workers, and register to clean up after ourselves at shutdown. +unregisterAllServiceWorkers(); +window.addEventListener('beforeunload', () => { + unregisterAllServiceWorkers(); +}); + +abstract class TestBaseWorker { + protected readonly ctsOptions: CTSOptions; + protected readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>(); + + constructor(worker: WorkerMode, ctsOptions?: CTSOptions) { + this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker } }; + } - // MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and - // update the entire results JSON somehow at some point). + onmessage(ev: MessageEvent) { + const query: string = ev.data.query; + const transferredResult: TransferredTestCaseResult = ev.data.result; + + const result: LiveTestCaseResult = { + status: transferredResult.status, + timems: transferredResult.timems, + logs: transferredResult.logs?.map(l => new LogMessageWithStack(l)), }; + + this.resolvers.get(query)!(result); + this.resolvers.delete(query); + + // MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and + // update the entire results JSON somehow at some point). } - async run( - rec: TestCaseRecorder, + makeRequestAndRecordResult( + target: MessagePort | Worker | ServiceWorker, query: string, - expectations: TestQueryWithExpectation[] = [] - ): Promise<void> { - this.worker.postMessage({ + expectations: TestQueryWithExpectation[] + ): Promise<LiveTestCaseResult> { + const request: WorkerTestRunRequest = { query, expectations, ctsOptions: this.ctsOptions, - }); - const workerResult = await new Promise<LiveTestCaseResult>(resolve => { + }; + target.postMessage(request); + + return new Promise<LiveTestCaseResult>(resolve => { + assert(!this.resolvers.has(query), "can't request same query twice simultaneously"); this.resolvers.set(query, resolve); }); - rec.injectResult(workerResult); + } + + async run( + rec: TestCaseRecorder, + query: string, + expectations: TestQueryWithExpectation[] = [] + ): Promise<void> { + try { + rec.injectResult(await this.runImpl(query, expectations)); + } catch (ex) { + rec.start(); + rec.threw(ex); + rec.finish(); + } + } + + protected abstract runImpl( + query: string, + expectations: TestQueryWithExpectation[] + ): Promise<LiveTestCaseResult>; +} + +export class TestDedicatedWorker extends TestBaseWorker { + private readonly worker: Worker | Error; + + constructor(ctsOptions?: CTSOptions) { + super('dedicated', ctsOptions); + try { + if (typeof Worker === 'undefined') { + throw new Error('Dedicated Workers not available'); + } + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + const workerPath = selfPathDir + '/test_worker-worker.js'; + this.worker = new Worker(workerPath, { type: 'module' }); + this.worker.onmessage = ev => this.onmessage(ev); + } catch (ex) { + assert(ex instanceof Error); + // Save the exception to re-throw in runImpl(). + this.worker = ex; + } + } + + override runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (this.worker instanceof Worker) { + return this.makeRequestAndRecordResult(this.worker, query, expectations); + } else { + throw this.worker; + } + } +} + +/** @deprecated Use TestDedicatedWorker instead. */ +export class TestWorker extends TestDedicatedWorker {} + +export class TestSharedWorker extends TestBaseWorker { + /** MessagePort to the SharedWorker, or an Error if it couldn't be initialized. */ + private readonly port: MessagePort | Error; + + constructor(ctsOptions?: CTSOptions) { + super('shared', ctsOptions); + try { + if (typeof SharedWorker === 'undefined') { + throw new Error('Shared Workers not available'); + } + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + const workerPath = selfPathDir + '/test_worker-worker.js'; + const worker = new SharedWorker(workerPath, { type: 'module' }); + this.port = worker.port; + this.port.start(); + this.port.onmessage = ev => this.onmessage(ev); + } catch (ex) { + assert(ex instanceof Error); + // Save the exception to re-throw in runImpl(). + this.port = ex; + } + } + + override runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (this.port instanceof MessagePort) { + return this.makeRequestAndRecordResult(this.port, query, expectations); + } else { + throw this.port; + } + } +} + +export class TestServiceWorker extends TestBaseWorker { + constructor(ctsOptions?: CTSOptions) { + super('service', ctsOptions); + } + + override async runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (!('serviceWorker' in navigator)) { + throw new Error('Service Workers not available'); + } + const [suite, name] = query.split(':', 2); + const fileName = name.split(',').join('/'); + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + // Construct the path to the worker file, then use URL to resolve the `../` components. + const serviceWorkerURL = new URL( + `${selfPathDir}/../../../${suite}/webworker/${fileName}.worker.js` + ).toString(); + + // If a registration already exists for this path, it will be ignored. + const registration = await navigator.serviceWorker.register(serviceWorkerURL, { + type: 'module', + }); + // Make sure the registration we just requested is active. (We don't worry about it being + // outdated from a previous page load, because we wipe all service workers on shutdown/startup.) + while (!registration.active || registration.active.scriptURL !== serviceWorkerURL) { + await new Promise(resolve => timeout(resolve, 0)); + } + const serviceWorker = registration.active; + + navigator.serviceWorker.onmessage = ev => this.onmessage(ev); + return this.makeRequestAndRecordResult(serviceWorker, query, expectations); } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts new file mode 100644 index 0000000000..13880635bc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts @@ -0,0 +1,35 @@ +import { globalTestConfig } from '../../framework/test_config.js'; +import { Logger } from '../../internal/logging/logger.js'; +import { TestQueryWithExpectation } from '../../internal/query/query.js'; +import { setDefaultRequestAdapterOptions } from '../../util/navigator_gpu.js'; + +import { CTSOptions } from './options.js'; + +export interface WorkerTestRunRequest { + query: string; + expectations: TestQueryWithExpectation[]; + ctsOptions: CTSOptions; +} + +/** + * Set config environment for workers with ctsOptions and return a Logger. + */ +export function setupWorkerEnvironment(ctsOptions: CTSOptions): Logger { + const { powerPreference, compatibility } = ctsOptions; + globalTestConfig.enableDebugLogs = ctsOptions.debug; + globalTestConfig.unrollConstEvalLoops = ctsOptions.unrollConstEvalLoops; + globalTestConfig.compatibility = compatibility; + globalTestConfig.logToWebSocket = ctsOptions.logToWebSocket; + + const log = new Logger(); + + if (powerPreference || compatibility) { + setDefaultRequestAdapterOptions({ + ...(powerPreference && { powerPreference }), + // MAINTENANCE_TODO: Change this to whatever the option ends up being + ...(compatibility && { compatibilityMode: true }), + }); + } + + return log; +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts new file mode 100644 index 0000000000..5f600fe89d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts @@ -0,0 +1,54 @@ +import { Fixture } from '../../framework/fixture'; +import { LogMessageWithStack } from '../../internal/logging/log_message.js'; +import { comparePaths, comparePublicParamsPaths, Ordering } from '../../internal/query/compare.js'; +import { parseQuery } from '../../internal/query/parseQuery.js'; +import { TestQuerySingleCase } from '../../internal/query/query.js'; +import { TestGroup } from '../../internal/test_group.js'; +import { assert } from '../../util/util.js'; + +import { setupWorkerEnvironment, WorkerTestRunRequest } from './utils_worker.js'; + +/** + * Sets up the currently running Web Worker to wrap the TestGroup object `g`. + * `g` is the `g` exported from a `.spec.ts` file: a TestGroupBuilder<F> interface, + * which underneath is actually a TestGroup<F> object. + * + * This is used in the generated `.worker.js` files that are generated to use as service workers. + */ +export function wrapTestGroupForWorker(g: TestGroup<Fixture>) { + self.onmessage = async (ev: MessageEvent) => { + const { query, expectations, ctsOptions } = ev.data as WorkerTestRunRequest; + try { + const log = setupWorkerEnvironment(ctsOptions); + + const testQuery = parseQuery(query); + assert(testQuery instanceof TestQuerySingleCase); + let testcase = null; + for (const t of g.iterate()) { + if (comparePaths(t.testPath, testQuery.testPathParts) !== Ordering.Equal) { + continue; + } + for (const c of t.iterate(testQuery.params)) { + if (comparePublicParamsPaths(c.id.params, testQuery.params) === Ordering.Equal) { + testcase = c; + } + } + } + assert(!!testcase, 'testcase not found'); + const [rec, result] = log.record(query); + await testcase.run(rec, testQuery, expectations); + + ev.source?.postMessage({ query, result }); + } catch (thrown) { + const ex = thrown instanceof Error ? thrown : new Error(`${thrown}`); + ev.source?.postMessage({ + query, + result: { + status: 'fail', + timems: 0, + logs: [LogMessageWithStack.wrapError('INTERNAL', ex)], + }, + }); + } + }; +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts index 8310784e3a..3999b285ba 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts @@ -5,6 +5,7 @@ import * as http from 'http'; import { AddressInfo } from 'net'; import { dataCache } from '../framework/data_cache.js'; +import { getResourcePath, setBaseResourcePath } from '../framework/resources.js'; import { globalTestConfig } from '../framework/test_config.js'; import { DefaultTestFileLoader } from '../internal/file_loader.js'; import { prettyPrintLog } from '../internal/logging/log_message.js'; @@ -20,21 +21,23 @@ import sys from './helper/sys.js'; function usage(rc: number): never { console.log(`Usage: - tools/run_${sys.type} [OPTIONS...] + tools/server [OPTIONS...] Options: --colors Enable ANSI colors in output. --compat Run tests in compatibility mode. --coverage Add coverage data to each result. - --data Path to the data cache directory. --verbose Print result/log of every test as it runs. + --debug Include debug messages in logging. --gpu-provider Path to node module that provides the GPU implementation. --gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value> --unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests --u Flag to set on the gpu-provider as <flag>=<value> Provides an HTTP server used for running tests via an HTTP RPC interface -To run a test, perform an HTTP GET or POST at the URL: - http://localhost:port/run?<test-name> +First, load some tree or subtree of tests: + http://localhost:port/load?unittests:basic:* +To run a single test case, perform an HTTP GET or POST at the URL: + http://localhost:port/run?unittests:basic:test,sync To shutdown the server perform an HTTP GET or POST at the URL: http://localhost:port/terminate `); @@ -46,6 +49,8 @@ interface RunResult { status: Status; // Any additional messages printed message: string; + // The time it took to execute the test + durationMS: number; // Code coverage data, if the server was started with `--coverage` // This data is opaque (implementation defined). coverageData?: string; @@ -71,13 +76,13 @@ if (!sys.existsSync('src/common/runtime/cmdline.ts')) { console.log('Must be run from repository root'); usage(1); } +setBaseResourcePath('out-node/resources'); Colors.enabled = false; let emitCoverage = false; let verbose = false; let gpuProviderModule: GPUProviderModule | undefined = undefined; -let dataPath: string | undefined = undefined; const gpuProviderFlags: string[] = []; for (let i = 0; i < sys.args.length; ++i) { @@ -89,13 +94,17 @@ for (let i = 0; i < sys.args.length; ++i) { globalTestConfig.compatibility = true; } else if (a === '--coverage') { emitCoverage = true; - } else if (a === '--data') { - dataPath = sys.args[++i]; + } else if (a === '--force-fallback-adapter') { + globalTestConfig.forceFallbackAdapter = true; + } else if (a === '--log-to-websocket') { + globalTestConfig.logToWebSocket = true; } else if (a === '--gpu-provider') { const modulePath = sys.args[++i]; gpuProviderModule = require(modulePath); } else if (a === '--gpu-provider-flag') { gpuProviderFlags.push(sys.args[++i]); + } else if (a === '--debug') { + globalTestConfig.enableDebugLogs = true; } else if (a === '--unroll-const-eval-loops') { globalTestConfig.unrollConstEvalLoops = true; } else if (a === '--help') { @@ -110,9 +119,12 @@ for (let i = 0; i < sys.args.length; ++i) { let codeCoverage: CodeCoverageProvider | undefined = undefined; -if (globalTestConfig.compatibility) { +if (globalTestConfig.compatibility || globalTestConfig.forceFallbackAdapter) { // MAINTENANCE_TODO: remove the cast once compatibilityMode is officially added - setDefaultRequestAdapterOptions({ compatibilityMode: true } as GPURequestAdapterOptions); + setDefaultRequestAdapterOptions({ + compatibilityMode: globalTestConfig.compatibility, + forceFallbackAdapter: globalTestConfig.forceFallbackAdapter, + } as GPURequestAdapterOptions); } if (gpuProviderModule) { @@ -130,28 +142,26 @@ Did you remember to build with code coverage instrumentation enabled?` } } -if (dataPath !== undefined) { - dataCache.setStore({ - load: (path: string) => { - return new Promise<Uint8Array>((resolve, reject) => { - fs.readFile(`${dataPath}/${path}`, (err, data) => { - if (err !== null) { - reject(err.message); - } else { - resolve(data); - } - }); +dataCache.setStore({ + load: (path: string) => { + return new Promise<Uint8Array>((resolve, reject) => { + fs.readFile(getResourcePath(`cache/${path}`), (err, data) => { + if (err !== null) { + reject(err.message); + } else { + resolve(data); + } }); - }, - }); -} + }); + }, +}); + if (verbose) { dataCache.setDebugLogger(console.log); } // eslint-disable-next-line @typescript-eslint/require-await (async () => { - Logger.globalDebugMode = verbose; const log = new Logger(); const testcases = new Map<string, TestTreeLeaf>(); @@ -198,14 +208,16 @@ if (verbose) { if (codeCoverage !== undefined) { codeCoverage.begin(); } + const start = performance.now(); const result = await runTestcase(testcase); + const durationMS = performance.now() - start; const coverageData = codeCoverage !== undefined ? codeCoverage.end() : undefined; let message = ''; if (result.logs !== undefined) { message = result.logs.map(log => prettyPrintLog(log)).join('\n'); } const status = result.status; - const res: RunResult = { status, message, coverageData }; + const res: RunResult = { status, message, durationMS, coverageData }; response.statusCode = 200; response.end(JSON.stringify(res)); } else { diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts index 0376f92dda..dc75e6fd01 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts @@ -2,7 +2,7 @@ /* eslint no-console: "off" */ import { dataCache } from '../framework/data_cache.js'; -import { setBaseResourcePath } from '../framework/resources.js'; +import { getResourcePath, setBaseResourcePath } from '../framework/resources.js'; import { globalTestConfig } from '../framework/test_config.js'; import { DefaultTestFileLoader } from '../internal/file_loader.js'; import { Logger } from '../internal/logging/logger.js'; @@ -21,7 +21,7 @@ import { OptionsInfos, camelCaseToSnakeCase, } from './helper/options.js'; -import { TestWorker } from './helper/test_worker.js'; +import { TestDedicatedWorker, TestSharedWorker, TestServiceWorker } from './helper/test_worker.js'; const rootQuerySpec = 'webgpu:*'; let promptBeforeReload = false; @@ -47,16 +47,26 @@ const { queries: qs, options } = parseSearchParamLikeWithOptions( kStandaloneOptionsInfos, window.location.search || rootQuerySpec ); -const { runnow, debug, unrollConstEvalLoops, powerPreference, compatibility } = options; -globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops; +const { runnow, powerPreference, compatibility, forceFallbackAdapter } = options; +globalTestConfig.enableDebugLogs = options.debug; +globalTestConfig.unrollConstEvalLoops = options.unrollConstEvalLoops; globalTestConfig.compatibility = compatibility; +globalTestConfig.logToWebSocket = options.logToWebSocket; -Logger.globalDebugMode = debug; const logger = new Logger(); setBaseResourcePath('../out/resources'); -const worker = options.worker ? new TestWorker(options) : undefined; +const testWorker = + options.worker === null + ? null + : options.worker === 'dedicated' + ? new TestDedicatedWorker(options) + : options.worker === 'shared' + ? new TestSharedWorker(options) + : options.worker === 'service' + ? new TestServiceWorker(options) + : unreachable(); const autoCloseOnPass = document.getElementById('autoCloseOnPass') as HTMLInputElement; const resultsVis = document.getElementById('resultsVis')!; @@ -70,17 +80,18 @@ stopButtonElem.addEventListener('click', () => { stopRequested = true; }); -if (powerPreference || compatibility) { +if (powerPreference || compatibility || forceFallbackAdapter) { setDefaultRequestAdapterOptions({ ...(powerPreference && { powerPreference }), // MAINTENANCE_TODO: Change this to whatever the option ends up being ...(compatibility && { compatibilityMode: true }), + ...(forceFallbackAdapter && { forceFallbackAdapter: true }), }); } dataCache.setStore({ load: async (path: string) => { - const response = await fetch(`data/${path}`); + const response = await fetch(getResourcePath(`cache/${path}`)); if (!response.ok) { return Promise.reject(response.statusText); } @@ -168,8 +179,8 @@ function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree { const [rec, res] = logger.record(name); caseResult = res; - if (worker) { - await worker.run(rec, name); + if (testWorker) { + await testWorker.run(rec, name); } else { await t.run(rec); } @@ -223,6 +234,12 @@ function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree { if (caseResult.logs) { caselogs.empty(); + // Show exceptions at the top since they are often unexpected can point out an error in the test itself vs the WebGPU implementation. + caseResult.logs + .filter(l => l.name === 'EXCEPTION') + .forEach(l => { + $('<pre>').addClass('testcaselogtext').text(l.toJSON()).appendTo(caselogs); + }); for (const l of caseResult.logs) { const caselog = $('<div>').addClass('testcaselog').appendTo(caselogs); $('<button>') @@ -500,13 +517,11 @@ function makeTreeNodeHeaderHTML( // Collapse s:f:t:* or s:f:t:c by default. let lastQueryLevelToExpand: TestQueryLevel = 2; -type ParamValue = string | undefined | null | boolean | string[]; - /** * Takes an array of string, ParamValue and returns an array of pairs * of [key, value] where value is a string. Converts boolean to '0' or '1'. */ -function keyValueToPairs([k, v]: [string, ParamValue]): [string, string][] { +function keyValueToPairs([k, v]: [string, boolean | string | null]): [string, string][] { const key = camelCaseToSnakeCase(k); if (typeof v === 'boolean') { return [[key, v ? '1' : '0']]; @@ -527,9 +542,9 @@ function keyValueToPairs([k, v]: [string, ParamValue]): [string, string][] { * @param params Some object with key value pairs. * @returns a search string. */ -function prepareParams(params: Record<string, ParamValue>): string { +function prepareParams(params: Record<string, boolean | string | null>): string { const pairsArrays = Object.entries(params) - .filter(([, v]) => !!v) + .filter(([, v]) => !(v === false || v === null || v === '0')) .map(keyValueToPairs); const pairs = pairsArrays.flat(); return new URLSearchParams(pairs).toString(); @@ -537,7 +552,7 @@ function prepareParams(params: Record<string, ParamValue>): string { // This is just a cast in one place. export function optionsToRecord(options: CTSOptions) { - return options as unknown as Record<string, boolean | string>; + return options as unknown as Record<string, boolean | string | null>; } /** @@ -597,15 +612,15 @@ void (async () => { }; const createSelect = (optionName: string, info: OptionInfo) => { - const select = $('<select>').on('change', function () { - optionValues[optionName] = (this as HTMLInputElement).value; + const select = $('<select>').on('change', function (this: HTMLSelectElement) { + optionValues[optionName] = JSON.parse(this.value); updateURLsWithCurrentOptions(); }); const currentValue = optionValues[optionName]; for (const { value, description } of info.selectValueDescriptions!) { $('<option>') .text(description) - .val(value) + .val(JSON.stringify(value)) .prop('selected', value === currentValue) .appendTo(select); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts index d4a4008154..79ed1b5924 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts @@ -8,8 +8,8 @@ import { parseQuery } from '../internal/query/parseQuery.js'; import { parseExpectationsForTestQuery, relativeQueryString } from '../internal/query/query.js'; import { assert } from '../util/util.js'; -import { optionEnabled } from './helper/options.js'; -import { TestWorker } from './helper/test_worker.js'; +import { optionEnabled, optionWorkerMode } from './helper/options.js'; +import { TestDedicatedWorker, TestServiceWorker, TestSharedWorker } from './helper/test_worker.js'; // testharness.js API (https://web-platform-tests.org/writing-tests/testharness-api.html) declare interface WptTestObject { @@ -31,8 +31,10 @@ setup({ }); void (async () => { - const workerEnabled = optionEnabled('worker'); - const worker = workerEnabled ? new TestWorker() : undefined; + const workerString = optionWorkerMode('worker'); + const dedicatedWorker = workerString === 'dedicated' ? new TestDedicatedWorker() : undefined; + const sharedWorker = workerString === 'shared' ? new TestSharedWorker() : undefined; + const serviceWorker = workerString === 'service' ? new TestServiceWorker() : undefined; globalTestConfig.unrollConstEvalLoops = optionEnabled('unroll_const_eval_loops'); @@ -63,8 +65,12 @@ void (async () => { const wpt_fn = async () => { const [rec, res] = log.record(name); - if (worker) { - await worker.run(rec, name, expectations); + if (dedicatedWorker) { + await dedicatedWorker.run(rec, name, expectations); + } else if (sharedWorker) { + await sharedWorker.run(rec, name, expectations); + } else if (serviceWorker) { + await serviceWorker.run(rec, name, expectations); } else { await testcase.run(rec, expectations); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts index 50340dd68b..21a335b11c 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts @@ -45,13 +45,16 @@ async function crawlFilesRecursively(dir: string): Promise<string[]> { ); } -export async function crawl(suiteDir: string, validate: boolean): Promise<TestSuiteListingEntry[]> { +export async function crawl( + suiteDir: string, + opts: { validate: boolean; printMetadataWarnings: boolean } | null = null +): Promise<TestSuiteListingEntry[]> { if (!fs.existsSync(suiteDir)) { throw new Error(`Could not find suite: ${suiteDir}`); } let validateTimingsEntries; - if (validate) { + if (opts?.validate) { const metadata = loadMetadataForSuite(suiteDir); if (metadata) { validateTimingsEntries = { @@ -75,7 +78,7 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu const suite = path.basename(suiteDir); - if (validate) { + if (opts?.validate) { const filename = `../../${suite}/${filepathWithoutExtension}.spec.js`; assert(!process.env.STANDALONE_DEV_SERVER); @@ -109,8 +112,6 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu } if (validateTimingsEntries) { - let failed = false; - const zeroEntries = []; const staleEntries = []; for (const [metadataKey, metadataValue] of Object.entries(validateTimingsEntries.metadata)) { @@ -125,36 +126,39 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu staleEntries.push(metadataKey); } } - if (zeroEntries.length) { - console.warn('WARNING: subcaseMS≤0 found in listing_meta.json (allowed, but try to avoid):'); + if (zeroEntries.length && opts?.printMetadataWarnings) { + console.warn( + 'WARNING: subcaseMS ≤ 0 found in listing_meta.json (see docs/adding_timing_metadata.md):' + ); for (const metadataKey of zeroEntries) { console.warn(` ${metadataKey}`); } } - if (staleEntries.length) { - console.error('ERROR: Non-existent tests found in listing_meta.json:'); - for (const metadataKey of staleEntries) { - console.error(` ${metadataKey}`); - } - failed = true; - } - const missingEntries = []; - for (const metadataKey of validateTimingsEntries.testsFoundInFiles) { - if (!(metadataKey in validateTimingsEntries.metadata)) { - missingEntries.push(metadataKey); + if (opts?.printMetadataWarnings) { + const missingEntries = []; + for (const metadataKey of validateTimingsEntries.testsFoundInFiles) { + if (!(metadataKey in validateTimingsEntries.metadata)) { + missingEntries.push(metadataKey); + } + } + if (missingEntries.length) { + console.error( + 'WARNING: Tests missing from listing_meta.json (see docs/adding_timing_metadata.md):' + ); + for (const metadataKey of missingEntries) { + console.error(` ${metadataKey}`); + } } } - if (missingEntries.length) { - console.error( - 'ERROR: Tests missing from listing_meta.json. Please add the new tests (See docs/adding_timing_metadata.md):' - ); - for (const metadataKey of missingEntries) { + + if (staleEntries.length) { + console.error('ERROR: Non-existent tests found in listing_meta.json. Please update:'); + for (const metadataKey of staleEntries) { console.error(` ${metadataKey}`); - failed = true; } + unreachable(); } - assert(!failed); } return entries; @@ -163,5 +167,5 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu export function makeListing(filename: string): Promise<TestSuiteListing> { // Don't validate. This path is only used for the dev server and running tests with Node. // Validation is done for listing generation and presubmit. - return crawl(path.dirname(filename), false); + return crawl(path.dirname(filename)); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts index 57cb6a7ea4..8e0e3bdbe6 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts @@ -144,6 +144,19 @@ app.get('/out/:suite([a-zA-Z0-9_-]+)/listing.js', async (req, res, next) => { } }); +// Serve .worker.js files by generating the necessary wrapper. +app.get('/out/:suite([a-zA-Z0-9_-]+)/webworker/:filepath(*).worker.js', (req, res, next) => { + const { suite, filepath } = req.params; + const result = `\ +import { g } from '/out/${suite}/${filepath}.spec.js'; +import { wrapTestGroupForWorker } from '/out/common/runtime/helper/wrap_for_worker.js'; + +wrapTestGroupForWorker(g); +`; + res.setHeader('Content-Type', 'application/javascript'); + res.send(result); +}); + // Serve all other .js files by fetching the source .ts file and compiling it. app.get('/out/**/*.js', async (req, res, next) => { const jsUrl = path.relative('/out', req.url); diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts index ce0854aa20..d8309ebcb1 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts @@ -3,32 +3,41 @@ import * as path from 'path'; import * as process from 'process'; import { Cacheable, dataCache, setIsBuildingDataCache } from '../framework/data_cache.js'; +import { crc32, toHexString } from '../util/crc32.js'; +import { parseImports } from '../util/parse_imports.js'; function usage(rc: number): void { - console.error(`Usage: tools/gen_cache [options] [OUT_DIR] [SUITE_DIRS...] + console.error(`Usage: tools/gen_cache [options] [SUITE_DIRS...] For each suite in SUITE_DIRS, pre-compute data that is expensive to generate -at runtime and store it under OUT_DIR. If the data file is found then the -DataCache will load this instead of building the expensive data at CTS runtime. +at runtime and store it under 'src/resources/cache'. If the data file is found +then the DataCache will load this instead of building the expensive data at CTS +runtime. +Note: Due to differences in gzip compression, different versions of node can +produce radically different binary cache files. gen_cache uses the hashes of the +source files to determine whether a cache file is 'up to date'. This is faster +and does not depend on the compressed output. Options: --help Print this message and exit. --list Print the list of output files without writing them. - --nth i/n Only process every file where (file_index % n == i) - --validate Check that cache should build (Tests for collisions). + --force Rebuild cache even if they're up to date + --validate Check the cache is up to date --verbose Print each action taken. `); process.exit(rc); } +// Where the cache is generated +const outDir = 'src/resources/cache'; + +let forceRebuild = false; let mode: 'emit' | 'list' | 'validate' = 'emit'; -let nth = { i: 0, n: 1 }; let verbose = false; const nonFlagsArgs: string[] = []; -for (let i = 0; i < process.argv.length; i++) { - const arg = process.argv[i]; +for (const arg of process.argv) { if (arg.startsWith('-')) { switch (arg) { case '--list': { @@ -39,6 +48,10 @@ for (let i = 0; i < process.argv.length; i++) { usage(0); break; } + case '--force': { + forceRebuild = true; + break; + } case '--verbose': { verbose = true; break; @@ -47,28 +60,6 @@ for (let i = 0; i < process.argv.length; i++) { mode = 'validate'; break; } - case '--nth': { - const err = () => { - console.error( - `--nth requires a value of the form 'i/n', where i and n are positive integers and i < n` - ); - process.exit(1); - }; - i++; - if (i >= process.argv.length) { - err(); - } - const value = process.argv[i]; - const parts = value.split('/'); - if (parts.length !== 2) { - err(); - } - nth = { i: parseInt(parts[0]), n: parseInt(parts[1]) }; - if (nth.i < 0 || nth.n < 1 || nth.i > nth.n) { - err(); - } - break; - } default: { console.log('unrecognized flag: ', arg); usage(1); @@ -79,12 +70,10 @@ for (let i = 0; i < process.argv.length; i++) { } } -if (nonFlagsArgs.length < 4) { +if (nonFlagsArgs.length < 3) { usage(0); } -const outRootDir = nonFlagsArgs[2]; - dataCache.setStore({ load: (path: string) => { return new Promise<Uint8Array>((resolve, reject) => { @@ -100,57 +89,133 @@ dataCache.setStore({ }); setIsBuildingDataCache(); +const cacheFileSuffix = __filename.endsWith('.ts') ? '.cache.ts' : '.cache.js'; + +/** + * @returns a list of all the files under 'dir' that has the given extension + * @param dir the directory to search + * @param ext the extension of the files to find + */ +function glob(dir: string, ext: string) { + const files: string[] = []; + for (const file of fs.readdirSync(dir)) { + const path = `${dir}/${file}`; + if (fs.statSync(path).isDirectory()) { + for (const child of glob(path, ext)) { + files.push(`${file}/${child}`); + } + } + + if (path.endsWith(ext) && fs.statSync(path).isFile()) { + files.push(file); + } + } + return files; +} + +/** + * Exception type thrown by SourceHasher.hashFile() when a file annotated with + * MUST_NOT_BE_IMPORTED_BY_DATA_CACHE is transitively imported by a .cache.ts file. + */ +class InvalidImportException { + constructor(path: string) { + this.stack = [path]; + } + toString(): string { + return `invalid transitive import for cache:\n ${this.stack.join('\n ')}`; + } + readonly stack: string[]; +} +/** + * SourceHasher is a utility for producing a hash of a source .ts file and its imported source files. + */ +class SourceHasher { + /** + * @param path the source file path + * @returns a hash of the source file and all of its imported dependencies. + */ + public hashOf(path: string) { + this.u32Array[0] = this.hashFile(path); + return this.u32Array[0].toString(16); + } + + hashFile(path: string): number { + if (!fs.existsSync(path) && path.endsWith('.js')) { + path = path.substring(0, path.length - 2) + 'ts'; + } + + const cached = this.hashes.get(path); + if (cached !== undefined) { + return cached; + } + + this.hashes.set(path, 0); // Store a zero hash to handle cyclic imports + + const content = fs.readFileSync(path, { encoding: 'utf-8' }); + const normalized = content.replace('\r\n', '\n'); + let hash = crc32(normalized); + for (const importPath of parseImports(path, normalized)) { + try { + const importHash = this.hashFile(importPath); + hash = this.hashCombine(hash, importHash); + } catch (ex) { + if (ex instanceof InvalidImportException) { + ex.stack.push(path); + throw ex; + } + } + } + + if (content.includes('MUST_NOT_BE_IMPORTED_BY_DATA_CACHE')) { + throw new InvalidImportException(path); + } + + this.hashes.set(path, hash); + return hash; + } + + /** Simple non-cryptographic hash combiner */ + hashCombine(a: number, b: number): number { + return crc32(`${toHexString(a)} ${toHexString(b)}`); + } + + private hashes = new Map<string, number>(); + private u32Array = new Uint32Array(1); +} + void (async () => { - for (const suiteDir of nonFlagsArgs.slice(3)) { + const suiteDirs = nonFlagsArgs.slice(2); // skip <exe> <js> + for (const suiteDir of suiteDirs) { await build(suiteDir); } })(); -const specFileSuffix = __filename.endsWith('.ts') ? '.spec.ts' : '.spec.js'; - -async function crawlFilesRecursively(dir: string): Promise<string[]> { - const subpathInfo = await Promise.all( - (await fs.promises.readdir(dir)).map(async d => { - const p = path.join(dir, d); - const stats = await fs.promises.stat(p); - return { - path: p, - isDirectory: stats.isDirectory(), - isFile: stats.isFile(), - }; - }) - ); - - const files = subpathInfo - .filter(i => i.isFile && i.path.endsWith(specFileSuffix)) - .map(i => i.path); - - return files.concat( - await subpathInfo - .filter(i => i.isDirectory) - .map(i => crawlFilesRecursively(i.path)) - .reduce(async (a, b) => (await a).concat(await b), Promise.resolve([])) - ); -} - async function build(suiteDir: string) { if (!fs.existsSync(suiteDir)) { console.error(`Could not find ${suiteDir}`); process.exit(1); } - // Crawl files and convert paths to be POSIX-style, relative to suiteDir. - let filesToEnumerate = (await crawlFilesRecursively(suiteDir)).sort(); + // Load hashes.json + const fileHashJsonPath = `${outDir}/hashes.json`; + let fileHashes: Record<string, string> = {}; + if (fs.existsSync(fileHashJsonPath)) { + const json = fs.readFileSync(fileHashJsonPath, { encoding: 'utf8' }); + fileHashes = JSON.parse(json); + } - // Filter out non-spec files - filesToEnumerate = filesToEnumerate.filter(f => f.endsWith(specFileSuffix)); + // Crawl files and convert paths to be POSIX-style, relative to suiteDir. + const filesToEnumerate = glob(suiteDir, cacheFileSuffix) + .map(p => `${suiteDir}/${p}`) + .sort(); + const fileHasher = new SourceHasher(); const cacheablePathToTS = new Map<string, string>(); + const errors: Array<string> = []; - let fileIndex = 0; for (const file of filesToEnumerate) { - const pathWithoutExtension = file.substring(0, file.length - specFileSuffix.length); - const mod = await import(`../../../${pathWithoutExtension}.spec.js`); + const pathWithoutExtension = file.substring(0, file.length - 3); + const mod = await import(`../../../${pathWithoutExtension}.js`); if (mod.d?.serialize !== undefined) { const cacheable = mod.d as Cacheable<unknown>; @@ -158,41 +223,78 @@ async function build(suiteDir: string) { // Check for collisions const existing = cacheablePathToTS.get(cacheable.path); if (existing !== undefined) { - console.error( - `error: Cacheable '${cacheable.path}' is emitted by both: + errors.push( + `'${cacheable.path}' is emitted by both: '${existing}' and '${file}'` ); - process.exit(1); } cacheablePathToTS.set(cacheable.path, file); } - const outPath = `${outRootDir}/data/${cacheable.path}`; + const outPath = `${outDir}/${cacheable.path}`; + const fileHash = fileHasher.hashOf(file); - if (fileIndex++ % nth.n === nth.i) { - switch (mode) { - case 'emit': { + switch (mode) { + case 'emit': { + if (!forceRebuild && fileHashes[cacheable.path] === fileHash) { if (verbose) { - console.log(`building '${outPath}'`); + console.log(`'${outPath}' is up to date`); } - const data = await cacheable.build(); - const serialized = cacheable.serialize(data); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, serialized, 'binary'); - break; + continue; } - case 'list': { - console.log(outPath); - break; - } - case 'validate': { - // Only check currently performed is the collision detection above - break; + console.log(`building '${outPath}'`); + const data = await cacheable.build(); + const serialized = cacheable.serialize(data); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, serialized, 'binary'); + fileHashes[cacheable.path] = fileHash; + break; + } + case 'list': { + console.log(outPath); + break; + } + case 'validate': { + if (fileHashes[cacheable.path] !== fileHash) { + errors.push( + `'${outPath}' needs rebuilding. Generate with 'npx grunt run:generate-cache'` + ); + } else if (verbose) { + console.log(`'${outPath}' is up to date`); } } } } } + + // Check that there aren't stale files in the cache directory + for (const file of glob(outDir, '.bin')) { + if (cacheablePathToTS.get(file) === undefined) { + switch (mode) { + case 'emit': + fs.rmSync(file); + break; + case 'validate': + errors.push( + `cache file '${outDir}/${file}' is no longer generated. Remove with 'npx grunt run:generate-cache'` + ); + break; + } + } + } + + // Update hashes.json + if (mode === 'emit') { + const json = JSON.stringify(fileHashes, undefined, ' '); + fs.writeFileSync(fileHashJsonPath, json, { encoding: 'utf8' }); + } + + if (errors.length > 0) { + for (const error of errors) { + console.error(error); + } + process.exit(1); + } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts index fc5e1f3cde..7cc8cb78f3 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts @@ -9,7 +9,7 @@ function usage(rc: number): void { For each suite in SUITE_DIRS, generate listings and write each listing.js into OUT_DIR/{suite}/listing.js. Example: - tools/gen_listings out/ src/unittests/ src/webgpu/ + tools/gen_listings gen/ src/unittests/ src/webgpu/ Options: --help Print this message and exit. @@ -40,7 +40,7 @@ const outDir = argv[2]; for (const suiteDir of argv.slice(3)) { // Run concurrently for each suite (might be a tiny bit more efficient) - void crawl(suiteDir, false).then(listing => { + void crawl(suiteDir).then(listing => { const suite = path.basename(suiteDir); const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`)); fs.mkdirSync(path.join(outDir, suite), { recursive: true }); @@ -52,12 +52,5 @@ for (const suiteDir of argv.slice(3)) { export const listing = ${JSON.stringify(listing, undefined, 2)}; ` ); - - // If there was a sourcemap for the file we just replaced, delete it. - try { - fs.unlinkSync(outFile + '.map'); - } catch (ex) { - // ignore if file didn't exist - } }); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings_and_webworkers.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings_and_webworkers.ts new file mode 100644 index 0000000000..04ce669de3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings_and_webworkers.ts @@ -0,0 +1,89 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as process from 'process'; + +import { crawl } from './crawl.js'; + +function usage(rc: number): void { + console.error(`Usage: tools/gen_listings_and_webworkers [options] [OUT_DIR] [SUITE_DIRS...] + +For each suite in SUITE_DIRS, generate listings into OUT_DIR/{suite}/listing.js, +and generate Web Worker proxies in OUT_DIR/{suite}/webworker/**/*.worker.js for +every .spec.js file. (Note {suite}/webworker/ is reserved for this purpose.) + +Example: + tools/gen_listings_and_webworkers gen/ src/unittests/ src/webgpu/ + +Options: + --help Print this message and exit. +`); + process.exit(rc); +} + +const argv = process.argv; +if (argv.indexOf('--help') !== -1) { + usage(0); +} + +{ + // Ignore old argument that is now the default + const i = argv.indexOf('--no-validate'); + if (i !== -1) { + argv.splice(i, 1); + } +} + +if (argv.length < 4) { + usage(0); +} + +const myself = 'src/common/tools/gen_listings_and_webworkers.ts'; + +const outDir = argv[2]; + +for (const suiteDir of argv.slice(3)) { + // Run concurrently for each suite (might be a tiny bit more efficient) + void crawl(suiteDir).then(listing => { + const suite = path.basename(suiteDir); + + // Write listing.js + const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`)); + fs.mkdirSync(path.join(outDir, suite), { recursive: true }); + fs.writeFileSync( + outFile, + `\ +// AUTO-GENERATED - DO NOT EDIT. See ${myself}. + +export const listing = ${JSON.stringify(listing, undefined, 2)}; +` + ); + + // Write suite/webworker/**/*.worker.js + for (const entry of listing) { + if ('readme' in entry) continue; + + const outFileDir = path.join( + outDir, + suite, + 'webworker', + ...entry.file.slice(0, entry.file.length - 1) + ); + const outFile = path.join(outDir, suite, 'webworker', ...entry.file) + '.worker.js'; + + const relPathToSuiteRoot = Array<string>(entry.file.length).fill('..').join('/'); + + fs.mkdirSync(outFileDir, { recursive: true }); + fs.writeFileSync( + outFile, + `\ +// AUTO-GENERATED - DO NOT EDIT. See ${myself}. + +import { g } from '${relPathToSuiteRoot}/${entry.file.join('/')}.spec.js'; +import { wrapTestGroupForWorker } from '${relPathToSuiteRoot}/../common/runtime/helper/wrap_for_worker.js'; + +wrapTestGroupForWorker(g); +` + ); + } + }); +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts index e8161304e9..46c2ae4354 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts @@ -23,6 +23,7 @@ gen_wpt_cts_html.ts. Example: { "suite": "webgpu", "out": "path/to/output/cts.https.html", + "outJSON": "path/to/output/webgpu_variant_list.json", "template": "path/to/template/cts.https.html", "maxChunkTimeMS": 2000 } @@ -35,15 +36,15 @@ where arguments.txt is a file containing a list of arguments prefixes to both ge in the expectations. The entire variant list generation runs *once per prefix*, so this multiplies the size of the variant list. - ?worker=0&q= - ?worker=1&q= + ?debug=0&q= + ?debug=1&q= and myexpectations.txt is a file containing a list of WPT paths to suppress, e.g.: - path/to/cts.https.html?worker=0&q=webgpu:a/foo:bar={"x":1} - path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":1} + path/to/cts.https.html?debug=0&q=webgpu:a/foo:bar={"x":1} + path/to/cts.https.html?debug=1&q=webgpu:a/foo:bar={"x":1} - path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":3} + path/to/cts.https.html?debug=1&q=webgpu:a/foo:bar={"x":3} `); process.exit(rc); } @@ -51,9 +52,11 @@ and myexpectations.txt is a file containing a list of WPT paths to suppress, e.g interface ConfigJSON { /** Test suite to generate from. */ suite: string; - /** Output filename, relative to JSON file. */ + /** Output path for HTML file, relative to config file. */ out: string; - /** Input template filename, relative to JSON file. */ + /** Output path for JSON file containing the "variant" list, relative to config file. */ + outVariantList?: string; + /** Input template filename, relative to config file. */ template: string; /** * Maximum time for a single WPT "variant" chunk, in milliseconds. Defaults to infinity. @@ -71,18 +74,31 @@ interface ConfigJSON { /** The prefix to trim from every line of the expectations_file. */ prefix: string; }; + /** Expend all subtrees for provided queries */ + fullyExpandSubtrees?: { + file: string; + prefix: string; + }; + /*No long path assert */ + noLongPathAssert?: boolean; } interface Config { suite: string; out: string; + outVariantList?: string; template: string; maxChunkTimeMS: number; argumentsPrefixes: string[]; + noLongPathAssert: boolean; expectations?: { file: string; prefix: string; }; + fullyExpandSubtrees?: { + file: string; + prefix: string; + }; } let config: Config; @@ -101,13 +117,23 @@ let config: Config; template: path.resolve(jsonFileDir, configJSON.template), maxChunkTimeMS: configJSON.maxChunkTimeMS ?? Infinity, argumentsPrefixes: configJSON.argumentsPrefixes ?? ['?q='], + noLongPathAssert: configJSON.noLongPathAssert ?? false, }; + if (configJSON.outVariantList) { + config.outVariantList = path.resolve(jsonFileDir, configJSON.outVariantList); + } if (configJSON.expectations) { config.expectations = { file: path.resolve(jsonFileDir, configJSON.expectations.file), prefix: configJSON.expectations.prefix, }; } + if (configJSON.fullyExpandSubtrees) { + config.fullyExpandSubtrees = { + file: path.resolve(jsonFileDir, configJSON.fullyExpandSubtrees.file), + prefix: configJSON.fullyExpandSubtrees.prefix, + }; + } break; } case 4: @@ -130,6 +156,7 @@ let config: Config; suite, maxChunkTimeMS: Infinity, argumentsPrefixes: ['?q='], + noLongPathAssert: false, }; if (process.argv.length >= 7) { config.argumentsPrefixes = (await fs.readFile(argsPrefixesFile, 'utf8')) @@ -153,29 +180,16 @@ let config: Config; config.argumentsPrefixes.sort((a, b) => b.length - a.length); // Load expectations (if any) - let expectationLines = new Set<string>(); - if (config.expectations) { - expectationLines = new Set( - (await fs.readFile(config.expectations.file, 'utf8')).split(/\r?\n/).filter(l => l.length) - ); - } + const expectations: Map<string, string[]> = await loadQueryFile( + config.argumentsPrefixes, + config.expectations + ); - const expectations: Map<string, string[]> = new Map(); - for (const prefix of config.argumentsPrefixes) { - expectations.set(prefix, []); - } - - expLoop: for (const exp of expectationLines) { - // Take each expectation for the longest prefix it matches. - for (const argsPrefix of config.argumentsPrefixes) { - const prefix = config.expectations!.prefix + argsPrefix; - if (exp.startsWith(prefix)) { - expectations.get(argsPrefix)!.push(exp.substring(prefix.length)); - continue expLoop; - } - } - console.log('note: ignored expectation: ' + exp); - } + // Load fullyExpandSubtrees queries (if any) + const fullyExpand: Map<string, string[]> = await loadQueryFile( + config.argumentsPrefixes, + config.fullyExpandSubtrees + ); const loader = new DefaultTestFileLoader(); const lines = []; @@ -183,6 +197,7 @@ let config: Config; const rootQuery = new TestQueryMultiFile(config.suite, []); const tree = await loader.loadTree(rootQuery, { subqueriesToExpand: expectations.get(prefix), + fullyExpandSubtrees: fullyExpand.get(prefix), maxChunkTime: config.maxChunkTimeMS, }); @@ -199,22 +214,24 @@ let config: Config; alwaysExpandThroughLevel, })) { assert(query instanceof TestQueryMultiCase); - const queryString = query.toString(); - // Check for a safe-ish path length limit. Filename must be <= 255, and on Windows the whole - // path must be <= 259. Leave room for e.g.: - // 'c:\b\s\w\xxxxxxxx\layout-test-results\external\wpt\webgpu\cts_worker=0_q=...-actual.txt' - assert( - queryString.length < 185, - `Generated test variant would produce too-long -actual.txt filename. Possible solutions: + if (!config.noLongPathAssert) { + const queryString = query.toString(); + // Check for a safe-ish path length limit. Filename must be <= 255, and on Windows the whole + // path must be <= 259. Leave room for e.g.: + // 'c:\b\s\w\xxxxxxxx\layout-test-results\external\wpt\webgpu\cts_worker=0_q=...-actual.txt' + assert( + queryString.length < 185, + `Generated test variant would produce too-long -actual.txt filename. Possible solutions: - Reduce the length of the parts of the test query - Reduce the parameterization of the test - Make the test function faster and regenerate the listing_meta entry - Reduce the specificity of test expectations (if you're using them) ${queryString}` - ); + ); + } lines.push({ - urlQueryString: prefix + query.toString(), // "?worker=0&q=..." + urlQueryString: prefix + query.toString(), // "?debug=0&q=..." comment: useChunking ? `estimated: ${subtreeCounts?.totalTimeMS.toFixed(3)} ms` : undefined, }); @@ -232,6 +249,39 @@ ${queryString}` process.exit(1); }); +async function loadQueryFile( + argumentsPrefixes: string[], + queryFile?: { + file: string; + prefix: string; + } +): Promise<Map<string, string[]>> { + let lines = new Set<string>(); + if (queryFile) { + lines = new Set( + (await fs.readFile(queryFile.file, 'utf8')).split(/\r?\n/).filter(l => l.length) + ); + } + + const result: Map<string, string[]> = new Map(); + for (const prefix of argumentsPrefixes) { + result.set(prefix, []); + } + + expLoop: for (const exp of lines) { + // Take each expectation for the longest prefix it matches. + for (const argsPrefix of argumentsPrefixes) { + const prefix = queryFile!.prefix + argsPrefix; + if (exp.startsWith(prefix)) { + result.get(argsPrefix)!.push(exp.substring(prefix.length)); + continue expLoop; + } + } + console.log('note: ignored expectation: ' + exp); + } + return result; +} + async function generateFile( lines: Array<{ urlQueryString?: string; comment?: string } | undefined> ): Promise<void> { @@ -240,13 +290,20 @@ async function generateFile( result += await fs.readFile(config.template, 'utf8'); + const variantList = []; for (const line of lines) { if (line !== undefined) { - if (line.urlQueryString) result += `<meta name=variant content='${line.urlQueryString}'>`; + if (line.urlQueryString) { + result += `<meta name=variant content='${line.urlQueryString}'>`; + variantList.push(line.urlQueryString); + } if (line.comment) result += `<!-- ${line.comment} -->`; } result += '\n'; } await fs.writeFile(config.out, result); + if (config.outVariantList) { + await fs.writeFile(config.outVariantList, JSON.stringify(variantList, undefined, 2)); + } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts index fb33ae20fb..a8bef354cc 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts @@ -36,21 +36,13 @@ In more detail: - For each suite seen, loads its listing_meta.json, takes the max of the old and new data, and writes it back out. -How to generate TIMING_LOG_FILES files: - -- Launch the 'websocket-logger' tool (see its README.md), which listens for - log messages on localhost:59497. -- Run the tests you want to capture data for, on the same system. Since - logging is done through the websocket side-channel, you can run the tests - under any runtime (standalone, WPT, etc.) as long as WebSocket support is - available (always true in browsers). -- Run \`tools/merge_listing_times webgpu -- tools/websocket-logger/wslog-*.txt\` +See 'docs/adding_timing_metadata.md' for how to generate TIMING_LOG_FILES files. `); process.exit(rc); } const kHeader = `{ - "_comment": "SEMI AUTO-GENERATED: Please read docs/adding_timing_metadata.md.", + "_comment": "SEMI AUTO-GENERATED. This list is NOT exhaustive. Please read docs/adding_timing_metadata.md.", `; const kFooter = `\ "_end": "" diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts index 164ee3259a..47aa9782a8 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts @@ -2,7 +2,7 @@ import * as process from 'process'; import { crawl } from './crawl.js'; -function usage(rc: number): void { +function usage(rc: number): never { console.error(`Usage: tools/validate [options] [SUITE_DIRS...] For each suite in SUITE_DIRS, validate some properties about the file: @@ -14,23 +14,40 @@ For each suite in SUITE_DIRS, validate some properties about the file: - That each case query is not too long Example: - tools/validate src/unittests/ src/webgpu/ + tools/validate src/unittests src/webgpu Options: - --help Print this message and exit. + --help Print this message and exit. + --print-metadata-warnings Print non-fatal warnings about listing_meta.json files. `); process.exit(rc); } const args = process.argv.slice(2); +if (args.length < 1) { + usage(0); +} if (args.indexOf('--help') !== -1) { usage(0); } -if (args.length < 1) { +let printMetadataWarnings = false; +const suiteDirs = []; +for (const arg of args) { + if (arg === '--print-metadata-warnings') { + printMetadataWarnings = true; + } else { + suiteDirs.push(arg); + } +} + +if (suiteDirs.length === 0) { usage(0); } -for (const suiteDir of args) { - void crawl(suiteDir, true); +for (const suiteDir of suiteDirs) { + void crawl(suiteDir, { + validate: true, + printMetadataWarnings, + }); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/crc32.ts b/dom/webgpu/tests/cts/checkout/src/common/util/crc32.ts new file mode 100644 index 0000000000..5f74b4662e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/util/crc32.ts @@ -0,0 +1,57 @@ +/// CRC32 immutable lookup table data. +const kCRC32LUT = [ + 0, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, + 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, + 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, + 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, + 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, + 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, + 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, + 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, + 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, + 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, + 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, + 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, + 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, + 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, + 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, + 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, + 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, + 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, + 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, + 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, + 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, + 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, + 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, + 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, + 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, + 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, + 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, + 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, +]; + +/** + * @param str the input string + * @returns the CRC32 of the input string + * @see https://en.wikipedia.org/wiki/Cyclic_redundancy_check#CRC-32_algorithm + */ +export function crc32(str: string): number { + const utf8 = new TextEncoder().encode(str); + const u32 = new Uint32Array(1); + + u32[0] = 0xffffffff; + for (const c of utf8) { + u32[0] = (u32[0] >>> 8) ^ kCRC32LUT[(u32[0] & 0xff) ^ c]; + } + u32[0] = u32[0] ^ 0xffffffff; + return u32[0]; +} + +/** @returns the input number has a 8-character hex string */ +export function toHexString(number: number): string { + return ('00000000' + number.toString(16)).slice(-8); +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/parse_imports.ts b/dom/webgpu/tests/cts/checkout/src/common/util/parse_imports.ts new file mode 100644 index 0000000000..4b5604b897 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/util/parse_imports.ts @@ -0,0 +1,36 @@ +/** + * Parses all the paths of the typescript `import` statements from content + * @param path the current path of the file + * @param content the file content + * @returns the list of import paths + */ +export function parseImports(path: string, content: string): string[] { + const out: string[] = []; + const importRE = /^import\s[^'"]*(['"])([./\w]*)(\1);/gm; + let importMatch: RegExpMatchArray | null; + while ((importMatch = importRE.exec(content))) { + const importPath = importMatch[2].replace(`'`, '').replace(`"`, ''); + out.push(joinPath(path, importPath)); + } + return out; +} + +function joinPath(a: string, b: string): string { + const aParts = a.split('/'); + const bParts = b.split('/'); + aParts.pop(); // remove file + let bStart = 0; + while (aParts.length > 0) { + switch (bParts[bStart]) { + case '.': + bStart++; + continue; + case '..': + aParts.pop(); + bStart++; + continue; + } + break; + } + return [...aParts, ...bParts.slice(bStart)].join('/'); +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/util.ts b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts index 9433aaddb0..4092b997a3 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/util/util.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts @@ -1,7 +1,6 @@ import { Float16Array } from '../../external/petamoriken/float16/float16.js'; import { SkipTestCase } from '../framework/fixture.js'; import { globalTestConfig } from '../framework/test_config.js'; -import { Logger } from '../internal/logging/logger.js'; import { keysOf } from './data_tables.js'; import { timeout } from './timeout.js'; @@ -24,7 +23,7 @@ export class ErrorWithExtra extends Error { super(message); const oldExtras = baseOrMessage instanceof ErrorWithExtra ? baseOrMessage.extra : {}; - this.extra = Logger.globalDebugMode + this.extra = globalTestConfig.enableDebugLogs ? { ...oldExtras, ...newExtra() } : { omitted: 'pass ?debug=1' }; } @@ -304,6 +303,8 @@ const TypedArrayBufferViewInstances = [ new Float16Array(), new Float32Array(), new Float64Array(), + new BigInt64Array(), + new BigUint64Array(), ] as const; export type TypedArrayBufferView = (typeof TypedArrayBufferViewInstances)[number]; diff --git a/dom/webgpu/tests/cts/checkout/src/external/README.md b/dom/webgpu/tests/cts/checkout/src/external/README.md index 84fbf9c732..29c5f41f3a 100644 --- a/dom/webgpu/tests/cts/checkout/src/external/README.md +++ b/dom/webgpu/tests/cts/checkout/src/external/README.md @@ -28,4 +28,4 @@ should be listed below. | **Name** | **Origin** | **License** | **Version** | **Purpose** | |----------------------|--------------------------------------------------|-------------|-------------|------------------------------------------------| -| petamoriken/float16 | [github](https://github.com/petamoriken/float16) | MIT | 3.6.6 | Fluent support for f16 numbers via TypedArrays | +| petamoriken/float16 | [github](https://github.com/petamoriken/float16) | MIT | 3.8.6 | Fluent support for f16 numbers via TypedArrays | diff --git a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt index e8eacf4e7f..e3d7962fe8 100644 --- a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt +++ b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2021 Kenta Moriuchi +Copyright (c) 2017-2024 Kenta Moriuchi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts index c9d66ab7ca..7e79bc177c 100644 --- a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts +++ b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts @@ -331,6 +331,26 @@ export interface Float16Array { subarray(begin?: number, end?: number): Float16Array; /** + * Copies the array and returns the copy with the elements in reverse order. + */ + toReversed(): Float16Array; + + /** + * Copies and sorts the array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending. + */ + toSorted(compareFn?: (a: number, b: number) => number): Float16Array; + + /** + * Copies the array and replaces the element at the given index with the provided value. + * @param index The zero-based location in the array for which to replace an element. + * @param value Element to insert into the array in place of the replaced element. + */ + with(index: number, value: number): Float16Array; + + /** * Converts a number to a string by using the current locale. */ toLocaleString(): string; @@ -468,4 +488,11 @@ export declare function setFloat16( * Returns the nearest half-precision float representation of a number. * @param x A numeric expression. */ +export declare function f16round(x: number): number; + +/** + * Returns the nearest half-precision float representation of a number. + * @alias f16round + * @param x A numeric expression. + */ export declare function hfround(x: number): number; diff --git a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js index 54843a4842..1031e2bcda 100644 --- a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js +++ b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js @@ -1,4 +1,4 @@ -/*! @petamoriken/float16 v3.6.6 | MIT License - https://github.com/petamoriken/float16 */ +/*! @petamoriken/float16 v3.8.6 | MIT License - https://github.com/petamoriken/float16 */ const THIS_IS_NOT_AN_OBJECT = "This is not an object"; const THIS_IS_NOT_A_FLOAT16ARRAY_OBJECT = "This is not a Float16Array object"; @@ -19,6 +19,8 @@ const CANNOT_MIX_BIGINT_AND_OTHER_TYPES = const ITERATOR_PROPERTY_IS_NOT_CALLABLE = "@@iterator property is not callable"; const REDUCE_OF_EMPTY_ARRAY_WITH_NO_INITIAL_VALUE = "Reduce of empty array with no initial value"; +const THE_COMPARISON_FUNCTION_MUST_BE_EITHER_A_FUNCTION_OR_UNDEFINED = + "The comparison function must be either a function or undefined"; const OFFSET_IS_OUT_OF_BOUNDS = "Offset is out of bounds"; function uncurryThis(target) { @@ -48,7 +50,8 @@ const { } = Reflect; const NativeProxy = Proxy; const { - MAX_SAFE_INTEGER: MAX_SAFE_INTEGER, + EPSILON, + MAX_SAFE_INTEGER, isFinite: NumberIsFinite, isNaN: NumberIsNaN, } = Number; @@ -97,7 +100,10 @@ const ArrayPrototypeToLocaleString = uncurryThis( ); const NativeArrayPrototypeSymbolIterator = ArrayPrototype[SymbolIterator]; const ArrayPrototypeSymbolIterator = uncurryThis(NativeArrayPrototypeSymbolIterator); -const MathTrunc = Math.trunc; +const { + abs: MathAbs, + trunc: MathTrunc, +} = Math; const NativeArrayBuffer = ArrayBuffer; const ArrayBufferIsView = NativeArrayBuffer.isView; const ArrayBufferPrototype = NativeArrayBuffer.prototype; @@ -146,6 +152,7 @@ const TypedArrayPrototypeGetSymbolToStringTag = uncurryThisGetter( TypedArrayPrototype, SymbolToStringTag ); +const NativeUint8Array = Uint8Array; const NativeUint16Array = Uint16Array; const Uint16ArrayFrom = (...args) => { return ReflectApply(TypedArrayFrom, NativeUint16Array, args); @@ -190,7 +197,10 @@ const SafeIteratorPrototype = ObjectCreate(null, { }, }); function safeIfNeeded(array) { - if (array[SymbolIterator] === NativeArrayPrototypeSymbolIterator) { + if ( + array[SymbolIterator] === NativeArrayPrototypeSymbolIterator && + ArrayIteratorPrototype.next === ArrayIteratorPrototypeNext + ) { return array; } const safe = ObjectCreate(SafeIteratorPrototype); @@ -221,8 +231,10 @@ function wrap(generator) { } function isObject(value) { - return (value !== null && typeof value === "object") || - typeof value === "function"; + return ( + (value !== null && typeof value === "object") || + typeof value === "function" + ); } function isObjectLike(value) { return value !== null && typeof value === "object"; @@ -232,11 +244,16 @@ function isNativeTypedArray(value) { } function isNativeBigIntTypedArray(value) { const typedArrayName = TypedArrayPrototypeGetSymbolToStringTag(value); - return typedArrayName === "BigInt64Array" || - typedArrayName === "BigUint64Array"; + return ( + typedArrayName === "BigInt64Array" || + typedArrayName === "BigUint64Array" + ); } function isArrayBuffer(value) { try { + if (ArrayIsArray(value)) { + return false; + } ArrayBufferPrototypeGetByteLength( (value)); return true; } catch (e) { @@ -254,25 +271,26 @@ function isSharedArrayBuffer(value) { return false; } } +function isAnyArrayBuffer(value) { + return isArrayBuffer(value) || isSharedArrayBuffer(value); +} function isOrdinaryArray(value) { if (!ArrayIsArray(value)) { return false; } - if (value[SymbolIterator] === NativeArrayPrototypeSymbolIterator) { - return true; - } - const iterator = value[SymbolIterator](); - return iterator[SymbolToStringTag] === "Array Iterator"; + return ( + value[SymbolIterator] === NativeArrayPrototypeSymbolIterator && + ArrayIteratorPrototype.next === ArrayIteratorPrototypeNext + ); } function isOrdinaryNativeTypedArray(value) { if (!isNativeTypedArray(value)) { return false; } - if (value[SymbolIterator] === NativeTypedArrayPrototypeSymbolIterator) { - return true; - } - const iterator = value[SymbolIterator](); - return iterator[SymbolToStringTag] === "Array Iterator"; + return ( + value[SymbolIterator] === NativeTypedArrayPrototypeSymbolIterator && + ArrayIteratorPrototype.next === ArrayIteratorPrototypeNext + ); } function isCanonicalIntegerIndexString(value) { if (typeof value !== "string") { @@ -307,11 +325,37 @@ function hasFloat16ArrayBrand(target) { return ReflectHas(constructor, brand); } +const INVERSE_OF_EPSILON = 1 / EPSILON; +function roundTiesToEven(num) { + return (num + INVERSE_OF_EPSILON) - INVERSE_OF_EPSILON; +} +const FLOAT16_MIN_VALUE = 6.103515625e-05; +const FLOAT16_MAX_VALUE = 65504; +const FLOAT16_EPSILON = 0.0009765625; +const FLOAT16_EPSILON_MULTIPLIED_BY_FLOAT16_MIN_VALUE = FLOAT16_EPSILON * FLOAT16_MIN_VALUE; +const FLOAT16_EPSILON_DEVIDED_BY_EPSILON = FLOAT16_EPSILON * INVERSE_OF_EPSILON; +function roundToFloat16(num) { + const number = +num; + if (!NumberIsFinite(number) || number === 0) { + return number; + } + const sign = number > 0 ? 1 : -1; + const absolute = MathAbs(number); + if (absolute < FLOAT16_MIN_VALUE) { + return sign * roundTiesToEven(absolute / FLOAT16_EPSILON_MULTIPLIED_BY_FLOAT16_MIN_VALUE) * FLOAT16_EPSILON_MULTIPLIED_BY_FLOAT16_MIN_VALUE; + } + const temp = (1 + FLOAT16_EPSILON_DEVIDED_BY_EPSILON) * absolute; + const result = temp - (temp - absolute); + if (result > FLOAT16_MAX_VALUE || NumberIsNaN(result)) { + return sign * Infinity; + } + return sign * result; +} const buffer = new NativeArrayBuffer(4); const floatView = new NativeFloat32Array(buffer); const uint32View = new NativeUint32Array(buffer); -const baseTable = new NativeUint32Array(512); -const shiftTable = new NativeUint32Array(512); +const baseTable = new NativeUint16Array(512); +const shiftTable = new NativeUint8Array(512); for (let i = 0; i < 256; ++i) { const e = i - 127; if (e < -27) { @@ -342,18 +386,16 @@ for (let i = 0; i < 256; ++i) { } } function roundToFloat16Bits(num) { - floatView[0] = (num); + floatView[0] = roundToFloat16(num); const f = uint32View[0]; const e = (f >> 23) & 0x1ff; return baseTable[e] + ((f & 0x007fffff) >> shiftTable[e]); } const mantissaTable = new NativeUint32Array(2048); -const exponentTable = new NativeUint32Array(64); -const offsetTable = new NativeUint32Array(64); for (let i = 1; i < 1024; ++i) { let m = i << 13; let e = 0; - while((m & 0x00800000) === 0) { + while ((m & 0x00800000) === 0) { m <<= 1; e -= 0x00800000; } @@ -364,6 +406,7 @@ for (let i = 1; i < 1024; ++i) { for (let i = 1024; i < 2048; ++i) { mantissaTable[i] = 0x38000000 + ((i - 1024) << 13); } +const exponentTable = new NativeUint32Array(64); for (let i = 1; i < 31; ++i) { exponentTable[i] = i << 23; } @@ -373,14 +416,15 @@ for (let i = 33; i < 63; ++i) { exponentTable[i] = 0x80000000 + ((i - 32) << 23); } exponentTable[63] = 0xc7800000; +const offsetTable = new NativeUint16Array(64); for (let i = 1; i < 64; ++i) { if (i !== 32) { offsetTable[i] = 1024; } } function convertToNumber(float16bits) { - const m = float16bits >> 10; - uint32View[0] = mantissaTable[offsetTable[m] + (float16bits & 0x3ff)] + exponentTable[m]; + const i = float16bits >> 10; + uint32View[0] = mantissaTable[offsetTable[i] + (float16bits & 0x3ff)] + exponentTable[i]; return floatView[0]; } @@ -572,26 +616,20 @@ class Float16Array { let float16bitsArray; if (isFloat16Array(input)) { float16bitsArray = ReflectConstruct(NativeUint16Array, [getFloat16BitsArray(input)], new.target); - } else if (isObject(input) && !isArrayBuffer(input)) { + } else if (isObject(input) && !isAnyArrayBuffer(input)) { let list; let length; if (isNativeTypedArray(input)) { list = input; length = TypedArrayPrototypeGetLength(input); const buffer = TypedArrayPrototypeGetBuffer(input); - const BufferConstructor = !isSharedArrayBuffer(buffer) - ? (SpeciesConstructor( - buffer, - NativeArrayBuffer - )) - : NativeArrayBuffer; if (IsDetachedBuffer(buffer)) { throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER); } if (isNativeBigIntTypedArray(input)) { throw NativeTypeError(CANNOT_MIX_BIGINT_AND_OTHER_TYPES); } - const data = new BufferConstructor( + const data = new NativeArrayBuffer( length * BYTES_PER_ELEMENT ); float16bitsArray = ReflectConstruct(NativeUint16Array, [data], new.target); @@ -758,6 +796,30 @@ class Float16Array { } return convertToNumber(float16bitsArray[k]); } + with(index, value) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const relativeIndex = ToIntegerOrInfinity(index); + const k = relativeIndex >= 0 ? relativeIndex : length + relativeIndex; + const number = +value; + if (k < 0 || k >= length) { + throw NativeRangeError(OFFSET_IS_OUT_OF_BOUNDS); + } + const uint16 = new NativeUint16Array( + TypedArrayPrototypeGetBuffer(float16bitsArray), + TypedArrayPrototypeGetByteOffset(float16bitsArray), + TypedArrayPrototypeGetLength(float16bitsArray) + ); + const cloned = new Float16Array( + TypedArrayPrototypeGetBuffer( + TypedArrayPrototypeSlice(uint16) + ) + ); + const array = getFloat16BitsArray(cloned); + array[k] = roundToFloat16Bits(number); + return cloned; + } map(callback, ...opts) { assertFloat16Array(this); const float16bitsArray = getFloat16BitsArray(this); @@ -995,6 +1057,23 @@ class Float16Array { TypedArrayPrototypeReverse(float16bitsArray); return this; } + toReversed() { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const uint16 = new NativeUint16Array( + TypedArrayPrototypeGetBuffer(float16bitsArray), + TypedArrayPrototypeGetByteOffset(float16bitsArray), + TypedArrayPrototypeGetLength(float16bitsArray) + ); + const cloned = new Float16Array( + TypedArrayPrototypeGetBuffer( + TypedArrayPrototypeSlice(uint16) + ) + ); + const clonedFloat16bitsArray = getFloat16BitsArray(cloned); + TypedArrayPrototypeReverse(clonedFloat16bitsArray); + return cloned; + } fill(value, ...opts) { assertFloat16Array(this); const float16bitsArray = getFloat16BitsArray(this); @@ -1020,6 +1099,29 @@ class Float16Array { }); return this; } + toSorted(compareFn) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + if (compareFn !== undefined && typeof compareFn !== "function") { + throw new NativeTypeError(THE_COMPARISON_FUNCTION_MUST_BE_EITHER_A_FUNCTION_OR_UNDEFINED); + } + const sortCompare = compareFn !== undefined ? compareFn : defaultCompare; + const uint16 = new NativeUint16Array( + TypedArrayPrototypeGetBuffer(float16bitsArray), + TypedArrayPrototypeGetByteOffset(float16bitsArray), + TypedArrayPrototypeGetLength(float16bitsArray) + ); + const cloned = new Float16Array( + TypedArrayPrototypeGetBuffer( + TypedArrayPrototypeSlice(uint16) + ) + ); + const clonedFloat16bitsArray = getFloat16BitsArray(cloned); + TypedArrayPrototypeSort(clonedFloat16bitsArray, (x, y) => { + return sortCompare(convertToNumber(x), convertToNumber(y)); + }); + return cloned; + } slice(start, end) { assertFloat16Array(this); const float16bitsArray = getFloat16BitsArray(this); @@ -1216,13 +1318,8 @@ function setFloat16(dataView, byteOffset, value, ...opts) { ); } -function hfround(x) { - const number = +x; - if (!NumberIsFinite(number) || number === 0) { - return number; - } - const x16 = roundToFloat16Bits(number); - return convertToNumber(x16); +function f16round(x) { + return roundToFloat16(x); } -export { Float16Array, getFloat16, hfround, isFloat16Array, isTypedArray, setFloat16 }; +export { Float16Array, f16round, getFloat16, f16round as hfround, isFloat16Array, isTypedArray, setFloat16 };
\ No newline at end of file diff --git a/dom/webgpu/tests/cts/checkout/src/resources/README.md b/dom/webgpu/tests/cts/checkout/src/resources/README.md index 824f82b998..a1ed060417 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/README.md +++ b/dom/webgpu/tests/cts/checkout/src/resources/README.md @@ -2,14 +2,92 @@ Always use `getResourcePath()` to get the appropriate path to these resources de on the context (WPT, standalone, worker, etc.) -The test video files were generated with the ffmpeg cmds below: -ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx -pix_fmt yuv420p -frames 500 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-vp8-bt601.webm -ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libtheora -pix_fmt yuv420p -frames 500 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-theora-bt601.ogv -ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libx264 -pix_fmt yuv420p -frames 500 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-h264-bt601.mp4 -ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 500 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-vp9-bt601.webm -ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 500 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vf scale=out_color_matrix=bt709:out_range=tv four-colors-vp9-bt709.webm - -These rotation test files are copies of four-colors-h264-bt601.mp4 with metadata changes. -ffmpeg.exe -i .\four-colors-h264-bt601.mp4 -c copy -metadata:s:v rotate=90 four-colors-h264-bt601-rotate-90.mp4 -ffmpeg.exe -i .\four-colors-h264-bt601.mp4 -c copy -metadata:s:v rotate=180 four-colors-h264-bt601-rotate-180.mp4 -ffmpeg.exe -i .\four-colors-h264-bt601.mp4 -c copy -metadata:s:v rotate=270 four-colors-h264-bt601-rotate-270.mp4
\ No newline at end of file +The test video files were generated with by ffmpeg cmds below: +``` +// Generate four-colors-vp8-bt601.webm, mimeType: 'video/webm; codecs=vp8' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-vp8-bt601.webm + +// Generate four-colors-h264-bt601.mp4, mimeType: 'video/mp4; codecs=avc1.4d400c' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libx264 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-h264-bt601.mp4 + +// Generate four-colors-vp9-bt601.webm, mimeType: 'video/webm; codecs=vp9' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-vp9-bt601.webm + +// Generate four-colors-vp9-bt709.webm, mimeType: 'video/webm; codecs=vp9' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vf scale=out_color_matrix=bt709:out_range=tv four-colors-vp9-bt709.webm + +// Generate four-colors-vp9-bt601.mp4, mimeType: 'video/mp4; codecs=vp9' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv four-colors-vp9-bt601.mp4 +``` + +Generate video files to test rotation behaviour. +Use ffmepg to rotate video content x degrees in cw direction (by using `transpose`) and update transform matrix in metadata through `display_rotation` to x degrees to apply ccw direction rotation. + +H264 rotated video files are generated by ffmpeg cmds below: +``` +// Generate four-colors-h264-bt601-rotate-90.mp4, mimeType: 'video/mp4; codecs=avc1.4d400c' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libx264 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv -vf transpose=2 temp.mp4 +ffmpeg -display_rotation 270 -i temp.mp4 -c copy four-colors-h264-bt601-rotate-90.mp4 +rm temp.mp4 + +// Generate four-colors-h264-bt601-rotate-180.mp4, mimeType: 'video/mp4; codecs=avc1.4d400c' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libx264 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv -vf transpose=2,transpose=2 temp.mp4 +ffmpeg -display_rotation 180 -i temp.mp4 -c copy four-colors-h264-bt601-rotate-180.mp4 +rm temp.mp4 + +// Generate four-colors-h264-bt601-rotate-270.mp4, mimeType: 'video/mp4; codecs=avc1.4d400c' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libx264 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv -vf transpose=1 temp.mp4 +ffmpeg -display_rotation 90 -i temp.mp4 -c copy four-colors-h264-bt601-rotate-270.mp4 +rm temp.mp4 + +``` + +Vp9 rotated video files are generated by ffmpeg cmds below: +``` +// Generate four-colors-vp9-bt601-rotate-90.mp4, mimeType: 'video/mp4; codecs=vp9' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv -vf transpose=2 temp.mp4 +ffmpeg -display_rotation 270 -i temp.mp4 -c copy four-colors-vp9-bt601-rotate-90.mp4 +rm temp.mp4 + +// Generate four-colors-vp9-bt601-rotate-180.mp4, mimeType: 'video/mp4; codecs=vp9' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv -vf transpose=2,transpose=2 temp.mp4 +ffmpeg -display_rotation 180 -i temp.mp4 -c copy four-colors-vp9-bt601-rotate-180.mp4 +rm temp.mp4 + +// Generate four-colors-vp9-bt601-rotate-270.mp4, mimeType: 'video/mp4; codecs=vp9' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv -vf transpose=1 temp.mp4 +ffmpeg -display_rotation 90 -i temp.mp4 -c copy four-colors-vp9-bt601-rotate-270.mp4 +rm temp.mp4 + +``` + +Generate video files to test flip behaviour. +Use ffmpeg to flip video content. Using `display_hflip` to do horizontal flip and `display_vflip` to do vertical flip. + +H264 flip video files are generated by ffmpeg cmds below: +``` +// Generate four-colors-h264-bt601-hflip.mp4, mimeType: 'video/mp4; codecs=avc1.4d400c' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libx264 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv temp.mp4 +ffmpeg -display_hflip -i temp.mp4 -c copy four-colors-h264-bt601-hflip.mp4 +rm temp.mp4 + +// Generate four-colors-h264-bt601-vflip.mp4, mimeType: 'video/mp4; codecs=avc1.4d400c' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libx264 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv temp.mp4 +ffmpeg -display_vflip -i temp.mp4 -c copy four-colors-h264-bt601-vflip.mp4 +rm temp.mp4 + +``` + +Vp9 flip video files are generated by ffmpeg cmds below: +``` +// Generate four-colors-vp9-bt601-hflip.mp4, mimeType: 'video/mp4; codecs=vp09.00.10.08' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv temp.mp4 +ffmpeg -display_hflip -i temp.mp4 -c copy four-colors-vp9-bt601-hflip.mp4 +rm temp.mp4 + +// Generate four-colors-vp9-bt601-vflip.mp4, mimeType: 'video/mp4; codecs=vp09.00.10.08' +ffmpeg.exe -loop 1 -i .\four-colors.png -c:v libvpx-vp9 -pix_fmt yuv420p -frames 50 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range tv temp.mp4 +ffmpeg -display_vflip -i temp.mp4 -c copy four-colors-vp9-bt601-vflip.mp4 +rm temp.mp4 + +``` diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/hashes.json b/dom/webgpu/tests/cts/checkout/src/resources/cache/hashes.json new file mode 100644 index 0000000000..90efb474ad --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/hashes.json @@ -0,0 +1,111 @@ +{ + "webgpu/shader/execution/binary/af_addition.bin": "17c26b18", + "webgpu/shader/execution/binary/af_logical.bin": "b4fdda88", + "webgpu/shader/execution/binary/af_division.bin": "fa1fc451", + "webgpu/shader/execution/binary/af_matrix_addition.bin": "a2bebfc0", + "webgpu/shader/execution/binary/af_matrix_subtraction.bin": "5456944c", + "webgpu/shader/execution/binary/af_multiplication.bin": "fc54cae0", + "webgpu/shader/execution/binary/af_remainder.bin": "2ee1a014", + "webgpu/shader/execution/binary/af_subtraction.bin": "ba82dab3", + "webgpu/shader/execution/binary/f16_addition.bin": "4ccf0cde", + "webgpu/shader/execution/binary/f16_logical.bin": "5ffb4769", + "webgpu/shader/execution/binary/f16_division.bin": "c69d5326", + "webgpu/shader/execution/binary/f16_matrix_addition.bin": "23006f90", + "webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "3b581360", + "webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "4c2cf2fa", + "webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "902ffcbc", + "webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "48acf022", + "webgpu/shader/execution/binary/f16_multiplication.bin": "e83fedc6", + "webgpu/shader/execution/binary/f16_remainder.bin": "68178090", + "webgpu/shader/execution/binary/f16_subtraction.bin": "de63294a", + "webgpu/shader/execution/binary/f32_addition.bin": "d693585", + "webgpu/shader/execution/binary/f32_logical.bin": "57b9e9da", + "webgpu/shader/execution/binary/f32_division.bin": "79048538", + "webgpu/shader/execution/binary/f32_matrix_addition.bin": "301c58ba", + "webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "41bae804", + "webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "d8a1003a", + "webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "3b1cc190", + "webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "66a57dbc", + "webgpu/shader/execution/binary/f32_multiplication.bin": "c0bf07da", + "webgpu/shader/execution/binary/f32_remainder.bin": "238fd8eb", + "webgpu/shader/execution/binary/f32_subtraction.bin": "d977904f", + "webgpu/shader/execution/binary/i32_arithmetic.bin": "8168bdb4", + "webgpu/shader/execution/binary/i32_comparison.bin": "eae9d767", + "webgpu/shader/execution/binary/u32_arithmetic.bin": "5c313ea9", + "webgpu/shader/execution/binary/u32_comparison.bin": "5ef80f48", + "webgpu/shader/execution/abs.bin": "1a23882d", + "webgpu/shader/execution/acos.bin": "66020d3b", + "webgpu/shader/execution/acosh.bin": "eeed5b15", + "webgpu/shader/execution/asin.bin": "e38b87bf", + "webgpu/shader/execution/asinh.bin": "d5cf509e", + "webgpu/shader/execution/atan.bin": "780f5bf9", + "webgpu/shader/execution/atan2.bin": "242c3f80", + "webgpu/shader/execution/atanh.bin": "2c6771e5", + "webgpu/shader/execution/bitcast.bin": "4ebd46da", + "webgpu/shader/execution/ceil.bin": "b190ce63", + "webgpu/shader/execution/clamp.bin": "9ba5f3c9", + "webgpu/shader/execution/cos.bin": "edeb923", + "webgpu/shader/execution/cosh.bin": "c12c748b", + "webgpu/shader/execution/cross.bin": "4cfaddf8", + "webgpu/shader/execution/degrees.bin": "dc77e03b", + "webgpu/shader/execution/determinant.bin": "a87e4d61", + "webgpu/shader/execution/distance.bin": "9e397f5a", + "webgpu/shader/execution/dot.bin": "db692304", + "webgpu/shader/execution/exp.bin": "b0cbd306", + "webgpu/shader/execution/exp2.bin": "b32745cd", + "webgpu/shader/execution/faceForward.bin": "f0cc892a", + "webgpu/shader/execution/floor.bin": "4a460013", + "webgpu/shader/execution/fma.bin": "c89c3d19", + "webgpu/shader/execution/fract.bin": "f6230a96", + "webgpu/shader/execution/frexp.bin": "132962c", + "webgpu/shader/execution/inverseSqrt.bin": "cc1b943c", + "webgpu/shader/execution/ldexp.bin": "4e14b67d", + "webgpu/shader/execution/length.bin": "a75b23ef", + "webgpu/shader/execution/log.bin": "61175a12", + "webgpu/shader/execution/log2.bin": "d24b375d", + "webgpu/shader/execution/max.bin": "5689d61b", + "webgpu/shader/execution/min.bin": "8fd8d393", + "webgpu/shader/execution/mix.bin": "caf44b85", + "webgpu/shader/execution/modf.bin": "223ff03f", + "webgpu/shader/execution/normalize.bin": "e0634ba", + "webgpu/shader/execution/pack2x16float.bin": "a8ca6b51", + "webgpu/shader/execution/pow.bin": "b2bbd5ce", + "webgpu/shader/execution/quantizeToF16.bin": "be9ef6ab", + "webgpu/shader/execution/radians.bin": "afaf4f61", + "webgpu/shader/execution/reflect.bin": "742b05b4", + "webgpu/shader/execution/refract.bin": "ad05949f", + "webgpu/shader/execution/round.bin": "3a006c0c", + "webgpu/shader/execution/saturate.bin": "61753b4f", + "webgpu/shader/execution/sign.bin": "71e6517c", + "webgpu/shader/execution/sin.bin": "17c44cc8", + "webgpu/shader/execution/sinh.bin": "151ae2d1", + "webgpu/shader/execution/smoothstep.bin": "35db8f9a", + "webgpu/shader/execution/sqrt.bin": "a4b4bdf9", + "webgpu/shader/execution/step.bin": "e3cc0f86", + "webgpu/shader/execution/tan.bin": "9ad0e1f1", + "webgpu/shader/execution/tanh.bin": "99454fdd", + "webgpu/shader/execution/transpose.bin": "959a2407", + "webgpu/shader/execution/trunc.bin": "1a47263e", + "webgpu/shader/execution/unpack2x16float.bin": "39f959fe", + "webgpu/shader/execution/unpack2x16snorm.bin": "b409d047", + "webgpu/shader/execution/unpack2x16unorm.bin": "49a89145", + "webgpu/shader/execution/unpack4x8snorm.bin": "e53a7842", + "webgpu/shader/execution/unpack4x8unorm.bin": "9bc3f0ea", + "webgpu/shader/execution/unary/af_arithmetic.bin": "e9e06bc7", + "webgpu/shader/execution/unary/af_assignment.bin": "5c9160c2", + "webgpu/shader/execution/unary/bool_conversion.bin": "a2260d3a", + "webgpu/shader/execution/unary/f16_arithmetic.bin": "7a3fd3db", + "webgpu/shader/execution/unary/f16_conversion.bin": "b9407945", + "webgpu/shader/execution/unary/f32_arithmetic.bin": "6e392701", + "webgpu/shader/execution/unary/f32_conversion.bin": "6ae4cce9", + "webgpu/shader/execution/unary/i32_arithmetic.bin": "f5ef6485", + "webgpu/shader/execution/unary/i32_conversion.bin": "75435733", + "webgpu/shader/execution/unary/u32_conversion.bin": "26baf99", + "webgpu/shader/execution/unary/ai_assignment.bin": "d1b00a8", + "webgpu/shader/execution/binary/ai_arithmetic.bin": "dc777f4e", + "webgpu/shader/execution/unary/ai_arithmetic.bin": "272c5df2", + "webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "ab6db19f", + "webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "1de2ec75", + "webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "e665650a", + "webgpu/shader/execution/derivatives.bin": "899e4e4c" +}
\ No newline at end of file diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/abs.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/abs.bin Binary files differnew file mode 100644 index 0000000000..4cba9b72df --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/abs.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/acos.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/acos.bin Binary files differnew file mode 100644 index 0000000000..2ecaaa389a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/acos.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/acosh.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/acosh.bin Binary files differnew file mode 100644 index 0000000000..d48659f3c3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/acosh.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/asin.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/asin.bin Binary files differnew file mode 100644 index 0000000000..b199953eaf --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/asin.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/asinh.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/asinh.bin Binary files differnew file mode 100644 index 0000000000..b370c53b01 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/asinh.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atan.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atan.bin Binary files differnew file mode 100644 index 0000000000..6ab0ba106a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atan.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atan2.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atan2.bin Binary files differnew file mode 100644 index 0000000000..0109ddbc37 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atan2.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atanh.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atanh.bin Binary files differnew file mode 100644 index 0000000000..e6a190b35d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/atanh.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_addition.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_addition.bin Binary files differnew file mode 100644 index 0000000000..ebd757c1b6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_addition.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_division.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_division.bin Binary files differnew file mode 100644 index 0000000000..656356d32e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_division.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_logical.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_logical.bin Binary files differnew file mode 100644 index 0000000000..e3594458f2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_logical.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_addition.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_addition.bin Binary files differnew file mode 100644 index 0000000000..ba9d123cbf --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_addition.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin Binary files differnew file mode 100644 index 0000000000..58d0d40cb9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin Binary files differnew file mode 100644 index 0000000000..c8a3b7205a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_subtraction.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_subtraction.bin Binary files differnew file mode 100644 index 0000000000..8f88d196ce --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_subtraction.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin Binary files differnew file mode 100644 index 0000000000..545a5112cf --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_multiplication.bin Binary files differnew file mode 100644 index 0000000000..552d8b4892 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_remainder.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_remainder.bin Binary files differnew file mode 100644 index 0000000000..c45792abf4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_remainder.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_subtraction.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_subtraction.bin Binary files differnew file mode 100644 index 0000000000..6f0be29785 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/af_subtraction.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/ai_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/ai_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..658eb46d39 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/ai_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_addition.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_addition.bin Binary files differnew file mode 100644 index 0000000000..30f099139d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_addition.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_division.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_division.bin Binary files differnew file mode 100644 index 0000000000..22e60bef81 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_division.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_logical.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_logical.bin Binary files differnew file mode 100644 index 0000000000..932af58208 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_logical.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_addition.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_addition.bin Binary files differnew file mode 100644 index 0000000000..452376760b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_addition.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin Binary files differnew file mode 100644 index 0000000000..e823daac6c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin Binary files differnew file mode 100644 index 0000000000..b48be81ebd --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_subtraction.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_subtraction.bin Binary files differnew file mode 100644 index 0000000000..386558c3f7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_subtraction.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin Binary files differnew file mode 100644 index 0000000000..cbf224a6b6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_multiplication.bin Binary files differnew file mode 100644 index 0000000000..e9d27019a3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_remainder.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_remainder.bin Binary files differnew file mode 100644 index 0000000000..d21370aec9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_remainder.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_subtraction.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_subtraction.bin Binary files differnew file mode 100644 index 0000000000..97080a8ce5 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f16_subtraction.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_addition.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_addition.bin Binary files differnew file mode 100644 index 0000000000..be7b997bd8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_addition.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_division.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_division.bin Binary files differnew file mode 100644 index 0000000000..f80461c21b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_division.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_logical.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_logical.bin Binary files differnew file mode 100644 index 0000000000..a819eab08c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_logical.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_addition.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_addition.bin Binary files differnew file mode 100644 index 0000000000..b1d7179264 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_addition.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin Binary files differnew file mode 100644 index 0000000000..232760828a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin Binary files differnew file mode 100644 index 0000000000..76f867b959 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_subtraction.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_subtraction.bin Binary files differnew file mode 100644 index 0000000000..0d0fd2460d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_subtraction.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin Binary files differnew file mode 100644 index 0000000000..e139fc9713 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_multiplication.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_multiplication.bin Binary files differnew file mode 100644 index 0000000000..1837ce922c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_multiplication.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_remainder.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_remainder.bin Binary files differnew file mode 100644 index 0000000000..3febfca1d1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_remainder.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_subtraction.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_subtraction.bin Binary files differnew file mode 100644 index 0000000000..32b34f690c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/f32_subtraction.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/i32_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/i32_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..bacd4c0c54 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/i32_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/i32_comparison.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/i32_comparison.bin Binary files differnew file mode 100644 index 0000000000..d5a745e85c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/i32_comparison.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/u32_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/u32_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..56ef292864 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/u32_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/u32_comparison.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/u32_comparison.bin Binary files differnew file mode 100644 index 0000000000..5ba639b3cd --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/binary/u32_comparison.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/bitcast.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/bitcast.bin Binary files differnew file mode 100644 index 0000000000..2acc4a318b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/bitcast.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/ceil.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/ceil.bin Binary files differnew file mode 100644 index 0000000000..9b93ed416f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/ceil.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/clamp.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/clamp.bin Binary files differnew file mode 100644 index 0000000000..492be017aa --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/clamp.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cos.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cos.bin Binary files differnew file mode 100644 index 0000000000..4e34eff3f1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cos.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cosh.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cosh.bin Binary files differnew file mode 100644 index 0000000000..5b30d2786c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cosh.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cross.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cross.bin Binary files differnew file mode 100644 index 0000000000..c8ee9d3e1a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/cross.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/degrees.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/degrees.bin Binary files differnew file mode 100644 index 0000000000..662558d78a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/degrees.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/derivatives.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/derivatives.bin Binary files differnew file mode 100644 index 0000000000..d6d0788775 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/derivatives.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/determinant.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/determinant.bin Binary files differnew file mode 100644 index 0000000000..16d58c6db6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/determinant.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/distance.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/distance.bin Binary files differnew file mode 100644 index 0000000000..23a4756a69 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/distance.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/dot.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/dot.bin Binary files differnew file mode 100644 index 0000000000..13622a686b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/dot.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/exp.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/exp.bin Binary files differnew file mode 100644 index 0000000000..29361a2b27 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/exp.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/exp2.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/exp2.bin Binary files differnew file mode 100644 index 0000000000..367b5a8e90 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/exp2.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/faceForward.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/faceForward.bin Binary files differnew file mode 100644 index 0000000000..8f065bb97c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/faceForward.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/floor.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/floor.bin Binary files differnew file mode 100644 index 0000000000..b5341907f8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/floor.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/fma.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/fma.bin Binary files differnew file mode 100644 index 0000000000..eb4cb9ebbe --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/fma.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/fract.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/fract.bin Binary files differnew file mode 100644 index 0000000000..f889961d8f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/fract.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/frexp.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/frexp.bin Binary files differnew file mode 100644 index 0000000000..6811dfa295 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/frexp.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/inverseSqrt.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/inverseSqrt.bin Binary files differnew file mode 100644 index 0000000000..5039345ad0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/inverseSqrt.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/ldexp.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/ldexp.bin Binary files differnew file mode 100644 index 0000000000..bab78ed2af --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/ldexp.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/length.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/length.bin Binary files differnew file mode 100644 index 0000000000..3644d9b683 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/length.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/log.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/log.bin Binary files differnew file mode 100644 index 0000000000..ba591faad8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/log.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/log2.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/log2.bin Binary files differnew file mode 100644 index 0000000000..00641ce119 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/log2.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/max.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/max.bin Binary files differnew file mode 100644 index 0000000000..3861a94aca --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/max.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/min.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/min.bin Binary files differnew file mode 100644 index 0000000000..21c29e62ed --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/min.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/mix.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/mix.bin Binary files differnew file mode 100644 index 0000000000..c42b2aa067 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/mix.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/modf.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/modf.bin Binary files differnew file mode 100644 index 0000000000..363cc161fd --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/modf.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/normalize.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/normalize.bin Binary files differnew file mode 100644 index 0000000000..01b8eab700 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/normalize.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/pack2x16float.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/pack2x16float.bin Binary files differnew file mode 100644 index 0000000000..e95227d36e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/pack2x16float.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/pow.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/pow.bin Binary files differnew file mode 100644 index 0000000000..4f5faf3293 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/pow.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/quantizeToF16.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/quantizeToF16.bin Binary files differnew file mode 100644 index 0000000000..9e4308d5cd --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/quantizeToF16.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/radians.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/radians.bin Binary files differnew file mode 100644 index 0000000000..f5285d1087 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/radians.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/reflect.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/reflect.bin Binary files differnew file mode 100644 index 0000000000..30cd7ee925 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/reflect.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/refract.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/refract.bin Binary files differnew file mode 100644 index 0000000000..c428285817 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/refract.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/round.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/round.bin Binary files differnew file mode 100644 index 0000000000..c3b30b68f0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/round.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/saturate.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/saturate.bin Binary files differnew file mode 100644 index 0000000000..2e1eb821a9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/saturate.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sign.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sign.bin Binary files differnew file mode 100644 index 0000000000..033f2e8158 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sign.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sin.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sin.bin Binary files differnew file mode 100644 index 0000000000..a2ca632008 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sin.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sinh.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sinh.bin Binary files differnew file mode 100644 index 0000000000..1176cd472b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sinh.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/smoothstep.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/smoothstep.bin Binary files differnew file mode 100644 index 0000000000..73b65d17c2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/smoothstep.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sqrt.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sqrt.bin Binary files differnew file mode 100644 index 0000000000..6dd8088c08 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/sqrt.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/step.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/step.bin Binary files differnew file mode 100644 index 0000000000..f6c6c7b5f3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/step.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/tan.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/tan.bin Binary files differnew file mode 100644 index 0000000000..572bee4df2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/tan.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/tanh.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/tanh.bin Binary files differnew file mode 100644 index 0000000000..a13028b165 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/tanh.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/transpose.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/transpose.bin Binary files differnew file mode 100644 index 0000000000..d1b6bf04ee --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/transpose.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/trunc.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/trunc.bin Binary files differnew file mode 100644 index 0000000000..ba81e2ada4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/trunc.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/af_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/af_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..21d3d702ae --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/af_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/af_assignment.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/af_assignment.bin Binary files differnew file mode 100644 index 0000000000..a92279b5ce --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/af_assignment.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/ai_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/ai_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..2fa273ff19 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/ai_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/ai_assignment.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/ai_assignment.bin Binary files differnew file mode 100644 index 0000000000..7956b3652a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/ai_assignment.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/bool_conversion.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/bool_conversion.bin Binary files differnew file mode 100644 index 0000000000..98a90ea45b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/bool_conversion.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f16_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f16_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..acf8a702ce --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f16_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f16_conversion.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f16_conversion.bin Binary files differnew file mode 100644 index 0000000000..14299da766 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f16_conversion.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f32_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f32_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..ebc60029fa --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f32_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f32_conversion.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f32_conversion.bin Binary files differnew file mode 100644 index 0000000000..bdcc0c7298 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/f32_conversion.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/i32_arithmetic.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/i32_arithmetic.bin Binary files differnew file mode 100644 index 0000000000..4753b020c9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/i32_arithmetic.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/i32_conversion.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/i32_conversion.bin Binary files differnew file mode 100644 index 0000000000..04841df607 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/i32_conversion.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/u32_conversion.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/u32_conversion.bin Binary files differnew file mode 100644 index 0000000000..277ffc4d76 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unary/u32_conversion.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16float.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16float.bin Binary files differnew file mode 100644 index 0000000000..7f06cb0df6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16float.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16snorm.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16snorm.bin Binary files differnew file mode 100644 index 0000000000..08c6af9e93 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16snorm.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16unorm.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16unorm.bin Binary files differnew file mode 100644 index 0000000000..1bb97b9c55 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack2x16unorm.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack4x8snorm.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack4x8snorm.bin Binary files differnew file mode 100644 index 0000000000..1db9856b05 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack4x8snorm.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack4x8unorm.bin b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack4x8unorm.bin Binary files differnew file mode 100644 index 0000000000..8d1f3dc7fb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/cache/webgpu/shader/execution/unpack4x8unorm.bin diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-hflip.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-hflip.mp4 Binary files differnew file mode 100644 index 0000000000..f83b4f9698 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-hflip.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-180.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-180.mp4 Binary files differindex 1f0e9094a5..6665ea900d 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-180.mp4 +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-180.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-270.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-270.mp4 Binary files differindex e0480ceff2..b1e32bc83a 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-270.mp4 +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-270.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-90.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-90.mp4 Binary files differindex 9a6261056e..66a98d0ed0 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-90.mp4 +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-rotate-90.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-vflip.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-vflip.mp4 Binary files differnew file mode 100644 index 0000000000..90c3297a9a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601-vflip.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601.mp4 Binary files differindex 81a5ade435..5317bbf7c6 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601.mp4 +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-h264-bt601.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-theora-bt601.ogv b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-theora-bt601.ogv Binary files differdeleted file mode 100644 index 79ed41163c..0000000000 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-theora-bt601.ogv +++ /dev/null diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp8-bt601.webm b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp8-bt601.webm Binary files differindex 20a2178596..d1504ee332 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp8-bt601.webm +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp8-bt601.webm diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-hflip.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-hflip.mp4 Binary files differnew file mode 100644 index 0000000000..f782c32651 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-hflip.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-180.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-180.mp4 Binary files differnew file mode 100644 index 0000000000..fc712becd7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-180.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-270.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-270.mp4 Binary files differnew file mode 100644 index 0000000000..a83558f53c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-270.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-90.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-90.mp4 Binary files differnew file mode 100644 index 0000000000..73a03795ba --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-rotate-90.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-vflip.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-vflip.mp4 Binary files differnew file mode 100644 index 0000000000..c9de14696a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601-vflip.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601.mp4 Binary files differnew file mode 100644 index 0000000000..0d8d4f829c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601.mp4 diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601.webm b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601.webm Binary files differindex a4044a9209..47a43a0695 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601.webm +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt601.webm diff --git a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt709.webm b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt709.webm Binary files differindex 189e422035..a9e069ee1c 100644 --- a/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt709.webm +++ b/dom/webgpu/tests/cts/checkout/src/resources/four-colors-vp9-bt709.webm diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts index 8606aa8717..e144f39288 100644 --- a/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts @@ -18,7 +18,7 @@ import { i32, kFloat16Format, kFloat32Format, - Matrix, + MatrixValue, numbersApproximatelyEqual, pack2x16float, pack2x16snorm, @@ -26,14 +26,14 @@ import { pack4x8snorm, pack4x8unorm, packRGB9E5UFloat, - Scalar, + ScalarValue, toMatrix, u32, unpackRGB9E5UFloat, vec2, vec3, vec4, - Vector, + VectorValue, } from '../webgpu/util/conversion.js'; import { UnitTest } from './unit_test.js'; @@ -191,7 +191,7 @@ g.test('floatBitsToULPFromZero,32').fn(t => { }); g.test('scalarWGSL').fn(t => { - const cases: Array<[Scalar, string]> = [ + const cases: Array<[ScalarValue, string]> = [ [f32(0.0), '0.0f'], // The number -0.0 can be remapped to 0.0 when stored in a Scalar // object. It is not possible to guarantee that '-0.0f' will @@ -227,7 +227,7 @@ expect: ${expect}` }); g.test('vectorWGSL').fn(t => { - const cases: Array<[Vector, string]> = [ + const cases: Array<[VectorValue, string]> = [ [vec2(f32(42.0), f32(24.0)), 'vec2(42.0f, 24.0f)'], [vec2(f16Bits(0x5140), f16Bits(0x4e00)), 'vec2(42.0h, 24.0h)'], [vec2(u32(42), u32(24)), 'vec2(42u, 24u)'], @@ -261,7 +261,7 @@ expect: ${expect}` }); g.test('matrixWGSL').fn(t => { - const cases: Array<[Matrix, string]> = [ + const cases: Array<[MatrixValue, string]> = [ [ toMatrix( [ @@ -391,7 +391,7 @@ g.test('constructorMatrix') return [...Array(rows).keys()].map(r => scalar_builder(c * cols + r)); }); - const got = new Matrix(elements); + const got = new MatrixValue(elements); const got_type = got.type; t.expect( got_type.cols === cols, diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/crc32.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/crc32.spec.ts new file mode 100644 index 0000000000..5986823c8a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/crc32.spec.ts @@ -0,0 +1,28 @@ +export const description = ` +Test for crc32 utility functions. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { crc32, toHexString } from '../common/util/crc32.js'; + +import { UnitTest } from './unit_test.js'; + +class F extends UnitTest { + test(content: string, expect: string): void { + const got = toHexString(crc32(content)); + this.expect( + expect === got, + ` +expected: ${expect} +got: ${got}` + ); + } +} + +export const g = makeTestGroup(F); + +g.test('strings').fn(t => { + t.test('', '00000000'); + t.test('hello world', '0d4a1185'); + t.test('123456789', 'cbf43926'); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts index e8f8525d7f..31501f77ff 100644 --- a/dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts @@ -5,7 +5,12 @@ Floating Point unit tests. import { makeTestGroup } from '../common/framework/test_group.js'; import { objectEquals, unreachable } from '../common/util/util.js'; import { kValue } from '../webgpu/util/constants.js'; -import { FP, FPInterval, FPIntervalParam, IntervalBounds } from '../webgpu/util/floating_point.js'; +import { + FP, + FPInterval, + FPIntervalParam, + IntervalEndpoints, +} from '../webgpu/util/floating_point.js'; import { map2DArray, oneULPF32, oneULPF16, oneULPF64 } from '../webgpu/util/math.js'; import { reinterpretU16AsF16, @@ -17,24 +22,19 @@ import { UnitTest } from './unit_test.js'; export const g = makeTestGroup(UnitTest); -/** - * For ULP purposes, abstract float behaves like f32, so need to swizzle it in - * for expectations. - */ const kFPTraitForULP = { - abstract: 'f32', f32: 'f32', f16: 'f16', } as const; -/** Bounds indicating an expectation of unbounded error */ -const kUnboundedBounds: IntervalBounds = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]; +/** Endpoints indicating an expectation of unbounded error */ +const kUnboundedEndpoints: IntervalEndpoints = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]; -/** Interval from kUnboundedBounds */ +/** Interval from kUnboundedEndpoints */ const kUnboundedInterval = { - f32: FP.f32.toParam(kUnboundedBounds), - f16: FP.f16.toParam(kUnboundedBounds), - abstract: FP.abstract.toParam(kUnboundedBounds), + f32: FP.f32.toParam(kUnboundedEndpoints), + f16: FP.f16.toParam(kUnboundedEndpoints), + abstract: FP.abstract.toParam(kUnboundedEndpoints), }; /** @returns a number N * ULP greater than the provided number */ @@ -89,17 +89,17 @@ const kMinusOneULPFunctions = { }, }; -/** @returns the expected IntervalBounds adjusted by the given error function +/** @returns the expected IntervalEndpoints adjusted by the given error function * - * @param expected the bounds to be adjusted - * @param error error function to adjust the bounds via + * @param expected the endpoints to be adjusted + * @param error error function to adjust the endpoints via */ function applyError( - expected: number | IntervalBounds, + expected: number | IntervalEndpoints, error: (n: number) => number -): IntervalBounds { +): IntervalEndpoints { // Avoiding going through FPInterval to avoid tying this to a specific kind - const unpack = (n: number | IntervalBounds): [number, number] => { + const unpack = (n: number | IntervalEndpoints): [number, number] => { if (expected instanceof Array) { switch (expected.length) { case 1: @@ -107,7 +107,7 @@ function applyError( case 2: return [expected[0], expected[1]]; } - unreachable(`Tried to unpack an IntervalBounds with length other than 1 or 2`); + unreachable(`Tried to unpack an IntervalEndpoints with length other than 1 or 2`); } else { // TS doesn't narrow this to number automatically return [n as number, n as number]; @@ -128,8 +128,8 @@ function applyError( // FPInterval interface ConstructorCase { - input: IntervalBounds; - expected: IntervalBounds; + input: IntervalEndpoints; + expected: IntervalEndpoints; } g.test('constructor') @@ -160,7 +160,7 @@ g.test('constructor') // Infinities { input: [0, constants.positive.infinity], expected: [0, Number.POSITIVE_INFINITY] }, { input: [constants.negative.infinity, 0], expected: [Number.NEGATIVE_INFINITY, 0] }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, ]; // Note: Out of range values are limited to infinities for abstract float, due to abstract @@ -182,13 +182,13 @@ g.test('constructor') .fn(t => { const i = new FPInterval(t.params.trait, ...t.params.input); t.expect( - objectEquals(i.bounds(), t.params.expected), + objectEquals(i.endpoints(), t.params.expected), `new FPInterval('${t.params.trait}', [${t.params.input}]) returned ${i}. Expected [${t.params.expected}]` ); }); interface ContainsNumberCase { - bounds: number | IntervalBounds; + endpoints: number | IntervalEndpoints; value: number; expected: boolean; } @@ -203,90 +203,90 @@ g.test('contains_number') // prettier-ignore const cases: ContainsNumberCase[] = [ // Common usage - { bounds: [0, 10], value: 0, expected: true }, - { bounds: [0, 10], value: 10, expected: true }, - { bounds: [0, 10], value: 5, expected: true }, - { bounds: [0, 10], value: -5, expected: false }, - { bounds: [0, 10], value: 50, expected: false }, - { bounds: [0, 10], value: Number.NaN, expected: false }, - { bounds: [-5, 10], value: 0, expected: true }, - { bounds: [-5, 10], value: 10, expected: true }, - { bounds: [-5, 10], value: 5, expected: true }, - { bounds: [-5, 10], value: -5, expected: true }, - { bounds: [-5, 10], value: -6, expected: false }, - { bounds: [-5, 10], value: 50, expected: false }, - { bounds: [-5, 10], value: -10, expected: false }, - { bounds: [-1.375, 2.5], value: -10, expected: false }, - { bounds: [-1.375, 2.5], value: 0.5, expected: true }, - { bounds: [-1.375, 2.5], value: 10, expected: false }, + { endpoints: [0, 10], value: 0, expected: true }, + { endpoints: [0, 10], value: 10, expected: true }, + { endpoints: [0, 10], value: 5, expected: true }, + { endpoints: [0, 10], value: -5, expected: false }, + { endpoints: [0, 10], value: 50, expected: false }, + { endpoints: [0, 10], value: Number.NaN, expected: false }, + { endpoints: [-5, 10], value: 0, expected: true }, + { endpoints: [-5, 10], value: 10, expected: true }, + { endpoints: [-5, 10], value: 5, expected: true }, + { endpoints: [-5, 10], value: -5, expected: true }, + { endpoints: [-5, 10], value: -6, expected: false }, + { endpoints: [-5, 10], value: 50, expected: false }, + { endpoints: [-5, 10], value: -10, expected: false }, + { endpoints: [-1.375, 2.5], value: -10, expected: false }, + { endpoints: [-1.375, 2.5], value: 0.5, expected: true }, + { endpoints: [-1.375, 2.5], value: 10, expected: false }, // Point - { bounds: 0, value: 0, expected: true }, - { bounds: 0, value: 10, expected: false }, - { bounds: 0, value: -1000, expected: false }, - { bounds: 10, value: 10, expected: true }, - { bounds: 10, value: 0, expected: false }, - { bounds: 10, value: -10, expected: false }, - { bounds: 10, value: 11, expected: false }, + { endpoints: 0, value: 0, expected: true }, + { endpoints: 0, value: 10, expected: false }, + { endpoints: 0, value: -1000, expected: false }, + { endpoints: 10, value: 10, expected: true }, + { endpoints: 10, value: 0, expected: false }, + { endpoints: 10, value: -10, expected: false }, + { endpoints: 10, value: 11, expected: false }, // Upper infinity - { bounds: [0, constants.positive.infinity], value: constants.positive.min, expected: true }, - { bounds: [0, constants.positive.infinity], value: constants.positive.max, expected: true }, - { bounds: [0, constants.positive.infinity], value: constants.positive.infinity, expected: true }, - { bounds: [0, constants.positive.infinity], value: constants.negative.min, expected: false }, - { bounds: [0, constants.positive.infinity], value: constants.negative.max, expected: false }, - { bounds: [0, constants.positive.infinity], value: constants.negative.infinity, expected: false }, + { endpoints: [0, constants.positive.infinity], value: constants.positive.min, expected: true }, + { endpoints: [0, constants.positive.infinity], value: constants.positive.max, expected: true }, + { endpoints: [0, constants.positive.infinity], value: constants.positive.infinity, expected: true }, + { endpoints: [0, constants.positive.infinity], value: constants.negative.min, expected: false }, + { endpoints: [0, constants.positive.infinity], value: constants.negative.max, expected: false }, + { endpoints: [0, constants.positive.infinity], value: constants.negative.infinity, expected: false }, // Lower infinity - { bounds: [constants.negative.infinity, 0], value: constants.positive.min, expected: false }, - { bounds: [constants.negative.infinity, 0], value: constants.positive.max, expected: false }, - { bounds: [constants.negative.infinity, 0], value: constants.positive.infinity, expected: false }, - { bounds: [constants.negative.infinity, 0], value: constants.negative.min, expected: true }, - { bounds: [constants.negative.infinity, 0], value: constants.negative.max, expected: true }, - { bounds: [constants.negative.infinity, 0], value: constants.negative.infinity, expected: true }, + { endpoints: [constants.negative.infinity, 0], value: constants.positive.min, expected: false }, + { endpoints: [constants.negative.infinity, 0], value: constants.positive.max, expected: false }, + { endpoints: [constants.negative.infinity, 0], value: constants.positive.infinity, expected: false }, + { endpoints: [constants.negative.infinity, 0], value: constants.negative.min, expected: true }, + { endpoints: [constants.negative.infinity, 0], value: constants.negative.max, expected: true }, + { endpoints: [constants.negative.infinity, 0], value: constants.negative.infinity, expected: true }, // Full infinity - { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.min, expected: true }, - { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.max, expected: true }, - { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.infinity, expected: true }, - { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.min, expected: true }, - { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.max, expected: true }, - { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.infinity, expected: true }, - { bounds: [constants.negative.infinity, constants.positive.infinity], value: Number.NaN, expected: true }, + { endpoints: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.min, expected: true }, + { endpoints: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.max, expected: true }, + { endpoints: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.infinity, expected: true }, + { endpoints: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.min, expected: true }, + { endpoints: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.max, expected: true }, + { endpoints: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.infinity, expected: true }, + { endpoints: [constants.negative.infinity, constants.positive.infinity], value: Number.NaN, expected: true }, // Maximum f32 boundary - { bounds: [0, constants.positive.max], value: constants.positive.min, expected: true }, - { bounds: [0, constants.positive.max], value: constants.positive.max, expected: true }, - { bounds: [0, constants.positive.max], value: constants.positive.infinity, expected: false }, - { bounds: [0, constants.positive.max], value: constants.negative.min, expected: false }, - { bounds: [0, constants.positive.max], value: constants.negative.max, expected: false }, - { bounds: [0, constants.positive.max], value: constants.negative.infinity, expected: false }, + { endpoints: [0, constants.positive.max], value: constants.positive.min, expected: true }, + { endpoints: [0, constants.positive.max], value: constants.positive.max, expected: true }, + { endpoints: [0, constants.positive.max], value: constants.positive.infinity, expected: false }, + { endpoints: [0, constants.positive.max], value: constants.negative.min, expected: false }, + { endpoints: [0, constants.positive.max], value: constants.negative.max, expected: false }, + { endpoints: [0, constants.positive.max], value: constants.negative.infinity, expected: false }, // Minimum f32 boundary - { bounds: [constants.negative.min, 0], value: constants.positive.min, expected: false }, - { bounds: [constants.negative.min, 0], value: constants.positive.max, expected: false }, - { bounds: [constants.negative.min, 0], value: constants.positive.infinity, expected: false }, - { bounds: [constants.negative.min, 0], value: constants.negative.min, expected: true }, - { bounds: [constants.negative.min, 0], value: constants.negative.max, expected: true }, - { bounds: [constants.negative.min, 0], value: constants.negative.infinity, expected: false }, + { endpoints: [constants.negative.min, 0], value: constants.positive.min, expected: false }, + { endpoints: [constants.negative.min, 0], value: constants.positive.max, expected: false }, + { endpoints: [constants.negative.min, 0], value: constants.positive.infinity, expected: false }, + { endpoints: [constants.negative.min, 0], value: constants.negative.min, expected: true }, + { endpoints: [constants.negative.min, 0], value: constants.negative.max, expected: true }, + { endpoints: [constants.negative.min, 0], value: constants.negative.infinity, expected: false }, // Subnormals - { bounds: [0, constants.positive.min], value: constants.positive.subnormal.min, expected: true }, - { bounds: [0, constants.positive.min], value: constants.positive.subnormal.max, expected: true }, - { bounds: [0, constants.positive.min], value: constants.negative.subnormal.min, expected: false }, - { bounds: [0, constants.positive.min], value: constants.negative.subnormal.max, expected: false }, - { bounds: [constants.negative.max, 0], value: constants.positive.subnormal.min, expected: false }, - { bounds: [constants.negative.max, 0], value: constants.positive.subnormal.max, expected: false }, - { bounds: [constants.negative.max, 0], value: constants.negative.subnormal.min, expected: true }, - { bounds: [constants.negative.max, 0], value: constants.negative.subnormal.max, expected: true }, - { bounds: [0, constants.positive.subnormal.min], value: constants.positive.subnormal.min, expected: true }, - { bounds: [0, constants.positive.subnormal.min], value: constants.positive.subnormal.max, expected: false }, - { bounds: [0, constants.positive.subnormal.min], value: constants.negative.subnormal.min, expected: false }, - { bounds: [0, constants.positive.subnormal.min], value: constants.negative.subnormal.max, expected: false }, - { bounds: [constants.negative.subnormal.max, 0], value: constants.positive.subnormal.min, expected: false }, - { bounds: [constants.negative.subnormal.max, 0], value: constants.positive.subnormal.max, expected: false }, - { bounds: [constants.negative.subnormal.max, 0], value: constants.negative.subnormal.min, expected: false }, - { bounds: [constants.negative.subnormal.max, 0], value: constants.negative.subnormal.max, expected: true }, + { endpoints: [0, constants.positive.min], value: constants.positive.subnormal.min, expected: true }, + { endpoints: [0, constants.positive.min], value: constants.positive.subnormal.max, expected: true }, + { endpoints: [0, constants.positive.min], value: constants.negative.subnormal.min, expected: false }, + { endpoints: [0, constants.positive.min], value: constants.negative.subnormal.max, expected: false }, + { endpoints: [constants.negative.max, 0], value: constants.positive.subnormal.min, expected: false }, + { endpoints: [constants.negative.max, 0], value: constants.positive.subnormal.max, expected: false }, + { endpoints: [constants.negative.max, 0], value: constants.negative.subnormal.min, expected: true }, + { endpoints: [constants.negative.max, 0], value: constants.negative.subnormal.max, expected: true }, + { endpoints: [0, constants.positive.subnormal.min], value: constants.positive.subnormal.min, expected: true }, + { endpoints: [0, constants.positive.subnormal.min], value: constants.positive.subnormal.max, expected: false }, + { endpoints: [0, constants.positive.subnormal.min], value: constants.negative.subnormal.min, expected: false }, + { endpoints: [0, constants.positive.subnormal.min], value: constants.negative.subnormal.max, expected: false }, + { endpoints: [constants.negative.subnormal.max, 0], value: constants.positive.subnormal.min, expected: false }, + { endpoints: [constants.negative.subnormal.max, 0], value: constants.positive.subnormal.max, expected: false }, + { endpoints: [constants.negative.subnormal.max, 0], value: constants.negative.subnormal.min, expected: false }, + { endpoints: [constants.negative.subnormal.max, 0], value: constants.negative.subnormal.max, expected: true }, ]; // Note: Out of range values are limited to infinities for abstract float, due to abstract @@ -296,20 +296,20 @@ g.test('contains_number') // prettier-ignore cases.push(...[ // Out of range high - { bounds: [0, 2 * constants.positive.max], value: constants.positive.min, expected: true }, - { bounds: [0, 2 * constants.positive.max], value: constants.positive.max, expected: true }, - { bounds: [0, 2 * constants.positive.max], value: constants.positive.infinity, expected: false }, - { bounds: [0, 2 * constants.positive.max], value: constants.negative.min, expected: false }, - { bounds: [0, 2 * constants.positive.max], value: constants.negative.max, expected: false }, - { bounds: [0, 2 * constants.positive.max], value: constants.negative.infinity, expected: false }, + { endpoints: [0, 2 * constants.positive.max], value: constants.positive.min, expected: true }, + { endpoints: [0, 2 * constants.positive.max], value: constants.positive.max, expected: true }, + { endpoints: [0, 2 * constants.positive.max], value: constants.positive.infinity, expected: false }, + { endpoints: [0, 2 * constants.positive.max], value: constants.negative.min, expected: false }, + { endpoints: [0, 2 * constants.positive.max], value: constants.negative.max, expected: false }, + { endpoints: [0, 2 * constants.positive.max], value: constants.negative.infinity, expected: false }, // Out of range low - { bounds: [2 * constants.negative.min, 0], value: constants.positive.min, expected: false }, - { bounds: [2 * constants.negative.min, 0], value: constants.positive.max, expected: false }, - { bounds: [2 * constants.negative.min, 0], value: constants.positive.infinity, expected: false }, - { bounds: [2 * constants.negative.min, 0], value: constants.negative.min, expected: true }, - { bounds: [2 * constants.negative.min, 0], value: constants.negative.max, expected: true }, - { bounds: [2 * constants.negative.min, 0], value: constants.negative.infinity, expected: false }, + { endpoints: [2 * constants.negative.min, 0], value: constants.positive.min, expected: false }, + { endpoints: [2 * constants.negative.min, 0], value: constants.positive.max, expected: false }, + { endpoints: [2 * constants.negative.min, 0], value: constants.positive.infinity, expected: false }, + { endpoints: [2 * constants.negative.min, 0], value: constants.negative.min, expected: true }, + { endpoints: [2 * constants.negative.min, 0], value: constants.negative.max, expected: true }, + { endpoints: [2 * constants.negative.min, 0], value: constants.negative.infinity, expected: false }, ] as ContainsNumberCase[]); } @@ -318,7 +318,7 @@ g.test('contains_number') ) .fn(t => { const trait = FP[t.params.trait]; - const i = trait.toInterval(t.params.bounds); + const i = trait.toInterval(t.params.endpoints); const value = t.params.value; const expected = t.params.expected; @@ -327,8 +327,8 @@ g.test('contains_number') }); interface ContainsIntervalCase { - lhs: number | IntervalBounds; - rhs: number | IntervalBounds; + lhs: number | IntervalEndpoints; + rhs: number | IntervalEndpoints; expected: boolean; } @@ -440,8 +440,8 @@ g.test('contains_interval') // Utilities interface SpanIntervalsCase { - intervals: (number | IntervalBounds)[]; - expected: number | IntervalBounds; + intervals: (number | IntervalEndpoints)[]; + expected: number | IntervalEndpoints; } g.test('spanIntervals') @@ -467,7 +467,7 @@ g.test('spanIntervals') { intervals: [[2, 5], [0, 1]], expected: [0, 5] }, { intervals: [[0, 2], [1, 5]], expected: [0, 5] }, { intervals: [[0, 5], [1, 2]], expected: [0, 5] }, - { intervals: [[constants.negative.infinity, 0], [0, constants.positive.infinity]], expected: kUnboundedBounds }, + { intervals: [[constants.negative.infinity, 0], [0, constants.positive.infinity]], expected: kUnboundedEndpoints }, // Multiple Intervals { intervals: [[0, 1], [2, 3], [4, 5]], expected: [0, 5] }, @@ -494,7 +494,7 @@ g.test('spanIntervals') }); interface isVectorCase { - input: (number | IntervalBounds | FPIntervalParam)[]; + input: (number | IntervalEndpoints | FPIntervalParam)[]; expected: boolean; } @@ -511,7 +511,7 @@ g.test('isVector') { input: [1, 2, 3], expected: false }, { input: [1, 2, 3, 4], expected: false }, - // IntervalBounds + // IntervalEndpoints { input: [[1], [2]], expected: false }, { input: [[1], [2], [3]], expected: false }, { input: [[1], [2], [3], [4]], expected: false }, @@ -600,8 +600,8 @@ g.test('isVector') }); interface toVectorCase { - input: (number | IntervalBounds | FPIntervalParam)[]; - expected: (number | IntervalBounds)[]; + input: (number | IntervalEndpoints | FPIntervalParam)[]; + expected: (number | IntervalEndpoints)[]; } g.test('toVector') @@ -617,7 +617,7 @@ g.test('toVector') { input: [1, 2, 3], expected: [1, 2, 3] }, { input: [1, 2, 3, 4], expected: [1, 2, 3, 4] }, - // IntervalBounds + // IntervalEndpoints { input: [[1], [2]], expected: [1, 2] }, { input: [[1], [2], [3]], expected: [1, 2, 3] }, { input: [[1], [2], [3], [4]], expected: [1, 2, 3, 4] }, @@ -704,7 +704,7 @@ g.test('toVector') { input: [1, trait.toParam([2]), [3], 4], expected: [1, 2, 3, 4] }, { input: [1, [2], [2, 3], kUnboundedInterval[p.trait]], - expected: [1, 2, [2, 3], kUnboundedBounds], + expected: [1, 2, [2, 3], kUnboundedEndpoints], }, ]; }) @@ -722,7 +722,7 @@ g.test('toVector') }); interface isMatrixCase { - input: (number | IntervalBounds | FPIntervalParam)[][]; + input: (number | IntervalEndpoints | FPIntervalParam)[][]; expected: boolean; } @@ -808,7 +808,7 @@ g.test('isMatrix') expected: false, }, - // IntervalBounds + // IntervalEndpoints { input: [ [[1], [2]], @@ -1157,8 +1157,8 @@ g.test('isMatrix') }); interface toMatrixCase { - input: (number | IntervalBounds | FPIntervalParam)[][]; - expected: (number | IntervalBounds)[][]; + input: (number | IntervalEndpoints | FPIntervalParam)[][]; + expected: (number | IntervalEndpoints)[][]; } g.test('toMatrix') @@ -1279,7 +1279,7 @@ g.test('toMatrix') ], }, - // IntervalBounds + // IntervalEndpoints { input: [ [[1], [2]], @@ -1822,7 +1822,7 @@ g.test('toMatrix') interface AbsoluteErrorCase { value: number; error: number; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } // Special values used for testing absolute error interval @@ -1856,23 +1856,24 @@ g.test('absoluteErrorInterval') const smallErr = kSmallAbsoluteErrorValue[p.trait]; const largeErr = kLargeAbsoluteErrorValue[p.trait]; const subnormalErr = kSubnormalAbsoluteErrorValue[p.trait]; + // prettier-ignore return [ // Edge Cases - // 1. Interval around infinity would be kUnboundedBounds - { value: constants.positive.infinity, error: 0, expected: kUnboundedBounds }, - { value: constants.positive.infinity, error: largeErr, expected: kUnboundedBounds }, - { value: constants.positive.infinity, error: 1, expected: kUnboundedBounds }, - { value: constants.negative.infinity, error: 0, expected: kUnboundedBounds }, - { value: constants.negative.infinity, error: largeErr, expected: kUnboundedBounds }, - { value: constants.negative.infinity, error: 1, expected: kUnboundedBounds }, + // 1. Interval around infinity would be kUnboundedEndpoints + { value: constants.positive.infinity, error: 0, expected: kUnboundedEndpoints }, + { value: constants.positive.infinity, error: largeErr, expected: kUnboundedEndpoints }, + { value: constants.positive.infinity, error: 1, expected: kUnboundedEndpoints }, + { value: constants.negative.infinity, error: 0, expected: kUnboundedEndpoints }, + { value: constants.negative.infinity, error: largeErr, expected: kUnboundedEndpoints }, + { value: constants.negative.infinity, error: 1, expected: kUnboundedEndpoints }, // 2. Interval around largest finite positive/negative { value: constants.positive.max, error: 0, expected: constants.positive.max }, - { value: constants.positive.max, error: largeErr, expected: kUnboundedBounds}, - { value: constants.positive.max, error: constants.positive.max, expected: kUnboundedBounds}, + { value: constants.positive.max, error: largeErr, expected: kUnboundedEndpoints}, + { value: constants.positive.max, error: constants.positive.max, expected: kUnboundedEndpoints}, { value: constants.negative.min, error: 0, expected: constants.negative.min }, - { value: constants.negative.min, error: largeErr, expected: kUnboundedBounds}, - { value: constants.negative.min, error: constants.positive.max, expected: kUnboundedBounds}, + { value: constants.negative.min, error: largeErr, expected: kUnboundedEndpoints}, + { value: constants.negative.min, error: constants.positive.max, expected: kUnboundedEndpoints}, // 3. Interval around small but normal center, center should not get flushed. { value: constants.positive.min, error: 0, expected: constants.positive.min }, { value: constants.positive.min, error: smallErr, expected: [constants.positive.min - smallErr, constants.positive.min + smallErr]}, @@ -1898,6 +1899,19 @@ g.test('absoluteErrorInterval') { value: constants.negative.subnormal.max, error: smallErr, expected: [constants.negative.subnormal.max - smallErr, smallErr] }, { value: constants.negative.subnormal.max, error: 1, expected: [constants.negative.subnormal.max - 1, 1] }, + // Zero + { value: 0, error: 0, expected: 0 }, + { value: 0, error: smallErr, expected: [-smallErr, smallErr] }, + { value: 0, error: 1, expected: [-1, 1] }, + + // Two + { value: 2, error: 0, expected: 2 }, + { value: 2, error: smallErr, expected: [2 - smallErr, 2 + smallErr] }, + { value: 2, error: 1, expected: [1, 3] }, + { value: -2, error: 0, expected: -2 }, + { value: -2, error: smallErr, expected: [-2 - smallErr, -2 + smallErr] }, + { value: -2, error: 1, expected: [-3, -1] }, + // 64-bit subnormals, expected to be treated as 0.0 or smallest subnormal of kind. { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), error: 0, expected: [0, constants.positive.subnormal.min] }, { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), error: subnormalErr, expected: [-subnormalErr, constants.positive.subnormal.min + subnormalErr] }, @@ -1912,19 +1926,6 @@ g.test('absoluteErrorInterval') { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), error: 0, expected: [constants.negative.subnormal.max, 0] }, { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), error: subnormalErr, expected: [constants.negative.subnormal.max - subnormalErr, subnormalErr] }, { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), error: 1, expected: [constants.negative.subnormal.max - 1, 1] }, - - // Zero - { value: 0, error: 0, expected: 0 }, - { value: 0, error: smallErr, expected: [-smallErr, smallErr] }, - { value: 0, error: 1, expected: [-1, 1] }, - - // Two - { value: 2, error: 0, expected: 2 }, - { value: 2, error: smallErr, expected: [2 - smallErr, 2 + smallErr] }, - { value: 2, error: 1, expected: [1, 3] }, - { value: -2, error: 0, expected: -2 }, - { value: -2, error: smallErr, expected: [-2 - smallErr, -2 + smallErr] }, - { value: -2, error: 1, expected: [-3, -1] }, ]; }) ) @@ -1942,7 +1943,7 @@ g.test('absoluteErrorInterval') interface CorrectlyRoundedCase { value: number; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } // Correctly rounded cases that input values are exactly representable normal values of target type @@ -2034,8 +2035,8 @@ g.test('correctlyRoundedInterval') // prettier-ignore return [ // Edge Cases - { value: constants.positive.infinity, expected: kUnboundedBounds }, - { value: constants.negative.infinity, expected: kUnboundedBounds }, + { value: constants.positive.infinity, expected: kUnboundedEndpoints }, + { value: constants.negative.infinity, expected: kUnboundedEndpoints }, { value: constants.positive.max, expected: constants.positive.max }, { value: constants.negative.min, expected: constants.negative.min }, { value: constants.positive.min, expected: constants.positive.min }, @@ -2074,7 +2075,7 @@ g.test('correctlyRoundedInterval') interface ULPCase { value: number; num_ulp: number; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } // Special values used for testing ULP error interval @@ -2086,7 +2087,7 @@ const kULPErrorValue = { g.test('ulpInterval') .params(u => u - .combine('trait', ['abstract', 'f32', 'f16'] as const) + .combine('trait', ['f32', 'f16'] as const) .beginSubcases() .expandWithParams<ULPCase>(p => { const trait = kFPTraitForULP[p.trait]; @@ -2099,21 +2100,21 @@ g.test('ulpInterval') // prettier-ignore return [ // Edge Cases - { value: constants.positive.infinity, num_ulp: 0, expected: kUnboundedBounds }, - { value: constants.positive.infinity, num_ulp: 1, expected: kUnboundedBounds }, - { value: constants.positive.infinity, num_ulp: ULPValue, expected: kUnboundedBounds }, - { value: constants.negative.infinity, num_ulp: 0, expected: kUnboundedBounds }, - { value: constants.negative.infinity, num_ulp: 1, expected: kUnboundedBounds }, - { value: constants.negative.infinity, num_ulp: ULPValue, expected: kUnboundedBounds }, + { value: constants.positive.infinity, num_ulp: 0, expected: kUnboundedEndpoints }, + { value: constants.positive.infinity, num_ulp: 1, expected: kUnboundedEndpoints }, + { value: constants.positive.infinity, num_ulp: ULPValue, expected: kUnboundedEndpoints }, + { value: constants.negative.infinity, num_ulp: 0, expected: kUnboundedEndpoints }, + { value: constants.negative.infinity, num_ulp: 1, expected: kUnboundedEndpoints }, + { value: constants.negative.infinity, num_ulp: ULPValue, expected: kUnboundedEndpoints }, { value: constants.positive.max, num_ulp: 0, expected: constants.positive.max }, - { value: constants.positive.max, num_ulp: 1, expected: kUnboundedBounds }, - { value: constants.positive.max, num_ulp: ULPValue, expected: kUnboundedBounds }, + { value: constants.positive.max, num_ulp: 1, expected: kUnboundedEndpoints }, + { value: constants.positive.max, num_ulp: ULPValue, expected: kUnboundedEndpoints }, { value: constants.positive.min, num_ulp: 0, expected: constants.positive.min }, { value: constants.positive.min, num_ulp: 1, expected: [0, plusOneULP(constants.positive.min)] }, { value: constants.positive.min, num_ulp: ULPValue, expected: [0, plusNULP(constants.positive.min, ULPValue)] }, { value: constants.negative.min, num_ulp: 0, expected: constants.negative.min }, - { value: constants.negative.min, num_ulp: 1, expected: kUnboundedBounds }, - { value: constants.negative.min, num_ulp: ULPValue, expected: kUnboundedBounds }, + { value: constants.negative.min, num_ulp: 1, expected: kUnboundedEndpoints }, + { value: constants.negative.min, num_ulp: ULPValue, expected: kUnboundedEndpoints }, { value: constants.negative.max, num_ulp: 0, expected: constants.negative.max }, { value: constants.negative.max, num_ulp: 1, expected: [minusOneULP(constants.negative.max), 0] }, { value: constants.negative.max, num_ulp: ULPValue, expected: [minusNULP(constants.negative.max, ULPValue), 0] }, @@ -2178,7 +2179,7 @@ const kConstantCorrectlyRoundedExpectation = { '1.9': [reinterpretU32AsF32(0x3ff33333), reinterpretU32AsF32(0x3ff33334)], // -1.9 falls between f32 0xBFF33334 and 0xBFF33333 '-1.9': [reinterpretU32AsF32(0xbff33334), reinterpretU32AsF32(0xbff33333)], - } as { [value in ConstantNumberFrequentlyUsedInCases]: IntervalBounds }, + } as { [value in ConstantNumberFrequentlyUsedInCases]: IntervalEndpoints }, f16: { // 0.1 falls between f16 0x2E66 and 0x2E67 '0.1': [reinterpretU16AsF16(0x2e66), reinterpretU16AsF16(0x2e67)], @@ -2188,7 +2189,7 @@ const kConstantCorrectlyRoundedExpectation = { '1.9': [reinterpretU16AsF16(0x3f99), reinterpretU16AsF16(0x3f9a)], // 1.9 falls between f16 0xBF9A and 0xBF99 '-1.9': [reinterpretU16AsF16(0xbf9a), reinterpretU16AsF16(0xbf99)], - } as { [value in ConstantNumberFrequentlyUsedInCases]: IntervalBounds }, + } as { [value in ConstantNumberFrequentlyUsedInCases]: IntervalEndpoints }, // Since abstract is actually f64 and JS number is also f64, the JS number value will map to // identical abstracty value without rounded. abstract: { @@ -2201,7 +2202,7 @@ const kConstantCorrectlyRoundedExpectation = { interface ScalarToIntervalCase { input: number; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } g.test('absInterval') @@ -2224,8 +2225,8 @@ g.test('absInterval') { input: -1.9, expected: kConstantCorrectlyRoundedExpectation[p.trait]['1.9']}, // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.positive.max, expected: constants.positive.max }, { input: constants.positive.min, expected: constants.positive.min }, { input: constants.negative.min, expected: constants.positive.max }, @@ -2290,17 +2291,18 @@ g.test('acosInterval') const constants = trait.constants(); // prettier-ignore return [ - // The acceptance interval @ x = -1 and 1 is kUnboundedBounds, because - // sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inverseqrt - // The acceptance interval @ x = 0 is kUnboundedBounds, because atan2 is not - // well-defined/implemented at 0. - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: -1, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, - { input: 1, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + // The acceptance interval @ x = -1 and 1 is kUnboundedEndpoints, + // because sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of + // inverseqrt. + // The acceptance interval @ x = 0 is kUnboundedEndpoints, because atan2 + // is not well-defined/implemented at 0. + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, + { input: 1, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, // Cases that bounded by absolute error and inherited from atan2(sqrt(1-x*x), x). Note that // even x is very close to 1.0 and the expected result is close to 0.0, the expected @@ -2346,13 +2348,13 @@ g.test('acoshAlternativeInterval') return [ ...kAcoshAlternativeIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: -1, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, - { input: 1, expected: kUnboundedBounds }, // 1/0 occurs in inverseSqrt in this formulation - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, + { input: 1, expected: kUnboundedEndpoints }, // 1/0 occurs in inverseSqrt in this formulation + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -2392,13 +2394,13 @@ g.test('acoshPrimaryInterval') return [ ...kAcoshPrimaryIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: -1, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, - { input: 1, expected: kUnboundedBounds }, // 1/0 occurs in inverseSqrt in this formulation - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, + { input: 1, expected: kUnboundedEndpoints }, // 1/0 occurs in inverseSqrt in this formulation + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -2434,28 +2436,29 @@ g.test('asinInterval') .expandWithParams<ScalarToIntervalCase>(p => { const trait = FP[p.trait]; const constants = trait.constants(); - const abs_error = p.trait === 'f32' ? 6.77e-5 : 3.91e-3; + const abs_error = p.trait === 'f32' ? 6.81e-5 : 3.91e-3; // prettier-ignore return [ - // The acceptance interval @ x = -1 and 1 is kUnboundedBounds, because - // sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inversqrt. - // The acceptance interval @ x = 0 is kUnboundedBounds, because atan2 is not - // well-defined/implemented at 0. - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: -1, expected: kUnboundedBounds }, - // Subnormal input may get flushed to 0, and result in kUnboundedBounds. - { input: constants.negative.subnormal.min, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, - { input: constants.positive.subnormal.max, expected: kUnboundedBounds }, - { input: 1, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + // The acceptance interval @ x = -1 and 1 is kUnboundedEndpoints, + // because sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of + // inversqrt. + // The acceptance interval @ x = 0 is kUnboundedEndpoints, because + // atan2 is not well-defined/implemented at 0. + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: -1, expected: kUnboundedEndpoints }, + // Subnormal input may get flushed to 0, and result in kUnboundedEndpoints. + { input: constants.negative.subnormal.min, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, + { input: constants.positive.subnormal.max, expected: kUnboundedEndpoints }, + { input: 1, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, // When input near 0, the expected result is bounded by absolute error rather than ULP // error. Away from 0 the atan2 inherited error should be larger. - { input: constants.negative.max, expected: trait.absoluteErrorInterval(Math.asin(constants.negative.max), abs_error).bounds() }, // ~0 - { input: constants.positive.min, expected: trait.absoluteErrorInterval(Math.asin(constants.positive.min), abs_error).bounds() }, // ~0 + { input: constants.negative.max, expected: trait.absoluteErrorInterval(Math.asin(constants.negative.max), abs_error).endpoints() }, // ~0 + { input: constants.positive.min, expected: trait.absoluteErrorInterval(Math.asin(constants.positive.min), abs_error).endpoints() }, // ~0 // Cases that inherited from atan2(x, sqrt(1-x*x)) ...kAsinIntervalInheritedCases[p.trait], @@ -2500,10 +2503,10 @@ g.test('asinhInterval') return [ ...kAsinhIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -2569,8 +2572,8 @@ g.test('atanInterval') { input: 0, expected: 0 }, ...kAtanIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -2619,12 +2622,12 @@ g.test('atanhInterval') return [ ...kAtanhIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: -1, expected: kUnboundedBounds }, - { input: 1, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 1, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -2650,12 +2653,17 @@ const kCeilIntervalCases = { { input: -(2 ** 14), expected: -(2 ** 14) }, { input: 0x8000, expected: 0x8000 }, // https://github.com/gpuweb/cts/issues/2766 ], + abstract: [ + { input: 2 ** 52, expected: 2 ** 52 }, + { input: -(2 ** 52), expected: -(2 ** 52) }, + { input: 0x8000000000000000, expected: 0x8000000000000000 }, // https://github.com/gpuweb/cts/issues/2766 + ], } as const; g.test('ceilInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() .expandWithParams<ScalarToIntervalCase>(p => { const constants = FP[p.trait].constants(); @@ -2674,15 +2682,15 @@ g.test('ceilInterval') { input: -1.9, expected: -1 }, // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.positive.max, expected: constants.positive.max }, { input: constants.positive.min, expected: 1 }, { input: constants.negative.min, expected: constants.negative.min }, { input: constants.negative.max, expected: 0 }, ...kCeilIntervalCases[p.trait], - // 32-bit subnormals + // Subnormals { input: constants.positive.subnormal.max, expected: [0, 1] }, { input: constants.positive.subnormal.min, expected: [0, 1] }, { input: constants.negative.subnormal.min, expected: 0 }, @@ -2714,12 +2722,12 @@ const kCosIntervalThirdPiCases = { // cos(-1.046875) = 0.50027931 { input: kValue.f16.negative.pi.third, - expected: FP['f16'].correctlyRoundedInterval(0.50027931).bounds(), + expected: FP['f16'].correctlyRoundedInterval(0.50027931).endpoints(), }, // cos(1.046875) = 0.50027931 { input: kValue.f16.positive.pi.third, - expected: FP['f16'].correctlyRoundedInterval(0.50027931).bounds(), + expected: FP['f16'].correctlyRoundedInterval(0.50027931).endpoints(), }, ], }; @@ -2740,13 +2748,13 @@ g.test('cosInterval') // substantially different, so instead of getting 0 you get a value on the // order of 10^-8 away from 0, thus difficult to express in a // human-readable manner. - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, { input: constants.negative.pi.whole, expected: [-1, kPlusOneULPFunctions[p.trait](-1)] }, { input: 0, expected: [1, 1] }, { input: constants.positive.pi.whole, expected: [-1, kPlusOneULPFunctions[p.trait](-1)] }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ...(kCosIntervalThirdPiCases[p.trait] as ScalarToIntervalCase[]), ]; @@ -2796,10 +2804,10 @@ g.test('coshInterval') return [ ...kCoshIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -2843,37 +2851,23 @@ const kDegreesIntervalCases = { { input: kValue.f16.positive.pi.three_quarters, expected: [kMinusOneULPFunctions['f16'](135), 135] }, { input: kValue.f16.positive.pi.whole, expected: [kMinusOneULPFunctions['f16'](180), 180] }, ] as ScalarToIntervalCase[], - abstract: [ - { input: kValue.f64.negative.pi.whole, expected: -180 }, - { input: kValue.f64.negative.pi.three_quarters, expected: -135 }, - { input: kValue.f64.negative.pi.half, expected: -90 }, - { input: kValue.f64.negative.pi.third, expected: kPlusOneULPFunctions['abstract'](-60) }, - { input: kValue.f64.negative.pi.quarter, expected: -45 }, - { input: kValue.f64.negative.pi.sixth, expected: kPlusOneULPFunctions['abstract'](-30) }, - { input: kValue.f64.positive.pi.sixth, expected: kMinusOneULPFunctions['abstract'](30) }, - { input: kValue.f64.positive.pi.quarter, expected: 45 }, - { input: kValue.f64.positive.pi.third, expected: kMinusOneULPFunctions['abstract'](60) }, - { input: kValue.f64.positive.pi.half, expected: 90 }, - { input: kValue.f64.positive.pi.three_quarters, expected: 135 }, - { input: kValue.f64.positive.pi.whole, expected: 180 }, - ] as ScalarToIntervalCase[], } as const; g.test('degreesInterval') .params(u => u - .combine('trait', ['f32', 'f16', 'abstract'] as const) + .combine('trait', ['f32', 'f16'] as const) .beginSubcases() .expandWithParams<ScalarToIntervalCase>(p => { const trait = p.trait; const constants = FP[trait].constants(); // prettier-ignore return [ - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, { input: 0, expected: 0 }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, ...kDegreesIntervalCases[trait] ]; }) @@ -2895,14 +2889,14 @@ const kExpIntervalCases = { // exp(88) = 1.6516362549940018555283297962649e+38 = 0x7ef882b6/7. { input: 88, expected: [reinterpretU32AsF32(0x7ef882b6), reinterpretU32AsF32(0x7ef882b7)] }, // exp(89) overflow f32. - { input: 89, expected: kUnboundedBounds }, + { input: 89, expected: kUnboundedEndpoints }, ] as ScalarToIntervalCase[], f16: [ { input: 1, expected: [kValue.f16.positive.e, kPlusOneULPFunctions['f16'](kValue.f16.positive.e)] }, // exp(11) = 59874.141715197818455326485792258 = 0x7b4f/0x7b50. { input: 11, expected: [reinterpretU16AsF16(0x7b4f), reinterpretU16AsF16(0x7b50)] }, // exp(12) = 162754.79141900392080800520489849 overflow f16. - { input: 12, expected: kUnboundedBounds }, + { input: 12, expected: kUnboundedEndpoints }, ] as ScalarToIntervalCase[], } as const; @@ -2916,7 +2910,7 @@ g.test('expInterval') const constants = FP[trait].constants(); // prettier-ignore return [ - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: 0, expected: 1 }, ...kExpIntervalCases[trait], ]; @@ -2954,13 +2948,13 @@ const kExp2IntervalCases = { // exp2(127) = 1.7014118346046923173168730371588e+38 = 0x7f000000, 3 + 2 * 127 = 258 ulps. { input: 127, expected: reinterpretU32AsF32(0x7f000000) }, // exp2(128) overflow f32. - { input: 128, expected: kUnboundedBounds }, + { input: 128, expected: kUnboundedEndpoints }, ] as ScalarToIntervalCase[], f16: [ // exp2(15) = 32768 = 0x7800, 1 + 2 * 15 = 31 ulps { input: 15, expected: reinterpretU16AsF16(0x7800) }, // exp2(16) = 65536 overflow f16. - { input: 16, expected: kUnboundedBounds }, + { input: 16, expected: kUnboundedEndpoints }, ] as ScalarToIntervalCase[], } as const; @@ -2974,7 +2968,7 @@ g.test('exp2Interval') const constants = FP[trait].constants(); // prettier-ignore return [ - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: 0, expected: 1 }, { input: 1, expected: 2 }, ...kExp2IntervalCases[trait], @@ -3051,8 +3045,8 @@ g.test('floorInterval') { input: -1.9, expected: -2 }, // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.positive.max, expected: constants.positive.max }, { input: constants.positive.min, expected: 0 }, { input: constants.negative.min, expected: constants.negative.min }, @@ -3099,12 +3093,24 @@ const kFractIntervalCases = { { input: -1.1, expected: [reinterpretU16AsF16(0x3b32), reinterpretU16AsF16(0x3b34)] }, // ~0.9 { input: 658.5, expected: 0.5 }, ] as ScalarToIntervalCase[], + abstract: [ + { input: 0.1, expected: reinterpretU64AsF64(0x3fb999999999999an) }, + { input: 0.9, expected: reinterpretU64AsF64(0x3feccccccccccccdn) }, + { input: 1.1, expected: reinterpretU64AsF64(0x3fb99999999999a0n) }, + { input: -0.1, expected: reinterpretU64AsF64(0x3feccccccccccccdn) }, + { input: -0.9, expected: reinterpretU64AsF64(0x3fb9999999999998n) }, + { input: -1.1, expected: reinterpretU64AsF64(0x3fecccccccccccccn) }, + + // https://github.com/gpuweb/cts/issues/2766 + { input: 0x80000000, expected: 0 }, + ] as ScalarToIntervalCase[], + } as const; g.test('fractInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() .expandWithParams<ScalarToIntervalCase>(p => { const constants = FP[p.trait].constants(); @@ -3117,8 +3123,8 @@ g.test('fractInterval') ...kFractIntervalCases[p.trait], // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.positive.max, expected: 0 }, { input: constants.positive.min, expected: constants.positive.min }, { input: constants.negative.min, expected: 0 }, @@ -3180,9 +3186,9 @@ g.test('inverseSqrtInterval') { input: 100, expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1 // Out of definition domain - { input: -1, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -3215,7 +3221,7 @@ const kRootSumSquareExpectionInterval = { '[1.0, 1.0]' : [reinterpretU64AsF64(0x3ff6_a09d_b000_0000n), reinterpretU64AsF64(0x3ff6_a09f_1000_0000n)], // ~√2 '[1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3ffb_b67a_1000_0000n), reinterpretU64AsF64(0x3ffb_b67b_b000_0000n)], // ~√3 '[1.0, 1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3fff_ffff_7000_0000n), reinterpretU64AsF64(0x4000_0000_9000_0000n)], // ~2 - } as {[s: string]: IntervalBounds}, + } as {[s: string]: IntervalEndpoints}, f16: { '[0.1]': [reinterpretU64AsF64(0x3fb9_7e00_0000_0000n), reinterpretU64AsF64(0x3fb9_b600_0000_0000n)], // ~0.1 '[1.0]' : [reinterpretU64AsF64(0x3fef_ee00_0000_0000n), reinterpretU64AsF64(0x3ff0_1200_0000_0000n)], // ~1.0 @@ -3223,7 +3229,7 @@ const kRootSumSquareExpectionInterval = { '[1.0, 1.0]' : [reinterpretU64AsF64(0x3ff6_8a00_0000_0000n), reinterpretU64AsF64(0x3ff6_b600_0000_0000n)], // ~√2 '[1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3ffb_9a00_0000_0000n), reinterpretU64AsF64(0x3ffb_d200_0000_0000n)], // ~√3 '[1.0, 1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3fff_ee00_0000_0000n), reinterpretU64AsF64(0x4000_1200_0000_0000n)], // ~2 - } as {[s: string]: IntervalBounds}, + } as {[s: string]: IntervalEndpoints}, } as const; g.test('lengthIntervalScalar') @@ -3243,22 +3249,22 @@ g.test('lengthIntervalScalar') {input: 10.0, expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10 {input: -10.0, expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10 - // length(0) = kUnboundedBounds, because length uses sqrt, which is defined as 1/inversesqrt - {input: 0, expected: kUnboundedBounds }, + // length(0) = kUnboundedEndpoints, because length uses sqrt, which is defined as 1/inversesqrt + {input: 0, expected: kUnboundedEndpoints }, // Subnormal Cases - { input: constants.negative.subnormal.min, expected: kUnboundedBounds }, - { input: constants.negative.subnormal.max, expected: kUnboundedBounds }, - { input: constants.positive.subnormal.min, expected: kUnboundedBounds }, - { input: constants.positive.subnormal.max, expected: kUnboundedBounds }, + { input: constants.negative.subnormal.min, expected: kUnboundedEndpoints }, + { input: constants.negative.subnormal.max, expected: kUnboundedEndpoints }, + { input: constants.positive.subnormal.min, expected: kUnboundedEndpoints }, + { input: constants.positive.subnormal.max, expected: kUnboundedEndpoints }, // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: constants.negative.max, expected: kUnboundedBounds }, - { input: constants.positive.min, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: constants.negative.max, expected: kUnboundedEndpoints }, + { input: constants.positive.min, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, ]; }) ) @@ -3300,8 +3306,8 @@ g.test('logInterval') .expandWithParams<ScalarToIntervalCase>(p => { // prettier-ignore return [ - { input: -1, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, { input: 1, expected: 0 }, ...kLogIntervalCases[p.trait], ]; @@ -3348,8 +3354,8 @@ g.test('log2Interval') .expandWithParams<ScalarToIntervalCase>(p => { // prettier-ignore return [ - { input: -1, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, { input: 1, expected: 0 }, { input: 2, expected: 1 }, { input: 16, expected: 4 }, @@ -3387,8 +3393,8 @@ g.test('negationInterval') // prettier-ignore return [ // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.positive.max, expected: constants.negative.min }, { input: constants.positive.min, expected: constants.negative.max }, { input: constants.negative.min, expected: constants.positive.max }, @@ -3425,8 +3431,8 @@ g.test('quantizeToF16Interval') .paramsSubcasesOnly<ScalarToIntervalCase>( // prettier-ignore [ - { input: kValue.f32.negative.infinity, expected: kUnboundedBounds }, - { input: kValue.f32.negative.min, expected: kUnboundedBounds }, + { input: kValue.f32.negative.infinity, expected: kUnboundedEndpoints }, + { input: kValue.f32.negative.min, expected: kUnboundedEndpoints }, { input: kValue.f16.negative.min, expected: kValue.f16.negative.min }, { input: -1.9, expected: kConstantCorrectlyRoundedExpectation['f16']['-1.9'] }, // ~-1.9 { input: -1, expected: -1 }, @@ -3444,8 +3450,8 @@ g.test('quantizeToF16Interval') { input: 1, expected: 1 }, { input: 1.9, expected: kConstantCorrectlyRoundedExpectation['f16']['1.9'] }, // ~1.9 { input: kValue.f16.positive.max, expected: kValue.f16.positive.max }, - { input: kValue.f32.positive.max, expected: kUnboundedBounds }, - { input: kValue.f32.positive.infinity, expected: kUnboundedBounds }, + { input: kValue.f32.positive.max, expected: kUnboundedEndpoints }, + { input: kValue.f32.positive.infinity, expected: kUnboundedEndpoints }, ] ) .fn(t => { @@ -3488,35 +3494,21 @@ const kRadiansIntervalCases = { { input: 135, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.three_quarters), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.three_quarters)] }, { input: 180, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.whole), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.whole)] }, ] as ScalarToIntervalCase[], - abstract: [ - { input: -180, expected: kValue.f64.negative.pi.whole }, - { input: -135, expected: kValue.f64.negative.pi.three_quarters }, - { input: -90, expected: kValue.f64.negative.pi.half }, - { input: -60, expected: kValue.f64.negative.pi.third }, - { input: -45, expected: kValue.f64.negative.pi.quarter }, - { input: -30, expected: kValue.f64.negative.pi.sixth }, - { input: 30, expected: kValue.f64.positive.pi.sixth }, - { input: 45, expected: kValue.f64.positive.pi.quarter }, - { input: 60, expected: kValue.f64.positive.pi.third }, - { input: 90, expected: kValue.f64.positive.pi.half }, - { input: 135, expected: kValue.f64.positive.pi.three_quarters }, - { input: 180, expected: kValue.f64.positive.pi.whole }, - ] as ScalarToIntervalCase[], } as const; g.test('radiansInterval') .params(u => u - .combine('trait', ['f32', 'f16', 'abstract'] as const) + .combine('trait', ['f32', 'f16'] as const) .beginSubcases() .expandWithParams<ScalarToIntervalCase>(p => { const trait = p.trait; const constants = FP[trait].constants(); // prettier-ignore return [ - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, { input: 0, expected: 0 }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, ...kRadiansIntervalCases[trait] ]; }) @@ -3536,19 +3528,27 @@ const kRoundIntervalCases = { f32: [ { input: 2 ** 30, expected: 2 ** 30 }, { input: -(2 ** 30), expected: -(2 ** 30) }, - { input: 0x80000000, expected: 0x80000000 }, // https://github.com/gpuweb/cts/issues/2766 + { input: 0x8000_0000, expected: 0x8000_0000 }, // https://github.com/gpuweb/cts/issues/2766 ], f16: [ { input: 2 ** 14, expected: 2 ** 14 }, { input: -(2 ** 14), expected: -(2 ** 14) }, { input: 0x8000, expected: 0x8000 }, // https://github.com/gpuweb/cts/issues/2766 ], + abstract: [ + { input: 2 ** 62, expected: 2 ** 62 }, + { input: -(2 ** 62), expected: -(2 ** 62) }, + { + input: 0x8000_0000_0000_0000, + expected: 0x8000_0000_0000_0000, + }, // https://github.com/gpuweb/cts/issues/2766 + ], } as const; g.test('roundInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() .expandWithParams<ScalarToIntervalCase>(p => { const constants = FP[p.trait].constants(); @@ -3571,15 +3571,15 @@ g.test('roundInterval') { input: -1.9, expected: -2 }, // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.positive.max, expected: constants.positive.max }, { input: constants.positive.min, expected: 0 }, { input: constants.negative.min, expected: constants.negative.min }, { input: constants.negative.max, expected: 0 }, ...kRoundIntervalCases[p.trait], - // 32-bit subnormals + // Subnormals { input: constants.positive.subnormal.max, expected: 0 }, { input: constants.positive.subnormal.min, expected: 0 }, { input: constants.negative.subnormal.min, expected: 0 }, @@ -3627,8 +3627,8 @@ g.test('saturateInterval') { input: constants.negative.subnormal.max, expected: [constants.negative.subnormal.max, 0.0] }, // Infinities - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -3651,7 +3651,7 @@ g.test('signInterval') const constants = FP[p.trait].constants(); // prettier-ignore return [ - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.negative.min, expected: -1 }, { input: -10, expected: -1 }, { input: -1, expected: -1 }, @@ -3667,7 +3667,7 @@ g.test('signInterval') { input: 1, expected: 1 }, { input: 10, expected: 1 }, { input: constants.positive.max, expected: 1 }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -3696,13 +3696,13 @@ g.test('sinInterval') // substantially different, so instead of getting 0 you get a value on the // order of 10^-8 away from it, thus difficult to express in a // human-readable manner. - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, { input: constants.negative.pi.half, expected: [-1, kPlusOneULPFunctions[p.trait](-1)] }, { input: 0, expected: 0 }, { input: constants.positive.pi.half, expected: [kMinusOneULPFunctions[p.trait](1), 1] }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -3750,10 +3750,10 @@ g.test('sinhInterval') return [ ...kSinhIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -3832,9 +3832,9 @@ g.test('sqrtInterval') ...kSqrtIntervalCases[p.trait], // Cases out of definition domain - { input: -1, expected: kUnboundedBounds }, - { input: 0, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: -1, expected: kUnboundedEndpoints }, + { input: 0, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -3913,12 +3913,12 @@ g.test('tanInterval') ...kTanIntervalCases[p.trait], // Cases that result in unbounded interval. - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: constants.negative.pi.half, expected: kUnboundedBounds }, - { input: constants.positive.pi.half, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: constants.negative.pi.half, expected: kUnboundedEndpoints }, + { input: constants.positive.pi.half, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -3960,10 +3960,10 @@ g.test('tanhInterval') return [ ...kTanhIntervalCases[p.trait], - { input: constants.negative.infinity, expected: kUnboundedBounds }, - { input: constants.negative.min, expected: kUnboundedBounds }, - { input: constants.positive.max, expected: kUnboundedBounds }, - { input: constants.positive.infinity, expected: kUnboundedBounds }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.min, expected: kUnboundedEndpoints }, + { input: constants.positive.max, expected: kUnboundedEndpoints }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, ]; }) ) @@ -4007,8 +4007,8 @@ g.test('truncInterval') { input: constants.negative.subnormal.max, expected: 0 }, // Edge cases - { input: constants.positive.infinity, expected: kUnboundedBounds }, - { input: constants.negative.infinity, expected: kUnboundedBounds }, + { input: constants.positive.infinity, expected: kUnboundedEndpoints }, + { input: constants.negative.infinity, expected: kUnboundedEndpoints }, { input: constants.positive.max, expected: constants.positive.max }, { input: constants.positive.min, expected: 0 }, { input: constants.negative.min, expected: constants.negative.min }, @@ -4030,7 +4030,7 @@ interface ScalarPairToIntervalCase { // input is a pair of independent values, not a range, so should not be // converted to a FPInterval. input: [number, number]; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } // prettier-ignore @@ -4112,14 +4112,14 @@ g.test('additionInterval') { input: [0, constants.negative.subnormal.min], expected: [constants.negative.subnormal.min, 0] }, // Infinities - { input: [0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, + { input: [0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -4226,33 +4226,33 @@ g.test('atan2Interval') // Cases that y out of bound. // positive y, positive x - { input: [Number.POSITIVE_INFINITY, 1], expected: kUnboundedBounds }, + { input: [Number.POSITIVE_INFINITY, 1], expected: kUnboundedEndpoints }, // positive y, negative x - { input: [Number.POSITIVE_INFINITY, -1], expected: kUnboundedBounds }, + { input: [Number.POSITIVE_INFINITY, -1], expected: kUnboundedEndpoints }, // negative y, negative x - { input: [Number.NEGATIVE_INFINITY, -1], expected: kUnboundedBounds }, + { input: [Number.NEGATIVE_INFINITY, -1], expected: kUnboundedEndpoints }, // negative y, positive x - { input: [Number.NEGATIVE_INFINITY, 1], expected: kUnboundedBounds }, + { input: [Number.NEGATIVE_INFINITY, 1], expected: kUnboundedEndpoints }, // Discontinuity @ origin (0,0) - { input: [0, 0], expected: kUnboundedBounds }, - { input: [0, constants.positive.subnormal.max], expected: kUnboundedBounds }, - { input: [0, constants.negative.subnormal.min], expected: kUnboundedBounds }, - { input: [0, constants.positive.min], expected: kUnboundedBounds }, - { input: [0, constants.negative.max], expected: kUnboundedBounds }, - { input: [0, constants.positive.max], expected: kUnboundedBounds }, - { input: [0, constants.negative.min], expected: kUnboundedBounds }, - { input: [0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [0, 1], expected: kUnboundedBounds }, - { input: [constants.positive.subnormal.max, 1], expected: kUnboundedBounds }, - { input: [constants.negative.subnormal.min, 1], expected: kUnboundedBounds }, - - // Very large |x| values should cause kUnboundedBounds to be returned, due to the restrictions on division - { input: [1, constants.positive.max], expected: kUnboundedBounds }, - { input: [1, constants.positive.nearest_max], expected: kUnboundedBounds }, - { input: [1, constants.negative.min], expected: kUnboundedBounds }, - { input: [1, constants.negative.nearest_min], expected: kUnboundedBounds }, + { input: [0, 0], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.subnormal.max], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.subnormal.min], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.min], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.max], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.max], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [0, 1], expected: kUnboundedEndpoints }, + { input: [constants.positive.subnormal.max, 1], expected: kUnboundedEndpoints }, + { input: [constants.negative.subnormal.min, 1], expected: kUnboundedEndpoints }, + + // Very large |x| values should cause kUnboundedEndpoints to be returned, due to the restrictions on division + { input: [1, constants.positive.max], expected: kUnboundedEndpoints }, + { input: [1, constants.positive.nearest_max], expected: kUnboundedEndpoints }, + { input: [1, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [1, constants.negative.nearest_min], expected: kUnboundedEndpoints }, ]; }) ) @@ -4290,25 +4290,25 @@ g.test('distanceIntervalScalar') { input: [-10.0, 0], expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10 { input: [0, -10.0], expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10 - // distance(x, y), where x - y = 0 has an acceptance interval of kUnboundedBounds, - // because distance(x, y) = length(x - y), and length(0) = kUnboundedBounds - { input: [0, 0], expected: kUnboundedBounds }, - { input: [1.0, 1.0], expected: kUnboundedBounds }, - { input: [-1.0, -1.0], expected: kUnboundedBounds }, + // distance(x, y), where x - y = 0 has an acceptance interval of kUnboundedEndpoints, + // because distance(x, y) = length(x - y), and length(0) = kUnboundedEndpoints + { input: [0, 0], expected: kUnboundedEndpoints }, + { input: [1.0, 1.0], expected: kUnboundedEndpoints }, + { input: [-1.0, -1.0], expected: kUnboundedEndpoints }, // Subnormal Cases - { input: [constants.negative.subnormal.min, 0], expected: kUnboundedBounds }, - { input: [constants.negative.subnormal.max, 0], expected: kUnboundedBounds }, - { input: [constants.positive.subnormal.min, 0], expected: kUnboundedBounds }, - { input: [constants.positive.subnormal.max, 0], expected: kUnboundedBounds }, + { input: [constants.negative.subnormal.min, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.subnormal.max, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.subnormal.min, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.subnormal.max, 0], expected: kUnboundedEndpoints }, // Edge cases - { input: [constants.positive.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.negative.min, 0], expected: kUnboundedBounds }, - { input: [constants.negative.max, 0], expected: kUnboundedBounds }, - { input: [constants.positive.min, 0], expected: kUnboundedBounds }, - { input: [constants.positive.max, 0], expected: kUnboundedBounds }, + { input: [constants.positive.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.min, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.max, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.min, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.max, 0], expected: kUnboundedEndpoints }, ]; }) ) @@ -4371,13 +4371,10 @@ const kDivisionInterval64BitsNormalCases = { g.test('divisionInterval') .params(u => u - .combine('trait', ['abstract', 'f32', 'f16'] as const) + .combine('trait', ['f32', 'f16'] as const) .beginSubcases() .expandWithParams<ScalarPairToIntervalCase>(p => { - // This is a ULP based interval, so abstract should behave like f32, so - // swizzling the trait as needed. - const trait = p.trait === 'abstract' ? 'f32' : p.trait; - const fp = FP[trait]; + const fp = FP[p.trait]; const constants = fp.constants(); // prettier-ignore return [ @@ -4394,26 +4391,23 @@ g.test('divisionInterval') { input: [-4, -2], expected: 2 }, // 64-bit normals that can not be exactly represented - ...kDivisionInterval64BitsNormalCases[trait], + ...kDivisionInterval64BitsNormalCases[p.trait], // Denominator out of range - { input: [1, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [1, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [1, constants.positive.max], expected: kUnboundedBounds }, - { input: [1, constants.negative.min], expected: kUnboundedBounds }, - { input: [1, 0], expected: kUnboundedBounds }, - { input: [1, constants.positive.subnormal.max], expected: kUnboundedBounds }, + { input: [1, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [1, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [1, constants.positive.max], expected: kUnboundedEndpoints }, + { input: [1, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [1, 0], expected: kUnboundedEndpoints }, + { input: [1, constants.positive.subnormal.max], expected: kUnboundedEndpoints }, ]; }) ) .fn(t => { - // This is a ULP based interval, so abstract should behave like f32, so - // swizzling the trait as needed for calculating the expected result. - const trait = t.params.trait === 'abstract' ? 'f32' : t.params.trait; - const fp = FP[trait]; + const fp = FP[t.params.trait]; const error = (n: number): number => { return 2.5 * fp.oneULP(n); @@ -4421,7 +4415,6 @@ g.test('divisionInterval') const [x, y] = t.params.input; - // Do not swizzle here, so the correct implementation under test is called. const expected = FP[t.params.trait].toInterval(applyError(t.params.expected, error)); const got = FP[t.params.trait].divisionInterval(x, y); t.expect( @@ -4452,11 +4445,11 @@ const kLdexpIntervalCases = { // e2 + bias <= 0, expect correctly rounded intervals. { input: [2 ** 120, -130], expected: 2 ** -10 }, // Out of Bounds - { input: [1, 128], expected: kUnboundedBounds }, - { input: [-1, 128], expected: kUnboundedBounds }, - { input: [100, 126], expected: kUnboundedBounds }, - { input: [-100, 126], expected: kUnboundedBounds }, - { input: [2 ** 100, 100], expected: kUnboundedBounds }, + { input: [1, 128], expected: kUnboundedEndpoints }, + { input: [-1, 128], expected: kUnboundedEndpoints }, + { input: [100, 126], expected: kUnboundedEndpoints }, + { input: [-100, 126], expected: kUnboundedEndpoints }, + { input: [2 ** 100, 100], expected: kUnboundedEndpoints }, ] as ScalarPairToIntervalCase[], f16: [ // 64-bit normals @@ -4478,25 +4471,66 @@ const kLdexpIntervalCases = { // e2 + bias <= 0, expect correctly rounded intervals. { input: [2 ** 12, -18], expected: 2 ** -6 }, // Out of Bounds - { input: [1, 16], expected: kUnboundedBounds }, - { input: [-1, 16], expected: kUnboundedBounds }, - { input: [100, 14], expected: kUnboundedBounds }, - { input: [-100, 14], expected: kUnboundedBounds }, - { input: [2 ** 10, 10], expected: kUnboundedBounds }, + { input: [1, 16], expected: kUnboundedEndpoints }, + { input: [-1, 16], expected: kUnboundedEndpoints }, + { input: [100, 14], expected: kUnboundedEndpoints }, + { input: [-100, 14], expected: kUnboundedEndpoints }, + { input: [2 ** 10, 10], expected: kUnboundedEndpoints }, + ] as ScalarPairToIntervalCase[], + abstract: [ + // Edge Cases + // 1.9999999999999997779553950749686919152736663818359375 * 2 ** 1023 = f64.positive.max + { + input: [1.9999999999999997779553950749686919152736663818359375, 1023], + expected: kValue.f64.positive.max, + }, + // f64.positive.min = 1 * 2 ** -1022 + { input: [1, -1022], expected: kValue.f64.positive.min }, + // f64.positive.subnormal.max = 1.9999999999999997779553950749686919152736663818359375 * 2 ** -1022 + { + input: [0.9999999999999997779553950749686919152736663818359375, -1022], + expected: [0, kValue.f64.positive.subnormal.max], + }, + // f64.positive.subnormal.min = 0.0000000000000002220446049250313080847263336181640625 * 2 ** -1022 + { + input: [0.0000000000000002220446049250313080847263336181640625, -1022], + expected: [0, kValue.f64.positive.subnormal.min], + }, + { + input: [-0.0000000000000002220446049250313080847263336181640625, -1022], + expected: [kValue.f64.negative.subnormal.max, 0], + }, + { + input: [-0.9999999999999997779553950749686919152736663818359375, -1022], + expected: [kValue.f64.negative.subnormal.min, 0], + }, + { input: [-1, -1022], expected: kValue.f64.negative.max }, + { + input: [-1.9999999999999997779553950749686919152736663818359375, 1023], + expected: kValue.f64.negative.min, + }, + // e2 + bias <= 0, expect correctly rounded intervals. + { input: [2 ** 120, -130], expected: 2 ** -10 }, + // Out of Bounds + { input: [1, 1024], expected: kUnboundedEndpoints }, + { input: [-1, 1024], expected: kUnboundedEndpoints }, + { input: [100, 1024], expected: kUnboundedEndpoints }, + { input: [-100, 1024], expected: kUnboundedEndpoints }, + { input: [2 ** 100, 1000], expected: kUnboundedEndpoints }, ] as ScalarPairToIntervalCase[], } as const; g.test('ldexpInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() .expandWithParams<ScalarPairToIntervalCase>(p => { const trait = FP[p.trait]; const constants = trait.constants(); // prettier-ignore return [ - // always exactly represeantable cases + // always exactly representable cases { input: [0, 0], expected: 0 }, { input: [0, 1], expected: 0 }, { input: [0, -1], expected: 0 }, @@ -4512,8 +4546,8 @@ g.test('ldexpInterval') { input: [constants.positive.max, kValue.i32.negative.min], expected: 0 }, { input: [constants.negative.min, kValue.i32.negative.min], expected: 0 }, // Out of Bounds - { input: [constants.positive.max, kValue.i32.positive.max], expected: kUnboundedBounds }, - { input: [constants.negative.min, kValue.i32.positive.max], expected: kUnboundedBounds }, + { input: [constants.positive.max, kValue.i32.positive.max], expected: kUnboundedEndpoints }, + { input: [constants.negative.min, kValue.i32.positive.max], expected: kUnboundedEndpoints }, ]; }) ) @@ -4572,14 +4606,14 @@ g.test('maxInterval') { input: [constants.negative.subnormal.min, constants.positive.subnormal.max], expected: [constants.negative.subnormal.min, constants.positive.subnormal.max] }, // Infinities - { input: [0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, + { input: [0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -4638,14 +4672,14 @@ g.test('minInterval') { input: [constants.negative.subnormal.min, constants.positive.subnormal.max], expected: [constants.negative.subnormal.min, constants.positive.subnormal.max] }, // Infinities - { input: [0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, + { input: [0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -4740,22 +4774,22 @@ g.test('multiplicationInterval') ...kMultiplicationInterval64BitsNormalCases[p.trait], // Infinities - { input: [0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [1, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [-1, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [1, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [-1, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, + { input: [0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [1, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [-1, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [1, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [-1, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, // Edges - { input: [constants.positive.max, constants.positive.max], expected: kUnboundedBounds }, - { input: [constants.negative.min, constants.negative.min], expected: kUnboundedBounds }, - { input: [constants.positive.max, constants.negative.min], expected: kUnboundedBounds }, - { input: [constants.negative.min, constants.positive.max], expected: kUnboundedBounds }, + { input: [constants.positive.max, constants.positive.max], expected: kUnboundedEndpoints }, + { input: [constants.negative.min, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [constants.positive.max, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [constants.negative.min, constants.positive.max], expected: kUnboundedEndpoints }, ]; }) ) @@ -4808,11 +4842,11 @@ g.test('powInterval') const constants = trait.constants(); // prettier-ignore return [ - { input: [-1, 0], expected: kUnboundedBounds }, - { input: [0, 0], expected: kUnboundedBounds }, - { input: [0, 1], expected: kUnboundedBounds }, - { input: [1, constants.positive.max], expected: kUnboundedBounds }, - { input: [constants.positive.max, 1], expected: kUnboundedBounds }, + { input: [-1, 0], expected: kUnboundedEndpoints }, + { input: [0, 0], expected: kUnboundedEndpoints }, + { input: [0, 1], expected: kUnboundedEndpoints }, + { input: [1, constants.positive.max], expected: kUnboundedEndpoints }, + { input: [constants.positive.max, 1], expected: kUnboundedEndpoints }, ...kPowIntervalCases[p.trait], ]; @@ -4848,7 +4882,7 @@ const kRemainderCases = { g.test('remainderInterval') .params(u => u - .combine('trait', ['abstract', 'f32', 'f16'] as const) + .combine('trait', ['f32', 'f16'] as const) .beginSubcases() .expandWithParams<ScalarPairToIntervalCase>(p => { const trait = kFPTraitForULP[p.trait]; @@ -4878,15 +4912,15 @@ g.test('remainderInterval') { input: [1.125, 1], expected: 0.125 }, // Denominator out of range - { input: [1, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [1, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [1, constants.positive.max], expected: kUnboundedBounds }, - { input: [1, constants.negative.min], expected: kUnboundedBounds }, - { input: [1, 0], expected: kUnboundedBounds }, - { input: [1, constants.positive.subnormal.max], expected: kUnboundedBounds }, + { input: [1, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [1, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [1, constants.positive.max], expected: kUnboundedEndpoints }, + { input: [1, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [1, 0], expected: kUnboundedEndpoints }, + { input: [1, constants.positive.subnormal.max], expected: kUnboundedEndpoints }, ]; }) ) @@ -4904,7 +4938,7 @@ g.test('remainderInterval') g.test('stepInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() .expandWithParams<ScalarPairToIntervalCase>(p => { const constants = FP[p.trait].constants(); @@ -4922,12 +4956,17 @@ g.test('stepInterval') { input: [1, -1], expected: 0 }, // 64-bit normals - { input: [0.1, 0.1], expected: [0, 1] }, + // number is f64 internally, so the value representing the literal + // 0.1/-0.1 will always be exactly representable in AbstractFloat, + // since AF is also f64 internally. + // It is impossible with normals to cause the rounding ambiguity that + // causes the 0 or 1 result. + { input: [0.1, 0.1], expected: p.trait === 'abstract' ? 1 : [0, 1] }, { input: [0, 0.1], expected: 1 }, { input: [0.1, 0], expected: 0 }, { input: [0.1, 1], expected: 1 }, { input: [1, 0.1], expected: 0 }, - { input: [-0.1, -0.1], expected: [0, 1] }, + { input: [-0.1, -0.1], expected: p.trait === 'abstract' ? 1 : [0, 1] }, { input: [0, -0.1], expected: 0 }, { input: [-0.1, 0], expected: 1 }, { input: [-0.1, -1], expected: 0 }, @@ -4962,14 +5001,14 @@ g.test('stepInterval') { input: [constants.positive.subnormal.max, constants.negative.subnormal.min], expected: [0, 1] }, // Infinities - { input: [0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, + { input: [0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -5060,14 +5099,14 @@ g.test('subtractionInterval') { input: [0, constants.negative.subnormal.min], expected: [0, constants.positive.subnormal.max] }, // Infinities - { input: [0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 0], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, + { input: [0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 0], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -5084,7 +5123,7 @@ g.test('subtractionInterval') interface ScalarTripleToIntervalCase { input: [number, number, number]; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } g.test('clampMedianInterval') @@ -5127,10 +5166,10 @@ g.test('clampMedianInterval') { input: [constants.positive.max, constants.positive.max, constants.positive.subnormal.min], expected: constants.positive.max }, // Infinities - { input: [0, 1, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, + { input: [0, 1, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -5185,10 +5224,10 @@ g.test('clampMinMaxInterval') { input: [constants.positive.max, constants.positive.max, constants.positive.subnormal.min], expected: [0, constants.positive.subnormal.min] }, // Infinities - { input: [0, 1, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, + { input: [0, 1, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -5240,21 +5279,12 @@ const kFmaIntervalCases = { // minimum case: -1 * [subnormal ulp] + -1 * [subnormal ulp] rounded to [-2 * [subnormal ulp], 0], // maximum case: -0.0 + -0.0 = 0. { input: [kValue.f16.positive.subnormal.max, kValue.f16.negative.subnormal.min, kValue.f16.negative.subnormal.max], expected: [-2 * FP['f16'].oneULP(0, 'no-flush'), 0] }, ] as ScalarTripleToIntervalCase[], - abstract: [ - // These operations break down in the CTS, because `number` is a f64 under the hood, so precision is sometimes lost - // if intermediate results are closer to 0 than the smallest subnormal will be precisely 0. - // See https://github.com/gpuweb/cts/issues/2993 for details - { input: [kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.max, 0], expected: 0 }, - { input: [kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.max], expected: [0, kValue.f64.positive.subnormal.max] }, - { input: [kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.min, kValue.f64.negative.subnormal.max], expected: [kValue.f64.negative.subnormal.max, 0] }, - { input: [kValue.f64.positive.subnormal.max, kValue.f64.negative.subnormal.min, kValue.f64.negative.subnormal.max], expected: [kValue.f64.negative.subnormal.max, 0] }, - ] as ScalarTripleToIntervalCase[], } as const; g.test('fmaInterval') .params(u => u - .combine('trait', ['f32', 'f16', 'abstract'] as const) + .combine('trait', ['f32', 'f16'] as const) .beginSubcases() .expandWithParams<ScalarTripleToIntervalCase>(p => { const trait = FP[p.trait]; @@ -5286,11 +5316,11 @@ g.test('fmaInterval') { input: [0, constants.positive.subnormal.max, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] }, // Infinities - { input: [0, 1, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.max, constants.positive.max, constants.positive.subnormal.min], expected: kUnboundedBounds }, + { input: [0, 1, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.max, constants.positive.max, constants.positive.subnormal.min], expected: kUnboundedEndpoints }, ...kFmaIntervalCases[p.trait], ]; }) @@ -5312,55 +5342,62 @@ g.test('fmaInterval') // prettier-ignore const kMixImpreciseIntervalCases = { f32: [ - // [0.0, 1.0] cases - { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9999_8000_0000n), reinterpretU64AsF64(0x3fb9_9999_a000_0000n)] }, // ~0.1 - { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9 - // [1.0, 0.0] cases - { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9 - { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_9999_0000_0000n), reinterpretU64AsF64(0x3fb9_999a_0000_0000n)] }, // ~0.1 - // [0.0, 10.0] cases - { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_ffff_e000_0000n), reinterpretU64AsF64(0x3ff0_0000_2000_0000n)] }, // ~1 - { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_ffff_e000_0000n), reinterpretU64AsF64(0x4022_0000_2000_0000n)] }, // ~9 - // [2.0, 10.0] cases - { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6666_6000_0000n), reinterpretU64AsF64(0x4006_6666_8000_0000n)] }, // ~2.8 - { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6666_6000_0000n), reinterpretU64AsF64(0x4022_6666_8000_0000n)] }, // ~9.2 - // [-1.0, 1.0] cases - { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9999_a000_0000n), reinterpretU64AsF64(0xbfe9_9999_8000_0000n)] }, // ~-0.8 - { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9999_8000_0000n), reinterpretU64AsF64(0x3fe9_9999_c000_0000n)] }, // ~0.8 - - // Showing how precise and imprecise versions diff - // Note that this expectation is 0 only in f32 as 10.0 is much smaller that f32.negative.min, - // So that 10 - f32.negative.min == f32.negative.min even in f64. But for f16, there is not - // a exactly-represenatble f16 value v that make v - f16.negative.min == f16.negative.min - // in f64, in fact that require v being smaller than 2**-37. - { input: [kValue.f32.negative.min, 10.0, 1.0], expected: 0.0 }, - // -10.0 is the same, much smaller than f32.negative.min - { input: [kValue.f32.negative.min, -10.0, 1.0], expected: 0.0 }, + // [0.0, 1.0] cases + { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9999_8000_0000n), reinterpretU64AsF64(0x3fb9_9999_a000_0000n)] }, // ~0.1 + { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9 + // [1.0, 0.0] cases + { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9 + { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_9999_0000_0000n), reinterpretU64AsF64(0x3fb9_999a_0000_0000n)] }, // ~0.1 + // [0.0, 10.0] cases + { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_ffff_e000_0000n), reinterpretU64AsF64(0x3ff0_0000_2000_0000n)] }, // ~1 + { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_ffff_e000_0000n), reinterpretU64AsF64(0x4022_0000_2000_0000n)] }, // ~9 + // [2.0, 10.0] cases + { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6666_6000_0000n), reinterpretU64AsF64(0x4006_6666_8000_0000n)] }, // ~2.8 + { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6666_6000_0000n), reinterpretU64AsF64(0x4022_6666_8000_0000n)] }, // ~9.2 + // [-1.0, 1.0] cases + { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9999_a000_0000n), reinterpretU64AsF64(0xbfe9_9999_8000_0000n)] }, // ~-0.8 + { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9999_8000_0000n), reinterpretU64AsF64(0x3fe9_9999_c000_0000n)] }, // ~0.8 + + // Showing how precise and imprecise versions diff + // Note that this expectation is 0 in f32 as |10.0| is much smaller than + // |f32.negative.min|. + // So that 10 - f32.negative.min == -f32.negative.min even in f64. + { input: [kValue.f32.negative.min, 10.0, 1.0], expected: 0.0 }, + // -10.0 is the same, much smaller than f32.negative.min + { input: [kValue.f32.negative.min, -10.0, 1.0], expected: 0.0 }, + { input: [kValue.f32.negative.min, 10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f32.negative.min, -10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f32.negative.min, 10.0, 0.5], expected: reinterpretU32AsF32(0xfeffffff) }, + { input: [kValue.f32.negative.min, -10.0, 0.5], expected: reinterpretU32AsF32(0xfeffffff) }, ] as ScalarTripleToIntervalCase[], f16: [ - // [0.0, 1.0] cases - { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9800_0000_0000n), reinterpretU64AsF64(0x3fb9_9c00_0000_0000n)] }, // ~0.1 - { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9 - // [1.0, 0.0] cases - { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9 - { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_8000_0000_0000n), reinterpretU64AsF64(0x3fb9_a000_0000_0000n)] }, // ~0.1 - // [0.0, 10.0] cases - { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_fc00_0000_0000n), reinterpretU64AsF64(0x3ff0_0400_0000_0000n)] }, // ~1 - { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_fc00_0000_0000n), reinterpretU64AsF64(0x4022_0400_0000_0000n)] }, // ~9 - // [2.0, 10.0] cases - { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6400_0000_0000n), reinterpretU64AsF64(0x4006_6800_0000_0000n)] }, // ~2.8 - { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6400_0000_0000n), reinterpretU64AsF64(0x4022_6800_0000_0000n)] }, // ~9.2 - // [-1.0, 1.0] cases - { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9c00_0000_0000n), reinterpretU64AsF64(0xbfe9_9800_0000_0000n)] }, // ~-0.8 - { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9800_0000_0000n), reinterpretU64AsF64(0x3fe9_a000_0000_0000n)] }, // ~0.8 - - // Showing how precise and imprecise versions diff - // In imprecise version, we compute (y - x), where y = 10 and x = -65504, the result is 65514 - // and cause an overflow in f16. - { input: [kValue.f16.negative.min, 10.0, 1.0], expected: kUnboundedBounds }, - // (y - x) * 1.0, where y = -10 and x = -65504, the result is 65494 rounded to 65472 or 65504. - // The result is -65504 + 65472 = -32 or -65504 + 65504 = 0. - { input: [kValue.f16.negative.min, -10.0, 1.0], expected: [-32, 0] }, + // [0.0, 1.0] cases + { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9800_0000_0000n), reinterpretU64AsF64(0x3fb9_9c00_0000_0000n)] }, // ~0.1 + { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9 + // [1.0, 0.0] cases + { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9 + { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_8000_0000_0000n), reinterpretU64AsF64(0x3fb9_a000_0000_0000n)] }, // ~0.1 + // [0.0, 10.0] cases + { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_fc00_0000_0000n), reinterpretU64AsF64(0x3ff0_0400_0000_0000n)] }, // ~1 + { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_fc00_0000_0000n), reinterpretU64AsF64(0x4022_0400_0000_0000n)] }, // ~9 + // [2.0, 10.0] cases + { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6400_0000_0000n), reinterpretU64AsF64(0x4006_6800_0000_0000n)] }, // ~2.8 + { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6400_0000_0000n), reinterpretU64AsF64(0x4022_6800_0000_0000n)] }, // ~9.2 + // [-1.0, 1.0] cases + { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9c00_0000_0000n), reinterpretU64AsF64(0xbfe9_9800_0000_0000n)] }, // ~-0.8 + { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9800_0000_0000n), reinterpretU64AsF64(0x3fe9_a000_0000_0000n)] }, // ~0.8 + + // Showing how precise and imprecise versions diff + // In imprecise version, we compute (y - x), where y = 10 and x = -65504, the result is 65514 + // and cause an overflow in f16. + { input: [kValue.f16.negative.min, 10.0, 1.0], expected: kUnboundedEndpoints }, + // (y - x) * 1.0, where y = -10 and x = -65504, the result is 65494 rounded to 65472 or 65504. + // The result is -65504 + 65472 = -32 or -65504 + 65504 = 0. + { input: [kValue.f16.negative.min, -10.0, 1.0], expected: [-32, 0] }, + { input: [kValue.f16.negative.min, 10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f16.negative.min, -10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f16.negative.min, 10.0, 0.5], expected: kUnboundedEndpoints }, + { input: [kValue.f16.negative.min, -10.0, 0.5], expected: [-32768.0, -32752.0] }, ] as ScalarTripleToIntervalCase[], } as const; @@ -5412,19 +5449,16 @@ g.test('mixImpreciseInterval') { input: [-1.0, 1.0, 2.0], expected: 3.0 }, // Infinities - { input: [0.0, constants.positive.infinity, 0.5], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 0.0, 0.5], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 1.0, 0.5], expected: kUnboundedBounds }, - { input: [1.0, constants.negative.infinity, 0.5], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, 0.5], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity, 0.5], expected: kUnboundedBounds }, - { input: [0.0, 1.0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [1.0, 0.0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [0.0, 1.0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [1.0, 0.0, constants.positive.infinity], expected: kUnboundedBounds }, - - // The [negative.min, +/-10.0, 1.0] cases has different result for different trait on - // imprecise version. + { input: [0.0, constants.positive.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 0.0, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 1.0, 0.5], expected: kUnboundedEndpoints }, + { input: [1.0, constants.negative.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [0.0, 1.0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [1.0, 0.0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [0.0, 1.0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [1.0, 0.0, constants.positive.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -5459,6 +5493,17 @@ const kMixPreciseIntervalCases = { // [-1.0, 1.0] cases { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9999_c000_0000n), reinterpretU64AsF64(0xbfe9_9999_8000_0000n)] }, // ~-0.8 { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9999_8000_0000n), reinterpretU64AsF64(0x3fe9_9999_c000_0000n)] }, // ~0.8 + + // Showing how precise and imprecise versions diff + { input: [kValue.f32.negative.min, 10.0, 1.0], expected: 10 }, + { input: [kValue.f32.negative.min, -10.0, 1.0], expected: -10 }, + { input: [kValue.f32.negative.min, 10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f32.negative.min, -10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f32.negative.min, 10.0, 0.5], expected: reinterpretU32AsF32(0xfeffffff) }, + { input: [kValue.f32.negative.min, -10.0, 0.5], expected: reinterpretU32AsF32(0xfeffffff) }, + + // Intermediate OOB + { input: [1.0, 2.0, kPlusOneULPFunctions['f32'](kValue.f32.positive.max / 2)], expected: kUnboundedEndpoints }, ] as ScalarTripleToIntervalCase[], f16: [ // [0.0, 1.0] cases @@ -5476,6 +5521,17 @@ const kMixPreciseIntervalCases = { // [-1.0, 1.0] cases { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_a000_0000_0000n), reinterpretU64AsF64(0xbfe9_9800_0000_0000n)] }, // ~-0.8 { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9800_0000_0000n), reinterpretU64AsF64(0x3fe9_a000_0000_0000n)] }, // ~0.8 + + // Showing how precise and imprecise versions diff + { input: [kValue.f64.negative.min, 10.0, 1.0], expected: kUnboundedEndpoints }, + { input: [kValue.f64.negative.min, -10.0, 1.0], expected: kUnboundedEndpoints }, + { input: [kValue.f64.negative.min, 10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f64.negative.min, -10.0, 5.0], expected: kUnboundedEndpoints }, + { input: [kValue.f64.negative.min, 10.0, 0.5], expected: kUnboundedEndpoints }, + { input: [kValue.f64.negative.min, -10.0, 0.5], expected: kUnboundedEndpoints }, + + // Intermediate OOB + { input: [1.0, 2.0, kPlusOneULPFunctions['f16'](kValue.f16.positive.max / 2)], expected: kUnboundedEndpoints }, ] as ScalarTripleToIntervalCase[], } as const; @@ -5527,20 +5583,16 @@ g.test('mixPreciseInterval') { input: [-1.0, 1.0, 2.0], expected: 3.0 }, // Infinities - { input: [0.0, constants.positive.infinity, 0.5], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 0.0, 0.5], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 1.0, 0.5], expected: kUnboundedBounds }, - { input: [1.0, constants.negative.infinity, 0.5], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, constants.positive.infinity, 0.5], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, constants.negative.infinity, 0.5], expected: kUnboundedBounds }, - { input: [0.0, 1.0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [1.0, 0.0, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [0.0, 1.0, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [1.0, 0.0, constants.positive.infinity], expected: kUnboundedBounds }, - - // Showing how precise and imprecise versions diff - { input: [constants.negative.min, 10.0, 1.0], expected: 10.0 }, - { input: [constants.negative.min, -10.0, 1.0], expected: -10.0 }, + { input: [0.0, constants.positive.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 0.0, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 1.0, 0.5], expected: kUnboundedEndpoints }, + { input: [1.0, constants.negative.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, constants.positive.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, constants.negative.infinity, 0.5], expected: kUnboundedEndpoints }, + { input: [0.0, 1.0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [1.0, 0.0, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [0.0, 1.0, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [1.0, 0.0, constants.positive.infinity], expected: kUnboundedEndpoints }, ]; }) ) @@ -5622,18 +5674,18 @@ g.test('smoothStepInterval') { input: [0, 1, -10], expected: 0 }, // Subnormals - { input: [0, constants.positive.subnormal.max, 1], expected: kUnboundedBounds }, - { input: [0, constants.positive.subnormal.min, 1], expected: kUnboundedBounds }, - { input: [0, constants.negative.subnormal.max, 1], expected: kUnboundedBounds }, - { input: [0, constants.negative.subnormal.min, 1], expected: kUnboundedBounds }, + { input: [0, constants.positive.subnormal.max, 1], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.subnormal.min, 1], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.subnormal.max, 1], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.subnormal.min, 1], expected: kUnboundedEndpoints }, // Infinities - { input: [0, 2, constants.positive.infinity], expected: kUnboundedBounds }, - { input: [0, 2, constants.negative.infinity], expected: kUnboundedBounds }, - { input: [constants.positive.infinity, 2, 1], expected: kUnboundedBounds }, - { input: [constants.negative.infinity, 2, 1], expected: kUnboundedBounds }, - { input: [0, constants.positive.infinity, 1], expected: kUnboundedBounds }, - { input: [0, constants.negative.infinity, 1], expected: kUnboundedBounds }, + { input: [0, 2, constants.positive.infinity], expected: kUnboundedEndpoints }, + { input: [0, 2, constants.negative.infinity], expected: kUnboundedEndpoints }, + { input: [constants.positive.infinity, 2, 1], expected: kUnboundedEndpoints }, + { input: [constants.negative.infinity, 2, 1], expected: kUnboundedEndpoints }, + { input: [0, constants.positive.infinity, 1], expected: kUnboundedEndpoints }, + { input: [0, constants.negative.infinity, 1], expected: kUnboundedEndpoints }, ]; }) ) @@ -5650,7 +5702,7 @@ g.test('smoothStepInterval') interface ScalarToVectorCase { input: number; - expected: (number | IntervalBounds)[]; + expected: (number | IntervalEndpoints)[]; } g.test('unpack2x16floatInterval') @@ -5674,8 +5726,8 @@ g.test('unpack2x16floatInterval') { input: 0x000083ff, expected: [[kValue.f16.negative.subnormal.min, 0], 0] }, // f16 out of bounds - { input: 0x7c000000, expected: [kUnboundedBounds, kUnboundedBounds] }, - { input: 0xffff0000, expected: [kUnboundedBounds, kUnboundedBounds] }, + { input: 0x7c000000, expected: [kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: 0xffff0000, expected: [kUnboundedEndpoints, kUnboundedEndpoints] }, ] ) .fn(t => { @@ -5691,24 +5743,24 @@ g.test('unpack2x16floatInterval') // magic numbers that don't pollute the global namespace or have unwieldy long // names. { - const kZeroBounds: IntervalBounds = [ + const kZeroEndpoints: IntervalEndpoints = [ reinterpretU32AsF32(0x81400000), reinterpretU32AsF32(0x01400000), ]; - const kOneBoundsSnorm: IntervalBounds = [ + const kOneEndpointsSnorm: IntervalEndpoints = [ reinterpretU64AsF64(0x3fef_ffff_a000_0000n), reinterpretU64AsF64(0x3ff0_0000_3000_0000n), ]; - const kNegOneBoundsSnorm: IntervalBounds = [ + const kNegOneEndpointsSnorm: IntervalEndpoints = [ reinterpretU64AsF64(0xbff0_0000_3000_0000n), reinterpretU64AsF64(0xbfef_ffff_a000_0000n), ]; - const kHalfBounds2x16snorm: IntervalBounds = [ + const kHalfEndpoints2x16snorm: IntervalEndpoints = [ reinterpretU64AsF64(0x3fe0_001f_a000_0000n), reinterpretU64AsF64(0x3fe0_0020_8000_0000n), ]; // ~0.5..., due to lack of precision in i16 - const kNegHalfBounds2x16snorm: IntervalBounds = [ + const kNegHalfEndpoints2x16snorm: IntervalEndpoints = [ reinterpretU64AsF64(0xbfdf_ffc0_6000_0000n), reinterpretU64AsF64(0xbfdf_ffbf_8000_0000n), ]; // ~-0.5..., due to lack of precision in i16 @@ -5717,13 +5769,13 @@ g.test('unpack2x16floatInterval') .paramsSubcasesOnly<ScalarToVectorCase>( // prettier-ignore [ - { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] }, - { input: 0x00007fff, expected: [kOneBoundsSnorm, kZeroBounds] }, - { input: 0x7fff0000, expected: [kZeroBounds, kOneBoundsSnorm] }, - { input: 0x7fff7fff, expected: [kOneBoundsSnorm, kOneBoundsSnorm] }, - { input: 0x80018001, expected: [kNegOneBoundsSnorm, kNegOneBoundsSnorm] }, - { input: 0x40004000, expected: [kHalfBounds2x16snorm, kHalfBounds2x16snorm] }, - { input: 0xc001c001, expected: [kNegHalfBounds2x16snorm, kNegHalfBounds2x16snorm] }, + { input: 0x00000000, expected: [kZeroEndpoints, kZeroEndpoints] }, + { input: 0x00007fff, expected: [kOneEndpointsSnorm, kZeroEndpoints] }, + { input: 0x7fff0000, expected: [kZeroEndpoints, kOneEndpointsSnorm] }, + { input: 0x7fff7fff, expected: [kOneEndpointsSnorm, kOneEndpointsSnorm] }, + { input: 0x80018001, expected: [kNegOneEndpointsSnorm, kNegOneEndpointsSnorm] }, + { input: 0x40004000, expected: [kHalfEndpoints2x16snorm, kHalfEndpoints2x16snorm] }, + { input: 0xc001c001, expected: [kNegHalfEndpoints2x16snorm, kNegHalfEndpoints2x16snorm] }, ] ) .fn(t => { @@ -5740,15 +5792,15 @@ g.test('unpack2x16floatInterval') // magic numbers that don't pollute the global namespace or have unwieldy long // names. { - const kZeroBounds: IntervalBounds = [ + const kZeroEndpoints: IntervalEndpoints = [ reinterpretU32AsF32(0x8140_0000), reinterpretU32AsF32(0x0140_0000), ]; // ~0 - const kOneBounds: IntervalBounds = [ + const kOneEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0x3fef_ffff_a000_0000n), reinterpretU64AsF64(0x3ff0_0000_3000_0000n), ]; // ~1 - const kHalfBounds: IntervalBounds = [ + const kHalfEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0x3fe0_000f_a000_0000n), reinterpretU64AsF64(0x3fe0_0010_8000_0000n), ]; // ~0.5..., due to the lack of accuracy in u16 @@ -5757,11 +5809,11 @@ g.test('unpack2x16floatInterval') .paramsSubcasesOnly<ScalarToVectorCase>( // prettier-ignore [ - { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] }, - { input: 0x0000ffff, expected: [kOneBounds, kZeroBounds] }, - { input: 0xffff0000, expected: [kZeroBounds, kOneBounds] }, - { input: 0xffffffff, expected: [kOneBounds, kOneBounds] }, - { input: 0x80008000, expected: [kHalfBounds, kHalfBounds] }, + { input: 0x00000000, expected: [kZeroEndpoints, kZeroEndpoints] }, + { input: 0x0000ffff, expected: [kOneEndpoints, kZeroEndpoints] }, + { input: 0xffff0000, expected: [kZeroEndpoints, kOneEndpoints] }, + { input: 0xffffffff, expected: [kOneEndpoints, kOneEndpoints] }, + { input: 0x80008000, expected: [kHalfEndpoints, kHalfEndpoints] }, ] ) .fn(t => { @@ -5778,23 +5830,23 @@ g.test('unpack2x16floatInterval') // magic numbers that don't pollute the global namespace or have unwieldy long // names. { - const kZeroBounds: IntervalBounds = [ + const kZeroEndpoints: IntervalEndpoints = [ reinterpretU32AsF32(0x8140_0000), reinterpretU32AsF32(0x0140_0000), ]; // ~0 - const kOneBounds: IntervalBounds = [ + const kOneEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0x3fef_ffff_a000_0000n), reinterpretU64AsF64(0x3ff0_0000_3000_0000n), ]; // ~1 - const kNegOneBounds: IntervalBounds = [ + const kNegOneEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0xbff0_0000_3000_0000n), reinterpretU64AsF64(0xbfef_ffff_a0000_000n), ]; // ~-1 - const kHalfBounds: IntervalBounds = [ + const kHalfEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0x3fe0_2040_2000_0000n), reinterpretU64AsF64(0x3fe0_2041_0000_0000n), ]; // ~0.50196..., due to lack of precision in i8 - const kNegHalfBounds: IntervalBounds = [ + const kNegHalfEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0xbfdf_bf7f_6000_0000n), reinterpretU64AsF64(0xbfdf_bf7e_8000_0000n), ]; // ~-0.49606..., due to lack of precision in i8 @@ -5803,27 +5855,27 @@ g.test('unpack2x16floatInterval') .paramsSubcasesOnly<ScalarToVectorCase>( // prettier-ignore [ - { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] }, - { input: 0x0000007f, expected: [kOneBounds, kZeroBounds, kZeroBounds, kZeroBounds] }, - { input: 0x00007f00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kZeroBounds] }, - { input: 0x007f0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kZeroBounds] }, - { input: 0x7f000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBounds] }, - { input: 0x00007f7f, expected: [kOneBounds, kOneBounds, kZeroBounds, kZeroBounds] }, - { input: 0x7f7f0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kOneBounds] }, - { input: 0x7f007f00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kOneBounds] }, - { input: 0x007f007f, expected: [kOneBounds, kZeroBounds, kOneBounds, kZeroBounds] }, - { input: 0x7f7f7f7f, expected: [kOneBounds, kOneBounds, kOneBounds, kOneBounds] }, + { input: 0x00000000, expected: [kZeroEndpoints, kZeroEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0x0000007f, expected: [kOneEndpoints, kZeroEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0x00007f00, expected: [kZeroEndpoints, kOneEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0x007f0000, expected: [kZeroEndpoints, kZeroEndpoints, kOneEndpoints, kZeroEndpoints] }, + { input: 0x7f000000, expected: [kZeroEndpoints, kZeroEndpoints, kZeroEndpoints, kOneEndpoints] }, + { input: 0x00007f7f, expected: [kOneEndpoints, kOneEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0x7f7f0000, expected: [kZeroEndpoints, kZeroEndpoints, kOneEndpoints, kOneEndpoints] }, + { input: 0x7f007f00, expected: [kZeroEndpoints, kOneEndpoints, kZeroEndpoints, kOneEndpoints] }, + { input: 0x007f007f, expected: [kOneEndpoints, kZeroEndpoints, kOneEndpoints, kZeroEndpoints] }, + { input: 0x7f7f7f7f, expected: [kOneEndpoints, kOneEndpoints, kOneEndpoints, kOneEndpoints] }, { input: 0x81818181, - expected: [kNegOneBounds, kNegOneBounds, kNegOneBounds, kNegOneBounds] + expected: [kNegOneEndpoints, kNegOneEndpoints, kNegOneEndpoints, kNegOneEndpoints] }, { input: 0x40404040, - expected: [kHalfBounds, kHalfBounds, kHalfBounds, kHalfBounds] + expected: [kHalfEndpoints, kHalfEndpoints, kHalfEndpoints, kHalfEndpoints] }, { input: 0xc1c1c1c1, - expected: [kNegHalfBounds, kNegHalfBounds, kNegHalfBounds, kNegHalfBounds] + expected: [kNegHalfEndpoints, kNegHalfEndpoints, kNegHalfEndpoints, kNegHalfEndpoints] }, ] ) @@ -5841,15 +5893,15 @@ g.test('unpack2x16floatInterval') // magic numbers that don't pollute the global namespace or have unwieldy long // names. { - const kZeroBounds: IntervalBounds = [ + const kZeroEndpoints: IntervalEndpoints = [ reinterpretU32AsF32(0x8140_0000), reinterpretU32AsF32(0x0140_0000), ]; // ~0 - const kOneBounds: IntervalBounds = [ + const kOneEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0x3fef_ffff_a000_0000n), reinterpretU64AsF64(0x3ff0_0000_3000_0000n), ]; // ~1 - const kHalfBounds: IntervalBounds = [ + const kHalfEndpoints: IntervalEndpoints = [ reinterpretU64AsF64(0x3fe0_100f_a000_0000n), reinterpretU64AsF64(0x3fe0_1010_8000_0000n), ]; // ~0.50196..., due to lack of precision in u8 @@ -5858,19 +5910,19 @@ g.test('unpack2x16floatInterval') .paramsSubcasesOnly<ScalarToVectorCase>( // prettier-ignore [ - { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] }, - { input: 0x000000ff, expected: [kOneBounds, kZeroBounds, kZeroBounds, kZeroBounds] }, - { input: 0x0000ff00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kZeroBounds] }, - { input: 0x00ff0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kZeroBounds] }, - { input: 0xff000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBounds] }, - { input: 0x0000ffff, expected: [kOneBounds, kOneBounds, kZeroBounds, kZeroBounds] }, - { input: 0xffff0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kOneBounds] }, - { input: 0xff00ff00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kOneBounds] }, - { input: 0x00ff00ff, expected: [kOneBounds, kZeroBounds, kOneBounds, kZeroBounds] }, - { input: 0xffffffff, expected: [kOneBounds, kOneBounds, kOneBounds, kOneBounds] }, + { input: 0x00000000, expected: [kZeroEndpoints, kZeroEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0x000000ff, expected: [kOneEndpoints, kZeroEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0x0000ff00, expected: [kZeroEndpoints, kOneEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0x00ff0000, expected: [kZeroEndpoints, kZeroEndpoints, kOneEndpoints, kZeroEndpoints] }, + { input: 0xff000000, expected: [kZeroEndpoints, kZeroEndpoints, kZeroEndpoints, kOneEndpoints] }, + { input: 0x0000ffff, expected: [kOneEndpoints, kOneEndpoints, kZeroEndpoints, kZeroEndpoints] }, + { input: 0xffff0000, expected: [kZeroEndpoints, kZeroEndpoints, kOneEndpoints, kOneEndpoints] }, + { input: 0xff00ff00, expected: [kZeroEndpoints, kOneEndpoints, kZeroEndpoints, kOneEndpoints] }, + { input: 0x00ff00ff, expected: [kOneEndpoints, kZeroEndpoints, kOneEndpoints, kZeroEndpoints] }, + { input: 0xffffffff, expected: [kOneEndpoints, kOneEndpoints, kOneEndpoints, kOneEndpoints] }, { input: 0x80808080, - expected: [kHalfBounds, kHalfBounds, kHalfBounds, kHalfBounds] + expected: [kHalfEndpoints, kHalfEndpoints, kHalfEndpoints, kHalfEndpoints] }, ] ) @@ -5886,7 +5938,7 @@ g.test('unpack2x16floatInterval') interface VectorToIntervalCase { input: number[]; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } g.test('lengthIntervalVector') @@ -5926,10 +5978,10 @@ g.test('lengthIntervalVector') {input: [-1.0, 1.0, -1.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2 {input: [0.1, 0.0, 0.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1 - // Test that dot going OOB bounds in the intermediate calculations propagates - { input: [constants.positive.nearest_max, constants.positive.max, constants.negative.min], expected: kUnboundedBounds }, - { input: [constants.positive.max, constants.positive.nearest_max, constants.negative.min], expected: kUnboundedBounds }, - { input: [constants.negative.min, constants.positive.max, constants.positive.nearest_max], expected: kUnboundedBounds }, + // Test that dot going OOB in the intermediate calculations propagates + { input: [constants.positive.nearest_max, constants.positive.max, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [constants.positive.max, constants.positive.nearest_max, constants.negative.min], expected: kUnboundedEndpoints }, + { input: [constants.negative.min, constants.positive.max, constants.positive.nearest_max], expected: kUnboundedEndpoints }, ]; }) ) @@ -5945,7 +5997,7 @@ g.test('lengthIntervalVector') interface VectorPairToIntervalCase { input: [number[], number[]]; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } g.test('distanceIntervalVector') @@ -5956,11 +6008,11 @@ g.test('distanceIntervalVector') .expandWithParams<VectorPairToIntervalCase>(p => { // prettier-ignore return [ - // distance(x, y), where x - y = 0 has an acceptance interval of kUnboundedBounds, - // because distance(x, y) = length(x - y), and length(0) = kUnboundedBounds. + // distance(x, y), where x - y = 0 has an acceptance interval of kUnboundedEndpoints, + // because distance(x, y) = length(x - y), and length(0) = kUnboundedEndpoints. // vec2 - { input: [[1.0, 0.0], [1.0, 0.0]], expected: kUnboundedBounds }, + { input: [[1.0, 0.0], [1.0, 0.0]], expected: kUnboundedEndpoints }, { input: [[1.0, 0.0], [0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 { input: [[0.0, 0.0], [1.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 { input: [[-1.0, 0.0], [0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 @@ -5969,7 +6021,7 @@ g.test('distanceIntervalVector') { input: [[0.1, 0.0], [0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1 // vec3 - { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kUnboundedBounds }, + { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kUnboundedEndpoints }, { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 { input: [[0.0, 1.0, 0.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 @@ -5984,7 +6036,7 @@ g.test('distanceIntervalVector') { input: [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1 // vec4 - { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: kUnboundedBounds }, + { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: kUnboundedEndpoints }, { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 { input: [[0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 { input: [[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1 @@ -6019,7 +6071,7 @@ const kDotIntervalCases = { // 3.0*3.0 = 9.0 is much smaller than kValue.f32.positive.max, as a result // kValue.f32.positive.max + 9.0 = kValue.f32.positive.max in f32 and even f64. So, if the // positive and negative large number cancel each other first, the result would be - // 2.0*2.0+3.0*3.0 = 13. Otherwise, the resule would be 0.0 or 4.0 or 9.0. + // 2.0*2.0+3.0*3.0 = 13. Otherwise, the result would be 0.0 or 4.0 or 9.0. // https://github.com/gpuweb/cts/issues/2155 { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f32.positive.max, -2.0, -3.0]], expected: [-13, 0] }, { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [1.0, kValue.f32.negative.min, 2.0, 3.0]], expected: [0, 13] }, @@ -6029,10 +6081,10 @@ const kDotIntervalCases = { // 3.0*3.0 = 9.0 is not small enough comparing to kValue.f16.positive.max = 65504, as a result // kValue.f16.positive.max + 9.0 = 65513 is exactly representable in f32 and f64. So, if the // positive and negative large number don't cancel each other first, the computation will - // overflow f16 and result in unbounded bounds. + // overflow f16 and result in unbounded endpoints. // https://github.com/gpuweb/cts/issues/2155 - { input: [[kValue.f16.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f16.positive.max, -2.0, -3.0]], expected: kUnboundedBounds }, - { input: [[kValue.f16.positive.max, 1.0, 2.0, 3.0], [1.0, kValue.f16.negative.min, 2.0, 3.0]], expected: kUnboundedBounds }, + { input: [[kValue.f16.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f16.positive.max, -2.0, -3.0]], expected: kUnboundedEndpoints }, + { input: [[kValue.f16.positive.max, 1.0, 2.0, 3.0], [1.0, kValue.f16.negative.min, 2.0, 3.0]], expected: kUnboundedEndpoints }, ] as VectorPairToIntervalCase[], } as const; @@ -6052,7 +6104,7 @@ g.test('dotInterval') { input: [[1.0, 1.0], [1.0, 1.0]], expected: 2.0 }, { input: [[-1.0, -1.0], [-1.0, -1.0]], expected: 2.0 }, { input: [[-1.0, 1.0], [1.0, -1.0]], expected: -2.0 }, - { input: [[0.1, 0.0], [1.0, 0.0]], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']}, // correclt rounded of 0.1 + { input: [[0.1, 0.0], [1.0, 0.0]], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']}, // correctly rounded of 0.1 // vec3 { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: 1.0 }, @@ -6061,7 +6113,7 @@ g.test('dotInterval') { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: 3.0 }, { input: [[-1.0, -1.0, -1.0], [-1.0, -1.0, -1.0]], expected: 3.0 }, { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: -1.0 }, - { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']}, // correclt rounded of 0.1 + { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']}, // correctly rounded of 0.1 // vec4 { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: 1.0 }, @@ -6076,12 +6128,12 @@ g.test('dotInterval') ...kDotIntervalCases[p.trait], // Test that going out of bounds in the intermediate calculations is caught correctly. - { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0]], expected: kUnboundedBounds }, - { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds }, - { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0]], expected: kUnboundedBounds }, - { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds }, - { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds }, - { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds }, + { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0]], expected: kUnboundedEndpoints }, + { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0]], expected: kUnboundedEndpoints }, + { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0]], expected: kUnboundedEndpoints }, + { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0]], expected: kUnboundedEndpoints }, + { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kUnboundedEndpoints }, + { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kUnboundedEndpoints }, ]; }) ) @@ -6098,54 +6150,54 @@ g.test('dotInterval') interface VectorToVectorCase { input: number[]; - expected: (number | IntervalBounds)[]; + expected: (number | IntervalEndpoints)[]; } // prettier-ignore const kNormalizeIntervalCases = { f32: [ // vec2 - {input: [1.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0] - {input: [0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~1.0] - {input: [-1.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0] - {input: [1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe6_a09d_5000_0000n), reinterpretU64AsF64(0x3fe6_a09f_9000_0000n)], [reinterpretU64AsF64(0x3fe6_a09d_5000_0000n), reinterpretU64AsF64(0x3fe6_a09f_9000_0000n)]] }, // [ ~1/√2, ~1/√2] + { input: [1.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0] + { input: [0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~1.0] + { input: [-1.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0] + { input: [1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe6_a09d_5000_0000n), reinterpretU64AsF64(0x3fe6_a09f_9000_0000n)], [reinterpretU64AsF64(0x3fe6_a09d_5000_0000n), reinterpretU64AsF64(0x3fe6_a09f_9000_0000n)]] }, // [ ~1/√2, ~1/√2] // vec3 - {input: [1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0] - {input: [0.0, 1.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0] - {input: [0.0, 0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0] - {input: [-1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0] - {input: [1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)], [reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)], [reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)]] }, // [ ~1/√3, ~1/√3, ~1/√3] + { input: [1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0] + { input: [0.0, 1.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0] + { input: [0.0, 0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0] + { input: [-1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0] + { input: [1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)], [reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)], [reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)]] }, // [ ~1/√3, ~1/√3, ~1/√3] // vec4 - {input: [1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] - {input: [0.0, 1.0, 0.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0] - {input: [0.0, 0.0, 1.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0] - {input: [0.0, 0.0, 0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0] - {input: [-1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] - {input: [1.0, 1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)]] }, // [ ~1/√4, ~1/√4, ~1/√4] + { input: [1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] + { input: [0.0, 1.0, 0.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0] + { input: [0.0, 0.0, 1.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0] + { input: [0.0, 0.0, 0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0] + { input: [-1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] + { input: [1.0, 1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)]] }, // [ ~1/√4, ~1/√4, ~1/√4] ] as VectorToVectorCase[], f16: [ // vec2 - {input: [1.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0] - {input: [0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~1.0] - {input: [-1.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0] - {input: [1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe6_7e00_0000_0000n), reinterpretU64AsF64(0x3fe6_c600_0000_0000n)], [reinterpretU64AsF64(0x3fe6_7e00_0000_0000n), reinterpretU64AsF64(0x3fe6_c600_0000_0000n)]] }, // [ ~1/√2, ~1/√2] + { input: [1.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0] + { input: [0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~1.0] + { input: [-1.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0] + { input: [1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe6_7e00_0000_0000n), reinterpretU64AsF64(0x3fe6_c600_0000_0000n)], [reinterpretU64AsF64(0x3fe6_7e00_0000_0000n), reinterpretU64AsF64(0x3fe6_c600_0000_0000n)]] }, // [ ~1/√2, ~1/√2] // vec3 - {input: [1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0] - {input: [0.0, 1.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~1.0, ~0.0] - {input: [0.0, 0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0] - {input: [-1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0] - {input: [1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)], [reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)], [reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)]] }, // [ ~1/√3, ~1/√3, ~1/√3] + { input: [1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0] + { input: [0.0, 1.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~1.0, ~0.0] + { input: [0.0, 0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0] + { input: [-1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0] + { input: [1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)], [reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)], [reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)]] }, // [ ~1/√3, ~1/√3, ~1/√3] // vec4 - {input: [1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] - {input: [0.0, 1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0] - {input: [0.0, 0.0, 1.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0] - {input: [0.0, 0.0, 0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0] - {input: [-1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] - {input: [1.0, 1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)]] }, // [ ~1/√4, ~1/√4, ~1/√4] + { input: [1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] + { input: [0.0, 1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0] + { input: [0.0, 0.0, 1.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0] + { input: [0.0, 0.0, 0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0] + { input: [-1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] + { input: [1.0, 1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)]] }, // [ ~1/√4, ~1/√4, ~1/√4] ] as VectorToVectorCase[], } as const; @@ -6154,7 +6206,20 @@ g.test('normalizeInterval') u .combine('trait', ['f32', 'f16'] as const) .beginSubcases() - .expandWithParams<VectorToVectorCase>(p => kNormalizeIntervalCases[p.trait]) + .expandWithParams<VectorToVectorCase>(p => { + const trait = FP[p.trait]; + const constants = trait.constants(); + // prettier-ignore + return [ + ...kNormalizeIntervalCases[p.trait], + + // Very small vectors go OOB due to division + { input: [constants.positive.subnormal.max, constants.positive.subnormal.max], expected: [kUnboundedEndpoints, kUnboundedEndpoints], }, + + // Very large vectors go OOB due to overflow + { input: [constants.positive.max, constants.positive.max], expected: [kUnboundedEndpoints, kUnboundedEndpoints], }, + ]; + }) ) .fn(t => { const x = t.params.input; @@ -6169,7 +6234,7 @@ g.test('normalizeInterval') interface VectorPairToVectorCase { input: [number[], number[]]; - expected: (number | IntervalBounds)[]; + expected: (number | IntervalEndpoints)[]; } // prettier-ignore @@ -6218,30 +6283,12 @@ const kCrossIntervalCases = { ] }, ] as VectorPairToVectorCase[], - abstract: [ - { input: [ - [kValue.f64.positive.subnormal.max, kValue.f64.negative.subnormal.max, kValue.f64.negative.subnormal.min], - [kValue.f64.negative.subnormal.min, kValue.f64.positive.subnormal.min, kValue.f64.negative.subnormal.max] - ], - expected: [0.0, 0.0, 0.0] - }, - { input: [ - [0.1, -0.1, -0.1], - [-0.1, 0.1, -0.1] - ], - expected: [ - reinterpretU64AsF64(0x3f94_7ae1_47ae_147cn), // ~0.02 - reinterpretU64AsF64(0x3f94_7ae1_47ae_147cn), // ~0.02 - 0.0 - ] - }, - ] as VectorPairToVectorCase[], } as const; g.test('crossInterval') .params(u => u - .combine('trait', ['f32', 'f16', 'abstract'] as const) + .combine('trait', ['f32', 'f16'] as const) .beginSubcases() .expandWithParams<VectorPairToVectorCase>(p => { const trait = FP[p.trait]; @@ -6261,6 +6308,9 @@ g.test('crossInterval') { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: [2.0, 2.0, 0.0] }, { input: [[1.0, 2, 3], [1.0, 5.0, 7.0]], expected: [-1, -4, 3] }, ...kCrossIntervalCases[p.trait], + + // OOB + { input: [[constants.positive.max, 1.0, 1.0], [1.0, constants.positive.max, -1.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, ]; }) ) @@ -6320,6 +6370,7 @@ g.test('reflectInterval') { input: [[0.0, 1.0], [1.0, 0.0]], expected: [0.0, 1.0] }, { input: [[1.0, 1.0], [1.0, 1.0]], expected: [-3.0, -3.0] }, { input: [[-1.0, -1.0], [1.0, 1.0]], expected: [3.0, 3.0] }, + // vec3s { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [-1.0, 0.0, 0.0] }, { input: [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], expected: [0.0, 1.0, 0.0] }, @@ -6328,6 +6379,7 @@ g.test('reflectInterval') { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], expected: [1.0, 0.0, 0.0] }, { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [-5.0, -5.0, -5.0] }, { input: [[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]], expected: [5.0, 5.0, 5.0] }, + // vec4s { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [-1.0, 0.0, 0.0, 0.0] }, { input: [[0.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [0.0, 1.0, 0.0, 0.0] }, @@ -6337,16 +6389,17 @@ g.test('reflectInterval') { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [1.0, 0.0, 0.0, 0.0] }, { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]], expected: [1.0, 0.0, 0.0, 0.0] }, { input: [[-1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0]], expected: [7.0, 7.0, 7.0, 7.0] }, - // Test that dot going OOB bounds in the intermediate calculations propagates - { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, + + // Test that dot going OOB in the intermediate calculations propagates + { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, // Test that post-dot going OOB propagates - { input: [[constants.positive.max, 1.0, 2.0, 3.0], [-1.0, constants.positive.max, -2.0, -3.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, + { input: [[constants.positive.max, 1.0, 2.0, 3.0], [-1.0, constants.positive.max, -2.0, -3.0]], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, ]; }) ) @@ -6365,7 +6418,7 @@ g.test('reflectInterval') interface MatrixToScalarCase { input: number[][]; - expected: number | IntervalBounds; + expected: number | IntervalEndpoints; } g.test('determinantInterval') @@ -6480,7 +6533,7 @@ g.test('determinantInterval') interface MatrixToMatrixCase { input: number[][]; - expected: (number | IntervalBounds)[][]; + expected: (number | IntervalEndpoints)[][]; } g.test('transposeInterval') @@ -6634,7 +6687,7 @@ g.test('transposeInterval') interface MatrixPairToMatrixCase { input: [number[][], number[][]]; - expected: (number | IntervalBounds)[][]; + expected: (number | IntervalEndpoints)[][]; } g.test('additionMatrixMatrixInterval') @@ -6642,184 +6695,205 @@ g.test('additionMatrixMatrixInterval') u .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() - .combineWithParams<MatrixPairToMatrixCase>([ - // Only testing that different shapes of matrices are handled correctly - // here, to reduce test duplication. - // additionMatrixMatrixInterval uses AdditionIntervalOp for calculating intervals, - // so the testing for additionInterval covers the actual interval - // calculations. - { - input: [ - [ - [1, 2], - [3, 4], + .expandWithParams<MatrixPairToMatrixCase>(p => { + const trait = FP[p.trait]; + const constants = trait.constants(); + return [ + // Only testing that different shapes of matrices are handled correctly + // here, to reduce test duplication. + // additionMatrixMatrixInterval uses AdditionIntervalOp for calculating intervals, + // so the testing for additionInterval covers the actual interval + // calculations. + { + input: [ + [ + [1, 2], + [3, 4], + ], + [ + [10, 20], + [30, 40], + ], ], - [ - [10, 20], - [30, 40], + expected: [ + [11, 22], + [33, 44], ], - ], - expected: [ - [11, 22], - [33, 44], - ], - }, - { - input: [ - [ - [1, 2], - [3, 4], - [5, 6], + }, + { + input: [ + [ + [1, 2], + [3, 4], + [5, 6], + ], + [ + [10, 20], + [30, 40], + [50, 60], + ], ], - [ - [10, 20], - [30, 40], - [50, 60], + expected: [ + [11, 22], + [33, 44], + [55, 66], ], - ], - expected: [ - [11, 22], - [33, 44], - [55, 66], - ], - }, - { - input: [ - [ - [1, 2], - [3, 4], - [5, 6], - [7, 8], + }, + { + input: [ + [ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + ], + [ + [10, 20], + [30, 40], + [50, 60], + [70, 80], + ], ], - [ - [10, 20], - [30, 40], - [50, 60], - [70, 80], + expected: [ + [11, 22], + [33, 44], + [55, 66], + [77, 88], ], - ], - expected: [ - [11, 22], - [33, 44], - [55, 66], - [77, 88], - ], - }, - { - input: [ - [ - [1, 2, 3], - [4, 5, 6], + }, + { + input: [ + [ + [1, 2, 3], + [4, 5, 6], + ], + [ + [10, 20, 30], + [40, 50, 60], + ], ], - [ - [10, 20, 30], - [40, 50, 60], + expected: [ + [11, 22, 33], + [44, 55, 66], ], - ], - expected: [ - [11, 22, 33], - [44, 55, 66], - ], - }, - { - input: [ - [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], + }, + { + input: [ + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + [ + [10, 20, 30], + [40, 50, 60], + [70, 80, 90], + ], ], - [ - [10, 20, 30], - [40, 50, 60], - [70, 80, 90], + expected: [ + [11, 22, 33], + [44, 55, 66], + [77, 88, 99], ], - ], - expected: [ - [11, 22, 33], - [44, 55, 66], - [77, 88, 99], - ], - }, - { - input: [ - [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10, 11, 12], + }, + { + input: [ + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + ], + [ + [10, 20, 30], + [40, 50, 60], + [70, 80, 90], + [1000, 1100, 1200], + ], ], - [ - [10, 20, 30], - [40, 50, 60], - [70, 80, 90], - [1000, 1100, 1200], + expected: [ + [11, 22, 33], + [44, 55, 66], + [77, 88, 99], + [1010, 1111, 1212], ], - ], - expected: [ - [11, 22, 33], - [44, 55, 66], - [77, 88, 99], - [1010, 1111, 1212], - ], - }, - { - input: [ - [ - [1, 2, 3, 4], - [5, 6, 7, 8], + }, + { + input: [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + ], + [ + [10, 20, 30, 40], + [50, 60, 70, 80], + ], ], - [ - [10, 20, 30, 40], - [50, 60, 70, 80], + expected: [ + [11, 22, 33, 44], + [55, 66, 77, 88], ], - ], - expected: [ - [11, 22, 33, 44], - [55, 66, 77, 88], - ], - }, - { - input: [ - [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], + }, + { + input: [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ], + [ + [10, 20, 30, 40], + [50, 60, 70, 80], + [90, 1000, 1100, 1200], + ], ], - [ - [10, 20, 30, 40], - [50, 60, 70, 80], - [90, 1000, 1100, 1200], + expected: [ + [11, 22, 33, 44], + [55, 66, 77, 88], + [99, 1010, 1111, 1212], ], - ], - expected: [ - [11, 22, 33, 44], - [55, 66, 77, 88], - [99, 1010, 1111, 1212], - ], - }, - { - input: [ - [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - [13, 14, 15, 16], + }, + { + input: [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + ], + [ + [10, 20, 30, 40], + [50, 60, 70, 80], + [90, 1000, 1100, 1200], + [1300, 1400, 1500, 1600], + ], ], - [ - [10, 20, 30, 40], - [50, 60, 70, 80], - [90, 1000, 1100, 1200], - [1300, 1400, 1500, 1600], + expected: [ + [11, 22, 33, 44], + [55, 66, 77, 88], + [99, 1010, 1111, 1212], + [1313, 1414, 1515, 1616], ], - ], - expected: [ - [11, 22, 33, 44], - [55, 66, 77, 88], - [99, 1010, 1111, 1212], - [1313, 1414, 1515, 1616], - ], - }, - ]) + }, + // Test the OOB is handled component-wise + { + input: [ + [ + [constants.positive.max, 2], + [3, 4], + ], + [ + [constants.positive.max, 20], + [30, 40], + ], + ], + expected: [ + [kUnboundedEndpoints, 22], + [33, 44], + ], + }, + ]; + }) ) .fn(t => { const [x, y] = t.params.input; @@ -6839,184 +6913,205 @@ g.test('subtractionMatrixMatrixInterval') u .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() - .combineWithParams<MatrixPairToMatrixCase>([ - // Only testing that different shapes of matrices are handled correctly - // here, to reduce test duplication. - // subtractionMatrixMatrixInterval uses AdditionIntervalOp for calculating intervals, - // so the testing for subtractionInterval covers the actual interval - // calculations. - { - input: [ - [ - [1, 2], - [3, 4], + .expandWithParams<MatrixPairToMatrixCase>(p => { + const trait = FP[p.trait]; + const constants = trait.constants(); + return [ + // Only testing that different shapes of matrices are handled correctly + // here, to reduce test duplication. + // subtractionMatrixMatrixInterval uses AdditionIntervalOp for calculating intervals, + // so the testing for subtractionInterval covers the actual interval + // calculations. + { + input: [ + [ + [1, 2], + [3, 4], + ], + [ + [-10, -20], + [-30, -40], + ], ], - [ - [-10, -20], - [-30, -40], + expected: [ + [11, 22], + [33, 44], ], - ], - expected: [ - [11, 22], - [33, 44], - ], - }, - { - input: [ - [ - [1, 2], - [3, 4], - [5, 6], + }, + { + input: [ + [ + [1, 2], + [3, 4], + [5, 6], + ], + [ + [-10, -20], + [-30, -40], + [-50, -60], + ], ], - [ - [-10, -20], - [-30, -40], - [-50, -60], + expected: [ + [11, 22], + [33, 44], + [55, 66], ], - ], - expected: [ - [11, 22], - [33, 44], - [55, 66], - ], - }, - { - input: [ - [ - [1, 2], - [3, 4], - [5, 6], - [7, 8], + }, + { + input: [ + [ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + ], + [ + [-10, -20], + [-30, -40], + [-50, -60], + [-70, -80], + ], ], - [ - [-10, -20], - [-30, -40], - [-50, -60], - [-70, -80], + expected: [ + [11, 22], + [33, 44], + [55, 66], + [77, 88], ], - ], - expected: [ - [11, 22], - [33, 44], - [55, 66], - [77, 88], - ], - }, - { - input: [ - [ - [1, 2, 3], - [4, 5, 6], + }, + { + input: [ + [ + [1, 2, 3], + [4, 5, 6], + ], + [ + [-10, -20, -30], + [-40, -50, -60], + ], ], - [ - [-10, -20, -30], - [-40, -50, -60], + expected: [ + [11, 22, 33], + [44, 55, 66], ], - ], - expected: [ - [11, 22, 33], - [44, 55, 66], - ], - }, - { - input: [ - [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], + }, + { + input: [ + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + [ + [-10, -20, -30], + [-40, -50, -60], + [-70, -80, -90], + ], ], - [ - [-10, -20, -30], - [-40, -50, -60], - [-70, -80, -90], + expected: [ + [11, 22, 33], + [44, 55, 66], + [77, 88, 99], ], - ], - expected: [ - [11, 22, 33], - [44, 55, 66], - [77, 88, 99], - ], - }, - { - input: [ - [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10, 11, 12], + }, + { + input: [ + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + ], + [ + [-10, -20, -30], + [-40, -50, -60], + [-70, -80, -90], + [-1000, -1100, -1200], + ], ], - [ - [-10, -20, -30], - [-40, -50, -60], - [-70, -80, -90], - [-1000, -1100, -1200], + expected: [ + [11, 22, 33], + [44, 55, 66], + [77, 88, 99], + [1010, 1111, 1212], ], - ], - expected: [ - [11, 22, 33], - [44, 55, 66], - [77, 88, 99], - [1010, 1111, 1212], - ], - }, - { - input: [ - [ - [1, 2, 3, 4], - [5, 6, 7, 8], + }, + { + input: [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + ], + [ + [-10, -20, -30, -40], + [-50, -60, -70, -80], + ], ], - [ - [-10, -20, -30, -40], - [-50, -60, -70, -80], + expected: [ + [11, 22, 33, 44], + [55, 66, 77, 88], ], - ], - expected: [ - [11, 22, 33, 44], - [55, 66, 77, 88], - ], - }, - { - input: [ - [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], + }, + { + input: [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ], + [ + [-10, -20, -30, -40], + [-50, -60, -70, -80], + [-90, -1000, -1100, -1200], + ], ], - [ - [-10, -20, -30, -40], - [-50, -60, -70, -80], - [-90, -1000, -1100, -1200], + expected: [ + [11, 22, 33, 44], + [55, 66, 77, 88], + [99, 1010, 1111, 1212], ], - ], - expected: [ - [11, 22, 33, 44], - [55, 66, 77, 88], - [99, 1010, 1111, 1212], - ], - }, - { - input: [ - [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - [13, 14, 15, 16], + }, + { + input: [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + ], + [ + [-10, -20, -30, -40], + [-50, -60, -70, -80], + [-90, -1000, -1100, -1200], + [-1300, -1400, -1500, -1600], + ], ], - [ - [-10, -20, -30, -40], - [-50, -60, -70, -80], - [-90, -1000, -1100, -1200], - [-1300, -1400, -1500, -1600], + expected: [ + [11, 22, 33, 44], + [55, 66, 77, 88], + [99, 1010, 1111, 1212], + [1313, 1414, 1515, 1616], ], - ], - expected: [ - [11, 22, 33, 44], - [55, 66, 77, 88], - [99, 1010, 1111, 1212], - [1313, 1414, 1515, 1616], - ], - }, - ]) + }, + // Test the OOB is handled component-wise + { + input: [ + [ + [constants.positive.max, 2], + [3, 4], + ], + [ + [constants.negative.min, -20], + [-30, -40], + ], + ], + expected: [ + [kUnboundedEndpoints, 22], + [33, 44], + ], + }, + ]; + }) ) .fn(t => { const [x, y] = t.params.input; @@ -7577,7 +7672,7 @@ g.test('multiplicationMatrixMatrixInterval') interface MatrixScalarToMatrixCase { matrix: number[][]; scalar: number; - expected: (number | IntervalBounds)[][]; + expected: (number | IntervalEndpoints)[][]; } const kMultiplicationMatrixScalarIntervalCases = { @@ -7609,14 +7704,30 @@ const kMultiplicationMatrixScalarIntervalCases = { ], }, ] as MatrixScalarToMatrixCase[], + abstract: [ + // From https://github.com/gpuweb/cts/issues/3044 + { + matrix: [ + [kValue.f64.negative.min, 0], + [0, 0], + ], + scalar: kValue.f64.negative.subnormal.min, + expected: [ + [[0, reinterpretU64AsF64(0x400ffffffffffffdn)], 0], // [[0, 3.9999995...], 0], + [0, 0], + ], + }, + ] as MatrixScalarToMatrixCase[], } as const; g.test('multiplicationMatrixScalarInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['f32', 'f16', 'abstract'] as const) .beginSubcases() .expandWithParams<MatrixScalarToMatrixCase>(p => { + const trait = FP[p.trait]; + const constants = trait.constants(); // Primarily testing that different shapes of matrices are handled correctly // here, to reduce test duplication. Additional testing for edge case // discovered in https://github.com/gpuweb/cts/issues/3044. @@ -7743,6 +7854,18 @@ g.test('multiplicationMatrixScalarInterval') ], }, ...kMultiplicationMatrixScalarIntervalCases[p.trait], + // Test that OOB is component-wise + { + matrix: [ + [1, 2], + [constants.positive.max, 4], + ], + scalar: 10, + expected: [ + [10, 20], + [kUnboundedEndpoints, 40], + ], + }, ]; }) ) @@ -7766,7 +7889,7 @@ g.test('multiplicationMatrixScalarInterval') interface MatrixVectorToVectorCase { matrix: number[][]; vector: number[]; - expected: (number | IntervalBounds)[]; + expected: (number | IntervalEndpoints)[]; } g.test('multiplicationMatrixVectorInterval') @@ -7883,7 +8006,7 @@ g.test('multiplicationMatrixVectorInterval') interface VectorMatrixToVectorCase { vector: number[]; matrix: number[][]; - expected: (number | IntervalBounds)[]; + expected: (number | IntervalEndpoints)[]; } g.test('multiplicationVectorMatrixInterval') @@ -7897,8 +8020,8 @@ g.test('multiplicationVectorMatrixInterval') // multiplicationVectorMatrixInterval uses DotIntervalOp for calculating // intervals, so the testing for dotInterval covers the actual interval // calculations. - // Keep all expected result integer no larger than 2047 to ensure that all result is exactly - // represeantable in both f32 and f16. + // Keep all expected result integer no larger than 2047 to ensure that + // all result is exactly representable in both f32 and f16. { vector: [1, 2], matrix: [ @@ -8002,7 +8125,7 @@ g.test('multiplicationVectorMatrixInterval') interface FaceForwardCase { input: [number[], number[], number[]]; - expected: ((number | IntervalBounds)[] | undefined)[]; + expected: ((number | IntervalEndpoints)[] | undefined)[]; } g.test('faceForwardIntervals') @@ -8081,8 +8204,8 @@ g.test('faceForwardIntervals') interface ModfCase { input: number; - fract: number | IntervalBounds; - whole: number | IntervalBounds; + fract: number | IntervalEndpoints; + whole: number | IntervalEndpoints; } g.test('modfInterval') @@ -8135,18 +8258,18 @@ g.test('modfInterval') interface RefractCase { input: [number[], number[], number]; - expected: (number | IntervalBounds)[]; + expected: (number | IntervalEndpoints)[]; } // Scope for refractInterval tests so that they can have constants for magic // numbers that don't pollute the global namespace or have unwieldy long names. { - const kNegativeOneBounds = { + const kNegativeOneEndpoints = { f32: [ reinterpretU64AsF64(0xbff0_0000_c000_0000n), reinterpretU64AsF64(0xbfef_ffff_4000_0000n), - ] as IntervalBounds, - f16: [reinterpretU16AsF16(0xbc06), reinterpretU16AsF16(0xbbfa)] as IntervalBounds, + ] as IntervalEndpoints, + f16: [reinterpretU16AsF16(0xbc06), reinterpretU16AsF16(0xbbfa)] as IntervalEndpoints, } as const; // prettier-ignore @@ -8178,7 +8301,7 @@ interface RefractCase { // vec4 // x = [1, -2, 3, -4], y = [-5, 6, -7, 8], z = 9, // dot(y, x) = -71, k = 1.0 - 9 * 9 * (1.0 - 71 * 71) = 408241 overflow f16. - { input: [[1, -2, 3, -4], [-5, 6, -7, 8], 9], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, + { input: [[1, -2, 3, -4], [-5, 6, -7, 8], 9], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, // x = [1, -2, 3, -4], y = [-5, 4, -3, 2], z = 2.5, // dot(y, x) = -30, k = 1.0 - 2.5 * 2.5 * (1.0 - 30 * 30) = 5619.75. // a = z * dot(y, x) + sqrt(k) = ~-0.035, result is about z * x - a * y = [~2.325, ~-4.86, ~7.4025, ~-9.93] @@ -8205,23 +8328,23 @@ interface RefractCase { { input: [[1, 1], [0.1, 0], 10], expected: [0, 0] }, // k contains 0 - { input: [[1, 1], [0.1, 0], 1.005038], expected: [kUnboundedBounds, kUnboundedBounds] }, + { input: [[1, 1], [0.1, 0], 1.005038], expected: [kUnboundedEndpoints, kUnboundedEndpoints] }, // k > 0 // vec2 - { input: [[1, 1], [1, 0], 1], expected: [kNegativeOneBounds[p.trait], 1] }, + { input: [[1, 1], [1, 0], 1], expected: [kNegativeOneEndpoints[p.trait], 1] }, // vec3 - { input: [[1, 1, 1], [1, 0, 0], 1], expected: [kNegativeOneBounds[p.trait], 1, 1] }, + { input: [[1, 1, 1], [1, 0, 0], 1], expected: [kNegativeOneEndpoints[p.trait], 1, 1] }, // vec4 - { input: [[1, 1, 1, 1], [1, 0, 0, 0], 1], expected: [kNegativeOneBounds[p.trait], 1, 1, 1] }, - - // Test that dot going OOB bounds in the intermediate calculations propagates - { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, - { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] }, + { input: [[1, 1, 1, 1], [1, 0, 0, 0], 1], expected: [kNegativeOneEndpoints[p.trait], 1, 1, 1] }, + + // Test that dot going OOB in the intermediate calculations propagates + { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0], 1], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0], 1], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, + { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedEndpoints, kUnboundedEndpoints, kUnboundedEndpoints] }, ]; }) ) diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts index 357c574281..d84299d993 100644 --- a/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts @@ -22,8 +22,8 @@ import { correctlyRoundedF32, FlushMode, frexp, - fullF16Range, - fullF32Range, + scalarF16Range, + scalarF32Range, fullI32Range, lerp, linearRange, @@ -36,6 +36,7 @@ import { oneULPF64, lerpBigInt, linearRangeBigInt, + biasedRangeBigInt, } from '../webgpu/util/math.js'; import { reinterpretU16AsF16, @@ -1525,6 +1526,41 @@ g.test('linearRangeBigInt') ); }); +g.test('biasedRangeBigInt') + .paramsSimple<rangeBigIntCase>( + // prettier-ignore + [ + { a: 0n, b: 0n, num_steps: 10, result: new Array<bigint>(10).fill(0n) }, + { a: 10n, b: 10n, num_steps: 10, result: new Array<bigint>(10).fill(10n) }, + { a: 0n, b: 10n, num_steps: 1, result: [0n] }, + { a: 10n, b: 0n, num_steps: 1, result: [10n] }, + { a: 0n, b: 10n, num_steps: 11, result: [0n, 0n, 0n, 0n, 1n, 2n, 3n, 4n, 6n, 8n, 10n] }, + { a: 10n, b: 0n, num_steps: 11, result: [10n, 10n, 10n, 10n, 9n, 8n, 7n, 6n, 4n, 2n, 0n] }, + { a: 0n, b: 1000n, num_steps: 11, result: [0n, 9n, 39n, 89n, 159n, 249n, 359n, 489n, 639n, 809n, 1000n] }, + { a: 1000n, b: 0n, num_steps: 11, result: [1000n, 991n, 961n, 911n, 841n, 751n, 641n, 511n, 361n, 191n, 0n] }, + { a: 1n, b: 5n, num_steps: 5, result: [1n, 1n, 2n, 3n, 5n] }, + { a: 5n, b: 1n, num_steps: 5, result: [5n, 5n, 4n, 3n, 1n] }, + { a: 0n, b: 10n, num_steps: 5, result: [0n, 0n, 2n, 5n, 10n] }, + { a: 10n, b: 0n, num_steps: 5, result: [10n, 10n, 8n, 5n, 0n] }, + { a: -10n, b: 10n, num_steps: 11, result: [-10n, -10n, -10n, -10n, -8n, -6n, -4n, -2n, 2n, 6n, 10n] }, + { a: 10n, b: -10n, num_steps: 11, result: [10n, 10n, 10n, 10n, 8n, 6n, 4n, 2n, -2n, -6n, -10n] }, + { a: -10n, b: 0n, num_steps: 11, result: [-10n, -10n, -10n, -10n, -9n, -8n, -7n, -6n, -4n, -2n, -0n] }, + { a: 0n, b: -10n, num_steps: 11, result: [0n, 0n, 0n, 0n, -1n, -2n, -3n, -4n, -6n, -8n, -10n] }, + ] + ) + .fn(test => { + const a = test.params.a; + const b = test.params.b; + const num_steps = test.params.num_steps; + const got = biasedRangeBigInt(a, b, num_steps); + const expect = test.params.result; + + test.expect( + objectEquals(got, expect), + `biasedRangeBigInt(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}` + ); + }); + interface fullF32RangeCase { neg_norm: number; neg_sub: number; @@ -1557,7 +1593,7 @@ g.test('fullF32Range') const neg_sub = test.params.neg_sub; const pos_sub = test.params.pos_sub; const pos_norm = test.params.pos_norm; - const got = fullF32Range({ neg_norm, neg_sub, pos_sub, pos_norm }); + const got = scalarF32Range({ neg_norm, neg_sub, pos_sub, pos_norm }); const expect = test.params.expect; test.expect( @@ -1598,7 +1634,7 @@ g.test('fullF16Range') const neg_sub = test.params.neg_sub; const pos_sub = test.params.pos_sub; const pos_norm = test.params.pos_norm; - const got = fullF16Range({ neg_norm, neg_sub, pos_sub, pos_norm }); + const got = scalarF16Range({ neg_norm, neg_sub, pos_sub, pos_norm }); const expect = test.params.expect; test.expect( diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/parse_imports.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/parse_imports.spec.ts new file mode 100644 index 0000000000..0efdc0d171 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/parse_imports.spec.ts @@ -0,0 +1,79 @@ +export const description = ` +Test for "parseImports" utility. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { parseImports } from '../common/util/parse_imports.js'; + +import { UnitTest } from './unit_test.js'; + +class F extends UnitTest { + test(content: string, expect: string[]): void { + const got = parseImports('a/b/c.js', content); + const expectJoined = expect.join('\n'); + const gotJoined = got.join('\n'); + this.expect( + expectJoined === gotJoined, + ` +expected: ${expectJoined} +got: ${gotJoined}` + ); + } +} + +export const g = makeTestGroup(F); + +g.test('empty').fn(t => { + t.test(``, []); + t.test(`\n`, []); + t.test(`\n\n`, []); +}); + +g.test('simple').fn(t => { + t.test(`import 'x/y/z.js';`, ['a/b/x/y/z.js']); + t.test(`import * as blah from 'x/y/z.js';`, ['a/b/x/y/z.js']); + t.test(`import { blah } from 'x/y/z.js';`, ['a/b/x/y/z.js']); +}); + +g.test('multiple').fn(t => { + t.test( + ` +blah blah blah +import 'x/y/z.js'; +more blah +import * as blah from 'm/n/o.js'; +extra blah +import { blah } from '../h.js'; +ending with blah +`, + ['a/b/x/y/z.js', 'a/b/m/n/o.js', 'a/h.js'] + ); +}); + +g.test('multiline').fn(t => { + t.test( + `import { + blah +} from 'x/y/z.js';`, + ['a/b/x/y/z.js'] + ); + t.test( + `import { + blahA, + blahB, +} from 'x/y/z.js';`, + ['a/b/x/y/z.js'] + ); +}); + +g.test('file_characters').fn(t => { + t.test(`import '01234_56789.js';`, ['a/b/01234_56789.js']); +}); + +g.test('relative_paths').fn(t => { + t.test(`import '../x.js';`, ['a/x.js']); + t.test(`import '../x/y.js';`, ['a/x/y.js']); + t.test(`import '../../x.js';`, ['x.js']); + t.test(`import '../../../x.js';`, ['../x.js']); + t.test(`import '../../../../x.js';`, ['../../x.js']); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts index 9717ba3ecf..ea6ed5e42f 100644 --- a/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts @@ -16,6 +16,8 @@ import { } from '../webgpu/util/compare.js'; import { kValue } from '../webgpu/util/constants.js'; import { + abstractFloat, + abstractInt, bool, deserializeValue, f16, @@ -61,6 +63,18 @@ g.test('value').fn(t => { u8(kValue.u8.max - 1), u8(kValue.u8.max - 0), + abstractInt(kValue.i64.negative.min), + abstractInt(kValue.i64.negative.min + 1n), + abstractInt(kValue.i64.negative.min + 2n), + abstractInt(kValue.i64.negative.max - 2n), + abstractInt(kValue.i64.negative.max - 1n), + abstractInt(kValue.i64.positive.min), + abstractInt(kValue.i64.positive.min + 1n), + abstractInt(kValue.i64.positive.min + 2n), + abstractInt(kValue.i64.positive.max - 2n), + abstractInt(kValue.i64.positive.max - 1n), + abstractInt(kValue.i64.positive.max), + i32(kValue.i32.negative.min + 0), i32(kValue.i32.negative.min + 1), i32(kValue.i32.negative.min + 2), @@ -97,6 +111,21 @@ g.test('value').fn(t => { i8(kValue.i8.positive.max - 1), i8(kValue.i8.positive.max - 0), + abstractFloat(0), + abstractFloat(-0), + abstractFloat(1), + abstractFloat(-1), + abstractFloat(0.5), + abstractFloat(-0.5), + abstractFloat(kValue.f64.positive.max), + abstractFloat(kValue.f64.positive.min), + abstractFloat(kValue.f64.positive.subnormal.max), + abstractFloat(kValue.f64.positive.subnormal.min), + abstractFloat(kValue.f64.negative.subnormal.max), + abstractFloat(kValue.f64.negative.subnormal.min), + abstractFloat(kValue.f64.positive.infinity), + abstractFloat(kValue.f64.negative.infinity), + f32(0), f32(-0), f32(1), @@ -139,6 +168,13 @@ g.test('value').fn(t => { [0.0, 1.0], [2.0, 3.0], ], + abstractFloat + ), + toMatrix( + [ + [0.0, 1.0], + [2.0, 3.0], + ], f32 ), toMatrix( @@ -153,6 +189,13 @@ g.test('value').fn(t => { [0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0], ], + abstractFloat + ), + toMatrix( + [ + [0.0, 1.0, 2.0, 3.0], + [4.0, 5.0, 6.0, 7.0], + ], f32 ), toMatrix( @@ -169,6 +212,14 @@ g.test('value').fn(t => { [3.0, 4.0, 5.0], [6.0, 7.0, 8.0], ], + abstractFloat + ), + toMatrix( + [ + [0.0, 1.0, 2.0], + [3.0, 4.0, 5.0], + [6.0, 7.0, 8.0], + ], f32 ), toMatrix( @@ -186,6 +237,15 @@ g.test('value').fn(t => { [4.0, 5.0], [6.0, 7.0], ], + abstractFloat + ), + toMatrix( + [ + [0.0, 1.0], + [2.0, 3.0], + [4.0, 5.0], + [6.0, 7.0], + ], f32 ), toMatrix( @@ -204,6 +264,15 @@ g.test('value').fn(t => { [8.0, 9.0, 10.0, 11.0], [12.0, 13.0, 14.0, 15.0], ], + abstractFloat + ), + toMatrix( + [ + [0.0, 1.0, 2.0, 3.0], + [4.0, 5.0, 6.0, 7.0], + [8.0, 9.0, 10.0, 11.0], + [12.0, 13.0, 14.0, 15.0], + ], f32 ), ]) { diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts index f1e6971a74..c126832b8d 100644 --- a/dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts @@ -4,7 +4,6 @@ Test for texture_ok utils. import { makeTestGroup } from '../common/framework/test_group.js'; import { typedArrayFromParam, typedArrayParam } from '../common/util/util.js'; -import { RegularTextureFormat } from '../webgpu/format_info.js'; import { TexelView } from '../webgpu/util/texture/texel_view.js'; import { findFailedPixels } from '../webgpu/util/texture/texture_ok.js'; @@ -30,103 +29,103 @@ g.test('findFailedPixels') u.combineWithParams([ // Sanity Check { - format: 'rgba8unorm' as RegularTextureFormat, + format: 'rgba8unorm', actual: typedArrayParam('Uint8Array', [0x00, 0x40, 0x80, 0xff]), expected: typedArrayParam('Uint8Array', [0x00, 0x40, 0x80, 0xff]), isSame: true, }, // Slightly different values { - format: 'rgba8unorm' as RegularTextureFormat, + format: 'rgba8unorm', actual: typedArrayParam('Uint8Array', [0x00, 0x40, 0x80, 0xff]), expected: typedArrayParam('Uint8Array', [0x00, 0x40, 0x81, 0xff]), isSame: false, }, // Different representations of the same value { - format: 'rgb9e5ufloat' as RegularTextureFormat, + format: 'rgb9e5ufloat', actual: typedArrayParam('Uint8Array', [0x78, 0x56, 0x34, 0x12]), expected: typedArrayParam('Uint8Array', [0xf0, 0xac, 0x68, 0x0c]), isSame: true, }, // Slightly different values { - format: 'rgb9e5ufloat' as RegularTextureFormat, + format: 'rgb9e5ufloat', actual: typedArrayParam('Uint8Array', [0x78, 0x56, 0x34, 0x12]), expected: typedArrayParam('Uint8Array', [0xf1, 0xac, 0x68, 0x0c]), isSame: false, }, // Test NaN === NaN { - format: 'r32float' as RegularTextureFormat, + format: 'r32float', actual: typedArrayParam('Float32Array', [parseFloat('abc')]), expected: typedArrayParam('Float32Array', [parseFloat('def')]), isSame: true, }, // Sanity Check { - format: 'r32float' as RegularTextureFormat, + format: 'r32float', actual: typedArrayParam('Float32Array', [1.23]), expected: typedArrayParam('Float32Array', [1.23]), isSame: true, }, // Slightly different values. { - format: 'r32float' as RegularTextureFormat, + format: 'r32float', actual: typedArrayParam('Uint32Array', [0x3f9d70a4]), expected: typedArrayParam('Uint32Array', [0x3f9d70a5]), isSame: false, }, // Slightly different { - format: 'rg11b10ufloat' as RegularTextureFormat, + format: 'rg11b10ufloat', actual: typedArrayParam('Uint32Array', [0x3ce]), expected: typedArrayParam('Uint32Array', [0x3cf]), isSame: false, }, // Positive.Infinity === Positive.Infinity (red) { - format: 'rg11b10ufloat' as RegularTextureFormat, + format: 'rg11b10ufloat', actual: typedArrayParam('Uint32Array', [0b11111000000]), expected: typedArrayParam('Uint32Array', [0b11111000000]), isSame: true, }, // Positive.Infinity === Positive.Infinity (green) { - format: 'rg11b10ufloat' as RegularTextureFormat, + format: 'rg11b10ufloat', actual: typedArrayParam('Uint32Array', [0b11111000000_00000000000]), expected: typedArrayParam('Uint32Array', [0b11111000000_00000000000]), isSame: true, }, // Positive.Infinity === Positive.Infinity (blue) { - format: 'rg11b10ufloat' as RegularTextureFormat, + format: 'rg11b10ufloat', actual: typedArrayParam('Uint32Array', [0b1111100000_00000000000_00000000000]), expected: typedArrayParam('Uint32Array', [0b1111100000_00000000000_00000000000]), isSame: true, }, // NaN === NaN (red) { - format: 'rg11b10ufloat' as RegularTextureFormat, + format: 'rg11b10ufloat', actual: typedArrayParam('Uint32Array', [0b11111000001]), expected: typedArrayParam('Uint32Array', [0b11111000010]), isSame: true, }, // NaN === NaN (green) { - format: 'rg11b10ufloat' as RegularTextureFormat, + format: 'rg11b10ufloat', actual: typedArrayParam('Uint32Array', [0b11111000100_00000000000]), expected: typedArrayParam('Uint32Array', [0b11111001000_00000000000]), isSame: true, }, // NaN === NaN (blue) { - format: 'rg11b10ufloat' as RegularTextureFormat, + format: 'rg11b10ufloat', actual: typedArrayParam('Uint32Array', [0b1111110000_00000000000_00000000000]), expected: typedArrayParam('Uint32Array', [0b1111101000_00000000000_00000000000]), isSame: true, }, - ]) + ] as const) ) .fn(t => { const { format, actual, expected, isSame } = t.params; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts index 314da6356e..51ab2303eb 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts @@ -311,6 +311,65 @@ g.test('limit,better_than_supported') t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits })); }); +g.test('limit,out_of_range') + .desc( + ` + Test that specifying limits that are out of range (<0, >MAX_SAFE_INTEGER, >2**31-2 for 32-bit + limits, =0 for alignment limits) produce the appropriate error (TypeError or OperationError). + ` + ) + .params(u => + u + .combine('limit', kLimits) + .beginSubcases() + .expand('value', function* () { + yield -(2 ** 64); + yield Number.MIN_SAFE_INTEGER - 3; + yield Number.MIN_SAFE_INTEGER - 1; + yield Number.MIN_SAFE_INTEGER; + yield -(2 ** 32); + yield -1; + yield 0; + yield 2 ** 32 - 2; + yield 2 ** 32 - 1; + yield 2 ** 32; + yield 2 ** 32 + 1; + yield 2 ** 32 + 2; + yield Number.MAX_SAFE_INTEGER; + yield Number.MAX_SAFE_INTEGER + 1; + yield Number.MAX_SAFE_INTEGER + 3; + yield 2 ** 64; + yield Number.MAX_VALUE; + }) + ) + .fn(async t => { + const { limit, value } = t.params; + + const gpu = getGPU(t.rec); + const adapter = await gpu.requestAdapter(); + assert(adapter !== null); + const limitInfo = getDefaultLimitsForAdapter(adapter)[limit]; + + const requiredLimits = { + [limit]: value, + }; + + const errorName = + value < 0 || value > Number.MAX_SAFE_INTEGER + ? 'TypeError' + : limitInfo.class === 'maximum' && value > adapter.limits[limit] + ? 'OperationError' + : limitInfo.class === 'alignment' && (value > 2 ** 31 || !isPowerOfTwo(value)) + ? 'OperationError' + : false; + + if (errorName) { + t.shouldReject(errorName, adapter.requestDevice({ requiredLimits })); + } else { + await adapter.requestDevice({ requiredLimits }); + } + }); + g.test('limit,worse_than_default') .desc( ` diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts index 4c55b5162f..edf08c3840 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts @@ -1,7 +1,7 @@ export const description = `copyTextureToTexture operation tests`; import { makeTestGroup } from '../../../../common/framework/test_group.js'; -import { assert, memcpy, unreachable } from '../../../../common/util/util.js'; +import { assert, ErrorWithExtra, memcpy } from '../../../../common/util/util.js'; import { kBufferSizeAlignment, kMinDynamicBufferOffsetAlignment, @@ -18,14 +18,18 @@ import { ColorTextureFormat, isCompressedTextureFormat, viewCompatible, + RegularTextureFormat, + isRegularTextureFormat, } from '../../../format_info.js'; import { GPUTest, TextureTestMixin } from '../../../gpu_test.js'; import { makeBufferWithContents } from '../../../util/buffer.js'; -import { checkElementsEqual, checkElementsEqualEither } from '../../../util/check_contents.js'; +import { checkElementsEqual } from '../../../util/check_contents.js'; import { align } from '../../../util/math.js'; import { physicalMipSize } from '../../../util/texture/base.js'; import { DataArrayGenerator } from '../../../util/texture/data_generation.js'; import { kBytesPerRowAlignment, dataBytesForCopyOrFail } from '../../../util/texture/layout.js'; +import { TexelView } from '../../../util/texture/texel_view.js'; +import { findFailedPixels } from '../../../util/texture/texture_ok.js'; const dataGenerator = new DataArrayGenerator(); @@ -207,7 +211,7 @@ class F extends TextureTestMixin(GPUTest) { align(dstBlocksPerRow * bytesPerBlock, 4); if (isCompressedTextureFormat(dstTexture.format) && this.isCompatibility) { - assert(viewCompatible(srcFormat, dstFormat)); + assert(viewCompatible(this.isCompatibility, srcFormat, dstFormat)); // compare by rendering. We need the expected texture to match // the dstTexture so we'll create a texture where we supply // all of the data in JavaScript. @@ -304,6 +308,7 @@ class F extends TextureTestMixin(GPUTest) { y: appliedDstOffset.y / blockHeight, z: appliedDstOffset.z, }; + const bytesInRow = appliedCopyBlocksPerRow * bytesPerBlock; for (let z = 0; z < appliedCopyDepth; ++z) { const srcOffsetZ = srcCopyOffsetInBlocks.z + z; @@ -321,7 +326,6 @@ class F extends TextureTestMixin(GPUTest) { (srcBlockRowsPerImage * srcOffsetZ + srcOffsetYInBlocks) + srcCopyOffsetInBlocks.x * bytesPerBlock; - const bytesInRow = appliedCopyBlocksPerRow * bytesPerBlock; memcpy( { src: expectedUint8Data, start: expectedDataOffset, length: bytesInRow }, { dst: expectedUint8DataWithPadding, start: expectedDataWithPaddingOffset } @@ -329,46 +333,69 @@ class F extends TextureTestMixin(GPUTest) { } } - let alternateExpectedData = expectedUint8DataWithPadding; - // For 8-byte snorm formats, allow an alternative encoding of -1. - // MAINTENANCE_TODO: Use textureContentIsOKByT2B with TexelView. - if (srcFormat.includes('snorm')) { - switch (srcFormat) { - case 'r8snorm': - case 'rg8snorm': - case 'rgba8snorm': - alternateExpectedData = alternateExpectedData.slice(); - for (let i = 0; i < alternateExpectedData.length; ++i) { - if (alternateExpectedData[i] === 128) { - alternateExpectedData[i] = 129; - } else if (alternateExpectedData[i] === 129) { - alternateExpectedData[i] = 128; - } - } - break; - case 'bc4-r-snorm': - case 'bc5-rg-snorm': - case 'eac-r11snorm': - case 'eac-rg11snorm': - break; - default: - unreachable(); - } + if (isCompressedTextureFormat(dstFormat)) { + this.expectGPUBufferValuesPassCheck( + dstBuffer, + vals => checkElementsEqual(vals, expectedUint8DataWithPadding), + { + srcByteOffset: 0, + type: Uint8Array, + typedLength: expectedUint8DataWithPadding.length, + } + ); + return; } + assert(isRegularTextureFormat(dstFormat)); + const regularDstFormat = dstFormat as RegularTextureFormat; + // Verify the content of the whole subresource of dstTexture at dstCopyLevel (in dstBuffer) is expected. - this.expectGPUBufferValuesPassCheck( - dstBuffer, - alternateExpectedData === expectedUint8DataWithPadding - ? vals => checkElementsEqual(vals, expectedUint8DataWithPadding) - : vals => - checkElementsEqualEither(vals, [expectedUint8DataWithPadding, alternateExpectedData]), - { - srcByteOffset: 0, - type: Uint8Array, - typedLength: expectedUint8DataWithPadding.length, + const checkByTextureFormat = (actual: Uint8Array) => { + const zero = { x: 0, y: 0, z: 0 }; + + const actTexelView = TexelView.fromTextureDataByReference(regularDstFormat, actual, { + bytesPerRow: bytesInRow, + rowsPerImage: dstBlockRowsPerImage, + subrectOrigin: zero, + subrectSize: dstTextureSizeAtLevel, + }); + const expTexelView = TexelView.fromTextureDataByReference( + regularDstFormat, + expectedUint8DataWithPadding, + { + bytesPerRow: bytesInRow, + rowsPerImage: dstBlockRowsPerImage, + subrectOrigin: zero, + subrectSize: dstTextureSizeAtLevel, + } + ); + + const failedPixelsMessage = findFailedPixels( + regularDstFormat, + zero, + dstTextureSizeAtLevel, + { actTexelView, expTexelView }, + { + maxFractionalDiff: 0, + } + ); + + if (failedPixelsMessage !== undefined) { + const msg = 'Texture level had unexpected contents:\n' + failedPixelsMessage; + return new ErrorWithExtra(msg, () => ({ + expTexelView, + actTexelView, + })); } - ); + + return undefined; + }; + + this.expectGPUBufferValuesPassCheck(dstBuffer, checkByTextureFormat, { + srcByteOffset: 0, + type: Uint8Array, + typedLength: expectedUint8DataWithPadding.length, + }); } InitializeStencilAspect( @@ -1349,6 +1376,9 @@ g.test('copy_multisampled_color') texture can only be 1. ` ) + .beforeAllSubcases(t => { + t.skipIf(t.isCompatibility, 'multisample textures are not copyable in compatibility mode'); + }) .fn(t => { const textureSize = [32, 16, 1] as const; const kColorFormat = 'rgba8unorm'; @@ -1537,6 +1567,9 @@ g.test('copy_multisampled_depth') texture can only be 1. ` ) + .beforeAllSubcases(t => { + t.skipIf(t.isCompatibility, 'multisample textures are not copyable in compatibility mode'); + }) .fn(t => { const textureSize = [32, 16, 1] as const; const kDepthFormat = 'depth24plus'; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts index 4eebc3d611..cff2bd50d5 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts @@ -23,8 +23,9 @@ export const description = `writeTexture + copyBufferToTexture + copyTextureToBu * copy_with_no_image_or_slice_padding_and_undefined_values: test that when copying a single row we can set any bytesPerRow value and when copying a single\ slice we can set rowsPerImage to 0. Also test setting offset, rowsPerImage, mipLevel, origin, origin.{x,y,z} to undefined. +Note: more coverage of memory synchronization for different read and write texture methods are in same_subresource.spec.ts. + * TODO: - - add another initMethod which renders the texture [3] - test copyT2B with buffer size not divisible by 4 (not done because expectContents 4-byte alignment) - Convert the float32 values in initialData into the ones compatible to the depth aspect of depthFormats when depth16unorm is supported by the browsers in @@ -86,7 +87,7 @@ type InitMethod = 'WriteTexture' | 'CopyB2T'; * - PartialCopyT2B: do CopyT2B to check that the part of the texture we copied to with InitMethod * matches the data we were copying and that we don't overwrite any data in the target buffer that * we're not supposed to - that's primarily for testing CopyT2B functionality. - * - FullCopyT2B: do CopyT2B on the whole texture and check wether the part we copied to matches + * - FullCopyT2B: do CopyT2B on the whole texture and check whether the part we copied to matches * the data we were copying and that the nothing else was modified - that's primarily for testing * WriteTexture and CopyB2T. * @@ -1132,6 +1133,10 @@ class ImageCopyTest extends TextureTestMixin(GPUTest) { copySize ); + const use2DArray = this.isCompatibility && inputTexture.depthOrArrayLayers > 1; + const [textureType, layerCode] = use2DArray + ? ['texture_2d_array', ', baseArrayLayer'] + : ['texture_2d', '']; const renderPipeline = this.device.createRenderPipeline({ layout: 'auto', vertex: { @@ -1154,10 +1159,11 @@ class ImageCopyTest extends TextureTestMixin(GPUTest) { fragment: { module: this.device.createShaderModule({ code: ` - @group(0) @binding(0) var inputTexture: texture_2d<f32>; + @group(0) @binding(0) var inputTexture: ${textureType}<f32>; + @group(0) @binding(1) var<uniform> baseArrayLayer: u32; @fragment fn main(@builtin(position) fragcoord : vec4<f32>) -> @builtin(frag_depth) f32 { - var depthValue : vec4<f32> = textureLoad(inputTexture, vec2<i32>(fragcoord.xy), 0); + var depthValue : vec4<f32> = textureLoad(inputTexture, vec2<i32>(fragcoord.xy)${layerCode}, 0); return depthValue.x; }`, }), @@ -1200,19 +1206,26 @@ class ImageCopyTest extends TextureTestMixin(GPUTest) { }); renderPass.setPipeline(renderPipeline); + const uniformBufferEntry = use2DArray + ? [this.createUniformBufferAndBindGroupEntryForBaseArrayLayer(z)] + : []; + const bindGroup = this.device.createBindGroup({ layout: renderPipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: inputTexture.createView({ - dimension: '2d', - baseArrayLayer: z, - arrayLayerCount: 1, + dimension: use2DArray ? '2d-array' : '2d', + ...(!use2DArray && { + baseArrayLayer: z, + arrayLayerCount: 1, + }), baseMipLevel: 0, mipLevelCount: 1, }), }, + ...uniformBufferEntry, ], }); renderPass.setBindGroup(0, bindGroup); @@ -1223,6 +1236,23 @@ class ImageCopyTest extends TextureTestMixin(GPUTest) { this.queue.submit([encoder.finish()]); } + createUniformBufferAndBindGroupEntryForBaseArrayLayer(z: number) { + const buffer = this.device.createBuffer({ + usage: GPUBufferUsage.UNIFORM, + size: 4, + mappedAtCreation: true, + }); + this.trackForCleanup(buffer); + new Uint32Array(buffer.getMappedRange()).set([z]); + buffer.unmap(); + return { + binding: 1, + resource: { + buffer, + }, + }; + } + DoCopyTextureToBufferWithDepthAspectTest( format: DepthStencilFormat, copySize: readonly [number, number, number], @@ -1328,8 +1358,6 @@ class ImageCopyTest extends TextureTestMixin(GPUTest) { /** * This is a helper function used for filtering test parameters - * - * [3]: Modify this after introducing tests with rendering. */ function formatCanBeTested({ format }: { format: ColorTextureFormat }): boolean { return kTextureFormatInfo[format].color.copyDst && kTextureFormatInfo[format].color.copySrc; @@ -1491,6 +1519,12 @@ works for every format with 2d and 2d-array textures. offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow offset > bytesInACompleteCopyImage + Covers spceial cases for OpenGL Compat: + offset % 4 > 0 while: + - padding bytes at end of each row/layer: bytesPerRow % 256 > 0 || rowsPerImage > copyDepth + - rows/layers are compact: bytesPerRow % 256 == 0 && rowsPerImage == copyDepth + - padding bytes at front and end of the same 4-byte word: format == 'r8snorm' && copyWidth <= 2 + TODO: Cover the special code paths for 3D textures in D3D12. TODO: Make a variant for depth-stencil formats. ` @@ -1505,6 +1539,18 @@ works for every format with 2d and 2d-array textures. .beginSubcases() .combineWithParams(kOffsetsAndSizesParams.offsetsAndPaddings) .combine('copyDepth', kOffsetsAndSizesParams.copyDepth) // 2d and 2d-array textures + .combine('copyWidth', [3, 1, 2, 127, 128, 255, 256] as const) // copyWidth === 3 is the default. Others covers special cases for r8snorm and rg8snorm on compatiblity mode. + .filter(({ format, copyWidth }) => { + switch (format) { + case 'r8snorm': + case 'rg8snorm': + return true; + default: + // Restrict test parameters to save run time. + return copyWidth === 3; + } + }) + .combine('rowsPerImageEqualsCopyHeight', [true, false] as const) .unless(p => p.dimension === '1d' && p.copyDepth !== 1) ) .beforeAllSubcases(t => { @@ -1521,25 +1567,43 @@ works for every format with 2d and 2d-array textures. dimension, initMethod, checkMethod, + copyWidth, + rowsPerImageEqualsCopyHeight, } = t.params; + + // Skip test cases designed for special cases coverage on compatibility mode to save run time. + if (!(t.isCompatibility && (format === 'r8snorm' || format === 'rg8snorm'))) { + if (rowsPerImageEqualsCopyHeight === false) { + t.skip( + 'rowsPerImageEqualsCopyHeight === false is only for r8snorm and rg8snorm on compatibility mode' + ); + } + + if (copyWidth !== 3) { + t.skip('copyWidth !== 3 is only for r8snorm and rg8snorm on compatibility mode'); + } + } + const info = kTextureFormatInfo[format]; const offset = offsetInBlocks * info.color.bytes; + const copyHeight = 3; const copySize = { - width: 3 * info.blockWidth, - height: 3 * info.blockHeight, + width: copyWidth * info.blockWidth, + height: copyHeight * info.blockHeight, depthOrArrayLayers: copyDepth, }; let textureHeight = 4 * info.blockHeight; - let rowsPerImage = 3; - const bytesPerRow = 256; + let rowsPerImage = rowsPerImageEqualsCopyHeight ? copyHeight : copyHeight + 1; + const bytesPerRow = align(copyWidth * info.color.bytes, 256); if (dimension === '1d') { copySize.height = 1; textureHeight = info.blockHeight; rowsPerImage = 1; } - const textureSize = [4 * info.blockWidth, textureHeight, copyDepth] as const; + // Add textureWidth by 1 to make sure we are doing a partial copy. + const textureSize = [(copyWidth + 1) * info.blockWidth, textureHeight, copyDepth] as const; const minDataSize = dataBytesForCopyOrFail({ layout: { offset, bytesPerRow, rowsPerImage }, @@ -1549,7 +1613,7 @@ works for every format with 2d and 2d-array textures. }); const dataSize = minDataSize + dataPaddingInBytes; - // We're copying a (3 x 3 x copyDepth) (in texel blocks) part of a (4 x 4 x copyDepth) + // We're copying a (copyWidth x 3 x copyDepth) (in texel blocks) part of a ((copyWidth + 1) x 4 x copyDepth) // (in texel blocks) texture with no origin. t.uploadTextureAndVerifyCopy({ textureDataLayout: { offset, bytesPerRow, rowsPerImage }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/occlusionQuery.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/occlusionQuery.spec.ts index 39b7a377fe..c4b80b7f4f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/occlusionQuery.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/occlusionQuery.spec.ts @@ -695,7 +695,7 @@ g.test('occlusion_query,scissor') const expectPassed = !occluded; t.expect( !!passed === expectPassed, - `queryIndex: ${queryIndex}, scissorCase: ${scissorCase}, was: ${!!passed}, expected: ${expectPassed}, ${name}` + `queryIndex: ${queryIndex}, scissorCase: ${scissorCase}, was: ${!!passed}, expected: ${expectPassed}` ); } ); @@ -739,7 +739,7 @@ g.test('occlusion_query,depth') const expectPassed = queryIndex % 2 === 0; t.expect( !!passed === expectPassed, - `queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}, ${name}` + `queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}` ); } ); @@ -783,7 +783,7 @@ g.test('occlusion_query,stencil') const expectPassed = queryIndex % 2 === 0; t.expect( !!passed === expectPassed, - `queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}, ${name}` + `queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}` ); } ); @@ -851,7 +851,7 @@ g.test('occlusion_query,sample_mask') const expectPassed = !!(sampleMask & drawMask); t.expect( !!passed === expectPassed, - `queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}, ${name}` + `queryIndex: ${queryIndex}, was: ${!!passed}, expected: ${expectPassed}` ); } ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/readonly_depth_stencil.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/readonly_depth_stencil.spec.ts new file mode 100644 index 0000000000..3c381f1c1f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/readonly_depth_stencil.spec.ts @@ -0,0 +1,329 @@ +export const description = ` +Memory synchronization tests for depth-stencil attachments in a single pass, with checks for readonlyness. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { kDepthStencilFormats, kTextureFormatInfo } from '../../../../format_info.js'; +import { GPUTest } from '../../../../gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('sampling_while_testing') + .desc( + `Tests concurrent sampling and testing of readonly depth-stencil attachments in a render pass. + - Test for all depth-stencil formats. + - Test for all valid combinations of depth/stencilReadOnly. + +In particular this test checks that a non-readonly aspect can be rendered to, and used for depth/stencil +testing while the other one is used for sampling. + ` + ) + .params(p => + p + .combine('format', kDepthStencilFormats) // + .combine('depthReadOnly', [true, false, undefined]) + .combine('stencilReadOnly', [true, false, undefined]) + .filter(p => { + const info = kTextureFormatInfo[p.format]; + const depthMatch = (info.depth === undefined) === (p.depthReadOnly === undefined); + const stencilMatch = (info.stencil === undefined) === (p.stencilReadOnly === undefined); + return depthMatch && stencilMatch; + }) + ) + .beforeAllSubcases(t => { + const { format } = t.params; + const formatInfo = kTextureFormatInfo[format]; + const hasDepth = formatInfo.depth !== undefined; + const hasStencil = formatInfo.stencil !== undefined; + + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + t.skipIf( + t.isCompatibility && hasDepth && hasStencil, + 'compatibility mode does not support different TEXTURE_BINDING views of the same texture in a single draw calls' + ); + }) + .fn(t => { + const { format, depthReadOnly, stencilReadOnly } = t.params; + const formatInfo = kTextureFormatInfo[format]; + const hasDepth = formatInfo.depth !== undefined; + const hasStencil = formatInfo.stencil !== undefined; + + // The 3x3 depth stencil texture used for the tests. + const ds = t.device.createTexture({ + label: 'testTexture', + size: [3, 3], + format, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + }); + t.trackForCleanup(ds); + + // Fill the texture along the X axis with stencil values 1, 2, 3 and along the Y axis depth + // values 0.1, 0.2, 0.3. The depth value is written using @builtin(frag_depth) while the + // stencil is written using stencil operation and modifying the stencilReference. + const initModule = t.device.createShaderModule({ + code: ` + @vertex fn vs( + @builtin(instance_index) x : u32, @builtin(vertex_index) y : u32 + ) -> @builtin(position) vec4f { + let texcoord = (vec2f(f32(x), f32(y)) + vec2f(0.5)) / 3; + return vec4f((texcoord * 2) - vec2f(1.0), 0, 1); + } + @fragment fn fs_with_depth(@builtin(position) pos : vec4f) -> @builtin(frag_depth) f32 { + return (pos.y + 0.5) / 10; + } + @fragment fn fs_no_depth() { + } + `, + }); + const initPipeline = t.device.createRenderPipeline({ + layout: 'auto', + label: 'initPipeline', + vertex: { module: initModule }, + fragment: { + module: initModule, + targets: [], + entryPoint: hasDepth ? 'fs_with_depth' : 'fs_no_depth', + }, + depthStencil: { + format, + ...(hasDepth && { + depthWriteEnabled: true, + depthCompare: 'always', + }), + ...(hasStencil && { + stencilBack: { compare: 'always', passOp: 'replace' }, + stencilFront: { compare: 'always', passOp: 'replace' }, + }), + }, + primitive: { topology: 'point-list' }, + }); + + const encoder = t.device.createCommandEncoder(); + + const initPass = encoder.beginRenderPass({ + colorAttachments: [], + depthStencilAttachment: { + view: ds.createView(), + ...(hasDepth && { + depthStoreOp: 'store', + depthLoadOp: 'clear', + depthClearValue: 0, + }), + ...(hasStencil && { + stencilStoreOp: 'store', + stencilLoadOp: 'clear', + stencilClearValue: 0, + }), + }, + }); + initPass.setPipeline(initPipeline); + for (let i = 0; i < 3; i++) { + initPass.setStencilReference(i + 1); + // Draw 3 points (Y = 0, 1, 2) at X = instance_index = i. + initPass.draw(3, 1, 0, i); + } + initPass.end(); + + // Perform the actual test: + // - The shader outputs depth 0.15 and stencil 2 (via stencilReference). + // - Test that the fragdepth / stencilref must be <= to what's in the depth-stencil attachment. + // -> Fragments that have depth 0.1 or stencil 1 are tested out. + // - Test that sampling the depth / stencil (when possible) is <= 0.2 for depth, <= 2 for stencil + // -> Fragments that have depth 0.3 or stencil 3 are discarded if that aspect is readonly. + // - Write the depth / increment the stencil if the aspect is not readonly. + // -> After the test, fragments that passed will have non-readonly aspects updated. + const kFragDepth = 0.15; + const kStencilRef = 2; + const testAndCheckModule = t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var depthTex : texture_2d<f32>; + @group(0) @binding(1) var stencilTex : texture_2d<u32>; + + @vertex fn full_quad_vs(@builtin(vertex_index) id : u32) -> @builtin(position) vec4f { + let pos = array(vec2f(-3, -1), vec2(3, -1), vec2(0, 2)); + return vec4f(pos[id], ${kFragDepth}, 1.0); + } + + @fragment fn test_texture(@builtin(position) pos : vec4f) { + let texel = vec2u(floor(pos.xy)); + if ${!!stencilReadOnly} && textureLoad(stencilTex, texel, 0).r > 2 { + discard; + } + if ${!!depthReadOnly} && textureLoad(depthTex, texel, 0).r > 0.21 { + discard; + } + } + + @fragment fn check_texture(@builtin(position) pos : vec4f) -> @location(0) u32 { + let texel = vec2u(floor(pos.xy)); + + // The current values in the framebuffer. + let initStencil = texel.x + 1; + let initDepth = f32(texel.y + 1) / 10.0; + + // Expected results of the test_texture step. + let stencilTestPasses = !${hasStencil} || ${kStencilRef} <= initStencil; + let depthTestPasses = !${hasDepth} || ${kFragDepth} <= initDepth; + let fsDiscards = (${!!stencilReadOnly} && initStencil > 2) || + (${!!depthReadOnly} && initDepth > 0.21); + + // Compute the values that should be in the framebuffer. + var stencil = initStencil; + var depth = initDepth; + + // When the fragments aren't discarded, fragment output operations happen. + if depthTestPasses && stencilTestPasses && !fsDiscards { + if ${!stencilReadOnly} { + stencil += 1; + } + if ${!depthReadOnly} { + depth = ${kFragDepth}; + } + } + + if ${hasStencil} && textureLoad(stencilTex, texel, 0).r != stencil { + return 0; + } + if ${hasDepth} && abs(textureLoad(depthTex, texel, 0).r - depth) > 0.01 { + return 0; + } + return 1; + } + `, + }); + const testPipeline = t.device.createRenderPipeline({ + label: 'testPipeline', + layout: 'auto', + vertex: { module: testAndCheckModule }, + fragment: { module: testAndCheckModule, entryPoint: 'test_texture', targets: [] }, + depthStencil: { + format, + ...(hasDepth && { + depthCompare: 'less-equal', + depthWriteEnabled: !depthReadOnly, + }), + ...(hasStencil && { + stencilBack: { + compare: 'less-equal', + passOp: stencilReadOnly ? 'keep' : 'increment-clamp', + }, + stencilFront: { + compare: 'less-equal', + passOp: stencilReadOnly ? 'keep' : 'increment-clamp', + }, + }), + }, + primitive: { topology: 'triangle-list' }, + }); + + // Make fake stencil or depth textures to put in the bindgroup if the aspect is not readonly. + const fakeStencil = t.device.createTexture({ + label: 'fakeStencil', + format: 'r32uint', + size: [1, 1], + usage: GPUTextureUsage.TEXTURE_BINDING, + }); + t.trackForCleanup(fakeStencil); + const fakeDepth = t.device.createTexture({ + label: 'fakeDepth', + format: 'r32float', + size: [1, 1], + usage: GPUTextureUsage.TEXTURE_BINDING, + }); + t.trackForCleanup(fakeDepth); + const stencilView = stencilReadOnly + ? ds.createView({ aspect: 'stencil-only' }) + : fakeStencil.createView(); + const depthView = depthReadOnly + ? ds.createView({ aspect: 'depth-only' }) + : fakeDepth.createView(); + const testBindGroup = t.device.createBindGroup({ + layout: testPipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: depthView }, + { binding: 1, resource: stencilView }, + ], + }); + + // Run the test. + const testPass = encoder.beginRenderPass({ + colorAttachments: [], + depthStencilAttachment: { + view: ds.createView(), + ...(hasDepth && + (depthReadOnly + ? { depthReadOnly: true } + : { + depthStoreOp: 'store', + depthLoadOp: 'load', + })), + ...(hasStencil && + (stencilReadOnly + ? { stencilReadOnly: true } + : { + stencilStoreOp: 'store', + stencilLoadOp: 'load', + })), + }, + }); + testPass.setPipeline(testPipeline); + testPass.setStencilReference(kStencilRef); + testPass.setBindGroup(0, testBindGroup); + testPass.draw(3); + testPass.end(); + + // Check that the contents of the textures are what we expect. See the shader module for the + // computation of what's expected, it writes a 1 on success, 0 otherwise. + const checkPipeline = t.device.createRenderPipeline({ + label: 'checkPipeline', + layout: 'auto', + vertex: { module: testAndCheckModule }, + fragment: { + module: testAndCheckModule, + entryPoint: 'check_texture', + targets: [{ format: 'r32uint' }], + }, + primitive: { topology: 'triangle-list' }, + }); + const checkBindGroup = t.device.createBindGroup({ + layout: checkPipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: hasDepth ? ds.createView({ aspect: 'depth-only' }) : fakeDepth.createView(), + }, + { + binding: 1, + resource: hasStencil + ? ds.createView({ aspect: 'stencil-only' }) + : fakeStencil.createView(), + }, + ], + }); + + const resultTexture = t.device.createTexture({ + label: 'resultTexture', + format: 'r32uint', + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + size: [3, 3], + }); + const checkPass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: resultTexture.createView(), + loadOp: 'clear', + clearValue: [0, 0, 0, 0], + storeOp: 'store', + }, + ], + }); + checkPass.setPipeline(checkPipeline); + checkPass.setBindGroup(0, checkBindGroup); + checkPass.draw(3); + checkPass.end(); + + t.queue.submit([encoder.finish()]); + + // The check texture should be full of success (a.k.a. 1)! + t.expectSingleColor(resultTexture, resultTexture.format, { size: [3, 3, 1], exp: { R: 1 } }); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts index e9f7b9726c..7fb1a230cc 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts @@ -8,26 +8,38 @@ import { GPUTest } from '../../gpu_test.js'; export const g = makeTestGroup(GPUTest); +function* extractValuePropertyKeys(obj: { [k: string]: unknown }) { + for (const key in obj) { + if (typeof obj[key] !== 'function') { + yield key; + } + } +} + +const kBufferSubcases: readonly { + size: number; + usage: GPUBufferUsageFlags; + label?: string; + invalid?: boolean; +}[] = [ + { size: 4, usage: GPUConst.BufferUsage.VERTEX }, + { + size: 16, + usage: + GPUConst.BufferUsage.STORAGE | GPUConst.BufferUsage.COPY_SRC | GPUConst.BufferUsage.UNIFORM, + }, + { size: 32, usage: GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.COPY_DST }, + { size: 40, usage: GPUConst.BufferUsage.INDEX, label: 'some label' }, + { + size: 32, + usage: GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.MAP_WRITE, + invalid: true, + }, +] as const; + g.test('buffer_reflection_attributes') .desc(`For every buffer attribute, the corresponding descriptor value is carried over.`) - .paramsSubcasesOnly(u => - u.combine('descriptor', [ - { size: 4, usage: GPUConst.BufferUsage.VERTEX }, - { - size: 16, - usage: - GPUConst.BufferUsage.STORAGE | - GPUConst.BufferUsage.COPY_SRC | - GPUConst.BufferUsage.UNIFORM, - }, - { size: 32, usage: GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.COPY_DST }, - { - size: 32, - usage: GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.MAP_WRITE, - invalid: true, - }, - ] as const) - ) + .paramsSubcasesOnly(u => u.combine('descriptor', kBufferSubcases)) .fn(t => { const { descriptor } = t.params; @@ -39,53 +51,102 @@ g.test('buffer_reflection_attributes') }, descriptor.invalid === true); }); -g.test('texture_reflection_attributes') - .desc(`For every texture attribute, the corresponding descriptor value is carried over.`) +g.test('buffer_creation_from_reflection') + .desc( + ` + Check that you can create a buffer from a buffer's reflection. + This check is to insure that as WebGPU develops this path doesn't + suddenly break because of new reflection. + ` + ) .paramsSubcasesOnly(u => - u.combine('descriptor', [ - { - size: { width: 4, height: 4 }, - format: 'rgba8unorm', - usage: GPUConst.TextureUsage.TEXTURE_BINDING, - }, - { - size: { width: 8, height: 8, depthOrArrayLayers: 8 }, - format: 'bgra8unorm', - usage: GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.COPY_SRC, - }, - { - size: [4, 4], - format: 'rgba8unorm', - usage: GPUConst.TextureUsage.TEXTURE_BINDING, - mipLevelCount: 2, - }, - { - size: [16, 16, 16], - format: 'rgba8unorm', - usage: GPUConst.TextureUsage.TEXTURE_BINDING, - dimension: '3d', - }, - { - size: [32], - format: 'rgba8unorm', - usage: GPUConst.TextureUsage.TEXTURE_BINDING, - dimension: '1d', - }, - { - size: { width: 4, height: 4 }, - format: 'rgba8unorm', - usage: GPUConst.TextureUsage.RENDER_ATTACHMENT, - sampleCount: 4, - }, - { - size: { width: 4, height: 4 }, - format: 'rgba8unorm', - usage: GPUConst.TextureUsage.TEXTURE_BINDING, - sampleCount: 4, - invalid: true, - }, - ] as const) + u.combine('descriptor', kBufferSubcases).filter(p => !p.descriptor.invalid) ) + + .fn(t => { + const { descriptor } = t.params; + + const buffer = t.device.createBuffer(descriptor); + t.trackForCleanup(buffer); + const buffer2 = t.device.createBuffer(buffer); + t.trackForCleanup(buffer2); + + const bufferAsObject = buffer as unknown as { [k: string]: unknown }; + const buffer2AsObject = buffer2 as unknown as { [k: string]: unknown }; + const keys = [...extractValuePropertyKeys(bufferAsObject)]; + + // Sanity check + t.expect(keys.includes('size')); + t.expect(keys.includes('usage')); + t.expect(keys.includes('label')); + + for (const key of keys) { + t.expect(bufferAsObject[key] === buffer2AsObject[key], key); + } + }); + +const kTextureSubcases: readonly { + size: GPUExtent3D; + format: GPUTextureFormat; + usage: GPUTextureUsageFlags; + mipLevelCount?: number; + label?: string; + dimension?: GPUTextureDimension; + sampleCount?: number; + invalid?: boolean; +}[] = [ + { + size: { width: 4, height: 4 }, + format: 'rgba8unorm', + usage: GPUConst.TextureUsage.TEXTURE_BINDING, + }, + { + size: { width: 4, height: 4 }, + format: 'rgba8unorm', + usage: GPUConst.TextureUsage.TEXTURE_BINDING, + label: 'some label', + }, + { + size: { width: 8, height: 8, depthOrArrayLayers: 8 }, + format: 'bgra8unorm', + usage: GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.COPY_SRC, + }, + { + size: [4, 4], + format: 'rgba8unorm', + usage: GPUConst.TextureUsage.TEXTURE_BINDING, + mipLevelCount: 2, + }, + { + size: [16, 16, 16], + format: 'rgba8unorm', + usage: GPUConst.TextureUsage.TEXTURE_BINDING, + dimension: '3d', + }, + { + size: [32], + format: 'rgba8unorm', + usage: GPUConst.TextureUsage.TEXTURE_BINDING, + dimension: '1d', + }, + { + size: { width: 4, height: 4 }, + format: 'rgba8unorm', + usage: GPUConst.TextureUsage.RENDER_ATTACHMENT, + sampleCount: 4, + }, + { + size: { width: 4, height: 4 }, + format: 'rgba8unorm', + usage: GPUConst.TextureUsage.TEXTURE_BINDING, + sampleCount: 4, + invalid: true, + }, +] as const; + +g.test('texture_reflection_attributes') + .desc(`For every texture attribute, the corresponding descriptor value is carried over.`) + .paramsSubcasesOnly(u => u.combine('descriptor', kTextureSubcases)) .fn(t => { const { descriptor } = t.params; @@ -116,18 +177,77 @@ g.test('texture_reflection_attributes') }, descriptor.invalid === true); }); -g.test('query_set_reflection_attributes') - .desc(`For every queue attribute, the corresponding descriptor value is carried over.`) +interface TextureWithSize extends GPUTexture { + size: GPUExtent3D; +} + +g.test('texture_creation_from_reflection') + .desc( + ` + Check that you can create a texture from a texture's reflection. + This check is to insure that as WebGPU develops this path doesn't + suddenly break because of new reflection. + ` + ) .paramsSubcasesOnly(u => - u.combine('descriptor', [ - { type: 'occlusion', count: 4 }, - { type: 'occlusion', count: 16 }, - { type: 'occlusion', count: 8193, invalid: true }, - ] as const) + u.combine('descriptor', kTextureSubcases).filter(p => !p.descriptor.invalid) ) .fn(t => { const { descriptor } = t.params; + const texture = t.device.createTexture(descriptor); + t.trackForCleanup(texture); + const textureWithSize = texture as TextureWithSize; + textureWithSize.size = [texture.width, texture.height, texture.depthOrArrayLayers]; + const texture2 = t.device.createTexture(textureWithSize); + t.trackForCleanup(texture2); + + const textureAsObject = texture as unknown as { [k: string]: unknown }; + const texture2AsObject = texture2 as unknown as { [k: string]: unknown }; + const keys = [...extractValuePropertyKeys(textureAsObject)].filter(k => k !== 'size'); + + // Sanity check + t.expect(keys.includes('format')); + t.expect(keys.includes('usage')); + t.expect(keys.includes('label')); + + for (const key of keys) { + t.expect(textureAsObject[key] === texture2AsObject[key], key); + } + + // MAINTENANCE_TODO: Check this if it is made possible by a spec change. + // + // texture3 = t.device.createTexture({ + // ...texture, + // size: [texture.width, texture.height, texture.depthOrArrayLayers], + // }); + // + // and this + // + // texture3 = t.device.createTexture({ + // size: [texture.width, texture.height, texture.depthOrArrayLayers], + // ...texture, + // }); + }); + +const kQuerySetSubcases: readonly { + type: GPUQueryType; + count: number; + label?: string; + invalid?: boolean; +}[] = [ + { type: 'occlusion', count: 4 }, + { type: 'occlusion', count: 16 }, + { type: 'occlusion', count: 32, label: 'some label' }, + { type: 'occlusion', count: 8193, invalid: true }, +] as const; + +g.test('query_set_reflection_attributes') + .desc(`For every queue attribute, the corresponding descriptor value is carried over.`) + .paramsSubcasesOnly(u => u.combine('descriptor', kQuerySetSubcases)) + .fn(t => { + const { descriptor } = t.params; + t.expectValidationError(() => { const querySet = t.device.createQuerySet(descriptor); @@ -135,3 +255,36 @@ g.test('query_set_reflection_attributes') t.expect(querySet.count === descriptor.count); }, descriptor.invalid === true); }); + +g.test('query_set_creation_from_reflection') + .desc( + ` + Check that you can create a queryset from a queryset's reflection. + This check is to insure that as WebGPU develops this path doesn't + suddenly break because of new reflection. + ` + ) + .paramsSubcasesOnly(u => + u.combine('descriptor', kQuerySetSubcases).filter(p => !p.descriptor.invalid) + ) + .fn(t => { + const { descriptor } = t.params; + + const querySet = t.device.createQuerySet(descriptor); + t.trackForCleanup(querySet); + const querySet2 = t.device.createQuerySet(querySet); + t.trackForCleanup(querySet2); + + const querySetAsObject = querySet as unknown as { [k: string]: unknown }; + const querySet2AsObject = querySet2 as unknown as { [k: string]: unknown }; + const keys = [...extractValuePropertyKeys(querySetAsObject)]; + + // Sanity check + t.expect(keys.includes('type')); + t.expect(keys.includes('count')); + t.expect(keys.includes('label')); + + for (const key of keys) { + t.expect(querySetAsObject[key] === querySet2AsObject[key], key); + } + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts index 00069b777f..b28e1b381c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts @@ -6,6 +6,8 @@ Also tested: - The positions of samples in the standard sample patterns. - Per-sample interpolation sampling: @interpolate(perspective, sample). +TODO: Test sample_mask as an input. + TODO: add a test without a 0th color attachment (sparse color attachment), with different color attachments and alpha value output. The cross-platform behavior is unknown. could be any of: - coverage is always 100% @@ -19,7 +21,7 @@ import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { assert, range } from '../../../../common/util/util.js'; import { GPUTest, TextureTestMixin } from '../../../gpu_test.js'; import { checkElementsPassPredicate, checkElementsEqual } from '../../../util/check_contents.js'; -import { TypeF32, TypeU32 } from '../../../util/conversion.js'; +import { Type } from '../../../util/conversion.js'; import { TexelView } from '../../../util/texture/texel_view.js'; const kColors = [ @@ -435,8 +437,8 @@ class F extends TextureTestMixin(GPUTest) { sampleMask: number, fragmentShaderOutputMask: number ) { - const buffer = this.copySinglePixelTextureToBufferUsingComputePass( - TypeF32, // correspond to 'rgba8unorm' format + const buffer = this.copy2DTextureToBufferUsingComputePass( + Type.f32, // correspond to 'rgba8unorm' format 4, texture.createView(), sampleCount @@ -459,10 +461,10 @@ class F extends TextureTestMixin(GPUTest) { sampleMask: number, fragmentShaderOutputMask: number ) { - const buffer = this.copySinglePixelTextureToBufferUsingComputePass( + const buffer = this.copy2DTextureToBufferUsingComputePass( // Use f32 as the scalar type for depth (depth24plus, depth32float) // Use u32 as the scalar type for stencil (stencil8) - aspect === 'depth-only' ? TypeF32 : TypeU32, + aspect === 'depth-only' ? Type.f32 : Type.u32, 1, depthStencilTexture.createView({ aspect }), sampleCount @@ -702,8 +704,8 @@ color' <= color. 2 ); - const colorBuffer = t.copySinglePixelTextureToBufferUsingComputePass( - TypeF32, // correspond to 'rgba8unorm' format + const colorBuffer = t.copy2DTextureToBufferUsingComputePass( + Type.f32, // correspond to 'rgba8unorm' format 4, color.createView(), sampleCount @@ -714,8 +716,8 @@ color' <= color. }); colorResultPromises.push(colorResult); - const depthBuffer = t.copySinglePixelTextureToBufferUsingComputePass( - TypeF32, // correspond to 'depth24plus-stencil8' format + const depthBuffer = t.copy2DTextureToBufferUsingComputePass( + Type.f32, // correspond to 'depth24plus-stencil8' format 1, depthStencil.createView({ aspect: 'depth-only' }), sampleCount @@ -726,8 +728,8 @@ color' <= color. }); depthResultPromises.push(depthResult); - const stencilBuffer = t.copySinglePixelTextureToBufferUsingComputePass( - TypeU32, // correspond to 'depth24plus-stencil8' format + const stencilBuffer = t.copy2DTextureToBufferUsingComputePass( + Type.u32, // correspond to 'depth24plus-stencil8' format 1, depthStencil.createView({ aspect: 'stencil-only' }), sampleCount diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/3d_texture_slices.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/3d_texture_slices.spec.ts new file mode 100644 index 0000000000..98f51d3dff --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/3d_texture_slices.spec.ts @@ -0,0 +1,363 @@ +export const description = ` +Test rendering to 3d texture slices. +- Render to same slice on different render pass, different textures, or texture [1, 1, N]'s different mip levels +- Render to different slices at mip levels on same texture in render pass +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { kTextureFormatInfo } from '../../../format_info.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { kBytesPerRowAlignment } from '../../../util/texture/layout.js'; + +const kSize = 4; +const kFormat = 'rgba8unorm' as const; + +class F extends GPUTest { + createShaderModule(attachmentCount: number = 1): GPUShaderModule { + let locations = ''; + let outputs = ''; + for (let i = 0; i < attachmentCount; i++) { + locations = locations + `@location(${i}) color${i} : vec4f, \n`; + outputs = outputs + `output.color${i} = vec4f(0.0, 1.0, 0.0, 1.0);\n`; + } + + return this.device.createShaderModule({ + code: ` + struct Output { + ${locations} + } + + @vertex + fn main_vs(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> { + var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>( + // Triangle is slightly extended so its edge doesn't cut through pixel centers. + vec2<f32>(-1.0, 1.01), + vec2<f32>(1.01, -1.0), + vec2<f32>(-1.0, -1.0)); + return vec4<f32>(pos[VertexIndex], 0.0, 1.0); + } + + @fragment + fn main_fs() -> Output { + var output : Output; + ${outputs} + return output; + } + `, + }); + } + + getBufferSizeAndOffset( + attachmentWidth: number, + attachmentHeight: number, + attachmentCount: number + ): { bufferSize: number; bufferOffset: number } { + const bufferSize = + (attachmentCount * attachmentHeight - 1) * kBytesPerRowAlignment + attachmentWidth * 4; + const bufferOffset = attachmentCount > 1 ? attachmentHeight * kBytesPerRowAlignment : 0; + return { bufferSize, bufferOffset }; + } + + checkAttachmentResult( + attachmentWidth: number, + attachmentHeight: number, + attachmentCount: number, + buffer: GPUBuffer + ) { + const { bufferSize, bufferOffset } = this.getBufferSizeAndOffset( + attachmentWidth, + attachmentHeight, + attachmentCount + ); + const expectedData = new Uint8Array(bufferSize); + for (let i = 0; i < attachmentCount; i++) { + for (let j = 0; j < attachmentHeight; j++) { + for (let k = 0; k < attachmentWidth; k++) { + expectedData[i * bufferOffset + j * 256 + k * 4] = k <= j ? 0x00 : 0xff; + expectedData[i * bufferOffset + j * 256 + k * 4 + 1] = k <= j ? 0xff : 0x00; + expectedData[i * bufferOffset + j * 256 + k * 4 + 2] = 0x00; + expectedData[i * bufferOffset + j * 256 + k * 4 + 3] = 0xff; + } + } + } + + this.expectGPUBufferValuesEqual(buffer, expectedData); + } +} + +export const g = makeTestGroup(F); + +g.test('one_color_attachment,mip_levels') + .desc( + ` + Render to a 3d texture slice with mip levels. + ` + ) + .params(u => u.combine('mipLevel', [0, 1, 2]).combine('depthSlice', [0, 1])) + .fn(t => { + const { mipLevel, depthSlice } = t.params; + + const texture = t.device.createTexture({ + size: [kSize << mipLevel, kSize << mipLevel, 2 << mipLevel], + dimension: '3d', + format: kFormat, + mipLevelCount: mipLevel + 1, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + + const { bufferSize } = t.getBufferSizeAndOffset(kSize, kSize, 1); + + const buffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + + const module = t.createShaderModule(); + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { + module, + targets: [{ format: kFormat }], + }, + primitive: { topology: 'triangle-list' }, + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: texture.createView({ + baseMipLevel: mipLevel, + mipLevelCount: 1, + }), + depthSlice, + clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.setPipeline(pipeline); + pass.draw(3); + pass.end(); + encoder.copyTextureToBuffer( + { texture, mipLevel, origin: { x: 0, y: 0, z: depthSlice } }, + { buffer, bytesPerRow: 256 }, + { width: kSize, height: kSize, depthOrArrayLayers: 1 } + ); + t.device.queue.submit([encoder.finish()]); + + t.checkAttachmentResult(kSize, kSize, 1, buffer); + }); + +g.test('multiple_color_attachments,same_mip_level') + .desc( + ` + Render to the different slices of 3d texture in multiple color attachments. + - Same 3d texture with different slices at same mip level + - Different 3d textures with same slice at same mip level + ` + ) + .params(u => + u + .combine('sameTexture', [true, false]) + .beginSubcases() + .combine('samePass', [true, false]) + .combine('mipLevel', [0, 1]) + ) + .fn(t => { + const { sameTexture, samePass, mipLevel } = t.params; + + const formatByteCost = kTextureFormatInfo[kFormat].colorRender.byteCost; + const maxAttachmentCountPerSample = Math.trunc( + t.device.limits.maxColorAttachmentBytesPerSample / formatByteCost + ); + const attachmentCount = Math.min( + maxAttachmentCountPerSample, + t.device.limits.maxColorAttachments + ); + + const descriptor = { + size: [kSize << mipLevel, kSize << mipLevel, (1 << attachmentCount) << mipLevel], + dimension: '3d', + format: kFormat, + mipLevelCount: mipLevel + 1, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + } as const; + + const texture = t.device.createTexture(descriptor); + + const textures: GPUTexture[] = []; + const colorAttachments: GPURenderPassColorAttachment[] = []; + for (let i = 0; i < attachmentCount; i++) { + if (sameTexture) { + textures.push(texture); + } else { + const diffTexture = t.device.createTexture(descriptor); + textures.push(diffTexture); + } + + const colorAttachment = { + view: textures[i].createView({ + baseMipLevel: mipLevel, + mipLevelCount: 1, + }), + depthSlice: sameTexture ? i : 0, + clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + } as const; + + colorAttachments.push(colorAttachment); + } + + const encoder = t.device.createCommandEncoder(); + + if (samePass) { + const module = t.createShaderModule(attachmentCount); + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { + module, + targets: new Array<GPUColorTargetState>(attachmentCount).fill({ format: kFormat }), + }, + primitive: { topology: 'triangle-list' }, + }); + + const pass = encoder.beginRenderPass({ colorAttachments }); + pass.setPipeline(pipeline); + pass.draw(3); + pass.end(); + } else { + const module = t.createShaderModule(); + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { + module, + targets: [{ format: kFormat }], + }, + primitive: { topology: 'triangle-list' }, + }); + + for (let i = 0; i < attachmentCount; i++) { + const pass = encoder.beginRenderPass({ colorAttachments: [colorAttachments[i]] }); + pass.setPipeline(pipeline); + pass.draw(3); + pass.end(); + } + } + + const { bufferSize, bufferOffset } = t.getBufferSizeAndOffset(kSize, kSize, attachmentCount); + const buffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + for (let i = 0; i < attachmentCount; i++) { + encoder.copyTextureToBuffer( + { + texture: textures[i], + mipLevel, + origin: { x: 0, y: 0, z: sameTexture ? i : 0 }, + }, + { buffer, bytesPerRow: 256, offset: bufferOffset * i }, + { width: kSize, height: kSize, depthOrArrayLayers: 1 } + ); + } + + t.device.queue.submit([encoder.finish()]); + + t.checkAttachmentResult(kSize, kSize, attachmentCount, buffer); + }); + +g.test('multiple_color_attachments,same_slice_with_diff_mip_levels') + .desc( + ` + Render to the same slice of a 3d texture at different mip levels in multiple color attachments. + - For texture size with 1x1xN, the same depth slice of different mip levels can be rendered. + ` + ) + .params(u => u.combine('depthSlice', [0, 1])) + .fn(t => { + const { depthSlice } = t.params; + + const kBaseSize = 1; + + const formatByteCost = kTextureFormatInfo[kFormat].colorRender.byteCost; + const maxAttachmentCountPerSample = Math.trunc( + t.device.limits.maxColorAttachmentBytesPerSample / formatByteCost + ); + const attachmentCount = Math.min( + maxAttachmentCountPerSample, + t.device.limits.maxColorAttachments + ); + + const module = t.createShaderModule(attachmentCount); + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { + module, + targets: new Array<GPUColorTargetState>(attachmentCount).fill({ format: kFormat }), + }, + primitive: { topology: 'triangle-list' }, + }); + + const texture = t.device.createTexture({ + size: [kBaseSize, kBaseSize, (depthSlice + 1) << attachmentCount], + dimension: '3d', + format: kFormat, + mipLevelCount: attachmentCount, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + + const colorAttachments: GPURenderPassColorAttachment[] = []; + for (let i = 0; i < attachmentCount; i++) { + const colorAttachment = { + view: texture.createView({ + baseMipLevel: i, + mipLevelCount: 1, + }), + depthSlice, + clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + } as const; + + colorAttachments.push(colorAttachment); + } + + const encoder = t.device.createCommandEncoder(); + + const pass = encoder.beginRenderPass({ colorAttachments }); + pass.setPipeline(pipeline); + pass.draw(3); + pass.end(); + + const { bufferSize, bufferOffset } = t.getBufferSizeAndOffset( + kBaseSize, + kBaseSize, + attachmentCount + ); + const buffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + for (let i = 0; i < attachmentCount; i++) { + encoder.copyTextureToBuffer( + { texture, mipLevel: i, origin: { x: 0, y: 0, z: depthSlice } }, + { buffer, bytesPerRow: 256, offset: bufferOffset * i }, + { width: kBaseSize, height: kBaseSize, depthOrArrayLayers: 1 } + ); + } + + t.device.queue.submit([encoder.finish()]); + + t.checkAttachmentResult(kBaseSize, kBaseSize, attachmentCount, buffer); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts index 1290c6bc99..673e33c2ea 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts @@ -11,7 +11,7 @@ import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { assert, TypedArrayBufferView, unreachable } from '../../../../common/util/util.js'; import { kBlendFactors, kBlendOperations } from '../../../capability_info.js'; import { GPUConst } from '../../../constants.js'; -import { kEncodableTextureFormats, kTextureFormatInfo } from '../../../format_info.js'; +import { kRegularTextureFormats, kTextureFormatInfo } from '../../../format_info.js'; import { GPUTest, TextureTestMixin } from '../../../gpu_test.js'; import { clamp } from '../../../util/math.js'; import { TexelView } from '../../../util/texture/texel_view.js'; @@ -165,6 +165,7 @@ g.test('blending,GPUBlendComponent') .combine('component', ['color', 'alpha'] as const) .combine('srcFactor', kBlendFactors) .combine('dstFactor', kBlendFactors) + .beginSubcases() .combine('operation', kBlendOperations) .filter(t => { if (t.operation === 'min' || t.operation === 'max') { @@ -172,7 +173,6 @@ g.test('blending,GPUBlendComponent') } return true; }) - .beginSubcases() .combine('srcColor', [{ r: 0.11, g: 0.61, b: 0.81, a: 0.44 }]) .combine('dstColor', [ { r: 0.51, g: 0.22, b: 0.71, a: 0.33 }, @@ -318,9 +318,9 @@ struct Uniform { ); }); -const kBlendableFormats = kEncodableTextureFormats.filter(f => { +const kBlendableFormats = kRegularTextureFormats.filter(f => { const info = kTextureFormatInfo[f]; - return info.renderable && info.sampleType === 'float'; + return info.colorRender && info.color.type === 'float'; }); g.test('blending,formats') diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts index 3b2227db98..a81a7c7812 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts @@ -467,13 +467,8 @@ g.test('reverse_depth') @vertex fn main( @builtin(vertex_index) VertexIndex : u32, @builtin(instance_index) InstanceIndex : u32) -> Output { - // TODO: remove workaround for Tint unary array access broke - var zv : array<vec2<f32>, 4> = array<vec2<f32>, 4>( - vec2<f32>(0.2, 0.2), - vec2<f32>(0.3, 0.3), - vec2<f32>(-0.1, -0.1), - vec2<f32>(1.1, 1.1)); - let z : f32 = zv[InstanceIndex].x; + let zv = array(0.2, 0.3, -0.1, 1.1); + let z = zv[InstanceIndex]; var output : Output; output.Position = vec4<f32>(0.5, 0.5, z, 1.0); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts index 03caff3b25..17e80c5da9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts @@ -304,6 +304,12 @@ g.test('depth_bias') }, ] as const) ) + .beforeAllSubcases(t => { + t.skipIf( + t.isCompatibility && t.params.biasClamp !== 0, + 'non zero depthBiasClamp is not supported in compatibility mode' + ); + }) .fn(t => { t.runDepthBiasTest('depth32float', t.params); }); @@ -346,6 +352,12 @@ g.test('depth_bias_24bit_format') }, ] as const) ) + .beforeAllSubcases(t => { + t.skipIf( + t.isCompatibility && t.params.biasClamp !== 0, + 'non zero depthBiasClamp is not supported in compatibility mode' + ); + }) .fn(t => { const { format } = t.params; t.runDepthBiasTestFor24BitFormat(format, t.params); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts index 65e2e8af1f..1d3b6d8b7a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts @@ -4,6 +4,7 @@ depth ranges as well. `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { assert } from '../../../../common/util/util.js'; import { kDepthStencilFormats, kTextureFormatInfo } from '../../../format_info.js'; import { GPUTest } from '../../../gpu_test.js'; import { @@ -52,6 +53,7 @@ have unexpected values then get drawn to the color buffer, which is later checke .fn(async t => { const { format, unclippedDepth, writeDepth, multisampled } = t.params; const info = kTextureFormatInfo[format]; + assert(!!info.depth); /** Number of depth values to test for both vertex output and frag_depth output. */ const kNumDepthValues = 8; @@ -222,16 +224,16 @@ have unexpected values then get drawn to the color buffer, which is later checke : undefined; const dsActual = - !multisampled && info.bytesPerBlock + !multisampled && info.depth.bytes ? t.device.createBuffer({ - size: kNumTestPoints * info.bytesPerBlock, + size: kNumTestPoints * info.depth.bytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }) : undefined; const dsExpected = - !multisampled && info.bytesPerBlock + !multisampled && info.depth.bytes ? t.device.createBuffer({ - size: kNumTestPoints * info.bytesPerBlock, + size: kNumTestPoints * info.depth.bytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }) : undefined; @@ -270,7 +272,9 @@ have unexpected values then get drawn to the color buffer, which is later checke pass.end(); } if (dsActual) { - enc.copyTextureToBuffer({ texture: dsTexture }, { buffer: dsActual }, [kNumTestPoints]); + enc.copyTextureToBuffer({ texture: dsTexture, aspect: 'depth-only' }, { buffer: dsActual }, [ + kNumTestPoints, + ]); } { const clearValue = [0, 0, 0, 0]; // Will see this color if the check passed. @@ -302,7 +306,11 @@ have unexpected values then get drawn to the color buffer, which is later checke } enc.copyTextureToBuffer({ texture: checkTexture }, { buffer: checkBuffer }, [kNumTestPoints]); if (dsExpected) { - enc.copyTextureToBuffer({ texture: dsTexture }, { buffer: dsExpected }, [kNumTestPoints]); + enc.copyTextureToBuffer( + { texture: dsTexture, aspect: 'depth-only' }, + { buffer: dsExpected }, + [kNumTestPoints] + ); } t.device.queue.submit([enc.finish()]); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts index 2a4ca5e6a4..546db16c7e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts @@ -1,7 +1,8 @@ import { assert } from '../../../../../common/util/util.js'; import { kTextureFormatInfo, EncodableTextureFormat } from '../../../../format_info.js'; import { virtualMipSize } from '../../../../util/texture/base.js'; -import { CheckContents } from '../texture_zero.spec.js'; + +import { CheckContents } from './texture_zero_init_test.js'; export const checkContentsByBufferCopy: CheckContents = ( t, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts index 8646062452..72ef2909f2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts @@ -2,7 +2,8 @@ import { assert } from '../../../../../common/util/util.js'; import { kTextureFormatInfo } from '../../../../format_info.js'; import { GPUTest } from '../../../../gpu_test.js'; import { virtualMipSize } from '../../../../util/texture/base.js'; -import { CheckContents } from '../texture_zero.spec.js'; + +import { CheckContents } from './texture_zero_init_test.js'; function makeFullscreenVertexModule(device: GPUDevice) { return device.createShaderModule({ diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts index 64b4f73b34..fd268be3b9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts @@ -6,7 +6,8 @@ import { getSingleDataType, getComponentReadbackTraits, } from '../../../../util/texture/texel_data.js'; -import { CheckContents } from '../texture_zero.spec.js'; + +import { CheckContents } from './texture_zero_init_test.js'; export const checkContentsBySampling: CheckContents = ( t, @@ -41,14 +42,20 @@ export const checkContentsBySampling: CheckContents = ( ? componentOrder[0].toLowerCase() : componentOrder.map(c => c.toLowerCase()).join('') + '[i]'; - const _xd = '_' + params.dimension; + const viewDimension = + t.isCompatibility && params.dimension === '2d' && texture.depthOrArrayLayers > 1 + ? '2d-array' + : params.dimension; + const _xd = `_${viewDimension.replace('-', '_')}`; const _multisampled = params.sampleCount > 1 ? '_multisampled' : ''; const texelIndexExpression = - params.dimension === '2d' + viewDimension === '2d' ? 'vec2<i32>(GlobalInvocationID.xy)' - : params.dimension === '3d' + : viewDimension === '2d-array' + ? 'vec2<i32>(GlobalInvocationID.xy), constants.layer' + : viewDimension === '3d' ? 'vec3<i32>(GlobalInvocationID.xyz)' - : params.dimension === '1d' + : viewDimension === '1d' ? 'i32(GlobalInvocationID.x)' : unreachable(); const computePipeline = t.device.createComputePipeline({ @@ -58,7 +65,8 @@ export const checkContentsBySampling: CheckContents = ( module: t.device.createShaderModule({ code: ` struct Constants { - level : i32 + level : i32, + layer : i32, }; @group(0) @binding(0) var<uniform> constants : Constants; @@ -90,10 +98,10 @@ export const checkContentsBySampling: CheckContents = ( for (const layer of layers) { const ubo = t.device.createBuffer({ mappedAtCreation: true, - size: 4, + size: 8, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - new Int32Array(ubo.getMappedRange(), 0, 1)[0] = level; + new Int32Array(ubo.getMappedRange()).set([level, layer]); ubo.unmap(); const byteLength = @@ -104,6 +112,14 @@ export const checkContentsBySampling: CheckContents = ( }); t.trackForCleanup(resultBuffer); + const viewDescriptor: GPUTextureViewDescriptor = { + ...(!t.isCompatibility && { + baseArrayLayer: layer, + arrayLayerCount: 1, + }), + dimension: viewDimension, + }; + const bindGroup = t.device.createBindGroup({ layout: computePipeline.getBindGroupLayout(0), entries: [ @@ -113,11 +129,7 @@ export const checkContentsBySampling: CheckContents = ( }, { binding: 1, - resource: texture.createView({ - baseArrayLayer: layer, - arrayLayerCount: 1, - dimension: params.dimension, - }), + resource: texture.createView(viewDescriptor), }, { binding: 3, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/texture_zero_init_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/texture_zero_init_test.ts new file mode 100644 index 0000000000..6ff3ab4c9b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/texture_zero_init_test.ts @@ -0,0 +1,548 @@ +import { TestCaseRecorder, TestParams } from '../../../../../common/framework/fixture.js'; +import { + kUnitCaseParamsBuilder, + ParamTypeOf, +} from '../../../../../common/framework/params_builder.js'; +import { assert, unreachable } from '../../../../../common/util/util.js'; +import { kTextureAspects, kTextureDimensions } from '../../../../capability_info.js'; +import { GPUConst } from '../../../../constants.js'; +import { + kTextureFormatInfo, + kUncompressedTextureFormats, + textureDimensionAndFormatCompatible, + UncompressedTextureFormat, + EncodableTextureFormat, +} from '../../../../format_info.js'; +import { GPUTest, GPUTestSubcaseBatchState } from '../../../../gpu_test.js'; +import { virtualMipSize } from '../../../../util/texture/base.js'; +import { createTextureUploadBuffer } from '../../../../util/texture/layout.js'; +import { BeginEndRange, SubresourceRange } from '../../../../util/texture/subresource.js'; +import { + PerTexelComponent, + kTexelRepresentationInfo, +} from '../../../../util/texture/texel_data.js'; + +export enum UninitializeMethod { + Creation = 'Creation', // The texture was just created. It is uninitialized. + StoreOpClear = 'StoreOpClear', // The texture was rendered to with GPUStoreOp "clear" +} +const kUninitializeMethods = Object.keys(UninitializeMethod) as UninitializeMethod[]; + +export const enum ReadMethod { + Sample = 'Sample', // The texture is sampled from + CopyToBuffer = 'CopyToBuffer', // The texture is copied to a buffer + CopyToTexture = 'CopyToTexture', // The texture is copied to another texture + DepthTest = 'DepthTest', // The texture is read as a depth buffer + StencilTest = 'StencilTest', // The texture is read as a stencil buffer + ColorBlending = 'ColorBlending', // Read the texture by blending as a color attachment + Storage = 'Storage', // Read the texture as a storage texture +} + +// Test with these mip level counts +type MipLevels = 1 | 5; +const kMipLevelCounts: MipLevels[] = [1, 5]; + +// For each mip level count, define the mip ranges to leave uninitialized. +const kUninitializedMipRangesToTest: { [k in MipLevels]: BeginEndRange[] } = { + 1: [{ begin: 0, end: 1 }], // Test the only mip + 5: [ + { begin: 0, end: 2 }, + { begin: 3, end: 4 }, + ], // Test a range and a single mip +}; + +// Test with these sample counts. +const kSampleCounts: number[] = [1, 4]; + +// Test with these layer counts. +type LayerCounts = 1 | 7; + +// For each layer count, define the layers to leave uninitialized. +const kUninitializedLayerRangesToTest: { [k in LayerCounts]: BeginEndRange[] } = { + 1: [{ begin: 0, end: 1 }], // Test the only layer + 7: [ + { begin: 2, end: 4 }, + { begin: 6, end: 7 }, + ], // Test a range and a single layer +}; + +// Enums to abstract over color / depth / stencil values in textures. Depending on the texture format, +// the data for each value may have a different representation. These enums are converted to a +// representation such that their values can be compared. ex.) An integer is needed to upload to an +// unsigned normalized format, but its value is read as a float in the shader. +export const enum InitializedState { + Canary, // Set on initialized subresources. It should stay the same. On discarded resources, we should observe zero. + Zero, // We check that uninitialized subresources are in this state when read back. +} + +const initializedStateAsFloat = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 1, +}; + +const initializedStateAsUint = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 1, +}; + +const initializedStateAsSint = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: -1, +}; + +function initializedStateAsColor( + state: InitializedState, + format: GPUTextureFormat +): [number, number, number, number] { + let value; + if (format.indexOf('uint') !== -1) { + value = initializedStateAsUint[state]; + } else if (format.indexOf('sint') !== -1) { + value = initializedStateAsSint[state]; + } else { + value = initializedStateAsFloat[state]; + } + return [value, value, value, value]; +} + +const initializedStateAsDepth = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 0.8, +}; + +const initializedStateAsStencil = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 42, +}; + +function allAspectsCopyDst(info: (typeof kTextureFormatInfo)[UncompressedTextureFormat]) { + return ( + (!info.color || info.color.copyDst) && + (!info.depth || info.depth.copyDst) && + (!info.stencil || info.stencil.copyDst) + ); +} + +export function getRequiredTextureUsage( + format: UncompressedTextureFormat, + sampleCount: number, + uninitializeMethod: UninitializeMethod, + readMethod: ReadMethod +): GPUTextureUsageFlags { + let usage: GPUTextureUsageFlags = GPUConst.TextureUsage.COPY_DST; + + switch (uninitializeMethod) { + case UninitializeMethod.Creation: + break; + case UninitializeMethod.StoreOpClear: + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + break; + default: + unreachable(); + } + + switch (readMethod) { + case ReadMethod.CopyToBuffer: + case ReadMethod.CopyToTexture: + usage |= GPUConst.TextureUsage.COPY_SRC; + break; + case ReadMethod.Sample: + usage |= GPUConst.TextureUsage.TEXTURE_BINDING; + break; + case ReadMethod.Storage: + usage |= GPUConst.TextureUsage.STORAGE_BINDING; + break; + case ReadMethod.DepthTest: + case ReadMethod.StencilTest: + case ReadMethod.ColorBlending: + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + break; + default: + unreachable(); + } + + if (sampleCount > 1) { + // Copies to multisampled textures are not allowed. We need OutputAttachment to initialize + // canary data in multisampled textures. + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + } + + const info = kTextureFormatInfo[format]; + if (!allAspectsCopyDst(info)) { + // Copies are not possible. We need OutputAttachment to initialize + // canary data. + if (info.color) assert(!!info.colorRender, 'not implemented for non-renderable color'); + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + } + + return usage; +} + +export class TextureZeroInitTest extends GPUTest { + readonly stateToTexelComponents: { [k in InitializedState]: PerTexelComponent<number> }; + + private p: TextureZeroParams; + constructor(sharedState: GPUTestSubcaseBatchState, rec: TestCaseRecorder, params: TestParams) { + super(sharedState, rec, params); + this.p = params as TextureZeroParams; + + const stateToTexelComponents = (state: InitializedState) => { + const [R, G, B, A] = initializedStateAsColor(state, this.p.format); + return { + R, + G, + B, + A, + Depth: initializedStateAsDepth[state], + Stencil: initializedStateAsStencil[state], + }; + }; + + this.stateToTexelComponents = { + [InitializedState.Zero]: stateToTexelComponents(InitializedState.Zero), + [InitializedState.Canary]: stateToTexelComponents(InitializedState.Canary), + }; + } + + get textureWidth(): number { + let width = 1 << this.p.mipLevelCount; + if (this.p.nonPowerOfTwo) { + width = 2 * width - 1; + } + return width; + } + + get textureHeight(): number { + if (this.p.dimension === '1d') { + return 1; + } + + let height = 1 << this.p.mipLevelCount; + if (this.p.nonPowerOfTwo) { + height = 2 * height - 1; + } + return height; + } + + get textureDepth(): number { + return this.p.dimension === '3d' ? 11 : 1; + } + + get textureDepthOrArrayLayers(): number { + return this.p.dimension === '2d' ? this.p.layerCount : this.textureDepth; + } + + // Used to iterate subresources and check that their uninitialized contents are zero when accessed + *iterateUninitializedSubresources(): Generator<SubresourceRange> { + for (const mipRange of kUninitializedMipRangesToTest[this.p.mipLevelCount]) { + for (const layerRange of kUninitializedLayerRangesToTest[this.p.layerCount]) { + yield new SubresourceRange({ mipRange, layerRange }); + } + } + } + + // Used to iterate and initialize other subresources not checked for zero-initialization. + // Zero-initialization of uninitialized subresources should not have side effects on already + // initialized subresources. + *iterateInitializedSubresources(): Generator<SubresourceRange> { + const uninitialized: boolean[][] = new Array(this.p.mipLevelCount); + for (let level = 0; level < uninitialized.length; ++level) { + uninitialized[level] = new Array(this.p.layerCount); + } + for (const subresources of this.iterateUninitializedSubresources()) { + for (const { level, layer } of subresources.each()) { + uninitialized[level][layer] = true; + } + } + for (let level = 0; level < uninitialized.length; ++level) { + for (let layer = 0; layer < uninitialized[level].length; ++layer) { + if (!uninitialized[level][layer]) { + yield new SubresourceRange({ + mipRange: { begin: level, count: 1 }, + layerRange: { begin: layer, count: 1 }, + }); + } + } + } + } + + *generateTextureViewDescriptorsForRendering( + aspect: GPUTextureAspect, + subresourceRange?: SubresourceRange + ): Generator<GPUTextureViewDescriptor> { + const viewDescriptor: GPUTextureViewDescriptor = { + dimension: '2d', + aspect, + }; + + if (subresourceRange === undefined) { + return viewDescriptor; + } + + for (const { level, layer } of subresourceRange.each()) { + yield { + ...viewDescriptor, + baseMipLevel: level, + mipLevelCount: 1, + baseArrayLayer: layer, + arrayLayerCount: 1, + }; + } + } + + private initializeWithStoreOp( + state: InitializedState, + texture: GPUTexture, + subresourceRange?: SubresourceRange + ): void { + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.pushDebugGroup('initializeWithStoreOp'); + + for (const viewDescriptor of this.generateTextureViewDescriptorsForRendering( + 'all', + subresourceRange + )) { + if (kTextureFormatInfo[this.p.format].color) { + commandEncoder + .beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(viewDescriptor), + storeOp: 'store', + clearValue: initializedStateAsColor(state, this.p.format), + loadOp: 'clear', + }, + ], + }) + .end(); + } else { + const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { + view: texture.createView(viewDescriptor), + }; + if (kTextureFormatInfo[this.p.format].depth) { + depthStencilAttachment.depthClearValue = initializedStateAsDepth[state]; + depthStencilAttachment.depthLoadOp = 'clear'; + depthStencilAttachment.depthStoreOp = 'store'; + } + if (kTextureFormatInfo[this.p.format].stencil) { + depthStencilAttachment.stencilClearValue = initializedStateAsStencil[state]; + depthStencilAttachment.stencilLoadOp = 'clear'; + depthStencilAttachment.stencilStoreOp = 'store'; + } + commandEncoder + .beginRenderPass({ + colorAttachments: [], + depthStencilAttachment, + }) + .end(); + } + } + + commandEncoder.popDebugGroup(); + this.queue.submit([commandEncoder.finish()]); + } + + private initializeWithCopy( + texture: GPUTexture, + state: InitializedState, + subresourceRange: SubresourceRange + ): void { + assert(this.p.format in kTextureFormatInfo); + const format = this.p.format as EncodableTextureFormat; + + const firstSubresource = subresourceRange.each().next().value; + assert(typeof firstSubresource !== 'undefined'); + + const [largestWidth, largestHeight, largestDepth] = virtualMipSize( + this.p.dimension, + [this.textureWidth, this.textureHeight, this.textureDepth], + firstSubresource.level + ); + + const rep = kTexelRepresentationInfo[format]; + const texelData = new Uint8Array(rep.pack(rep.encode(this.stateToTexelComponents[state]))); + const { buffer, bytesPerRow, rowsPerImage } = createTextureUploadBuffer( + texelData, + this.device, + format, + this.p.dimension, + [largestWidth, largestHeight, largestDepth] + ); + + const commandEncoder = this.device.createCommandEncoder(); + + for (const { level, layer } of subresourceRange.each()) { + const [width, height, depth] = virtualMipSize( + this.p.dimension, + [this.textureWidth, this.textureHeight, this.textureDepth], + level + ); + + commandEncoder.copyBufferToTexture( + { + buffer, + bytesPerRow, + rowsPerImage, + }, + { texture, mipLevel: level, origin: { x: 0, y: 0, z: layer } }, + { width, height, depthOrArrayLayers: depth } + ); + } + this.queue.submit([commandEncoder.finish()]); + buffer.destroy(); + } + + initializeTexture( + texture: GPUTexture, + state: InitializedState, + subresourceRange: SubresourceRange + ): void { + const info = kTextureFormatInfo[this.p.format]; + if (this.p.sampleCount > 1 || !allAspectsCopyDst(info)) { + // Copies to multisampled textures not yet specified. + // Use a storeOp for now. + if (info.color) assert(!!info.colorRender, 'not implemented for non-renderable color'); + this.initializeWithStoreOp(state, texture, subresourceRange); + } else { + this.initializeWithCopy(texture, state, subresourceRange); + } + } + + discardTexture(texture: GPUTexture, subresourceRange: SubresourceRange): void { + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.pushDebugGroup('discardTexture'); + + for (const desc of this.generateTextureViewDescriptorsForRendering('all', subresourceRange)) { + if (kTextureFormatInfo[this.p.format].color) { + commandEncoder + .beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(desc), + storeOp: 'discard', + loadOp: 'load', + }, + ], + }) + .end(); + } else { + const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { + view: texture.createView(desc), + }; + if (kTextureFormatInfo[this.p.format].depth) { + depthStencilAttachment.depthLoadOp = 'load'; + depthStencilAttachment.depthStoreOp = 'discard'; + } + if (kTextureFormatInfo[this.p.format].stencil) { + depthStencilAttachment.stencilLoadOp = 'load'; + depthStencilAttachment.stencilStoreOp = 'discard'; + } + commandEncoder + .beginRenderPass({ + colorAttachments: [], + depthStencilAttachment, + }) + .end(); + } + } + + commandEncoder.popDebugGroup(); + this.queue.submit([commandEncoder.finish()]); + } +} + +export const kTestParams = kUnitCaseParamsBuilder + .combine('dimension', kTextureDimensions) + .combine('readMethod', [ + ReadMethod.CopyToBuffer, + ReadMethod.CopyToTexture, + ReadMethod.Sample, + ReadMethod.DepthTest, + ReadMethod.StencilTest, + ]) + // [3] compressed formats + .combine('format', kUncompressedTextureFormats) + .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format)) + .beginSubcases() + .combine('aspect', kTextureAspects) + .unless(({ readMethod, format, aspect }) => { + const info = kTextureFormatInfo[format]; + return ( + (readMethod === ReadMethod.DepthTest && (!info.depth || aspect === 'stencil-only')) || + (readMethod === ReadMethod.StencilTest && (!info.stencil || aspect === 'depth-only')) || + (readMethod === ReadMethod.ColorBlending && !info.color) || + // [1]: Test with depth/stencil sampling + (readMethod === ReadMethod.Sample && (!!info.depth || !!info.stencil)) || + (aspect === 'depth-only' && !info.depth) || + (aspect === 'stencil-only' && !info.stencil) || + (aspect === 'all' && !!info.depth && !!info.stencil) || + // Cannot copy from a packed depth format. + // [2]: Test copying out of the stencil aspect. + ((readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) && + (format === 'depth24plus' || format === 'depth24plus-stencil8')) + ); + }) + .combine('mipLevelCount', kMipLevelCounts) + // 1D texture can only have a single mip level + .unless(p => p.dimension === '1d' && p.mipLevelCount !== 1) + .combine('sampleCount', kSampleCounts) + .unless( + ({ readMethod, sampleCount }) => + // We can only read from multisampled textures by sampling. + sampleCount > 1 && + (readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) + ) + // Multisampled textures may only have one mip + .unless(({ sampleCount, mipLevelCount }) => sampleCount > 1 && mipLevelCount > 1) + .combine('uninitializeMethod', kUninitializeMethods) + .unless(({ dimension, readMethod, uninitializeMethod, format, sampleCount }) => { + const formatInfo = kTextureFormatInfo[format]; + return ( + dimension !== '2d' && + (sampleCount > 1 || + !!formatInfo.depth || + !!formatInfo.stencil || + readMethod === ReadMethod.DepthTest || + readMethod === ReadMethod.StencilTest || + readMethod === ReadMethod.ColorBlending || + uninitializeMethod === UninitializeMethod.StoreOpClear) + ); + }) + .expandWithParams(function* ({ dimension }) { + switch (dimension) { + case '2d': + yield { layerCount: 1 as LayerCounts }; + yield { layerCount: 7 as LayerCounts }; + break; + case '1d': + case '3d': + yield { layerCount: 1 as LayerCounts }; + break; + } + }) + // Multisampled 3D / 2D array textures not supported. + .unless(({ sampleCount, layerCount }) => sampleCount > 1 && layerCount > 1) + .unless(({ format, sampleCount, uninitializeMethod, readMethod }) => { + const usage = getRequiredTextureUsage(format, sampleCount, uninitializeMethod, readMethod); + const info = kTextureFormatInfo[format]; + + return ( + ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 && + info.color && + !info.colorRender) || + ((usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 && !info.color?.storage) || + (sampleCount > 1 && !info.multisample) + ); + }) + .combine('nonPowerOfTwo', [false, true]) + .combine('canaryOnCreation', [false, true]); + +type TextureZeroParams = ParamTypeOf<typeof kTestParams>; + +export type CheckContents = ( + t: TextureZeroInitTest, + params: TextureZeroParams, + texture: GPUTexture, + state: InitializedState, + subresourceRange: SubresourceRange +) => void; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts index 3f0baeccbd..ee4c141aef 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts @@ -7,550 +7,9 @@ TODO: - test compressed texture formats [3] `; -// MAINTENANCE_TODO: This is a test file, it probably shouldn't export anything. -// Everything that's exported should be moved to another file. - -import { TestCaseRecorder, TestParams } from '../../../../common/framework/fixture.js'; -import { - kUnitCaseParamsBuilder, - ParamTypeOf, -} from '../../../../common/framework/params_builder.js'; import { makeTestGroup } from '../../../../common/framework/test_group.js'; -import { assert, unreachable } from '../../../../common/util/util.js'; -import { kTextureAspects, kTextureDimensions } from '../../../capability_info.js'; -import { GPUConst } from '../../../constants.js'; -import { - kTextureFormatInfo, - kUncompressedTextureFormats, - textureDimensionAndFormatCompatible, - UncompressedTextureFormat, - EncodableTextureFormat, -} from '../../../format_info.js'; -import { GPUTest, GPUTestSubcaseBatchState } from '../../../gpu_test.js'; -import { virtualMipSize } from '../../../util/texture/base.js'; -import { createTextureUploadBuffer } from '../../../util/texture/layout.js'; -import { BeginEndRange, SubresourceRange } from '../../../util/texture/subresource.js'; -import { PerTexelComponent, kTexelRepresentationInfo } from '../../../util/texture/texel_data.js'; - -export enum UninitializeMethod { - Creation = 'Creation', // The texture was just created. It is uninitialized. - StoreOpClear = 'StoreOpClear', // The texture was rendered to with GPUStoreOp "clear" -} -const kUninitializeMethods = Object.keys(UninitializeMethod) as UninitializeMethod[]; - -export const enum ReadMethod { - Sample = 'Sample', // The texture is sampled from - CopyToBuffer = 'CopyToBuffer', // The texture is copied to a buffer - CopyToTexture = 'CopyToTexture', // The texture is copied to another texture - DepthTest = 'DepthTest', // The texture is read as a depth buffer - StencilTest = 'StencilTest', // The texture is read as a stencil buffer - ColorBlending = 'ColorBlending', // Read the texture by blending as a color attachment - Storage = 'Storage', // Read the texture as a storage texture -} - -// Test with these mip level counts -type MipLevels = 1 | 5; -const kMipLevelCounts: MipLevels[] = [1, 5]; - -// For each mip level count, define the mip ranges to leave uninitialized. -const kUninitializedMipRangesToTest: { [k in MipLevels]: BeginEndRange[] } = { - 1: [{ begin: 0, end: 1 }], // Test the only mip - 5: [ - { begin: 0, end: 2 }, - { begin: 3, end: 4 }, - ], // Test a range and a single mip -}; - -// Test with these sample counts. -const kSampleCounts: number[] = [1, 4]; - -// Test with these layer counts. -type LayerCounts = 1 | 7; - -// For each layer count, define the layers to leave uninitialized. -const kUninitializedLayerRangesToTest: { [k in LayerCounts]: BeginEndRange[] } = { - 1: [{ begin: 0, end: 1 }], // Test the only layer - 7: [ - { begin: 2, end: 4 }, - { begin: 6, end: 7 }, - ], // Test a range and a single layer -}; - -// Enums to abstract over color / depth / stencil values in textures. Depending on the texture format, -// the data for each value may have a different representation. These enums are converted to a -// representation such that their values can be compared. ex.) An integer is needed to upload to an -// unsigned normalized format, but its value is read as a float in the shader. -export const enum InitializedState { - Canary, // Set on initialized subresources. It should stay the same. On discarded resources, we should observe zero. - Zero, // We check that uninitialized subresources are in this state when read back. -} - -const initializedStateAsFloat = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 1, -}; - -const initializedStateAsUint = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 1, -}; - -const initializedStateAsSint = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: -1, -}; - -function initializedStateAsColor( - state: InitializedState, - format: GPUTextureFormat -): [number, number, number, number] { - let value; - if (format.indexOf('uint') !== -1) { - value = initializedStateAsUint[state]; - } else if (format.indexOf('sint') !== -1) { - value = initializedStateAsSint[state]; - } else { - value = initializedStateAsFloat[state]; - } - return [value, value, value, value]; -} - -const initializedStateAsDepth = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 0.8, -}; - -const initializedStateAsStencil = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 42, -}; - -function getRequiredTextureUsage( - format: UncompressedTextureFormat, - sampleCount: number, - uninitializeMethod: UninitializeMethod, - readMethod: ReadMethod -): GPUTextureUsageFlags { - let usage: GPUTextureUsageFlags = GPUConst.TextureUsage.COPY_DST; - - switch (uninitializeMethod) { - case UninitializeMethod.Creation: - break; - case UninitializeMethod.StoreOpClear: - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - break; - default: - unreachable(); - } - - switch (readMethod) { - case ReadMethod.CopyToBuffer: - case ReadMethod.CopyToTexture: - usage |= GPUConst.TextureUsage.COPY_SRC; - break; - case ReadMethod.Sample: - usage |= GPUConst.TextureUsage.TEXTURE_BINDING; - break; - case ReadMethod.Storage: - usage |= GPUConst.TextureUsage.STORAGE_BINDING; - break; - case ReadMethod.DepthTest: - case ReadMethod.StencilTest: - case ReadMethod.ColorBlending: - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - break; - default: - unreachable(); - } - - if (sampleCount > 1) { - // Copies to multisampled textures are not allowed. We need OutputAttachment to initialize - // canary data in multisampled textures. - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - } - - if (!kTextureFormatInfo[format].copyDst) { - // Copies are not possible. We need OutputAttachment to initialize - // canary data. - assert(kTextureFormatInfo[format].renderable); - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - } - - return usage; -} - -export class TextureZeroInitTest extends GPUTest { - readonly stateToTexelComponents: { [k in InitializedState]: PerTexelComponent<number> }; - - private p: TextureZeroParams; - constructor(sharedState: GPUTestSubcaseBatchState, rec: TestCaseRecorder, params: TestParams) { - super(sharedState, rec, params); - this.p = params as TextureZeroParams; - - const stateToTexelComponents = (state: InitializedState) => { - const [R, G, B, A] = initializedStateAsColor(state, this.p.format); - return { - R, - G, - B, - A, - Depth: initializedStateAsDepth[state], - Stencil: initializedStateAsStencil[state], - }; - }; - - this.stateToTexelComponents = { - [InitializedState.Zero]: stateToTexelComponents(InitializedState.Zero), - [InitializedState.Canary]: stateToTexelComponents(InitializedState.Canary), - }; - } - - get textureWidth(): number { - let width = 1 << this.p.mipLevelCount; - if (this.p.nonPowerOfTwo) { - width = 2 * width - 1; - } - return width; - } - - get textureHeight(): number { - if (this.p.dimension === '1d') { - return 1; - } - - let height = 1 << this.p.mipLevelCount; - if (this.p.nonPowerOfTwo) { - height = 2 * height - 1; - } - return height; - } - - get textureDepth(): number { - return this.p.dimension === '3d' ? 11 : 1; - } - - get textureDepthOrArrayLayers(): number { - return this.p.dimension === '2d' ? this.p.layerCount : this.textureDepth; - } - - // Used to iterate subresources and check that their uninitialized contents are zero when accessed - *iterateUninitializedSubresources(): Generator<SubresourceRange> { - for (const mipRange of kUninitializedMipRangesToTest[this.p.mipLevelCount]) { - for (const layerRange of kUninitializedLayerRangesToTest[this.p.layerCount]) { - yield new SubresourceRange({ mipRange, layerRange }); - } - } - } - - // Used to iterate and initialize other subresources not checked for zero-initialization. - // Zero-initialization of uninitialized subresources should not have side effects on already - // initialized subresources. - *iterateInitializedSubresources(): Generator<SubresourceRange> { - const uninitialized: boolean[][] = new Array(this.p.mipLevelCount); - for (let level = 0; level < uninitialized.length; ++level) { - uninitialized[level] = new Array(this.p.layerCount); - } - for (const subresources of this.iterateUninitializedSubresources()) { - for (const { level, layer } of subresources.each()) { - uninitialized[level][layer] = true; - } - } - for (let level = 0; level < uninitialized.length; ++level) { - for (let layer = 0; layer < uninitialized[level].length; ++layer) { - if (!uninitialized[level][layer]) { - yield new SubresourceRange({ - mipRange: { begin: level, count: 1 }, - layerRange: { begin: layer, count: 1 }, - }); - } - } - } - } - - *generateTextureViewDescriptorsForRendering( - aspect: GPUTextureAspect, - subresourceRange?: SubresourceRange - ): Generator<GPUTextureViewDescriptor> { - const viewDescriptor: GPUTextureViewDescriptor = { - dimension: '2d', - aspect, - }; - - if (subresourceRange === undefined) { - return viewDescriptor; - } - - for (const { level, layer } of subresourceRange.each()) { - yield { - ...viewDescriptor, - baseMipLevel: level, - mipLevelCount: 1, - baseArrayLayer: layer, - arrayLayerCount: 1, - }; - } - } - - private initializeWithStoreOp( - state: InitializedState, - texture: GPUTexture, - subresourceRange?: SubresourceRange - ): void { - const commandEncoder = this.device.createCommandEncoder(); - commandEncoder.pushDebugGroup('initializeWithStoreOp'); - - for (const viewDescriptor of this.generateTextureViewDescriptorsForRendering( - 'all', - subresourceRange - )) { - if (kTextureFormatInfo[this.p.format].color) { - commandEncoder - .beginRenderPass({ - colorAttachments: [ - { - view: texture.createView(viewDescriptor), - storeOp: 'store', - clearValue: initializedStateAsColor(state, this.p.format), - loadOp: 'clear', - }, - ], - }) - .end(); - } else { - const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { - view: texture.createView(viewDescriptor), - }; - if (kTextureFormatInfo[this.p.format].depth) { - depthStencilAttachment.depthClearValue = initializedStateAsDepth[state]; - depthStencilAttachment.depthLoadOp = 'clear'; - depthStencilAttachment.depthStoreOp = 'store'; - } - if (kTextureFormatInfo[this.p.format].stencil) { - depthStencilAttachment.stencilClearValue = initializedStateAsStencil[state]; - depthStencilAttachment.stencilLoadOp = 'clear'; - depthStencilAttachment.stencilStoreOp = 'store'; - } - commandEncoder - .beginRenderPass({ - colorAttachments: [], - depthStencilAttachment, - }) - .end(); - } - } - - commandEncoder.popDebugGroup(); - this.queue.submit([commandEncoder.finish()]); - } - - private initializeWithCopy( - texture: GPUTexture, - state: InitializedState, - subresourceRange: SubresourceRange - ): void { - assert(this.p.format in kTextureFormatInfo); - const format = this.p.format as EncodableTextureFormat; - - const firstSubresource = subresourceRange.each().next().value; - assert(typeof firstSubresource !== 'undefined'); - - const [largestWidth, largestHeight, largestDepth] = virtualMipSize( - this.p.dimension, - [this.textureWidth, this.textureHeight, this.textureDepth], - firstSubresource.level - ); - - const rep = kTexelRepresentationInfo[format]; - const texelData = new Uint8Array(rep.pack(rep.encode(this.stateToTexelComponents[state]))); - const { buffer, bytesPerRow, rowsPerImage } = createTextureUploadBuffer( - texelData, - this.device, - format, - this.p.dimension, - [largestWidth, largestHeight, largestDepth] - ); - - const commandEncoder = this.device.createCommandEncoder(); - - for (const { level, layer } of subresourceRange.each()) { - const [width, height, depth] = virtualMipSize( - this.p.dimension, - [this.textureWidth, this.textureHeight, this.textureDepth], - level - ); - - commandEncoder.copyBufferToTexture( - { - buffer, - bytesPerRow, - rowsPerImage, - }, - { texture, mipLevel: level, origin: { x: 0, y: 0, z: layer } }, - { width, height, depthOrArrayLayers: depth } - ); - } - this.queue.submit([commandEncoder.finish()]); - buffer.destroy(); - } - - initializeTexture( - texture: GPUTexture, - state: InitializedState, - subresourceRange: SubresourceRange - ): void { - if (this.p.sampleCount > 1 || !kTextureFormatInfo[this.p.format].copyDst) { - // Copies to multisampled textures not yet specified. - // Use a storeOp for now. - assert(kTextureFormatInfo[this.p.format].renderable); - this.initializeWithStoreOp(state, texture, subresourceRange); - } else { - this.initializeWithCopy(texture, state, subresourceRange); - } - } - - discardTexture(texture: GPUTexture, subresourceRange: SubresourceRange): void { - const commandEncoder = this.device.createCommandEncoder(); - commandEncoder.pushDebugGroup('discardTexture'); - - for (const desc of this.generateTextureViewDescriptorsForRendering('all', subresourceRange)) { - if (kTextureFormatInfo[this.p.format].color) { - commandEncoder - .beginRenderPass({ - colorAttachments: [ - { - view: texture.createView(desc), - storeOp: 'discard', - loadOp: 'load', - }, - ], - }) - .end(); - } else { - const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { - view: texture.createView(desc), - }; - if (kTextureFormatInfo[this.p.format].depth) { - depthStencilAttachment.depthLoadOp = 'load'; - depthStencilAttachment.depthStoreOp = 'discard'; - } - if (kTextureFormatInfo[this.p.format].stencil) { - depthStencilAttachment.stencilLoadOp = 'load'; - depthStencilAttachment.stencilStoreOp = 'discard'; - } - commandEncoder - .beginRenderPass({ - colorAttachments: [], - depthStencilAttachment, - }) - .end(); - } - } - - commandEncoder.popDebugGroup(); - this.queue.submit([commandEncoder.finish()]); - } -} - -const kTestParams = kUnitCaseParamsBuilder - .combine('dimension', kTextureDimensions) - .combine('readMethod', [ - ReadMethod.CopyToBuffer, - ReadMethod.CopyToTexture, - ReadMethod.Sample, - ReadMethod.DepthTest, - ReadMethod.StencilTest, - ]) - // [3] compressed formats - .combine('format', kUncompressedTextureFormats) - .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format)) - .beginSubcases() - .combine('aspect', kTextureAspects) - .unless(({ readMethod, format, aspect }) => { - const info = kTextureFormatInfo[format]; - return ( - (readMethod === ReadMethod.DepthTest && (!info.depth || aspect === 'stencil-only')) || - (readMethod === ReadMethod.StencilTest && (!info.stencil || aspect === 'depth-only')) || - (readMethod === ReadMethod.ColorBlending && !info.color) || - // [1]: Test with depth/stencil sampling - (readMethod === ReadMethod.Sample && (!!info.depth || !!info.stencil)) || - (aspect === 'depth-only' && !info.depth) || - (aspect === 'stencil-only' && !info.stencil) || - (aspect === 'all' && !!info.depth && !!info.stencil) || - // Cannot copy from a packed depth format. - // [2]: Test copying out of the stencil aspect. - ((readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) && - (format === 'depth24plus' || format === 'depth24plus-stencil8')) - ); - }) - .combine('mipLevelCount', kMipLevelCounts) - // 1D texture can only have a single mip level - .unless(p => p.dimension === '1d' && p.mipLevelCount !== 1) - .combine('sampleCount', kSampleCounts) - .unless( - ({ readMethod, sampleCount }) => - // We can only read from multisampled textures by sampling. - sampleCount > 1 && - (readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) - ) - // Multisampled textures may only have one mip - .unless(({ sampleCount, mipLevelCount }) => sampleCount > 1 && mipLevelCount > 1) - .combine('uninitializeMethod', kUninitializeMethods) - .unless(({ dimension, readMethod, uninitializeMethod, format, sampleCount }) => { - const formatInfo = kTextureFormatInfo[format]; - return ( - dimension !== '2d' && - (sampleCount > 1 || - !!formatInfo.depth || - !!formatInfo.stencil || - readMethod === ReadMethod.DepthTest || - readMethod === ReadMethod.StencilTest || - readMethod === ReadMethod.ColorBlending || - uninitializeMethod === UninitializeMethod.StoreOpClear) - ); - }) - .expandWithParams(function* ({ dimension }) { - switch (dimension) { - case '2d': - yield { layerCount: 1 as LayerCounts }; - yield { layerCount: 7 as LayerCounts }; - break; - case '1d': - case '3d': - yield { layerCount: 1 as LayerCounts }; - break; - } - }) - // Multisampled 3D / 2D array textures not supported. - .unless(({ sampleCount, layerCount }) => sampleCount > 1 && layerCount > 1) - .unless(({ format, sampleCount, uninitializeMethod, readMethod }) => { - const usage = getRequiredTextureUsage(format, sampleCount, uninitializeMethod, readMethod); - const info = kTextureFormatInfo[format]; - - return ( - ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 && !info.renderable) || - ((usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 && !info.color?.storage) || - (sampleCount > 1 && !info.multisample) - ); - }) - .combine('nonPowerOfTwo', [false, true]) - .combine('canaryOnCreation', [false, true]) - .filter(({ canaryOnCreation, format }) => { - // We can only initialize the texture if it's encodable or renderable. - const canInitialize = format in kTextureFormatInfo || kTextureFormatInfo[format].renderable; - - // Filter out cases where we want canary values but can't initialize. - return !canaryOnCreation || canInitialize; - }); - -type TextureZeroParams = ParamTypeOf<typeof kTestParams>; - -export type CheckContents = ( - t: TextureZeroInitTest, - params: TextureZeroParams, - texture: GPUTexture, - state: InitializedState, - subresourceRange: SubresourceRange -) => void; +import { unreachable } from '../../../../common/util/util.js'; +import { kTextureFormatInfo } from '../../../format_info.js'; import { checkContentsByBufferCopy, checkContentsByTextureCopy } from './check_texture/by_copy.js'; import { @@ -558,6 +17,15 @@ import { checkContentsByStencilTest, } from './check_texture/by_ds_test.js'; import { checkContentsBySampling } from './check_texture/by_sampling.js'; +import { + getRequiredTextureUsage, + ReadMethod, + CheckContents, + TextureZeroInitTest, + kTestParams, + UninitializeMethod, + InitializedState, +} from './check_texture/texture_zero_init_test.js'; const checkContentsImpl: { [k in ReadMethod]: CheckContents } = { Sample: checkContentsBySampling, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts index 93fa4575c4..3382dabc37 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts @@ -64,6 +64,17 @@ const kInvalidShaderSources = [ return unknown(0.0, 0.0, 0.0, 1.0); }`, }, + { + valid: false, + name: 'unicode-multi-byte-characters', + _errorLine: 1, + // This shader is simplistic enough to always result in the same error position. + // Generally, various backends may choose to report the error at different positions within the + // line, so it's difficult to meaningfully validate them. + _errorLinePos: 19, + _code: `/*🐈🐈🐈🐈🐈🐈🐈*/? +// Expected Error: invalid character found`, + }, ]; const kAllShaderSources = [...kValidShaderSources, ...kInvalidShaderSources]; @@ -174,7 +185,7 @@ g.test('line_number_and_position') .combine('sourceMapName', kSourceMapsKeys) ) .fn(async t => { - const { _code, _errorLine, sourceMapName } = t.params; + const { _code, _errorLine, _errorLinePos, sourceMapName } = t.params; const shaderModule = t.expectGPUError('validation', () => { const sourceMap = kSourceMaps[sourceMapName]; @@ -191,14 +202,22 @@ g.test('line_number_and_position') // If a line is reported, it should point at the correct line (1-based). t.expect( (message.lineNum === 0) === (message.linePos === 0), - "GPUCompilationMessages that don't report a line number should not report a line position." + `Got message.lineNum ${message.lineNum}, .linePos ${message.linePos}, but GPUCompilationMessage should specify both or neither` ); - if (message.lineNum === 0 || message.lineNum === _errorLine) { + if (message.lineNum === 0) { foundAppropriateError = true; + break; + } - // Various backends may choose to report the error at different positions within the line, - // so it's difficult to meaningfully validate them. + if (message.lineNum === _errorLine) { + foundAppropriateError = true; + if (_errorLinePos !== undefined) { + t.expect( + message.linePos === _errorLinePos, + `Got message.linePos ${message.linePos}, expected ${_errorLinePos}` + ); + } break; } } @@ -239,10 +258,9 @@ g.test('offset_and_length') for (const message of info.messages) { // Any offsets and lengths should reference valid spans of the shader code. - t.expect(message.offset <= _code.length, 'Message offset should be within the shader source'); t.expect( - message.offset + message.length <= _code.length, - 'Message offset and length should be within the shader source' + message.offset <= _code.length && message.offset + message.length <= _code.length, + 'message.offset and .length should be within the shader source' ); // If a valid line number and position are given, the offset should point the the same @@ -255,9 +273,10 @@ g.test('offset_and_length') lineOffset += 1; } + const expectedOffset = lineOffset + message.linePos - 1; t.expect( - message.offset === lineOffset + message.linePos - 1, - 'lineNum and linePos should point to the same location as offset' + message.offset === expectedOffset, + `message.lineNum (${message.lineNum}) and .linePos (${message.linePos}) point to a different offset (${lineOffset} + ${message.linePos} - 1 = ${expectedOffset}) than .offset (${message.offset})` ); } } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/storage_texture/read_only.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/storage_texture/read_only.spec.ts new file mode 100644 index 0000000000..978924aabd --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/storage_texture/read_only.spec.ts @@ -0,0 +1,626 @@ +export const description = ` +Tests for the behavior of read-only storage textures. + +TODO: +- Test mipmap level > 0 +- Test resource usage transitions with read-only storage textures +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { unreachable, assert } from '../../../../common/util/util.js'; +import { Float16Array } from '../../../../external/petamoriken/float16/float16.js'; +import { kTextureDimensions } from '../../../capability_info.js'; +import { + ColorTextureFormat, + kColorTextureFormats, + kTextureFormatInfo, +} from '../../../format_info.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { kValidShaderStages, TValidShaderStage } from '../../../util/shader.js'; + +function ComponentCount(format: ColorTextureFormat): number { + switch (format) { + case 'r32float': + case 'r32sint': + case 'r32uint': + return 1; + case 'rg32float': + case 'rg32sint': + case 'rg32uint': + return 2; + case 'rgba32float': + case 'rgba32sint': + case 'rgba32uint': + case 'rgba8sint': + case 'rgba8uint': + case 'rgba8snorm': + case 'rgba8unorm': + case 'rgba16float': + case 'rgba16sint': + case 'rgba16uint': + case 'bgra8unorm': + return 4; + default: + unreachable(); + return 0; + } +} + +class F extends GPUTest { + InitTextureAndGetExpectedOutputBufferData( + storageTexture: GPUTexture, + format: ColorTextureFormat + ): ArrayBuffer { + const bytesPerBlock = kTextureFormatInfo[format].color.bytes; + assert(bytesPerBlock !== undefined); + + const width = storageTexture.width; + const height = storageTexture.height; + const depthOrArrayLayers = storageTexture.depthOrArrayLayers; + + const texelData = new ArrayBuffer(bytesPerBlock * width * height * depthOrArrayLayers); + const texelTypedDataView = this.GetTypedArrayBufferViewForTexelData(texelData, format); + const componentCount = ComponentCount(format); + const outputBufferData = new ArrayBuffer(4 * 4 * width * height * depthOrArrayLayers); + const outputBufferTypedData = this.GetTypedArrayBufferForOutputBufferData( + outputBufferData, + format + ); + + const SetData = ( + texelValue: number, + outputValue: number, + texelDataIndex: number, + component: number, + outputComponent: number = component + ) => { + const texelComponentIndex = texelDataIndex * componentCount + component; + texelTypedDataView[texelComponentIndex] = texelValue; + const outputTexelComponentIndex = texelDataIndex * 4 + outputComponent; + outputBufferTypedData[outputTexelComponentIndex] = outputValue; + }; + for (let z = 0; z < depthOrArrayLayers; ++z) { + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + const texelDataIndex = z * width * height + y * width + x; + outputBufferTypedData[4 * texelDataIndex] = 0; + outputBufferTypedData[4 * texelDataIndex + 1] = 0; + outputBufferTypedData[4 * texelDataIndex + 2] = 0; + outputBufferTypedData[4 * texelDataIndex + 3] = 1; + for (let component = 0; component < componentCount; ++component) { + switch (format) { + case 'r32uint': + case 'rg32uint': + case 'rgba16uint': + case 'rgba32uint': { + const texelValue = 4 * texelDataIndex + component + 1; + SetData(texelValue, texelValue, texelDataIndex, component); + break; + } + case 'rgba8uint': { + const texelValue = (4 * texelDataIndex + component + 1) % 256; + SetData(texelValue, texelValue, texelDataIndex, component); + break; + } + case 'rgba8unorm': { + const texelValue = (4 * texelDataIndex + component + 1) % 256; + const outputValue = texelValue / 255.0; + SetData(texelValue, outputValue, texelDataIndex, component); + break; + } + case 'bgra8unorm': { + const texelValue = (4 * texelDataIndex + component + 1) % 256; + const outputValue = texelValue / 255.0; + // BGRA -> RGBA + assert(component < 4); + const outputComponent = [2, 1, 0, 3][component]; + SetData(texelValue, outputValue, texelDataIndex, component, outputComponent); + break; + } + case 'r32sint': + case 'rg32sint': + case 'rgba16sint': + case 'rgba32sint': { + const texelValue = + (texelDataIndex & 1 ? 1 : -1) * (4 * texelDataIndex + component + 1); + SetData(texelValue, texelValue, texelDataIndex, component); + break; + } + case 'rgba8sint': { + const texelValue = ((4 * texelDataIndex + component + 1) % 256) - 128; + SetData(texelValue, texelValue, texelDataIndex, component); + break; + } + case 'rgba8snorm': { + const texelValue = ((4 * texelDataIndex + component + 1) % 256) - 128; + const outputValue = Math.max(texelValue / 127.0, -1.0); + SetData(texelValue, outputValue, texelDataIndex, component); + break; + } + case 'r32float': + case 'rg32float': + case 'rgba32float': { + const texelValue = (4 * texelDataIndex + component + 1) / 10.0; + SetData(texelValue, texelValue, texelDataIndex, component); + break; + } + case 'rgba16float': { + const texelValue = (4 * texelDataIndex + component + 1) / 10.0; + const f16Array = new Float16Array(1); + f16Array[0] = texelValue; + SetData(texelValue, f16Array[0], texelDataIndex, component); + break; + } + default: + unreachable(); + break; + } + } + } + } + } + this.queue.writeTexture( + { + texture: storageTexture, + }, + texelData, + { + bytesPerRow: bytesPerBlock * width, + rowsPerImage: height, + }, + [width, height, depthOrArrayLayers] + ); + + return outputBufferData; + } + + GetTypedArrayBufferForOutputBufferData(arrayBuffer: ArrayBuffer, format: ColorTextureFormat) { + switch (kTextureFormatInfo[format].color.type) { + case 'uint': + return new Uint32Array(arrayBuffer); + case 'sint': + return new Int32Array(arrayBuffer); + case 'float': + case 'unfilterable-float': + return new Float32Array(arrayBuffer); + } + } + + GetTypedArrayBufferViewForTexelData(arrayBuffer: ArrayBuffer, format: ColorTextureFormat) { + switch (format) { + case 'r32uint': + case 'rg32uint': + case 'rgba32uint': + return new Uint32Array(arrayBuffer); + case 'rgba8uint': + case 'rgba8unorm': + case 'bgra8unorm': + return new Uint8Array(arrayBuffer); + case 'rgba16uint': + return new Uint16Array(arrayBuffer); + case 'r32sint': + case 'rg32sint': + case 'rgba32sint': + return new Int32Array(arrayBuffer); + case 'rgba8sint': + case 'rgba8snorm': + return new Int8Array(arrayBuffer); + case 'rgba16sint': + return new Int16Array(arrayBuffer); + case 'r32float': + case 'rg32float': + case 'rgba32float': + return new Float32Array(arrayBuffer); + case 'rgba16float': + return new Float16Array(arrayBuffer); + default: + unreachable(); + return new Uint8Array(arrayBuffer); + } + } + + GetOutputBufferWGSLType(format: ColorTextureFormat) { + switch (kTextureFormatInfo[format].color.type) { + case 'uint': + return 'vec4u'; + case 'sint': + return 'vec4i'; + case 'float': + case 'unfilterable-float': + return 'vec4f'; + default: + unreachable(); + return ''; + } + } + + DoTransform( + storageTexture: GPUTexture, + shaderStage: TValidShaderStage, + format: ColorTextureFormat, + outputBuffer: GPUBuffer + ) { + let declaration = ''; + switch (storageTexture.dimension) { + case '1d': + declaration = 'texture_storage_1d'; + break; + case '2d': + declaration = + storageTexture.depthOrArrayLayers > 1 ? 'texture_storage_2d_array' : 'texture_storage_2d'; + break; + case '3d': + declaration = 'texture_storage_3d'; + break; + } + const textureDeclaration = ` + @group(0) @binding(0) var readOnlyTexture: ${declaration}<${format}, read>; + `; + const bindingResourceDeclaration = ` + ${textureDeclaration} + @group(0) @binding(1) + var<storage,read_write> outputBuffer : array<${this.GetOutputBufferWGSLType(format)}>; + `; + + const bindGroupEntries = [ + { + binding: 0, + resource: storageTexture.createView(), + }, + { + binding: 1, + resource: { + buffer: outputBuffer, + }, + }, + ]; + + const commandEncoder = this.device.createCommandEncoder(); + + switch (shaderStage) { + case 'compute': { + let textureLoadCoord = ''; + switch (storageTexture.dimension) { + case '1d': + textureLoadCoord = 'invocationID.x'; + break; + case '2d': + textureLoadCoord = + storageTexture.depthOrArrayLayers > 1 + ? `vec2u(invocationID.x, invocationID.y), invocationID.z` + : `vec2u(invocationID.x, invocationID.y)`; + break; + case '3d': + textureLoadCoord = 'invocationID'; + break; + } + + const computeShader = ` + ${bindingResourceDeclaration} + @compute + @workgroup_size( + ${storageTexture.width}, ${storageTexture.height}, ${storageTexture.depthOrArrayLayers}) + fn main( + @builtin(local_invocation_id) invocationID: vec3u, + @builtin(local_invocation_index) invocationIndex: u32) { + let initialValue = textureLoad(readOnlyTexture, ${textureLoadCoord}); + outputBuffer[invocationIndex] = initialValue; + }`; + const computePipeline = this.device.createComputePipeline({ + compute: { + module: this.device.createShaderModule({ + code: computeShader, + }), + }, + layout: 'auto', + }); + const bindGroup = this.device.createBindGroup({ + layout: computePipeline.getBindGroupLayout(0), + entries: bindGroupEntries, + }); + + const computePassEncoder = commandEncoder.beginComputePass(); + computePassEncoder.setPipeline(computePipeline); + computePassEncoder.setBindGroup(0, bindGroup); + computePassEncoder.dispatchWorkgroups(1); + computePassEncoder.end(); + break; + } + case 'fragment': { + let textureLoadCoord = ''; + switch (storageTexture.dimension) { + case '1d': + textureLoadCoord = 'textureCoord.x'; + break; + case '2d': + textureLoadCoord = + storageTexture.depthOrArrayLayers > 1 ? 'textureCoord, z' : 'textureCoord'; + break; + case '3d': + textureLoadCoord = 'vec3u(textureCoord, z)'; + break; + } + + const fragmentShader = ` + ${bindingResourceDeclaration} + @fragment + fn main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f { + let textureCoord = vec2u(fragCoord.xy); + let storageTextureTexelCountPerImage = ${storageTexture.width * storageTexture.height}u; + for (var z = 0u; z < ${storageTexture.depthOrArrayLayers}; z++) { + let initialValue = textureLoad(readOnlyTexture, ${textureLoadCoord}); + let outputIndex = + storageTextureTexelCountPerImage * z + textureCoord.y * ${storageTexture.width} + + textureCoord.x; + outputBuffer[outputIndex] = initialValue; + } + return vec4f(0.0, 1.0, 0.0, 1.0); + }`; + const vertexShader = ` + @vertex + fn main(@builtin(vertex_index) vertexIndex : u32) -> @builtin(position) vec4f { + var pos = array( + vec2f(-1.0, -1.0), + vec2f(-1.0, 1.0), + vec2f( 1.0, -1.0), + vec2f(-1.0, 1.0), + vec2f( 1.0, -1.0), + vec2f( 1.0, 1.0)); + return vec4f(pos[vertexIndex], 0.0, 1.0); + } + `; + const renderPipeline = this.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this.device.createShaderModule({ + code: vertexShader, + }), + }, + fragment: { + module: this.device.createShaderModule({ + code: fragmentShader, + }), + targets: [ + { + format: 'rgba8unorm', + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + }); + + const bindGroup = this.device.createBindGroup({ + layout: renderPipeline.getBindGroupLayout(0), + entries: bindGroupEntries, + }); + + const placeholderColorTexture = this.device.createTexture({ + size: [storageTexture.width, storageTexture.height, 1], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + this.trackForCleanup(placeholderColorTexture); + + const renderPassEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: placeholderColorTexture.createView(), + loadOp: 'clear', + clearValue: { r: 0, g: 0, b: 0, a: 0 }, + storeOp: 'store', + }, + ], + }); + renderPassEncoder.setPipeline(renderPipeline); + renderPassEncoder.setBindGroup(0, bindGroup); + renderPassEncoder.draw(6); + renderPassEncoder.end(); + break; + } + case 'vertex': { + // For each texel location (coordX, coordY), draw one point at (coordX + 0.5, coordY + 0.5) + // in the storageTexture.width * storageTexture.height grid, and save all the texel values + // at (coordX, coordY, z) (z >= 0 && z < storageTexture.depthOrArrayLayers) into the + // corresponding vertex shader outputs. + let vertexOutputs = ''; + for (let layer = 0; layer < storageTexture.depthOrArrayLayers; ++layer) { + vertexOutputs = vertexOutputs.concat( + ` + @location(${layer + 1}) @interpolate(flat) + vertex_out${layer}: ${this.GetOutputBufferWGSLType(format)},` + ); + } + + let loadFromTextureWGSL = ''; + switch (storageTexture.dimension) { + case '1d': + loadFromTextureWGSL = ` + output.vertex_out0 = textureLoad(readOnlyTexture, coordX);`; + break; + case '2d': + if (storageTexture.depthOrArrayLayers === 1) { + loadFromTextureWGSL = ` + output.vertex_out0 = textureLoad(readOnlyTexture, vec2u(coordX, coordY));`; + } else { + for (let z = 0; z < storageTexture.depthOrArrayLayers; ++z) { + loadFromTextureWGSL = loadFromTextureWGSL.concat(` + output.vertex_out${z} = + textureLoad(readOnlyTexture, vec2u(coordX, coordY), ${z});`); + } + } + break; + case '3d': + for (let z = 0; z < storageTexture.depthOrArrayLayers; ++z) { + loadFromTextureWGSL = loadFromTextureWGSL.concat(` + output.vertex_out${z} = textureLoad(readOnlyTexture, vec3u(coordX, coordY, ${z}));`); + } + break; + } + + let outputToBufferWGSL = ''; + for (let layer = 0; layer < storageTexture.depthOrArrayLayers; ++layer) { + outputToBufferWGSL = outputToBufferWGSL.concat( + ` + let outputIndex${layer} = + storageTextureTexelCountPerImage * ${layer}u + + fragmentInput.tex_coord.y * ${storageTexture.width}u + fragmentInput.tex_coord.x; + outputBuffer[outputIndex${layer}] = fragmentInput.vertex_out${layer};` + ); + } + + const shader = ` + ${bindingResourceDeclaration} + struct VertexOutput { + @builtin(position) my_pos: vec4f, + @location(0) @interpolate(flat) tex_coord: vec2u, + ${vertexOutputs} + } + @vertex + fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { + var output : VertexOutput; + let coordX = vertexIndex % ${storageTexture.width}u; + let coordY = vertexIndex / ${storageTexture.width}u; + // Each vertex in the mesh take an even step along X axis from -1.0 to 1.0. + let posXStep = f32(${2.0 / storageTexture.width}); + // As well as along Y axis. + let posYStep = f32(${2.0 / storageTexture.height}); + // And the vertex located in the middle of the step, i.e. with a bias of 0.5 step. + let outputPosX = -1.0 + posXStep * 0.5 + posXStep * f32(coordX); + let outputPosY = -1.0 + posYStep * 0.5 + posYStep * f32(coordY); + output.my_pos = vec4f(outputPosX, outputPosY, 0.0, 1.0); + output.tex_coord = vec2u(coordX, coordY); + ${loadFromTextureWGSL} + return output; + } + @fragment + fn fs_main(fragmentInput : VertexOutput) -> @location(0) vec4f { + let storageTextureTexelCountPerImage = ${storageTexture.width * storageTexture.height}u; + ${outputToBufferWGSL} + return vec4f(0.0, 1.0, 0.0, 1.0); + } + `; + + const renderPipeline = this.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this.device.createShaderModule({ + code: shader, + }), + }, + fragment: { + module: this.device.createShaderModule({ + code: shader, + }), + targets: [ + { + format: 'rgba8unorm', + }, + ], + }, + primitive: { + topology: 'point-list', + }, + }); + + const bindGroup = this.device.createBindGroup({ + layout: renderPipeline.getBindGroupLayout(0), + entries: bindGroupEntries, + }); + + const placeholderColorTexture = this.device.createTexture({ + size: [storageTexture.width, storageTexture.height, 1], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + this.trackForCleanup(placeholderColorTexture); + + const renderPassEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: placeholderColorTexture.createView(), + loadOp: 'clear', + clearValue: { r: 0, g: 0, b: 0, a: 0 }, + storeOp: 'store', + }, + ], + }); + renderPassEncoder.setPipeline(renderPipeline); + renderPassEncoder.setBindGroup(0, bindGroup); + renderPassEncoder.draw(storageTexture.width * storageTexture.height); + renderPassEncoder.end(); + break; + } + } + + this.queue.submit([commandEncoder.finish()]); + } +} + +export const g = makeTestGroup(F); + +g.test('basic') + .desc( + `The basic functionality tests for read-only storage textures. In the test we read data from + the read-only storage texture, write the data into an output storage buffer, and check if the + data in the output storage buffer is exactly what we expect.` + ) + .params(u => + u + .combine('format', kColorTextureFormats) + .filter( + p => p.format === 'bgra8unorm' || kTextureFormatInfo[p.format].color?.storage === true + ) + .combine('shaderStage', kValidShaderStages) + .combine('dimension', kTextureDimensions) + .combine('depthOrArrayLayers', [1, 2] as const) + .unless(p => p.dimension === '1d' && p.depthOrArrayLayers > 1) + ) + .beforeAllSubcases(t => { + if (t.params.format === 'bgra8unorm') { + t.selectDeviceOrSkipTestCase('bgra8unorm-storage'); + } + if (t.isCompatibility) { + t.skipIfTextureFormatNotUsableAsStorageTexture(t.params.format); + } + }) + .fn(t => { + const { format, shaderStage, dimension, depthOrArrayLayers } = t.params; + + const kWidth = 8; + const height = dimension === '1d' ? 1 : 8; + const storageTexture = t.device.createTexture({ + format, + dimension, + size: [kWidth, height, depthOrArrayLayers], + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING, + }); + t.trackForCleanup(storageTexture); + + const expectedData = t.InitTextureAndGetExpectedOutputBufferData(storageTexture, format); + + const outputBuffer = t.device.createBuffer({ + size: 4 * 4 * kWidth * height * depthOrArrayLayers, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE, + }); + t.trackForCleanup(outputBuffer); + + t.DoTransform(storageTexture, shaderStage, format, outputBuffer); + + switch (kTextureFormatInfo[format].color.type) { + case 'uint': + t.expectGPUBufferValuesEqual(outputBuffer, new Uint32Array(expectedData)); + break; + case 'sint': + t.expectGPUBufferValuesEqual(outputBuffer, new Int32Array(expectedData)); + break; + case 'float': + case 'unfilterable-float': + t.expectGPUBufferValuesEqual(outputBuffer, new Float32Array(expectedData)); + break; + default: + unreachable(); + break; + } + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/storage_texture/read_write.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/storage_texture/read_write.spec.ts new file mode 100644 index 0000000000..9eb04b2b45 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/storage_texture/read_write.spec.ts @@ -0,0 +1,385 @@ +export const description = ` +Tests for the behavior of read-write storage textures. + +TODO: +- Test resource usage transitions with read-write storage textures +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { assert, unreachable } from '../../../../common/util/util.js'; +import { kTextureDimensions } from '../../../capability_info.js'; +import { kColorTextureFormats, kTextureFormatInfo } from '../../../format_info.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { align } from '../../../util/math.js'; + +const kShaderStagesForReadWriteStorageTexture = ['fragment', 'compute'] as const; +type ShaderStageForReadWriteStorageTexture = + (typeof kShaderStagesForReadWriteStorageTexture)[number]; + +class F extends GPUTest { + GetInitialData(storageTexture: GPUTexture): ArrayBuffer { + const format = storageTexture.format; + const bytesPerBlock = kTextureFormatInfo[format].bytesPerBlock; + assert(bytesPerBlock !== undefined); + + const width = storageTexture.width; + const height = storageTexture.height; + const depthOrArrayLayers = storageTexture.depthOrArrayLayers; + const initialData = new ArrayBuffer(bytesPerBlock * width * height * depthOrArrayLayers); + const initialTypedData = this.GetTypedArrayBuffer(initialData, format); + for (let z = 0; z < depthOrArrayLayers; ++z) { + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + const index = z * width * height + y * width + x; + switch (format) { + case 'r32sint': + initialTypedData[index] = (index & 1 ? 1 : -1) * (2 * index + 1); + break; + case 'r32uint': + initialTypedData[index] = 2 * index + 1; + break; + case 'r32float': + initialTypedData[index] = (2 * index + 1) / 10.0; + break; + } + } + } + } + return initialData; + } + + GetTypedArrayBuffer(arrayBuffer: ArrayBuffer, format: GPUTextureFormat) { + switch (format) { + case 'r32sint': + return new Int32Array(arrayBuffer); + case 'r32uint': + return new Uint32Array(arrayBuffer); + case 'r32float': + return new Float32Array(arrayBuffer); + default: + unreachable(); + return new Uint8Array(arrayBuffer); + } + } + + GetExpectedData( + shaderStage: ShaderStageForReadWriteStorageTexture, + storageTexture: GPUTexture, + initialData: ArrayBuffer + ): ArrayBuffer { + const format = storageTexture.format; + const bytesPerBlock = kTextureFormatInfo[format].bytesPerBlock; + assert(bytesPerBlock !== undefined); + + const width = storageTexture.width; + const height = storageTexture.height; + const depthOrArrayLayers = storageTexture.depthOrArrayLayers; + const bytesPerRowAlignment = align(bytesPerBlock * width, 256); + const itemsPerRow = bytesPerRowAlignment / bytesPerBlock; + + const expectedData = new ArrayBuffer( + bytesPerRowAlignment * (height * depthOrArrayLayers - 1) + bytesPerBlock * width + ); + const expectedTypedData = this.GetTypedArrayBuffer(expectedData, format); + const initialTypedData = this.GetTypedArrayBuffer(initialData, format); + for (let z = 0; z < depthOrArrayLayers; ++z) { + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + const expectedIndex = z * itemsPerRow * height + y * itemsPerRow + x; + switch (shaderStage) { + case 'compute': { + // In the compute shader we flip the texture along the diagonal. + const initialIndex = + (depthOrArrayLayers - 1 - z) * width * height + + (height - 1 - y) * width + + (width - 1 - x); + expectedTypedData[expectedIndex] = initialTypedData[initialIndex]; + break; + } + case 'fragment': { + // In the fragment shader we double the original texel value of the read-write storage + // texture. + const initialIndex = z * width * height + y * width + x; + expectedTypedData[expectedIndex] = initialTypedData[initialIndex] * 2; + break; + } + } + } + } + } + return expectedData; + } + + RecordCommandsToTransform( + device: GPUDevice, + shaderStage: ShaderStageForReadWriteStorageTexture, + commandEncoder: GPUCommandEncoder, + rwTexture: GPUTexture + ) { + let declaration = ''; + switch (rwTexture.dimension) { + case '1d': + declaration = 'texture_storage_1d'; + break; + case '2d': + declaration = + rwTexture.depthOrArrayLayers > 1 ? 'texture_storage_2d_array' : 'texture_storage_2d'; + break; + case '3d': + declaration = 'texture_storage_3d'; + break; + } + const textureDeclaration = ` + @group(0) @binding(0) var rwTexture: ${declaration}<${rwTexture.format}, read_write>; + `; + + switch (shaderStage) { + case 'fragment': { + const vertexShader = ` + @vertex + fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f { + var pos = array( + vec2f(-1.0, -1.0), + vec2f(-1.0, 1.0), + vec2f( 1.0, -1.0), + vec2f(-1.0, 1.0), + vec2f( 1.0, -1.0), + vec2f( 1.0, 1.0)); + return vec4f(pos[VertexIndex], 0.0, 1.0); + } + `; + let textureLoadStoreCoord = ''; + switch (rwTexture.dimension) { + case '1d': + textureLoadStoreCoord = 'textureCoord.x'; + break; + case '2d': + textureLoadStoreCoord = + rwTexture.depthOrArrayLayers > 1 ? 'textureCoord, z' : 'textureCoord'; + break; + case '3d': + textureLoadStoreCoord = 'vec3u(textureCoord, z)'; + break; + } + const fragmentShader = ` + ${textureDeclaration} + @fragment + fn main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f { + let textureCoord = vec2u(fragCoord.xy); + + for (var z = 0u; z < ${rwTexture.depthOrArrayLayers}; z++) { + let initialValue = textureLoad(rwTexture, ${textureLoadStoreCoord}); + let outputValue = initialValue * 2; + textureStore(rwTexture, ${textureLoadStoreCoord}, outputValue); + } + + return vec4f(0.0, 1.0, 0.0, 1.0); + } + `; + const renderPipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: vertexShader, + }), + }, + fragment: { + module: device.createShaderModule({ + code: fragmentShader, + }), + targets: [ + { + format: 'rgba8unorm', + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + }); + + const bindGroup = device.createBindGroup({ + layout: renderPipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: rwTexture.createView(), + }, + ], + }); + + const placeholderColorTexture = device.createTexture({ + size: [rwTexture.width, rwTexture.height, 1], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + format: 'rgba8unorm', + }); + this.trackForCleanup(placeholderColorTexture); + + const renderPassEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: placeholderColorTexture.createView(), + loadOp: 'clear', + clearValue: { r: 0, g: 0, b: 0, a: 0 }, + storeOp: 'store', + }, + ], + }); + renderPassEncoder.setPipeline(renderPipeline); + renderPassEncoder.setBindGroup(0, bindGroup); + renderPassEncoder.draw(6); + renderPassEncoder.end(); + break; + } + case 'compute': { + let textureLoadCoord = ''; + let textureStoreCoord = ''; + switch (rwTexture.dimension) { + case '1d': + textureLoadCoord = 'dimension - 1u - invocationID.x'; + textureStoreCoord = 'invocationID.x'; + break; + case '2d': + textureLoadCoord = + rwTexture.depthOrArrayLayers > 1 + ? `vec2u(dimension.x - 1u - invocationID.x, dimension.y - 1u - invocationID.y), + textureNumLayers(rwTexture) - 1u - invocationID.z` + : `vec2u(dimension.x - 1u - invocationID.x, dimension.y - 1u - invocationID.y)`; + textureStoreCoord = + rwTexture.depthOrArrayLayers > 1 + ? 'invocationID.xy, invocationID.z' + : 'invocationID.xy'; + break; + case '3d': + textureLoadCoord = ` + vec3u(dimension.x - 1u - invocationID.x, dimension.y - 1u - invocationID.y, + dimension.z - 1u - invocationID.z)`; + textureStoreCoord = 'invocationID'; + break; + } + + const computeShader = ` + ${textureDeclaration} + @compute + @workgroup_size(${rwTexture.width}, ${rwTexture.height}, ${rwTexture.depthOrArrayLayers}) + fn main(@builtin(local_invocation_id) invocationID: vec3u) { + let dimension = textureDimensions(rwTexture); + + let initialValue = textureLoad(rwTexture, ${textureLoadCoord}); + textureBarrier(); + + textureStore(rwTexture, ${textureStoreCoord}, initialValue); + }`; + + const computePipeline = device.createComputePipeline({ + compute: { + module: device.createShaderModule({ + code: computeShader, + }), + }, + layout: 'auto', + }); + const bindGroup = device.createBindGroup({ + layout: computePipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: rwTexture.createView(), + }, + ], + }); + const computePassEncoder = commandEncoder.beginComputePass(); + computePassEncoder.setPipeline(computePipeline); + computePassEncoder.setBindGroup(0, bindGroup); + computePassEncoder.dispatchWorkgroups(1); + computePassEncoder.end(); + break; + } + } + } +} + +export const g = makeTestGroup(F); + +g.test('basic') + .desc( + `The basic functionality tests for read-write storage textures. In the test we read data from + the read-write storage texture, do transforms and write the data back to the read-write storage + texture. textureBarrier() is also called in the tests using compute pipelines.` + ) + .params(u => + u + .combine('format', kColorTextureFormats) + .filter(p => kTextureFormatInfo[p.format].color?.readWriteStorage === true) + .combine('shaderStage', kShaderStagesForReadWriteStorageTexture) + .combine('textureDimension', kTextureDimensions) + .combine('depthOrArrayLayers', [1, 2] as const) + .unless(p => p.textureDimension === '1d' && p.depthOrArrayLayers > 1) + ) + .beforeAllSubcases(t => { + t.skipIfTextureFormatNotUsableAsStorageTexture(t.params.format); + }) + .fn(t => { + const { format, shaderStage, textureDimension, depthOrArrayLayers } = t.params; + + // In compatibility mode the lowest maxComputeInvocationsPerWorkgroup is 128 vs non-compat which is 256 + // So in non-compat we get 16 * 8 * 2, vs compat where we get 8 * 8 * 2 + const kWidth = t.isCompatibility ? 8 : 16; + const height = textureDimension === '1d' ? 1 : 8; + const textureSize = [kWidth, height, depthOrArrayLayers] as const; + const storageTexture = t.device.createTexture({ + format, + dimension: textureDimension, + size: textureSize, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING, + }); + t.trackForCleanup(storageTexture); + + const bytesPerBlock = kTextureFormatInfo[format].bytesPerBlock; + const initialData = t.GetInitialData(storageTexture); + t.queue.writeTexture( + { texture: storageTexture }, + initialData, + { + bytesPerRow: bytesPerBlock * kWidth, + rowsPerImage: height, + }, + textureSize + ); + + const commandEncoder = t.device.createCommandEncoder(); + + t.RecordCommandsToTransform(t.device, shaderStage, commandEncoder, storageTexture); + + const expectedData = t.GetExpectedData(shaderStage, storageTexture, initialData); + const readbackBuffer = t.device.createBuffer({ + size: expectedData.byteLength, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + t.trackForCleanup(readbackBuffer); + const bytesPerRow = align(bytesPerBlock * kWidth, 256); + commandEncoder.copyTextureToBuffer( + { + texture: storageTexture, + }, + { + buffer: readbackBuffer, + bytesPerRow, + rowsPerImage: height, + }, + textureSize + ); + t.queue.submit([commandEncoder.finish()]); + + switch (format) { + case 'r32sint': + t.expectGPUBufferValuesEqual(readbackBuffer, new Int32Array(expectedData)); + break; + case 'r32uint': + t.expectGPUBufferValuesEqual(readbackBuffer, new Uint32Array(expectedData)); + break; + case 'r32float': + t.expectGPUBufferValuesEqual(readbackBuffer, new Float32Array(expectedData)); + break; + } + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts index c032415327..f7fb49818e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts @@ -100,12 +100,15 @@ g.test('texture_binding') .combine('format', kRegularTextureFormats) .combine('viewFormat', kRegularTextureFormats) .filter( - ({ format, viewFormat }) => format !== viewFormat && viewCompatible(format, viewFormat) + ({ format, viewFormat }) => + format !== viewFormat && viewCompatible(false, format, viewFormat) ) ) .beforeAllSubcases(t => { const { format, viewFormat } = t.params; t.skipIfTextureFormatNotSupported(format, viewFormat); + // Compatibility mode does not support format reinterpretation. + t.skipIf(t.isCompatibility); }) .fn(t => { const { format, viewFormat } = t.params; @@ -200,13 +203,16 @@ in view format and match in base format.` .combine('format', kRenderableColorTextureFormats) .combine('viewFormat', kRenderableColorTextureFormats) .filter( - ({ format, viewFormat }) => format !== viewFormat && viewCompatible(format, viewFormat) + ({ format, viewFormat }) => + format !== viewFormat && viewCompatible(false, format, viewFormat) ) .combine('sampleCount', [1, 4]) ) .beforeAllSubcases(t => { const { format, viewFormat } = t.params; t.skipIfTextureFormatNotSupported(format, viewFormat); + // Compatibility mode does not support format reinterpretation. + t.skipIf(t.isCompatibility); }) .fn(t => { const { format, viewFormat, sampleCount } = t.params; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts index 0340121334..b4ce6f4cec 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts @@ -1,6 +1,9 @@ export const description = ` Test the result of writing textures through texture views with various options. +Reads value from a shader array, writes the value via various write methods. +Check the texture result with the expected texel view. + All x= every possible view write method: { - storage write {fragment, compute} - render pass store @@ -13,20 +16,358 @@ TODO: Write helper for this if not already available (see resource_init, buffer_ `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; -import { GPUTest } from '../../../gpu_test.js'; +import { unreachable } from '../../../../common/util/util.js'; +import { + kRegularTextureFormats, + kTextureFormatInfo, + RegularTextureFormat, +} from '../../../format_info.js'; +import { GPUTest, TextureTestMixin } from '../../../gpu_test.js'; +import { kFullscreenQuadVertexShaderCode } from '../../../util/shader.js'; +import { TexelView } from '../../../util/texture/texel_view.js'; + +export const g = makeTestGroup(TextureTestMixin(GPUTest)); + +const kTextureViewWriteMethods = [ + 'storage-write-fragment', + 'storage-write-compute', + 'render-pass-store', + 'render-pass-resolve', +] as const; +type TextureViewWriteMethod = (typeof kTextureViewWriteMethods)[number]; + +// Src color values to read from a shader array. +const kColorsFloat = [ + { R: 1.0, G: 0.0, B: 0.0, A: 0.8 }, + { R: 0.0, G: 1.0, B: 0.0, A: 0.7 }, + { R: 0.0, G: 0.0, B: 0.0, A: 0.6 }, + { R: 0.0, G: 0.0, B: 0.0, A: 0.5 }, + { R: 1.0, G: 1.0, B: 1.0, A: 0.4 }, + { R: 0.7, G: 0.0, B: 0.0, A: 0.3 }, + { R: 0.0, G: 0.8, B: 0.0, A: 0.2 }, + { R: 0.0, G: 0.0, B: 0.9, A: 0.1 }, + { R: 0.1, G: 0.2, B: 0.0, A: 0.3 }, + { R: 0.4, G: 0.3, B: 0.6, A: 0.8 }, +]; + +function FloatToIntColor(c: number) { + return Math.floor(c * 100); +} + +const kColorsInt = kColorsFloat.map(c => { + return { + R: FloatToIntColor(c.R), + G: FloatToIntColor(c.G), + B: FloatToIntColor(c.B), + A: FloatToIntColor(c.A), + }; +}); -export const g = makeTestGroup(GPUTest); +const kTextureSize = 16; + +function writeTextureAndGetExpectedTexelView( + t: GPUTest, + method: TextureViewWriteMethod, + view: GPUTextureView, + format: RegularTextureFormat, + sampleCount: number +) { + const info = kTextureFormatInfo[format]; + const isFloatType = info.color.type === 'float' || info.color.type === 'unfilterable-float'; + const kColors = isFloatType ? kColorsFloat : kColorsInt; + const expectedTexelView = TexelView.fromTexelsAsColors( + format, + coords => { + const pixelPos = coords.y * kTextureSize + coords.x; + return kColors[pixelPos % kColors.length]; + }, + { clampToFormatRange: true } + ); + const vecType = isFloatType ? 'vec4f' : info.color.type === 'sint' ? 'vec4i' : 'vec4u'; + const kColorArrayShaderString = `array<${vecType}, ${kColors.length}>( + ${kColors.map(t => `${vecType}(${t.R}, ${t.G}, ${t.B}, ${t.A}) `).join(',')} + )`; + + switch (method) { + case 'storage-write-compute': + { + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var dst: texture_storage_2d<${format}, write>; + @compute @workgroup_size(1, 1) fn main( + @builtin(global_invocation_id) global_id: vec3<u32>, + ) { + const src = ${kColorArrayShaderString}; + let coord = vec2u(global_id.xy); + let idx = coord.x + coord.y * ${kTextureSize}; + textureStore(dst, coord, src[idx % ${kColors.length}]); + }`, + }), + entryPoint: 'main', + }, + }); + const commandEncoder = t.device.createCommandEncoder(); + const pass = commandEncoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup( + 0, + t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: view, + }, + ], + }) + ); + pass.dispatchWorkgroups(kTextureSize, kTextureSize); + pass.end(); + t.device.queue.submit([commandEncoder.finish()]); + } + break; + + case 'storage-write-fragment': + { + // Create a placeholder color attachment texture, + // The size of which equals that of format texture we are testing, + // so that we have the same number of fragments and texels. + const kPlaceholderTextureFormat = 'rgba8unorm'; + const placeholderTexture = t.trackForCleanup( + t.device.createTexture({ + format: kPlaceholderTextureFormat, + size: [kTextureSize, kTextureSize], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }) + ); + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ + code: kFullscreenQuadVertexShaderCode, + }), + }, + fragment: { + module: t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var dst: texture_storage_2d<${format}, write>; + @fragment fn main( + @builtin(position) fragCoord: vec4<f32>, + ) { + const src = ${kColorArrayShaderString}; + let coord = vec2u(fragCoord.xy); + let idx = coord.x + coord.y * ${kTextureSize}; + textureStore(dst, coord, src[idx % ${kColors.length}]); + }`, + }), + // Set writeMask to 0 as the fragment shader has no output. + targets: [ + { + format: kPlaceholderTextureFormat, + writeMask: 0, + }, + ], + }, + }); + const commandEncoder = t.device.createCommandEncoder(); + const pass = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: placeholderTexture.createView(), + loadOp: 'clear', + storeOp: 'discard', + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup( + 0, + t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: view, + }, + ], + }) + ); + pass.draw(6); + pass.end(); + t.device.queue.submit([commandEncoder.finish()]); + } + break; + + case 'render-pass-store': + case 'render-pass-resolve': + { + // Create a placeholder color attachment texture for the store target when tesing texture is used as resolve target. + const targetView = + method === 'render-pass-store' + ? view + : t + .trackForCleanup( + t.device.createTexture({ + format, + size: [kTextureSize, kTextureSize], + usage: GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: 4, + }) + ) + .createView(); + const resolveView = method === 'render-pass-store' ? undefined : view; + const multisampleCount = method === 'render-pass-store' ? sampleCount : 4; + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ + code: kFullscreenQuadVertexShaderCode, + }), + }, + fragment: { + module: t.device.createShaderModule({ + code: ` + @fragment fn main( + @builtin(position) fragCoord: vec4<f32>, + ) -> @location(0) ${vecType} { + const src = ${kColorArrayShaderString}; + let coord = vec2u(fragCoord.xy); + let idx = coord.x + coord.y * ${kTextureSize}; + return src[idx % ${kColors.length}]; + }`, + }), + targets: [ + { + format, + }, + ], + }, + multisample: { + count: multisampleCount, + }, + }); + const commandEncoder = t.device.createCommandEncoder(); + const pass = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: targetView, + resolveTarget: resolveView, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.setPipeline(pipeline); + pass.draw(6); + pass.end(); + t.device.queue.submit([commandEncoder.finish()]); + } + break; + default: + unreachable(); + } + + return expectedTexelView; +} g.test('format') .desc( `Views of every allowed format. +Read values from color array in the shader, and write it to the texture view via different write methods. + - x= every texture format - x= sampleCount {1, 4} if valid - x= every possible view write method (see above) + +TODO: Test sampleCount > 1 for 'render-pass-store' after extending copySinglePixelTextureToBufferUsingComputePass + to read multiple pixels from multisampled textures. [1] +TODO: Test rgb10a2uint when TexelRepresentation.numericRange is made per-component. [2] ` ) - .unimplemented(); + .params(u => + u // + .combine('method', kTextureViewWriteMethods) + .combine('format', kRegularTextureFormats) + .combine('sampleCount', [1, 4]) + .filter(({ format, method, sampleCount }) => { + const info = kTextureFormatInfo[format]; + + if (sampleCount > 1 && !info.multisample) { + return false; + } + + // [2] + if (format === 'rgb10a2uint') { + return false; + } + + switch (method) { + case 'storage-write-compute': + case 'storage-write-fragment': + return info.color?.storage && sampleCount === 1; + case 'render-pass-store': + // [1] + if (sampleCount > 1) { + return false; + } + return !!info.colorRender; + case 'render-pass-resolve': + return !!info.colorRender?.resolve && sampleCount === 1; + } + return true; + }) + ) + .beforeAllSubcases(t => { + const { format, method } = t.params; + t.skipIfTextureFormatNotSupported(format); + + switch (method) { + case 'storage-write-compute': + case 'storage-write-fragment': + // Still need to filter again for compat mode. + t.skipIfTextureFormatNotUsableAsStorageTexture(format); + break; + } + }) + .fn(t => { + const { format, method, sampleCount } = t.params; + + const usage = + GPUTextureUsage.COPY_SRC | + (method.includes('storage') + ? GPUTextureUsage.STORAGE_BINDING + : GPUTextureUsage.RENDER_ATTACHMENT); + + const texture = t.trackForCleanup( + t.device.createTexture({ + format, + usage, + size: [kTextureSize, kTextureSize], + sampleCount, + }) + ); + + const view = texture.createView(); + const expectedTexelView = writeTextureAndGetExpectedTexelView( + t, + method, + view, + format, + sampleCount + ); + + // [1] Use copySinglePixelTextureToBufferUsingComputePass to check multisampled texture. + t.expectTexelViewComparisonIsOkInTexture({ texture }, expectedTexelView, [ + kTextureSize, + kTextureSize, + ]); + }); g.test('dimension') .desc( diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts index 58d7f2767a..250918f2c9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts @@ -27,8 +27,10 @@ class F extends ValidationTest { this.expectValidationError(() => { p = buffer.mapAsync(mode, offset, size); }, expectation.validationError); + let caught = false; let rejectedEarly = false; + let microtaskBRan = false; // If mapAsync rejected early, microtask A will run before B. // If not, B will run before A. p!.catch(() => { @@ -38,20 +40,31 @@ class F extends ValidationTest { queueMicrotask(() => { // Microtask B rejectedEarly = caught; + microtaskBRan = true; }); - try { - // This await will always complete after microtasks A and B are both done. - await p!; - assert(expectation.rejectName === null, 'mapAsync unexpectedly passed'); - } catch (ex) { - assert(ex instanceof Error, 'mapAsync rejected with non-error'); - assert(typeof ex.stack === 'string', 'mapAsync rejected without a stack'); - assert(expectation.rejectName === ex.name, `mapAsync rejected unexpectedly with: ${ex}`); - assert( - expectation.earlyRejection === rejectedEarly, - 'mapAsync rejected at an unexpected timing' - ); - } + + // These handlers should always run after microtasks A and B are both done. + await p!.then( + () => { + unreachable('mapAsync unexpectedly passed'); + }, + ex => { + const suffix = `\n Rejection: ${ex}`; + + this.expect(microtaskBRan, 'scheduling problem?: microtaskB has not run yet' + suffix); + assert(ex instanceof Error, 'mapAsync rejected with non-error' + suffix); + this.expect(typeof ex.stack === 'string', 'mapAsync rejected without a stack' + suffix); + this.expect( + expectation.rejectName === ex.name, + 'mapAsync rejected with wrong exception name' + suffix + ); + if (expectation.earlyRejection) { + this.expect(rejectedEarly, 'expected early mapAsync rejection, got deferred' + suffix); + } else { + this.expect(!rejectedEarly, 'expected deferred mapAsync rejection, got early' + suffix); + } + } + ); } } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts index 8016252b1e..9d6ab1cdce 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts @@ -43,11 +43,13 @@ g.test('createQuerySet') }); }); -g.test('writeTimestamp') +g.test('timestamp') .desc( ` Tests that writing a timestamp throws a type error exception if the features don't contain 'timestamp-query'. + + TODO: writeTimestamp test is disabled since it's removed from the spec for now. ` ) .params(u => u.combine('featureContainsTimestampQuery', [false, true])) @@ -66,11 +68,53 @@ g.test('writeTimestamp') const querySet = t.device.createQuerySet({ type: featureContainsTimestampQuery ? 'timestamp' : 'occlusion', - count: 1, + count: 2, }); - const encoder = t.createEncoder('non-pass'); - t.shouldThrow(featureContainsTimestampQuery ? false : 'TypeError', () => { - encoder.encoder.writeTimestamp(querySet, 0); - }); + { + let expected = featureContainsTimestampQuery ? false : 'TypeError'; + // writeTimestamp no longer exists and this should always TypeError. + expected = 'TypeError'; + + const encoder = t.createEncoder('non-pass'); + t.shouldThrow(expected, () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (encoder.encoder as any).writeTimestamp(querySet, 0); + }); + encoder.finish(); + } + + { + const encoder = t.createEncoder('non-pass'); + encoder.encoder + .beginComputePass({ + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 }, + }) + .end(); + t.expectValidationError(() => { + encoder.finish(); + }, !featureContainsTimestampQuery); + } + + { + const encoder = t.createEncoder('non-pass'); + const view = t + .trackForCleanup( + t.device.createTexture({ + size: [16, 16, 1], + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }) + ) + .createView(); + encoder.encoder + .beginRenderPass({ + colorAttachments: [{ view, loadOp: 'clear', storeOp: 'discard' }], + timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 }, + }) + .end(); + t.expectValidationError(() => { + encoder.finish(); + }, !featureContainsTimestampQuery); + } }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts index fee2ea716e..2daed5c57c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts @@ -45,32 +45,47 @@ export function getPipelineTypeForBindingCombination(bindingCombination: Binding } } -function getBindGroupIndex(bindGroupTest: BindGroupTest, i: number) { +function getBindGroupIndex(bindGroupTest: BindGroupTest, numBindGroups: number, i: number) { switch (bindGroupTest) { case 'sameGroup': return 0; case 'differentGroups': - return i % 3; + return i % numBindGroups; + } +} + +function getBindingIndex(bindGroupTest: BindGroupTest, numBindGroups: number, i: number) { + switch (bindGroupTest) { + case 'sameGroup': + return i; + case 'differentGroups': + return (i / numBindGroups) | 0; } } function getWGSLBindings( - order: ReorderOrder, - bindGroupTest: BindGroupTest, - storageDefinitionWGSLSnippetFn: (i: number, j: number) => string, + { + order, + bindGroupTest, + storageDefinitionWGSLSnippetFn, + numBindGroups, + }: { + order: ReorderOrder; + bindGroupTest: BindGroupTest; + storageDefinitionWGSLSnippetFn: (i: number, j: number) => string; + numBindGroups: number; + }, numBindings: number, id: number ) { return reorder( order, - range( - numBindings, - i => - `@group(${getBindGroupIndex( - bindGroupTest, - i - )}) @binding(${i}) ${storageDefinitionWGSLSnippetFn(i, id)};` - ) + range(numBindings, i => { + const groupNdx = getBindGroupIndex(bindGroupTest, numBindGroups, i); + const bindingNdx = getBindingIndex(bindGroupTest, numBindGroups, i); + const storageWGSL = storageDefinitionWGSLSnippetFn(i, id); + return `@group(${groupNdx}) @binding(${bindingNdx}) ${storageWGSL};`; + }) ).join('\n '); } @@ -80,15 +95,22 @@ export function getPerStageWGSLForBindingCombinationImpl( bindGroupTest: BindGroupTest, storageDefinitionWGSLSnippetFn: (i: number, j: number) => string, bodyFn: (numBindings: number, set: number) => string, + numBindGroups: number, numBindings: number, extraWGSL = '' ) { + const bindingParams = { + order, + bindGroupTest, + storageDefinitionWGSLSnippetFn, + numBindGroups, + }; switch (bindingCombination) { case 'vertex': return ` ${extraWGSL} - ${getWGSLBindings(order, bindGroupTest, storageDefinitionWGSLSnippetFn, numBindings, 0)} + ${getWGSLBindings(bindingParams, numBindings, 0)} @vertex fn mainVS() -> @builtin(position) vec4f { ${bodyFn(numBindings, 0)} @@ -99,7 +121,7 @@ export function getPerStageWGSLForBindingCombinationImpl( return ` ${extraWGSL} - ${getWGSLBindings(order, bindGroupTest, storageDefinitionWGSLSnippetFn, numBindings, 0)} + ${getWGSLBindings(bindingParams, numBindings, 0)} @vertex fn mainVS() -> @builtin(position) vec4f { return vec4f(0); @@ -113,9 +135,9 @@ export function getPerStageWGSLForBindingCombinationImpl( return ` ${extraWGSL} - ${getWGSLBindings(order, bindGroupTest, storageDefinitionWGSLSnippetFn, numBindings, 0)} + ${getWGSLBindings(bindingParams, numBindings, 0)} - ${getWGSLBindings(order, bindGroupTest, storageDefinitionWGSLSnippetFn, numBindings - 1, 1)} + ${getWGSLBindings(bindingParams, numBindings - 1, 1)} @vertex fn mainVS() -> @builtin(position) vec4f { ${bodyFn(numBindings, 0)} @@ -131,9 +153,9 @@ export function getPerStageWGSLForBindingCombinationImpl( return ` ${extraWGSL} - ${getWGSLBindings(order, bindGroupTest, storageDefinitionWGSLSnippetFn, numBindings - 1, 0)} + ${getWGSLBindings(bindingParams, numBindings - 1, 0)} - ${getWGSLBindings(order, bindGroupTest, storageDefinitionWGSLSnippetFn, numBindings, 1)} + ${getWGSLBindings(bindingParams, numBindings, 1)} @vertex fn mainVS() -> @builtin(position) vec4f { ${bodyFn(numBindings - 1, 0)} @@ -148,8 +170,7 @@ export function getPerStageWGSLForBindingCombinationImpl( case 'compute': return ` ${extraWGSL} - ${getWGSLBindings(order, bindGroupTest, storageDefinitionWGSLSnippetFn, numBindings, 0)} - @group(3) @binding(0) var<storage, read_write> d: f32; + ${getWGSLBindings(bindingParams, numBindings, 0)} @compute @workgroup_size(1) fn main() { ${bodyFn(numBindings, 0)} } @@ -164,6 +185,7 @@ export function getPerStageWGSLForBindingCombination( bindGroupTest: BindGroupTest, storageDefinitionWGSLSnippetFn: (i: number, j: number) => string, usageWGSLSnippetFn: (i: number, j: number) => string, + maxBindGroups: number, numBindings: number, extraWGSL = '' ) { @@ -174,6 +196,7 @@ export function getPerStageWGSLForBindingCombination( storageDefinitionWGSLSnippetFn, (numBindings: number, set: number) => `${range(numBindings, i => usageWGSLSnippetFn(i, set)).join('\n ')}`, + maxBindGroups, numBindings, extraWGSL ); @@ -185,6 +208,7 @@ export function getPerStageWGSLForBindingCombinationStorageTextures( bindGroupTest: BindGroupTest, storageDefinitionWGSLSnippetFn: (i: number, j: number) => string, usageWGSLSnippetFn: (i: number, j: number) => string, + numBindGroups: number, numBindings: number, extraWGSL = '' ) { @@ -195,6 +219,7 @@ export function getPerStageWGSLForBindingCombinationStorageTextures( storageDefinitionWGSLSnippetFn, (numBindings: number, set: number) => `${range(numBindings, i => usageWGSLSnippetFn(i, set)).join('\n ')}`, + numBindGroups, numBindings, extraWGSL ); @@ -854,7 +879,7 @@ export class LimitTestsImpl extends GPUTestBase { /** * Creates an GPURenderCommandsMixin setup with some initial state. */ - _getGPURenderCommandsMixin(encoderType: RenderEncoderType) { + #getGPURenderCommandsMixin(encoderType: RenderEncoderType) { const { device } = this; switch (encoderType) { @@ -895,7 +920,7 @@ export class LimitTestsImpl extends GPUTestBase { }); const encoder = device.createCommandEncoder(); - const mixin = encoder.beginRenderPass({ + const passEncoder = encoder.beginRenderPass({ colorAttachments: [ { view: texture.createView(), @@ -906,10 +931,10 @@ export class LimitTestsImpl extends GPUTestBase { }); return { - mixin, + passEncoder, bindGroup, prep() { - mixin.end(); + passEncoder.end(); }, test() { encoder.finish(); @@ -946,16 +971,16 @@ export class LimitTestsImpl extends GPUTestBase { ], }); - const mixin = device.createRenderBundleEncoder({ + const passEncoder = device.createRenderBundleEncoder({ colorFormats: ['rgba8unorm'], }); return { - mixin, + passEncoder, bindGroup, prep() {}, test() { - mixin.finish(); + passEncoder.finish(); }, }; break; @@ -964,17 +989,23 @@ export class LimitTestsImpl extends GPUTestBase { } /** - * Tests a method on GPURenderCommandsMixin - * The function will be called with the mixin. + * Test a method on GPURenderCommandsMixin or GPUBindingCommandsMixin + * The function will be called with the passEncoder. */ - async testGPURenderCommandsMixin( + async testGPURenderAndBindingCommandsMixin( encoderType: RenderEncoderType, - fn: ({ mixin }: { mixin: GPURenderCommandsMixin }) => void, + fn: ({ + passEncoder, + bindGroup, + }: { + passEncoder: GPURenderCommandsMixin & GPUBindingCommandsMixin; + bindGroup: GPUBindGroup; + }) => void, shouldError: boolean, msg = '' ) { - const { mixin, prep, test } = this._getGPURenderCommandsMixin(encoderType); - fn({ mixin }); + const { passEncoder, prep, test, bindGroup } = this.#getGPURenderCommandsMixin(encoderType); + fn({ passEncoder, bindGroup }); prep(); await this.expectValidationError(test, shouldError, msg); @@ -983,7 +1014,7 @@ export class LimitTestsImpl extends GPUTestBase { /** * Creates GPUBindingCommandsMixin setup with some initial state. */ - _getGPUBindingCommandsMixin(encoderType: EncoderType) { + #getGPUBindingCommandsMixin(encoderType: EncoderType) { const { device } = this; switch (encoderType) { @@ -1016,12 +1047,12 @@ export class LimitTestsImpl extends GPUTestBase { }); const encoder = device.createCommandEncoder(); - const mixin = encoder.beginComputePass(); + const passEncoder = encoder.beginComputePass(); return { - mixin, + passEncoder, bindGroup, prep() { - mixin.end(); + passEncoder.end(); }, test() { encoder.finish(); @@ -1030,24 +1061,24 @@ export class LimitTestsImpl extends GPUTestBase { break; } case 'render': - return this._getGPURenderCommandsMixin('render'); + return this.#getGPURenderCommandsMixin('render'); case 'renderBundle': - return this._getGPURenderCommandsMixin('renderBundle'); + return this.#getGPURenderCommandsMixin('renderBundle'); } } /** * Tests a method on GPUBindingCommandsMixin - * The function pass will be called with the mixin and a bindGroup + * The function pass will be called with the passEncoder and a bindGroup */ async testGPUBindingCommandsMixin( encoderType: EncoderType, - fn: ({ bindGroup }: { mixin: GPUBindingCommandsMixin; bindGroup: GPUBindGroup }) => void, + fn: ({ bindGroup }: { passEncoder: GPUBindingCommandsMixin; bindGroup: GPUBindGroup }) => void, shouldError: boolean, msg = '' ) { - const { mixin, bindGroup, prep, test } = this._getGPUBindingCommandsMixin(encoderType); - fn({ mixin, bindGroup }); + const { passEncoder, bindGroup, prep, test } = this.#getGPUBindingCommandsMixin(encoderType); + fn({ passEncoder, bindGroup }); prep(); await this.expectValidationError(test, shouldError, msg); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxBindGroups.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxBindGroups.spec.ts index 334b49cc90..166b40ff2c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxBindGroups.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxBindGroups.spec.ts @@ -1,4 +1,4 @@ -import { range } from '../../../../../common/util/util.js'; +import { assert } from '../../../../../common/util/util.js'; import { kCreatePipelineTypes, @@ -10,30 +10,152 @@ import { const limit = 'maxBindGroups'; export const { g, description } = makeLimitTestGroup(limit); +type BindingLayout = { + buffer?: GPUBufferBindingLayout; + sampler?: GPUSamplerBindingLayout; + texture?: GPUTextureBindingLayout; + storageTexture?: GPUStorageTextureBindingLayout; + externalTexture?: GPUExternalTextureBindingLayout; +}; + +type LimitToBindingLayout = { + name: keyof GPUSupportedLimits; + entry: BindingLayout; +}; + +const kLimitToBindingLayout: readonly LimitToBindingLayout[] = [ + { + name: 'maxSampledTexturesPerShaderStage', + entry: { + texture: {}, + }, + }, + { + name: 'maxSamplersPerShaderStage', + entry: { + sampler: {}, + }, + }, + { + name: 'maxUniformBuffersPerShaderStage', + entry: { + buffer: {}, + }, + }, + { + name: 'maxStorageBuffersPerShaderStage', + entry: { + buffer: { + type: 'read-only-storage', + }, + }, + }, + { + name: 'maxStorageTexturesPerShaderStage', + entry: { + storageTexture: { + access: 'write-only', + format: 'rgba8unorm', + viewDimension: '2d', + }, + }, + }, +] as const; + +/** + * Yields all possible binding layout entries for a stage. + */ +function* getBindingLayoutEntriesForStage(device: GPUDevice) { + for (const { name, entry } of kLimitToBindingLayout) { + const limit = device.limits[name] as number; + for (let i = 0; i < limit; ++i) { + yield entry; + } + } +} + +/** + * Yields all of the possible BindingLayoutEntryAndVisibility entries for a render pipeline + */ +function* getBindingLayoutEntriesForRenderPipeline( + device: GPUDevice +): Generator<GPUBindGroupLayoutEntry> { + const visibilities = [GPUShaderStage.VERTEX, GPUShaderStage.FRAGMENT]; + for (const visibility of visibilities) { + for (const bindEntryResourceType of getBindingLayoutEntriesForStage(device)) { + const entry: GPUBindGroupLayoutEntry = { + binding: 0, + visibility, + ...bindEntryResourceType, + }; + yield entry; + } + } +} + +/** + * Returns the total possible bindings per render pipeline + */ +function getTotalPossibleBindingsPerRenderPipeline(device: GPUDevice) { + const totalPossibleBindingsPerStage = + device.limits.maxSampledTexturesPerShaderStage + + device.limits.maxSamplersPerShaderStage + + device.limits.maxUniformBuffersPerShaderStage + + device.limits.maxStorageBuffersPerShaderStage + + device.limits.maxStorageTexturesPerShaderStage; + return totalPossibleBindingsPerStage * 2; +} + +/** + * Yields count GPUBindGroupLayoutEntries + */ +function* getBindingLayoutEntries( + device: GPUDevice, + count: number +): Generator<GPUBindGroupLayoutEntry> { + assert(count < getTotalPossibleBindingsPerRenderPipeline(device)); + const iter = getBindingLayoutEntriesForRenderPipeline(device); + for (; count > 0; --count) { + yield iter.next().value; + } +} + g.test('createPipelineLayout,at_over') .desc(`Test using createPipelineLayout at and over ${limit} limit`) .params(kMaximumLimitBaseParams) .fn(async t => { const { limitTest, testValueName } = t.params; + await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, - async ({ device, testValue, shouldError }) => { - const bindGroupLayouts = range(testValue, _i => - device.createBindGroupLayout({ - entries: [ - { - binding: 0, - visibility: GPUShaderStage.VERTEX, - buffer: {}, - }, - ], - }) + async ({ device, testValue, shouldError, actualLimit }) => { + const totalPossibleBindingsPerPipeline = getTotalPossibleBindingsPerRenderPipeline(device); + // Not sure what to do if we ever hit this but I think it's better to assert than silently skip. + assert( + testValue < totalPossibleBindingsPerPipeline, + `not enough possible bindings(${totalPossibleBindingsPerPipeline}) to test ${testValue} bindGroups` ); - await t.expectValidationError(() => { - device.createPipelineLayout({ bindGroupLayouts }); - }, shouldError); + const bindingDescriptions: string[] = []; + const bindGroupLayouts = [...getBindingLayoutEntries(device, testValue)].map(entry => { + bindingDescriptions.push( + `${JSON.stringify(entry)} // group(${bindingDescriptions.length})` + ); + return device.createBindGroupLayout({ + entries: [entry], + }); + }); + + await t.expectValidationError( + () => { + device.createPipelineLayout({ bindGroupLayouts }); + }, + shouldError, + `testing ${testValue} bindGroups on maxBindGroups = ${actualLimit} with \n${bindingDescriptions.join( + '\n' + )}` + ); } ); }); @@ -76,8 +198,8 @@ g.test('setBindGroup,at_over') const lastIndex = testValue - 1; await t.testGPUBindingCommandsMixin( encoderType, - ({ mixin, bindGroup }) => { - mixin.setBindGroup(lastIndex, bindGroup); + ({ passEncoder, bindGroup }) => { + passEncoder.setBindGroup(lastIndex, bindGroup); }, shouldError, `shouldError: ${shouldError}, actualLimit: ${actualLimit}, testValue: ${lastIndex}` diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxBindGroupsPlusVertexBuffers.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxBindGroupsPlusVertexBuffers.spec.ts new file mode 100644 index 0000000000..d75ef0ce7d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxBindGroupsPlusVertexBuffers.spec.ts @@ -0,0 +1,301 @@ +import { + kRenderEncoderTypes, + kMaximumLimitBaseParams, + makeLimitTestGroup, + LimitsRequest, +} from './limit_utils.js'; + +const kVertexBufferBindGroupPreferences = ['vertexBuffers', 'bindGroups'] as const; +type VertexBufferBindGroupPreference = (typeof kVertexBufferBindGroupPreferences)[number]; + +const kLayoutTypes = ['auto', 'explicit'] as const; +type LayoutType = (typeof kLayoutTypes)[number]; + +/** + * Given testValue, choose more vertex buffers or more bind groups based on preference + */ +function getNumBindGroupsAndNumVertexBuffers( + device: GPUDevice, + preference: VertexBufferBindGroupPreference, + testValue: number +) { + switch (preference) { + case 'bindGroups': { + const numBindGroups = Math.min(testValue, device.limits.maxBindGroups); + const numVertexBuffers = Math.max(0, testValue - numBindGroups); + return { numVertexBuffers, numBindGroups }; + } + case 'vertexBuffers': { + const numVertexBuffers = Math.min(testValue, device.limits.maxVertexBuffers); + const numBindGroups = Math.max(0, testValue - numVertexBuffers); + return { numVertexBuffers, numBindGroups }; + } + } +} + +function createLayout(device: GPUDevice, layoutType: LayoutType, numBindGroups: number) { + switch (layoutType) { + case 'auto': + return 'auto'; + case 'explicit': { + const bindGroupLayouts = new Array(numBindGroups); + if (numBindGroups > 0) { + bindGroupLayouts.fill(device.createBindGroupLayout({ entries: [] })); + bindGroupLayouts[numBindGroups - 1] = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: {}, + }, + ], + }); + } + return device.createPipelineLayout({ bindGroupLayouts }); + } + } +} + +/** + * Generate a render pipeline that can be used to test maxBindGroupsPlusVertexBuffers + */ +function getPipelineDescriptor( + device: GPUDevice, + preference: VertexBufferBindGroupPreference, + testValue: number, + layoutType: LayoutType +) { + // Get the numVertexBuffers and numBindGroups we could use given testValue as a total. + // We will only use 1 of each but we'll use the last index. + const { numVertexBuffers, numBindGroups } = getNumBindGroupsAndNumVertexBuffers( + device, + preference, + testValue + ); + + const layout = createLayout(device, layoutType, numBindGroups); + + const [bindGroupDecl, bindGroupUsage] = + numBindGroups === 0 + ? ['', ''] + : [`@group(${numBindGroups - 1}) @binding(0) var<uniform> u: f32;`, `_ = u;`]; + + const [attribDecl, attribUsage] = + numVertexBuffers === 0 + ? ['', ''] + : ['@location(0) v: vec4f', `_ = v; // will use vertex buffer ${numVertexBuffers - 1}`]; + + const code = ` + ${bindGroupDecl} + + @vertex fn vs(${attribDecl}) -> @builtin(position) vec4f { + ${bindGroupUsage} + ${attribUsage} + return vec4f(0); + } + + @fragment fn fs() -> @location(0) vec4f { + return vec4f(0); + } + `; + + const module = device.createShaderModule({ code }); + const buffers = new Array<GPUVertexBufferLayout | null>(numVertexBuffers); + if (numVertexBuffers > 0) { + buffers[numVertexBuffers - 1] = { + arrayStride: 16, + attributes: [{ shaderLocation: 0, offset: 0, format: 'float32' }], + }; + } + + return { + code, + descriptor: { + layout, + vertex: { + module, + buffers, + }, + fragment: { + module, + targets: [{ format: 'rgba8unorm' }], + }, + } as const, + }; +} + +const kExtraLimits: LimitsRequest = { + maxBindGroups: 'adapterLimit', + maxVertexBuffers: 'adapterLimit', +}; + +const limit = 'maxBindGroupsPlusVertexBuffers'; +export const { g, description } = makeLimitTestGroup(limit); + +g.test('createRenderPipeline,at_over') + .desc(`Test using at and over ${limit} limit in createRenderPipeline(Async).`) + .params( + kMaximumLimitBaseParams + .combine('async', [false, true]) + .beginSubcases() + .combine('preference', kVertexBufferBindGroupPreferences) + .combine('layoutType', kLayoutTypes) + ) + .fn(async t => { + const { limitTest, testValueName, async, preference, layoutType } = t.params; + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError, actualLimit }) => { + const maxUsableBindGroupsPlusVertexBuffers = + device.limits.maxBindGroups + device.limits.maxVertexBuffers; + t.skipIf( + maxUsableBindGroupsPlusVertexBuffers < actualLimit, + `can not test because the max usable bindGroups + vertexBuffers (${maxUsableBindGroupsPlusVertexBuffers}) is < maxBindGroupsAndVertexBuffers (${actualLimit})` + ); + t.skipIf( + maxUsableBindGroupsPlusVertexBuffers === actualLimit && testValue > actualLimit, + `can not test because the max usable bindGroups + vertexBuffers (${maxUsableBindGroupsPlusVertexBuffers}) === maxBindGroupsAndVertexBuffers (${actualLimit}) + but the testValue (${testValue}) > maxBindGroupsAndVertexBuffers (${actualLimit})` + ); + + const { code, descriptor } = getPipelineDescriptor( + device, + preference, + testValue, + layoutType + ); + + await t.testCreateRenderPipeline( + descriptor, + async, + shouldError, + `testValue: ${testValue}, actualLimit: ${actualLimit}, shouldError: ${shouldError}\n${code}` + ); + }, + kExtraLimits + ); + }); + +g.test('draw,at_over') + .desc(`Test using at and over ${limit} limit draw/drawIndexed/drawIndirect/drawIndexedIndirect`) + .params( + kMaximumLimitBaseParams + .combine('encoderType', kRenderEncoderTypes) + .beginSubcases() + .combine('preference', kVertexBufferBindGroupPreferences) + .combine('drawType', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const) + ) + .fn(async t => { + const { limitTest, testValueName, encoderType, drawType, preference } = t.params; + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError, actualLimit }) => { + const maxUsableVertexBuffers = Math.min( + device.limits.maxVertexBuffers, + device.limits.maxVertexAttributes + ); + const maxUsableBindGroupsPlusVertexBuffers = + device.limits.maxBindGroups + maxUsableVertexBuffers; + t.skipIf( + maxUsableBindGroupsPlusVertexBuffers < actualLimit, + `can not test because the max usable bindGroups + vertexBuffers (${maxUsableBindGroupsPlusVertexBuffers}) is < maxBindGroupsAndVertexBuffers (${actualLimit})` + ); + t.skipIf( + maxUsableBindGroupsPlusVertexBuffers === actualLimit && testValue > actualLimit, + `can not test because the max usable bindGroups + vertexBuffers (${maxUsableBindGroupsPlusVertexBuffers}) === maxBindGroupsAndVertexBuffers (${actualLimit}) + but the testValue (${testValue}) > maxBindGroupsAndVertexBuffers (${actualLimit})` + ); + + // Get the numVertexBuffers and numBindGroups we could use given testValue as a total. + // We will only use 1 of each but we'll use the last index. + const { numVertexBuffers, numBindGroups } = getNumBindGroupsAndNumVertexBuffers( + device, + preference, + testValue + ); + + const module = device.createShaderModule({ + code: ` + @vertex fn vs() -> @builtin(position) vec4f { + return vec4f(0); + } + @fragment fn fs() -> @location(0) vec4f { + return vec4f(0); + } + `, + }); + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { module, targets: [{ format: 'rgba8unorm' }] }, + }); + + const vertexBuffer = device.createBuffer({ + size: 16, + usage: GPUBufferUsage.VERTEX, + }); + t.trackForCleanup(vertexBuffer); + + await t.testGPURenderAndBindingCommandsMixin( + encoderType, + ({ bindGroup, passEncoder }) => { + // Set the last vertex buffer and clear it. This should have no effect + // unless there is a bug in bookkeeping in the implementation. + passEncoder.setVertexBuffer(device.limits.maxVertexBuffers - 1, vertexBuffer); + passEncoder.setVertexBuffer(device.limits.maxVertexBuffers - 1, null); + + // Set the last bindGroup and clear it. This should have no effect + // unless there is a bug in bookkeeping in the implementation. + passEncoder.setBindGroup(device.limits.maxBindGroups - 1, bindGroup); + passEncoder.setBindGroup(device.limits.maxBindGroups - 1, null); + + if (numVertexBuffers) { + passEncoder.setVertexBuffer(numVertexBuffers - 1, vertexBuffer); + } + + if (numBindGroups) { + passEncoder.setBindGroup(numBindGroups - 1, bindGroup); + } + + passEncoder.setPipeline(pipeline); + + const indirectBuffer = device.createBuffer({ + size: 20, + usage: GPUBufferUsage.INDIRECT, + }); + t.trackForCleanup(indirectBuffer); + + const indexBuffer = device.createBuffer({ + size: 4, + usage: GPUBufferUsage.INDEX, + }); + t.trackForCleanup(indexBuffer); + + switch (drawType) { + case 'draw': + passEncoder.draw(0); + break; + case 'drawIndexed': + passEncoder.setIndexBuffer(indexBuffer, 'uint16'); + passEncoder.drawIndexed(0); + break; + case 'drawIndirect': + passEncoder.drawIndirect(indirectBuffer, 0); + break; + case 'drawIndexedIndirect': + passEncoder.setIndexBuffer(indexBuffer, 'uint16'); + passEncoder.drawIndexedIndirect(indirectBuffer, 0); + break; + } + }, + shouldError, + `testValue: ${testValue}, actualLimit: ${actualLimit}, shouldError: ${shouldError}` + ); + + vertexBuffer.destroy(); + }, + kExtraLimits + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxComputeWorkgroupStorageSize.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxComputeWorkgroupStorageSize.spec.ts index cb26e18ebe..8a9176983d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxComputeWorkgroupStorageSize.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxComputeWorkgroupStorageSize.spec.ts @@ -12,7 +12,8 @@ import { const limit = 'maxComputeWorkgroupStorageSize'; export const { g, description } = makeLimitTestGroup(limit); -const kSmallestWorkgroupVarSize = 4; +// Each var is roundUp(16, SizeOf(T)) +const kSmallestWorkgroupVarSize = 16; const wgslF16Types = { f16: { alignOf: 2, sizeOf: 2, requireF16: true }, @@ -71,7 +72,9 @@ function getModuleForWorkgroupStorageSize(device: GPUDevice, wgslType: WGSLType, const { sizeOf, alignOf, requireF16 } = wgslTypes[wgslType]; const unitSize = align(sizeOf, alignOf); const units = Math.floor(size / unitSize); - const extra = (size - units * unitSize) / kSmallestWorkgroupVarSize; + const sizeUsed = align(units * unitSize, 16); + const sizeLeft = size - sizeUsed; + const extra = Math.floor(sizeLeft / kSmallestWorkgroupVarSize); const code = (requireF16 ? 'enable f16;\n' : '') + @@ -89,7 +92,7 @@ function getModuleForWorkgroupStorageSize(device: GPUDevice, wgslType: WGSLType, b: vec2f, }; var<workgroup> d0: array<${wgslType}, ${units}>; - ${extra ? `var<workgroup> d1: array<f32, ${extra}>;` : ''} + ${extra ? `var<workgroup> d1: array<vec4<f32>, ${extra}>;` : ''} @compute @workgroup_size(1) fn main() { _ = d0; ${extra ? '_ = d1;' : ''} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderComponents.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderComponents.spec.ts index eeb9eb0faf..409dc72724 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderComponents.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderComponents.spec.ts @@ -111,8 +111,12 @@ g.test('createRenderPipeline,at_over') .combine('sampleMaskOut', [false, true]) ) .beforeAllSubcases(t => { - if (t.isCompatibility && (t.params.sampleMaskIn || t.params.sampleMaskOut)) { - t.skip('sample_mask not supported in compatibility mode'); + if (t.isCompatibility) { + t.skipIf( + t.params.sampleMaskIn || t.params.sampleMaskOut, + 'sample_mask not supported in compatibility mode' + ); + t.skipIf(t.params.sampleIndex, 'sample_index not supported in compatibility mode'); } }) .fn(async t => { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSampledTexturesPerShaderStage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSampledTexturesPerShaderStage.spec.ts index cd90d9d907..57e602c40a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSampledTexturesPerShaderStage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSampledTexturesPerShaderStage.spec.ts @@ -3,6 +3,7 @@ import { reorder, kReorderOrderKeys, ReorderOrder, + assert, } from '../../../../../common/util/util.js'; import { kShaderStageCombinationsWithStage } from '../../../../capability_info.js'; @@ -13,8 +14,14 @@ import { kBindingCombinations, getPipelineTypeForBindingCombination, getPerStageWGSLForBindingCombination, + LimitsRequest, } from './limit_utils.js'; +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', +}; + const limit = 'maxSampledTexturesPerShaderStage'; export const { g, description } = makeLimitTestGroup(limit); @@ -43,6 +50,9 @@ g.test('createBindGroupLayout,at_over') Note: We also test order to make sure the implementation isn't just looking at just the last entry. + + Note: It's also possible the maxBindingsPerBindGroup is lower than + ${limit} in which case skip the test since we can not hit the limit. ` ) .params( @@ -56,11 +66,17 @@ g.test('createBindGroupLayout,at_over') limitTest, testValueName, async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + await t.expectValidationError( () => createBindGroupLayout(device, visibility, order, testValue), shouldError ); - } + }, + kExtraLimits ); }); @@ -83,18 +99,30 @@ g.test('createPipelineLayout,at_over') await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, - async ({ device, testValue, shouldError }) => { - const kNumGroups = 3; + async ({ device, testValue, shouldError, actualLimit }) => { + const maxBindingsPerBindGroup = Math.min( + t.device.limits.maxBindingsPerBindGroup, + actualLimit + ); + const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup); + + // Not sure what to do in this case but best we get notified if it happens. + assert(kNumGroups <= t.device.limits.maxBindGroups); + const bindGroupLayouts = range(kNumGroups, i => { - const minInGroup = Math.floor(testValue / kNumGroups); - const numInGroup = i ? minInGroup : testValue - minInGroup * (kNumGroups - 1); + const numInGroup = Math.min( + testValue - i * maxBindingsPerBindGroup, + maxBindingsPerBindGroup + ); return createBindGroupLayout(device, visibility, order, numInGroup); }); + await t.expectValidationError( () => device.createPipelineLayout({ bindGroupLayouts }), shouldError ); - } + }, + kExtraLimits ); }); @@ -122,16 +150,21 @@ g.test('createPipeline,at_over') limitTest, testValueName, async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + const code = getPerStageWGSLForBindingCombination( bindingCombination, order, bindGroupTest, (i, j) => `var u${j}_${i}: texture_2d<f32>`, (i, j) => `_ = textureLoad(u${j}_${i}, vec2u(0), 0);`, + device.limits.maxBindGroups, testValue ); const module = device.createShaderModule({ code }); - await t.testCreatePipeline( pipelineType, async, @@ -139,6 +172,7 @@ g.test('createPipeline,at_over') shouldError, `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` ); - } + }, + kExtraLimits ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSamplersPerShaderStage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSamplersPerShaderStage.spec.ts index 3103d423c9..892c1f498d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSamplersPerShaderStage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxSamplersPerShaderStage.spec.ts @@ -3,6 +3,7 @@ import { reorder, kReorderOrderKeys, ReorderOrder, + assert, } from '../../../../../common/util/util.js'; import { kShaderStageCombinationsWithStage } from '../../../../capability_info.js'; @@ -13,8 +14,14 @@ import { kBindingCombinations, getPipelineTypeForBindingCombination, getPerStageWGSLForBindingCombination, + LimitsRequest, } from './limit_utils.js'; +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', +}; + const limit = 'maxSamplersPerShaderStage'; export const { g, description } = makeLimitTestGroup(limit); @@ -56,11 +63,17 @@ g.test('createBindGroupLayout,at_over') limitTest, testValueName, async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + await t.expectValidationError( () => createBindGroupLayout(device, visibility, order, testValue), shouldError ); - } + }, + kExtraLimits ); }); @@ -83,18 +96,29 @@ g.test('createPipelineLayout,at_over') await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, - async ({ device, testValue, shouldError }) => { - const kNumGroups = 3; + async ({ device, testValue, shouldError, actualLimit }) => { + const maxBindingsPerBindGroup = Math.min( + t.device.limits.maxBindingsPerBindGroup, + actualLimit + ); + const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup); + + // Not sure what to do in this case but best we get notified if it happens. + assert(kNumGroups <= t.device.limits.maxBindGroups); + const bindGroupLayouts = range(kNumGroups, i => { - const minInGroup = Math.floor(testValue / kNumGroups); - const numInGroup = i ? minInGroup : testValue - minInGroup * (kNumGroups - 1); + const numInGroup = Math.min( + testValue - i * maxBindingsPerBindGroup, + maxBindingsPerBindGroup + ); return createBindGroupLayout(device, visibility, order, numInGroup); }); await t.expectValidationError( () => device.createPipelineLayout({ bindGroupLayouts }), shouldError ); - } + }, + kExtraLimits ); }); @@ -122,14 +146,27 @@ g.test('createPipeline,at_over') limitTest, testValueName, async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + + // If this was false the texture binding would overlap the sampler bindings. + assert(testValue < device.limits.maxBindGroups * device.limits.maxBindingsPerBindGroup); + + // Put the texture on the last possible binding. + const groupNdx = device.limits.maxBindGroups - 1; + const bindingNdx = device.limits.maxBindingsPerBindGroup - 1; + const code = getPerStageWGSLForBindingCombination( bindingCombination, order, bindGroupTest, (i, j) => `var u${j}_${i}: sampler`, (i, j) => `_ = textureGather(0, tex, u${j}_${i}, vec2f(0));`, + device.limits.maxBindGroups, testValue, - '@group(3) @binding(1) var tex: texture_2d<f32>;' + `@group(${groupNdx}) @binding(${bindingNdx}) var tex: texture_2d<f32>;` ); const module = device.createShaderModule({ code }); @@ -140,6 +177,7 @@ g.test('createPipeline,at_over') shouldError, `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` ); - } + }, + kExtraLimits ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts index 5dfff78907..ee7c3a0246 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts @@ -3,7 +3,9 @@ import { reorder, kReorderOrderKeys, ReorderOrder, + assert, } from '../../../../../common/util/util.js'; +import { kShaderStageCombinationsWithStage } from '../../../../capability_info.js'; import { GPUConst } from '../../../../constants.js'; import { @@ -13,8 +15,14 @@ import { kBindingCombinations, getPipelineTypeForBindingCombination, getPerStageWGSLForBindingCombination, + LimitsRequest, } from './limit_utils.js'; +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', +}; + const limit = 'maxStorageBuffersPerShaderStage'; export const { g, description } = makeLimitTestGroup(limit); @@ -48,34 +56,31 @@ g.test('createBindGroupLayout,at_over') ) .params( kMaximumLimitBaseParams - .combine('visibility', [ - GPUConst.ShaderStage.VERTEX, - GPUConst.ShaderStage.FRAGMENT, - GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.FRAGMENT, - GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE, - ]) + .combine('visibility', kShaderStageCombinationsWithStage) .combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[]) .combine('order', kReorderOrderKeys) + .filter( + ({ visibility, type }) => + (visibility & GPUConst.ShaderStage.VERTEX) === 0 || type !== 'storage' + ) ) .fn(async t => { const { limitTest, testValueName, visibility, order, type } = t.params; - if (visibility & GPUConst.ShaderStage.VERTEX && type === 'storage') { - // vertex stage does not support storage buffers - return; - } - await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + await t.expectValidationError(() => { createBindGroupLayout(device, visibility, type, order, testValue); }, shouldError); - } + }, + kExtraLimits ); }); @@ -90,41 +95,44 @@ g.test('createPipelineLayout,at_over') ) .params( kMaximumLimitBaseParams - .combine('visibility', [ - GPUConst.ShaderStage.VERTEX, - GPUConst.ShaderStage.FRAGMENT, - GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.FRAGMENT, - GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE, - ]) + .combine('visibility', kShaderStageCombinationsWithStage) .combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[]) .combine('order', kReorderOrderKeys) + .filter( + ({ visibility, type }) => + (visibility & GPUConst.ShaderStage.VERTEX) === 0 || type !== 'storage' + ) ) .fn(async t => { const { limitTest, testValueName, visibility, order, type } = t.params; - if (visibility & GPUConst.ShaderStage.VERTEX && type === 'storage') { - // vertex stage does not support storage buffers - return; - } - await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, - async ({ device, testValue, shouldError }) => { - const kNumGroups = 3; + async ({ device, testValue, shouldError, actualLimit }) => { + const maxBindingsPerBindGroup = Math.min( + t.device.limits.maxBindingsPerBindGroup, + actualLimit + ); + const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup); + + // Not sure what to do in this case but best we get notified if it happens. + assert(kNumGroups <= t.device.limits.maxBindGroups); + const bindGroupLayouts = range(kNumGroups, i => { - const minInGroup = Math.floor(testValue / kNumGroups); - const numInGroup = i ? minInGroup : testValue - minInGroup * (kNumGroups - 1); + const numInGroup = Math.min( + testValue - i * maxBindingsPerBindGroup, + maxBindingsPerBindGroup + ); return createBindGroupLayout(device, visibility, type, order, numInGroup); }); + await t.expectValidationError( () => device.createPipelineLayout({ bindGroupLayouts }), shouldError ); - } + }, + kExtraLimits ); }); @@ -152,12 +160,18 @@ g.test('createPipeline,at_over') limitTest, testValueName, async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + const code = getPerStageWGSLForBindingCombination( bindingCombination, order, bindGroupTest, (i, j) => `var<storage> u${j}_${i}: f32`, (i, j) => `_ = u${j}_${i};`, + device.limits.maxBindGroups, testValue ); const module = device.createShaderModule({ code }); @@ -169,6 +183,7 @@ g.test('createPipeline,at_over') shouldError, `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` ); - } + }, + kExtraLimits ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts index dee6069b44..8af61f51fc 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts @@ -3,6 +3,7 @@ import { reorder, ReorderOrder, kReorderOrderKeys, + assert, } from '../../../../../common/util/util.js'; import { GPUConst } from '../../../../constants.js'; @@ -13,8 +14,14 @@ import { getPerStageWGSLForBindingCombinationStorageTextures, getPipelineTypeForBindingCombination, BindingCombination, + LimitsRequest, } from './limit_utils.js'; +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', +}; + const limit = 'maxStorageTexturesPerShaderStage'; export const { g, description } = makeLimitTestGroup(limit); @@ -60,11 +67,17 @@ g.test('createBindGroupLayout,at_over') limitTest, testValueName, async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + await t.expectValidationError( () => createBindGroupLayout(device, visibility, order, testValue), shouldError ); - } + }, + kExtraLimits ); }); @@ -91,18 +104,30 @@ g.test('createPipelineLayout,at_over') await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, - async ({ device, testValue, shouldError }) => { - const kNumGroups = 3; + async ({ device, testValue, shouldError, actualLimit }) => { + const maxBindingsPerBindGroup = Math.min( + t.device.limits.maxBindingsPerBindGroup, + actualLimit + ); + const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup); + + // Not sure what to do in this case but best we get notified if it happens. + assert(kNumGroups <= t.device.limits.maxBindGroups); + const bindGroupLayouts = range(kNumGroups, i => { - const minInGroup = Math.floor(testValue / kNumGroups); - const numInGroup = i ? minInGroup : testValue - minInGroup * (kNumGroups - 1); + const numInGroup = Math.min( + testValue - i * maxBindingsPerBindGroup, + maxBindingsPerBindGroup + ); return createBindGroupLayout(device, visibility, order, numInGroup); }); + await t.expectValidationError( () => device.createPipelineLayout({ bindGroupLayouts }), shouldError ); - } + }, + kExtraLimits ); }); @@ -130,6 +155,11 @@ g.test('createPipeline,at_over') limitTest, testValueName, async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + if (bindingCombination === 'fragment') { return; } @@ -140,6 +170,7 @@ g.test('createPipeline,at_over') bindGroupTest, (i, j) => `var u${j}_${i}: texture_storage_2d<rgba8unorm, write>`, (i, j) => `textureStore(u${j}_${i}, vec2u(0), vec4f(1));`, + device.limits.maxBindGroups, testValue ); const module = device.createShaderModule({ code }); @@ -151,6 +182,7 @@ g.test('createPipeline,at_over') shouldError, `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` ); - } + }, + kExtraLimits ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxUniformBuffersPerShaderStage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxUniformBuffersPerShaderStage.spec.ts index 7e55078f16..64de1a71f6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxUniformBuffersPerShaderStage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxUniformBuffersPerShaderStage.spec.ts @@ -3,6 +3,7 @@ import { reorder, kReorderOrderKeys, ReorderOrder, + assert, } from '../../../../../common/util/util.js'; import { kShaderStageCombinationsWithStage } from '../../../../capability_info.js'; @@ -13,8 +14,14 @@ import { kBindingCombinations, getPipelineTypeForBindingCombination, getPerStageWGSLForBindingCombination, + LimitsRequest, } from './limit_utils.js'; +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', +}; + const limit = 'maxUniformBuffersPerShaderStage'; export const { g, description } = makeLimitTestGroup(limit); @@ -56,11 +63,17 @@ g.test('createBindGroupLayout,at_over') limitTest, testValueName, async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + await t.expectValidationError( () => createBindGroupLayout(device, visibility, order, testValue), shouldError ); - } + }, + kExtraLimits ); }); @@ -83,18 +96,30 @@ g.test('createPipelineLayout,at_over') await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, - async ({ device, testValue, shouldError }) => { - const kNumGroups = 3; + async ({ device, testValue, shouldError, actualLimit }) => { + const maxBindingsPerBindGroup = Math.min( + t.device.limits.maxBindingsPerBindGroup, + actualLimit + ); + const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup); + + // Not sure what to do in this case but best we get notified if it happens. + assert(kNumGroups <= t.device.limits.maxBindGroups); + const bindGroupLayouts = range(kNumGroups, i => { - const minInGroup = Math.floor(testValue / kNumGroups); - const numInGroup = i ? minInGroup : testValue - minInGroup * (kNumGroups - 1); + const numInGroup = Math.min( + testValue - i * maxBindingsPerBindGroup, + maxBindingsPerBindGroup + ); return createBindGroupLayout(device, visibility, order, numInGroup); }); + await t.expectValidationError( () => device.createPipelineLayout({ bindGroupLayouts }), shouldError ); - } + }, + kExtraLimits ); }); @@ -122,12 +147,18 @@ g.test('createPipeline,at_over') limitTest, testValueName, async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + const code = getPerStageWGSLForBindingCombination( bindingCombination, order, bindGroupTest, (i, j) => `var<uniform> u${j}_${i}: f32`, (i, j) => `_ = u${j}_${i};`, + device.limits.maxBindGroups, testValue ); const module = device.createShaderModule({ code }); @@ -139,6 +170,7 @@ g.test('createPipeline,at_over') shouldError, `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` ); - } + }, + kExtraLimits ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxVertexBuffers.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxVertexBuffers.spec.ts index 7f760fe9b6..51cb44d55b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxVertexBuffers.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/maxVertexBuffers.spec.ts @@ -1,41 +1,23 @@ -import { range } from '../../../../../common/util/util.js'; - import { kRenderEncoderTypes, kMaximumLimitBaseParams, makeLimitTestGroup } from './limit_utils.js'; -const kPipelineTypes = ['withoutLocations', 'withLocations'] as const; -type PipelineType = (typeof kPipelineTypes)[number]; +function getPipelineDescriptor(device: GPUDevice, testValue: number): GPURenderPipelineDescriptor { + const module = device.createShaderModule({ + code: ` + @vertex fn vs(@location(0) p: vec4f) -> @builtin(position) vec4f { + return p; + }`, + }); + const buffers = new Array<GPUVertexBufferLayout>(testValue); + buffers[testValue - 1] = { + arrayStride: 16, + attributes: [{ shaderLocation: 0, offset: 0, format: 'float32' }], + }; -function getPipelineDescriptor( - device: GPUDevice, - pipelineType: PipelineType, - testValue: number -): GPURenderPipelineDescriptor { - const code = - pipelineType === 'withLocations' - ? ` - struct VSInput { - ${range(testValue, i => `@location(${i}) p${i}: f32,`).join('\n')} - } - @vertex fn vs(v: VSInput) -> @builtin(position) vec4f { - let x = ${range(testValue, i => `v.p${i}`).join(' + ')}; - return vec4f(x, 0, 0, 1); - } - ` - : ` - @vertex fn vs() -> @builtin(position) vec4f { - return vec4f(0); - } - `; - const module = device.createShaderModule({ code }); return { layout: 'auto', vertex: { module, - entryPoint: 'vs', - buffers: range(testValue, i => ({ - arrayStride: 32, - attributes: [{ shaderLocation: i, offset: 0, format: 'float32' }], - })), + buffers, }, }; } @@ -45,18 +27,22 @@ export const { g, description } = makeLimitTestGroup(limit); g.test('createRenderPipeline,at_over') .desc(`Test using at and over ${limit} limit in createRenderPipeline(Async)`) - .params( - kMaximumLimitBaseParams.combine('async', [false, true]).combine('pipelineType', kPipelineTypes) - ) + .params(kMaximumLimitBaseParams.combine('async', [false, true])) .fn(async t => { - const { limitTest, testValueName, async, pipelineType } = t.params; + const { limitTest, testValueName, async } = t.params; await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, - async ({ device, testValue, shouldError }) => { - const pipelineDescriptor = getPipelineDescriptor(device, pipelineType, testValue); + async ({ device, testValue, shouldError, actualLimit }) => { + const pipelineDescriptor = getPipelineDescriptor(device, testValue); + const lastIndex = testValue - 1; - await t.testCreateRenderPipeline(pipelineDescriptor, async, shouldError); + await t.testCreateRenderPipeline( + pipelineDescriptor, + async, + shouldError, + `lastIndex: ${lastIndex}, actualLimit: ${actualLimit}, shouldError: ${shouldError}` + ); } ); }); @@ -77,10 +63,10 @@ g.test('setVertexBuffer,at_over') usage: GPUBufferUsage.VERTEX, }); - await t.testGPURenderCommandsMixin( + await t.testGPURenderAndBindingCommandsMixin( encoderType, - ({ mixin }) => { - mixin.setVertexBuffer(lastIndex, buffer); + ({ passEncoder }) => { + passEncoder.setVertexBuffer(lastIndex, buffer); }, shouldError, `lastIndex: ${lastIndex}, actualLimit: ${actualLimit}, shouldError: ${shouldError}` diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts index 3a0a51b363..790f25897a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts @@ -5,9 +5,16 @@ Note: entry point matching tests are in shader_module/entry_point.spec.ts `; import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { keysOf } from '../../../common/util/data_tables.js'; import { kValue } from '../../util/constants.js'; import { TShaderStage, getShaderWithEntryPoint } from '../../util/shader.js'; +import { + kAPIResources, + getWGSLShaderForResource, + getAPIBindGroupLayoutForResource, + doResourcesMatch, +} from './utils.js'; import { ValidationTest } from './validation_test.js'; class F extends ValidationTest { @@ -690,3 +697,46 @@ Tests calling createComputePipeline(Async) validation for overridable constants testFn(maxVec4Count + 1, 0, false); testFn(0, maxMat4Count + 1, false); }); + +g.test('resource_compatibility') + .desc( + 'Tests validation of resource (bind group) compatibility between pipeline layout and WGSL shader' + ) + .params(u => + u // + .combine('apiResource', keysOf(kAPIResources)) + .beginSubcases() + .combine('isAsync', [true, false] as const) + .combine('wgslResource', keysOf(kAPIResources)) + ) + .fn(t => { + const apiResource = kAPIResources[t.params.apiResource]; + const wgslResource = kAPIResources[t.params.wgslResource]; + t.skipIf( + wgslResource.storageTexture !== undefined && + wgslResource.storageTexture.access !== 'write-only' && + !t.hasLanguageFeature('readonly_and_readwrite_storage_textures'), + 'Storage textures require language feature' + ); + + const layout = t.device.createPipelineLayout({ + bindGroupLayouts: [ + getAPIBindGroupLayoutForResource(t.device, GPUShaderStage.COMPUTE, apiResource), + ], + }); + + const descriptor = { + layout, + compute: { + module: t.device.createShaderModule({ + code: getWGSLShaderForResource('compute', wgslResource), + }), + entryPoint: 'main', + }, + }; + t.doCreateComputePipelineTest( + t.params.isAsync, + doResourcesMatch(apiResource, wgslResource), + descriptor + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts index ddd0f8b39f..b2d1939a4f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts @@ -8,6 +8,7 @@ import { makeTestGroup } from '../../../common/framework/test_group.js'; import { assert, makeValueTestVariant, unreachable } from '../../../common/util/util.js'; import { allBindingEntries, + BindableResource, bindingTypeInfo, bufferBindingEntries, bufferBindingTypeInfo, @@ -106,7 +107,7 @@ g.test('binding_must_contain_resource_defined_in_layout') .desc( 'Test that only compatible resource types specified in the BindGroupLayout are allowed for each entry.' ) - .paramsSubcasesOnly(u => + .params(u => u // .combine('resourceType', kBindableResources) .combine('entry', allBindingEntries(false)) @@ -121,6 +122,17 @@ g.test('binding_must_contain_resource_defined_in_layout') const resource = t.getBindingResource(resourceType); + const IsStorageTextureResourceType = (resourceType: BindableResource) => { + switch (resourceType) { + case 'readonlyStorageTex': + case 'readwriteStorageTex': + case 'writeonlyStorageTex': + return true; + default: + return false; + } + }; + let resourceBindingIsCompatible; switch (info.resource) { // Either type of sampler may be bound to a filtering sampler binding. @@ -131,6 +143,11 @@ g.test('binding_must_contain_resource_defined_in_layout') case 'nonFiltSamp': resourceBindingIsCompatible = resourceType === 'nonFiltSamp'; break; + case 'readonlyStorageTex': + case 'readwriteStorageTex': + case 'writeonlyStorageTex': + resourceBindingIsCompatible = IsStorageTextureResourceType(resourceType); + break; default: resourceBindingIsCompatible = info.resource === resourceType; break; @@ -166,7 +183,7 @@ g.test('texture_binding_must_have_correct_usage') const descriptor = { size: { width: 16, height: 16, depthOrArrayLayers: 1 }, - format: 'rgba8unorm' as const, + format: 'r32float' as const, usage: appliedUsage, sampleCount: info.resource === 'sampledTexMS' ? 4 : 1, }; @@ -313,6 +330,19 @@ g.test('texture_must_have_correct_dimension') }); t.skipIfTextureViewDimensionNotSupported(viewDimension, dimension); + if (t.isCompatibility && texture.dimension === '2d') { + if (depthOrArrayLayers === 1) { + t.skipIf( + viewDimension !== '2d', + '1 layer 2d textures default to textureBindingViewDimension: "2d" in compat mode' + ); + } else { + t.skipIf( + viewDimension !== '2d-array', + '> 1 layer 2d textures default to textureBindingViewDimension "2d-array" in compat mode' + ); + } + } const shouldError = viewDimension !== dimension; const textureView = texture.createView({ dimension }); @@ -526,9 +556,7 @@ g.test('buffer,resource_state') g.test('texture,resource_state') .desc('Test bind group creation with various texture resource states') .paramsSubcasesOnly(u => - u - .combine('state', kResourceStates) - .combine('entry', sampledAndStorageBindingEntries(true, 'rgba8unorm')) + u.combine('state', kResourceStates).combine('entry', sampledAndStorageBindingEntries(true)) ) .fn(t => { const { state, entry } = t.params; @@ -548,10 +576,11 @@ g.test('texture,resource_state') const usage = entry.texture?.multisampled ? info.usage | GPUConst.TextureUsage.RENDER_ATTACHMENT : info.usage; + const format = entry.storageTexture !== undefined ? 'r32float' : 'rgba8unorm'; const texture = t.createTextureWithState(state, { usage, size: [1, 1], - format: 'rgba8unorm', + format, sampleCount: entry.texture?.multisampled ? 4 : 1, }); @@ -626,7 +655,9 @@ g.test('binding_resources,device_mismatch') { buffer: { type: 'storage' } }, { sampler: { type: 'filtering' } }, { texture: { multisampled: false } }, - { storageTexture: { access: 'write-only', format: 'rgba8unorm' } }, + { storageTexture: { access: 'write-only', format: 'r32float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + { storageTexture: { access: 'read-write', format: 'r32float' } }, ] as const) .beginSubcases() .combineWithParams([ @@ -784,6 +815,10 @@ g.test('storage_texture,format') .combine('storageTextureFormat', kStorageTextureFormats) .combine('resourceFormat', kStorageTextureFormats) ) + .beforeAllSubcases(t => { + const { storageTextureFormat, resourceFormat } = t.params; + t.skipIfTextureFormatNotUsableAsStorageTexture(storageTextureFormat, resourceFormat); + }) .fn(t => { const { storageTextureFormat, resourceFormat } = t.params; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts index a50247aa13..b09adc2af1 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts @@ -163,8 +163,10 @@ g.test('visibility,VERTEX_shader_stage_storage_texture_access') .fn(t => { const { shaderStage, access } = t.params; + const appliedAccess = access ?? 'write-only'; const success = !( - (access ?? 'write-only') === 'write-only' && shaderStage & GPUShaderStage.VERTEX + // If visibility includes VERETX, storageTexture.access must be "read-only" + (shaderStage & GPUShaderStage.VERTEX && appliedAccess !== 'read-only') ); t.expectValidationError(() => { @@ -173,7 +175,7 @@ g.test('visibility,VERTEX_shader_stage_storage_texture_access') { binding: 0, visibility: shaderStage, - storageTexture: { access, format: 'rgba8unorm' }, + storageTexture: { access, format: 'r32uint' }, }, ], }); @@ -436,29 +438,36 @@ g.test('storage_texture,layout_dimension') g.test('storage_texture,formats') .desc( ` - Test that a validation error is generated if the format doesn't support the storage usage. + Test that a validation error is generated if the format doesn't support the storage usage. A + validation error is also generated if the format doesn't support the 'read-write' storage access + when the storage access is 'read-write'. ` ) - .params(u => u.combine('format', kAllTextureFormats)) + .params(u => + u // + .combine('format', kAllTextureFormats) // + .combine('access', kStorageTextureAccessValues) + ) .beforeAllSubcases(t => { t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + t.skipIfTextureFormatNotUsableAsStorageTexture(t.params.format); }) .fn(t => { - const { format } = t.params; + const { format, access } = t.params; const info = kTextureFormatInfo[format]; - t.expectValidationError( - () => { - t.device.createBindGroupLayout({ - entries: [ - { - binding: 0, - visibility: GPUShaderStage.COMPUTE, - storageTexture: { format }, - }, - ], - }); - }, - !info.color?.storage - ); + const success = + info.color?.storage && !(access === 'read-write' && !info.color?.readWriteStorage); + + t.expectValidationError(() => { + t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + storageTexture: { format, access }, + }, + ], + }); + }, !success); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts index a9fe352b74..fc1c8b86b2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts @@ -6,7 +6,7 @@ import { assert, makeValueTestVariant } from '../../../common/util/util.js'; import { kTextureDimensions, kTextureUsages } from '../../capability_info.js'; import { GPUConst } from '../../constants.js'; import { - kTextureFormats, + kAllTextureFormats, kTextureFormatInfo, kCompressedTextureFormats, kUncompressedTextureFormats, @@ -15,6 +15,7 @@ import { filterFormatsByFeature, viewCompatible, textureDimensionAndFormatCompatible, + isTextureFormatUsableAsStorageFormat, } from '../../format_info.js'; import { maxMipLevelCount } from '../../util/texture/base.js'; @@ -103,7 +104,9 @@ g.test('dimension_type_and_format_compatibility') `Test every dimension type on every format. Note that compressed formats and depth/stencil formats are not valid for 1D/3D dimension types.` ) .params(u => - u.combine('dimension', [undefined, ...kTextureDimensions]).combine('format', kTextureFormats) + u // + .combine('dimension', [undefined, ...kTextureDimensions]) + .combine('format', kAllTextureFormats) ) .beforeAllSubcases(t => { const { format } = t.params; @@ -135,7 +138,7 @@ g.test('mipLevelCount,format') .params(u => u .combine('dimension', [undefined, ...kTextureDimensions]) - .combine('format', kTextureFormats) + .combine('format', kAllTextureFormats) .beginSubcases() .combine('mipLevelCount', [1, 2, 3, 6, 7]) // Filter out incompatible dimension type and format combinations. @@ -270,7 +273,7 @@ g.test('sampleCount,various_sampleCount_with_all_formats') .params(u => u .combine('dimension', [undefined, '2d'] as const) - .combine('format', kTextureFormats) + .combine('format', kAllTextureFormats) .beginSubcases() .combine('sampleCount', [0, 1, 2, 4, 8, 16, 32, 256]) ) @@ -296,7 +299,7 @@ g.test('sampleCount,various_sampleCount_with_all_formats') usage, }; - const success = sampleCount === 1 || (sampleCount === 4 && info.multisample && info.renderable); + const success = sampleCount === 1 || (sampleCount === 4 && info.multisample); t.expectValidationError(() => { t.device.createTexture(descriptor); @@ -317,7 +320,7 @@ g.test('sampleCount,valid_sampleCount_with_other_parameter_varies') .params(u => u .combine('dimension', [undefined, ...kTextureDimensions]) - .combine('format', kTextureFormats) + .combine('format', kAllTextureFormats) .beginSubcases() .combine('sampleCount', [1, 4]) .combine('arrayLayerCount', [1, 2]) @@ -372,8 +375,12 @@ g.test('sampleCount,valid_sampleCount_with_other_parameter_varies') usage, }; + const satisfyWithStorageUsageRequirement = + (usage & GPUConst.TextureUsage.STORAGE_BINDING) === 0 || + isTextureFormatUsableAsStorageFormat(format, t.isCompatibility); + const success = - sampleCount === 1 || + (sampleCount === 1 && satisfyWithStorageUsageRequirement) || (sampleCount === 4 && (dimension === '2d' || dimension === undefined) && kTextureFormatInfo[format].multisample && @@ -589,6 +596,7 @@ g.test('texture_size,2d_texture,compressed_format') u .combine('dimension', [undefined, '2d'] as const) .combine('format', kCompressedTextureFormats) + .beginSubcases() .expand('sizeVariant', p => { const { blockWidth, blockHeight } = kTextureFormatInfo[p.format]; return [ @@ -1026,7 +1034,7 @@ g.test('texture_usage') .params(u => u .combine('dimension', [undefined, ...kTextureDimensions]) - .combine('format', kTextureFormats) + .combine('format', kAllTextureFormats) .beginSubcases() // If usage0 and usage1 are the same, then the usage being test is a single usage. Otherwise, it is a combined usage. .combine('usage0', kTextureUsages) @@ -1058,12 +1066,13 @@ g.test('texture_usage') // Note that we unconditionally test copy usages for all formats. We don't check copySrc/copyDst in kTextureFormatInfo in capability_info.js // if (!info.copySrc && (usage & GPUTextureUsage.COPY_SRC) !== 0) success = false; // if (!info.copyDst && (usage & GPUTextureUsage.COPY_DST) !== 0) success = false; - if (!info.color?.storage && (usage & GPUTextureUsage.STORAGE_BINDING) !== 0) success = false; - if ( - (!info.renderable || appliedDimension !== '2d') && - (usage & GPUTextureUsage.RENDER_ATTACHMENT) !== 0 - ) - success = false; + if (usage & GPUTextureUsage.STORAGE_BINDING) { + if (!isTextureFormatUsableAsStorageFormat(format, t.isCompatibility)) success = false; + } + if (usage & GPUTextureUsage.RENDER_ATTACHMENT) { + if (appliedDimension === '1d') success = false; + if (info.color && !info.colorRender) success = false; + } t.expectValidationError(() => { t.device.createTexture(descriptor); @@ -1080,10 +1089,10 @@ g.test('viewFormats') .combine('viewFormatFeature', kFeaturesForFormats) .beginSubcases() .expand('format', ({ formatFeature }) => - filterFormatsByFeature(formatFeature, kTextureFormats) + filterFormatsByFeature(formatFeature, kAllTextureFormats) ) .expand('viewFormat', ({ viewFormatFeature }) => - filterFormatsByFeature(viewFormatFeature, kTextureFormats) + filterFormatsByFeature(viewFormatFeature, kAllTextureFormats) ) ) .beforeAllSubcases(t => { @@ -1096,7 +1105,7 @@ g.test('viewFormats') t.skipIfTextureFormatNotSupported(format, viewFormat); - const compatible = viewCompatible(format, viewFormat); + const compatible = viewCompatible(t.isCompatibility, format, viewFormat); // Test the viewFormat in the list. t.expectValidationError(() => { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts index e4871c5d80..8949ea1eeb 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts @@ -10,7 +10,7 @@ import { } from '../../capability_info.js'; import { kTextureFormatInfo, - kTextureFormats, + kAllTextureFormats, kFeaturesForFormats, filterFormatsByFeature, viewCompatible, @@ -39,10 +39,10 @@ g.test('format') .combine('viewFormatFeature', kFeaturesForFormats) .beginSubcases() .expand('textureFormat', ({ textureFormatFeature }) => - filterFormatsByFeature(textureFormatFeature, kTextureFormats) + filterFormatsByFeature(textureFormatFeature, kAllTextureFormats) ) .expand('viewFormat', ({ viewFormatFeature }) => - filterFormatsByFeature(viewFormatFeature, [undefined, ...kTextureFormats]) + filterFormatsByFeature(viewFormatFeature, [undefined, ...kAllTextureFormats]) ) .combine('useViewFormatList', [false, true]) ) @@ -56,7 +56,8 @@ g.test('format') t.skipIfTextureFormatNotSupported(textureFormat, viewFormat); - const compatible = viewFormat === undefined || viewCompatible(textureFormat, viewFormat); + const compatible = + viewFormat === undefined || viewCompatible(t.isCompatibility, textureFormat, viewFormat); const texture = t.device.createTexture({ format: textureFormat, @@ -123,7 +124,7 @@ g.test('aspect') ) .params(u => u // - .combine('format', kTextureFormats) + .combine('format', kAllTextureFormats) .combine('aspect', kTextureAspects) ) .beforeAllSubcases(t => { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts index f1c3d91e29..75f4a092fc 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts @@ -6,7 +6,7 @@ import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { kTextureUsages, kTextureDimensions } from '../../../../capability_info.js'; import { kTextureFormatInfo, - kTextureFormats, + kAllTextureFormats, kCompressedTextureFormats, kDepthStencilFormats, kFeaturesForFormats, @@ -255,6 +255,12 @@ Test that textures in copyTextureToTexture must have the same sample count. .combine('srcSampleCount', [1, 4]) .combine('dstSampleCount', [1, 4]) ) + .beforeAllSubcases(t => { + t.skipIf( + t.isCompatibility && (t.params.srcSampleCount !== 1 || t.params.dstSampleCount !== 1), + 'multisample textures are not copyable in compatibility mode' + ); + }) .fn(t => { const { srcSampleCount, dstSampleCount } = t.params; @@ -307,6 +313,9 @@ TODO: Check the source and destination constraints separately. .expand('copyWidth', p => [32 - Math.max(p.srcCopyOrigin.x, p.dstCopyOrigin.x), 16]) .expand('copyHeight', p => [16 - Math.max(p.srcCopyOrigin.y, p.dstCopyOrigin.y), 8]) ) + .beforeAllSubcases(t => { + t.skipIf(t.isCompatibility, 'multisample textures are not copyable in compatibility mode'); + }) .fn(t => { const { srcCopyOrigin, dstCopyOrigin, copyWidth, copyHeight } = t.params; @@ -351,10 +360,10 @@ Test the formats of textures in copyTextureToTexture must be copy-compatible. .combine('dstFormatFeature', kFeaturesForFormats) .beginSubcases() .expand('srcFormat', ({ srcFormatFeature }) => - filterFormatsByFeature(srcFormatFeature, kTextureFormats) + filterFormatsByFeature(srcFormatFeature, kAllTextureFormats) ) .expand('dstFormat', ({ dstFormatFeature }) => - filterFormatsByFeature(dstFormatFeature, kTextureFormats) + filterFormatsByFeature(dstFormatFeature, kAllTextureFormats) ) ) .beforeAllSubcases(t => { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts index 2eaa9b43fd..b4beeb8fbe 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts @@ -196,10 +196,7 @@ g.test('valid_texture_formats') g.test('depth_stencil_readonly') .desc( ` - Tests that createRenderBundleEncoder validation of depthReadOnly and stencilReadOnly - - With depth-only formats - - With stencil-only formats - - With depth-stencil-combined formats + Test that allow combinations of depth-stencil format, depthReadOnly and stencilReadOnly are allowed. ` ) .params(u => @@ -215,44 +212,9 @@ g.test('depth_stencil_readonly') }) .fn(t => { const { depthStencilFormat, depthReadOnly, stencilReadOnly } = t.params; - - let shouldError = false; - if ( - kTextureFormatInfo[depthStencilFormat].depth && - kTextureFormatInfo[depthStencilFormat].stencil && - depthReadOnly !== stencilReadOnly - ) { - shouldError = true; - } - - t.expectValidationError(() => { - t.device.createRenderBundleEncoder({ - colorFormats: [], - depthStencilFormat, - depthReadOnly, - stencilReadOnly, - }); - }, shouldError); - }); - -g.test('depth_stencil_readonly_with_undefined_depth') - .desc( - ` - Tests that createRenderBundleEncoder validation of depthReadOnly and stencilReadOnly is ignored - if there is no depthStencilFormat set. - ` - ) - .params(u => - u // - .beginSubcases() - .combine('depthReadOnly', [false, true]) - .combine('stencilReadOnly', [false, true]) - ) - .fn(t => { - const { depthReadOnly, stencilReadOnly } = t.params; - t.device.createRenderBundleEncoder({ - colorFormats: ['bgra8unorm'], + colorFormats: [], + depthStencilFormat, depthReadOnly, stencilReadOnly, }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts index 0d56222eed..815ca5a198 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts @@ -57,7 +57,10 @@ class F extends ValidationTest { export const g = makeTestGroup(F); -type EncoderCommands = keyof Omit<GPUCommandEncoder, '__brand' | 'label' | 'finish'>; +// MAINTENANCE_TODO: Remove writeTimestamp from here once it's (hopefully) added back to the spec. +type EncoderCommands = + | keyof Omit<GPUCommandEncoder, '__brand' | 'label' | 'finish'> + | 'writeTimestamp'; const kEncoderCommandInfo: { readonly [k in EncoderCommands]: {}; } = { @@ -146,6 +149,8 @@ g.test('non_pass_commands') ` Test that functions of GPUCommandEncoder generate a validation error if the encoder is already finished. + + TODO: writeTimestamp is removed from the spec so it's skipped if it TypeErrors. ` ) .params(u => @@ -260,8 +265,11 @@ g.test('non_pass_commands') } break; case 'writeTimestamp': - { - encoder.writeTimestamp(querySet, 0); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (encoder as any).writeTimestamp(querySet, 0); + } catch (ex) { + t.skipIf(ex instanceof TypeError, 'writeTimestamp is actually not available'); } break; case 'resolveQuerySet': diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts index 163c20c311..8da3be33de 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts @@ -32,13 +32,32 @@ type ComputeCmd = (typeof kComputeCmds)[number]; const kRenderCmds = ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const; type RenderCmd = (typeof kRenderCmds)[number]; +const kPipelineTypes = ['auto0', 'explicit'] as const; +type PipelineType = (typeof kPipelineTypes)[number]; +const kBindingTypes = ['auto0', 'auto1', 'explicit'] as const; +type BindingType = (typeof kBindingTypes)[number]; + +const kEmptyBindGroup0Ndx = 0; +const kEmptyBindGroup1Ndx = 1; +const kNonEmptyBindGroup0Ndx = 2; +const kNonEmptyBindGroup1Ndx = 3; + +// Swaps 2 array elements in place. +function swapArrayElements<T>(array: T[], ndx1: number, ndx2: number) { + const t = array[ndx1]; + array[ndx1] = array[ndx2]; + array[ndx2] = t; +} + // Test resource type compatibility in pipeline and bind group // [1]: Need to add externalTexture const kResourceTypes: ValidBindableResource[] = [ 'uniformBuf', 'filtSamp', 'sampledTex', - 'storageTex', + 'readonlyStorageTex', + 'writeonlyStorageTex', + 'readwriteStorageTex', ]; function getTestCmds( @@ -75,7 +94,17 @@ class F extends ValidationTest { if (entry.buffer !== undefined) return 'uniformBuf'; if (entry.sampler !== undefined) return 'filtSamp'; if (entry.texture !== undefined) return 'sampledTex'; - if (entry.storageTexture !== undefined) return 'storageTex'; + if (entry.storageTexture !== undefined) { + switch (entry.storageTexture.access) { + case undefined: + case 'write-only': + return 'writeonlyStorageTex'; + case 'read-only': + return 'readonlyStorageTex'; + case 'read-write': + return 'readwriteStorageTex'; + } + } unreachable(); } @@ -208,8 +237,14 @@ class F extends ValidationTest { case 'sampledTex': entry.texture = {}; // default sampleType: float break; - case 'storageTex': - entry.storageTexture = { access: 'write-only', format: 'rgba8unorm' }; + case 'readonlyStorageTex': + entry.storageTexture = { access: 'read-only', format: 'r32float' }; + break; + case 'writeonlyStorageTex': + entry.storageTexture = { access: 'write-only', format: 'r32float' }; + break; + case 'readwriteStorageTex': + entry.storageTexture = { access: 'read-write', format: 'r32float' }; break; } @@ -259,6 +294,135 @@ class F extends ValidationTest { validateFinish(success); } + + runDefaultLayoutBindingTest<T extends GPURenderPipeline | GPUComputePipeline>({ + visibility, + empty, + pipelineType, + bindingType, + swap, + success, + makePipelinesFn, + doCommandFn, + }: { + visibility: number; + empty: boolean; + pipelineType: PipelineType; + bindingType: BindingType; + swap: boolean; + success: boolean; + makePipelinesFn: (t: F, explicitPipelineLayout: GPUPipelineLayout) => T[]; + doCommandFn: (params: { + t: F; + encoder: GPUCommandEncoder; + pipeline: T; + emptyBindGroups: GPUBindGroup[]; + nonEmptyBindGroups: GPUBindGroup[]; + }) => void; + }) { + const { device } = this; + const explicitEmptyBindGroupLayout = device.createBindGroupLayout({ + entries: [], + }); + const explicitBindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility, + buffer: {}, + }, + ], + }); + const explicitPipelineLayout = device.createPipelineLayout({ + bindGroupLayouts: [ + explicitEmptyBindGroupLayout, + explicitEmptyBindGroupLayout, + explicitBindGroupLayout, + explicitBindGroupLayout, + ], + }); + + const [pipelineAuto0, pipelineAuto1, pipelineExplicit] = makePipelinesFn( + this, + explicitPipelineLayout + ); + + const buffer = device.createBuffer({ + size: 16, + usage: GPUBufferUsage.UNIFORM, + }); + this.trackForCleanup(buffer); + + let emptyBindGroupLayouts; + let nonEmptyBindGroupLayouts; + const pipeline = pipelineType === 'auto0' ? pipelineAuto0 : pipelineExplicit; + + // Gets a bindGroupLayout either from the explicit layout passed in + // or one of the 2 `layout: 'auto'` pipelines. + const getBindGroupLayout = ( + explicitBindGroupLayout: GPUBindGroupLayout, + bindGroupIndex: number + ) => + bindingType === 'explicit' + ? explicitBindGroupLayout + : bindingType === 'auto0' + ? pipelineAuto0.getBindGroupLayout(bindGroupIndex) + : pipelineAuto1.getBindGroupLayout(bindGroupIndex); + + if (empty) { + // Testing empty: + // - emptyBindGroupLayout comes from a possibly incompatible place. + // - nonEmptyBindGroupLayout comes from the pipeline we'll render/compute with. + emptyBindGroupLayouts = [ + getBindGroupLayout(explicitEmptyBindGroupLayout, kEmptyBindGroup0Ndx), + getBindGroupLayout(explicitEmptyBindGroupLayout, kEmptyBindGroup1Ndx), + ]; + if (swap) { + swapArrayElements(emptyBindGroupLayouts, 0, 1); + } + nonEmptyBindGroupLayouts = [ + pipeline.getBindGroupLayout(kNonEmptyBindGroup0Ndx), + pipeline.getBindGroupLayout(kNonEmptyBindGroup1Ndx), + ]; + } else { + // Testing non-empty: + // - nonEmptyBindGroupLayout comes from a possibly incompatible place. + // - emptyBindGroupLayout comes from the pipeline we'll render/compute with. + nonEmptyBindGroupLayouts = [ + getBindGroupLayout(explicitBindGroupLayout, kNonEmptyBindGroup0Ndx), + getBindGroupLayout(explicitBindGroupLayout, kNonEmptyBindGroup1Ndx), + ]; + if (swap) { + swapArrayElements(nonEmptyBindGroupLayouts, 0, 1); + } + emptyBindGroupLayouts = [ + pipeline.getBindGroupLayout(kEmptyBindGroup0Ndx), + pipeline.getBindGroupLayout(kEmptyBindGroup1Ndx), + ]; + } + + const emptyBindGroups = emptyBindGroupLayouts.map(layout => + device.createBindGroup({ + layout, + entries: [], + }) + ); + + const nonEmptyBindGroups = nonEmptyBindGroupLayouts.map(layout => + device.createBindGroup({ + layout, + entries: [{ binding: 0, resource: { buffer } }], + }) + ); + + const encoder = device.createCommandEncoder(); + + doCommandFn({ t: this, encoder, pipeline, emptyBindGroups, nonEmptyBindGroups }); + + this.expectValidationError(() => { + encoder.finish(); + }, !success); + } } export const g = makeTestGroup(F); @@ -775,3 +939,174 @@ g.test('empty_bind_group_layouts_requires_empty_bind_groups,render_pass') encoder.finish(); }, !success); }); + +// pipelineType specifies which pipeline to try to render/compute with +// auto0 = the first `layout: 'auto'` pipeline +// explicit = a pipeline crated with an explicit pipeline layout using explicit bind group layouts +// +// bindingType specifies where to get bindGroupLayouts to use to create bindGroups +// auto0 = the first `layout: 'auto'` pipeline +// auto1 = the second `layout: 'auto'` pipeline +// explicit = a pipeline crated with an explicit pipeline layout using explicit bind group layouts +// +// swap specifies to swap the bindgroups we're testing. We test 2 of each type, 2 empty bindgroups and +// 2 non-empty bindgroups. The 2 empty bindgroups, when swapped should still be compatible. Similarly +// the 2 non-empty bindgroups, when swapped, should still be compatible. +const kPipelineTypesAndBindingTypeParams = [ + { pipelineType: 'auto0', bindingType: 'auto0', swap: false, _success: true }, + { pipelineType: 'explicit', bindingType: 'explicit', swap: false, _success: true }, + { pipelineType: 'explicit', bindingType: 'auto0', swap: false, _success: false }, + { pipelineType: 'auto0', bindingType: 'explicit', swap: false, _success: false }, + { pipelineType: 'auto0', bindingType: 'auto1', swap: false, _success: false }, + { pipelineType: 'auto0', bindingType: 'auto0', swap: true, _success: true }, +] as const; + +g.test('default_bind_group_layouts_never_match,compute_pass') + .desc( + ` + Test that bind groups created with default bind group layouts never match other layouts, including empty bind groups. + + * Test that a pipeline with an explicit layout can not be used with a bindGroup from an auto layout + * Test that a pipeline with an auto layout can not be used with a bindGroup from an explicit layout + * Test that an auto layout from one pipeline can not be used with an auto layout from a different pipeline. + * Test matching bindgroup layouts on the same default layout pipeline are compatible. In other words if + you only define group(2) then group(0)'s empty layout and group(1)'s empty layout should be compatible. + Similarly if group(2) and group(3) have the same types of resources they should be compatible. + ` + ) + .params(u => + u + .combineWithParams(kPipelineTypesAndBindingTypeParams) + .combine('empty', [false, true]) + .combine('computeCommand', ['dispatchIndirect', 'dispatch'] as const) + ) + .fn(t => { + const { pipelineType, bindingType, swap, _success: success, computeCommand, empty } = t.params; + + t.runDefaultLayoutBindingTest<GPUComputePipeline>({ + visibility: GPUShaderStage.COMPUTE, + empty, + pipelineType, + bindingType, + swap, + success, + makePipelinesFn: (t, explicitPipelineLayout) => { + return (['auto', 'auto', explicitPipelineLayout] as const).map<GPUComputePipeline>(layout => + t.device.createComputePipeline({ + layout, + compute: { + module: t.device.createShaderModule({ + code: ` + @group(2) @binding(0) var<uniform> u1: vec4f; + @group(3) @binding(0) var<uniform> u2: vec4f; + @compute @workgroup_size(2) fn main() { _ = u1; _ = u2; } + `, + }), + entryPoint: 'main', + }, + }) + ); + }, + doCommandFn: ({ t, encoder, pipeline, emptyBindGroups, nonEmptyBindGroups }) => { + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(kEmptyBindGroup0Ndx, emptyBindGroups[0]); + pass.setBindGroup(kEmptyBindGroup1Ndx, emptyBindGroups[1]); + pass.setBindGroup(kNonEmptyBindGroup0Ndx, nonEmptyBindGroups[0]); + pass.setBindGroup(kNonEmptyBindGroup1Ndx, nonEmptyBindGroups[1]); + t.doCompute(pass, computeCommand, true); + pass.end(); + }, + }); + }); + +g.test('default_bind_group_layouts_never_match,render_pass') + .desc( + ` + Test that bind groups created with default bind group layouts never match other layouts, including empty bind groups. + + * Test that a pipeline with an explicit layout can not be used with a bindGroup from an auto layout + * Test that a pipeline with an auto layout can not be used with a bindGroup from an explicit layout + * Test that an auto layout from one pipeline can not be used with an auto layout from a different pipeline. + * Test matching bindgroup layouts on the same default layout pipeline are compatible. In other words if + you only define group(2) then group(0)'s empty layout and group(1)'s empty layout should be compatible. + Similarly if group(2) and group(3) have the same types of resources they should be compatible. + ` + ) + .params(u => + u + .combineWithParams(kPipelineTypesAndBindingTypeParams) + .combine('empty', [false, true]) + .combine('renderCommand', [ + 'draw', + 'drawIndexed', + 'drawIndirect', + 'drawIndexedIndirect', + ] as const) + ) + .fn(t => { + const { pipelineType, bindingType, swap, _success: success, renderCommand, empty } = t.params; + + t.runDefaultLayoutBindingTest<GPURenderPipeline>({ + visibility: GPUShaderStage.VERTEX, + empty, + pipelineType, + bindingType, + swap, + success, + makePipelinesFn: (t, explicitPipelineLayout) => { + return (['auto', 'auto', explicitPipelineLayout] as const).map<GPURenderPipeline>( + layout => { + const colorFormat = 'rgba8unorm'; + return t.device.createRenderPipeline({ + layout, + vertex: { + module: t.device.createShaderModule({ + code: ` + @group(2) @binding(0) var<uniform> u1: vec4f; + @group(3) @binding(0) var<uniform> u2: vec4f; + @vertex fn main() -> @builtin(position) vec4f { return u1 + u2; } + `, + }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ + code: `@fragment fn main() {}`, + }), + entryPoint: 'main', + targets: [{ format: colorFormat, writeMask: 0 }], + }, + }); + } + ); + }, + doCommandFn: ({ t, encoder, pipeline, emptyBindGroups, nonEmptyBindGroups }) => { + const attachmentTexture = t.device.createTexture({ + format: 'rgba8unorm', + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + t.trackForCleanup(attachmentTexture); + + const renderPass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: attachmentTexture.createView(), + clearValue: [0, 0, 0, 0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + + renderPass.setPipeline(pipeline); + renderPass.setBindGroup(kEmptyBindGroup0Ndx, emptyBindGroups[0]); + renderPass.setBindGroup(kEmptyBindGroup1Ndx, emptyBindGroups[1]); + renderPass.setBindGroup(kNonEmptyBindGroup0Ndx, nonEmptyBindGroups[0]); + renderPass.setBindGroup(kNonEmptyBindGroup1Ndx, nonEmptyBindGroups[1]); + t.doRender(renderPass, renderCommand, true); + renderPass.end(); + }, + }); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts index 0ed2352bfd..8c9c4bb551 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts @@ -68,14 +68,16 @@ Tests that begin occlusion query with query index: encoder.validateFinish(t.params.queryIndex < 2); }); -g.test('timestamp_query,query_type_and_index') +g.test('writeTimestamp,query_type_and_index') .desc( ` Tests that write timestamp to all types of query set on all possible encoders: - type {occlusion, timestamp} - queryIndex {in, out of} range for GPUQuerySet - x= {non-pass} encoder - ` + +TODO: writeTimestamp is removed from the spec so it's skipped if it TypeErrors. +` ) .params(u => u @@ -101,16 +103,23 @@ Tests that write timestamp to all types of query set on all possible encoders: const querySet = createQuerySetWithType(t, type, count); const encoder = t.createEncoder('non-pass'); - encoder.encoder.writeTimestamp(querySet, queryIndex); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (encoder.encoder as any).writeTimestamp(querySet, queryIndex); + } catch (ex) { + t.skipIf(ex instanceof TypeError, 'writeTimestamp is actually not available'); + } encoder.validateFinish(type === 'timestamp' && queryIndex < count); }); -g.test('timestamp_query,invalid_query_set') +g.test('writeTimestamp,invalid_query_set') .desc( ` Tests that write timestamp to a invalid query set that failed during creation: - x= {non-pass} encoder - ` + +TODO: writeTimestamp is removed from the spec so it's skipped if it TypeErrors. +` ) .paramsSubcasesOnly(u => u.combine('querySetState', ['valid', 'invalid'] as const)) .beforeAllSubcases(t => { @@ -125,12 +134,22 @@ Tests that write timestamp to a invalid query set that failed during creation: }); const encoder = t.createEncoder('non-pass'); - encoder.encoder.writeTimestamp(querySet, 0); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (encoder.encoder as any).writeTimestamp(querySet, 0); + } catch (ex) { + t.skipIf(ex instanceof TypeError, 'writeTimestamp is actually not available'); + } encoder.validateFinish(querySetState !== 'invalid'); }); -g.test('timestamp_query,device_mismatch') - .desc('Tests writeTimestamp cannot be called with a query set created from another device') +g.test('writeTimestamp,device_mismatch') + .desc( + `Tests writeTimestamp cannot be called with a query set created from another device + + TODO: writeTimestamp is removed from the spec so it's skipped if it TypeErrors. + ` + ) .paramsSubcasesOnly(u => u.combine('mismatched', [true, false])) .beforeAllSubcases(t => { t.selectDeviceForQueryTypeOrSkipTestCase('timestamp'); @@ -147,6 +166,11 @@ g.test('timestamp_query,device_mismatch') t.trackForCleanup(querySet); const encoder = t.createEncoder('non-pass'); - encoder.encoder.writeTimestamp(querySet, 0); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (encoder.encoder as any).writeTimestamp(querySet, 0); + } catch (ex) { + t.skipIf(ex instanceof TypeError, 'writeTimestamp is actually not available'); + } encoder.validateFinish(!mismatched); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts index 883b634446..35bdc99583 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts @@ -3,7 +3,7 @@ Tests execution of render bundles. `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; -import { kDepthStencilFormats, kTextureFormatInfo } from '../../../format_info.js'; +import { kDepthStencilFormats } from '../../../format_info.js'; import { ValidationTest } from '../validation_test.js'; export const g = makeTestGroup(ValidationTest); @@ -170,18 +170,6 @@ g.test('depth_stencil_readonly_mismatch') .combine('bundleStencilReadOnly', [false, true]) .combine('passDepthReadOnly', [false, true]) .combine('passStencilReadOnly', [false, true]) - .filter(p => { - // For combined depth/stencil formats the depth and stencil read only state must match - // in order to create a valid render bundle or render pass. - const depthStencilInfo = kTextureFormatInfo[p.depthStencilFormat]; - if (depthStencilInfo.depth && depthStencilInfo.stencil) { - return ( - p.passDepthReadOnly === p.passStencilReadOnly && - p.bundleDepthReadOnly === p.bundleStencilReadOnly - ); - } - return true; - }) ) .beforeAllSubcases(t => { t.selectDeviceForTextureFormatOrSkipTestCase(t.params.depthStencilFormat); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts index cb5581fed6..8a8369dc3b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts @@ -26,7 +26,17 @@ class ErrorScopeTests extends Fixture { const gpu = getGPU(this.rec); const adapter = await gpu.requestAdapter(); assert(adapter !== null); - const device = await adapter.requestDevice(); + + // We need to max out the adapter limits related to texture dimensions to more reliably cause an + // OOM error when asked for it, so set that on the device now. + const device = this.trackForCleanup( + await adapter.requestDevice({ + requiredLimits: { + maxTextureDimension2D: adapter.limits.maxTextureDimension2D, + maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers, + }, + }) + ); assert(device !== null); this._device = device; } @@ -146,7 +156,7 @@ Tests that popping an empty error scope stack should reject. ) .fn(t => { const promise = t.device.popErrorScope(); - t.shouldReject('OperationError', promise); + t.shouldReject('OperationError', promise, { allowMissingStack: true }); }); g.test('parent_scope') @@ -250,7 +260,7 @@ Tests that sibling error scopes need to be balanced. { // Trying to pop an additional non-existing scope should reject. const promise = t.device.popErrorScope(); - t.shouldReject('OperationError', promise); + t.shouldReject('OperationError', promise, { allowMissingStack: true }); } const errors = await Promise.all(promises); @@ -286,6 +296,6 @@ Tests that nested error scopes need to be balanced. { // Trying to pop an additional non-existing scope should reject. const promise = t.device.popErrorScope(); - t.shouldReject('OperationError', promise); + t.shouldReject('OperationError', promise, { allowMissingStack: true }); } }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/gpu_external_texture_expiration.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/gpu_external_texture_expiration.spec.ts index 7d77329920..20ea4897e6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/gpu_external_texture_expiration.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/gpu_external_texture_expiration.spec.ts @@ -86,10 +86,7 @@ g.test('import_multiple_times_in_same_task_scope') sourceType === 'VideoFrame' ? await getVideoFrameFromVideoElement(t, videoElement) : videoElement; - externalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }); + externalTexture = t.device.importExternalTexture({ source }); bindGroup = t.device.createBindGroup({ layout: t.getDefaultBindGroupLayout(), @@ -99,10 +96,7 @@ g.test('import_multiple_times_in_same_task_scope') t.submitCommandBuffer(bindGroup, true); // Import again in the same task scope should return same object. - const mayBeTheSameExternalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }); + const mayBeTheSameExternalTexture = t.device.importExternalTexture({ source }); if (externalTexture === mayBeTheSameExternalTexture) { t.submitCommandBuffer(bindGroup, true); @@ -142,10 +136,7 @@ g.test('import_and_use_in_different_microtask') // Import GPUExternalTexture queueMicrotask(() => { - externalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }); + externalTexture = t.device.importExternalTexture({ source }); }); // Submit GPUExternalTexture @@ -182,10 +173,7 @@ g.test('import_and_use_in_different_task') sourceType === 'VideoFrame' ? await getVideoFrameFromVideoElement(t, videoElement) : videoElement; - externalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }); + externalTexture = t.device.importExternalTexture({ source }); bindGroup = t.device.createBindGroup({ layout: t.getDefaultBindGroupLayout(), @@ -218,10 +206,7 @@ g.test('use_import_to_refresh') let source: HTMLVideoElement | VideoFrame; await startPlayingAndWaitForVideo(videoElement, () => { source = videoElement; - externalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }); + externalTexture = t.device.importExternalTexture({ source }); bindGroup = t.device.createBindGroup({ layout: t.getDefaultBindGroupLayout(), @@ -232,10 +217,7 @@ g.test('use_import_to_refresh') }); await waitForNextTask(() => { - const mayBeTheSameExternalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }); + const mayBeTheSameExternalTexture = t.device.importExternalTexture({ source }); if (externalTexture === mayBeTheSameExternalTexture) { // ImportExternalTexture should refresh expired GPUExternalTexture. @@ -264,10 +246,7 @@ g.test('webcodec_video_frame_close_expire_immediately') let externalTexture: GPUExternalTexture; await startPlayingAndWaitForVideo(videoElement, async () => { const source = await getVideoFrameFromVideoElement(t, videoElement); - externalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }); + externalTexture = t.device.importExternalTexture({ source }); bindGroup = t.device.createBindGroup({ layout: t.getDefaultBindGroupLayout(), diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts index 686a5ee1cf..0290046675 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts @@ -255,9 +255,9 @@ export function formatCopyableWithMethod({ format, method }: WithFormatAndMethod return supportedAspects.length > 0; } if (method === 'CopyT2B') { - return info.copySrc; + return info.color.copySrc; } else { - return info.copyDst; + return info.color.copyDst; } } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts index a0fe38e8e3..cbc36b1431 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts @@ -440,7 +440,7 @@ Test that the copy size must be aligned to the texture's format's block size. const texture = t.createAlignedTexture(format, size, origin, dimension); const bytesPerRow = align( - Math.max(1, Math.ceil(size.width / info.blockWidth)) * info.bytesPerBlock, + Math.max(1, Math.ceil(size.width / info.blockWidth)) * info.color.bytes, 256 ); const rowsPerImage = Math.ceil(size.height / info.blockHeight); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts index 986fc42296..2b5e609c55 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts @@ -1,14 +1,293 @@ export const description = ` TODO: - interface matching between pipeline layout and shader - - x= {compute, vertex, fragment, vertex+fragment}, visibilities - x= bind group index values, binding index values, multiple bindings - - x= types of bindings - - x= {equal, superset, subset} + - x= {superset, subset} `; import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { + kShaderStageCombinations, + kShaderStages, + ValidBindableResource, +} from '../../capability_info.js'; +import { GPUConst } from '../../constants.js'; import { ValidationTest } from './validation_test.js'; -export const g = makeTestGroup(ValidationTest); +type BindableResourceType = ValidBindableResource | 'readonlyStorageBuf'; +const kBindableResources = [ + 'uniformBuf', + 'storageBuf', + 'readonlyStorageBuf', + 'filtSamp', + 'nonFiltSamp', + 'compareSamp', + 'sampledTex', + 'sampledTexMS', + 'readonlyStorageTex', + 'writeonlyStorageTex', + 'readwriteStorageTex', +] as const; + +const bindGroupLayoutEntryContents = { + compareSamp: { + sampler: { + type: 'comparison', + }, + }, + filtSamp: { + sampler: { + type: 'filtering', + }, + }, + nonFiltSamp: { + sampler: { + type: 'non-filtering', + }, + }, + sampledTex: { + texture: { + sampleType: 'unfilterable-float', + }, + }, + sampledTexMS: { + texture: { + sampleType: 'unfilterable-float', + multisampled: true, + }, + }, + storageBuf: { + buffer: { + type: 'storage', + }, + }, + readonlyStorageBuf: { + buffer: { + type: 'read-only-storage', + }, + }, + uniformBuf: { + buffer: { + type: 'uniform', + }, + }, + readonlyStorageTex: { + storageTexture: { + format: 'r32float', + access: 'read-only', + }, + }, + writeonlyStorageTex: { + storageTexture: { + format: 'r32float', + access: 'write-only', + }, + }, + readwriteStorageTex: { + storageTexture: { + format: 'r32float', + access: 'read-write', + }, + }, +} as const; + +class F extends ValidationTest { + createPipelineLayout( + bindingInPipelineLayout: BindableResourceType, + visibility: number + ): GPUPipelineLayout { + return this.device.createPipelineLayout({ + bindGroupLayouts: [ + this.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility, + ...bindGroupLayoutEntryContents[bindingInPipelineLayout], + }, + ], + }), + ], + }); + } + + GetBindableResourceShaderDeclaration(bindableResource: BindableResourceType): string { + switch (bindableResource) { + case 'compareSamp': + return 'var tmp : sampler_comparison'; + case 'filtSamp': + case 'nonFiltSamp': + return 'var tmp : sampler'; + case 'sampledTex': + return 'var tmp : texture_2d<f32>'; + case 'sampledTexMS': + return 'var tmp : texture_multisampled_2d<f32>'; + case 'storageBuf': + return 'var<storage, read_write> tmp : vec4u'; + case 'readonlyStorageBuf': + return 'var<storage, read> tmp : vec4u'; + case 'uniformBuf': + return 'var<uniform> tmp : vec4u;'; + case 'readonlyStorageTex': + return 'var tmp : texture_storage_2d<r32float, read>'; + case 'writeonlyStorageTex': + return 'var tmp : texture_storage_2d<r32float, write>'; + case 'readwriteStorageTex': + return 'var tmp : texture_storage_2d<r32float, read_write>'; + } + } +} + +const BindingResourceCompatibleWithShaderStages = function ( + bindingResource: BindableResourceType, + shaderStages: number +): boolean { + if ((shaderStages & GPUConst.ShaderStage.VERTEX) > 0) { + switch (bindingResource) { + case 'writeonlyStorageTex': + case 'readwriteStorageTex': + case 'storageBuf': + return false; + default: + break; + } + } + return true; +}; + +export const g = makeTestGroup(F); + +g.test('pipeline_layout_shader_exact_match') + .desc( + ` + Test that the binding type in the pipeline layout must match the related declaration in shader. + Note that read-write storage textures in the pipeline layout can match write-only storage textures + in the shader. + ` + ) + .params(u => + u + .combine('bindingInPipelineLayout', kBindableResources) + .combine('bindingInShader', kBindableResources) + .beginSubcases() + .combine('pipelineLayoutVisibility', kShaderStageCombinations) + .combine('shaderStageWithBinding', kShaderStages) + .combine('isBindingStaticallyUsed', [true, false] as const) + .unless( + p => + // We don't test using non-filtering sampler in shader because it has the same declaration + // as filtering sampler. + p.bindingInShader === 'nonFiltSamp' || + !BindingResourceCompatibleWithShaderStages( + p.bindingInPipelineLayout, + p.pipelineLayoutVisibility + ) || + !BindingResourceCompatibleWithShaderStages(p.bindingInShader, p.shaderStageWithBinding) + ) + ) + .fn(t => { + const { + bindingInPipelineLayout, + bindingInShader, + pipelineLayoutVisibility, + shaderStageWithBinding, + isBindingStaticallyUsed, + } = t.params; + + const layout = t.createPipelineLayout(bindingInPipelineLayout, pipelineLayoutVisibility); + const bindResourceDeclaration = `@group(0) @binding(0) ${t.GetBindableResourceShaderDeclaration( + bindingInShader + )}`; + const staticallyUseBinding = isBindingStaticallyUsed ? '_ = tmp; ' : ''; + const isAsync = false; + + let success = true; + if (isBindingStaticallyUsed) { + success = bindingInPipelineLayout === bindingInShader; + + // Filtering and non-filtering both have the same shader declaration. + success ||= bindingInPipelineLayout === 'nonFiltSamp' && bindingInShader === 'filtSamp'; + + // Promoting storage textures that are read-write in the layout can be readonly in the shader. + success ||= + bindingInPipelineLayout === 'readwriteStorageTex' && + bindingInShader === 'writeonlyStorageTex'; + + // The shader using the resource must be included in the visibility in the layout. + success &&= (pipelineLayoutVisibility & shaderStageWithBinding) > 0; + } + + switch (shaderStageWithBinding) { + case GPUConst.ShaderStage.COMPUTE: { + const computeShader = ` + ${bindResourceDeclaration}; + @compute @workgroup_size(1) + fn main() { + ${staticallyUseBinding} + } + `; + t.doCreateComputePipelineTest(isAsync, success, { + layout, + compute: { + module: t.device.createShaderModule({ + code: computeShader, + }), + }, + }); + break; + } + case GPUConst.ShaderStage.VERTEX: { + const vertexShader = ` + ${bindResourceDeclaration}; + @vertex + fn main() -> @builtin(position) vec4f { + ${staticallyUseBinding} + return vec4f(); + } + `; + t.doCreateRenderPipelineTest(isAsync, success, { + layout, + vertex: { + module: t.device.createShaderModule({ + code: vertexShader, + }), + }, + }); + break; + } + case GPUConst.ShaderStage.FRAGMENT: { + const fragmentShader = ` + ${bindResourceDeclaration}; + @fragment + fn main() -> @location(0) vec4f { + ${staticallyUseBinding} + return vec4f(); + } + `; + t.doCreateRenderPipelineTest(isAsync, success, { + layout, + vertex: { + module: t.device.createShaderModule({ + code: ` + @vertex + fn main() -> @builtin(position) vec4f { + return vec4f(); + }`, + }), + }, + fragment: { + module: t.device.createShaderModule({ + code: fragmentShader, + }), + targets: [ + { + format: 'rgba8unorm', + }, + ], + }, + }); + break; + } + } + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts index 4ac240d66e..34d7ef1c83 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts @@ -14,7 +14,7 @@ import { raceWithRejectOnTimeout, unreachable, assert } from '../../../../../com import { kTextureUsages } from '../../../../capability_info.js'; import { kTextureFormatInfo, - kTextureFormats, + kAllTextureFormats, kValidTextureFormatsForCopyE2T, } from '../../../../format_info.js'; import { kResourceStates } from '../../../../gpu_test.js'; @@ -669,7 +669,7 @@ g.test('destination_texture,format') ) .params(u => u - .combine('format', kTextureFormats) + .combine('format', kAllTextureFormats) .beginSubcases() .combine('copySize', [ { width: 0, height: 0, depthOrArrayLayers: 0 }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts index 1d8adab7e8..987a88830d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts @@ -24,11 +24,13 @@ Tests that use a destroyed query set in occlusion query on render pass encoder. encoder.validateFinishAndSubmitGivenState(t.params.querySetState); }); -g.test('writeTimestamp') +g.test('timestamps') .desc( ` -Tests that use a destroyed query set in writeTimestamp on {non-pass, compute, render} encoder. +Tests that use a destroyed query set in timestamp query on {non-pass, compute, render} encoder. - x= {destroyed, not destroyed (control case)} + + TODO: writeTimestamp is removed from the spec so it's skipped if it TypeErrors. ` ) .params(u => u.beginSubcases().combine('querySetState', ['valid', 'destroyed'] as const)) @@ -39,9 +41,50 @@ Tests that use a destroyed query set in writeTimestamp on {non-pass, compute, re count: 2, }); - const encoder = t.createEncoder('non-pass'); - encoder.encoder.writeTimestamp(querySet, 0); - encoder.validateFinishAndSubmitGivenState(t.params.querySetState); + { + const encoder = t.createEncoder('non-pass'); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (encoder.encoder as any).writeTimestamp(querySet, 0); + } catch (ex) { + t.skipIf(ex instanceof TypeError, 'writeTimestamp is actually not available'); + } + encoder.validateFinishAndSubmitGivenState(t.params.querySetState); + } + + { + const encoder = t.createEncoder('non-pass'); + encoder.encoder + .beginComputePass({ + timestampWrites: { querySet, beginningOfPassWriteIndex: 0 }, + }) + .end(); + encoder.validateFinishAndSubmitGivenState(t.params.querySetState); + } + + { + const texture = t.trackForCleanup( + t.device.createTexture({ + size: [1, 1, 1], + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }) + ); + const encoder = t.createEncoder('non-pass'); + encoder.encoder + .beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(), + loadOp: 'load', + storeOp: 'store', + }, + ], + timestampWrites: { querySet, beginningOfPassWriteIndex: 0 }, + }) + .end(); + encoder.validateFinishAndSubmitGivenState(t.params.querySetState); + } }); g.test('resolveQuerySet') diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/texture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/texture.spec.ts index 42036bd881..8e73cbf67b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/texture.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/texture.spec.ts @@ -165,15 +165,16 @@ Tests that using a destroyed texture referenced by a bindGroup set with setBindG u .combine('destroyed', [false, true] as const) .combine('encoderType', ['compute pass', 'render pass', 'render bundle'] as const) + .combine('bindingType', ['texture', 'storageTexture'] as const) ) .fn(t => { - const { destroyed, encoderType } = t.params; + const { destroyed, encoderType, bindingType } = t.params; const { device } = t; const texture = t.trackForCleanup( t.device.createTexture({ size: [1, 1, 1], format: 'rgba8unorm', - usage: GPUTextureUsage.TEXTURE_BINDING, + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, }) ); @@ -182,7 +183,9 @@ Tests that using a destroyed texture referenced by a bindGroup set with setBindG { binding: 0, visibility: GPUShaderStage.COMPUTE, - texture: {}, + [bindingType]: { + format: texture.format, + }, }, ], }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts index c0ab23b91c..241d576e39 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts @@ -551,13 +551,6 @@ Test that the depth stencil read only state in render passes or bundles is compa .filter(p => { if (p.format) { const depthStencilInfo = kTextureFormatInfo[p.format]; - // For combined depth/stencil formats the depth and stencil read only state must match - // in order to create a valid render bundle or render pass. - if (depthStencilInfo.depth && depthStencilInfo.stencil) { - if (p.depthReadOnly !== p.stencilReadOnly) { - return false; - } - } // If the format has no depth aspect, the depthReadOnly, depthWriteEnabled of the pipeline must not be true // in order to create a valid render pipeline. if (!depthStencilInfo.depth && p.depthWriteEnabled) { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts index 9713beea52..0b471c5f6d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts @@ -20,6 +20,7 @@ class F extends ValidationTest { createTexture( options: { format?: GPUTextureFormat; + dimension?: GPUTextureDimension; width?: number; height?: number; arrayLayerCount?: number; @@ -30,6 +31,7 @@ class F extends ValidationTest { ): GPUTexture { const { format = 'rgba8unorm', + dimension = '2d', width = 16, height = 16, arrayLayerCount = 1, @@ -41,6 +43,7 @@ class F extends ValidationTest { return this.device.createTexture({ size: { width, height, depthOrArrayLayers: arrayLayerCount }, format, + dimension, mipLevelCount, sampleCount, usage, @@ -90,6 +93,7 @@ class F extends ValidationTest { } export const g = makeTestGroup(F); +const kArrayLayerCount = 10; g.test('attachments,one_color_attachment') .desc(`Test that a render pass works with only one color attachment.`) @@ -278,6 +282,184 @@ g.test('color_attachments,limits,maxColorAttachmentBytesPerSample,unaligned') t.tryRenderPass(success, { colorAttachments }); }); +g.test('color_attachments,depthSlice,definedness') + .desc( + ` + Test that depthSlice must be undefined for 2d color attachments and defined for 3d color attachments." + - The special value '0xFFFFFFFF' is not treated as 'undefined'. + ` + ) + .params(u => + u + .combine('dimension', ['2d', '3d'] as GPUTextureDimension[]) + .beginSubcases() + .combine('depthSlice', [undefined, 0, 0xffffffff]) + ) + .fn(t => { + const { dimension, depthSlice } = t.params; + const texture = t.createTexture({ dimension }); + + const colorAttachment = t.getColorAttachment(texture); + if (depthSlice !== undefined) { + colorAttachment.depthSlice = depthSlice; + } + + const descriptor: GPURenderPassDescriptor = { + colorAttachments: [colorAttachment], + }; + + const success = + (dimension === '2d' && depthSlice === undefined) || (dimension === '3d' && depthSlice === 0); + + t.tryRenderPass(success, descriptor); + }); + +g.test('color_attachments,depthSlice,bound_check') + .desc( + ` + Test that depthSlice must be less than the depthOrArrayLayers of 3d texture's subresource at mip levels. + - Check depth bounds with 3d texture size [16, 1, 10], which has 5 mip levels with depth [10, 5, 2, 1, 1] + for testing more mip level size computation. + - Failed if depthSlice >= the depth of each mip level. + ` + ) + .params(u => + u + .combine('mipLevel', [0, 1, 2, 3, 4]) + .beginSubcases() + .expand('depthSlice', ({ mipLevel }) => { + const depthAtMipLevel = Math.max(kArrayLayerCount >> mipLevel, 1); + // Use Set() to exclude duplicates when the depthAtMipLevel is 1 and 2 + return [...new Set([0, 1, depthAtMipLevel - 1, depthAtMipLevel])]; + }) + ) + .fn(t => { + const { mipLevel, depthSlice } = t.params; + + const texture = t.createTexture({ + dimension: '3d', + width: 16, + height: 1, + arrayLayerCount: kArrayLayerCount, + mipLevelCount: mipLevel + 1, + }); + + const viewDescriptor: GPUTextureViewDescriptor = { + baseMipLevel: mipLevel, + mipLevelCount: 1, + baseArrayLayer: 0, + arrayLayerCount: 1, + }; + + const colorAttachment = t.getColorAttachment(texture, viewDescriptor); + colorAttachment.depthSlice = depthSlice; + + const passDescriptor: GPURenderPassDescriptor = { + colorAttachments: [colorAttachment], + }; + + const success = depthSlice < Math.max(kArrayLayerCount >> mipLevel, 1); + + t.tryRenderPass(success, passDescriptor); + }); + +g.test('color_attachments,depthSlice,overlaps,same_miplevel') + .desc( + ` + Test that the depth slices of 3d color attachments have no overlaps for same texture in a render + pass. + - Succeed if the depth slices are different, or from different textures, or on different render + passes. + - Fail if same depth slice from same texture's same mip level is overwritten in a render pass. + ` + ) + .params(u => + u + .combine('sameDepthSlice', [true, false]) + .beginSubcases() + .combine('sameTexture', [true, false]) + .combine('samePass', [true, false]) + ) + .fn(t => { + const { sameDepthSlice, sameTexture, samePass } = t.params; + const arrayLayerCount = 4; + + const texDescriptor = { + dimension: '3d' as GPUTextureDimension, + arrayLayerCount, + }; + const texture = t.createTexture(texDescriptor); + + const colorAttachments = []; + for (let i = 0; i < arrayLayerCount; i++) { + const colorAttachment = t.getColorAttachment( + sameTexture ? texture : t.createTexture(texDescriptor) + ); + colorAttachment.depthSlice = sameDepthSlice ? 0 : i; + colorAttachments.push(colorAttachment); + } + + const encoder = t.createEncoder('non-pass'); + if (samePass) { + const pass = encoder.encoder.beginRenderPass({ colorAttachments }); + pass.end(); + } else { + for (let i = 0; i < arrayLayerCount; i++) { + const pass = encoder.encoder.beginRenderPass({ colorAttachments: [colorAttachments[i]] }); + pass.end(); + } + } + + const success = !sameDepthSlice || !sameTexture || !samePass; + + encoder.validateFinish(success); + }); + +g.test('color_attachments,depthSlice,overlaps,diff_miplevel') + .desc( + ` + Test that the same depth slice from different mip levels of a 3d texture with size [1, 1, N] can + be set in a render pass's color attachments. + ` + ) + .params(u => u.combine('sameMipLevel', [true, false])) + .fn(t => { + const { sameMipLevel } = t.params; + const mipLevelCount = 4; + + const texDescriptor = { + dimension: '3d' as GPUTextureDimension, + width: 1, + height: 1, + arrayLayerCount: 1 << mipLevelCount, + mipLevelCount, + }; + const texture = t.createTexture(texDescriptor); + + const viewDescriptor: GPUTextureViewDescriptor = { + baseMipLevel: 0, + mipLevelCount: 1, + baseArrayLayer: 0, + arrayLayerCount: 1, + }; + + const colorAttachments = []; + for (let i = 0; i < mipLevelCount; i++) { + if (!sameMipLevel) { + viewDescriptor.baseMipLevel = i; + } + const colorAttachment = t.getColorAttachment(texture, viewDescriptor); + colorAttachment.depthSlice = 0; + colorAttachments.push(colorAttachment); + } + + const encoder = t.createEncoder('non-pass'); + const pass = encoder.encoder.beginRenderPass({ colorAttachments }); + pass.end(); + + encoder.validateFinish(!sameMipLevel); + }); + g.test('attachments,same_size') .desc( ` @@ -909,10 +1091,8 @@ g.test('depth_stencil_attachment,loadOp_storeOp_match_depthReadOnly_stencilReadO const hasDepth = info.depth; const hasStencil = info.stencil; - const goodAspectCombo = - (hasDepth && hasStencil ? !depthReadOnly === !stencilReadOnly : true) && - (hasDepthSettings ? hasDepth : true) && - (hasStencilSettings ? hasStencil : true); + const goodAspectSettingsPresent = + (hasDepthSettings ? hasDepth : true) && (hasStencilSettings ? hasStencil : true); const hasBothDepthOps = !!depthLoadOp && !!depthStoreOp; const hasBothStencilOps = !!stencilLoadOp && !!stencilStoreOp; @@ -923,7 +1103,7 @@ g.test('depth_stencil_attachment,loadOp_storeOp_match_depthReadOnly_stencilReadO const goodStencilCombo = hasStencil && !stencilReadOnly ? hasBothStencilOps : hasNeitherStencilOps; - const shouldError = !goodAspectCombo || !goodDepthCombo || !goodStencilCombo; + const shouldError = !goodAspectSettingsPresent || !goodDepthCombo || !goodStencilCombo; t.expectValidationError(() => { encoder.finish(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts index 93b0932042..e0316a5517 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts @@ -1,4 +1,4 @@ -import { kTextureFormatInfo } from '../../../format_info.js'; +import { ColorTextureFormat, kTextureFormatInfo } from '../../../format_info.js'; import { getFragmentShaderCodeWithOutput, getPlainTypeInfo, @@ -6,12 +6,14 @@ import { } from '../../../util/shader.js'; import { ValidationTest } from '../validation_test.js'; +type ColorTargetState = GPUColorTargetState & { format: ColorTextureFormat }; + const values = [0, 1, 0, 1]; export class CreateRenderPipelineValidationTest extends ValidationTest { getDescriptor( options: { primitive?: GPUPrimitiveState; - targets?: GPUColorTargetState[]; + targets?: ColorTargetState[]; multisample?: GPUMultisampleState; depthStencil?: GPUDepthStencilState; fragmentShaderCode?: string; @@ -19,17 +21,16 @@ export class CreateRenderPipelineValidationTest extends ValidationTest { fragmentConstants?: Record<string, GPUPipelineConstantValue>; } = {} ): GPURenderPipelineDescriptor { - const defaultTargets: GPUColorTargetState[] = [{ format: 'rgba8unorm' }]; const { primitive = {}, - targets = defaultTargets, + targets = [{ format: 'rgba8unorm' }] as const, multisample = {}, depthStencil, fragmentShaderCode = getFragmentShaderCodeWithOutput([ { values, plainType: getPlainTypeInfo( - kTextureFormatInfo[targets[0] ? targets[0].format : 'rgba8unorm'].sampleType + kTextureFormatInfo[targets[0] ? targets[0].format : 'rgba8unorm'].color.type ), componentCount: 4, }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts index eaaf78af66..403f463943 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts @@ -5,7 +5,11 @@ This test dedicatedly tests validation of GPUDepthStencilState of createRenderPi import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { unreachable } from '../../../../common/util/util.js'; import { kCompareFunctions, kStencilOperations } from '../../../capability_info.js'; -import { kTextureFormats, kTextureFormatInfo, kDepthStencilFormats } from '../../../format_info.js'; +import { + kAllTextureFormats, + kTextureFormatInfo, + kDepthStencilFormats, +} from '../../../format_info.js'; import { getFragmentShaderCodeWithOutput } from '../../../util/shader.js'; import { CreateRenderPipelineValidationTest } from './common.js'; @@ -14,7 +18,11 @@ export const g = makeTestGroup(CreateRenderPipelineValidationTest); g.test('format') .desc(`The texture format in depthStencilState must be a depth/stencil format.`) - .params(u => u.combine('isAsync', [false, true]).combine('format', kTextureFormats)) + .params(u => + u // + .combine('isAsync', [false, true]) + .combine('format', kAllTextureFormats) + ) .beforeAllSubcases(t => { const { format } = t.params; const info = kTextureFormatInfo[format]; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts index 0206431eee..c01c2ba9ef 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts @@ -10,15 +10,17 @@ import { kMaxColorAttachmentsToTest, } from '../../../capability_info.js'; import { - kTextureFormats, + kAllTextureFormats, kRenderableColorTextureFormats, kTextureFormatInfo, computeBytesPerSampleFromFormats, + kColorTextureFormats, } from '../../../format_info.js'; import { getFragmentShaderCodeWithOutput, getPlainTypeInfo, kDefaultFragmentShaderCode, + kDefaultVertexShaderCode, } from '../../../util/shader.js'; import { kTexelRepresentationInfo } from '../../../util/texture/texel_data.js'; @@ -49,9 +51,60 @@ g.test('color_target_exists') t.doCreateRenderPipelineTest(isAsync, false, badDescriptor); }); +g.test('targets_format_is_color_format') + .desc( + `Tests that color target state format must be a color format, regardless of how the + fragment shader writes to it.` + ) + .params(u => + u + // Test all non-color texture formats, plus 'rgba8unorm' as a control case. + .combine('format', kAllTextureFormats) + .filter(({ format }) => { + return format === 'rgba8unorm' || !kTextureFormatInfo[format].color; + }) + .combine('isAsync', [false, true]) + .beginSubcases() + .combine('fragOutType', ['f32', 'u32', 'i32'] as const) + ) + .beforeAllSubcases(t => { + const { format } = t.params; + const info = kTextureFormatInfo[format]; + t.skipIfTextureFormatNotSupported(t.params.format); + t.selectDeviceOrSkipTestCase(info.feature); + }) + .fn(t => { + const { isAsync, format, fragOutType } = t.params; + + const fragmentShaderCode = getFragmentShaderCodeWithOutput([ + { values, plainType: fragOutType, componentCount: 4 }, + ]); + + const success = format === 'rgba8unorm' && fragOutType === 'f32'; + t.doCreateRenderPipelineTest(isAsync, success, { + vertex: { + module: t.device.createShaderModule({ code: kDefaultVertexShaderCode }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ code: fragmentShaderCode }), + entryPoint: 'main', + targets: [{ format }], + }, + layout: 'auto', + }); + }); + g.test('targets_format_renderable') - .desc(`Tests that color target state format must have RENDER_ATTACHMENT capability.`) - .params(u => u.combine('isAsync', [false, true]).combine('format', kTextureFormats)) + .desc( + `Tests that color target state format must have RENDER_ATTACHMENT capability + (tests only color formats).` + ) + .params(u => + u // + .combine('isAsync', [false, true]) + .combine('format', kColorTextureFormats) + ) .beforeAllSubcases(t => { const { format } = t.params; const info = kTextureFormatInfo[format]; @@ -158,24 +211,12 @@ g.test('limits,maxColorAttachmentBytesPerSample,unaligned') // become 4 and 4+4+8+16+1 > 32. Re-ordering this so the R8Unorm's are at the end, however // is allowed: 4+8+16+1+1 < 32. { - formats: [ - 'r8unorm', - 'r32float', - 'rgba8unorm', - 'rgba32float', - 'r8unorm', - ] as GPUTextureFormat[], + formats: ['r8unorm', 'r32float', 'rgba8unorm', 'rgba32float', 'r8unorm'], }, { - formats: [ - 'r32float', - 'rgba8unorm', - 'rgba32float', - 'r8unorm', - 'r8unorm', - ] as GPUTextureFormat[], + formats: ['r32float', 'rgba8unorm', 'rgba32float', 'r8unorm', 'r8unorm'], }, - ]) + ] as const) .beginSubcases() .combine('isAsync', [false, true]) ) diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts index 91aabb0ab8..db65ba8bf4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts @@ -3,7 +3,7 @@ Interface matching between vertex and fragment shader validation for createRende `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; -import { assert, range } from '../../../../common/util/util.js'; +import { range } from '../../../../common/util/util.js'; import { CreateRenderPipelineValidationTest } from './common.js'; @@ -97,8 +97,18 @@ g.test('location,mismatch') }); g.test('location,superset') - .desc(`TODO: implement after spec is settled: https://github.com/gpuweb/gpuweb/issues/2038`) - .unimplemented(); + .desc(`Tests that validation should succeed when vertex output is superset of fragment input`) + .params(u => u.combine('isAsync', [false, true])) + .fn(t => { + const { isAsync } = t.params; + + const descriptor = t.getDescriptorWithStates( + t.getVertexStateWithOutputs(['@location(0) vout0: f32', '@location(1) vout1: f32']), + t.getFragmentStateWithInputs(['@location(1) fin1: f32']) + ); + + t.doCreateRenderPipelineTest(isAsync, true, descriptor); + }); g.test('location,subset') .desc(`Tests that validation should fail when vertex output is a subset of fragment input.`) @@ -159,20 +169,27 @@ g.test('interpolation_type') { output: '@interpolate(linear)', input: '@interpolate(perspective)' }, { output: '@interpolate(flat)', input: '@interpolate(perspective)' }, { output: '@interpolate(linear)', input: '@interpolate(flat)' }, - { output: '@interpolate(linear, center)', input: '@interpolate(linear, center)' }, + { + output: '@interpolate(linear, center)', + input: '@interpolate(linear, center)', + _compat_success: false, + }, ]) ) .fn(t => { - const { isAsync, output, input, _success } = t.params; + const { isAsync, output, input, _success, _compat_success } = t.params; const descriptor = t.getDescriptorWithStates( t.getVertexStateWithOutputs([`@location(0) ${output} vout0: f32`]), t.getFragmentStateWithInputs([`@location(0) ${input} fin0: f32`]) ); - t.doCreateRenderPipelineTest(isAsync, _success ?? output === input, descriptor); - }); + const shouldSucceed = + (_success ?? output === input) && (!t.isCompatibility || _compat_success !== false); + t.doCreateRenderPipelineTest(isAsync, shouldSucceed, descriptor); + }); +1; g.test('interpolation_sampling') .desc( `Tests that validation should fail when interpolation sampling of vertex output and fragment input at the same location doesn't match.` @@ -186,7 +203,12 @@ g.test('interpolation_sampling') input: '@interpolate(perspective, center)', _success: true, }, - { output: '@interpolate(linear, center)', input: '@interpolate(linear)', _success: true }, + { + output: '@interpolate(linear, center)', + input: '@interpolate(linear)', + _success: true, + _compat_success: false, + }, { output: '@interpolate(flat)', input: '@interpolate(flat)' }, { output: '@interpolate(perspective)', input: '@interpolate(perspective, sample)' }, { output: '@interpolate(perspective, center)', input: '@interpolate(perspective, sample)' }, @@ -198,14 +220,17 @@ g.test('interpolation_sampling') ]) ) .fn(t => { - const { isAsync, output, input, _success } = t.params; + const { isAsync, output, input, _success, _compat_success } = t.params; const descriptor = t.getDescriptorWithStates( t.getVertexStateWithOutputs([`@location(0) ${output} vout0: f32`]), t.getFragmentStateWithInputs([`@location(0) ${input} fin0: f32`]) ); - t.doCreateRenderPipelineTest(isAsync, _success ?? output === input, descriptor); + const shouldSucceed = + (_success ?? output === input) && (!t.isCompatibility || _compat_success !== false); + + t.doCreateRenderPipelineTest(isAsync, shouldSucceed, descriptor); }); g.test('max_shader_variable_location') @@ -251,9 +276,6 @@ g.test('max_components_count,output') const numVec4 = Math.floor(numScalarComponents / 4); const numTrailingScalars = numScalarComponents % 4; - const numUserDefinedInterStageVariables = numTrailingScalars > 0 ? numVec4 + 1 : numVec4; - - assert(numUserDefinedInterStageVariables <= t.device.limits.maxInterStageShaderVariables); const outputs = range(numVec4, i => `@location(${i}) vout${i}: vec4<f32>`); const inputs = range(numVec4, i => `@location(${i}) fin${i}: vec4<f32>`); @@ -280,23 +302,23 @@ g.test('max_components_count,input') .params(u => u.combine('isAsync', [false, true]).combineWithParams([ // Number of user-defined input scalar components in test shader = device.limits.maxInterStageShaderComponents + numScalarDelta. - { numScalarDelta: 0, useExtraBuiltinInputs: false, _success: true }, - { numScalarDelta: 1, useExtraBuiltinInputs: false, _success: false }, - { numScalarDelta: 0, useExtraBuiltinInputs: true, _success: false }, - { numScalarDelta: -3, useExtraBuiltinInputs: true, _success: true }, - { numScalarDelta: -2, useExtraBuiltinInputs: true, _success: false }, + { numScalarDelta: 0, useExtraBuiltinInputs: false }, + { numScalarDelta: 1, useExtraBuiltinInputs: false }, + { numScalarDelta: 0, useExtraBuiltinInputs: true }, + { numScalarDelta: -3, useExtraBuiltinInputs: true }, + { numScalarDelta: -2, useExtraBuiltinInputs: true }, ] as const) ) .fn(t => { - const { isAsync, numScalarDelta, useExtraBuiltinInputs, _success } = t.params; + const { isAsync, numScalarDelta, useExtraBuiltinInputs } = t.params; const numScalarComponents = t.device.limits.maxInterStageShaderComponents + numScalarDelta; + const numExtraComponents = useExtraBuiltinInputs ? (t.isCompatibility ? 2 : 3) : 0; + const numUsedComponents = numScalarComponents + numExtraComponents; + const success = numUsedComponents <= t.device.limits.maxInterStageShaderComponents; const numVec4 = Math.floor(numScalarComponents / 4); const numTrailingScalars = numScalarComponents % 4; - const numUserDefinedInterStageVariables = numTrailingScalars > 0 ? numVec4 + 1 : numVec4; - - assert(numUserDefinedInterStageVariables <= t.device.limits.maxInterStageShaderVariables); const outputs = range(numVec4, i => `@location(${i}) vout${i}: vec4<f32>`); const inputs = range(numVec4, i => `@location(${i}) fin${i}: vec4<f32>`); @@ -310,9 +332,11 @@ g.test('max_components_count,input') if (useExtraBuiltinInputs) { inputs.push( '@builtin(front_facing) front_facing_in: bool', - '@builtin(sample_index) sample_index_in: u32', '@builtin(sample_mask) sample_mask_in: u32' ); + if (!t.isCompatibility) { + inputs.push('@builtin(sample_index) sample_index_in: u32'); + } } const descriptor = t.getDescriptorWithStates( @@ -320,5 +344,5 @@ g.test('max_components_count,input') t.getFragmentStateWithInputs(inputs, true) ); - t.doCreateRenderPipelineTest(isAsync, _success, descriptor); + t.doCreateRenderPipelineTest(isAsync, success, descriptor); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts index 1e3ccf5637..adb091a236 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts @@ -96,3 +96,37 @@ g.test('pipeline_layout,device_mismatch') t.doCreateRenderPipelineTest(isAsync, !mismatched, descriptor); }); + +g.test('external_texture') + .desc('Tests createRenderPipeline() with an external_texture') + .fn(t => { + const shader = t.device.createShaderModule({ + code: ` + @vertex + fn vertexMain() -> @builtin(position) vec4f { + return vec4f(1); + } + + @group(0) @binding(0) var myTexture: texture_external; + + @fragment + fn fragmentMain() -> @location(0) vec4f { + let result = textureLoad(myTexture, vec2u(1, 1)); + return vec4f(1); + } + `, + }); + + const descriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module: shader, + }, + fragment: { + module: shader, + targets: [{ format: 'rgba8unorm' }], + }, + }; + + t.doCreateRenderPipelineTest(false, true, descriptor); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/resource_compatibility.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/resource_compatibility.spec.ts new file mode 100644 index 0000000000..5d6bc8d125 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/resource_compatibility.spec.ts @@ -0,0 +1,95 @@ +export const description = ` +Tests for resource compatibilty between pipeline layout and shader modules + `; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { + kAPIResources, + getWGSLShaderForResource, + getAPIBindGroupLayoutForResource, + doResourcesMatch, +} from '../utils.js'; + +import { CreateRenderPipelineValidationTest } from './common.js'; + +export const g = makeTestGroup(CreateRenderPipelineValidationTest); + +g.test('resource_compatibility') + .desc( + 'Tests validation of resource (bind group) compatibility between pipeline layout and WGSL shader' + ) + .params(u => + u // + .combine('stage', ['vertex', 'fragment'] as const) + .combine('apiResource', keysOf(kAPIResources)) + .filter(t => { + const res = kAPIResources[t.apiResource]; + if (t.stage === 'vertex') { + if (res.buffer && res.buffer.type === 'storage') { + return false; + } + if (res.storageTexture && res.storageTexture.access !== 'read-only') { + return false; + } + } + return true; + }) + .beginSubcases() + .combine('isAsync', [true, false] as const) + .combine('wgslResource', keysOf(kAPIResources)) + ) + .fn(t => { + const apiResource = kAPIResources[t.params.apiResource]; + const wgslResource = kAPIResources[t.params.wgslResource]; + t.skipIf( + wgslResource.storageTexture !== undefined && + wgslResource.storageTexture.access !== 'write-only' && + !t.hasLanguageFeature('readonly_and_readwrite_storage_textures'), + 'Storage textures require language feature' + ); + const emptyVS = ` +@vertex +fn main() -> @builtin(position) vec4f { + return vec4f(); +} +`; + const emptyFS = ` +@fragment +fn main() -> @location(0) vec4f { + return vec4f(); +} +`; + + const code = getWGSLShaderForResource(t.params.stage, wgslResource); + const vsCode = t.params.stage === 'vertex' ? code : emptyVS; + const fsCode = t.params.stage === 'fragment' ? code : emptyFS; + const gpuStage: GPUShaderStageFlags = + t.params.stage === 'vertex' ? GPUShaderStage.VERTEX : GPUShaderStage.FRAGMENT; + const layout = t.device.createPipelineLayout({ + bindGroupLayouts: [getAPIBindGroupLayoutForResource(t.device, gpuStage, apiResource)], + }); + + const descriptor = { + layout, + vertex: { + module: t.device.createShaderModule({ + code: vsCode, + }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ + code: fsCode, + }), + entryPoint: 'main', + targets: [{ format: 'rgba8unorm' }] as const, + }, + }; + + t.doCreateRenderPipelineTest( + t.params.isAsync, + doResourcesMatch(apiResource, wgslResource), + descriptor + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts index d316f26c06..5114cbb266 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts @@ -13,11 +13,18 @@ import { } from '../../../../format_info.js'; import { ValidationTest } from '../../validation_test.js'; -type TextureBindingType = 'sampled-texture' | 'multisampled-texture' | 'writeonly-storage-texture'; +type TextureBindingType = + | 'sampled-texture' + | 'multisampled-texture' + | 'writeonly-storage-texture' + | 'readonly-storage-texture' + | 'readwrite-storage-texture'; const kTextureBindingTypes = [ 'sampled-texture', 'multisampled-texture', 'writeonly-storage-texture', + 'readonly-storage-texture', + 'readwrite-storage-texture', ] as const; const SIZE = 32; @@ -39,7 +46,7 @@ class TextureUsageTracking extends ValidationTest { arrayLayerCount = 1, mipLevelCount = 1, sampleCount = 1, - format = 'rgba8unorm', + format = 'r32float', usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, } = options; @@ -75,6 +82,14 @@ class TextureUsageTracking extends ValidationTest { assert(format !== undefined); entry = { storageTexture: { access: 'write-only', format, viewDimension } }; break; + case 'readonly-storage-texture': + assert(format !== undefined); + entry = { storageTexture: { access: 'read-only', format, viewDimension } }; + break; + case 'readwrite-storage-texture': + assert(format !== undefined); + entry = { storageTexture: { access: 'read-write', format, viewDimension } }; + break; } return this.device.createBindGroupLayout({ @@ -107,7 +122,7 @@ class TextureUsageTracking extends ValidationTest { depthStencilFormat?: GPUTextureFormat ) { const bundleEncoder = this.device.createRenderBundleEncoder({ - colorFormats: ['rgba8unorm'], + colorFormats: ['r32float'], depthStencilFormat, }); bundleEncoder.setBindGroup(binding, bindGroup); @@ -129,16 +144,21 @@ class TextureUsageTracking extends ValidationTest { } /** - * Create two bind groups. Resource usages conflict between these two bind groups. But resource - * usage inside each bind group doesn't conflict. + * Create two bind groups with one texture view. */ - makeConflictingBindGroups() { + makeTwoBindGroupsWithOneTextureView(usage1: TextureBindingType, usage2: TextureBindingType) { const view = this.createTexture({ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING, }).createView(); const bindGroupLayouts = [ - this.createBindGroupLayout(0, 'sampled-texture', '2d'), - this.createBindGroupLayout(0, 'writeonly-storage-texture', '2d', { format: 'rgba8unorm' }), + this.createBindGroupLayout(0, usage1, '2d', { + sampleType: 'unfilterable-float', + format: 'r32float', + }), + this.createBindGroupLayout(0, usage2, '2d', { + sampleType: 'unfilterable-float', + format: 'r32float', + }), ]; return { bindGroupLayouts, @@ -155,14 +175,21 @@ class TextureUsageTracking extends ValidationTest { }; } - testValidationScope(compute: boolean): { + testValidationScope( + compute: boolean, + usage1: TextureBindingType, + usage2: TextureBindingType + ): { bindGroup0: GPUBindGroup; bindGroup1: GPUBindGroup; encoder: GPUCommandEncoder; pass: GPURenderPassEncoder | GPUComputePassEncoder; pipeline: GPURenderPipeline | GPUComputePipeline; } { - const { bindGroupLayouts, bindGroups } = this.makeConflictingBindGroups(); + const { bindGroupLayouts, bindGroups } = this.makeTwoBindGroupsWithOneTextureView( + usage1, + usage2 + ); const encoder = this.device.createCommandEncoder(); const pass = compute @@ -175,7 +202,7 @@ class TextureUsageTracking extends ValidationTest { }); const pipeline = compute ? this.createNoOpComputePipeline(pipelineLayout) - : this.createNoOpRenderPipeline(pipelineLayout); + : this.createNoOpRenderPipeline(pipelineLayout, 'r32float'); return { bindGroup0: bindGroups[0], bindGroup1: bindGroups[1], @@ -237,6 +264,8 @@ g.test('subresources_and_binding_types_combination_for_color') [ { _usageOK: true, type0: 'sampled-texture', type1: 'sampled-texture' }, { _usageOK: false, type0: 'sampled-texture', type1: 'writeonly-storage-texture' }, + { _usageOK: true, type0: 'sampled-texture', type1: 'readonly-storage-texture' }, + { _usageOK: false, type0: 'sampled-texture', type1: 'readwrite-storage-texture' }, { _usageOK: false, type0: 'sampled-texture', type1: 'render-target' }, // Race condition upon multiple writable storage texture is valid. // For p.compute === true, fails at pass.dispatch because aliasing exists. @@ -245,7 +274,34 @@ g.test('subresources_and_binding_types_combination_for_color') type0: 'writeonly-storage-texture', type1: 'writeonly-storage-texture', }, + { + _usageOK: true, + type0: 'readonly-storage-texture', + type1: 'readonly-storage-texture', + }, + { + _usageOK: !p.compute, + type0: 'readwrite-storage-texture', + type1: 'readwrite-storage-texture', + }, + { + _usageOK: false, + type0: 'readonly-storage-texture', + type1: 'writeonly-storage-texture', + }, + { + _usageOK: false, + type0: 'readonly-storage-texture', + type1: 'readwrite-storage-texture', + }, + { + _usageOK: false, + type0: 'writeonly-storage-texture', + type1: 'readwrite-storage-texture', + }, + { _usageOK: false, type0: 'readonly-storage-texture', type1: 'render-target' }, { _usageOK: false, type0: 'writeonly-storage-texture', type1: 'render-target' }, + { _usageOK: false, type0: 'readwrite-storage-texture', type1: 'render-target' }, { _usageOK: false, type0: 'render-target', type1: 'render-target' }, ] as const ) @@ -428,6 +484,11 @@ g.test('subresources_and_binding_types_combination_for_color') _resourceSuccess, } = t.params; + t.skipIf( + t.isCompatibility, + 'multiple views of the same texture in a single draw/dispatch are not supported in compat, nor are sub ranges of layers' + ); + const texture = t.createTexture({ arrayLayerCount: TOTAL_LAYERS, mipLevelCount: TOTAL_LEVELS, @@ -497,9 +558,13 @@ g.test('subresources_and_binding_types_combination_for_color') const bgls: GPUBindGroupLayout[] = []; // Create bind groups. Set bind groups in pass directly or set bind groups in bundle. - const storageTextureFormat0 = type0 === 'sampled-texture' ? undefined : 'rgba8unorm'; + const storageTextureFormat0 = type0 === 'sampled-texture' ? undefined : 'r32float'; + const sampleType0 = type0 === 'sampled-texture' ? 'unfilterable-float' : undefined; - const bgl0 = t.createBindGroupLayout(0, type0, dimension0, { format: storageTextureFormat0 }); + const bgl0 = t.createBindGroupLayout(0, type0, dimension0, { + format: storageTextureFormat0, + sampleType: sampleType0, + }); const bindGroup0 = t.device.createBindGroup({ layout: bgl0, entries: [{ binding: 0, resource: view0 }], @@ -513,10 +578,11 @@ g.test('subresources_and_binding_types_combination_for_color') pass.setBindGroup(0, bindGroup0); } if (type1 !== 'render-target') { - const storageTextureFormat1 = type1 === 'sampled-texture' ? undefined : 'rgba8unorm'; - + const storageTextureFormat1 = type1 === 'sampled-texture' ? undefined : 'r32float'; + const sampleType1 = type1 === 'sampled-texture' ? 'unfilterable-float' : undefined; const bgl1 = t.createBindGroupLayout(1, type1, dimension1, { format: storageTextureFormat1, + sampleType: sampleType1, }); const bindGroup1 = t.device.createBindGroup({ layout: bgl1, @@ -653,6 +719,8 @@ g.test('subresources_and_binding_types_combination_for_aspect') _usageSuccess, } = t.params; + t.skipIf(t.isCompatibility, 'sub ranges of layers are not supported in compat mode'); + const texture = t.createTexture({ arrayLayerCount: TOTAL_LAYERS, mipLevelCount: TOTAL_LEVELS, @@ -782,9 +850,21 @@ g.test('shader_stages_and_visibility,storage_write') GPUConst.ShaderStage.COMPUTE, ]) .combine('writeVisibility', [0, GPUConst.ShaderStage.FRAGMENT, GPUConst.ShaderStage.COMPUTE]) + .combine('readEntry', [ + { texture: { sampleType: 'unfilterable-float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + ] as const) + .combine('storageWriteAccess', ['write-only', 'read-write'] as const) ) .fn(t => { - const { compute, readVisibility, writeVisibility, secondUseConflicts } = t.params; + const { + compute, + readEntry, + storageWriteAccess, + readVisibility, + writeVisibility, + secondUseConflicts, + } = t.params; const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING; const view = t.createTexture({ usage }).createView(); @@ -792,11 +872,11 @@ g.test('shader_stages_and_visibility,storage_write') const bgl = t.device.createBindGroupLayout({ entries: [ - { binding: 0, visibility: readVisibility, texture: {} }, + { binding: 0, visibility: readVisibility, ...readEntry }, { binding: 1, visibility: writeVisibility, - storageTexture: { access: 'write-only', format: 'rgba8unorm' }, + storageTexture: { access: storageWriteAccess, format: 'r32float' }, }, ], }); @@ -851,19 +931,23 @@ g.test('shader_stages_and_visibility,attachment_write') GPUConst.ShaderStage.FRAGMENT, GPUConst.ShaderStage.COMPUTE, ]) + .combine('readEntry', [ + { texture: { sampleType: 'unfilterable-float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + ] as const) ) .fn(t => { - const { readVisibility, secondUseConflicts } = t.params; + const { readVisibility, readEntry, secondUseConflicts } = t.params; - // writeonly-storage-texture binding type is not supported in vertex stage. So, this test - // uses writeonly-storage-texture binding as writable binding upon the same subresource if - // vertex stage is not included. Otherwise, it uses output attachment instead. - const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT; + const usage = + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.STORAGE_BINDING; const view = t.createTexture({ usage }).createView(); const view2 = secondUseConflicts ? view : t.createTexture({ usage }).createView(); const bgl = t.device.createBindGroupLayout({ - entries: [{ binding: 0, visibility: readVisibility, texture: {} }], + entries: [{ binding: 0, visibility: readVisibility, ...readEntry }], }); const bindGroup = t.device.createBindGroup({ layout: bgl, @@ -898,8 +982,10 @@ g.test('replaced_binding') .combine('compute', [false, true]) .combine('callDrawOrDispatch', [false, true]) .combine('entry', [ - { texture: {} }, - { storageTexture: { access: 'write-only', format: 'rgba8unorm' } }, + { texture: { sampleType: 'unfilterable-float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + { storageTexture: { access: 'write-only', format: 'r32float' } }, + { storageTexture: { access: 'read-write', format: 'r32float' } }, ] as const) ) .fn(t => { @@ -912,7 +998,11 @@ g.test('replaced_binding') // Create bindGroup0. It has two bindings. These two bindings use different views/subresources. const bglEntries0: GPUBindGroupLayoutEntry[] = [ - { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} }, + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: { sampleType: 'unfilterable-float' }, + }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, @@ -930,7 +1020,9 @@ g.test('replaced_binding') // Create bindGroup1. It has one binding, which use the same view/subresource of a binding in // bindGroup0. So it may or may not conflicts with that binding in bindGroup0. - const bindGroup1 = t.createBindGroup(0, sampledStorageView, 'sampled-texture', '2d', undefined); + const bindGroup1 = t.createBindGroup(0, sampledStorageView, 'sampled-texture', '2d', { + sampleType: 'unfilterable-float', + }); const encoder = t.device.createCommandEncoder(); const pass = compute @@ -941,7 +1033,9 @@ g.test('replaced_binding') // But bindings in bindGroup0 should be validated too. pass.setBindGroup(0, bindGroup0); if (callDrawOrDispatch) { - const pipeline = compute ? t.createNoOpComputePipeline() : t.createNoOpRenderPipeline(); + const pipeline = compute + ? t.createNoOpComputePipeline() + : t.createNoOpRenderPipeline('auto', 'r32float'); t.setPipeline(pass, pipeline); t.issueDrawOrDispatch(pass); } @@ -951,7 +1045,9 @@ g.test('replaced_binding') // MAINTENANCE_TODO: If the Compatible Usage List // (https://gpuweb.github.io/gpuweb/#compatible-usage-list) gets programmatically defined in // capability_info, use it here, instead of this logic, for clarity. - let success = entry.storageTexture?.access !== 'write-only'; + let success = + entry.storageTexture?.access !== 'write-only' && + entry.storageTexture?.access !== 'read-write'; // Replaced bindings should not be validated in compute pass, because validation only occurs // inside dispatchWorkgroups() which only looks at the current resource usages. success ||= compute; @@ -981,7 +1077,9 @@ g.test('bindings_in_bundle') case 'multisampled-texture': case 'sampled-texture': return 'TEXTURE_BINDING' as const; + case 'readonly-storage-texture': case 'writeonly-storage-texture': + case 'readwrite-storage-texture': return 'STORAGE_BINDING' as const; case 'render-target': return 'RENDER_ATTACHMENT' as const; @@ -1033,17 +1131,17 @@ g.test('bindings_in_bundle') const bindGroups: GPUBindGroup[] = []; if (type0 !== 'render-target') { - const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'rgba8unorm'; + const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'r32float'; bindGroups[0] = t.createBindGroup(0, view, type0, '2d', { format: binding0TexFormat, - sampleType: _sampleCount && 'unfilterable-float', + sampleType: 'unfilterable-float', }); } if (type1 !== 'render-target') { - const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'rgba8unorm'; + const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'r32float'; bindGroups[1] = t.createBindGroup(1, view, type1, '2d', { format: binding1TexFormat, - sampleType: _sampleCount && 'unfilterable-float', + sampleType: 'unfilterable-float', }); } @@ -1062,7 +1160,7 @@ g.test('bindings_in_bundle') // 'render-target'). if (bindingsInBundle[i]) { const bundleEncoder = t.device.createRenderBundleEncoder({ - colorFormats: ['rgba8unorm'], + colorFormats: ['r32float'], }); bundleEncoder.setBindGroup(i, bindGroups[i]); const bundleInPass = bundleEncoder.finish(); @@ -1078,6 +1176,7 @@ g.test('bindings_in_bundle') switch (t) { case 'sampled-texture': case 'multisampled-texture': + case 'readonly-storage-texture': return true; default: return false; @@ -1089,7 +1188,8 @@ g.test('bindings_in_bundle') success = true; } - if (type0 === 'writeonly-storage-texture' && type1 === 'writeonly-storage-texture') { + // Writable storage textures (write-only and read-write storage textures) cannot be aliased. + if (type0 === type1) { success = true; } @@ -1110,6 +1210,8 @@ g.test('unused_bindings_in_pipeline') .params(u => u .combine('compute', [false, true]) + .combine('readOnlyUsage', ['sampled-texture', 'readonly-storage-texture'] as const) + .combine('writableUsage', ['writeonly-storage-texture', 'readwrite-storage-texture'] as const) .combine('useBindGroup0', [false, true]) .combine('useBindGroup1', [false, true]) .combine('setBindGroupsOrder', ['common', 'reversed'] as const) @@ -1119,41 +1221,49 @@ g.test('unused_bindings_in_pipeline') .fn(t => { const { compute, + readOnlyUsage, + writableUsage, useBindGroup0, useBindGroup1, setBindGroupsOrder, setPipeline, callDrawOrDispatch, } = t.params; + if (writableUsage === 'readwrite-storage-texture') { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + } + const view = t .createTexture({ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING }) .createView(); - const bindGroup0 = t.createBindGroup(0, view, 'sampled-texture', '2d', { - format: 'rgba8unorm', + const bindGroup0 = t.createBindGroup(0, view, readOnlyUsage, '2d', { + sampleType: 'unfilterable-float', + format: 'r32float', }); - const bindGroup1 = t.createBindGroup(0, view, 'writeonly-storage-texture', '2d', { - format: 'rgba8unorm', + const bindGroup1 = t.createBindGroup(0, view, writableUsage, '2d', { + format: 'r32float', }); + const writeAccess = writableUsage === 'writeonly-storage-texture' ? 'write' : 'read_write'; const wgslVertex = `@vertex fn main() -> @builtin(position) vec4<f32> { return vec4<f32>(); }`; const wgslFragment = pp` ${pp._if(useBindGroup0)} - @group(0) @binding(0) var image0 : texture_storage_2d<rgba8unorm, write>; + @group(0) @binding(0) var image0 : texture_storage_2d<r32float, ${writeAccess}>; ${pp._endif} ${pp._if(useBindGroup1)} - @group(1) @binding(0) var image1 : texture_storage_2d<rgba8unorm, write>; + @group(1) @binding(0) var image1 : texture_storage_2d<r32float, ${writeAccess}>; ${pp._endif} @fragment fn main() {} `; const wgslCompute = pp` ${pp._if(useBindGroup0)} - @group(0) @binding(0) var image0 : texture_storage_2d<rgba8unorm, write>; + @group(0) @binding(0) var image0 : texture_storage_2d<r32float, ${writeAccess}>; ${pp._endif} ${pp._if(useBindGroup1)} - @group(1) @binding(0) var image1 : texture_storage_2d<rgba8unorm, write>; + @group(1) @binding(0) var image1 : texture_storage_2d<r32float, ${writeAccess}>; ${pp._endif} @compute @workgroup_size(1) fn main() {} `; @@ -1181,7 +1291,7 @@ g.test('unused_bindings_in_pipeline') code: wgslFragment, }), entryPoint: 'main', - targets: [{ format: 'rgba8unorm', writeMask: 0 }], + targets: [{ format: 'r32float', writeMask: 0 }], }, primitive: { topology: 'triangle-list' }, }); @@ -1237,14 +1347,28 @@ g.test('scope,dispatch') .params(u => u .combine('dispatch', ['none', 'direct', 'indirect']) + .expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) .beginSubcases() .expand('setBindGroup0', p => (p.dispatch ? [true] : [false, true])) .expand('setBindGroup1', p => (p.dispatch ? [true] : [false, true])) ) .fn(t => { - const { dispatch, setBindGroup0, setBindGroup1 } = t.params; + const { dispatch, usage1, usage2, setBindGroup0, setBindGroup1 } = t.params; - const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(true); + const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope( + true, + usage1, + usage2 + ); assert(pass instanceof GPUComputePassEncoder); t.setPipeline(pass, pipeline); @@ -1284,11 +1408,21 @@ g.test('scope,basic,render') u // .combine('setBindGroup0', [false, true]) .combine('setBindGroup1', [false, true]) + .expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) ) .fn(t => { - const { setBindGroup0, setBindGroup1 } = t.params; + const { setBindGroup0, setBindGroup1, usage1, usage2 } = t.params; - const { bindGroup0, bindGroup1, encoder, pass } = t.testValidationScope(false); + const { bindGroup0, bindGroup1, encoder, pass } = t.testValidationScope(false, usage1, usage2); assert(pass instanceof GPURenderPassEncoder); if (setBindGroup0) pass.setBindGroup(0, bindGroup0); @@ -1308,11 +1442,22 @@ g.test('scope,pass_boundary,compute') boundary in between. This should always be valid. ` ) - .paramsSubcasesOnly(u => u.combine('splitPass', [false, true])) + .paramsSubcasesOnly(u => + u.combine('splitPass', [false, true]).expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) + ) .fn(t => { - const { splitPass } = t.params; + const { splitPass, usage1, usage2 } = t.params; - const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups(); + const { bindGroupLayouts, bindGroups } = t.makeTwoBindGroupsWithOneTextureView(usage1, usage2); const encoder = t.device.createCommandEncoder(); @@ -1355,23 +1500,35 @@ g.test('scope,pass_boundary,render') u // .combine('splitPass', [false, true]) .combine('draw', [false, true]) + .expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) ) .fn(t => { - const { splitPass, draw } = t.params; + const { splitPass, draw, usage1, usage2 } = t.params; - const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups(); + const { bindGroupLayouts, bindGroups } = t.makeTwoBindGroupsWithOneTextureView(usage1, usage2); const encoder = t.device.createCommandEncoder(); const pipelineUsingBG0 = t.createNoOpRenderPipeline( t.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayouts[0]], - }) + }), + 'r32float' ); const pipelineUsingBG1 = t.createNoOpRenderPipeline( t.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayouts[1]], - }) + }), + 'r32float' ); const attachment = t.createTexture().createView(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts index 0c41098556..120aadfb07 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts @@ -6,6 +6,21 @@ import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { assert, unreachable } from '../../../../../common/util/util.js'; import { ValidationTest } from '../../validation_test.js'; +export type TextureBindingType = + | 'sampled-texture' + | 'writeonly-storage-texture' + | 'readonly-storage-texture' + | 'readwrite-storage-texture'; +export const kTextureBindingTypes = [ + 'sampled-texture', + 'writeonly-storage-texture', + 'readonly-storage-texture', + 'readwrite-storage-texture', +] as const; +export function IsReadOnlyTextureBindingType(t: TextureBindingType): boolean { + return t === 'sampled-texture' || t === 'readonly-storage-texture'; +} + class F extends ValidationTest { getColorAttachment( texture: GPUTexture, @@ -23,21 +38,35 @@ class F extends ValidationTest { createBindGroupForTest( textureView: GPUTextureView, - textureUsage: 'texture' | 'storage', - sampleType: 'float' | 'depth' | 'uint' + textureUsage: TextureBindingType, + sampleType: 'unfilterable-float' | 'depth' | 'uint' ) { const bindGroupLayoutEntry: GPUBindGroupLayoutEntry = { binding: 0, visibility: GPUShaderStage.FRAGMENT, }; switch (textureUsage) { - case 'texture': + case 'sampled-texture': bindGroupLayoutEntry.texture = { viewDimension: '2d-array', sampleType }; break; - case 'storage': + case 'readonly-storage-texture': + bindGroupLayoutEntry.storageTexture = { + access: 'read-only', + format: 'r32float', + viewDimension: '2d-array', + }; + break; + case 'readwrite-storage-texture': + bindGroupLayoutEntry.storageTexture = { + access: 'read-write', + format: 'r32float', + viewDimension: '2d-array', + }; + break; + case 'writeonly-storage-texture': bindGroupLayoutEntry.storageTexture = { access: 'write-only', - format: 'rgba8unorm', + format: 'r32float', viewDimension: '2d-array', }; break; @@ -89,7 +118,7 @@ g.test('subresources,color_attachments') const { layer0, level0, layer1, level1, inSamePass } = t.params; const texture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, kTextureLayers], mipLevelCount: kTextureLevels, @@ -152,8 +181,8 @@ g.test('subresources,color_attachment_and_bind_group') { bgLayer: 1, bgLayerCount: 1 }, { bgLayer: 1, bgLayerCount: 2 }, ]) - .combine('bgUsage', ['texture', 'storage'] as const) - .unless(t => t.bgUsage === 'storage' && t.bgLevelCount > 1) + .combine('bgUsage', kTextureBindingTypes) + .unless(t => t.bgUsage !== 'sampled-texture' && t.bgLevelCount > 1) .combine('inSamePass', [true, false]) ) .fn(t => { @@ -169,7 +198,7 @@ g.test('subresources,color_attachment_and_bind_group') } = t.params; const texture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | @@ -184,7 +213,7 @@ g.test('subresources,color_attachment_and_bind_group') baseMipLevel: bgLevel, mipLevelCount: bgLevelCount, }); - const bindGroup = t.createBindGroupForTest(bindGroupView, bgUsage, 'float'); + const bindGroup = t.createBindGroupForTest(bindGroupView, bgUsage, 'unfilterable-float'); const colorAttachment = t.getColorAttachment(texture, { dimension: '2d', @@ -205,7 +234,7 @@ g.test('subresources,color_attachment_and_bind_group') renderPass.end(); const texture2 = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, 1], mipLevelCount: 1, @@ -261,7 +290,8 @@ g.test('subresources,depth_stencil_attachment_and_bind_group') { bgLayer: 1, bgLayerCount: 2 }, ]) .beginSubcases() - .combine('dsReadOnly', [true, false]) + .combine('depthReadOnly', [true, false]) + .combine('stencilReadOnly', [true, false]) .combine('bgAspect', ['depth-only', 'stencil-only'] as const) .combine('inSamePass', [true, false]) ) @@ -273,7 +303,8 @@ g.test('subresources,depth_stencil_attachment_and_bind_group') bgLevelCount, bgLayer, bgLayerCount, - dsReadOnly, + depthReadOnly, + stencilReadOnly, bgAspect, inSamePass, } = t.params; @@ -293,7 +324,7 @@ g.test('subresources,depth_stencil_attachment_and_bind_group') aspect: bgAspect, }); const sampleType = bgAspect === 'depth-only' ? 'depth' : 'uint'; - const bindGroup = t.createBindGroupForTest(bindGroupView, 'texture', sampleType); + const bindGroup = t.createBindGroupForTest(bindGroupView, 'sampled-texture', sampleType); const attachmentView = texture.createView({ dimension: '2d', @@ -304,12 +335,12 @@ g.test('subresources,depth_stencil_attachment_and_bind_group') }); const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { view: attachmentView, - depthReadOnly: dsReadOnly, - depthLoadOp: dsReadOnly ? undefined : 'load', - depthStoreOp: dsReadOnly ? undefined : 'store', - stencilReadOnly: dsReadOnly, - stencilLoadOp: dsReadOnly ? undefined : 'load', - stencilStoreOp: dsReadOnly ? undefined : 'store', + depthReadOnly, + depthLoadOp: depthReadOnly ? undefined : 'load', + depthStoreOp: depthReadOnly ? undefined : 'store', + stencilReadOnly, + stencilLoadOp: stencilReadOnly ? undefined : 'load', + stencilStoreOp: stencilReadOnly ? undefined : 'store', }; const encoder = t.device.createCommandEncoder(); @@ -350,8 +381,11 @@ g.test('subresources,depth_stencil_attachment_and_bind_group') bgLayer + bgLayerCount - 1 ); const isNotOverlapped = isMipLevelNotOverlapped || isArrayLayerNotOverlapped; + const readonly = + (bgAspect === 'stencil-only' && stencilReadOnly) || + (bgAspect === 'depth-only' && depthReadOnly); - const success = !inSamePass || isNotOverlapped || dsReadOnly; + const success = !inSamePass || isNotOverlapped || readonly; t.expectValidationError(() => { encoder.finish(); }, !success); @@ -388,20 +422,21 @@ g.test('subresources,multiple_bind_groups') { base: 1, count: 1 }, { base: 1, count: 2 }, ]) - .combine('bgUsage0', ['texture', 'storage'] as const) - .combine('bgUsage1', ['texture', 'storage'] as const) + .combine('bgUsage0', kTextureBindingTypes) + .combine('bgUsage1', kTextureBindingTypes) .unless( t => - (t.bgUsage0 === 'storage' && t.bg0Levels.count > 1) || - (t.bgUsage1 === 'storage' && t.bg1Levels.count > 1) + (t.bgUsage0 !== 'sampled-texture' && t.bg0Levels.count > 1) || + (t.bgUsage1 !== 'sampled-texture' && t.bg1Levels.count > 1) ) + .beginSubcases() .combine('inSamePass', [true, false]) ) .fn(t => { const { bg0Levels, bg0Layers, bg1Levels, bg1Layers, bgUsage0, bgUsage1, inSamePass } = t.params; const texture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING, size: [kTextureSize, kTextureSize, kTextureLayers], mipLevelCount: kTextureLevels, @@ -420,11 +455,11 @@ g.test('subresources,multiple_bind_groups') baseMipLevel: bg1Levels.base, mipLevelCount: bg1Levels.count, }); - const bindGroup0 = t.createBindGroupForTest(bg0, bgUsage0, 'float'); - const bindGroup1 = t.createBindGroupForTest(bg1, bgUsage1, 'float'); + const bindGroup0 = t.createBindGroupForTest(bg0, bgUsage0, 'unfilterable-float'); + const bindGroup1 = t.createBindGroupForTest(bg1, bgUsage1, 'unfilterable-float'); const colorTexture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, 1], mipLevelCount: 1, @@ -449,6 +484,8 @@ g.test('subresources,multiple_bind_groups') renderPass2.end(); } + const bothReadOnly = + IsReadOnlyTextureBindingType(bgUsage0) && IsReadOnlyTextureBindingType(bgUsage1); const isMipLevelNotOverlapped = t.isRangeNotOverlapped( bg0Levels.base, bg0Levels.base + bg0Levels.count - 1, @@ -463,7 +500,7 @@ g.test('subresources,multiple_bind_groups') ); const isNotOverlapped = isMipLevelNotOverlapped || isArrayLayerNotOverlapped; - const success = !inSamePass || isNotOverlapped || bgUsage0 === bgUsage1; + const success = !inSamePass || bothReadOnly || isNotOverlapped || bgUsage0 === bgUsage1; t.expectValidationError(() => { encoder.finish(); }, !success); @@ -531,8 +568,8 @@ g.test('subresources,depth_stencil_texture_in_bind_groups') const sampleType0 = aspect0 === 'depth-only' ? 'depth' : 'uint'; const sampleType1 = aspect1 === 'depth-only' ? 'depth' : 'uint'; - const bindGroup0 = t.createBindGroupForTest(bindGroupView0, 'texture', sampleType0); - const bindGroup1 = t.createBindGroupForTest(bindGroupView1, 'texture', sampleType1); + const bindGroup0 = t.createBindGroupForTest(bindGroupView0, 'sampled-texture', sampleType0); + const bindGroup1 = t.createBindGroupForTest(bindGroupView1, 'sampled-texture', sampleType1); const colorTexture = t.device.createTexture({ format: 'rgba8unorm', diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts index 1b80a2f73e..db18a0140f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts @@ -5,11 +5,16 @@ Texture Usages Validation Tests on All Kinds of WebGPU Subresource Usage Scopes. import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { unreachable } from '../../../../../common/util/util.js'; import { ValidationTest } from '../../validation_test.js'; +import { + TextureBindingType, + kTextureBindingTypes, + IsReadOnlyTextureBindingType, +} from '../texture/in_render_common.spec.js'; class F extends ValidationTest { createBindGroupLayoutForTest( - textureUsage: 'texture' | 'storage', - sampleType: 'float' | 'depth' | 'uint', + textureUsage: TextureBindingType, + sampleType: 'unfilterable-float' | 'depth' | 'uint', visibility: GPUShaderStage['FRAGMENT'] | GPUShaderStage['COMPUTE'] = GPUShaderStage['FRAGMENT'] ): GPUBindGroupLayout { const bindGroupLayoutEntry: GPUBindGroupLayoutEntry = { @@ -18,13 +23,27 @@ class F extends ValidationTest { }; switch (textureUsage) { - case 'texture': + case 'sampled-texture': bindGroupLayoutEntry.texture = { viewDimension: '2d-array', sampleType }; break; - case 'storage': + case 'readonly-storage-texture': + bindGroupLayoutEntry.storageTexture = { + access: 'read-only', + format: 'r32float', + viewDimension: '2d-array', + }; + break; + case 'writeonly-storage-texture': bindGroupLayoutEntry.storageTexture = { access: 'write-only', - format: 'rgba8unorm', + format: 'r32float', + viewDimension: '2d-array', + }; + break; + case 'readwrite-storage-texture': + bindGroupLayoutEntry.storageTexture = { + access: 'read-write', + format: 'r32float', viewDimension: '2d-array', }; break; @@ -39,8 +58,8 @@ class F extends ValidationTest { createBindGroupForTest( textureView: GPUTextureView, - textureUsage: 'texture' | 'storage', - sampleType: 'float' | 'depth' | 'uint', + textureUsage: TextureBindingType, + sampleType: 'unfilterable-float' | 'depth' | 'uint', visibility: GPUShaderStage['FRAGMENT'] | GPUShaderStage['COMPUTE'] = GPUShaderStage['FRAGMENT'] ) { return this.device.createBindGroup({ @@ -64,20 +83,16 @@ g.test('subresources,set_bind_group_on_same_index_color_texture') ) .params(u => u - .combineWithParams([ - { useDifferentTextureAsTexture2: true, baseLayer2: 0, view2Binding: 'texture' }, - { useDifferentTextureAsTexture2: false, baseLayer2: 0, view2Binding: 'texture' }, - { useDifferentTextureAsTexture2: false, baseLayer2: 1, view2Binding: 'texture' }, - { useDifferentTextureAsTexture2: false, baseLayer2: 0, view2Binding: 'storage' }, - { useDifferentTextureAsTexture2: false, baseLayer2: 1, view2Binding: 'storage' }, - ] as const) - .combine('hasConflict', [true, false]) + .combine('useDifferentTextureAsTexture2', [true, false]) + .combine('baseLayer2', [0, 1] as const) + .combine('view1Binding', kTextureBindingTypes) + .combine('view2Binding', kTextureBindingTypes) ) .fn(t => { - const { useDifferentTextureAsTexture2, baseLayer2, view2Binding, hasConflict } = t.params; + const { useDifferentTextureAsTexture2, baseLayer2, view1Binding, view2Binding } = t.params; const texture0 = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, size: [kTextureSize, kTextureSize, kTextureLayers], }); @@ -87,19 +102,12 @@ g.test('subresources,set_bind_group_on_same_index_color_texture') baseArrayLayer: 0, arrayLayerCount: 1, }); - const bindGroup0 = t.createBindGroupForTest(textureView0, view2Binding, 'float'); - - // In one renderPassEncoder it is an error to set both bindGroup0 and bindGroup1. - const view1Binding = hasConflict - ? view2Binding === 'texture' - ? 'storage' - : 'texture' - : view2Binding; - const bindGroup1 = t.createBindGroupForTest(textureView0, view1Binding, 'float'); + const bindGroup0 = t.createBindGroupForTest(textureView0, view1Binding, 'unfilterable-float'); + const bindGroup1 = t.createBindGroupForTest(textureView0, view2Binding, 'unfilterable-float'); const texture2 = useDifferentTextureAsTexture2 ? t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, size: [kTextureSize, kTextureSize, kTextureLayers], }) @@ -110,10 +118,14 @@ g.test('subresources,set_bind_group_on_same_index_color_texture') arrayLayerCount: kTextureLayers - baseLayer2, }); // There should be no conflict between bindGroup0 and validBindGroup2. - const validBindGroup2 = t.createBindGroupForTest(textureView2, view2Binding, 'float'); + const validBindGroup2 = t.createBindGroupForTest( + textureView2, + view2Binding, + 'unfilterable-float' + ); - const colorTexture = t.device.createTexture({ - format: 'rgba8unorm', + const unusedColorTexture = t.device.createTexture({ + format: 'r32float', usage: GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, 1], }); @@ -121,7 +133,7 @@ g.test('subresources,set_bind_group_on_same_index_color_texture') const renderPassEncoder = encoder.beginRenderPass({ colorAttachments: [ { - view: colorTexture.createView(), + view: unusedColorTexture.createView(), loadOp: 'load', storeOp: 'store', }, @@ -132,9 +144,12 @@ g.test('subresources,set_bind_group_on_same_index_color_texture') renderPassEncoder.setBindGroup(1, validBindGroup2); renderPassEncoder.end(); + const noConflict = + (IsReadOnlyTextureBindingType(view1Binding) && IsReadOnlyTextureBindingType(view2Binding)) || + view1Binding === view2Binding; t.expectValidationError(() => { encoder.finish(); - }, hasConflict); + }, !noConflict); }); g.test('subresources,set_bind_group_on_same_index_depth_stencil_texture') @@ -155,6 +170,9 @@ g.test('subresources,set_bind_group_on_same_index_depth_stencil_texture') format: 'depth24plus-stencil8', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, 1], + ...(t.isCompatibility && { + textureBindingViewDimension: '2d-array', + }), }); const conflictedToNonReadOnlyAttachmentBindGroup = t.createBindGroupForTest( @@ -162,21 +180,24 @@ g.test('subresources,set_bind_group_on_same_index_depth_stencil_texture') dimension: '2d-array', aspect: bindAspect, }), - 'texture', + 'sampled-texture', bindAspect === 'depth-only' ? 'depth' : 'uint' ); const colorTexture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, size: [kTextureSize, kTextureSize, 1], + ...(t.isCompatibility && { + textureBindingViewDimension: '2d-array', + }), }); const validBindGroup = t.createBindGroupForTest( colorTexture.createView({ dimension: '2d-array', }), - 'texture', - 'float' + 'sampled-texture', + 'unfilterable-float' ); const encoder = t.device.createCommandEncoder(); @@ -204,12 +225,24 @@ g.test('subresources,set_unused_bind_group') used in the same render or compute pass encoder, its list of internal usages within one usage scope can only be a compatible usage list.` ) - .params(u => u.combine('inRenderPass', [true, false]).combine('hasConflict', [true, false])) + .params(u => + u + .combine('inRenderPass', [true, false]) + .combine('textureUsage0', kTextureBindingTypes) + .combine('textureUsage1', kTextureBindingTypes) + ) .fn(t => { - const { inRenderPass, hasConflict } = t.params; + const { inRenderPass, textureUsage0, textureUsage1 } = t.params; + + if ( + textureUsage0 === 'readwrite-storage-texture' || + textureUsage1 === 'readwrite-storage-texture' + ) { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + } const texture0 = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, size: [kTextureSize, kTextureSize, kTextureLayers], }); @@ -221,40 +254,85 @@ g.test('subresources,set_unused_bind_group') }); const visibility = inRenderPass ? GPUShaderStage.FRAGMENT : GPUShaderStage.COMPUTE; // bindGroup0 is used by the pipelines, and bindGroup1 is not used by the pipelines. - const textureUsage0 = inRenderPass ? 'texture' : 'storage'; - const textureUsage1 = hasConflict ? (inRenderPass ? 'storage' : 'texture') : textureUsage0; - const bindGroup0 = t.createBindGroupForTest(textureView0, textureUsage0, 'float', visibility); - const bindGroup1 = t.createBindGroupForTest(textureView0, textureUsage1, 'float', visibility); + const bindGroup0 = t.createBindGroupForTest( + textureView0, + textureUsage0, + 'unfilterable-float', + visibility + ); + const bindGroup1 = t.createBindGroupForTest( + textureView0, + textureUsage1, + 'unfilterable-float', + visibility + ); const encoder = t.device.createCommandEncoder(); const colorTexture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, 1], }); - const pipelineLayout = t.device.createPipelineLayout({ - bindGroupLayouts: [t.createBindGroupLayoutForTest(textureUsage0, 'float', visibility)], - }); if (inRenderPass) { + let fragmentShader = ''; + switch (textureUsage0) { + case 'sampled-texture': + fragmentShader = ` + @group(0) @binding(0) var texture0 : texture_2d_array<f32>; + @fragment fn main() + -> @location(0) vec4<f32> { + return textureLoad(texture0, vec2<i32>(), 0, 0); + } + `; + break; + case `readonly-storage-texture`: + fragmentShader = ` + @group(0) @binding(0) var texture0 : texture_storage_2d_array<r32float, read>; + @fragment fn main() + -> @location(0) vec4<f32> { + return textureLoad(texture0, vec2<i32>(), 0); + } + `; + break; + case `writeonly-storage-texture`: + fragmentShader = ` + @group(0) @binding(0) var texture0 : texture_storage_2d_array<r32float, write>; + @fragment fn main() + -> @location(0) vec4<f32> { + textureStore(texture0, vec2i(), 0, vec4f(1, 0, 0, 1)); + return vec4f(0, 0, 0, 1); + } + `; + break; + case `readwrite-storage-texture`: + fragmentShader = ` + @group(0) @binding(0) var texture0 : texture_storage_2d_array<r32float, read_write>; + @fragment fn main() + -> @location(0) vec4<f32> { + let color = textureLoad(texture0, vec2i(), 0); + textureStore(texture0, vec2i(), 0, vec4f(1, 0, 0, 1)); + return color; + } + `; + break; + } + const renderPipeline = t.device.createRenderPipeline({ - layout: pipelineLayout, + layout: t.device.createPipelineLayout({ + bindGroupLayouts: [ + t.createBindGroupLayoutForTest(textureUsage0, 'unfilterable-float', visibility), + ], + }), vertex: { module: t.device.createShaderModule({ code: t.getNoOpShaderCode('VERTEX'), }), - entryPoint: 'main', }, fragment: { module: t.device.createShaderModule({ - code: ` - @group(0) @binding(0) var texture0 : texture_2d_array<f32>; - @fragment fn main() - -> @location(0) vec4<f32> { - return textureLoad(texture0, vec2<i32>(), 0, 0); - }`, + code: fragmentShader, }), - entryPoint: 'main', - targets: [{ format: 'rgba8unorm' }], + targets: [{ format: 'r32float' }], }, }); @@ -273,29 +351,97 @@ g.test('subresources,set_unused_bind_group') renderPassEncoder.draw(1); renderPassEncoder.end(); } else { + let computeShader = ''; + switch (textureUsage0) { + case 'sampled-texture': + computeShader = ` + @group(0) @binding(0) var texture0 : texture_2d_array<f32>; + @group(1) @binding(0) var writableStorage : texture_storage_2d_array<r32float, write>; + @compute @workgroup_size(1) fn main() { + let value = textureLoad(texture0, vec2i(), 0, 0); + textureStore(writableStorage, vec2i(), 0, value); + } + `; + break; + case `readonly-storage-texture`: + computeShader = ` + @group(0) @binding(0) var texture0 : texture_storage_2d_array<r32float, read>; + @group(1) @binding(0) var writableStorage : texture_storage_2d_array<r32float, write>; + @compute @workgroup_size(1) fn main() { + let value = textureLoad(texture0, vec2<i32>(), 0); + textureStore(writableStorage, vec2i(), 0, value); + } + `; + break; + case `writeonly-storage-texture`: + computeShader = ` + @group(0) @binding(0) var texture0 : texture_storage_2d_array<r32float, write>; + @group(1) @binding(0) var writableStorage : texture_storage_2d_array<r32float, write>; + @compute @workgroup_size(1) fn main() { + textureStore(texture0, vec2i(), 0, vec4f(1, 0, 0, 1)); + textureStore(writableStorage, vec2i(), 0, vec4f(1, 0, 0, 1)); + } + `; + break; + case `readwrite-storage-texture`: + computeShader = ` + @group(0) @binding(0) var texture0 : texture_storage_2d_array<r32float, read_write>; + @group(1) @binding(0) var writableStorage : texture_storage_2d_array<r32float, write>; + @compute @workgroup_size(1) fn main() { + let color = textureLoad(texture0, vec2i(), 0); + textureStore(texture0, vec2i(), 0, vec4f(1, 0, 0, 1)); + textureStore(writableStorage, vec2i(), 0, color); + } + `; + break; + } + + const pipelineLayout = t.device.createPipelineLayout({ + bindGroupLayouts: [ + t.createBindGroupLayoutForTest(textureUsage0, 'unfilterable-float', visibility), + t.createBindGroupLayoutForTest( + 'writeonly-storage-texture', + 'unfilterable-float', + visibility + ), + ], + }); const computePipeline = t.device.createComputePipeline({ layout: pipelineLayout, compute: { module: t.device.createShaderModule({ - code: ` - @group(0) @binding(0) var texture0 : texture_storage_2d_array<rgba8unorm, write>; - @compute @workgroup_size(1) - fn main() { - textureStore(texture0, vec2<i32>(), 0, vec4<f32>()); - }`, + code: computeShader, }), - entryPoint: 'main', }, }); + + const writableStorageTexture = t.device.createTexture({ + format: 'r32float', + usage: GPUTextureUsage.STORAGE_BINDING, + size: [kTextureSize, kTextureSize, 1], + }); + const writableStorageTextureView = writableStorageTexture.createView({ + dimension: '2d-array', + baseArrayLayer: 0, + arrayLayerCount: 1, + }); + const writableStorageTextureBindGroup = t.createBindGroupForTest( + writableStorageTextureView, + 'writeonly-storage-texture', + 'unfilterable-float', + visibility + ); + const computePassEncoder = encoder.beginComputePass(); computePassEncoder.setBindGroup(0, bindGroup0); - computePassEncoder.setBindGroup(1, bindGroup1); + computePassEncoder.setBindGroup(1, writableStorageTextureBindGroup); + computePassEncoder.setBindGroup(2, bindGroup1); computePassEncoder.setPipeline(computePipeline); computePassEncoder.dispatchWorkgroups(1); computePassEncoder.end(); } - // In WebGPU SPEC (Chapter 3.4.5, Synchronization): + // In WebGPU SPEC (https://gpuweb.github.io/gpuweb/#programming-model-synchronization): // This specification defines the following usage scopes: // - In a compute pass, each dispatch command (dispatchWorkgroups() or // dispatchWorkgroupsIndirect()) is one usage scope. A subresource is "used" in the usage @@ -306,7 +452,11 @@ g.test('subresources,set_unused_bind_group') // referenced by any (state-setting or non-state-setting) command. For example, in // setBindGroup(index, bindGroup, dynamicOffsets), every subresource in bindGroup is "used" in // the render pass’s usage scope. - const success = !inRenderPass || !hasConflict; + const success = + !inRenderPass || + (IsReadOnlyTextureBindingType(textureUsage0) && + IsReadOnlyTextureBindingType(textureUsage1)) || + textureUsage0 === textureUsage1; t.expectValidationError(() => { encoder.finish(); }, !success); @@ -324,16 +474,14 @@ g.test('subresources,texture_usages_in_copy_and_render_pass') .combine('usage0', [ 'copy-src', 'copy-dst', - 'texture', - 'storage', 'color-attachment', + ...kTextureBindingTypes, ] as const) .combine('usage1', [ 'copy-src', 'copy-dst', - 'texture', - 'storage', 'color-attachment', + ...kTextureBindingTypes, ] as const) .filter( ({ usage0, usage1 }) => @@ -347,7 +495,7 @@ g.test('subresources,texture_usages_in_copy_and_render_pass') const { usage0, usage1 } = t.params; const texture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | @@ -355,11 +503,14 @@ g.test('subresources,texture_usages_in_copy_and_render_pass') GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, 1], + ...(t.isCompatibility && { + textureBindingViewDimension: '2d-array', + }), }); const UseTextureOnCommandEncoder = ( texture: GPUTexture, - usage: 'copy-src' | 'copy-dst' | 'texture' | 'storage' | 'color-attachment', + usage: 'copy-src' | 'copy-dst' | 'color-attachment' | TextureBindingType, encoder: GPUCommandEncoder ) => { switch (usage) { @@ -386,10 +537,12 @@ g.test('subresources,texture_usages_in_copy_and_render_pass') renderPassEncoder.end(); break; } - case 'texture': - case 'storage': { + case 'sampled-texture': + case 'readonly-storage-texture': + case 'writeonly-storage-texture': + case 'readwrite-storage-texture': { const colorTexture = t.device.createTexture({ - format: 'rgba8unorm', + format: 'r32float', usage: GPUTextureUsage.RENDER_ATTACHMENT, size: [kTextureSize, kTextureSize, 1], }); @@ -403,7 +556,7 @@ g.test('subresources,texture_usages_in_copy_and_render_pass') dimension: '2d-array', }), usage, - 'float' + 'unfilterable-float' ); renderPassEncoder.setBindGroup(0, bindGroup); renderPassEncoder.end(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts index 1a8da470a4..c956dc3021 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts @@ -3,6 +3,7 @@ This tests entry point validation of compute/render pipelines and their shader m The entryPoint in shader module include standard "main" and others. The entryPoint assigned in descriptor include: +- Undefined with matching entry point for stage - Matching case (control case) - Empty string - Mistyping @@ -10,7 +11,6 @@ The entryPoint assigned in descriptor include: - Unicode entrypoints and their ASCIIfied version TODO: -- Test unicode normalization (gpuweb/gpuweb#1160) - Fine-tune test cases to reduce number by removing trivially similar cases `; @@ -43,23 +43,52 @@ const kEntryPointTestCases = [ g.test('compute') .desc( ` -Tests calling createComputePipeline(Async) with valid vertex stage shader and different entryPoints, +Tests calling createComputePipeline(Async) with valid compute stage shader and different entryPoints, and check that the APIs only accept matching entryPoint. ` ) - .params(u => u.combine('isAsync', [true, false]).combineWithParams(kEntryPointTestCases)) + .params(u => + u + .combine('isAsync', [true, false]) + .combine('shaderModuleStage', ['compute', 'vertex', 'fragment'] as const) + .beginSubcases() + .combine('provideEntryPoint', [true, false]) + .combine('extraEntryPoint', [true, false]) + .combineWithParams(kEntryPointTestCases) + ) .fn(t => { - const { isAsync, shaderModuleEntryPoint, stageEntryPoint } = t.params; + const { + isAsync, + provideEntryPoint, + extraEntryPoint, + shaderModuleStage, + shaderModuleEntryPoint, + stageEntryPoint, + } = t.params; + const entryPoint = provideEntryPoint ? stageEntryPoint : undefined; + let code = getShaderWithEntryPoint(shaderModuleStage, shaderModuleEntryPoint); + if (extraEntryPoint) { + code += ` ${getShaderWithEntryPoint(shaderModuleStage, 'extra')}`; + } const descriptor: GPUComputePipelineDescriptor = { layout: 'auto', compute: { module: t.device.createShaderModule({ - code: getShaderWithEntryPoint('compute', shaderModuleEntryPoint), + code, }), - entryPoint: stageEntryPoint, + entryPoint, }, }; - const _success = shaderModuleEntryPoint === stageEntryPoint; + let _success = true; + if (shaderModuleStage !== 'compute') { + _success = false; + } + if (!provideEntryPoint && extraEntryPoint) { + _success = false; + } + if (shaderModuleEntryPoint !== stageEntryPoint && provideEntryPoint) { + _success = false; + } t.doCreateComputePipelineTest(isAsync, _success, descriptor); }); @@ -70,19 +99,46 @@ Tests calling createRenderPipeline(Async) with valid vertex stage shader and dif and check that the APIs only accept matching entryPoint. ` ) - .params(u => u.combine('isAsync', [true, false]).combineWithParams(kEntryPointTestCases)) + .params(u => + u + .combine('isAsync', [true, false]) + .combine('shaderModuleStage', ['compute', 'vertex', 'fragment'] as const) + .beginSubcases() + .combine('provideEntryPoint', [true, false]) + .combine('extraEntryPoint', [true, false]) + .combineWithParams(kEntryPointTestCases) + ) .fn(t => { - const { isAsync, shaderModuleEntryPoint, stageEntryPoint } = t.params; + const { + isAsync, + provideEntryPoint, + extraEntryPoint, + shaderModuleStage, + shaderModuleEntryPoint, + stageEntryPoint, + } = t.params; + const entryPoint = provideEntryPoint ? stageEntryPoint : undefined; + let code = getShaderWithEntryPoint(shaderModuleStage, shaderModuleEntryPoint); + if (extraEntryPoint) { + code += ` ${getShaderWithEntryPoint(shaderModuleStage, 'extra')}`; + } const descriptor: GPURenderPipelineDescriptor = { layout: 'auto', vertex: { - module: t.device.createShaderModule({ - code: getShaderWithEntryPoint('vertex', shaderModuleEntryPoint), - }), - entryPoint: stageEntryPoint, + module: t.device.createShaderModule({ code }), + entryPoint, }, }; - const _success = shaderModuleEntryPoint === stageEntryPoint; + let _success = true; + if (shaderModuleStage !== 'vertex') { + _success = false; + } + if (!provideEntryPoint && extraEntryPoint) { + _success = false; + } + if (shaderModuleEntryPoint !== stageEntryPoint && provideEntryPoint) { + _success = false; + } t.doCreateRenderPipelineTest(isAsync, _success, descriptor); }); @@ -93,25 +149,155 @@ Tests calling createRenderPipeline(Async) with valid fragment stage shader and d and check that the APIs only accept matching entryPoint. ` ) - .params(u => u.combine('isAsync', [true, false]).combineWithParams(kEntryPointTestCases)) + .params(u => + u + .combine('isAsync', [true, false]) + .combine('shaderModuleStage', ['compute', 'vertex', 'fragment'] as const) + .beginSubcases() + .combine('provideEntryPoint', [true, false]) + .combine('extraEntryPoint', [true, false]) + .combineWithParams(kEntryPointTestCases) + ) .fn(t => { - const { isAsync, shaderModuleEntryPoint, stageEntryPoint } = t.params; + const { + isAsync, + provideEntryPoint, + extraEntryPoint, + shaderModuleStage, + shaderModuleEntryPoint, + stageEntryPoint, + } = t.params; + const entryPoint = provideEntryPoint ? stageEntryPoint : undefined; + let code = getShaderWithEntryPoint(shaderModuleStage, shaderModuleEntryPoint); + if (extraEntryPoint) { + code += ` ${getShaderWithEntryPoint(shaderModuleStage, 'extra')}`; + } const descriptor: GPURenderPipelineDescriptor = { layout: 'auto', vertex: { module: t.device.createShaderModule({ code: kDefaultVertexShaderCode, }), - entryPoint: 'main', }, fragment: { module: t.device.createShaderModule({ - code: getShaderWithEntryPoint('fragment', shaderModuleEntryPoint), + code, }), - entryPoint: stageEntryPoint, + entryPoint, targets: [{ format: 'rgba8unorm' }], }, }; - const _success = shaderModuleEntryPoint === stageEntryPoint; + let _success = true; + if (shaderModuleStage !== 'fragment') { + _success = false; + } + if (!provideEntryPoint && extraEntryPoint) { + _success = false; + } + if (shaderModuleEntryPoint !== stageEntryPoint && provideEntryPoint) { + _success = false; + } t.doCreateRenderPipelineTest(isAsync, _success, descriptor); }); + +g.test('compute_undefined_entry_point_and_extra_stage') + .desc( + ` +Tests calling createComputePipeline(Async) with compute stage shader and +an undefined entryPoint is valid if there's an extra shader stage. +` + ) + .params(u => + u + .combine('isAsync', [true, false]) + .combine('extraShaderModuleStage', ['compute', 'vertex', 'fragment'] as const) + ) + .fn(t => { + const { isAsync, extraShaderModuleStage } = t.params; + const code = ` + ${getShaderWithEntryPoint('compute', 'main')} + ${getShaderWithEntryPoint(extraShaderModuleStage, 'extra')} + `; + const descriptor: GPUComputePipelineDescriptor = { + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: undefined, + }, + }; + + const success = extraShaderModuleStage !== 'compute'; + t.doCreateComputePipelineTest(isAsync, success, descriptor); + }); + +g.test('vertex_undefined_entry_point_and_extra_stage') + .desc( + ` +Tests calling createRenderPipeline(Async) with vertex stage shader and +an undefined entryPoint is valid if there's an extra shader stage. +` + ) + .params(u => + u + .combine('isAsync', [true, false]) + .combine('extraShaderModuleStage', ['compute', 'vertex', 'fragment'] as const) + ) + .fn(t => { + const { isAsync, extraShaderModuleStage } = t.params; + const code = ` + ${getShaderWithEntryPoint('vertex', 'main')} + ${getShaderWithEntryPoint(extraShaderModuleStage, 'extra')} + `; + const descriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: undefined, + }, + }; + + const success = extraShaderModuleStage !== 'vertex'; + t.doCreateRenderPipelineTest(isAsync, success, descriptor); + }); + +g.test('fragment_undefined_entry_point_and_extra_stage') + .desc( + ` +Tests calling createRenderPipeline(Async) with fragment stage shader and +an undefined entryPoint is valid if there's an extra shader stage. +` + ) + .params(u => + u + .combine('isAsync', [true, false]) + .combine('extraShaderModuleStage', ['compute', 'vertex', 'fragment'] as const) + ) + .fn(t => { + const { isAsync, extraShaderModuleStage } = t.params; + const code = ` + ${getShaderWithEntryPoint('fragment', 'main')} + ${getShaderWithEntryPoint(extraShaderModuleStage, 'extra')} + `; + const descriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ + code: kDefaultVertexShaderCode, + }), + }, + fragment: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: undefined, + targets: [{ format: 'rgba8unorm' }], + }, + }; + + const success = extraShaderModuleStage !== 'fragment'; + t.doCreateRenderPipelineTest(isAsync, success, descriptor); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts index df03427a0a..42f069ee77 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts @@ -704,10 +704,7 @@ Tests import external texture on destroyed device. Tests valid combinations of: entries: [ { binding: 0, - resource: t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, - }), + resource: t.device.importExternalTexture({ source }), }, ], }); @@ -899,7 +896,12 @@ Tests encoding and finishing a writeTimestamp command on destroyed device. const { type, stage, awaitLost } = t.params; const querySet = t.device.createQuerySet({ type, count: 2 }); await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => { - maker.encoder.writeTimestamp(querySet, 0); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (maker.encoder as any).writeTimestamp(querySet, 0); + } catch (ex) { + t.skipIf(ex instanceof TypeError, 'writeTimestamp is actually not available'); + } return maker; }); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/bgra8unorm_storage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/bgra8unorm_storage.spec.ts index 80872fd5d3..0ae600eb49 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/bgra8unorm_storage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/bgra8unorm_storage.spec.ts @@ -81,37 +81,6 @@ validation cases where this feature is not enabled, which are skipped here. }); }); -g.test('create_shader_module_with_bgra8unorm_storage') - .desc( - ` -Test that it is valid to declare the format of a storage texture as bgra8unorm in a shader module if -the feature bgra8unorm-storage is enabled. -` - ) - .beforeAllSubcases(t => { - t.selectDeviceOrSkipTestCase('bgra8unorm-storage'); - }) - .params(u => u.combine('shaderType', ['fragment', 'compute'] as const)) - .fn(t => { - const { shaderType } = t.params; - - t.testCreateShaderModuleWithBGRA8UnormStorage(shaderType, true); - }); - -g.test('create_shader_module_without_bgra8unorm_storage') - .desc( - ` -Test that it is invalid to declare the format of a storage texture as bgra8unorm in a shader module -if the feature bgra8unorm-storage is not enabled. -` - ) - .params(u => u.combine('shaderType', ['fragment', 'compute'] as const)) - .fn(t => { - const { shaderType } = t.params; - - t.testCreateShaderModuleWithBGRA8UnormStorage(shaderType, false); - }); - g.test('configure_storage_usage_on_canvas_context_without_bgra8unorm_storage') .desc( ` diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/utils.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/utils.ts new file mode 100644 index 0000000000..e6ee87f797 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/utils.ts @@ -0,0 +1,275 @@ +interface Resource { + readonly buffer?: GPUBufferBindingLayout; + readonly sampler?: GPUSamplerBindingLayout; + readonly texture?: GPUTextureBindingLayout; + readonly storageTexture?: GPUStorageTextureBindingLayout; + readonly externalTexture?: GPUExternalTextureBindingLayout; + readonly code: string; + readonly staticUse?: string; +} + +/** + * Returns an array of possible resources + */ +function generateResources(): Resource[] { + const resources: Resource[] = [ + // Buffers + { + buffer: { type: 'uniform' }, + code: `var<uniform> res : array<vec4u, 16>`, + staticUse: `res[0]`, + }, + { + buffer: { type: 'storage' }, + code: `var<storage, read_write> res : array<vec4u>`, + staticUse: `res[0]`, + }, + { + buffer: { type: 'read-only-storage' }, + code: `var<storage> res : array<vec4u>`, + staticUse: `res[0]`, + }, + + // Samplers + { + sampler: { type: 'filtering' }, + code: `var res : sampler`, + }, + { + sampler: { type: 'non-filtering' }, + code: `var res : sampler`, + }, + { + sampler: { type: 'comparison' }, + code: `var res : sampler_comparison`, + }, + // Multisampled textures + { + texture: { sampleType: 'depth', viewDimension: '2d', multisampled: true }, + code: `var res : texture_depth_multisampled_2d`, + }, + { + texture: { sampleType: 'unfilterable-float', viewDimension: '2d', multisampled: true }, + code: `var res : texture_multisampled_2d<f32>`, + }, + { + texture: { sampleType: 'sint', viewDimension: '2d', multisampled: true }, + code: `var res : texture_multisampled_2d<i32>`, + }, + { + texture: { sampleType: 'uint', viewDimension: '2d', multisampled: true }, + code: `var res : texture_multisampled_2d<u32>`, + }, + ]; + + // Sampled textures + const sampleDims: GPUTextureViewDimension[] = [ + '1d', + '2d', + '2d-array', + '3d', + 'cube', + 'cube-array', + ]; + const sampleTypes: GPUTextureSampleType[] = ['float', 'unfilterable-float', 'sint', 'uint']; + const sampleWGSL = ['f32', 'f32', 'i32', 'u32']; + for (const dim of sampleDims) { + let i = 0; + for (const type of sampleTypes) { + resources.push({ + texture: { sampleType: type, viewDimension: dim, multisampled: false }, + code: `var res : texture_${dim.replace('-', '_')}<${sampleWGSL[i++]}>`, + }); + } + } + + // Depth textures + const depthDims: GPUTextureViewDimension[] = ['2d', '2d-array', 'cube', 'cube-array']; + for (const dim of depthDims) { + resources.push({ + texture: { sampleType: 'depth', viewDimension: dim, multisampled: false }, + code: `var res : texture_depth_${dim.replace('-', '_')}`, + }); + } + + // Storage textures + // Only cover r32uint, r32sint, and r32float here for ease of testing. + const storageDims: GPUTextureViewDimension[] = ['1d', '2d', '2d-array', '3d']; + const formats: GPUTextureFormat[] = ['r32float', 'r32sint', 'r32uint']; + const accesses: GPUStorageTextureAccess[] = ['write-only', 'read-only', 'read-write']; + for (const dim of storageDims) { + for (const format of formats) { + for (const access of accesses) { + resources.push({ + storageTexture: { access, format, viewDimension: dim }, + code: `var res : texture_storage_${dim.replace('-', '_')}<${format},${access + .replace('-only', '') + .replace('-', '_')}>`, + }); + } + } + } + + return resources; +} + +/** + * Returns a string suitable as a Record key. + */ +function resourceKey(res: Resource): string { + if (res.buffer) { + return `${res.buffer.type}_buffer`; + } + if (res.sampler) { + return `${res.sampler.type}_sampler`; + } + if (res.texture) { + return `texture_${res.texture.sampleType}_${res.texture.viewDimension}_${res.texture.multisampled}`; + } + if (res.storageTexture) { + return `storage_texture_${res.storageTexture.viewDimension}_${res.storageTexture.format}_${res.storageTexture.access}`; + } + if (res.externalTexture) { + return `external_texture`; + } + return ``; +} + +/** + * Resource array converted to a Record for nicer test parameterization names. + */ +export const kAPIResources: Record<string, Resource> = Object.fromEntries( + generateResources().map(x => [resourceKey(x), x]) +) as Record<string, Resource>; + +/** + * Generates a shader of the specified stage using the specified resource at binding (0,0). + */ +export function getWGSLShaderForResource(stage: string, resource: Resource): string { + let code = `@group(0) @binding(0) ${resource.code};\n`; + + code += `@${stage}`; + if (stage === 'compute') { + code += `@workgroup_size(1)`; + } + + let retTy = ''; + let retVal = ''; + if (stage === 'vertex') { + retTy = ' -> @builtin(position) vec4f'; + retVal = 'return vec4f();'; + } else if (stage === 'fragment') { + retTy = ' -> @location(0) vec4f'; + retVal = 'return vec4f();'; + } + code += ` +fn main() ${retTy} { + _ = ${resource.staticUse ?? 'res'}; + ${retVal} +} +`; + + return code; +} + +/** + * Generates a bind group layout for for the given resource at binding 0. + */ +export function getAPIBindGroupLayoutForResource( + device: GPUDevice, + stage: GPUShaderStageFlags, + resource: Resource +): GPUBindGroupLayout { + const entry: GPUBindGroupLayoutEntry = { + binding: 0, + visibility: stage, + }; + if (resource.buffer) { + entry.buffer = resource.buffer; + } + if (resource.sampler) { + entry.sampler = resource.sampler; + } + if (resource.texture) { + entry.texture = resource.texture; + } + if (resource.storageTexture) { + entry.storageTexture = resource.storageTexture; + } + if (resource.externalTexture) { + entry.externalTexture = resource.externalTexture; + } + + const entries: GPUBindGroupLayoutEntry[] = [entry]; + return device.createBindGroupLayout({ entries }); +} + +/** + * Returns true if the sample types are compatible. + */ +function doSampleTypesMatch(api: GPUTextureSampleType, wgsl: GPUTextureSampleType): boolean { + if (api === 'float' || api === 'unfilterable-float') { + return wgsl === 'float' || wgsl === 'unfilterable-float'; + } + return api === wgsl; +} + +/** + * Returns true if the access modes are compatible. + */ +function doAccessesMatch(api: GPUStorageTextureAccess, wgsl: GPUStorageTextureAccess): boolean { + if (api === 'read-write') { + return wgsl === 'read-write' || wgsl === 'write-only'; + } + return api === wgsl; +} + +/** + * Returns true if the resources are compatible. + */ +export function doResourcesMatch(api: Resource, wgsl: Resource): boolean { + if (api.buffer) { + if (!wgsl.buffer) { + return false; + } + return api.buffer.type === wgsl.buffer.type; + } + if (api.sampler) { + if (!wgsl.sampler) { + return false; + } + return ( + api.sampler.type === wgsl.sampler.type || + (api.sampler.type !== 'comparison' && wgsl.sampler.type !== 'comparison') + ); + } + if (api.texture) { + if (!wgsl.texture) { + return false; + } + const aType = api.texture.sampleType as GPUTextureSampleType; + const wType = wgsl.texture.sampleType as GPUTextureSampleType; + return ( + doSampleTypesMatch(aType, wType) && + api.texture.viewDimension === wgsl.texture.viewDimension && + api.texture.multisampled === wgsl.texture.multisampled + ); + } + if (api.storageTexture) { + if (!wgsl.storageTexture) { + return false; + } + const aAccess = api.storageTexture.access as GPUStorageTextureAccess; + const wAccess = wgsl.storageTexture.access as GPUStorageTextureAccess; + return ( + doAccessesMatch(aAccess, wAccess) && + api.storageTexture.format === wgsl.storageTexture.format && + api.storageTexture.viewDimension === wgsl.storageTexture.viewDimension + ); + } + if (api.externalTexture) { + return wgsl.externalTexture !== undefined; + } + + return false; +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts index 7ee5b9f7c1..6e6802d95f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts @@ -152,11 +152,11 @@ export class ValidationTest extends GPUTest { } /** Return an arbitrarily-configured GPUTexture with the `STORAGE_BINDING` usage. */ - getStorageTexture(): GPUTexture { + getStorageTexture(format: GPUTextureFormat): GPUTexture { return this.trackForCleanup( this.device.createTexture({ size: { width: 16, height: 16, depthOrArrayLayers: 1 }, - format: 'rgba8unorm', + format, usage: GPUTextureUsage.STORAGE_BINDING, }) ); @@ -220,8 +220,10 @@ export class ValidationTest extends GPUTest { return this.getSampledTexture(1).createView(); case 'sampledTexMS': return this.getSampledTexture(4).createView(); - case 'storageTex': - return this.getStorageTexture().createView(); + case 'readonlyStorageTex': + case 'writeonlyStorageTex': + case 'readwriteStorageTex': + return this.getStorageTexture('r32float').createView(); } } @@ -255,10 +257,10 @@ export class ValidationTest extends GPUTest { } /** Return an arbitrarily-configured GPUTexture with the `STORAGE` usage from mismatched device. */ - getDeviceMismatchedStorageTexture(): GPUTexture { + getDeviceMismatchedStorageTexture(format: GPUTextureFormat): GPUTexture { return this.getDeviceMismatchedTexture({ size: { width: 4, height: 4, depthOrArrayLayers: 1 }, - format: 'rgba8unorm', + format, usage: GPUTextureUsage.STORAGE_BINDING, }); } @@ -289,8 +291,10 @@ export class ValidationTest extends GPUTest { return this.getDeviceMismatchedSampledTexture(1).createView(); case 'sampledTexMS': return this.getDeviceMismatchedSampledTexture(4).createView(); - case 'storageTex': - return this.getDeviceMismatchedStorageTexture().createView(); + case 'readonlyStorageTex': + case 'writeonlyStorageTex': + case 'readwriteStorageTex': + return this.getDeviceMismatchedStorageTexture('r32float').createView(); } } @@ -317,7 +321,8 @@ export class ValidationTest extends GPUTest { /** Return a GPURenderPipeline with default options and no-op vertex and fragment shaders. */ createNoOpRenderPipeline( - layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto' + layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto', + colorFormat: GPUTextureFormat = 'rgba8unorm' ): GPURenderPipeline { return this.device.createRenderPipeline({ layout, @@ -332,7 +337,7 @@ export class ValidationTest extends GPUTest { code: this.getNoOpShaderCode('FRAGMENT'), }), entryPoint: 'main', - targets: [{ format: 'rgba8unorm', writeMask: 0 }], + targets: [{ format: colorFormat, writeMask: 0 }], }, primitive: { topology: 'triangle-list' }, }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts index d65313c006..d26d93ca2e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts @@ -322,7 +322,9 @@ export type PerStageBindingLimitClass = | 'storageBuf' | 'sampler' | 'sampledTex' - | 'storageTex'; + | 'readonlyStorageTex' + | 'writeonlyStorageTex' + | 'readwriteStorageTex'; /** * Classes of `PerPipelineLayout` binding limits. Two bindings with the same class * count toward the same `PerPipelineLayout` limit(s) in the spec (if any). @@ -337,7 +339,9 @@ export type ValidBindableResource = | 'compareSamp' | 'sampledTex' | 'sampledTexMS' - | 'storageTex'; + | 'readonlyStorageTex' + | 'writeonlyStorageTex' + | 'readwriteStorageTex'; type ErrorBindableResource = 'errorBuf' | 'errorSamp' | 'errorTex'; /** @@ -353,7 +357,9 @@ export const kBindableResources = [ 'compareSamp', 'sampledTex', 'sampledTexMS', - 'storageTex', + 'readonlyStorageTex', + 'writeonlyStorageTex', + 'readwriteStorageTex', 'errorBuf', 'errorSamp', 'errorTex', @@ -376,11 +382,13 @@ export const kPerStageBindingLimits: { }; } = /* prettier-ignore */ { - 'uniformBuf': { class: 'uniformBuf', maxLimit: 'maxUniformBuffersPerShaderStage', }, - 'storageBuf': { class: 'storageBuf', maxLimit: 'maxStorageBuffersPerShaderStage', }, - 'sampler': { class: 'sampler', maxLimit: 'maxSamplersPerShaderStage', }, - 'sampledTex': { class: 'sampledTex', maxLimit: 'maxSampledTexturesPerShaderStage', }, - 'storageTex': { class: 'storageTex', maxLimit: 'maxStorageTexturesPerShaderStage', }, + 'uniformBuf': { class: 'uniformBuf', maxLimit: 'maxUniformBuffersPerShaderStage', }, + 'storageBuf': { class: 'storageBuf', maxLimit: 'maxStorageBuffersPerShaderStage', }, + 'sampler': { class: 'sampler', maxLimit: 'maxSamplersPerShaderStage', }, + 'sampledTex': { class: 'sampledTex', maxLimit: 'maxSampledTexturesPerShaderStage', }, + 'readonlyStorageTex': { class: 'readonlyStorageTex', maxLimit: 'maxStorageTexturesPerShaderStage', }, + 'writeonlyStorageTex': { class: 'writeonlyStorageTex', maxLimit: 'maxStorageTexturesPerShaderStage', }, + 'readwriteStorageTex': { class: 'readwriteStorageTex', maxLimit: 'maxStorageTexturesPerShaderStage', }, }; /** @@ -398,11 +406,13 @@ export const kPerPipelineBindingLimits: { }; } = /* prettier-ignore */ { - 'uniformBuf': { class: 'uniformBuf', maxDynamicLimit: 'maxDynamicUniformBuffersPerPipelineLayout', }, - 'storageBuf': { class: 'storageBuf', maxDynamicLimit: 'maxDynamicStorageBuffersPerPipelineLayout', }, - 'sampler': { class: 'sampler', maxDynamicLimit: '', }, - 'sampledTex': { class: 'sampledTex', maxDynamicLimit: '', }, - 'storageTex': { class: 'storageTex', maxDynamicLimit: '', }, + 'uniformBuf': { class: 'uniformBuf', maxDynamicLimit: 'maxDynamicUniformBuffersPerPipelineLayout', }, + 'storageBuf': { class: 'storageBuf', maxDynamicLimit: 'maxDynamicStorageBuffersPerPipelineLayout', }, + 'sampler': { class: 'sampler', maxDynamicLimit: '', }, + 'sampledTex': { class: 'sampledTex', maxDynamicLimit: '', }, + 'readonlyStorageTex': { class: 'readonlyStorageTex', maxDynamicLimit: '', }, + 'writeonlyStorageTex': { class: 'writeonlyStorageTex', maxDynamicLimit: '', }, + 'readwriteStorageTex': { class: 'readwriteStorageTex', maxDynamicLimit: '', }, }; interface BindingKindInfo { @@ -416,14 +426,16 @@ const kBindingKind: { readonly [k in ValidBindableResource]: BindingKindInfo; } = /* prettier-ignore */ { - uniformBuf: { resource: 'uniformBuf', perStageLimitClass: kPerStageBindingLimits.uniformBuf, perPipelineLimitClass: kPerPipelineBindingLimits.uniformBuf, }, - storageBuf: { resource: 'storageBuf', perStageLimitClass: kPerStageBindingLimits.storageBuf, perPipelineLimitClass: kPerPipelineBindingLimits.storageBuf, }, - filtSamp: { resource: 'filtSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, }, - nonFiltSamp: { resource: 'nonFiltSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, }, - compareSamp: { resource: 'compareSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, }, - sampledTex: { resource: 'sampledTex', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, }, - sampledTexMS: { resource: 'sampledTexMS', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, }, - storageTex: { resource: 'storageTex', perStageLimitClass: kPerStageBindingLimits.storageTex, perPipelineLimitClass: kPerPipelineBindingLimits.storageTex, }, + uniformBuf: { resource: 'uniformBuf', perStageLimitClass: kPerStageBindingLimits.uniformBuf, perPipelineLimitClass: kPerPipelineBindingLimits.uniformBuf, }, + storageBuf: { resource: 'storageBuf', perStageLimitClass: kPerStageBindingLimits.storageBuf, perPipelineLimitClass: kPerPipelineBindingLimits.storageBuf, }, + filtSamp: { resource: 'filtSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, }, + nonFiltSamp: { resource: 'nonFiltSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, }, + compareSamp: { resource: 'compareSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, }, + sampledTex: { resource: 'sampledTex', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, }, + sampledTexMS: { resource: 'sampledTexMS', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, }, + readonlyStorageTex: { resource: 'readonlyStorageTex', perStageLimitClass: kPerStageBindingLimits.readonlyStorageTex, perPipelineLimitClass: kPerPipelineBindingLimits.readonlyStorageTex, }, + writeonlyStorageTex: { resource: 'writeonlyStorageTex', perStageLimitClass: kPerStageBindingLimits.writeonlyStorageTex, perPipelineLimitClass: kPerPipelineBindingLimits.writeonlyStorageTex, }, + readwriteStorageTex: { resource: 'readwriteStorageTex', perStageLimitClass: kPerStageBindingLimits.readwriteStorageTex, perPipelineLimitClass: kPerPipelineBindingLimits.readwriteStorageTex, }, }; // Binding type info @@ -483,14 +495,30 @@ assertTypeTrue<TypeEqual<GPUTextureSampleType, (typeof kTextureSampleTypes)[numb /** Binding type info (including class limits) for the specified GPUStorageTextureBindingLayout. */ export function storageTextureBindingTypeInfo(d: GPUStorageTextureBindingLayout) { - return { - usage: GPUConst.TextureUsage.STORAGE_BINDING, - ...kBindingKind.storageTex, - ...kValidStagesStorageWrite, - }; + switch (d.access) { + case undefined: + case 'write-only': + return { + usage: GPUConst.TextureUsage.STORAGE_BINDING, + ...kBindingKind.writeonlyStorageTex, + ...kValidStagesStorageWrite, + }; + case 'read-only': + return { + usage: GPUConst.TextureUsage.STORAGE_BINDING, + ...kBindingKind.readonlyStorageTex, + ...kValidStagesAll, + }; + case 'read-write': + return { + usage: GPUConst.TextureUsage.STORAGE_BINDING, + ...kBindingKind.readwriteStorageTex, + ...kValidStagesStorageWrite, + }; + } } /** List of all GPUStorageTextureAccess values. */ -export const kStorageTextureAccessValues = ['write-only'] as const; +export const kStorageTextureAccessValues = ['read-only', 'read-write', 'write-only'] as const; assertTypeTrue<TypeEqual<GPUStorageTextureAccess, (typeof kStorageTextureAccessValues)[number]>>(); /** GPUBindGroupLayoutEntry, but only the "union" fields, not the common fields. */ @@ -539,8 +567,10 @@ export function samplerBindingEntries(includeUndefined: boolean): readonly BGLEn */ export function textureBindingEntries(includeUndefined: boolean): readonly BGLEntry[] { return [ - ...(includeUndefined ? [{ texture: { multisampled: undefined } }] : []), - { texture: { multisampled: false } }, + ...(includeUndefined + ? [{ texture: { multisampled: undefined, sampleType: 'unfilterable-float' } } as const] + : []), + { texture: { multisampled: false, sampleType: 'unfilterable-float' } }, { texture: { multisampled: true, sampleType: 'unfilterable-float' } }, ] as const; } @@ -549,19 +579,17 @@ export function textureBindingEntries(includeUndefined: boolean): readonly BGLEn * * Note: Generates different `access` options, but not `format` or `viewDimension` options. */ -export function storageTextureBindingEntries(format: GPUTextureFormat): readonly BGLEntry[] { - return [{ storageTexture: { access: 'write-only', format } }] as const; -} -/** Generate a list of possible texture-or-storageTexture-typed BGLEntry values. */ -export function sampledAndStorageBindingEntries( - includeUndefined: boolean, - storageTextureFormat: GPUTextureFormat = 'rgba8unorm' -): readonly BGLEntry[] { +export function storageTextureBindingEntries(): readonly BGLEntry[] { return [ - ...textureBindingEntries(includeUndefined), - ...storageTextureBindingEntries(storageTextureFormat), + { storageTexture: { access: 'write-only', format: 'r32float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + { storageTexture: { access: 'read-write', format: 'r32float' } }, ] as const; } +/** Generate a list of possible texture-or-storageTexture-typed BGLEntry values. */ +export function sampledAndStorageBindingEntries(includeUndefined: boolean): readonly BGLEntry[] { + return [...textureBindingEntries(includeUndefined), ...storageTextureBindingEntries()] as const; +} /** * Generate a list of possible BGLEntry values of every type, but not variants with different: * - buffer.hasDynamicOffset @@ -569,14 +597,11 @@ export function sampledAndStorageBindingEntries( * - texture.viewDimension * - storageTexture.viewDimension */ -export function allBindingEntries( - includeUndefined: boolean, - storageTextureFormat: GPUTextureFormat = 'rgba8unorm' -): readonly BGLEntry[] { +export function allBindingEntries(includeUndefined: boolean): readonly BGLEntry[] { return [ ...bufferBindingEntries(includeUndefined), ...samplerBindingEntries(includeUndefined), - ...sampledAndStorageBindingEntries(includeUndefined, storageTextureFormat), + ...sampledAndStorageBindingEntries(includeUndefined), ] as const; } @@ -689,7 +714,7 @@ const [kLimitInfoKeys, kLimitInfoDefaults, kLimitInfoData] = 'maxVertexAttributes': [ , 16, 16, ], 'maxVertexBufferArrayStride': [ , 2048, 2048, ], 'maxInterStageShaderComponents': [ , 60, 60, ], - 'maxInterStageShaderVariables': [ , 16, 16, ], + 'maxInterStageShaderVariables': [ , 16, 15, ], 'maxColorAttachments': [ , 8, 4, ], 'maxColorAttachmentBytesPerSample': [ , 32, 32, ], @@ -790,3 +815,13 @@ export const kFeatureNameInfo: { }; /** List of all GPUFeatureName values. */ export const kFeatureNames = keysOf(kFeatureNameInfo); + +/** List of all known WGSL language features */ +export const kKnownWGSLLanguageFeatures = [ + 'readonly_and_readwrite_storage_textures', + 'packed_4x8_integer_dot_product', + 'unrestricted_pointer_parameters', + 'pointer_composite_access', +] as const; + +export type WGSLLanguageFeature = (typeof kKnownWGSLLanguageFeatures)[number]; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/createBindGroup.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/createBindGroup.spec.ts new file mode 100644 index 0000000000..b48fa80422 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/createBindGroup.spec.ts @@ -0,0 +1,178 @@ +export const description = ` +Tests that, in compat mode, the dimension of a view is compatible with a texture's textureBindingViewDimension. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { kTextureDimensions, kTextureViewDimensions } from '../../../capability_info.js'; +import { + effectiveViewDimensionForTexture, + getTextureDimensionFromView, +} from '../../../util/texture/base.js'; +import { CompatibilityTest } from '../../compatibility_test.js'; + +export const g = makeTestGroup(CompatibilityTest); + +function isTextureBindingViewDimensionCompatibleWithDimension( + dimension: GPUTextureDimension = '2d', + textureBindingViewDimension: GPUTextureViewDimension = '2d' +) { + return getTextureDimensionFromView(textureBindingViewDimension) === dimension; +} + +function isValidViewDimensionForDimension( + dimension: GPUTextureDimension | undefined, + depthOrArrayLayers: number, + viewDimension: GPUTextureViewDimension | undefined +) { + if (viewDimension === undefined) { + return true; + } + + switch (dimension) { + case '1d': + return viewDimension === '1d'; + case '2d': + case undefined: + switch (viewDimension) { + case undefined: + case '2d': + case '2d-array': + return true; + case 'cube': + return depthOrArrayLayers === 6; + case 'cube-array': + return depthOrArrayLayers % 6 === 0; + default: + return false; + } + break; + case '3d': + return viewDimension === '3d'; + } +} + +function isValidDimensionForDepthOrArrayLayers( + dimension: GPUTextureDimension | undefined, + depthOrArrayLayers: number +) { + switch (dimension) { + case '1d': + return depthOrArrayLayers === 1; + default: + return true; + } +} + +function isValidViewDimensionForDepthOrArrayLayers( + viewDimension: GPUTextureViewDimension | undefined, + depthOrArrayLayers: number +) { + switch (viewDimension) { + case '2d': + return depthOrArrayLayers === 1; + case 'cube': + return depthOrArrayLayers === 6; + case 'cube-array': + return depthOrArrayLayers % 6 === 0; + default: + return true; + } + return viewDimension === 'cube'; +} + +function getEffectiveTextureBindingViewDimension( + dimension: GPUTextureDimension | undefined, + depthOrArrayLayers: number, + textureBindingViewDimension: GPUTextureViewDimension | undefined +) { + if (textureBindingViewDimension) { + return textureBindingViewDimension; + } + + switch (dimension) { + case '1d': + return '1d'; + case '2d': + case undefined: + return depthOrArrayLayers > 1 ? '2d-array' : '2d'; + break; + case '3d': + return '3d'; + } +} + +g.test('viewDimension_matches_textureBindingViewDimension') + .desc( + ` + Tests that, in compat mode, the dimension of a view is compatible with a texture's textureBindingViewDimension + when used as a TEXTURE_BINDING. + ` + ) + .params(u => + u // + .combine('dimension', [...kTextureDimensions, undefined]) + .combine('textureBindingViewDimension', [...kTextureViewDimensions, undefined]) + .combine('viewDimension', [...kTextureViewDimensions, undefined]) + .combine('depthOrArrayLayers', [1, 2, 6]) + .filter( + ({ dimension, textureBindingViewDimension, depthOrArrayLayers, viewDimension }) => + textureBindingViewDimension !== 'cube-array' && + viewDimension !== 'cube-array' && + isTextureBindingViewDimensionCompatibleWithDimension( + dimension, + textureBindingViewDimension + ) && + isValidViewDimensionForDimension(dimension, depthOrArrayLayers, viewDimension) && + isValidViewDimensionForDepthOrArrayLayers( + textureBindingViewDimension, + depthOrArrayLayers + ) && + isValidDimensionForDepthOrArrayLayers(dimension, depthOrArrayLayers) + ) + ) + .fn(t => { + const { dimension, textureBindingViewDimension, viewDimension, depthOrArrayLayers } = t.params; + + const texture = t.device.createTexture({ + size: [1, 1, depthOrArrayLayers], + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING, + ...(dimension && { dimension }), + ...(textureBindingViewDimension && { textureBindingViewDimension }), + } as GPUTextureDescriptor); // MAINTENANCE_TODO: remove cast once textureBindingViewDimension is added to IDL + t.trackForCleanup(texture); + + const effectiveTextureBindingViewDimension = getEffectiveTextureBindingViewDimension( + dimension, + texture.depthOrArrayLayers, + textureBindingViewDimension + ); + + const effectiveViewDimension = getEffectiveTextureBindingViewDimension( + dimension, + texture.depthOrArrayLayers, + viewDimension + ); + + const layout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + texture: { + viewDimension: effectiveViewDimensionForTexture(texture, viewDimension), + }, + }, + ], + }); + + const resource = texture.createView({ dimension: viewDimension }); + const shouldError = effectiveTextureBindingViewDimension !== effectiveViewDimension; + + t.expectValidationError(() => { + t.device.createBindGroup({ + layout, + entries: [{ binding: 0, resource }], + }); + }, shouldError); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/createBindGroupLayout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/createBindGroupLayout.spec.ts new file mode 100644 index 0000000000..f05af2860e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/createBindGroupLayout.spec.ts @@ -0,0 +1,34 @@ +export const description = ` +Tests that, in compat mode, you can not create a bind group layout with unsupported storage texture formats. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { kCompatModeUnsupportedStorageTextureFormats } from '../../../format_info.js'; +import { CompatibilityTest } from '../../compatibility_test.js'; + +export const g = makeTestGroup(CompatibilityTest); + +g.test('unsupportedStorageTextureFormats') + .desc( + ` + Tests that, in compat mode, you can not create a bind group layout with unsupported storage texture formats. + ` + ) + .params(u => u.combine('format', kCompatModeUnsupportedStorageTextureFormats)) + .fn(t => { + const { format } = t.params; + + t.expectValidationError(() => { + t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + storageTexture: { + format, + }, + }, + ], + }); + }, true); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/encoding/cmds/copyTextureToBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/encoding/cmds/copyTextureToBuffer.spec.ts index a9af7795b3..e7d5164c45 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/encoding/cmds/copyTextureToBuffer.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/encoding/cmds/copyTextureToBuffer.spec.ts @@ -19,16 +19,17 @@ g.test('compressed') .fn(t => { const { format } = t.params; - const { blockWidth, blockHeight, bytesPerBlock } = kTextureFormatInfo[format]; + const info = kTextureFormatInfo[format]; + const textureSize = [info.blockWidth, info.blockHeight, 1]; const texture = t.device.createTexture({ - size: [blockWidth, blockHeight, 1], + size: textureSize, format, usage: GPUTextureUsage.COPY_SRC, }); t.trackForCleanup(texture); - const bytesPerRow = align(bytesPerBlock, 256); + const bytesPerRow = align(info.color.bytes, 256); const buffer = t.device.createBuffer({ size: bytesPerRow, @@ -37,7 +38,7 @@ g.test('compressed') t.trackForCleanup(buffer); const encoder = t.device.createCommandEncoder(); - encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [blockWidth, blockHeight, 1]); + encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, textureSize); t.expectGPUError('validation', () => { encoder.finish(); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/encoding/cmds/copyTextureToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/encoding/cmds/copyTextureToTexture.spec.ts new file mode 100644 index 0000000000..cd551f9b63 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/encoding/cmds/copyTextureToTexture.spec.ts @@ -0,0 +1,94 @@ +export const description = ` +Tests limitations of copyTextureToTextures in compat mode. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { + kAllTextureFormats, + kCompressedTextureFormats, + kTextureFormatInfo, +} from '../../../../../format_info.js'; +import { CompatibilityTest } from '../../../../compatibility_test.js'; + +export const g = makeTestGroup(CompatibilityTest); + +g.test('compressed') + .desc(`Tests that you can not call copyTextureToTexture with compressed textures in compat mode.`) + .params(u => u.combine('format', kCompressedTextureFormats)) + .beforeAllSubcases(t => { + const { format } = t.params; + t.selectDeviceOrSkipTestCase([kTextureFormatInfo[format].feature]); + }) + .fn(t => { + const { format } = t.params; + + const { blockWidth, blockHeight } = kTextureFormatInfo[format]; + + const srcTexture = t.device.createTexture({ + size: [blockWidth, blockHeight, 1], + format, + usage: GPUTextureUsage.COPY_SRC, + }); + t.trackForCleanup(srcTexture); + + const dstTexture = t.device.createTexture({ + size: [blockWidth, blockHeight, 1], + format, + usage: GPUTextureUsage.COPY_DST, + }); + t.trackForCleanup(dstTexture); + + const encoder = t.device.createCommandEncoder(); + encoder.copyTextureToTexture({ texture: srcTexture }, { texture: dstTexture }, [ + blockWidth, + blockHeight, + 1, + ]); + t.expectGPUError('validation', () => { + encoder.finish(); + }); + }); + +g.test('multisample') + .desc(`Test that you can not call copyTextureToTexture with multisample textures in compat mode.`) + .params(u => + u + .beginSubcases() + .combine('format', kAllTextureFormats) + .filter(({ format }) => { + const info = kTextureFormatInfo[format]; + return info.multisample && !info.feature; + }) + ) + .fn(t => { + const { format } = t.params; + const { blockWidth, blockHeight } = kTextureFormatInfo[format]; + + t.skipIfTextureFormatNotSupported(format as GPUTextureFormat); + + const srcTexture = t.device.createTexture({ + size: [blockWidth, blockHeight, 1], + format, + sampleCount: 4, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + t.trackForCleanup(srcTexture); + + const dstTexture = t.device.createTexture({ + size: [blockWidth, blockHeight, 1], + format, + sampleCount: 4, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + }); + t.trackForCleanup(dstTexture); + + const encoder = t.device.createCommandEncoder(); + encoder.copyTextureToTexture({ texture: srcTexture }, { texture: dstTexture }, [ + blockWidth, + blockHeight, + 1, + ]); + t.expectGPUError('validation', () => { + encoder.finish(); + }); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/render_pipeline/depth_stencil_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/render_pipeline/depth_stencil_state.spec.ts new file mode 100644 index 0000000000..66045c2b8a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/render_pipeline/depth_stencil_state.spec.ts @@ -0,0 +1,53 @@ +export const description = ` +Tests that depthBiasClamp must be zero in compat mode. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { CompatibilityTest } from '../../../compatibility_test.js'; + +export const g = makeTestGroup(CompatibilityTest); + +g.test('depthBiasClamp') + .desc('Tests that depthBiasClamp must be zero in compat mode.') + .params(u => + u // + .combine('depthBiasClamp', [undefined, 0, 0.1, 1]) + .combine('async', [false, true] as const) + ) + .fn(t => { + const { depthBiasClamp, async } = t.params; + + const module = t.device.createShaderModule({ + code: ` + @vertex fn vs() -> @builtin(position) vec4f { + return vec4f(0); + } + + @fragment fn fs() -> @location(0) vec4f { + return vec4f(0); + } + `, + }); + + const pipelineDescriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + }, + fragment: { + module, + entryPoint: 'fs', + targets: [{ format: 'rgba8unorm' }], + }, + depthStencil: { + format: 'depth24plus', + depthWriteEnabled: true, + depthCompare: 'always', + ...(depthBiasClamp !== undefined && { depthBiasClamp }), + }, + }; + + const success = !depthBiasClamp; + t.doCreateRenderPipelineTest(async, success, pipelineDescriptor); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/render_pipeline/shader_module.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/render_pipeline/shader_module.spec.ts index abe2b063e7..56da7d355e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/render_pipeline/shader_module.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/render_pipeline/shader_module.spec.ts @@ -3,6 +3,7 @@ Tests limitations of createRenderPipeline related to shader modules in compat mo `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { kCompatModeUnsupportedStorageTextureFormats } from '../../../../format_info.js'; import { CompatibilityTest } from '../../../compatibility_test.js'; export const g = makeTestGroup(CompatibilityTest); @@ -72,3 +73,209 @@ Tests that you can not create a render pipeline with a shader module that uses s !isValid ); }); + +g.test('sample_index') + .desc( + ` +Tests that you can not create a render pipeline with a shader module that uses sample_index in compat mode. + +- Test that a pipeline with a shader that uses sample_index fails. +- Test that a pipeline that references a module that has a shader that uses sample_index + but the pipeline does not reference that shader succeeds. + ` + ) + .params(u => + u.combine('entryPoint', ['fsWithoutSampleIndexUsage', 'fsWithSampleIndexUsage'] as const) + ) + .fn(t => { + const { entryPoint } = t.params; + + const module = t.device.createShaderModule({ + code: ` + @vertex fn vs() -> @builtin(position) vec4f { + return vec4f(1); + } + @fragment fn fsWithoutSampleIndexUsage() -> @location(0) vec4f { + return vec4f(0); + } + @fragment fn fsWithSampleIndexUsage(@builtin(sample_index) sampleIndex: u32) -> @location(0) vec4f { + _ = sampleIndex; + return vec4f(0); + } + `, + }); + + const pipelineDescriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + }, + fragment: { + module, + entryPoint, + targets: [ + { + format: 'rgba8unorm', + }, + ], + }, + multisample: { + count: 4, + }, + }; + + const isValid = entryPoint === 'fsWithoutSampleIndexUsage'; + t.expectGPUError( + 'validation', + () => t.device.createRenderPipeline(pipelineDescriptor), + !isValid + ); + }); + +g.test('interpolate') + .desc( + ` +Tests that you can not create a render pipeline with a shader module that uses interpolate(linear) nor interpolate(...,sample) in compat mode. + +- Test that a pipeline with a shader that uses interpolate(linear) or interpolate(sample) fails. +- Test that a pipeline that references a module that has a shader that uses interpolate(linear/sample) + but the pipeline does not reference that shader succeeds. + ` + ) + .params(u => + u + .combine('interpolate', [ + '', + '@interpolate(linear)', + '@interpolate(linear, sample)', + '@interpolate(perspective, sample)', + ] as const) + .combine('entryPoint', [ + 'fsWithoutInterpolationUsage', + 'fsWithInterpolationUsage1', + 'fsWithInterpolationUsage2', + 'fsWithInterpolationUsage3', + ] as const) + ) + .fn(t => { + const { entryPoint, interpolate } = t.params; + + const module = t.device.createShaderModule({ + code: ` + struct Vertex { + @builtin(position) pos: vec4f, + @location(0) ${interpolate} color : vec4f, + }; + @vertex fn vs() -> Vertex { + var v: Vertex; + v.pos = vec4f(1); + v.color = vec4f(1); + return v; + } + @fragment fn fsWithoutInterpolationUsage() -> @location(0) vec4f { + return vec4f(1); + } + @fragment fn fsWithInterpolationUsage1(v: Vertex) -> @location(0) vec4f { + return vec4f(1); + } + @fragment fn fsWithInterpolationUsage2(v: Vertex) -> @location(0) vec4f { + return v.pos; + } + @fragment fn fsWithInterpolationUsage3(v: Vertex) -> @location(0) vec4f { + return v.color; + } + `, + }); + + const pipelineDescriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + }, + fragment: { + module, + entryPoint, + targets: [ + { + format: 'rgba8unorm', + }, + ], + }, + }; + + const isValid = entryPoint === 'fsWithoutInterpolationUsage' || interpolate === ''; + t.expectGPUError( + 'validation', + () => t.device.createRenderPipeline(pipelineDescriptor), + !isValid + ); + }); + +g.test('unsupportedStorageTextureFormats,computePipeline') + .desc( + ` +Tests that you can not create a compute pipeline with unsupported storage texture formats in compat mode. + ` + ) + .params(u => + u // + .combine('format', kCompatModeUnsupportedStorageTextureFormats) + .combine('async', [false, true] as const) + ) + .fn(t => { + const { format, async } = t.params; + + const module = t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var s: texture_storage_2d<${format}, read>; + @compute @workgroup_size(1) fn cs() { + _ = textureLoad(s, vec2u(0)); + } + `, + }); + + const pipelineDescriptor: GPUComputePipelineDescriptor = { + layout: 'auto', + compute: { + module, + entryPoint: 'cs', + }, + }; + t.doCreateComputePipelineTest(async, false, pipelineDescriptor); + }); + +g.test('unsupportedStorageTextureFormats,renderPipeline') + .desc( + ` +Tests that you can not create a render pipeline with unsupported storage texture formats in compat mode. + ` + ) + .params(u => + u // + .combine('format', kCompatModeUnsupportedStorageTextureFormats) + .combine('async', [false, true] as const) + ) + .fn(t => { + const { format, async } = t.params; + + const module = t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var s: texture_storage_2d<${format}, read>; + @vertex fn vs() -> @builtin(position) vec4f { + _ = textureLoad(s, vec2u(0)); + return vec4f(0); + } + `, + }); + + const pipelineDescriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + }, + }; + t.doCreateRenderPipelineTest(async, false, pipelineDescriptor); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/texture/createTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/texture/createTexture.spec.ts index 9f0d353268..58dcd41ec7 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/texture/createTexture.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/compat/api/validation/texture/createTexture.spec.ts @@ -1,8 +1,13 @@ +/* eslint-disable prettier/prettier */ export const description = ` Tests that you can not use bgra8unorm-srgb in compat mode. +Tests that textureBindingViewDimension must compatible with texture dimension `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { kTextureDimensions, kTextureViewDimensions } from '../../../../capability_info.js'; +import { kColorTextureFormats, kCompatModeUnsupportedStorageTextureFormats, kTextureFormatInfo } from '../../../../format_info.js'; +import { getTextureDimensionFromView } from '../../../../util/texture/base.js'; import { CompatibilityTest } from '../../../compatibility_test.js'; export const g = makeTestGroup(CompatibilityTest); @@ -39,3 +44,129 @@ g.test('unsupportedTextureViewFormats') true ); }); + +g.test('invalidTextureBindingViewDimension') + .desc( + `Tests that you can not specify a textureBindingViewDimension that is incompatible with the texture's dimension.` + ) + .params(u => + u // + .combine('dimension', kTextureDimensions) + .combine('textureBindingViewDimension', kTextureViewDimensions) + ) + .fn(t => { + const { dimension, textureBindingViewDimension } = t.params; + const depthOrArrayLayers = textureBindingViewDimension === '1d' || textureBindingViewDimension === '2d' ? 1 : 6; + const shouldError = getTextureDimensionFromView(textureBindingViewDimension) !== dimension; + t.expectGPUError( + 'validation', + () => { + const texture = t.device.createTexture({ + size: [1, 1, depthOrArrayLayers], + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING, + dimension, + textureBindingViewDimension, + } as GPUTextureDescriptor); // MAINTENANCE_TODO: remove cast once textureBindingViewDimension is added to IDL + t.trackForCleanup(texture); + }, + shouldError + ); + }); + +g.test('depthOrArrayLayers_incompatible_with_textureBindingViewDimension') + .desc( + `Tests + * if textureBindingViewDimension is '2d' then depthOrArrayLayers must be 1 + * if textureBindingViewDimension is 'cube' then depthOrArrayLayers must be 6 + ` + ) + .params(u => + u // + .combine('textureBindingViewDimension', ['2d', 'cube']) + .combine('depthOrArrayLayers', [1, 3, 6, 12]) + ) + .fn(t => { + const { textureBindingViewDimension, depthOrArrayLayers } = t.params; + const shouldError = + (textureBindingViewDimension === '2d' && depthOrArrayLayers !== 1) || + (textureBindingViewDimension === 'cube' && depthOrArrayLayers !== 6); + t.expectGPUError( + 'validation', + () => { + const texture = t.device.createTexture({ + size: [1, 1, depthOrArrayLayers], + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING, + textureBindingViewDimension, + } as GPUTextureDescriptor); // MAINTENANCE_TODO: remove cast once textureBindingViewDimension is added to IDL + t.trackForCleanup(texture); + }, + shouldError + ); + }); + +g.test('format_reinterpretation') + .desc( + ` + Tests that you can not request different view formats when creating a texture. + For example, rgba8unorm can not be viewed as rgba8unorm-srgb + ` + ) + .params(u => + u // + .combine('format', kColorTextureFormats as GPUTextureFormat[]) + .filter( + ({ format }) => + !!kTextureFormatInfo[format].baseFormat && + kTextureFormatInfo[format].baseFormat !== format + ) + ) + .beforeAllSubcases(t => { + const info = kTextureFormatInfo[t.params.format]; + t.skipIfTextureFormatNotSupported(t.params.format); + t.selectDeviceOrSkipTestCase(info.feature); + }) + .fn(t => { + const { format } = t.params; + const info = kTextureFormatInfo[format]; + + const formatPairs = [ + { format, viewFormats: [info.baseFormat!] }, + { format: info.baseFormat!, viewFormats: [format] }, + { format, viewFormats: [format, info.baseFormat!] }, + { format: info.baseFormat!, viewFormats: [format, info.baseFormat!] }, + ]; + for (const { format, viewFormats } of formatPairs) { + t.expectGPUError( + 'validation', + () => { + const texture = t.device.createTexture({ + size: [info.blockWidth, info.blockHeight], + format, + viewFormats, + usage: GPUTextureUsage.TEXTURE_BINDING, + }); + t.trackForCleanup(texture); + }, + true + ); + } + }); + +g.test('unsupportedStorageTextureFormats') + .desc(`Tests that you can not create unsupported storage texture formats in compat mode.`) + .params(u => u.combine('format', kCompatModeUnsupportedStorageTextureFormats)) + .fn(t => { + const { format } = t.params; + t.expectGPUError( + 'validation', + () => + t.device.createTexture({ + size: [1, 1, 1], + format, + usage: GPUTextureUsage.STORAGE_BINDING, + }), + true + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts index c7a28cb837..60cdd63674 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts @@ -60,3 +60,8 @@ export const GPUConst = { export const kMaxUnsignedLongValue = 4294967295; export const kMaxUnsignedLongLongValue = Number.MAX_SAFE_INTEGER; + +export const kInterpolationSampling = ['center', 'centroid', 'sample'] as const; +export const kInterpolationType = ['perspective', 'linear', 'flat'] as const; +export type InterpolationType = (typeof kInterpolationType)[number]; +export type InterpolationSampling = (typeof kInterpolationSampling)[number]; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/format_info.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/format_info.ts index 566027714f..be1320d3cd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/format_info.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/format_info.ts @@ -1,5 +1,5 @@ import { keysOf } from '../common/util/data_tables.js'; -import { assert } from '../common/util/util.js'; +import { assert, unreachable } from '../common/util/util.js'; import { align } from './util/math.js'; import { ImageCopyType } from './util/texture/layout.js'; @@ -13,25 +13,26 @@ import { ImageCopyType } from './util/texture/layout.js'; * `formatTableWithDefaults`. This ensures keys are never missing, always explicitly `undefined`. * * All top-level keys must be defined here, or they won't be exposed at all. + * Documentation is also written here; this makes it propagate through to the end types. */ const kFormatUniversalDefaults = { + /** Texel block width. */ blockWidth: undefined, + /** Texel block height. */ blockHeight: undefined, color: undefined, depth: undefined, stencil: undefined, colorRender: undefined, + /** Whether the format can be used in a multisample texture. */ multisample: undefined, + /** Optional feature required to use this format, or `undefined` if none. */ feature: undefined, + /** The base format for srgb formats. Specified on both srgb and equivalent non-srgb formats. */ baseFormat: undefined, - sampleType: undefined, - copySrc: undefined, - copyDst: undefined, + /** @deprecated Use `.color.bytes`, `.depth.bytes`, or `.stencil.bytes`. */ bytesPerBlock: undefined, - renderable: false, - renderTargetPixelByteCost: undefined, - renderTargetComponentAlignment: undefined, // IMPORTANT: // Add new top-level keys both here and in TextureFormatInfo_TypeCheck. @@ -67,382 +68,506 @@ function formatTableWithDefaults<Defaults extends {}, Table extends { readonly [ /** "plain color formats", plus rgb9e5ufloat. */ const kRegularTextureFormatInfo = formatTableWithDefaults({ - defaults: { blockWidth: 1, blockHeight: 1, copySrc: true, copyDst: true }, + defaults: { blockWidth: 1, blockHeight: 1 }, table: { // plain, 8 bits per component r8unorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 1 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 1, + }, colorRender: { blend: true, resolve: true, byteCost: 1, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, r8snorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 1 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 1, + }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, r8uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 1 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 1, + }, colorRender: { blend: false, resolve: false, byteCost: 1, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, r8sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: false, bytes: 1 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 1, + }, colorRender: { blend: false, resolve: false, byteCost: 1, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg8unorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 2 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, colorRender: { blend: true, resolve: true, byteCost: 2, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg8snorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 2 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg8uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 2 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, colorRender: { blend: false, resolve: false, byteCost: 2, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg8sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: false, bytes: 2 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, colorRender: { blend: false, resolve: false, byteCost: 2, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba8unorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: true, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: true, resolve: true, byteCost: 8, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, baseFormat: 'rgba8unorm', - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'rgba8unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: true, resolve: true, byteCost: 8, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, baseFormat: 'rgba8unorm', - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba8snorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: true, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 4, + }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba8uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: true, bytes: 4 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 4, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba8sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: true, bytes: 4 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 4, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, bgra8unorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: true, resolve: true, byteCost: 8, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, baseFormat: 'bgra8unorm', - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bgra8unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: true, resolve: true, byteCost: 8, alignment: 1 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, baseFormat: 'bgra8unorm', - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, // plain, 16 bits per component r16uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 2 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, colorRender: { blend: false, resolve: false, byteCost: 2, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, r16sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: false, bytes: 2 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, colorRender: { blend: false, resolve: false, byteCost: 2, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, r16float: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 2 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, colorRender: { blend: true, resolve: true, byteCost: 2, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg16uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 4, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg16sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 4, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg16float: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: true, resolve: true, byteCost: 4, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba16uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: true, bytes: 8 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 8, + }, colorRender: { blend: false, resolve: false, byteCost: 8, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba16sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: true, bytes: 8 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 8, + }, colorRender: { blend: false, resolve: false, byteCost: 8, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba16float: { - color: { type: 'float', copySrc: true, copyDst: true, storage: true, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 8, + }, colorRender: { blend: true, resolve: true, byteCost: 8, alignment: 2 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, // plain, 32 bits per component r32uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: true, bytes: 4 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: true, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 4, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, r32sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: true, bytes: 4 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: true, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 4, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, r32float: { - color: { type: 'unfilterable-float', copySrc: true, copyDst: true, storage: true, bytes: 4 }, + color: { + type: 'unfilterable-float', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: true, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 4, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg32uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: true, bytes: 8 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 8, + }, colorRender: { blend: false, resolve: false, byteCost: 8, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg32sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: true, bytes: 8 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 8, + }, colorRender: { blend: false, resolve: false, byteCost: 8, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg32float: { - color: { type: 'unfilterable-float', copySrc: true, copyDst: true, storage: true, bytes: 8 }, + color: { + type: 'unfilterable-float', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 8, + }, colorRender: { blend: false, resolve: false, byteCost: 8, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba32uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: true, bytes: 16 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 16, + }, colorRender: { blend: false, resolve: false, byteCost: 16, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba32sint: { - color: { type: 'sint', copySrc: true, copyDst: true, storage: true, bytes: 16 }, + color: { + type: 'sint', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 16, + }, colorRender: { blend: false, resolve: false, byteCost: 16, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgba32float: { - color: { type: 'unfilterable-float', copySrc: true, copyDst: true, storage: true, bytes: 16 }, + color: { + type: 'unfilterable-float', + copySrc: true, + copyDst: true, + storage: true, + readWriteStorage: false, + bytes: 16, + }, colorRender: { blend: false, resolve: false, byteCost: 16, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, // plain, mixed component width, 32 bits per texel rgb10a2uint: { - color: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: false, resolve: false, byteCost: 8, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rgb10a2unorm: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, colorRender: { blend: true, resolve: true, byteCost: 8, alignment: 4 }, - renderable: true, - /*prettier-ignore*/ get renderTargetComponentAlignment() { return this.colorRender.alignment; }, - /*prettier-ignore*/ get renderTargetPixelByteCost() { return this.colorRender.byteCost; }, multisample: true, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, rg11b10ufloat: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, - renderTargetPixelByteCost: 8, - renderTargetComponentAlignment: 4, }, // packed rgb9e5ufloat: { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 4 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 4, + }, multisample: false, - /*prettier-ignore*/ get sampleType() { return this.color.type; }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, }, @@ -452,24 +577,39 @@ const kRegularTextureFormatInfo = formatTableWithDefaults({ // because one aspect can be sized and one can be unsized. This should be cleaned up, but is kept // this way during a migration phase. const kSizedDepthStencilFormatInfo = formatTableWithDefaults({ - defaults: { blockWidth: 1, blockHeight: 1, multisample: true, copySrc: true, renderable: true }, + defaults: { blockWidth: 1, blockHeight: 1, multisample: true }, table: { stencil8: { - stencil: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 1 }, - sampleType: 'uint', - copyDst: true, + stencil: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 1, + }, bytesPerBlock: 1, }, depth16unorm: { - depth: { type: 'depth', copySrc: true, copyDst: true, storage: false, bytes: 2 }, - sampleType: 'depth', - copyDst: true, + depth: { + type: 'depth', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 2, + }, bytesPerBlock: 2, }, depth32float: { - depth: { type: 'depth', copySrc: true, copyDst: false, storage: false, bytes: 4 }, - sampleType: 'depth', - copyDst: false, + depth: { + type: 'depth', + copySrc: true, + copyDst: false, + storage: false, + readWriteStorage: false, + bytes: 4, + }, bytesPerBlock: 4, }, }, @@ -478,28 +618,51 @@ const kUnsizedDepthStencilFormatInfo = formatTableWithDefaults({ defaults: { blockWidth: 1, blockHeight: 1, multisample: true }, table: { depth24plus: { - depth: { type: 'depth', copySrc: false, copyDst: false, storage: false, bytes: undefined }, - copySrc: false, - copyDst: false, - sampleType: 'depth', - renderable: true, + depth: { + type: 'depth', + copySrc: false, + copyDst: false, + storage: false, + readWriteStorage: false, + bytes: undefined, + }, }, 'depth24plus-stencil8': { - depth: { type: 'depth', copySrc: false, copyDst: false, storage: false, bytes: undefined }, - stencil: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 1 }, - copySrc: false, - copyDst: false, - sampleType: 'depth', - renderable: true, + depth: { + type: 'depth', + copySrc: false, + copyDst: false, + storage: false, + readWriteStorage: false, + bytes: undefined, + }, + stencil: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 1, + }, }, 'depth32float-stencil8': { - depth: { type: 'depth', copySrc: true, copyDst: false, storage: false, bytes: 4 }, - stencil: { type: 'uint', copySrc: true, copyDst: true, storage: false, bytes: 1 }, + depth: { + type: 'depth', + copySrc: true, + copyDst: false, + storage: false, + readWriteStorage: false, + bytes: 4, + }, + stencil: { + type: 'uint', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 1, + }, feature: 'depth32float-stencil8', - copySrc: false, - copyDst: false, - sampleType: 'depth', - renderable: true, }, }, } as const); @@ -510,78 +673,173 @@ const kBCTextureFormatInfo = formatTableWithDefaults({ blockHeight: 4, multisample: false, feature: 'texture-compression-bc', - sampleType: 'float', - copySrc: true, - copyDst: true, }, table: { 'bc1-rgba-unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, baseFormat: 'bc1-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc1-rgba-unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, baseFormat: 'bc1-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc2-rgba-unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'bc2-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc2-rgba-unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'bc2-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc3-rgba-unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'bc3-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc3-rgba-unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'bc3-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc4-r-unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc4-r-snorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc5-rg-unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc5-rg-snorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc6h-rgb-ufloat': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc6h-rgb-float': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc7-rgba-unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'bc7-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'bc7-rgba-unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'bc7-rgba-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -594,59 +852,126 @@ const kETC2TextureFormatInfo = formatTableWithDefaults({ blockHeight: 4, multisample: false, feature: 'texture-compression-etc2', - sampleType: 'float', - copySrc: true, - copyDst: true, }, table: { 'etc2-rgb8unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, baseFormat: 'etc2-rgb8unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'etc2-rgb8unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, baseFormat: 'etc2-rgb8unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'etc2-rgb8a1unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, baseFormat: 'etc2-rgb8a1unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'etc2-rgb8a1unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, baseFormat: 'etc2-rgb8a1unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'etc2-rgba8unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'etc2-rgba8unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'etc2-rgba8unorm-srgb': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'etc2-rgba8unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'eac-r11unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'eac-r11snorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 8 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 8, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'eac-rg11unorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'eac-rg11snorm': { - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, }, @@ -656,22 +981,33 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ defaults: { multisample: false, feature: 'texture-compression-astc', - sampleType: 'float', - copySrc: true, - copyDst: true, }, table: { 'astc-4x4-unorm': { blockWidth: 4, blockHeight: 4, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-4x4-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-4x4-unorm-srgb': { blockWidth: 4, blockHeight: 4, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-4x4-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -679,14 +1015,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-5x4-unorm': { blockWidth: 5, blockHeight: 4, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-5x4-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-5x4-unorm-srgb': { blockWidth: 5, blockHeight: 4, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-5x4-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -694,14 +1044,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-5x5-unorm': { blockWidth: 5, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-5x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-5x5-unorm-srgb': { blockWidth: 5, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-5x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -709,14 +1073,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-6x5-unorm': { blockWidth: 6, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-6x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-6x5-unorm-srgb': { blockWidth: 6, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-6x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -724,14 +1102,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-6x6-unorm': { blockWidth: 6, blockHeight: 6, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-6x6-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-6x6-unorm-srgb': { blockWidth: 6, blockHeight: 6, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-6x6-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -739,14 +1131,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-8x5-unorm': { blockWidth: 8, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-8x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-8x5-unorm-srgb': { blockWidth: 8, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-8x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -754,14 +1160,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-8x6-unorm': { blockWidth: 8, blockHeight: 6, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-8x6-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-8x6-unorm-srgb': { blockWidth: 8, blockHeight: 6, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-8x6-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -769,14 +1189,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-8x8-unorm': { blockWidth: 8, blockHeight: 8, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-8x8-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-8x8-unorm-srgb': { blockWidth: 8, blockHeight: 8, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-8x8-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -784,14 +1218,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-10x5-unorm': { blockWidth: 10, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-10x5-unorm-srgb': { blockWidth: 10, blockHeight: 5, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x5-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -799,14 +1247,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-10x6-unorm': { blockWidth: 10, blockHeight: 6, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x6-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-10x6-unorm-srgb': { blockWidth: 10, blockHeight: 6, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x6-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -814,14 +1276,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-10x8-unorm': { blockWidth: 10, blockHeight: 8, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x8-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-10x8-unorm-srgb': { blockWidth: 10, blockHeight: 8, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x8-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -829,14 +1305,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-10x10-unorm': { blockWidth: 10, blockHeight: 10, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x10-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-10x10-unorm-srgb': { blockWidth: 10, blockHeight: 10, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-10x10-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -844,14 +1334,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-12x10-unorm': { blockWidth: 12, blockHeight: 10, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-12x10-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-12x10-unorm-srgb': { blockWidth: 12, blockHeight: 10, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-12x10-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -859,14 +1363,28 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ 'astc-12x12-unorm': { blockWidth: 12, blockHeight: 12, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-12x12-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, 'astc-12x12-unorm-srgb': { blockWidth: 12, blockHeight: 12, - color: { type: 'float', copySrc: true, copyDst: true, storage: false, bytes: 16 }, + color: { + type: 'float', + copySrc: true, + copyDst: true, + storage: false, + readWriteStorage: false, + bytes: 16, + }, baseFormat: 'astc-12x12-unorm', /*prettier-ignore*/ get bytesPerBlock() { return this.color.bytes; }, }, @@ -921,13 +1439,6 @@ const kASTCTextureFormatInfo = formatTableWithDefaults({ export const kRenderableColorTextureFormats = kRegularTextureFormats.filter( v => kColorTextureFormatInfo[v].colorRender ); -assert( - kRenderableColorTextureFormats.every( - f => - kAllTextureFormatInfo[f].renderTargetComponentAlignment !== undefined && - kAllTextureFormatInfo[f].renderTargetPixelByteCost !== undefined - ) -); /** Per-GPUTextureFormat-per-aspect info. */ interface TextureFormatAspectInfo { @@ -937,6 +1448,8 @@ interface TextureFormatAspectInfo { copyDst: boolean; /** Whether the aspect can be used as `STORAGE`. */ storage: boolean; + /** Whether the aspect can be used as `STORAGE` with `read-write` storage texture access. */ + readWriteStorage: boolean; /** The "texel block copy footprint" of one texel block; `undefined` if the aspect is unsized. */ bytes: number | undefined; } @@ -962,34 +1475,15 @@ interface TextureFormatStencilAspectInfo extends TextureFormatAspectInfo { * This is not actually the type of values in kTextureFormatInfo; that type is fully const * so that it can be narrowed very precisely at usage sites by the compiler. * This type exists only as a type check on the inferred type of kTextureFormatInfo. - * Documentation is also written here, but not actually visible to the IDE. */ type TextureFormatInfo_TypeCheck = { - /** Texel block width. */ blockWidth: number; - /** Texel block height. */ blockHeight: number; - /** Whether the format can be used in a multisample texture. */ multisample: boolean; - /** The base format for srgb formats. Specified on both srgb and equivalent non-srgb formats. */ baseFormat: GPUTextureFormat | undefined; - /** Optional feature required to use this format, or `undefined` if none. */ feature: GPUFeatureName | undefined; - /** @deprecated */ - sampleType: GPUTextureSampleType; - /** @deprecated */ - copySrc: boolean; - /** @deprecated */ - copyDst: boolean; - /** @deprecated */ bytesPerBlock: number | undefined; - /** @deprecated */ - renderable: boolean; - /** @deprecated */ - renderTargetPixelByteCost: number | undefined; - /** @deprecated */ - renderTargetComponentAlignment: number | undefined; // IMPORTANT: // Add new top-level keys both here and in kUniversalDefaults. @@ -1043,10 +1537,6 @@ const kTextureFormatInfo_TypeCheck: { readonly [F in GPUTextureFormat]: TextureFormatInfo_TypeCheck; } = kTextureFormatInfo; -/** List of all GPUTextureFormat values. */ -// MAINTENANCE_TODO: dedup with kAllTextureFormats -export const kTextureFormats: readonly GPUTextureFormat[] = keysOf(kAllTextureFormatInfo); - /** Valid GPUTextureFormats for `copyExternalImageToTexture`, by spec. */ export const kValidTextureFormatsForCopyE2T = [ 'r8unorm', @@ -1170,6 +1660,35 @@ export function resolvePerAspectFormat( } /** + * @returns the sample type of the specified aspect of the specified format. + */ +export function sampleTypeForFormatAndAspect( + format: GPUTextureFormat, + aspect: GPUTextureAspect +): 'uint' | 'depth' | 'float' | 'sint' | 'unfilterable-float' { + const info = kTextureFormatInfo[format]; + if (info.color) { + assert(aspect === 'all', `color format ${format} used with aspect ${aspect}`); + return info.color.type; + } else if (info.depth && info.stencil) { + if (aspect === 'depth-only') { + return info.depth.type; + } else if (aspect === 'stencil-only') { + return info.stencil.type; + } else { + unreachable(`depth-stencil format ${format} used with aspect ${aspect}`); + } + } else if (info.depth) { + assert(aspect !== 'stencil-only', `depth-only format ${format} used with aspect ${aspect}`); + return info.depth.type; + } else if (info.stencil) { + assert(aspect !== 'depth-only', `stencil-only format ${format} used with aspect ${aspect}`); + return info.stencil.type; + } + unreachable(); +} + +/** * Gets all copyable aspects for copies between texture and buffer for specified depth/stencil format and copy type, by spec. */ export function depthStencilFormatCopyableAspects( @@ -1229,8 +1748,12 @@ export function textureDimensionAndFormatCompatible( * * This function may need to be generalized to use `baseFormat` from `kTextureFormatInfo`. */ -export function viewCompatible(a: GPUTextureFormat, b: GPUTextureFormat): boolean { - return a === b || a + '-srgb' === b || b + '-srgb' === a; +export function viewCompatible( + compatibilityMode: boolean, + a: GPUTextureFormat, + b: GPUTextureFormat +): boolean { + return compatibilityMode ? a === b : a === b || a + '-srgb' === b || b + '-srgb' === a; } export function getFeaturesForFormats<T>( @@ -1250,7 +1773,29 @@ export function isCompressedTextureFormat(format: GPUTextureFormat) { return format in kCompressedTextureFormatInfo; } -export const kFeaturesForFormats = getFeaturesForFormats(kTextureFormats); +export const kCompatModeUnsupportedStorageTextureFormats: readonly GPUTextureFormat[] = [ + 'rg32float', + 'rg32sint', + 'rg32uint', +] as const; + +export function isTextureFormatUsableAsStorageFormat( + format: GPUTextureFormat, + isCompatibilityMode: boolean +) { + if (isCompatibilityMode) { + if (kCompatModeUnsupportedStorageTextureFormats.indexOf(format) >= 0) { + return false; + } + } + return !!kTextureFormatInfo[format].color?.storage; +} + +export function isRegularTextureFormat(format: GPUTextureFormat) { + return format in kRegularTextureFormatInfo; +} + +export const kFeaturesForFormats = getFeaturesForFormats(kAllTextureFormats); /** * Given an array of texture formats return the number of bytes per sample. diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts index f9ef1f2f06..fab1844c3c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts @@ -9,6 +9,7 @@ import { 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, @@ -20,7 +21,13 @@ import { unreachable, } from '../common/util/util.js'; -import { getDefaultLimits, kLimits, kQueryTypeInfo } from './capability_info.js'; +import { + getDefaultLimits, + kLimits, + kQueryTypeInfo, + WGSLLanguageFeature, +} from './capability_info.js'; +import { InterpolationType, InterpolationSampling } from './constants.js'; import { kTextureFormatInfo, kEncodableTextureFormats, @@ -29,6 +36,7 @@ import { EncodableTextureFormat, isCompressedTextureFormat, ColorTextureFormat, + isTextureFormatUsableAsStorageFormat, } from './format_info.js'; import { makeBufferWithContents } from './util/buffer.js'; import { checkElementsEqual, checkElementsBetween } from './util/check_contents.js'; @@ -52,7 +60,7 @@ import { textureContentIsOKByT2B, } from './util/texture/texture_ok.js'; import { createTextureFromTexelView, createTextureFromTexelViews } from './util/texture.js'; -import { reifyOrigin3D } from './util/unions.js'; +import { reifyExtent3D, reifyOrigin3D } from './util/unions.js'; const devicePool = new DevicePool(); @@ -245,6 +253,56 @@ export class GPUTestSubcaseBatchState extends SubcaseBatchState { } } } + + 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 test if the given interpolation type or sampling is not supported. + */ + 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' + ); + } + } + + /** 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 iff the `langFeature` is supported */ + hasLanguageFeature(langFeature: WGSLLanguageFeature) { + const lf = getGPU(this.recorder).wgslLanguageFeatures; + return lf !== undefined && lf.has(langFeature); + } } /** @@ -420,6 +478,26 @@ export class GPUTestBase extends Fixture<GPUTestSubcaseBatchState> { } } + /** 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 iff the `langFeature` is supported */ + hasLanguageFeature(langFeature: WGSLLanguageFeature) { + const lf = getGPU(this.rec).wgslLanguageFeatures; + return lf !== undefined && lf.has(langFeature); + } + /** * Expect a GPUBuffer's contents to pass the provided check. * @@ -730,7 +808,8 @@ export class GPUTestBase extends Fixture<GPUTestSubcaseBatchState> { slice = 0, layout, generateWarningOnly = false, - checkElementsBetweenFn = (act, [a, b]) => checkElementsBetween(act, [i => a[i], i => b[i]]), + checkElementsBetweenFn = (act, [a, b]) => + checkElementsBetween(act, [i => a[i] as number, i => b[i] as number]), }: { exp: [TypedArrayBufferView, TypedArrayBufferView]; slice?: number; @@ -757,24 +836,32 @@ export class GPUTestBase extends Fixture<GPUTestSubcaseBatchState> { /** * Emulate a texture to buffer copy by using a compute shader - * to load texture value of a single pixel and write to a storage buffer. - * For sample count == 1, the buffer contains only one value of the sample. - * For sample count > 1, the buffer contains (N = sampleCount) values sorted + * to load texture values of a subregion of a 2d texture and write to a storage buffer. + * For sample count == 1, the buffer contains extent[0] * extent[1] of the sample. + * For sample count > 1, the buffer contains extent[0] * extent[1] * (N = sampleCount) values sorted * in the order of their sample index [0, sampleCount - 1] * * This can be useful when the texture to buffer copy is not available to the texture format * e.g. (depth24plus), or when the texture is multisampled. * - * MAINTENANCE_TODO: extend to read multiple pixels with given origin and size. + * MAINTENANCE_TODO: extend texture dimension to 1d and 3d. * * @returns storage buffer containing the copied value from the texture. */ - copySinglePixelTextureToBufferUsingComputePass( + copy2DTextureToBufferUsingComputePass( type: ScalarType, componentCount: number, textureView: GPUTextureView, - sampleCount: number + sampleCount: number = 1, + extent_: GPUExtent3D = [1, 1, 1], + origin_: GPUOrigin3D = [0, 0, 0] ): GPUBuffer { + const origin = reifyOrigin3D(origin_); + const extent = reifyExtent3D(extent_); + const width = extent.width; + const height = extent.height; + const kWorkgroupSizeX = 8; + const kWorkgroupSizeY = 8; const textureSrcCode = sampleCount === 1 ? `@group(0) @binding(0) var src: texture_2d<${type}>;` @@ -787,13 +874,24 @@ export class GPUTestBase extends Fixture<GPUTestSubcaseBatchState> { ${textureSrcCode} @group(0) @binding(1) var<storage, read_write> dst : Buffer; - @compute @workgroup_size(1) fn main() { - var coord = vec2<i32>(0, 0); - for (var sampleIndex = 0; sampleIndex < ${sampleCount}; + struct Params { + origin: vec2u, + extent: vec2u, + }; + @group(0) @binding(2) var<uniform> params : Params; + + @compute @workgroup_size(${kWorkgroupSizeX}, ${kWorkgroupSizeY}, 1) fn main(@builtin(global_invocation_id) id : vec3u) { + let boundary = params.origin + params.extent; + let coord = params.origin + id.xy; + if (any(coord >= boundary)) { + return; + } + let offset = (id.x + id.y * params.extent.x) * ${componentCount} * ${sampleCount}; + for (var sampleIndex = 0u; sampleIndex < ${sampleCount}; sampleIndex = sampleIndex + 1) { - let o = sampleIndex * ${componentCount}; - let v = textureLoad(src, coord, sampleIndex); - for (var component = 0; component < ${componentCount}; component = component + 1) { + let o = offset + sampleIndex * ${componentCount}; + let v = textureLoad(src, coord.xy, sampleIndex); + for (var component = 0u; component < ${componentCount}; component = component + 1) { dst.data[o + component] = v[component]; } } @@ -810,11 +908,16 @@ export class GPUTestBase extends Fixture<GPUTestSubcaseBatchState> { }); const storageBuffer = this.device.createBuffer({ - size: sampleCount * type.size * componentCount, + size: sampleCount * type.size * componentCount * width * height, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, }); this.trackForCleanup(storageBuffer); + const uniformBuffer = this.makeBufferWithContents( + new Uint32Array([origin.x, origin.y, width, height]), + GPUBufferUsage.UNIFORM + ); + const uniformBindGroup = this.device.createBindGroup({ layout: computePipeline.getBindGroupLayout(0), entries: [ @@ -828,6 +931,12 @@ export class GPUTestBase extends Fixture<GPUTestSubcaseBatchState> { buffer: storageBuffer, }, }, + { + binding: 2, + resource: { + buffer: uniformBuffer, + }, + }, ], }); @@ -835,7 +944,11 @@ export class GPUTestBase extends Fixture<GPUTestSubcaseBatchState> { const pass = encoder.beginComputePass(); pass.setPipeline(computePipeline); pass.setBindGroup(0, uniformBindGroup); - pass.dispatchWorkgroups(1); + pass.dispatchWorkgroups( + Math.floor((width + kWorkgroupSizeX - 1) / kWorkgroupSizeX), + Math.floor((height + kWorkgroupSizeY - 1) / kWorkgroupSizeY), + 1 + ); pass.end(); this.device.queue.submit([encoder.finish()]); @@ -1081,11 +1194,17 @@ export class GPUTest extends GPUTestBase { this.mismatchedProvider = await this.sharedState.acquireMismatchedProvider(); } + /** GPUAdapter that the device was created from. */ + get adapter(): GPUAdapter { + assert(this.provider !== undefined, 'internal error: DeviceProvider missing'); + return this.provider.adapter; + } + /** * GPUDevice for the test to use. */ override get device(): GPUDevice { - assert(this.provider !== undefined, 'internal error: GPUDevice missing?'); + assert(this.provider !== undefined, 'internal error: DeviceProvider missing'); return this.provider.device; } @@ -1237,8 +1356,10 @@ export interface TextureTestMixinType { ): Generator<Required<GPUOrigin3DDict>>; } +type PipelineType = '2d' | '2d-array'; + type ImageCopyTestResources = { - pipeline: GPURenderPipeline; + pipelineByPipelineType: Map<PipelineType, GPURenderPipeline>; }; const s_deviceToResourcesMap = new WeakMap<GPUDevice, ImageCopyTestResources>(); @@ -1246,8 +1367,23 @@ const s_deviceToResourcesMap = new WeakMap<GPUDevice, ImageCopyTestResources>(); /** * Gets a (cached) pipeline to render a texture to an rgba8unorm texture */ -function getPipelineToRenderTextureToRGB8UnormTexture(device: GPUDevice) { +function getPipelineToRenderTextureToRGB8UnormTexture( + device: GPUDevice, + texture: GPUTexture, + isCompatibility: boolean +) { if (!s_deviceToResourcesMap.has(device)) { + s_deviceToResourcesMap.set(device, { + pipelineByPipelineType: new Map<PipelineType, GPURenderPipeline>(), + }); + } + + const { pipelineByPipelineType } = s_deviceToResourcesMap.get(device)!; + const pipelineType: PipelineType = + isCompatibility && texture.depthOrArrayLayers > 1 ? '2d-array' : '2d'; + if (!pipelineByPipelineType.get(pipelineType)) { + const [textureType, layerCode] = + pipelineType === '2d' ? ['texture_2d', ''] : ['texture_2d_array', ', uni.baseArrayLayer']; const module = device.createShaderModule({ code: ` struct VSOutput { @@ -1255,6 +1391,10 @@ function getPipelineToRenderTextureToRGB8UnormTexture(device: GPUDevice) { @location(0) texcoord: vec2f, }; + struct Uniforms { + baseArrayLayer: u32, + }; + @vertex fn vs( @builtin(vertex_index) vertexIndex : u32 ) -> VSOutput { @@ -1275,10 +1415,11 @@ function getPipelineToRenderTextureToRGB8UnormTexture(device: GPUDevice) { } @group(0) @binding(0) var ourSampler: sampler; - @group(0) @binding(1) var ourTexture: texture_2d<f32>; + @group(0) @binding(1) var ourTexture: ${textureType}<f32>; + @group(0) @binding(2) var<uniform> uni: Uniforms; @fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f { - return textureSample(ourTexture, ourSampler, fsInput.texcoord); + return textureSample(ourTexture, ourSampler, fsInput.texcoord${layerCode}); } `, }); @@ -1294,10 +1435,10 @@ function getPipelineToRenderTextureToRGB8UnormTexture(device: GPUDevice) { targets: [{ format: 'rgba8unorm' }], }, }); - s_deviceToResourcesMap.set(device, { pipeline }); + pipelineByPipelineType.set(pipelineType, pipeline); } - const { pipeline } = s_deviceToResourcesMap.get(device)!; - return pipeline; + const pipeline = pipelineByPipelineType.get(pipelineType)!; + return { pipelineType, pipeline }; } type LinearCopyParameters = { @@ -1441,7 +1582,11 @@ export function TextureTestMixin<F extends FixtureClass<GPUTest>>( // 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 pipeline = getPipelineToRenderTextureToRGB8UnormTexture(this.device); + 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); @@ -1457,24 +1602,45 @@ export function TextureTestMixin<F extends FixtureClass<GPUTest>>( const numLayers = texture.depthOrArrayLayers; const readbackPromisesPerLayer = []; + + const uniformBuffer = this.device.createBuffer({ + size: 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + this.trackForCleanup(uniformBuffer); + for (let layer = 0; layer < numLayers; ++layer) { + const viewDescriptor: GPUTextureViewDescriptor = { + baseMipLevel: mipLevel, + mipLevelCount: 1, + ...(!this.isCompatibility && { + baseArrayLayer: layer, + arrayLayerCount: 1, + }), + dimension: pipelineType, + }; + const bindGroup = this.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: sampler }, { binding: 1, - resource: texture.createView({ - baseMipLevel: mipLevel, - mipLevelCount: 1, - baseArrayLayer: layer, - arrayLayerCount: 1, - dimension: '2d', - }), + resource: texture.createView(viewDescriptor), }, + ...(pipelineType === '2d-array' + ? [ + { + binding: 2, + resource: { buffer: uniformBuffer }, + }, + ] + : []), ], }); + this.device.queue.writeBuffer(uniformBuffer, 0, new Uint32Array([layer])); + const encoder = this.device.createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: [ diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/idl/constructable.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/constructable.spec.ts new file mode 100644 index 0000000000..8fd9d08d3c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/constructable.spec.ts @@ -0,0 +1,54 @@ +export const description = ` +Test that constructable WebGPU objects are actually constructable. +`; + +import { makeTestGroup } from './../../common/framework/test_group.js'; +import { IDLTest } from './idl_test.js'; + +export const g = makeTestGroup(IDLTest); + +g.test('gpu_errors') + .desc('tests that GPUErrors are constructable') + .params(u => + u.combine('errorType', [ + 'GPUInternalError', + 'GPUOutOfMemoryError', + 'GPUValidationError', + ] as const) + ) + .fn(t => { + const { errorType } = t.params; + const Ctor = globalThis[errorType]; + const msg = 'this is a test'; + const error = new Ctor(msg); + t.expect(error.message === msg); + }); + +const pipelineErrorOptions: GPUPipelineErrorInit[] = [ + { reason: 'validation' }, + { reason: 'internal' }, +]; + +g.test('pipeline_errors') + .desc('tests that GPUPipelineError is constructable') + .params(u => + u // + .combine('msg', [undefined, 'some msg']) + .combine('options', pipelineErrorOptions) + ) + .fn(t => { + const { msg, options } = t.params; + const error = new GPUPipelineError(msg, options); + const expectedMsg = msg || ''; + t.expect(error.message === expectedMsg); + t.expect(error.reason === options.reason); + }); + +g.test('uncaptured_error_event') + .desc('tests that GPUUncapturedErrorEvent is constructable') + .fn(t => { + const msg = 'this is a test'; + const error = new GPUValidationError(msg); + const event = new GPUUncapturedErrorEvent('uncapturedError', { error }); + t.expect(event.error === error); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/listing_meta.json b/dom/webgpu/tests/cts/checkout/src/webgpu/listing_meta.json index f9caeefc6e..6ee25813f4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/listing_meta.json +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/listing_meta.json @@ -1,5 +1,5 @@ { - "_comment": "SEMI AUTO-GENERATED: Please read docs/adding_timing_metadata.md.", + "_comment": "SEMI AUTO-GENERATED. This list is NOT exhaustive. Please read docs/adding_timing_metadata.md.", "webgpu:api,operation,adapter,requestAdapter:requestAdapter:*": { "subcaseMS": 152.083 }, "webgpu:api,operation,adapter,requestAdapter:requestAdapter_no_parameters:*": { "subcaseMS": 384.601 }, "webgpu:api,operation,adapter,requestAdapterInfo:adapter_info:*": { "subcaseMS": 136.601 }, @@ -9,6 +9,7 @@ "webgpu:api,operation,adapter,requestDevice:features,unknown:*": { "subcaseMS": 13.600 }, "webgpu:api,operation,adapter,requestDevice:invalid:*": { "subcaseMS": 27.801 }, "webgpu:api,operation,adapter,requestDevice:limit,better_than_supported:*": { "subcaseMS": 3.614 }, + "webgpu:api,operation,adapter,requestDevice:limit,out_of_range:*": { "subcaseMS": 1.000 }, "webgpu:api,operation,adapter,requestDevice:limit,worse_than_default:*": { "subcaseMS": 6.711 }, "webgpu:api,operation,adapter,requestDevice:limits,supported:*": { "subcaseMS": 4.579 }, "webgpu:api,operation,adapter,requestDevice:limits,unknown:*": { "subcaseMS": 0.601 }, @@ -93,6 +94,7 @@ "webgpu:api,operation,memory_sync,buffer,single_buffer:two_draws_in_the_same_render_pass:*": { "subcaseMS": 4.925 }, "webgpu:api,operation,memory_sync,buffer,single_buffer:wr:*": { "subcaseMS": 18.296 }, "webgpu:api,operation,memory_sync,buffer,single_buffer:ww:*": { "subcaseMS": 18.802 }, + "webgpu:api,operation,memory_sync,texture,readonly_depth_stencil:sampling_while_testing:*": { "subcaseMS": 50.000 }, "webgpu:api,operation,memory_sync,texture,same_subresource:rw,single_pass,load_resolve:*": { "subcaseMS": 1.200 }, "webgpu:api,operation,memory_sync,texture,same_subresource:rw,single_pass,load_store:*": { "subcaseMS": 14.200 }, "webgpu:api,operation,memory_sync,texture,same_subresource:rw:*": { "subcaseMS": 10.908 }, @@ -108,8 +110,11 @@ "webgpu:api,operation,pipeline,default_layout:layout:*": { "subcaseMS": 11.500 }, "webgpu:api,operation,queue,writeBuffer:array_types:*": { "subcaseMS": 12.032 }, "webgpu:api,operation,queue,writeBuffer:multiple_writes_at_different_offsets_and_sizes:*": { "subcaseMS": 2.087 }, + "webgpu:api,operation,reflection:buffer_creation_from_reflection:*": { "subcaseMS": 0.800 }, "webgpu:api,operation,reflection:buffer_reflection_attributes:*": { "subcaseMS": 0.800 }, + "webgpu:api,operation,reflection:query_set_creation_from_reflection:*": { "subcaseMS": 0.634 }, "webgpu:api,operation,reflection:query_set_reflection_attributes:*": { "subcaseMS": 0.634 }, + "webgpu:api,operation,reflection:texture_creation_from_reflection:*": { "subcaseMS": 1.829 }, "webgpu:api,operation,reflection:texture_reflection_attributes:*": { "subcaseMS": 1.829 }, "webgpu:api,operation,render_pass,clear_value:layout:*": { "subcaseMS": 1.401 }, "webgpu:api,operation,render_pass,clear_value:loaded:*": { "subcaseMS": 14.300 }, @@ -135,6 +140,9 @@ "webgpu:api,operation,render_pipeline,sample_mask:alpha_to_coverage_mask:*": { "subcaseMS": 68.512 }, "webgpu:api,operation,render_pipeline,sample_mask:fragment_output_mask:*": { "subcaseMS": 6.154 }, "webgpu:api,operation,render_pipeline,vertex_only_render_pipeline:draw_depth_and_stencil_with_vertex_only_pipeline:*": { "subcaseMS": 14.100 }, + "webgpu:api,operation,rendering,3d_texture_slices:multiple_color_attachments,same_mip_level:*": { "subcaseMS": 69.400 }, + "webgpu:api,operation,rendering,3d_texture_slices:multiple_color_attachments,same_slice_with_diff_mip_levels:*": { "subcaseMS": 9.800 }, + "webgpu:api,operation,rendering,3d_texture_slices:one_color_attachment,mip_levels:*": { "subcaseMS": 54.100 }, "webgpu:api,operation,rendering,basic:clear:*": { "subcaseMS": 3.700 }, "webgpu:api,operation,rendering,basic:fullscreen_quad:*": { "subcaseMS": 16.601 }, "webgpu:api,operation,rendering,basic:large_draw:*": { "subcaseMS": 2335.425 }, @@ -194,6 +202,8 @@ "webgpu:api,operation,shader_module,compilation_info:getCompilationInfo_returns:*": { "subcaseMS": 0.284 }, "webgpu:api,operation,shader_module,compilation_info:line_number_and_position:*": { "subcaseMS": 1.867 }, "webgpu:api,operation,shader_module,compilation_info:offset_and_length:*": { "subcaseMS": 1.648 }, + "webgpu:api,operation,storage_texture,read_only:basic:*": { "subcaseMS": 20.000 }, + "webgpu:api,operation,storage_texture,read_write:basic:*": { "subcaseMS": 5.000 }, "webgpu:api,operation,texture_view,format_reinterpretation:render_and_resolve_attachment:*": { "subcaseMS": 14.488 }, "webgpu:api,operation,texture_view,format_reinterpretation:texture_binding:*": { "subcaseMS": 17.225 }, "webgpu:api,operation,texture_view,read:aspect:*": { "subcaseMS": 0.601 }, @@ -264,7 +274,7 @@ "webgpu:api,validation,buffer,mapping:unmap,state,mappingPending:*": { "subcaseMS": 22.951 }, "webgpu:api,validation,buffer,mapping:unmap,state,unmapped:*": { "subcaseMS": 74.200 }, "webgpu:api,validation,capability_checks,features,query_types:createQuerySet:*": { "subcaseMS": 10.451 }, - "webgpu:api,validation,capability_checks,features,query_types:writeTimestamp:*": { "subcaseMS": 1.200 }, + "webgpu:api,validation,capability_checks,features,query_types:timestamp:*": { "subcaseMS": 1.200 }, "webgpu:api,validation,capability_checks,features,texture_formats:canvas_configuration:*": { "subcaseMS": 4.339 }, "webgpu:api,validation,capability_checks,features,texture_formats:canvas_configuration_view_formats:*": { "subcaseMS": 4.522 }, "webgpu:api,validation,capability_checks,features,texture_formats:check_capability_guarantees:*": { "subcaseMS": 55.901 }, @@ -277,6 +287,8 @@ "webgpu:api,validation,capability_checks,limits,maxBindGroups:createPipelineLayout,at_over:*": { "subcaseMS": 9.310 }, "webgpu:api,validation,capability_checks,limits,maxBindGroups:setBindGroup,at_over:*": { "subcaseMS": 9.984 }, "webgpu:api,validation,capability_checks,limits,maxBindGroups:validate,maxBindGroupsPlusVertexBuffers:*": { "subcaseMS": 11.200 }, + "webgpu:api,validation,capability_checks,limits,maxBindGroupsPlusVertexBuffers:createRenderPipeline,at_over:*": { "subcaseMS": 11.200 }, + "webgpu:api,validation,capability_checks,limits,maxBindGroupsPlusVertexBuffers:draw,at_over:*": { "subcaseMS": 11.200 }, "webgpu:api,validation,capability_checks,limits,maxBindingsPerBindGroup:createBindGroupLayout,at_over:*": { "subcaseMS": 12.441 }, "webgpu:api,validation,capability_checks,limits,maxBindingsPerBindGroup:createPipeline,at_over:*": { "subcaseMS": 11.179 }, "webgpu:api,validation,capability_checks,limits,maxBindingsPerBindGroup:validate:*": { "subcaseMS": 12.401 }, @@ -356,6 +368,7 @@ "webgpu:api,validation,compute_pipeline:overrides,workgroup_size,limits:*": { "subcaseMS": 14.751 }, "webgpu:api,validation,compute_pipeline:overrides,workgroup_size:*": { "subcaseMS": 6.376 }, "webgpu:api,validation,compute_pipeline:pipeline_layout,device_mismatch:*": { "subcaseMS": 1.175 }, + "webgpu:api,validation,compute_pipeline:resource_compatibility:*": { "subcaseMS": 1.175 }, "webgpu:api,validation,compute_pipeline:shader_module,compute:*": { "subcaseMS": 6.867 }, "webgpu:api,validation,compute_pipeline:shader_module,device_mismatch:*": { "subcaseMS": 15.350 }, "webgpu:api,validation,compute_pipeline:shader_module,invalid:*": { "subcaseMS": 2.500 }, @@ -516,7 +529,6 @@ "webgpu:api,validation,encoding,createRenderBundleEncoder:attachment_state,limits,maxColorAttachmentBytesPerSample,unaligned:*": { "subcaseMS": 0.750 }, "webgpu:api,validation,encoding,createRenderBundleEncoder:attachment_state,limits,maxColorAttachments:*": { "subcaseMS": 0.145 }, "webgpu:api,validation,encoding,createRenderBundleEncoder:depth_stencil_readonly:*": { "subcaseMS": 1.804 }, - "webgpu:api,validation,encoding,createRenderBundleEncoder:depth_stencil_readonly_with_undefined_depth:*": { "subcaseMS": 14.825 }, "webgpu:api,validation,encoding,createRenderBundleEncoder:valid_texture_formats:*": { "subcaseMS": 2.130 }, "webgpu:api,validation,encoding,encoder_open_state:compute_pass_commands:*": { "subcaseMS": 4.208 }, "webgpu:api,validation,encoding,encoder_open_state:non_pass_commands:*": { "subcaseMS": 26.191 }, @@ -532,6 +544,8 @@ "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:bgl_visibility_mismatch:*": { "subcaseMS": 0.608 }, "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:bind_groups_and_pipeline_layout_mismatch:*": { "subcaseMS": 1.535 }, "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:buffer_binding,render_pipeline:*": { "subcaseMS": 1.734 }, + "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:default_bind_group_layouts_never_match,compute_pass:*": { "subcaseMS": 1.734 }, + "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:default_bind_group_layouts_never_match,render_pass:*": { "subcaseMS": 1.734 }, "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:empty_bind_group_layouts_requires_empty_bind_groups,compute_pass:*": { "subcaseMS": 2.325 }, "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:empty_bind_group_layouts_requires_empty_bind_groups,render_pass:*": { "subcaseMS": 10.838 }, "webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:sampler_binding,render_pipeline:*": { "subcaseMS": 10.523 }, @@ -542,9 +556,9 @@ "webgpu:api,validation,encoding,queries,general:occlusion_query,invalid_query_set:*": { "subcaseMS": 1.651 }, "webgpu:api,validation,encoding,queries,general:occlusion_query,query_index:*": { "subcaseMS": 0.500 }, "webgpu:api,validation,encoding,queries,general:occlusion_query,query_type:*": { "subcaseMS": 4.702 }, - "webgpu:api,validation,encoding,queries,general:timestamp_query,device_mismatch:*": { "subcaseMS": 0.101 }, - "webgpu:api,validation,encoding,queries,general:timestamp_query,invalid_query_set:*": { "subcaseMS": 0.101 }, - "webgpu:api,validation,encoding,queries,general:timestamp_query,query_type_and_index:*": { "subcaseMS": 0.301 }, + "webgpu:api,validation,encoding,queries,general:writeTimestamp,device_mismatch:*": { "subcaseMS": 0.101 }, + "webgpu:api,validation,encoding,queries,general:writeTimestamp,invalid_query_set:*": { "subcaseMS": 0.101 }, + "webgpu:api,validation,encoding,queries,general:writeTimestamp,query_type_and_index:*": { "subcaseMS": 0.301 }, "webgpu:api,validation,encoding,queries,resolveQuerySet:destination_buffer_usage:*": { "subcaseMS": 16.050 }, "webgpu:api,validation,encoding,queries,resolveQuerySet:destination_offset_alignment:*": { "subcaseMS": 0.325 }, "webgpu:api,validation,encoding,queries,resolveQuerySet:first_query_and_query_count:*": { "subcaseMS": 0.250 }, @@ -599,6 +613,7 @@ "webgpu:api,validation,image_copy,texture_related:texture,device_mismatch:*": { "subcaseMS": 5.417 }, "webgpu:api,validation,image_copy,texture_related:usage:*": { "subcaseMS": 1.224 }, "webgpu:api,validation,image_copy,texture_related:valid:*": { "subcaseMS": 3.678 }, + "webgpu:api,validation,layout_shader_compat:pipeline_layout_shader_exact_match:*": { "subcaseMS": 2.000 }, "webgpu:api,validation,query_set,create:count:*": { "subcaseMS": 0.967 }, "webgpu:api,validation,query_set,destroy:invalid_queryset:*": { "subcaseMS": 0.801 }, "webgpu:api,validation,query_set,destroy:twice:*": { "subcaseMS": 0.700 }, @@ -629,7 +644,7 @@ "webgpu:api,validation,queue,destroyed,buffer:writeBuffer:*": { "subcaseMS": 2.151 }, "webgpu:api,validation,queue,destroyed,query_set:beginOcclusionQuery:*": { "subcaseMS": 17.401 }, "webgpu:api,validation,queue,destroyed,query_set:resolveQuerySet:*": { "subcaseMS": 16.401 }, - "webgpu:api,validation,queue,destroyed,query_set:writeTimestamp:*": { "subcaseMS": 0.901 }, + "webgpu:api,validation,queue,destroyed,query_set:timestamps:*": { "subcaseMS": 0.901 }, "webgpu:api,validation,queue,destroyed,texture:beginRenderPass:*": { "subcaseMS": 0.350 }, "webgpu:api,validation,queue,destroyed,texture:copyBufferToTexture:*": { "subcaseMS": 16.550 }, "webgpu:api,validation,queue,destroyed,texture:copyTextureToBuffer:*": { "subcaseMS": 15.900 }, @@ -663,6 +678,10 @@ "webgpu:api,validation,render_pass,render_pass_descriptor:attachments,one_color_attachment:*": { "subcaseMS": 33.401 }, "webgpu:api,validation,render_pass,render_pass_descriptor:attachments,one_depth_stencil_attachment:*": { "subcaseMS": 15.301 }, "webgpu:api,validation,render_pass,render_pass_descriptor:attachments,same_size:*": { "subcaseMS": 33.400 }, + "webgpu:api,validation,render_pass,render_pass_descriptor:color_attachments,depthSlice,bound_check:*": { "subcaseMS": 9.400 }, + "webgpu:api,validation,render_pass,render_pass_descriptor:color_attachments,depthSlice,definedness:*": { "subcaseMS": 5.601 }, + "webgpu:api,validation,render_pass,render_pass_descriptor:color_attachments,depthSlice,overlaps,diff_miplevel:*": { "subcaseMS": 3.901 }, + "webgpu:api,validation,render_pass,render_pass_descriptor:color_attachments,depthSlice,overlaps,same_miplevel:*": { "subcaseMS": 6.400 }, "webgpu:api,validation,render_pass,render_pass_descriptor:color_attachments,empty:*": { "subcaseMS": 0.400 }, "webgpu:api,validation,render_pass,render_pass_descriptor:color_attachments,limits,maxColorAttachmentBytesPerSample,aligned:*": { "subcaseMS": 1.825 }, "webgpu:api,validation,render_pass,render_pass_descriptor:color_attachments,limits,maxColorAttachmentBytesPerSample,unaligned:*": { "subcaseMS": 17.151 }, @@ -701,6 +720,7 @@ "webgpu:api,validation,render_pipeline,fragment_state:pipeline_output_targets:*": { "subcaseMS": 0.497 }, "webgpu:api,validation,render_pipeline,fragment_state:targets_blend:*": { "subcaseMS": 1.203 }, "webgpu:api,validation,render_pipeline,fragment_state:targets_format_filterable:*": { "subcaseMS": 2.143 }, + "webgpu:api,validation,render_pipeline,fragment_state:targets_format_is_color_format:*": { "subcaseMS": 2.000 }, "webgpu:api,validation,render_pipeline,fragment_state:targets_format_renderable:*": { "subcaseMS": 3.339 }, "webgpu:api,validation,render_pipeline,fragment_state:targets_write_mask:*": { "subcaseMS": 12.272 }, "webgpu:api,validation,render_pipeline,inter_stage:interpolation_sampling:*": { "subcaseMS": 3.126 }, @@ -713,6 +733,7 @@ "webgpu:api,validation,render_pipeline,inter_stage:max_shader_variable_location:*": { "subcaseMS": 11.050 }, "webgpu:api,validation,render_pipeline,inter_stage:type:*": { "subcaseMS": 6.170 }, "webgpu:api,validation,render_pipeline,misc:basic:*": { "subcaseMS": 0.901 }, + "webgpu:api,validation,render_pipeline,misc:external_texture:*": { "subcaseMS": 35.189 }, "webgpu:api,validation,render_pipeline,misc:pipeline_layout,device_mismatch:*": { "subcaseMS": 8.700 }, "webgpu:api,validation,render_pipeline,misc:vertex_state_only:*": { "subcaseMS": 1.125 }, "webgpu:api,validation,render_pipeline,multisample_state:alpha_to_coverage,count:*": { "subcaseMS": 3.200 }, @@ -730,6 +751,7 @@ "webgpu:api,validation,render_pipeline,overrides:value,validation_error,vertex:*": { "subcaseMS": 6.022 }, "webgpu:api,validation,render_pipeline,primitive_state:strip_index_format:*": { "subcaseMS": 5.267 }, "webgpu:api,validation,render_pipeline,primitive_state:unclipped_depth:*": { "subcaseMS": 1.025 }, + "webgpu:api,validation,render_pipeline,resource_compatibility:resource_compatibility:*": { "subcaseMS": 1.025 }, "webgpu:api,validation,render_pipeline,shader_module:device_mismatch:*": { "subcaseMS": 0.700 }, "webgpu:api,validation,render_pipeline,shader_module:invalid,fragment:*": { "subcaseMS": 5.800 }, "webgpu:api,validation,render_pipeline,shader_module:invalid,vertex:*": { "subcaseMS": 15.151 }, @@ -775,8 +797,11 @@ "webgpu:api,validation,resource_usages,texture,in_render_misc:subresources,set_unused_bind_group:*": { "subcaseMS": 6.200 }, "webgpu:api,validation,resource_usages,texture,in_render_misc:subresources,texture_usages_in_copy_and_render_pass:*": { "subcaseMS": 4.763 }, "webgpu:api,validation,shader_module,entry_point:compute:*": { "subcaseMS": 4.439 }, + "webgpu:api,validation,shader_module,entry_point:compute_undefined_entry_point_and_extra_stage:*": { "subcaseMS": 17.075 }, "webgpu:api,validation,shader_module,entry_point:fragment:*": { "subcaseMS": 5.865 }, + "webgpu:api,validation,shader_module,entry_point:fragment_undefined_entry_point_and_extra_stage:*": { "subcaseMS": 16.050 }, "webgpu:api,validation,shader_module,entry_point:vertex:*": { "subcaseMS": 5.803 }, + "webgpu:api,validation,shader_module,entry_point:vertex_undefined_entry_point_and_extra_stage:*": { "subcaseMS": 15.851 }, "webgpu:api,validation,shader_module,overrides:id_conflict:*": { "subcaseMS": 36.700 }, "webgpu:api,validation,shader_module,overrides:name_conflict:*": { "subcaseMS": 1.500 }, "webgpu:api,validation,state,device_lost,destroy:command,clearBuffer:*": { "subcaseMS": 11.826 }, @@ -815,8 +840,6 @@ "webgpu:api,validation,texture,bgra8unorm_storage:configure_storage_usage_on_canvas_context_with_bgra8unorm_storage:*": { "subcaseMS": 3.230 }, "webgpu:api,validation,texture,bgra8unorm_storage:configure_storage_usage_on_canvas_context_without_bgra8unorm_storage:*": { "subcaseMS": 1.767 }, "webgpu:api,validation,texture,bgra8unorm_storage:create_bind_group_layout:*": { "subcaseMS": 21.500 }, - "webgpu:api,validation,texture,bgra8unorm_storage:create_shader_module_with_bgra8unorm_storage:*": { "subcaseMS": 11.201 }, - "webgpu:api,validation,texture,bgra8unorm_storage:create_shader_module_without_bgra8unorm_storage:*": { "subcaseMS": 1.601 }, "webgpu:api,validation,texture,bgra8unorm_storage:create_texture:*": { "subcaseMS": 22.900 }, "webgpu:api,validation,texture,destroy:base:*": { "subcaseMS": 4.000 }, "webgpu:api,validation,texture,destroy:invalid_texture:*": { "subcaseMS": 27.200 }, @@ -828,14 +851,27 @@ "webgpu:api,validation,texture,rg11b10ufloat_renderable:begin_render_pass_single_sampled:*": { "subcaseMS": 1.200 }, "webgpu:api,validation,texture,rg11b10ufloat_renderable:create_render_pipeline:*": { "subcaseMS": 2.400 }, "webgpu:api,validation,texture,rg11b10ufloat_renderable:create_texture:*": { "subcaseMS": 12.700 }, + "webgpu:compat,api,validation,createBindGroup:viewDimension_matches_textureBindingViewDimension:*": { "subcaseMS": 6.523 }, + "webgpu:compat,api,validation,createBindGroupLayout:unsupportedStorageTextureFormats:*": { "subcaseMS": 0.601 }, "webgpu:compat,api,validation,encoding,cmds,copyTextureToBuffer:compressed:*": { "subcaseMS": 202.929 }, + "webgpu:compat,api,validation,encoding,cmds,copyTextureToTexture:compressed:*": { "subcaseMS": 0.600 }, + "webgpu:compat,api,validation,encoding,cmds,copyTextureToTexture:multisample:*": { "subcaseMS": 0.600 }, "webgpu:compat,api,validation,encoding,programmable,pipeline_bind_group_compat:twoDifferentTextureViews,compute_pass,unused:*": { "subcaseMS": 1.501 }, "webgpu:compat,api,validation,encoding,programmable,pipeline_bind_group_compat:twoDifferentTextureViews,compute_pass,used:*": { "subcaseMS": 49.405 }, "webgpu:compat,api,validation,encoding,programmable,pipeline_bind_group_compat:twoDifferentTextureViews,render_pass,unused:*": { "subcaseMS": 16.002 }, "webgpu:compat,api,validation,encoding,programmable,pipeline_bind_group_compat:twoDifferentTextureViews,render_pass,used:*": { "subcaseMS": 0.000 }, + "webgpu:compat,api,validation,render_pipeline,depth_stencil_state:depthBiasClamp:*": { "subcaseMS": 1.604 }, "webgpu:compat,api,validation,render_pipeline,fragment_state:colorState:*": { "subcaseMS": 32.604 }, + "webgpu:compat,api,validation,render_pipeline,shader_module:interpolate:*": { "subcaseMS": 1.502 }, + "webgpu:compat,api,validation,render_pipeline,shader_module:sample_index:*": { "subcaseMS": 1.502 }, "webgpu:compat,api,validation,render_pipeline,shader_module:sample_mask:*": { "subcaseMS": 14.801 }, + "webgpu:compat,api,validation,render_pipeline,shader_module:unsupportedStorageTextureFormats,computePipeline:*": { "subcaseMS": 0.601 }, + "webgpu:compat,api,validation,render_pipeline,shader_module:unsupportedStorageTextureFormats,renderPipeline:*": { "subcaseMS": 0.601 }, "webgpu:compat,api,validation,render_pipeline,vertex_state:maxVertexAttributesVertexIndexInstanceIndex:*": { "subcaseMS": 3.700 }, + "webgpu:compat,api,validation,texture,createTexture:depthOrArrayLayers_incompatible_with_textureBindingViewDimension:*": { "subcaseMS": 12.712 }, + "webgpu:compat,api,validation,texture,createTexture:format_reinterpretation:*": { "subcaseMS": 7.012 }, + "webgpu:compat,api,validation,texture,createTexture:invalidTextureBindingViewDimension:*": { "subcaseMS": 6.022 }, + "webgpu:compat,api,validation,texture,createTexture:unsupportedStorageTextureFormats:*": { "subcaseMS": 0.601 }, "webgpu:compat,api,validation,texture,createTexture:unsupportedTextureFormats:*": { "subcaseMS": 0.700 }, "webgpu:compat,api,validation,texture,createTexture:unsupportedTextureViewFormats:*": { "subcaseMS": 0.601 }, "webgpu:compat,api,validation,texture,cubeArray:cube_array:*": { "subcaseMS": 13.701 }, @@ -862,6 +898,30 @@ "webgpu:idl,constants,flags:ShaderStage,values:*": { "subcaseMS": 0.034 }, "webgpu:idl,constants,flags:TextureUsage,count:*": { "subcaseMS": 0.101 }, "webgpu:idl,constants,flags:TextureUsage,values:*": { "subcaseMS": 0.040 }, + "webgpu:idl,constructable:gpu_errors:*": { "subcaseMS": 0.101 }, + "webgpu:idl,constructable:pipeline_errors:*": { "subcaseMS": 0.101 }, + "webgpu:idl,constructable:uncaptured_error_event:*": { "subcaseMS": 0.101 }, + "webgpu:shader,execution,expression,access,array,index:abstract_scalar:*": { "subcaseMS": 235.962 }, + "webgpu:shader,execution,expression,access,array,index:bool:*": { "subcaseMS": 663.038 }, + "webgpu:shader,execution,expression,access,array,index:concrete_scalar:*": { "subcaseMS": 1439.796 }, + "webgpu:shader,execution,expression,access,array,index:runtime_sized:*": { "subcaseMS": 830.024 }, + "webgpu:shader,execution,expression,access,array,index:vector:*": { "subcaseMS": 2137.684 }, + "webgpu:shader,execution,expression,access,matrix,index:abstract_float_column:*": { "subcaseMS": 14.643 }, + "webgpu:shader,execution,expression,access,matrix,index:abstract_float_element:*": { "subcaseMS": 587.333 }, + "webgpu:shader,execution,expression,access,matrix,index:concrete_float_column:*": { "subcaseMS": 4690.055 }, + "webgpu:shader,execution,expression,access,matrix,index:concrete_float_element:*": { "subcaseMS": 6855.570 }, + "webgpu:shader,execution,expression,access,structure,index:buffer:*": { "subcaseMS": 325.082 }, + "webgpu:shader,execution,expression,access,structure,index:buffer_align:*": { "subcaseMS": 84.911 }, + "webgpu:shader,execution,expression,access,structure,index:buffer_pointer:*": { "subcaseMS": 307.453 }, + "webgpu:shader,execution,expression,access,structure,index:buffer_size:*": { "subcaseMS": 45.423 }, + "webgpu:shader,execution,expression,access,structure,index:const:*": { "subcaseMS": 211.459 }, + "webgpu:shader,execution,expression,access,structure,index:const_nested:*": { "subcaseMS": 214.727 }, + "webgpu:shader,execution,expression,access,structure,index:let:*": { "subcaseMS": 201.392 }, + "webgpu:shader,execution,expression,access,structure,index:param:*": { "subcaseMS": 215.826 }, + "webgpu:shader,execution,expression,access,vector,components:abstract_scalar:*": { "subcaseMS": 533.768 }, + "webgpu:shader,execution,expression,access,vector,components:concrete_scalar:*": { "subcaseMS": 11636.652 }, + "webgpu:shader,execution,expression,access,vector,index:abstract_scalar:*": { "subcaseMS": 215.404 }, + "webgpu:shader,execution,expression,access,vector,index:concrete_scalar:*": { "subcaseMS": 1707.582 }, "webgpu:shader,execution,expression,binary,af_addition:scalar:*": { "subcaseMS": 815.300 }, "webgpu:shader,execution,expression,binary,af_addition:scalar_vector:*": { "subcaseMS": 1803.434 }, "webgpu:shader,execution,expression,binary,af_addition:vector:*": { "subcaseMS": 719.600 }, @@ -877,7 +937,12 @@ "webgpu:shader,execution,expression,binary,af_division:vector:*": { "subcaseMS": 237.134 }, "webgpu:shader,execution,expression,binary,af_division:vector_scalar:*": { "subcaseMS": 580.000 }, "webgpu:shader,execution,expression,binary,af_matrix_addition:matrix:*": { "subcaseMS": 11169.534 }, + "webgpu:shader,execution,expression,binary,af_matrix_matrix_multiplication:matrix_matrix:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,af_matrix_scalar_multiplication:matrix_scalar:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,af_matrix_scalar_multiplication:scalar_matrix:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,expression,binary,af_matrix_subtraction:matrix:*": { "subcaseMS": 14060.956 }, + "webgpu:shader,execution,expression,binary,af_matrix_vector_multiplication:matrix_vector:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,af_matrix_vector_multiplication:vector_matrix:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,expression,binary,af_multiplication:scalar:*": { "subcaseMS": 777.901 }, "webgpu:shader,execution,expression,binary,af_multiplication:scalar_vector:*": { "subcaseMS": 2025.534 }, "webgpu:shader,execution,expression,binary,af_multiplication:vector:*": { "subcaseMS": 710.667 }, @@ -890,6 +955,27 @@ "webgpu:shader,execution,expression,binary,af_subtraction:scalar_vector:*": { "subcaseMS": 2336.534 }, "webgpu:shader,execution,expression,binary,af_subtraction:vector:*": { "subcaseMS": 764.201 }, "webgpu:shader,execution,expression,binary,af_subtraction:vector_scalar:*": { "subcaseMS": 2437.701 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:addition:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:addition_scalar_vector:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:addition_vector_scalar:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:division:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:division_scalar_vector:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:division_vector_scalar:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:multiplication:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:multiplication_scalar_vector:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:multiplication_vector_scalar:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:remainder:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:remainder_scalar_vector:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:remainder_vector_scalar:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:subtraction:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:subtraction_scalar_vector:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_arithmetic:subtraction_vector_scalar:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,binary,ai_comparison:equals:*": { "subcaseMS": 338.609 }, + "webgpu:shader,execution,expression,binary,ai_comparison:greater_equals:*": { "subcaseMS": 219.452 }, + "webgpu:shader,execution,expression,binary,ai_comparison:greater_than:*": { "subcaseMS": 232.750 }, + "webgpu:shader,execution,expression,binary,ai_comparison:less_equals:*": { "subcaseMS": 228.676 }, + "webgpu:shader,execution,expression,binary,ai_comparison:less_than:*": { "subcaseMS": 245.506 }, + "webgpu:shader,execution,expression,binary,ai_comparison:not_equals:*": { "subcaseMS": 222.561 }, "webgpu:shader,execution,expression,binary,bitwise:bitwise_and:*": { "subcaseMS": 20.982 }, "webgpu:shader,execution,expression,binary,bitwise:bitwise_and_compound:*": { "subcaseMS": 22.513 }, "webgpu:shader,execution,expression,binary,bitwise:bitwise_exclusive_or:*": { "subcaseMS": 21.294 }, @@ -1066,16 +1152,16 @@ "webgpu:shader,execution,expression,binary,u32_comparison:less_equals:*": { "subcaseMS": 7.844 }, "webgpu:shader,execution,expression,binary,u32_comparison:less_than:*": { "subcaseMS": 6.700 }, "webgpu:shader,execution,expression,binary,u32_comparison:not_equals:*": { "subcaseMS": 6.850 }, - "webgpu:shader,execution,expression,call,builtin,abs:abstract_float:*": { "subcaseMS": 464.126 }, + "webgpu:shader,execution,expression,call,builtin,abs:abstract_float:*": { "subcaseMS": 13489.454 }, "webgpu:shader,execution,expression,call,builtin,abs:abstract_int:*": { "subcaseMS": 16.810 }, "webgpu:shader,execution,expression,call,builtin,abs:f16:*": { "subcaseMS": 22.910 }, "webgpu:shader,execution,expression,call,builtin,abs:f32:*": { "subcaseMS": 9.844 }, "webgpu:shader,execution,expression,call,builtin,abs:i32:*": { "subcaseMS": 7.088 }, "webgpu:shader,execution,expression,call,builtin,abs:u32:*": { "subcaseMS": 7.513 }, - "webgpu:shader,execution,expression,call,builtin,acos:abstract_float:*": { "subcaseMS": 15.505 }, + "webgpu:shader,execution,expression,call,builtin,acos:abstract_float:*": { "subcaseMS": 12032.378 }, "webgpu:shader,execution,expression,call,builtin,acos:f16:*": { "subcaseMS": 26.005 }, "webgpu:shader,execution,expression,call,builtin,acos:f32:*": { "subcaseMS": 33.063 }, - "webgpu:shader,execution,expression,call,builtin,acosh:abstract_float:*": { "subcaseMS": 17.210 }, + "webgpu:shader,execution,expression,call,builtin,acosh:abstract_float:*": { "subcaseMS": 12832.129 }, "webgpu:shader,execution,expression,call,builtin,acosh:f16:*": { "subcaseMS": 140.494 }, "webgpu:shader,execution,expression,call,builtin,acosh:f32:*": { "subcaseMS": 12.588 }, "webgpu:shader,execution,expression,call,builtin,all:bool:*": { "subcaseMS": 6.938 }, @@ -1085,19 +1171,19 @@ "webgpu:shader,execution,expression,call,builtin,arrayLength:read_only:*": { "subcaseMS": 4.500 }, "webgpu:shader,execution,expression,call,builtin,arrayLength:single_element:*": { "subcaseMS": 6.569 }, "webgpu:shader,execution,expression,call,builtin,arrayLength:struct_member:*": { "subcaseMS": 6.819 }, - "webgpu:shader,execution,expression,call,builtin,asin:abstract_float:*": { "subcaseMS": 16.606 }, + "webgpu:shader,execution,expression,call,builtin,asin:abstract_float:*": { "subcaseMS": 12414.721 }, "webgpu:shader,execution,expression,call,builtin,asin:f16:*": { "subcaseMS": 6.708 }, "webgpu:shader,execution,expression,call,builtin,asin:f32:*": { "subcaseMS": 33.969 }, - "webgpu:shader,execution,expression,call,builtin,asinh:abstract_float:*": { "subcaseMS": 23.305 }, + "webgpu:shader,execution,expression,call,builtin,asinh:abstract_float:*": { "subcaseMS": 13136.027 }, "webgpu:shader,execution,expression,call,builtin,asinh:f16:*": { "subcaseMS": 59.538 }, "webgpu:shader,execution,expression,call,builtin,asinh:f32:*": { "subcaseMS": 9.731 }, - "webgpu:shader,execution,expression,call,builtin,atan2:abstract_float:*": { "subcaseMS": 24.705 }, + "webgpu:shader,execution,expression,call,builtin,atan2:abstract_float:*": { "subcaseMS": 6683.345 }, "webgpu:shader,execution,expression,call,builtin,atan2:f16:*": { "subcaseMS": 32.506 }, "webgpu:shader,execution,expression,call,builtin,atan2:f32:*": { "subcaseMS": 25.938 }, - "webgpu:shader,execution,expression,call,builtin,atan:abstract_float:*": { "subcaseMS": 32.408 }, + "webgpu:shader,execution,expression,call,builtin,atan:abstract_float:*": { "subcaseMS": 12036.687 }, "webgpu:shader,execution,expression,call,builtin,atan:f16:*": { "subcaseMS": 21.106 }, "webgpu:shader,execution,expression,call,builtin,atan:f32:*": { "subcaseMS": 10.251 }, - "webgpu:shader,execution,expression,call,builtin,atanh:abstract_float:*": { "subcaseMS": 16.807 }, + "webgpu:shader,execution,expression,call,builtin,atanh:abstract_float:*": { "subcaseMS": 12956.533 }, "webgpu:shader,execution,expression,call,builtin,atanh:f16:*": { "subcaseMS": 81.619 }, "webgpu:shader,execution,expression,call,builtin,atanh:f32:*": { "subcaseMS": 12.332 }, "webgpu:shader,execution,expression,call,builtin,atomics,atomicAdd:add_storage:*": { "subcaseMS": 6.482 }, @@ -1128,6 +1214,14 @@ "webgpu:shader,execution,expression,call,builtin,atomics,atomicSub:sub_workgroup:*": { "subcaseMS": 7.238 }, "webgpu:shader,execution,expression,call,builtin,atomics,atomicXor:xor_storage:*": { "subcaseMS": 6.807 }, "webgpu:shader,execution,expression,call,builtin,atomics,atomicXor:xor_workgroup:*": { "subcaseMS": 7.821 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:af_to_f32:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:af_to_i32:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:af_to_u32:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:af_to_vec2f16:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:ai_to_f32:*": { "subcaseMS": 960.104 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:ai_to_i32:*": { "subcaseMS": 741.443 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:ai_to_u32:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:ai_to_vec2h:*": { "subcaseMS": 170.902 }, "webgpu:shader,execution,expression,call,builtin,bitcast:f16_to_f16:*": { "subcaseMS": 21.112 }, "webgpu:shader,execution,expression,call,builtin,bitcast:f32_to_f32:*": { "subcaseMS": 8.625 }, "webgpu:shader,execution,expression,call,builtin,bitcast:f32_to_i32:*": { "subcaseMS": 8.175 }, @@ -1141,6 +1235,8 @@ "webgpu:shader,execution,expression,call,builtin,bitcast:u32_to_i32:*": { "subcaseMS": 6.982 }, "webgpu:shader,execution,expression,call,builtin,bitcast:u32_to_u32:*": { "subcaseMS": 6.907 }, "webgpu:shader,execution,expression,call,builtin,bitcast:u32_to_vec2h:*": { "subcaseMS": 22.210 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:vec2af_to_vec4f16:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,bitcast:vec2ai_to_vec4h:*": { "subcaseMS": 304.357 }, "webgpu:shader,execution,expression,call,builtin,bitcast:vec2f_to_vec4h:*": { "subcaseMS": 24.015 }, "webgpu:shader,execution,expression,call,builtin,bitcast:vec2h_to_f32:*": { "subcaseMS": 21.412 }, "webgpu:shader,execution,expression,call,builtin,bitcast:vec2h_to_i32:*": { "subcaseMS": 38.312 }, @@ -1150,19 +1246,19 @@ "webgpu:shader,execution,expression,call,builtin,bitcast:vec4h_to_vec2f:*": { "subcaseMS": 22.812 }, "webgpu:shader,execution,expression,call,builtin,bitcast:vec4h_to_vec2i:*": { "subcaseMS": 20.915 }, "webgpu:shader,execution,expression,call,builtin,bitcast:vec4h_to_vec2u:*": { "subcaseMS": 29.514 }, - "webgpu:shader,execution,expression,call,builtin,ceil:abstract_float:*": { "subcaseMS": 23.611 }, + "webgpu:shader,execution,expression,call,builtin,ceil:abstract_float:*": { "subcaseMS": 15217.441 }, "webgpu:shader,execution,expression,call,builtin,ceil:f16:*": { "subcaseMS": 29.209 }, "webgpu:shader,execution,expression,call,builtin,ceil:f32:*": { "subcaseMS": 11.132 }, - "webgpu:shader,execution,expression,call,builtin,clamp:abstract_float:*": { "subcaseMS": 11800.350 }, + "webgpu:shader,execution,expression,call,builtin,clamp:abstract_float:*": { "subcaseMS": 121937.540 }, "webgpu:shader,execution,expression,call,builtin,clamp:abstract_int:*": { "subcaseMS": 18.104 }, "webgpu:shader,execution,expression,call,builtin,clamp:f16:*": { "subcaseMS": 32.809 }, "webgpu:shader,execution,expression,call,builtin,clamp:f32:*": { "subcaseMS": 159.926 }, "webgpu:shader,execution,expression,call,builtin,clamp:i32:*": { "subcaseMS": 54.200 }, "webgpu:shader,execution,expression,call,builtin,clamp:u32:*": { "subcaseMS": 272.419 }, - "webgpu:shader,execution,expression,call,builtin,cos:abstract_float:*": { "subcaseMS": 16.706 }, + "webgpu:shader,execution,expression,call,builtin,cos:abstract_float:*": { "subcaseMS": 17109.500 }, "webgpu:shader,execution,expression,call,builtin,cos:f16:*": { "subcaseMS": 23.905 }, "webgpu:shader,execution,expression,call,builtin,cos:f32:*": { "subcaseMS": 25.275 }, - "webgpu:shader,execution,expression,call,builtin,cosh:abstract_float:*": { "subcaseMS": 22.909 }, + "webgpu:shader,execution,expression,call,builtin,cosh:abstract_float:*": { "subcaseMS": 11033.538 }, "webgpu:shader,execution,expression,call,builtin,cosh:f16:*": { "subcaseMS": 58.475 }, "webgpu:shader,execution,expression,call,builtin,cosh:f32:*": { "subcaseMS": 9.694 }, "webgpu:shader,execution,expression,call,builtin,countLeadingZeros:i32:*": { "subcaseMS": 7.494 }, @@ -1171,16 +1267,19 @@ "webgpu:shader,execution,expression,call,builtin,countOneBits:u32:*": { "subcaseMS": 8.644 }, "webgpu:shader,execution,expression,call,builtin,countTrailingZeros:i32:*": { "subcaseMS": 7.844 }, "webgpu:shader,execution,expression,call,builtin,countTrailingZeros:u32:*": { "subcaseMS": 7.851 }, - "webgpu:shader,execution,expression,call,builtin,cross:abstract_float:*": { "subcaseMS": 3.002 }, + "webgpu:shader,execution,expression,call,builtin,cross:abstract_float:*": { "subcaseMS": 60020.496 }, "webgpu:shader,execution,expression,call,builtin,cross:f16:*": { "subcaseMS": 115.503 }, "webgpu:shader,execution,expression,call,builtin,cross:f32:*": { "subcaseMS": 664.926 }, - "webgpu:shader,execution,expression,call,builtin,degrees:abstract_float:*": { "subcaseMS": 533.052 }, + "webgpu:shader,execution,expression,call,builtin,degrees:abstract_float:*": { "subcaseMS": 12611.693 }, "webgpu:shader,execution,expression,call,builtin,degrees:f16:*": { "subcaseMS": 29.308 }, "webgpu:shader,execution,expression,call,builtin,degrees:f32:*": { "subcaseMS": 79.525 }, - "webgpu:shader,execution,expression,call,builtin,determinant:abstract_float:*": { "subcaseMS": 15.306 }, + "webgpu:shader,execution,expression,call,builtin,determinant:abstract_float:*": { "subcaseMS": 1785.618 }, "webgpu:shader,execution,expression,call,builtin,determinant:f16:*": { "subcaseMS": 37.192 }, "webgpu:shader,execution,expression,call,builtin,determinant:f32:*": { "subcaseMS": 10.742 }, - "webgpu:shader,execution,expression,call,builtin,distance:abstract_float:*": { "subcaseMS": 14.503 }, + "webgpu:shader,execution,expression,call,builtin,distance:abstract_float:*": { "subcaseMS": 10152.221 }, + "webgpu:shader,execution,expression,call,builtin,distance:abstract_float_vec2:*": { "subcaseMS": 2896.823 }, + "webgpu:shader,execution,expression,call,builtin,distance:abstract_float_vec3:*": { "subcaseMS": 3191.871 }, + "webgpu:shader,execution,expression,call,builtin,distance:abstract_float_vec4:*": { "subcaseMS": 3250.958 }, "webgpu:shader,execution,expression,call,builtin,distance:f16:*": { "subcaseMS": 6675.626 }, "webgpu:shader,execution,expression,call,builtin,distance:f16_vec2:*": { "subcaseMS": 78.300 }, "webgpu:shader,execution,expression,call,builtin,distance:f16_vec3:*": { "subcaseMS": 47.925 }, @@ -1189,31 +1288,43 @@ "webgpu:shader,execution,expression,call,builtin,distance:f32_vec2:*": { "subcaseMS": 9.826 }, "webgpu:shader,execution,expression,call,builtin,distance:f32_vec3:*": { "subcaseMS": 10.901 }, "webgpu:shader,execution,expression,call,builtin,distance:f32_vec4:*": { "subcaseMS": 12.700 }, - "webgpu:shader,execution,expression,call,builtin,dot:abstract_float:*": { "subcaseMS": 8.902 }, - "webgpu:shader,execution,expression,call,builtin,dot:abstract_int:*": { "subcaseMS": 2.902 }, + "webgpu:shader,execution,expression,call,builtin,dot4I8Packed:basic:*": { "subcaseMS": 1.000 }, + "webgpu:shader,execution,expression,call,builtin,dot4U8Packed:basic:*": { "subcaseMS": 1.000 }, + "webgpu:shader,execution,expression,call,builtin,dot:abstract_float_vec2:*": { "subcaseMS": 3042.966 }, + "webgpu:shader,execution,expression,call,builtin,dot:abstract_float_vec3:*": { "subcaseMS": 980.205 }, + "webgpu:shader,execution,expression,call,builtin,dot:abstract_float_vec4:*": { "subcaseMS": 1036.933 }, + "webgpu:shader,execution,expression,call,builtin,dot:abstract_int_vec2:*": { "subcaseMS": 2570.488 }, + "webgpu:shader,execution,expression,call,builtin,dot:abstract_int_vec3:*": { "subcaseMS": 1848.038 }, + "webgpu:shader,execution,expression,call,builtin,dot:abstract_int_vec4:*": { "subcaseMS": 1742.054 }, "webgpu:shader,execution,expression,call,builtin,dot:f16_vec2:*": { "subcaseMS": 981.225 }, "webgpu:shader,execution,expression,call,builtin,dot:f16_vec3:*": { "subcaseMS": 50.350 }, "webgpu:shader,execution,expression,call,builtin,dot:f16_vec4:*": { "subcaseMS": 52.250 }, "webgpu:shader,execution,expression,call,builtin,dot:f32_vec2:*": { "subcaseMS": 210.350 }, "webgpu:shader,execution,expression,call,builtin,dot:f32_vec3:*": { "subcaseMS": 11.176 }, "webgpu:shader,execution,expression,call,builtin,dot:f32_vec4:*": { "subcaseMS": 11.876 }, - "webgpu:shader,execution,expression,call,builtin,dot:i32:*": { "subcaseMS": 3.103 }, - "webgpu:shader,execution,expression,call,builtin,dot:u32:*": { "subcaseMS": 3.101 }, + "webgpu:shader,execution,expression,call,builtin,dot:i32_vec2:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,dot:i32_vec3:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,dot:i32_vec4:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,dot:u32_vec2:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,dot:u32_vec3:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,builtin,dot:u32_vec4:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,expression,call,builtin,dpdx:f32:*": { "subcaseMS": 22.804 }, "webgpu:shader,execution,expression,call,builtin,dpdxCoarse:f32:*": { "subcaseMS": 22.404 }, "webgpu:shader,execution,expression,call,builtin,dpdxFine:f32:*": { "subcaseMS": 17.708 }, "webgpu:shader,execution,expression,call,builtin,dpdy:f32:*": { "subcaseMS": 17.006 }, "webgpu:shader,execution,expression,call,builtin,dpdyCoarse:f32:*": { "subcaseMS": 17.909 }, "webgpu:shader,execution,expression,call,builtin,dpdyFine:f32:*": { "subcaseMS": 16.806 }, - "webgpu:shader,execution,expression,call,builtin,exp2:abstract_float:*": { "subcaseMS": 22.705 }, + "webgpu:shader,execution,expression,call,builtin,exp2:abstract_float:*": { "subcaseMS": 17938.514 }, "webgpu:shader,execution,expression,call,builtin,exp2:f16:*": { "subcaseMS": 79.501 }, "webgpu:shader,execution,expression,call,builtin,exp2:f32:*": { "subcaseMS": 12.169 }, - "webgpu:shader,execution,expression,call,builtin,exp:abstract_float:*": { "subcaseMS": 17.210 }, + "webgpu:shader,execution,expression,call,builtin,exp:abstract_float:*": { "subcaseMS": 18734.085 }, "webgpu:shader,execution,expression,call,builtin,exp:f16:*": { "subcaseMS": 135.363 }, "webgpu:shader,execution,expression,call,builtin,exp:f32:*": { "subcaseMS": 12.557 }, "webgpu:shader,execution,expression,call,builtin,extractBits:i32:*": { "subcaseMS": 8.125 }, "webgpu:shader,execution,expression,call,builtin,extractBits:u32:*": { "subcaseMS": 7.838 }, - "webgpu:shader,execution,expression,call,builtin,faceForward:abstract_float:*": { "subcaseMS": 120.702 }, + "webgpu:shader,execution,expression,call,builtin,faceForward:abstract_float_vec2:*": { "subcaseMS": 4753.524 }, + "webgpu:shader,execution,expression,call,builtin,faceForward:abstract_float_vec3:*": { "subcaseMS": 4697.114 }, + "webgpu:shader,execution,expression,call,builtin,faceForward:abstract_float_vec4:*": { "subcaseMS": 3417.393 }, "webgpu:shader,execution,expression,call,builtin,faceForward:f16_vec2:*": { "subcaseMS": 485.775 }, "webgpu:shader,execution,expression,call,builtin,faceForward:f16_vec3:*": { "subcaseMS": 560.225 }, "webgpu:shader,execution,expression,call,builtin,faceForward:f16_vec4:*": { "subcaseMS": 670.325 }, @@ -1224,15 +1335,23 @@ "webgpu:shader,execution,expression,call,builtin,firstLeadingBit:u32:*": { "subcaseMS": 9.363 }, "webgpu:shader,execution,expression,call,builtin,firstTrailingBit:i32:*": { "subcaseMS": 8.132 }, "webgpu:shader,execution,expression,call,builtin,firstTrailingBit:u32:*": { "subcaseMS": 9.047 }, - "webgpu:shader,execution,expression,call,builtin,floor:abstract_float:*": { "subcaseMS": 34.108 }, + "webgpu:shader,execution,expression,call,builtin,floor:abstract_float:*": { "subcaseMS": 20061.136 }, "webgpu:shader,execution,expression,call,builtin,floor:f16:*": { "subcaseMS": 30.708 }, "webgpu:shader,execution,expression,call,builtin,floor:f32:*": { "subcaseMS": 10.119 }, - "webgpu:shader,execution,expression,call,builtin,fma:abstract_float:*": { "subcaseMS": 18.208 }, + "webgpu:shader,execution,expression,call,builtin,fma:abstract_float:*": { "subcaseMS": 148432.980 }, "webgpu:shader,execution,expression,call,builtin,fma:f16:*": { "subcaseMS": 485.857 }, "webgpu:shader,execution,expression,call,builtin,fma:f32:*": { "subcaseMS": 80.388 }, - "webgpu:shader,execution,expression,call,builtin,fract:abstract_float:*": { "subcaseMS": 17.408 }, + "webgpu:shader,execution,expression,call,builtin,fract:abstract_float:*": { "subcaseMS": 8575.078 }, "webgpu:shader,execution,expression,call,builtin,fract:f16:*": { "subcaseMS": 46.500 }, "webgpu:shader,execution,expression,call,builtin,fract:f32:*": { "subcaseMS": 12.269 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_exp:*": { "subcaseMS": 383.892 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_fract:*": { "subcaseMS": 934.813 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_vec2_exp:*": { "subcaseMS": 805.237 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_vec2_fract:*": { "subcaseMS": 1657.028 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_vec3_exp:*": { "subcaseMS": 1287.254 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_vec3_fract:*": { "subcaseMS": 2943.004 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_vec4_exp:*": { "subcaseMS": 1905.417 }, + "webgpu:shader,execution,expression,call,builtin,frexp:abstract_float_vec4_fract:*": { "subcaseMS": 4122.700 }, "webgpu:shader,execution,expression,call,builtin,frexp:f16_exp:*": { "subcaseMS": 8.503 }, "webgpu:shader,execution,expression,call,builtin,frexp:f16_fract:*": { "subcaseMS": 17.900 }, "webgpu:shader,execution,expression,call,builtin,frexp:f16_vec2_exp:*": { "subcaseMS": 1.801 }, @@ -1253,13 +1372,16 @@ "webgpu:shader,execution,expression,call,builtin,fwidthCoarse:f32:*": { "subcaseMS": 17.110 }, "webgpu:shader,execution,expression,call,builtin,fwidthFine:f32:*": { "subcaseMS": 16.906 }, "webgpu:shader,execution,expression,call,builtin,insertBits:integer:*": { "subcaseMS": 9.569 }, - "webgpu:shader,execution,expression,call,builtin,inversesqrt:abstract_float:*": { "subcaseMS": 24.310 }, + "webgpu:shader,execution,expression,call,builtin,inversesqrt:abstract_float:*": { "subcaseMS": 19408.045 }, "webgpu:shader,execution,expression,call,builtin,inversesqrt:f16:*": { "subcaseMS": 21.411 }, "webgpu:shader,execution,expression,call,builtin,inversesqrt:f32:*": { "subcaseMS": 50.125 }, - "webgpu:shader,execution,expression,call,builtin,ldexp:abstract_float:*": { "subcaseMS": 142.805 }, + "webgpu:shader,execution,expression,call,builtin,ldexp:abstract_float:*": { "subcaseMS": 87.640 }, "webgpu:shader,execution,expression,call,builtin,ldexp:f16:*": { "subcaseMS": 271.038 }, "webgpu:shader,execution,expression,call,builtin,ldexp:f32:*": { "subcaseMS": 161.250 }, - "webgpu:shader,execution,expression,call,builtin,length:abstract_float:*": { "subcaseMS": 31.303 }, + "webgpu:shader,execution,expression,call,builtin,length:abstract_float:*": { "subcaseMS": 377.202 }, + "webgpu:shader,execution,expression,call,builtin,length:abstract_float_vec2:*": { "subcaseMS": 2253.267 }, + "webgpu:shader,execution,expression,call,builtin,length:abstract_float_vec3:*": { "subcaseMS": 1989.791 }, + "webgpu:shader,execution,expression,call,builtin,length:abstract_float_vec4:*": { "subcaseMS": 1756.180 }, "webgpu:shader,execution,expression,call,builtin,length:f16:*": { "subcaseMS": 490.450 }, "webgpu:shader,execution,expression,call,builtin,length:f16_vec2:*": { "subcaseMS": 33.551 }, "webgpu:shader,execution,expression,call,builtin,length:f16_vec3:*": { "subcaseMS": 79.301 }, @@ -1268,28 +1390,28 @@ "webgpu:shader,execution,expression,call,builtin,length:f32_vec2:*": { "subcaseMS": 9.751 }, "webgpu:shader,execution,expression,call,builtin,length:f32_vec3:*": { "subcaseMS": 10.825 }, "webgpu:shader,execution,expression,call,builtin,length:f32_vec4:*": { "subcaseMS": 9.476 }, - "webgpu:shader,execution,expression,call,builtin,log2:abstract_float:*": { "subcaseMS": 23.607 }, + "webgpu:shader,execution,expression,call,builtin,log2:abstract_float:*": { "subcaseMS": 59147.901 }, "webgpu:shader,execution,expression,call,builtin,log2:f16:*": { "subcaseMS": 9.404 }, "webgpu:shader,execution,expression,call,builtin,log2:f32:*": { "subcaseMS": 27.838 }, - "webgpu:shader,execution,expression,call,builtin,log:abstract_float:*": { "subcaseMS": 17.911 }, + "webgpu:shader,execution,expression,call,builtin,log:abstract_float:*": { "subcaseMS": 63419.245 }, "webgpu:shader,execution,expression,call,builtin,log:f16:*": { "subcaseMS": 8.603 }, "webgpu:shader,execution,expression,call,builtin,log:f32:*": { "subcaseMS": 26.725 }, - "webgpu:shader,execution,expression,call,builtin,max:abstract_float:*": { "subcaseMS": 2810.001 }, + "webgpu:shader,execution,expression,call,builtin,max:abstract_float:*": { "subcaseMS": 31421.439 }, "webgpu:shader,execution,expression,call,builtin,max:abstract_int:*": { "subcaseMS": 33.508 }, "webgpu:shader,execution,expression,call,builtin,max:f16:*": { "subcaseMS": 37.404 }, "webgpu:shader,execution,expression,call,builtin,max:f32:*": { "subcaseMS": 300.619 }, "webgpu:shader,execution,expression,call,builtin,max:i32:*": { "subcaseMS": 7.350 }, "webgpu:shader,execution,expression,call,builtin,max:u32:*": { "subcaseMS": 6.700 }, - "webgpu:shader,execution,expression,call,builtin,min:abstract_float:*": { "subcaseMS": 3054.101 }, + "webgpu:shader,execution,expression,call,builtin,min:abstract_float:*": { "subcaseMS": 36353.551 }, "webgpu:shader,execution,expression,call,builtin,min:abstract_int:*": { "subcaseMS": 19.806 }, "webgpu:shader,execution,expression,call,builtin,min:f16:*": { "subcaseMS": 8.006 }, "webgpu:shader,execution,expression,call,builtin,min:f32:*": { "subcaseMS": 298.463 }, "webgpu:shader,execution,expression,call,builtin,min:i32:*": { "subcaseMS": 7.825 }, "webgpu:shader,execution,expression,call,builtin,min:u32:*": { "subcaseMS": 6.932 }, - "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_matching:*": { "subcaseMS": 215.206 }, - "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_nonmatching_vec2:*": { "subcaseMS": 14.601 }, - "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_nonmatching_vec3:*": { "subcaseMS": 18.302 }, - "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_nonmatching_vec4:*": { "subcaseMS": 12.602 }, + "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_matching:*": { "subcaseMS": 23421.613 }, + "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_nonmatching_vec2:*": { "subcaseMS": 8447.128 }, + "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_nonmatching_vec3:*": { "subcaseMS": 8537.399 }, + "webgpu:shader,execution,expression,call,builtin,mix:abstract_float_nonmatching_vec4:*": { "subcaseMS": 11860.514 }, "webgpu:shader,execution,expression,call,builtin,mix:f16_matching:*": { "subcaseMS": 321.700 }, "webgpu:shader,execution,expression,call,builtin,mix:f16_nonmatching_vec2:*": { "subcaseMS": 653.851 }, "webgpu:shader,execution,expression,call,builtin,mix:f16_nonmatching_vec3:*": { "subcaseMS": 832.076 }, @@ -1322,7 +1444,9 @@ "webgpu:shader,execution,expression,call,builtin,modf:f32_vec4_fract:*": { "subcaseMS": 147.876 }, "webgpu:shader,execution,expression,call,builtin,modf:f32_vec4_whole:*": { "subcaseMS": 134.576 }, "webgpu:shader,execution,expression,call,builtin,modf:f32_whole:*": { "subcaseMS": 94.025 }, - "webgpu:shader,execution,expression,call,builtin,normalize:abstract_float:*": { "subcaseMS": 28.508 }, + "webgpu:shader,execution,expression,call,builtin,normalize:abstract_float_vec2:*": { "subcaseMS": 3621.170 }, + "webgpu:shader,execution,expression,call,builtin,normalize:abstract_float_vec3:*": { "subcaseMS": 6170.292 }, + "webgpu:shader,execution,expression,call,builtin,normalize:abstract_float_vec4:*": { "subcaseMS": 7460.960 }, "webgpu:shader,execution,expression,call,builtin,normalize:f16_vec2:*": { "subcaseMS": 635.100 }, "webgpu:shader,execution,expression,call,builtin,normalize:f16_vec3:*": { "subcaseMS": 112.501 }, "webgpu:shader,execution,expression,call,builtin,normalize:f16_vec4:*": { "subcaseMS": 210.526 }, @@ -1334,21 +1458,29 @@ "webgpu:shader,execution,expression,call,builtin,pack2x16unorm:pack:*": { "subcaseMS": 9.525 }, "webgpu:shader,execution,expression,call,builtin,pack4x8snorm:pack:*": { "subcaseMS": 14.751 }, "webgpu:shader,execution,expression,call,builtin,pack4x8unorm:pack:*": { "subcaseMS": 14.575 }, - "webgpu:shader,execution,expression,call,builtin,pow:abstract_float:*": { "subcaseMS": 23.106 }, + "webgpu:shader,execution,expression,call,builtin,pack4xI8:basic:*": { "subcaseMS": 64.702 }, + "webgpu:shader,execution,expression,call,builtin,pack4xI8Clamp:basic:*": { "subcaseMS": 92.602 }, + "webgpu:shader,execution,expression,call,builtin,pack4xU8:basic:*": { "subcaseMS": 166.600 }, + "webgpu:shader,execution,expression,call,builtin,pack4xU8Clamp:basic:*": { "subcaseMS": 62.802 }, + "webgpu:shader,execution,expression,call,builtin,pow:abstract_float:*": { "subcaseMS": 30535.000 }, "webgpu:shader,execution,expression,call,builtin,pow:f16:*": { "subcaseMS": 816.063 }, "webgpu:shader,execution,expression,call,builtin,pow:f32:*": { "subcaseMS": 151.269 }, "webgpu:shader,execution,expression,call,builtin,quantizeToF16:f32:*": { "subcaseMS": 11.063 }, - "webgpu:shader,execution,expression,call,builtin,radians:abstract_float:*": { "subcaseMS": 492.827 }, + "webgpu:shader,execution,expression,call,builtin,radians:abstract_float:*": { "subcaseMS": 12268.988 }, "webgpu:shader,execution,expression,call,builtin,radians:f16:*": { "subcaseMS": 18.707 }, "webgpu:shader,execution,expression,call,builtin,radians:f32:*": { "subcaseMS": 74.432 }, - "webgpu:shader,execution,expression,call,builtin,reflect:abstract_float:*": { "subcaseMS": 47.108 }, + "webgpu:shader,execution,expression,call,builtin,reflect:abstract_float_vec2:*": { "subcaseMS": 5636.961 }, + "webgpu:shader,execution,expression,call,builtin,reflect:abstract_float_vec3:*": { "subcaseMS": 10753.506 }, + "webgpu:shader,execution,expression,call,builtin,reflect:abstract_float_vec4:*": { "subcaseMS": 13283.920 }, "webgpu:shader,execution,expression,call,builtin,reflect:f16_vec2:*": { "subcaseMS": 76.975 }, "webgpu:shader,execution,expression,call,builtin,reflect:f16_vec3:*": { "subcaseMS": 69.451 }, "webgpu:shader,execution,expression,call,builtin,reflect:f16_vec4:*": { "subcaseMS": 79.826 }, "webgpu:shader,execution,expression,call,builtin,reflect:f32_vec2:*": { "subcaseMS": 1182.226 }, "webgpu:shader,execution,expression,call,builtin,reflect:f32_vec3:*": { "subcaseMS": 56.326 }, "webgpu:shader,execution,expression,call,builtin,reflect:f32_vec4:*": { "subcaseMS": 65.250 }, - "webgpu:shader,execution,expression,call,builtin,refract:abstract_float:*": { "subcaseMS": 114.404 }, + "webgpu:shader,execution,expression,call,builtin,refract:abstract_float_vec2:*": { "subcaseMS": 2981.912 }, + "webgpu:shader,execution,expression,call,builtin,refract:abstract_float_vec3:*": { "subcaseMS": 3440.181 }, + "webgpu:shader,execution,expression,call,builtin,refract:abstract_float_vec4:*": { "subcaseMS": 5284.328 }, "webgpu:shader,execution,expression,call,builtin,refract:f16_vec2:*": { "subcaseMS": 536.225 }, "webgpu:shader,execution,expression,call,builtin,refract:f16_vec3:*": { "subcaseMS": 627.450 }, "webgpu:shader,execution,expression,call,builtin,refract:f16_vec4:*": { "subcaseMS": 699.801 }, @@ -1357,46 +1489,46 @@ "webgpu:shader,execution,expression,call,builtin,refract:f32_vec4:*": { "subcaseMS": 610.150 }, "webgpu:shader,execution,expression,call,builtin,reverseBits:i32:*": { "subcaseMS": 9.594 }, "webgpu:shader,execution,expression,call,builtin,reverseBits:u32:*": { "subcaseMS": 7.969 }, - "webgpu:shader,execution,expression,call,builtin,round:abstract_float:*": { "subcaseMS": 19.408 }, + "webgpu:shader,execution,expression,call,builtin,round:abstract_float:*": { "subcaseMS": 15818.921 }, "webgpu:shader,execution,expression,call,builtin,round:f16:*": { "subcaseMS": 30.509 }, "webgpu:shader,execution,expression,call,builtin,round:f32:*": { "subcaseMS": 12.407 }, - "webgpu:shader,execution,expression,call,builtin,saturate:abstract_float:*": { "subcaseMS": 527.425 }, + "webgpu:shader,execution,expression,call,builtin,saturate:abstract_float:*": { "subcaseMS": 15450.768 }, "webgpu:shader,execution,expression,call,builtin,saturate:f16:*": { "subcaseMS": 23.407 }, "webgpu:shader,execution,expression,call,builtin,saturate:f32:*": { "subcaseMS": 116.275 }, "webgpu:shader,execution,expression,call,builtin,select:scalar:*": { "subcaseMS": 6.882 }, "webgpu:shader,execution,expression,call,builtin,select:vector:*": { "subcaseMS": 7.096 }, - "webgpu:shader,execution,expression,call,builtin,sign:abstract_float:*": { "subcaseMS": 412.925 }, + "webgpu:shader,execution,expression,call,builtin,sign:abstract_float:*": { "subcaseMS": 17131.997 }, "webgpu:shader,execution,expression,call,builtin,sign:abstract_int:*": { "subcaseMS": 25.806 }, "webgpu:shader,execution,expression,call,builtin,sign:f16:*": { "subcaseMS": 25.103 }, "webgpu:shader,execution,expression,call,builtin,sign:f32:*": { "subcaseMS": 8.188 }, "webgpu:shader,execution,expression,call,builtin,sign:i32:*": { "subcaseMS": 10.225 }, - "webgpu:shader,execution,expression,call,builtin,sin:abstract_float:*": { "subcaseMS": 19.206 }, + "webgpu:shader,execution,expression,call,builtin,sin:abstract_float:*": { "subcaseMS": 17098.512 }, "webgpu:shader,execution,expression,call,builtin,sin:f16:*": { "subcaseMS": 8.707 }, "webgpu:shader,execution,expression,call,builtin,sin:f32:*": { "subcaseMS": 26.826 }, - "webgpu:shader,execution,expression,call,builtin,sinh:abstract_float:*": { "subcaseMS": 22.009 }, + "webgpu:shader,execution,expression,call,builtin,sinh:abstract_float:*": { "subcaseMS": 11721.224 }, "webgpu:shader,execution,expression,call,builtin,sinh:f16:*": { "subcaseMS": 58.288 }, "webgpu:shader,execution,expression,call,builtin,sinh:f32:*": { "subcaseMS": 11.038 }, - "webgpu:shader,execution,expression,call,builtin,smoothstep:abstract_float:*": { "subcaseMS": 23.807 }, + "webgpu:shader,execution,expression,call,builtin,smoothstep:abstract_float:*": { "subcaseMS": 73577.621 }, "webgpu:shader,execution,expression,call,builtin,smoothstep:f16:*": { "subcaseMS": 616.457 }, "webgpu:shader,execution,expression,call,builtin,smoothstep:f32:*": { "subcaseMS": 88.063 }, - "webgpu:shader,execution,expression,call,builtin,sqrt:abstract_float:*": { "subcaseMS": 19.004 }, + "webgpu:shader,execution,expression,call,builtin,sqrt:abstract_float:*": { "subcaseMS": 1545.649 }, "webgpu:shader,execution,expression,call,builtin,sqrt:f16:*": { "subcaseMS": 22.908 }, "webgpu:shader,execution,expression,call,builtin,sqrt:f32:*": { "subcaseMS": 10.813 }, - "webgpu:shader,execution,expression,call,builtin,step:abstract_float:*": { "subcaseMS": 19.104 }, + "webgpu:shader,execution,expression,call,builtin,step:abstract_float:*": { "subcaseMS": 92.310 }, "webgpu:shader,execution,expression,call,builtin,step:f16:*": { "subcaseMS": 32.508 }, "webgpu:shader,execution,expression,call,builtin,step:f32:*": { "subcaseMS": 291.363 }, "webgpu:shader,execution,expression,call,builtin,storageBarrier:barrier:*": { "subcaseMS": 0.801 }, "webgpu:shader,execution,expression,call,builtin,storageBarrier:stage:*": { "subcaseMS": 2.402 }, - "webgpu:shader,execution,expression,call,builtin,tan:abstract_float:*": { "subcaseMS": 31.007 }, + "webgpu:shader,execution,expression,call,builtin,tan:abstract_float:*": { "subcaseMS": 17043.428 }, "webgpu:shader,execution,expression,call,builtin,tan:f16:*": { "subcaseMS": 116.157 }, "webgpu:shader,execution,expression,call,builtin,tan:f32:*": { "subcaseMS": 13.532 }, - "webgpu:shader,execution,expression,call,builtin,tanh:abstract_float:*": { "subcaseMS": 18.406 }, + "webgpu:shader,execution,expression,call,builtin,tanh:abstract_float:*": { "subcaseMS": 13578.577 }, "webgpu:shader,execution,expression,call,builtin,tanh:f16:*": { "subcaseMS": 75.982 }, "webgpu:shader,execution,expression,call,builtin,tanh:f32:*": { "subcaseMS": 32.719 }, - "webgpu:shader,execution,expression,call,builtin,textureDimension:depth:*": { "subcaseMS": 20.801 }, - "webgpu:shader,execution,expression,call,builtin,textureDimension:external:*": { "subcaseMS": 1.700 }, - "webgpu:shader,execution,expression,call,builtin,textureDimension:sampled:*": { "subcaseMS": 16.506 }, - "webgpu:shader,execution,expression,call,builtin,textureDimension:storage:*": { "subcaseMS": 25.907 }, + "webgpu:shader,execution,expression,call,builtin,textureDimensions:depth:*": { "subcaseMS": 20.801 }, + "webgpu:shader,execution,expression,call,builtin,textureDimensions:external:*": { "subcaseMS": 1.700 }, + "webgpu:shader,execution,expression,call,builtin,textureDimensions:sampled_and_multisampled:*": { "subcaseMS": 16.506 }, + "webgpu:shader,execution,expression,call,builtin,textureDimensions:storage:*": { "subcaseMS": 25.907 }, "webgpu:shader,execution,expression,call,builtin,textureGather:depth_2d_coords:*": { "subcaseMS": 11.601 }, "webgpu:shader,execution,expression,call,builtin,textureGather:depth_3d_coords:*": { "subcaseMS": 2.200 }, "webgpu:shader,execution,expression,call,builtin,textureGather:depth_array_2d_coords:*": { "subcaseMS": 23.801 }, @@ -1423,7 +1555,6 @@ "webgpu:shader,execution,expression,call,builtin,textureNumLevels:sampled:*": { "subcaseMS": 6.201 }, "webgpu:shader,execution,expression,call,builtin,textureNumSamples:depth:*": { "subcaseMS": 1.101 }, "webgpu:shader,execution,expression,call,builtin,textureNumSamples:sampled:*": { "subcaseMS": 6.600 }, - "webgpu:shader,execution,expression,call,builtin,textureSample:control_flow:*": { "subcaseMS": 2.801 }, "webgpu:shader,execution,expression,call,builtin,textureSample:depth_2d_coords:*": { "subcaseMS": 12.301 }, "webgpu:shader,execution,expression,call,builtin,textureSample:depth_3d_coords:*": { "subcaseMS": 2.101 }, "webgpu:shader,execution,expression,call,builtin,textureSample:depth_array_2d_coords:*": { "subcaseMS": 92.601 }, @@ -1433,19 +1564,14 @@ "webgpu:shader,execution,expression,call,builtin,textureSample:sampled_3d_coords:*": { "subcaseMS": 36.002 }, "webgpu:shader,execution,expression,call,builtin,textureSample:sampled_array_2d_coords:*": { "subcaseMS": 92.500 }, "webgpu:shader,execution,expression,call,builtin,textureSample:sampled_array_3d_coords:*": { "subcaseMS": 20.200 }, - "webgpu:shader,execution,expression,call,builtin,textureSample:stage:*": { "subcaseMS": 3.000 }, "webgpu:shader,execution,expression,call,builtin,textureSampleBias:arrayed_2d_coords:*": { "subcaseMS": 585.100 }, "webgpu:shader,execution,expression,call,builtin,textureSampleBias:arrayed_3d_coords:*": { "subcaseMS": 121.600 }, - "webgpu:shader,execution,expression,call,builtin,textureSampleBias:control_flow:*": { "subcaseMS": 2.502 }, "webgpu:shader,execution,expression,call,builtin,textureSampleBias:sampled_2d_coords:*": { "subcaseMS": 48.601 }, "webgpu:shader,execution,expression,call,builtin,textureSampleBias:sampled_3d_coords:*": { "subcaseMS": 133.600 }, - "webgpu:shader,execution,expression,call,builtin,textureSampleBias:stage:*": { "subcaseMS": 2.803 }, "webgpu:shader,execution,expression,call,builtin,textureSampleCompare:2d_coords:*": { "subcaseMS": 24.000 }, "webgpu:shader,execution,expression,call,builtin,textureSampleCompare:3d_coords:*": { "subcaseMS": 9.000 }, "webgpu:shader,execution,expression,call,builtin,textureSampleCompare:arrayed_2d_coords:*": { "subcaseMS": 295.601 }, "webgpu:shader,execution,expression,call,builtin,textureSampleCompare:arrayed_3d_coords:*": { "subcaseMS": 60.301 }, - "webgpu:shader,execution,expression,call,builtin,textureSampleCompare:control_flow:*": { "subcaseMS": 2.702 }, - "webgpu:shader,execution,expression,call,builtin,textureSampleCompare:stage:*": { "subcaseMS": 7.701 }, "webgpu:shader,execution,expression,call,builtin,textureSampleCompareLevel:2d_coords:*": { "subcaseMS": 30.401 }, "webgpu:shader,execution,expression,call,builtin,textureSampleCompareLevel:3d_coords:*": { "subcaseMS": 10.301 }, "webgpu:shader,execution,expression,call,builtin,textureSampleCompareLevel:arrayed_2d_coords:*": { "subcaseMS": 705.100 }, @@ -1467,10 +1593,10 @@ "webgpu:shader,execution,expression,call,builtin,textureStore:store_2d_coords:*": { "subcaseMS": 28.809 }, "webgpu:shader,execution,expression,call,builtin,textureStore:store_3d_coords:*": { "subcaseMS": 37.206 }, "webgpu:shader,execution,expression,call,builtin,textureStore:store_array_2d_coords:*": { "subcaseMS": 98.804 }, - "webgpu:shader,execution,expression,call,builtin,transpose:abstract_float:*": { "subcaseMS": 755.012 }, + "webgpu:shader,execution,expression,call,builtin,transpose:abstract_float:*": { "subcaseMS": 64537.678 }, "webgpu:shader,execution,expression,call,builtin,transpose:f16:*": { "subcaseMS": 33.311 }, "webgpu:shader,execution,expression,call,builtin,transpose:f32:*": { "subcaseMS": 75.887 }, - "webgpu:shader,execution,expression,call,builtin,trunc:abstract_float:*": { "subcaseMS": 455.726 }, + "webgpu:shader,execution,expression,call,builtin,trunc:abstract_float:*": { "subcaseMS": 12197.517 }, "webgpu:shader,execution,expression,call,builtin,trunc:f16:*": { "subcaseMS": 120.204 }, "webgpu:shader,execution,expression,call,builtin,trunc:f32:*": { "subcaseMS": 48.544 }, "webgpu:shader,execution,expression,call,builtin,unpack2x16float:unpack:*": { "subcaseMS": 11.651 }, @@ -1478,12 +1604,54 @@ "webgpu:shader,execution,expression,call,builtin,unpack2x16unorm:unpack:*": { "subcaseMS": 8.701 }, "webgpu:shader,execution,expression,call,builtin,unpack4x8snorm:unpack:*": { "subcaseMS": 12.275 }, "webgpu:shader,execution,expression,call,builtin,unpack4x8unorm:unpack:*": { "subcaseMS": 11.776 }, + "webgpu:shader,execution,expression,call,builtin,unpack4xI8:basic:*": { "subcaseMS": 76.901 }, + "webgpu:shader,execution,expression,call,builtin,unpack4xU8:basic:*": { "subcaseMS": 78.501 }, "webgpu:shader,execution,expression,call,builtin,workgroupBarrier:barrier:*": { "subcaseMS": 0.701 }, "webgpu:shader,execution,expression,call,builtin,workgroupBarrier:stage:*": { "subcaseMS": 1.801 }, + "webgpu:shader,execution,expression,call,builtin,workgroupUniformLoad:types:*": { "subcaseMS": 1.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:array_length:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:atomic_ptr_to_element:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:mixed_ptr_parameters:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:read_full_object:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:read_ptr_to_element:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:read_ptr_to_member:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:write_full_object:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:write_ptr_to_element:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,call,user,ptr_params:write_ptr_to_member:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,constructor,non_zero:abstract_array_elements:*": { "subcaseMS": 115.862 }, + "webgpu:shader,execution,expression,constructor,non_zero:abstract_matrix_column_vectors:*": { "subcaseMS": 3363.820 }, + "webgpu:shader,execution,expression,constructor,non_zero:abstract_matrix_elements:*": { "subcaseMS": 35.110 }, + "webgpu:shader,execution,expression,constructor,non_zero:abstract_vector_elements:*": { "subcaseMS": 52.760 }, + "webgpu:shader,execution,expression,constructor,non_zero:abstract_vector_mix:*": { "subcaseMS": 29.135 }, + "webgpu:shader,execution,expression,constructor,non_zero:abstract_vector_splat:*": { "subcaseMS": 70.635 }, + "webgpu:shader,execution,expression,constructor,non_zero:concrete_array_elements:*": { "subcaseMS": 1578.242 }, + "webgpu:shader,execution,expression,constructor,non_zero:concrete_matrix_column_vectors:*": { "subcaseMS": 1125.607 }, + "webgpu:shader,execution,expression,constructor,non_zero:concrete_matrix_elements:*": { "subcaseMS": 2352.444 }, + "webgpu:shader,execution,expression,constructor,non_zero:concrete_vector_elements:*": { "subcaseMS": 2697.119 }, + "webgpu:shader,execution,expression,constructor,non_zero:concrete_vector_mix:*": { "subcaseMS": 4056.031 }, + "webgpu:shader,execution,expression,constructor,non_zero:concrete_vector_splat:*": { "subcaseMS": 10222.094 }, + "webgpu:shader,execution,expression,constructor,non_zero:matrix_identity:*": { "subcaseMS": 1137.176 }, + "webgpu:shader,execution,expression,constructor,non_zero:scalar_identity:*": { "subcaseMS": 3153.723 }, + "webgpu:shader,execution,expression,constructor,non_zero:structure:*": { "subcaseMS": 0.303 }, + "webgpu:shader,execution,expression,constructor,non_zero:vector_identity:*": { "subcaseMS": 2274.048 }, + "webgpu:shader,execution,expression,constructor,zero_value:array:*": { "subcaseMS": 500.400 }, + "webgpu:shader,execution,expression,constructor,zero_value:matrix:*": { "subcaseMS": 205.881 }, + "webgpu:shader,execution,expression,constructor,zero_value:scalar:*": { "subcaseMS": 39.484 }, + "webgpu:shader,execution,expression,constructor,zero_value:structure:*": { "subcaseMS": 0.650 }, + "webgpu:shader,execution,expression,constructor,zero_value:vector:*": { "subcaseMS": 159.975 }, + "webgpu:shader,execution,expression,precedence:precedence:*": { "subcaseMS": 531.048 }, + "webgpu:shader,execution,expression,unary,address_of_and_indirection:deref:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,unary,address_of_and_indirection:deref_index:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,unary,address_of_and_indirection:deref_member:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,expression,unary,af_arithmetic:negation:*": { "subcaseMS": 2165.950 }, "webgpu:shader,execution,expression,unary,af_assignment:abstract:*": { "subcaseMS": 788.400 }, "webgpu:shader,execution,expression,unary,af_assignment:f16:*": { "subcaseMS": 1.000 }, "webgpu:shader,execution,expression,unary,af_assignment:f32:*": { "subcaseMS": 42.000 }, + "webgpu:shader,execution,expression,unary,ai_arithmetic:negation:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,unary,ai_assignment:abstract:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,unary,ai_assignment:i32:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,unary,ai_assignment:u32:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,expression,unary,ai_complement:complement:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,expression,unary,bool_conversion:bool:*": { "subcaseMS": 8.357 }, "webgpu:shader,execution,expression,unary,bool_conversion:f16:*": { "subcaseMS": 44.794 }, "webgpu:shader,execution,expression,unary,bool_conversion:f32:*": { "subcaseMS": 41.276 }, @@ -1491,6 +1659,9 @@ "webgpu:shader,execution,expression,unary,bool_conversion:u32:*": { "subcaseMS": 7.401 }, "webgpu:shader,execution,expression,unary,bool_logical:negation:*": { "subcaseMS": 6.413 }, "webgpu:shader,execution,expression,unary,f16_arithmetic:negation:*": { "subcaseMS": 117.604 }, + "webgpu:shader,execution,expression,unary,f16_conversion:abstract_float:*": { "subcaseMS": 416.757 }, + "webgpu:shader,execution,expression,unary,f16_conversion:abstract_float_mat:*": { "subcaseMS": 1142.700 }, + "webgpu:shader,execution,expression,unary,f16_conversion:abstract_int:*": { "subcaseMS": 50.186 }, "webgpu:shader,execution,expression,unary,f16_conversion:bool:*": { "subcaseMS": 34.694 }, "webgpu:shader,execution,expression,unary,f16_conversion:f16:*": { "subcaseMS": 36.013 }, "webgpu:shader,execution,expression,unary,f16_conversion:f16_mat:*": { "subcaseMS": 47.109 }, @@ -1508,12 +1679,15 @@ "webgpu:shader,execution,expression,unary,f32_conversion:u32:*": { "subcaseMS": 7.132 }, "webgpu:shader,execution,expression,unary,i32_arithmetic:negation:*": { "subcaseMS": 7.244 }, "webgpu:shader,execution,expression,unary,i32_complement:i32_complement:*": { "subcaseMS": 9.075 }, + "webgpu:shader,execution,expression,unary,i32_conversion:abstract_float:*": { "subcaseMS": 4.333 }, + "webgpu:shader,execution,expression,unary,i32_conversion:abstract_int:*": { "subcaseMS": 167.273 }, "webgpu:shader,execution,expression,unary,i32_conversion:bool:*": { "subcaseMS": 6.457 }, "webgpu:shader,execution,expression,unary,i32_conversion:f16:*": { "subcaseMS": 44.363 }, "webgpu:shader,execution,expression,unary,i32_conversion:f32:*": { "subcaseMS": 8.275 }, "webgpu:shader,execution,expression,unary,i32_conversion:i32:*": { "subcaseMS": 7.707 }, "webgpu:shader,execution,expression,unary,i32_conversion:u32:*": { "subcaseMS": 6.969 }, "webgpu:shader,execution,expression,unary,u32_complement:u32_complement:*": { "subcaseMS": 7.632 }, + "webgpu:shader,execution,expression,unary,u32_conversion:abstract_float:*": { "subcaseMS": 68.918 }, "webgpu:shader,execution,expression,unary,u32_conversion:abstract_int:*": { "subcaseMS": 20.406 }, "webgpu:shader,execution,expression,unary,u32_conversion:bool:*": { "subcaseMS": 7.713 }, "webgpu:shader,execution,expression,unary,u32_conversion:f16:*": { "subcaseMS": 34.251 }, @@ -1521,6 +1695,10 @@ "webgpu:shader,execution,expression,unary,u32_conversion:i32:*": { "subcaseMS": 8.319 }, "webgpu:shader,execution,expression,unary,u32_conversion:u32:*": { "subcaseMS": 7.057 }, "webgpu:shader,execution,float_parse:valid:*": { "subcaseMS": 6.801 }, + "webgpu:shader,execution,flow_control,call:arg_eval:*": { "subcaseMS": 40.386 }, + "webgpu:shader,execution,flow_control,call:arg_eval_logical_and:*": { "subcaseMS": 43.760 }, + "webgpu:shader,execution,flow_control,call:arg_eval_logical_or:*": { "subcaseMS": 48.924 }, + "webgpu:shader,execution,flow_control,call:arg_eval_pointers:*": { "subcaseMS": 147.050 }, "webgpu:shader,execution,flow_control,call:call_basic:*": { "subcaseMS": 4.901 }, "webgpu:shader,execution,flow_control,call:call_nested:*": { "subcaseMS": 5.500 }, "webgpu:shader,execution,flow_control,call:call_repeated:*": { "subcaseMS": 10.851 }, @@ -1570,6 +1748,8 @@ "webgpu:shader,execution,flow_control,for:for_continue:*": { "subcaseMS": 10.601 }, "webgpu:shader,execution,flow_control,for:for_continuing:*": { "subcaseMS": 5.000 }, "webgpu:shader,execution,flow_control,for:for_initalizer:*": { "subcaseMS": 7.751 }, + "webgpu:shader,execution,flow_control,for:for_logical_and_condition:*": { "subcaseMS": 46.477 }, + "webgpu:shader,execution,flow_control,for:for_logical_or_condition:*": { "subcaseMS": 48.615 }, "webgpu:shader,execution,flow_control,for:nested_for_break:*": { "subcaseMS": 5.901 }, "webgpu:shader,execution,flow_control,for:nested_for_continue:*": { "subcaseMS": 12.851 }, "webgpu:shader,execution,flow_control,if:else_if:*": { "subcaseMS": 7.950 }, @@ -1577,6 +1757,8 @@ "webgpu:shader,execution,flow_control,if:if_true:*": { "subcaseMS": 4.850 }, "webgpu:shader,execution,flow_control,if:nested_if_else:*": { "subcaseMS": 11.650 }, "webgpu:shader,execution,flow_control,loop:loop_break:*": { "subcaseMS": 6.000 }, + "webgpu:shader,execution,flow_control,loop:loop_break_if_logical_and_condition:*": { "subcaseMS": 6.827 }, + "webgpu:shader,execution,flow_control,loop:loop_break_if_logical_or_condition:*": { "subcaseMS": 5.846 }, "webgpu:shader,execution,flow_control,loop:loop_continue:*": { "subcaseMS": 11.200 }, "webgpu:shader,execution,flow_control,loop:loop_continuing_basic:*": { "subcaseMS": 12.450 }, "webgpu:shader,execution,flow_control,loop:nested_loops:*": { "subcaseMS": 12.900 }, @@ -1591,13 +1773,18 @@ "webgpu:shader,execution,flow_control,switch:switch:*": { "subcaseMS": 12.750 }, "webgpu:shader,execution,flow_control,switch:switch_default:*": { "subcaseMS": 5.400 }, "webgpu:shader,execution,flow_control,switch:switch_default_only:*": { "subcaseMS": 12.550 }, + "webgpu:shader,execution,flow_control,switch:switch_inside_loop_with_continue:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,flow_control,switch:switch_multiple_case:*": { "subcaseMS": 5.550 }, "webgpu:shader,execution,flow_control,switch:switch_multiple_case_default:*": { "subcaseMS": 12.000 }, "webgpu:shader,execution,flow_control,while:while_basic:*": { "subcaseMS": 5.951 }, "webgpu:shader,execution,flow_control,while:while_break:*": { "subcaseMS": 12.450 }, "webgpu:shader,execution,flow_control,while:while_continue:*": { "subcaseMS": 5.650 }, + "webgpu:shader,execution,flow_control,while:while_logical_and_condition:*": { "subcaseMS": 55.574 }, + "webgpu:shader,execution,flow_control,while:while_logical_or_condition:*": { "subcaseMS": 49.961 }, "webgpu:shader,execution,flow_control,while:while_nested_break:*": { "subcaseMS": 12.701 }, "webgpu:shader,execution,flow_control,while:while_nested_continue:*": { "subcaseMS": 5.450 }, + "webgpu:shader,execution,memory_layout:read_layout:*": { "subcaseMS": 0.000 }, + "webgpu:shader,execution,memory_layout:write_layout:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,memory_model,adjacent:f16:*": { "subcaseMS": 23.625 }, "webgpu:shader,execution,memory_model,atomicity:atomicity:*": { "subcaseMS": 77.201 }, "webgpu:shader,execution,memory_model,barrier:workgroup_barrier_load_store:*": { "subcaseMS": 65.850 }, @@ -1608,6 +1795,7 @@ "webgpu:shader,execution,memory_model,coherence:corw2:*": { "subcaseMS": 244.384 }, "webgpu:shader,execution,memory_model,coherence:cowr:*": { "subcaseMS": 250.484 }, "webgpu:shader,execution,memory_model,coherence:coww:*": { "subcaseMS": 245.850 }, + "webgpu:shader,execution,memory_model,texture_intra_invocation_coherence:texture_intra_invocation_coherence:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,memory_model,weak:2_plus_2_write:*": { "subcaseMS": 185.150 }, "webgpu:shader,execution,memory_model,weak:load_buffer:*": { "subcaseMS": 184.900 }, "webgpu:shader,execution,memory_model,weak:message_passing:*": { "subcaseMS": 196.550 }, @@ -1625,9 +1813,17 @@ "webgpu:shader,execution,robust_access:linear_memory:*": { "subcaseMS": 5.293 }, "webgpu:shader,execution,robust_access_vertex:vertex_buffer_access:*": { "subcaseMS": 6.487 }, "webgpu:shader,execution,shader_io,compute_builtins:inputs:*": { "subcaseMS": 19.342 }, + "webgpu:shader,execution,shader_io,fragment_builtins:inputs,front_facing:*": { "subcaseMS": 1.001 }, + "webgpu:shader,execution,shader_io,fragment_builtins:inputs,interStage,centroid:*": { "subcaseMS": 1.001 }, + "webgpu:shader,execution,shader_io,fragment_builtins:inputs,interStage:*": { "subcaseMS": 1.001 }, + "webgpu:shader,execution,shader_io,fragment_builtins:inputs,position:*": { "subcaseMS": 1.001 }, + "webgpu:shader,execution,shader_io,fragment_builtins:inputs,sample_index:*": { "subcaseMS": 1.001 }, + "webgpu:shader,execution,shader_io,fragment_builtins:inputs,sample_mask:*": { "subcaseMS": 1.001 }, "webgpu:shader,execution,shader_io,shared_structs:shared_between_stages:*": { "subcaseMS": 9.601 }, "webgpu:shader,execution,shader_io,shared_structs:shared_with_buffer:*": { "subcaseMS": 20.701 }, "webgpu:shader,execution,shader_io,shared_structs:shared_with_non_entry_point_function:*": { "subcaseMS": 6.801 }, + "webgpu:shader,execution,shader_io,user_io:passthrough:*": { "subcaseMS": 373.385 }, + "webgpu:shader,execution,shader_io,workgroup_size:workgroup_size:*": { "subcaseMS": 0.000 }, "webgpu:shader,execution,shadow:builtin:*": { "subcaseMS": 4.700 }, "webgpu:shader,execution,shadow:declaration:*": { "subcaseMS": 9.700 }, "webgpu:shader,execution,shadow:for_loop:*": { "subcaseMS": 17.201 }, @@ -1635,6 +1831,15 @@ "webgpu:shader,execution,shadow:loop:*": { "subcaseMS": 4.901 }, "webgpu:shader,execution,shadow:switch:*": { "subcaseMS": 4.601 }, "webgpu:shader,execution,shadow:while:*": { "subcaseMS": 7.400 }, + "webgpu:shader,execution,stage:basic_compute:*": { "subcaseMS": 1.000 }, + "webgpu:shader,execution,stage:basic_render:*": { "subcaseMS": 1.000 }, + "webgpu:shader,execution,statement,compound:decl:*": { "subcaseMS": 29.767 }, + "webgpu:shader,execution,statement,discard:all:*": { "subcaseMS": 36.094 }, + "webgpu:shader,execution,statement,discard:derivatives:*": { "subcaseMS": 15.287 }, + "webgpu:shader,execution,statement,discard:function_call:*": { "subcaseMS": 11.744 }, + "webgpu:shader,execution,statement,discard:loop:*": { "subcaseMS": 11.821 }, + "webgpu:shader,execution,statement,discard:three_quarters:*": { "subcaseMS": 34.735 }, + "webgpu:shader,execution,statement,discard:uniform_read_loop:*": { "subcaseMS": 13.095 }, "webgpu:shader,execution,statement,increment_decrement:frexp_exp_increment:*": { "subcaseMS": 4.700 }, "webgpu:shader,execution,statement,increment_decrement:scalar_i32_decrement:*": { "subcaseMS": 20.301 }, "webgpu:shader,execution,statement,increment_decrement:scalar_i32_decrement_underflow:*": { "subcaseMS": 4.900 }, @@ -1658,12 +1863,33 @@ "webgpu:shader,validation,const_assert,const_assert:constant_expression_logical_or_no_assert:*": { "subcaseMS": 1.373 }, "webgpu:shader,validation,const_assert,const_assert:constant_expression_no_assert:*": { "subcaseMS": 1.655 }, "webgpu:shader,validation,const_assert,const_assert:evaluation_stage:*": { "subcaseMS": 3.367 }, + "webgpu:shader,validation,decl,compound_statement:decl_conflict:*": { "subcaseMS": 5.225 }, + "webgpu:shader,validation,decl,compound_statement:decl_use:*": { "subcaseMS": 0.625 }, + "webgpu:shader,validation,decl,const:function_scope:*": { "subcaseMS": 2.088 }, + "webgpu:shader,validation,decl,const:initializer:*": { "subcaseMS": 0.768 }, "webgpu:shader,validation,decl,const:no_direct_recursion:*": { "subcaseMS": 0.951 }, "webgpu:shader,validation,decl,const:no_indirect_recursion:*": { "subcaseMS": 0.950 }, "webgpu:shader,validation,decl,const:no_indirect_recursion_via_array_size:*": { "subcaseMS": 2.601 }, "webgpu:shader,validation,decl,const:no_indirect_recursion_via_struct_attribute:*": { "subcaseMS": 1.034 }, + "webgpu:shader,validation,decl,const:type:*": { "subcaseMS": 10.651 }, + "webgpu:shader,validation,decl,context_dependent_resolution:attribute_names:*": { "subcaseMS": 533.132 }, + "webgpu:shader,validation,decl,context_dependent_resolution:builtin_value_names:*": { "subcaseMS": 25.538 }, + "webgpu:shader,validation,decl,context_dependent_resolution:diagnostic_rule_names:*": { "subcaseMS": 6.860 }, + "webgpu:shader,validation,decl,context_dependent_resolution:diagnostic_severity_names:*": { "subcaseMS": 6.252 }, + "webgpu:shader,validation,decl,context_dependent_resolution:enable_names:*": { "subcaseMS": 2.226 }, + "webgpu:shader,validation,decl,context_dependent_resolution:interpolation_sampling_names:*": { "subcaseMS": 4.971 }, + "webgpu:shader,validation,decl,context_dependent_resolution:interpolation_type_names:*": { "subcaseMS": 4.687 }, + "webgpu:shader,validation,decl,context_dependent_resolution:language_names:*": { "subcaseMS": 4.920 }, + "webgpu:shader,validation,decl,context_dependent_resolution:swizzle_names:*": { "subcaseMS": 27.579 }, + "webgpu:shader,validation,decl,let:initializer:*": { "subcaseMS": 0.706 }, + "webgpu:shader,validation,decl,let:module_scope:*": { "subcaseMS": 0.619 }, + "webgpu:shader,validation,decl,let:type:*": { "subcaseMS": 122.199 }, + "webgpu:shader,validation,decl,override:function_scope:*": { "subcaseMS": 1.003 }, + "webgpu:shader,validation,decl,override:id:*": { "subcaseMS": 69.432 }, + "webgpu:shader,validation,decl,override:initializer:*": { "subcaseMS": 4.810 }, "webgpu:shader,validation,decl,override:no_direct_recursion:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,decl,override:no_indirect_recursion:*": { "subcaseMS": 0.951 }, + "webgpu:shader,validation,decl,override:type:*": { "subcaseMS": 12.518 }, "webgpu:shader,validation,decl,ptr_spelling:let_ptr_explicit_type_matches_var:*": { "subcaseMS": 1.500 }, "webgpu:shader,validation,decl,ptr_spelling:let_ptr_reads:*": { "subcaseMS": 1.216 }, "webgpu:shader,validation,decl,ptr_spelling:let_ptr_writes:*": { "subcaseMS": 1.250 }, @@ -1671,32 +1897,76 @@ "webgpu:shader,validation,decl,ptr_spelling:ptr_bad_store_type:*": { "subcaseMS": 0.967 }, "webgpu:shader,validation,decl,ptr_spelling:ptr_handle_space_invalid:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,decl,ptr_spelling:ptr_not_instantiable:*": { "subcaseMS": 1.310 }, + "webgpu:shader,validation,decl,var:binding_collision_unused_helper:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:binding_collisions:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:binding_point_on_function_var:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:binding_point_on_non_resources:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:binding_point_on_resources:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:function_addrspace_at_module_scope:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:function_scope_types:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:handle_initializer:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:initializer_kind:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:module_scope_initializers:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,decl,var:module_scope_types:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,decl,var_access_mode:explicit_access_mode:*": { "subcaseMS": 1.373 }, "webgpu:shader,validation,decl,var_access_mode:implicit_access_mode:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,decl,var_access_mode:read_access:*": { "subcaseMS": 1.177 }, "webgpu:shader,validation,decl,var_access_mode:write_access:*": { "subcaseMS": 1.154 }, "webgpu:shader,validation,expression,access,vector:vector:*": { "subcaseMS": 1.407 }, + "webgpu:shader,validation,expression,binary,add_sub_mul:invalid_type_with_itself:*": { "subcaseMS": 68.949 }, + "webgpu:shader,validation,expression,binary,add_sub_mul:scalar_vector:*": { "subcaseMS": 1811.699 }, + "webgpu:shader,validation,expression,binary,add_sub_mul:scalar_vector_out_of_range:*": { "subcaseMS": 0.719 }, + "webgpu:shader,validation,expression,binary,and_or_xor:invalid_types:*": { "subcaseMS": 24.069 }, + "webgpu:shader,validation,expression,binary,and_or_xor:scalar_vector:*": { "subcaseMS": 666.807 }, + "webgpu:shader,validation,expression,binary,bitwise_shift:invalid_types:*": { "subcaseMS": 22.058 }, + "webgpu:shader,validation,expression,binary,bitwise_shift:scalar_vector:*": { "subcaseMS": 525.052 }, "webgpu:shader,validation,expression,binary,bitwise_shift:shift_left_concrete:*": { "subcaseMS": 1.216 }, - "webgpu:shader,validation,expression,binary,bitwise_shift:shift_left_vec_size_mismatch:*": { "subcaseMS": 1.367 }, "webgpu:shader,validation,expression,binary,bitwise_shift:shift_right_concrete:*": { "subcaseMS": 1.237 }, - "webgpu:shader,validation,expression,binary,bitwise_shift:shift_right_vec_size_mismatch:*": { "subcaseMS": 1.334 }, + "webgpu:shader,validation,expression,binary,comparison:invalid_types:*": { "subcaseMS": 39.526 }, + "webgpu:shader,validation,expression,binary,comparison:scalar_vector:*": { "subcaseMS": 1598.064 }, + "webgpu:shader,validation,expression,binary,div_rem:invalid_type_with_itself:*": { "subcaseMS": 38.059 }, + "webgpu:shader,validation,expression,binary,div_rem:scalar_vector:*": { "subcaseMS": 743.721 }, + "webgpu:shader,validation,expression,binary,div_rem:scalar_vector_out_of_range:*": { "subcaseMS": 650.727 }, + "webgpu:shader,validation,expression,call,builtin,abs:parameters:*": { "subcaseMS": 10.133 }, "webgpu:shader,validation,expression,call,builtin,abs:values:*": { "subcaseMS": 0.391 }, "webgpu:shader,validation,expression,call,builtin,acos:integer_argument:*": { "subcaseMS": 1.512 }, + "webgpu:shader,validation,expression,call,builtin,acos:parameters:*": { "subcaseMS": 44.578 }, "webgpu:shader,validation,expression,call,builtin,acos:values:*": { "subcaseMS": 0.342 }, "webgpu:shader,validation,expression,call,builtin,acosh:integer_argument:*": { "subcaseMS": 1.234 }, + "webgpu:shader,validation,expression,call,builtin,acosh:parameters:*": { "subcaseMS": 152.403 }, "webgpu:shader,validation,expression,call,builtin,acosh:values:*": { "subcaseMS": 0.217 }, + "webgpu:shader,validation,expression,call,builtin,all:argument_types:*": { "subcaseMS": 58740.580 }, + "webgpu:shader,validation,expression,call,builtin,all:arguments:*": { "subcaseMS": 80483.389 }, + "webgpu:shader,validation,expression,call,builtin,all:must_use:*": { "subcaseMS": 7564.378 }, + "webgpu:shader,validation,expression,call,builtin,any:argument_types:*": { "subcaseMS": 160136.896 }, + "webgpu:shader,validation,expression,call,builtin,any:arguments:*": { "subcaseMS": 50268.983 }, + "webgpu:shader,validation,expression,call,builtin,any:must_use:*": { "subcaseMS": 7467.652 }, + "webgpu:shader,validation,expression,call,builtin,arrayLength:access_mode:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,expression,call,builtin,arrayLength:bool_type:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,expression,call,builtin,arrayLength:type:*": { "subcaseMS": 0.000 }, "webgpu:shader,validation,expression,call,builtin,asin:integer_argument:*": { "subcaseMS": 0.878 }, + "webgpu:shader,validation,expression,call,builtin,asin:parameters:*": { "subcaseMS": 20072.777 }, "webgpu:shader,validation,expression,call,builtin,asin:values:*": { "subcaseMS": 0.359 }, "webgpu:shader,validation,expression,call,builtin,asinh:integer_argument:*": { "subcaseMS": 1.267 }, + "webgpu:shader,validation,expression,call,builtin,asinh:parameters:*": { "subcaseMS": 17189.159 }, "webgpu:shader,validation,expression,call,builtin,asinh:values:*": { "subcaseMS": 0.372 }, - "webgpu:shader,validation,expression,call,builtin,atan2:integer_argument_x:*": { "subcaseMS": 0.912 }, - "webgpu:shader,validation,expression,call,builtin,atan2:integer_argument_y:*": { "subcaseMS": 0.867 }, + "webgpu:shader,validation,expression,call,builtin,atan2:invalid_argument_x:*": { "subcaseMS": 6011.564 }, + "webgpu:shader,validation,expression,call,builtin,atan2:invalid_argument_y:*": { "subcaseMS": 24242.032 }, + "webgpu:shader,validation,expression,call,builtin,atan2:must_use:*": { "subcaseMS": 242.807 }, + "webgpu:shader,validation,expression,call,builtin,atan2:parameters:*": { "subcaseMS": 1360.903 }, "webgpu:shader,validation,expression,call,builtin,atan2:values:*": { "subcaseMS": 0.359 }, "webgpu:shader,validation,expression,call,builtin,atan:integer_argument:*": { "subcaseMS": 1.545 }, + "webgpu:shader,validation,expression,call,builtin,atan:parameters:*": { "subcaseMS": 14928.226 }, "webgpu:shader,validation,expression,call,builtin,atan:values:*": { "subcaseMS": 0.335 }, "webgpu:shader,validation,expression,call,builtin,atanh:integer_argument:*": { "subcaseMS": 0.912 }, + "webgpu:shader,validation,expression,call,builtin,atanh:parameters:*": { "subcaseMS": 19071.799 }, "webgpu:shader,validation,expression,call,builtin,atanh:values:*": { "subcaseMS": 0.231 }, + "webgpu:shader,validation,expression,call,builtin,atomics:atomic_parameterization:*": { "subcaseMS": 1.346 }, + "webgpu:shader,validation,expression,call,builtin,atomics:data_parameters:*": { "subcaseMS": 38.382 }, + "webgpu:shader,validation,expression,call,builtin,atomics:return_types:*": { "subcaseMS": 28.021 }, "webgpu:shader,validation,expression,call,builtin,atomics:stage:*": { "subcaseMS": 1.346 }, + "webgpu:shader,validation,expression,call,builtin,barriers:no_return_value:*": { "subcaseMS": 1.500 }, + "webgpu:shader,validation,expression,call,builtin,barriers:only_in_compute:*": { "subcaseMS": 1.500 }, "webgpu:shader,validation,expression,call,builtin,bitcast:bad_const_to_f16:*": { "subcaseMS": 0.753 }, "webgpu:shader,validation,expression,call,builtin,bitcast:bad_const_to_f32:*": { "subcaseMS": 0.844 }, "webgpu:shader,validation,expression,call,builtin,bitcast:bad_to_f16:*": { "subcaseMS": 8.518 }, @@ -1708,52 +1978,279 @@ "webgpu:shader,validation,expression,call,builtin,ceil:integer_argument:*": { "subcaseMS": 1.456 }, "webgpu:shader,validation,expression,call,builtin,ceil:values:*": { "subcaseMS": 1.539 }, "webgpu:shader,validation,expression,call,builtin,clamp:values:*": { "subcaseMS": 0.377 }, + "webgpu:shader,validation,expression,call,builtin,cos:args:*": { "subcaseMS": 4.445 }, "webgpu:shader,validation,expression,call,builtin,cos:integer_argument:*": { "subcaseMS": 1.601 }, + "webgpu:shader,validation,expression,call,builtin,cos:must_use:*": { "subcaseMS": 0.526 }, "webgpu:shader,validation,expression,call,builtin,cos:values:*": { "subcaseMS": 0.338 }, - "webgpu:shader,validation,expression,call,builtin,cosh:integer_argument:*": { "subcaseMS": 0.889 }, + "webgpu:shader,validation,expression,call,builtin,cosh:args:*": { "subcaseMS": 41.832 }, + "webgpu:shader,validation,expression,call,builtin,cosh:must_use:*": { "subcaseMS": 5.658 }, "webgpu:shader,validation,expression,call,builtin,cosh:values:*": { "subcaseMS": 0.272 }, + "webgpu:shader,validation,expression,call,builtin,countLeadingZeros:arguments:*": { "subcaseMS": 77.173 }, + "webgpu:shader,validation,expression,call,builtin,countLeadingZeros:float_argument:*": { "subcaseMS": 64.191 }, + "webgpu:shader,validation,expression,call,builtin,countLeadingZeros:must_use:*": { "subcaseMS": 4.120 }, + "webgpu:shader,validation,expression,call,builtin,countLeadingZeros:values:*": { "subcaseMS": 3153.457 }, + "webgpu:shader,validation,expression,call,builtin,countOneBits:arguments:*": { "subcaseMS": 66.449 }, + "webgpu:shader,validation,expression,call,builtin,countOneBits:float_argument:*": { "subcaseMS": 44.219 }, + "webgpu:shader,validation,expression,call,builtin,countOneBits:must_use:*": { "subcaseMS": 3.284 }, + "webgpu:shader,validation,expression,call,builtin,countOneBits:values:*": { "subcaseMS": 3771.859 }, + "webgpu:shader,validation,expression,call,builtin,countTrailingZeros:arguments:*": { "subcaseMS": 70.424 }, + "webgpu:shader,validation,expression,call,builtin,countTrailingZeros:float_argument:*": { "subcaseMS": 46.181 }, + "webgpu:shader,validation,expression,call,builtin,countTrailingZeros:must_use:*": { "subcaseMS": 3.934 }, + "webgpu:shader,validation,expression,call,builtin,countTrailingZeros:values:*": { "subcaseMS": 3125.847 }, + "webgpu:shader,validation,expression,call,builtin,cross:args:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,cross:must_use:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,cross:values:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,degrees:args:*": { "subcaseMS": 4.949 }, "webgpu:shader,validation,expression,call,builtin,degrees:integer_argument:*": { "subcaseMS": 1.311 }, + "webgpu:shader,validation,expression,call,builtin,degrees:must_use:*": { "subcaseMS": 1.406 }, "webgpu:shader,validation,expression,call,builtin,degrees:values:*": { "subcaseMS": 0.303 }, - "webgpu:shader,validation,expression,call,builtin,exp2:integer_argument:*": { "subcaseMS": 0.967 }, + "webgpu:shader,validation,expression,call,builtin,derivatives:invalid_argument_types:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,derivatives:only_in_fragment:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,determinant:args:*": { "subcaseMS": 15.538 }, + "webgpu:shader,validation,expression,call,builtin,determinant:matrix_args:*": { "subcaseMS": 193.897 }, + "webgpu:shader,validation,expression,call,builtin,determinant:must_use:*": { "subcaseMS": 2.856 }, + "webgpu:shader,validation,expression,call,builtin,distance:args:*": { "subcaseMS": 84.655 }, + "webgpu:shader,validation,expression,call,builtin,distance:must_use:*": { "subcaseMS": 6.757 }, + "webgpu:shader,validation,expression,call,builtin,distance:values:*": { "subcaseMS": 9849.553 }, + "webgpu:shader,validation,expression,call,builtin,dot4I8Packed:args:*": { "subcaseMS": 48.785 }, + "webgpu:shader,validation,expression,call,builtin,dot4I8Packed:must_use:*": { "subcaseMS": 0.300 }, + "webgpu:shader,validation,expression,call,builtin,dot4I8Packed:supported:*": { "subcaseMS": 1.100 }, + "webgpu:shader,validation,expression,call,builtin,dot4I8Packed:unsupported:*": { "subcaseMS": 7.250 }, + "webgpu:shader,validation,expression,call,builtin,dot4U8Packed:args:*": { "subcaseMS": 45.347 }, + "webgpu:shader,validation,expression,call,builtin,dot4U8Packed:must_use:*": { "subcaseMS": 0.100 }, + "webgpu:shader,validation,expression,call,builtin,dot4U8Packed:supported:*": { "subcaseMS": 0.401 }, + "webgpu:shader,validation,expression,call,builtin,dot4U8Packed:unsupported:*": { "subcaseMS": 3.200 }, + "webgpu:shader,validation,expression,call,builtin,exp2:args:*": { "subcaseMS": 45.200 }, + "webgpu:shader,validation,expression,call,builtin,exp2:must_use:*": { "subcaseMS": 6.346 }, "webgpu:shader,validation,expression,call,builtin,exp2:values:*": { "subcaseMS": 0.410 }, - "webgpu:shader,validation,expression,call,builtin,exp:integer_argument:*": { "subcaseMS": 1.356 }, + "webgpu:shader,validation,expression,call,builtin,exp:args:*": { "subcaseMS": 44.946 }, + "webgpu:shader,validation,expression,call,builtin,exp:must_use:*": { "subcaseMS": 6.140 }, "webgpu:shader,validation,expression,call,builtin,exp:values:*": { "subcaseMS": 0.311 }, - "webgpu:shader,validation,expression,call,builtin,inverseSqrt:integer_argument:*": { "subcaseMS": 1.356 }, + "webgpu:shader,validation,expression,call,builtin,extractBits:count_offset:*": { "subcaseMS": 131.573 }, + "webgpu:shader,validation,expression,call,builtin,extractBits:must_use:*": { "subcaseMS": 4.630 }, + "webgpu:shader,validation,expression,call,builtin,extractBits:typed_arguments:*": { "subcaseMS": 7928.233 }, + "webgpu:shader,validation,expression,call,builtin,extractBits:values:*": { "subcaseMS": 28802.248 }, + "webgpu:shader,validation,expression,call,builtin,faceForward:args:*": { "subcaseMS": 105.517 }, + "webgpu:shader,validation,expression,call,builtin,faceForward:must_use:*": { "subcaseMS": 5.412 }, + "webgpu:shader,validation,expression,call,builtin,faceForward:values:*": { "subcaseMS": 96662.064 }, + "webgpu:shader,validation,expression,call,builtin,firstLeadingBit:arguments:*": { "subcaseMS": 210.892 }, + "webgpu:shader,validation,expression,call,builtin,firstLeadingBit:float_argument:*": { "subcaseMS": 29.714 }, + "webgpu:shader,validation,expression,call,builtin,firstLeadingBit:must_use:*": { "subcaseMS": 2.214 }, + "webgpu:shader,validation,expression,call,builtin,firstLeadingBit:values:*": { "subcaseMS": 4227.758 }, + "webgpu:shader,validation,expression,call,builtin,firstTrailingBit:arguments:*": { "subcaseMS": 65.356 }, + "webgpu:shader,validation,expression,call,builtin,firstTrailingBit:float_argument:*": { "subcaseMS": 41.249 }, + "webgpu:shader,validation,expression,call,builtin,firstTrailingBit:must_use:*": { "subcaseMS": 1.982 }, + "webgpu:shader,validation,expression,call,builtin,firstTrailingBit:values:*": { "subcaseMS": 4203.191 }, + "webgpu:shader,validation,expression,call,builtin,floor:args:*": { "subcaseMS": 4.221 }, + "webgpu:shader,validation,expression,call,builtin,floor:integer_argument:*": { "subcaseMS": 48.400 }, + "webgpu:shader,validation,expression,call,builtin,floor:must_use:*": { "subcaseMS": 0.170 }, + "webgpu:shader,validation,expression,call,builtin,floor:values:*": { "subcaseMS": 29.668 }, + "webgpu:shader,validation,expression,call,builtin,fract:args:*": { "subcaseMS": 45.422 }, + "webgpu:shader,validation,expression,call,builtin,fract:must_use:*": { "subcaseMS": 5.547 }, + "webgpu:shader,validation,expression,call,builtin,fract:values:*": { "subcaseMS": 4441.607 }, + "webgpu:shader,validation,expression,call,builtin,frexp:args:*": { "subcaseMS": 43.518 }, + "webgpu:shader,validation,expression,call,builtin,frexp:must_use:*": { "subcaseMS": 6.130 }, + "webgpu:shader,validation,expression,call,builtin,frexp:values:*": { "subcaseMS": 4741.981 }, + "webgpu:shader,validation,expression,call,builtin,insertBits:count_offset:*": { "subcaseMS": 11904.035 }, + "webgpu:shader,validation,expression,call,builtin,insertBits:mismatched:*": { "subcaseMS": 659.718 }, + "webgpu:shader,validation,expression,call,builtin,insertBits:must_use:*": { "subcaseMS": 4.243 }, + "webgpu:shader,validation,expression,call,builtin,insertBits:typed_arguments:*": { "subcaseMS": 3025.440 }, + "webgpu:shader,validation,expression,call,builtin,insertBits:values:*": { "subcaseMS": 98566.796 }, + "webgpu:shader,validation,expression,call,builtin,inverseSqrt:args:*": { "subcaseMS": 41.941 }, + "webgpu:shader,validation,expression,call,builtin,inverseSqrt:must_use:*": { "subcaseMS": 5.614 }, "webgpu:shader,validation,expression,call,builtin,inverseSqrt:values:*": { "subcaseMS": 0.315 }, "webgpu:shader,validation,expression,call,builtin,length:integer_argument:*": { "subcaseMS": 2.011 }, "webgpu:shader,validation,expression,call,builtin,length:scalar:*": { "subcaseMS": 0.245 }, "webgpu:shader,validation,expression,call,builtin,length:vec2:*": { "subcaseMS": 0.319 }, "webgpu:shader,validation,expression,call,builtin,length:vec3:*": { "subcaseMS": 1.401 }, "webgpu:shader,validation,expression,call,builtin,length:vec4:*": { "subcaseMS": 1.301 }, + "webgpu:shader,validation,expression,call,builtin,log2:args:*": { "subcaseMS": 4.528 }, "webgpu:shader,validation,expression,call,builtin,log2:integer_argument:*": { "subcaseMS": 1.034 }, + "webgpu:shader,validation,expression,call,builtin,log2:must_use:*": { "subcaseMS": 1.149 }, "webgpu:shader,validation,expression,call,builtin,log2:values:*": { "subcaseMS": 0.398 }, + "webgpu:shader,validation,expression,call,builtin,log:args:*": { "subcaseMS": 5.197 }, "webgpu:shader,validation,expression,call,builtin,log:integer_argument:*": { "subcaseMS": 1.134 }, + "webgpu:shader,validation,expression,call,builtin,log:must_use:*": { "subcaseMS": 1597.590 }, "webgpu:shader,validation,expression,call,builtin,log:values:*": { "subcaseMS": 0.291 }, + "webgpu:shader,validation,expression,call,builtin,max:args:*": { "subcaseMS": 77.529 }, + "webgpu:shader,validation,expression,call,builtin,max:must_use:*": { "subcaseMS": 8.286 }, + "webgpu:shader,validation,expression,call,builtin,max:values:*": { "subcaseMS": 260241.074 }, + "webgpu:shader,validation,expression,call,builtin,min:args:*": { "subcaseMS": 67.260 }, + "webgpu:shader,validation,expression,call,builtin,min:must_use:*": { "subcaseMS": 3.899 }, + "webgpu:shader,validation,expression,call,builtin,min:values:*": { "subcaseMS": 246733.594 }, "webgpu:shader,validation,expression,call,builtin,modf:integer_argument:*": { "subcaseMS": 1.089 }, "webgpu:shader,validation,expression,call,builtin,modf:values:*": { "subcaseMS": 1.866 }, + "webgpu:shader,validation,expression,call,builtin,normalize:args:*": { "subcaseMS": 25.416 }, + "webgpu:shader,validation,expression,call,builtin,normalize:invalid_argument:*": { "subcaseMS": 3.258 }, + "webgpu:shader,validation,expression,call,builtin,normalize:must_use:*": { "subcaseMS": 5.626 }, + "webgpu:shader,validation,expression,call,builtin,normalize:values:*": { "subcaseMS": 3241.079 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8:args:*": { "subcaseMS": 36.226 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8:must_use:*": { "subcaseMS": 6.500 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8:supported:*": { "subcaseMS": 113.501 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8:unsupported:*": { "subcaseMS": 739.400 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8Clamp:args:*": { "subcaseMS": 21.994 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8Clamp:must_use:*": { "subcaseMS": 34.301 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8Clamp:supported:*": { "subcaseMS": 100.450 }, + "webgpu:shader,validation,expression,call,builtin,pack4xI8Clamp:unsupported:*": { "subcaseMS": 751.101 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8:args:*": { "subcaseMS": 24.783 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8:must_use:*": { "subcaseMS": 5.300 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8:supported:*": { "subcaseMS": 449.800 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8:unsupported:*": { "subcaseMS": 773.702 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8Clamp:args:*": { "subcaseMS": 26.118 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8Clamp:must_use:*": { "subcaseMS": 32.600 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8Clamp:supported:*": { "subcaseMS": 134.750 }, + "webgpu:shader,validation,expression,call,builtin,pack4xU8Clamp:unsupported:*": { "subcaseMS": 570.500 }, + "webgpu:shader,validation,expression,call,builtin,quantizeToF16:args:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,quantizeToF16:must_use:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,quantizeToF16:values:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,radians:args:*": { "subcaseMS": 4.561 }, "webgpu:shader,validation,expression,call,builtin,radians:integer_argument:*": { "subcaseMS": 1.811 }, + "webgpu:shader,validation,expression,call,builtin,radians:must_use:*": { "subcaseMS": 0.757 }, "webgpu:shader,validation,expression,call,builtin,radians:values:*": { "subcaseMS": 0.382 }, + "webgpu:shader,validation,expression,call,builtin,reflect:args:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,reflect:must_use:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,reflect:values:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,reverseBits:arguments:*": { "subcaseMS": 77.380 }, + "webgpu:shader,validation,expression,call,builtin,reverseBits:float_argument:*": { "subcaseMS": 28.806 }, + "webgpu:shader,validation,expression,call,builtin,reverseBits:must_use:*": { "subcaseMS": 2.063 }, + "webgpu:shader,validation,expression,call,builtin,reverseBits:values:*": { "subcaseMS": 3140.778 }, "webgpu:shader,validation,expression,call,builtin,round:integer_argument:*": { "subcaseMS": 1.834 }, "webgpu:shader,validation,expression,call,builtin,round:values:*": { "subcaseMS": 0.382 }, "webgpu:shader,validation,expression,call,builtin,saturate:integer_argument:*": { "subcaseMS": 1.878 }, "webgpu:shader,validation,expression,call,builtin,saturate:values:*": { "subcaseMS": 0.317 }, - "webgpu:shader,validation,expression,call,builtin,sign:unsigned_integer_argument:*": { "subcaseMS": 1.120 }, + "webgpu:shader,validation,expression,call,builtin,select:argument_types_1_and_2:*": { "subcaseMS": 101642.926 }, + "webgpu:shader,validation,expression,call,builtin,select:argument_types_3:*": { "subcaseMS": 148.474 }, + "webgpu:shader,validation,expression,call,builtin,select:arguments:*": { "subcaseMS": 66398.067 }, + "webgpu:shader,validation,expression,call,builtin,select:must_use:*": { "subcaseMS": 4.312 }, + "webgpu:shader,validation,expression,call,builtin,sign:args:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,sign:must_use:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,expression,call,builtin,sign:values:*": { "subcaseMS": 0.343 }, + "webgpu:shader,validation,expression,call,builtin,sin:args:*": { "subcaseMS": 4.443 }, "webgpu:shader,validation,expression,call,builtin,sin:integer_argument:*": { "subcaseMS": 1.189 }, + "webgpu:shader,validation,expression,call,builtin,sin:must_use:*": { "subcaseMS": 0.588 }, "webgpu:shader,validation,expression,call,builtin,sin:values:*": { "subcaseMS": 0.349 }, - "webgpu:shader,validation,expression,call,builtin,sinh:integer_argument:*": { "subcaseMS": 1.078 }, + "webgpu:shader,validation,expression,call,builtin,sinh:args:*": { "subcaseMS": 42.542 }, + "webgpu:shader,validation,expression,call,builtin,sinh:must_use:*": { "subcaseMS": 5.980 }, "webgpu:shader,validation,expression,call,builtin,sinh:values:*": { "subcaseMS": 0.357 }, + "webgpu:shader,validation,expression,call,builtin,smoothstep:argument_types:*": { "subcaseMS": 69163.359 }, + "webgpu:shader,validation,expression,call,builtin,smoothstep:arguments:*": { "subcaseMS": 131.134 }, + "webgpu:shader,validation,expression,call,builtin,smoothstep:values:*": { "subcaseMS": 81643.500 }, + "webgpu:shader,validation,expression,call,builtin,sqrt:args:*": { "subcaseMS": 5.398 }, "webgpu:shader,validation,expression,call,builtin,sqrt:integer_argument:*": { "subcaseMS": 1.356 }, + "webgpu:shader,validation,expression,call,builtin,sqrt:must_use:*": { "subcaseMS": 1.286 }, "webgpu:shader,validation,expression,call,builtin,sqrt:values:*": { "subcaseMS": 0.302 }, - "webgpu:shader,validation,expression,call,builtin,tan:integer_argument:*": { "subcaseMS": 1.734 }, + "webgpu:shader,validation,expression,call,builtin,step:args:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,step:must_use:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,step:values:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,tan:args:*": { "subcaseMS": 43.560 }, + "webgpu:shader,validation,expression,call,builtin,tan:must_use:*": { "subcaseMS": 5.401 }, "webgpu:shader,validation,expression,call,builtin,tan:values:*": { "subcaseMS": 0.350 }, + "webgpu:shader,validation,expression,call,builtin,tanh:args:*": { "subcaseMS": 43.571 }, + "webgpu:shader,validation,expression,call,builtin,tanh:must_use:*": { "subcaseMS": 6.270 }, + "webgpu:shader,validation,expression,call,builtin,tanh:values:*": { "subcaseMS": 4590.491 }, + "webgpu:shader,validation,expression,call,builtin,textureGather:array_index_argument:*": { "subcaseMS": 1.123 }, + "webgpu:shader,validation,expression,call,builtin,textureGather:component_argument,non_const:*": { "subcaseMS": 1.731 }, + "webgpu:shader,validation,expression,call,builtin,textureGather:component_argument:*": { "subcaseMS": 1.321 }, + "webgpu:shader,validation,expression,call,builtin,textureGather:coords_argument:*": { "subcaseMS": 1.352 }, + "webgpu:shader,validation,expression,call,builtin,textureGather:offset_argument,non_const:*": { "subcaseMS": 1.231 }, + "webgpu:shader,validation,expression,call,builtin,textureGather:offset_argument:*": { "subcaseMS": 1.534 }, + "webgpu:shader,validation,expression,call,builtin,textureGatherCompare:array_index_argument:*": { "subcaseMS": 1.932 }, + "webgpu:shader,validation,expression,call,builtin,textureGatherCompare:coords_argument:*": { "subcaseMS": 1.764 }, + "webgpu:shader,validation,expression,call,builtin,textureGatherCompare:depth_ref_argument:*": { "subcaseMS": 1.753 }, + "webgpu:shader,validation,expression,call,builtin,textureGatherCompare:offset_argument,non_const:*": { "subcaseMS": 1.534 }, + "webgpu:shader,validation,expression,call,builtin,textureGatherCompare:offset_argument:*": { "subcaseMS": 1.243 }, + "webgpu:shader,validation,expression,call,builtin,textureLoad:array_index_argument,non_storage:*": { "subcaseMS": 1.358 }, + "webgpu:shader,validation,expression,call,builtin,textureLoad:array_index_argument,storage:*": { "subcaseMS": 1.906 }, + "webgpu:shader,validation,expression,call,builtin,textureLoad:coords_argument,non_storage:*": { "subcaseMS": 1.717 }, + "webgpu:shader,validation,expression,call,builtin,textureLoad:coords_argument,storage:*": { "subcaseMS": 1.750 }, + "webgpu:shader,validation,expression,call,builtin,textureLoad:level_argument,non_storage:*": { "subcaseMS": 1.113 }, + "webgpu:shader,validation,expression,call,builtin,textureLoad:sample_index_argument,non_storage:*": { "subcaseMS": 1.395 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:array_index_argument:*": { "subcaseMS": 1.888 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:coords_argument:*": { "subcaseMS": 1.342 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:offset_argument,non_const:*": { "subcaseMS": 1.604 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:offset_argument:*": { "subcaseMS": 1.401 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:only_in_fragment:*": { "subcaseMS": 1.121 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleBaseClampToEdge:coords_argument:*": { "subcaseMS": 239.356 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleBias:array_index_argument:*": { "subcaseMS": 1.630 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleBias:bias_argument:*": { "subcaseMS": 1.102 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleBias:coords_argument:*": { "subcaseMS": 1.938 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleBias:offset_argument,non_const:*": { "subcaseMS": 1.985 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleBias:offset_argument:*": { "subcaseMS": 1.081 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleBias:only_in_fragment:*": { "subcaseMS": 1.181 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompare:array_index_argument:*": { "subcaseMS": 1.932 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompare:coords_argument:*": { "subcaseMS": 1.282 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompare:depth_ref_argument:*": { "subcaseMS": 1.563 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompare:offset_argument,non_const:*": { "subcaseMS": 1.720 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompare:offset_argument:*": { "subcaseMS": 1.540 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompare:only_in_fragment:*": { "subcaseMS": 1.121 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompareLevel:array_index_argument:*": { "subcaseMS": 1.989 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompareLevel:coords_argument:*": { "subcaseMS": 1.932 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompareLevel:depth_ref_argument:*": { "subcaseMS": 1.578 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompareLevel:offset_argument,non_const:*": { "subcaseMS": 1.347 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleCompareLevel:offset_argument:*": { "subcaseMS": 1.619 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleGrad:array_index_argument:*": { "subcaseMS": 1.740 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleGrad:coords_argument:*": { "subcaseMS": 1.802 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleGrad:ddX_argument:*": { "subcaseMS": 1.882 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleGrad:ddY_argument:*": { "subcaseMS": 1.515 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleGrad:offset_argument,non_const:*": { "subcaseMS": 1.987 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleGrad:offset_argument:*": { "subcaseMS": 1.317 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleLevel:array_index_argument:*": { "subcaseMS": 1.888 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleLevel:coords_argument:*": { "subcaseMS": 1.342 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleLevel:level_argument:*": { "subcaseMS": 1.422 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleLevel:offset_argument,non_const:*": { "subcaseMS": 1.604 }, + "webgpu:shader,validation,expression,call,builtin,textureSampleLevel:offset_argument:*": { "subcaseMS": 1.401 }, + "webgpu:shader,validation,expression,call,builtin,textureStore:array_index_argument:*": { "subcaseMS": 1.240 }, + "webgpu:shader,validation,expression,call,builtin,textureStore:coords_argument:*": { "subcaseMS": 1.350 }, + "webgpu:shader,validation,expression,call,builtin,textureStore:value_argument:*": { "subcaseMS": 1.442 }, + "webgpu:shader,validation,expression,call,builtin,trunc:args:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,trunc:must_use:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,trunc:values:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xI8:args:*": { "subcaseMS": 23.923 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xI8:must_use:*": { "subcaseMS": 35.200 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xI8:supported:*": { "subcaseMS": 24.150 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xI8:unsupported:*": { "subcaseMS": 615.301 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xU8:args:*": { "subcaseMS": 21.830 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xU8:must_use:*": { "subcaseMS": 32.800 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xU8:supported:*": { "subcaseMS": 98.501 }, + "webgpu:shader,validation,expression,call,builtin,unpack4xU8:unsupported:*": { "subcaseMS": 346.801 }, + "webgpu:shader,validation,expression,call,builtin,workgroupUniformLoad:no_atomics:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,call,builtin,workgroupUniformLoad:only_in_compute:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,expression,overload_resolution:implicit_conversions:*": { "subcaseMS": 519.260 }, + "webgpu:shader,validation,expression,overload_resolution:overload_resolution:*": { "subcaseMS": 436.603 }, + "webgpu:shader,validation,expression,precedence:binary_requires_parentheses:*": { "subcaseMS": 149.921 }, + "webgpu:shader,validation,expression,precedence:mixed_logical_requires_parentheses:*": { "subcaseMS": 6.107 }, + "webgpu:shader,validation,expression,precedence:other:*": { "subcaseMS": 5.459 }, + "webgpu:shader,validation,expression,unary,address_of_and_indirection:basic:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,expression,unary,address_of_and_indirection:composite:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,expression,unary,address_of_and_indirection:invalid:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,expression,unary,arithmetic_negation:invalid_types:*": { "subcaseMS": 25.894 }, + "webgpu:shader,validation,expression,unary,arithmetic_negation:scalar_vector:*": { "subcaseMS": 31.344 }, + "webgpu:shader,validation,expression,unary,bitwise_complement:invalid_types:*": { "subcaseMS": 7.564 }, + "webgpu:shader,validation,expression,unary,bitwise_complement:scalar_vector:*": { "subcaseMS": 73.852 }, + "webgpu:shader,validation,expression,unary,logical_negation:invalid_types:*": { "subcaseMS": 7.100 }, + "webgpu:shader,validation,expression,unary,logical_negation:scalar_vector:*": { "subcaseMS": 84.680 }, + "webgpu:shader,validation,extension,pointer_composite_access:deref:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,extension,pointer_composite_access:pointer:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,extension,readonly_and_readwrite_storage_textures:textureBarrier:*": { "subcaseMS": 1.141 }, + "webgpu:shader,validation,extension,readonly_and_readwrite_storage_textures:var_decl:*": { "subcaseMS": 42.299 }, "webgpu:shader,validation,functions,alias_analysis:aliasing_inside_function:*": { "subcaseMS": 1.200 }, "webgpu:shader,validation,functions,alias_analysis:member_accessors:*": { "subcaseMS": 1.656 }, + "webgpu:shader,validation,functions,alias_analysis:one_atomic_pointer_one_module_scope:*": { "subcaseMS": 0.000 }, "webgpu:shader,validation,functions,alias_analysis:one_pointer_one_module_scope:*": { "subcaseMS": 1.598 }, "webgpu:shader,validation,functions,alias_analysis:same_pointer_read_and_write:*": { "subcaseMS": 1.301 }, "webgpu:shader,validation,functions,alias_analysis:subcalls:*": { "subcaseMS": 1.673 }, + "webgpu:shader,validation,functions,alias_analysis:two_atomic_pointers:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,alias_analysis:two_atomic_pointers_to_array_elements:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,alias_analysis:two_atomic_pointers_to_struct_members:*": { "subcaseMS": 0.000 }, "webgpu:shader,validation,functions,alias_analysis:two_pointers:*": { "subcaseMS": 1.537 }, - "webgpu:shader,validation,functions,restrictions:call_arg_types_match_params:*": { "subcaseMS": 1.518 }, + "webgpu:shader,validation,functions,alias_analysis:two_pointers_to_array_elements:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,alias_analysis:two_pointers_to_array_elements_indirect:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,alias_analysis:two_pointers_to_struct_members:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,alias_analysis:two_pointers_to_struct_members_indirect:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,alias_analysis:workgroup_uniform_load:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,restrictions:call_arg_types_match_1_param:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,restrictions:call_arg_types_match_2_params:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,functions,restrictions:call_arg_types_match_3_params:*": { "subcaseMS": 0.000 }, "webgpu:shader,validation,functions,restrictions:entry_point_call_target:*": { "subcaseMS": 1.734 }, "webgpu:shader,validation,functions,restrictions:function_parameter_matching:*": { "subcaseMS": 1.953 }, "webgpu:shader,validation,functions,restrictions:function_parameter_types:*": { "subcaseMS": 1.520 }, @@ -1774,17 +2271,23 @@ "webgpu:shader,validation,parse,blankspace:bom:*": { "subcaseMS": 1.101 }, "webgpu:shader,validation,parse,blankspace:null_characters:*": { "subcaseMS": 3.217 }, "webgpu:shader,validation,parse,break:placement:*": { "subcaseMS": 1.254 }, + "webgpu:shader,validation,parse,break_if:non_bool_param:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,break_if:placement:*": { "subcaseMS": 0.000 }, "webgpu:shader,validation,parse,builtin:parse:*": { "subcaseMS": 3.277 }, "webgpu:shader,validation,parse,builtin:placement:*": { "subcaseMS": 1.267 }, "webgpu:shader,validation,parse,comments:comments:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,comments:line_comment_eof:*": { "subcaseMS": 4.500 }, "webgpu:shader,validation,parse,comments:line_comment_terminators:*": { "subcaseMS": 1.021 }, "webgpu:shader,validation,parse,comments:unterminated_block_comment:*": { "subcaseMS": 8.950 }, + "webgpu:shader,validation,parse,compound:parse:*": { "subcaseMS": 4.315 }, "webgpu:shader,validation,parse,const:placement:*": { "subcaseMS": 1.167 }, "webgpu:shader,validation,parse,const_assert:parse:*": { "subcaseMS": 1.400 }, + "webgpu:shader,validation,parse,continuing:placement:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,diagnostic:after_other_directives:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,diagnostic:conflicting_attribute_different_location:*": { "subcaseMS": 2.257 }, "webgpu:shader,validation,parse,diagnostic:conflicting_attribute_same_location:*": { "subcaseMS": 1.400 }, "webgpu:shader,validation,parse,diagnostic:conflicting_directive:*": { "subcaseMS": 1.244 }, + "webgpu:shader,validation,parse,diagnostic:diagnostic_scoping:*": { "subcaseMS": 1.244 }, "webgpu:shader,validation,parse,diagnostic:invalid_locations:*": { "subcaseMS": 1.930 }, "webgpu:shader,validation,parse,diagnostic:invalid_severity:*": { "subcaseMS": 1.361 }, "webgpu:shader,validation,parse,diagnostic:valid_locations:*": { "subcaseMS": 1.368 }, @@ -1814,14 +2317,17 @@ "webgpu:shader,validation,parse,must_use:builtin_no_must_use:*": { "subcaseMS": 1.206 }, "webgpu:shader,validation,parse,must_use:call:*": { "subcaseMS": 1.275 }, "webgpu:shader,validation,parse,must_use:declaration:*": { "subcaseMS": 1.523 }, + "webgpu:shader,validation,parse,must_use:ignore_result_of_non_must_use_that_returns_call_of_must_use:*": { "subcaseMS": 0.000 }, "webgpu:shader,validation,parse,pipeline_stage:compute_parsing:*": { "subcaseMS": 1.000 }, - "webgpu:shader,validation,parse,pipeline_stage:duplicate_compute_on_function:*": { "subcaseMS": 2.651 }, - "webgpu:shader,validation,parse,pipeline_stage:duplicate_fragment_on_function:*": { "subcaseMS": 1.001 }, - "webgpu:shader,validation,parse,pipeline_stage:duplicate_vertex_on_function:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,parse,pipeline_stage:extra_on_compute_function:*": { "subcaseMS": 2.651 }, + "webgpu:shader,validation,parse,pipeline_stage:extra_on_fragment_function:*": { "subcaseMS": 1.001 }, + "webgpu:shader,validation,parse,pipeline_stage:extra_on_vertex_function:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,pipeline_stage:fragment_parsing:*": { "subcaseMS": 2.600 }, "webgpu:shader,validation,parse,pipeline_stage:multiple_entry_points:*": { "subcaseMS": 1.100 }, "webgpu:shader,validation,parse,pipeline_stage:placement:*": { "subcaseMS": 1.388 }, "webgpu:shader,validation,parse,pipeline_stage:vertex_parsing:*": { "subcaseMS": 1.500 }, + "webgpu:shader,validation,parse,requires:requires:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,parse,requires:wgsl_matches_api:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,semicolon:after_assignment:*": { "subcaseMS": 1.400 }, "webgpu:shader,validation,parse,semicolon:after_call:*": { "subcaseMS": 1.301 }, "webgpu:shader,validation,parse,semicolon:after_case:*": { "subcaseMS": 1.301 }, @@ -1830,6 +2336,7 @@ "webgpu:shader,validation,parse,semicolon:after_continuing:*": { "subcaseMS": 0.900 }, "webgpu:shader,validation,parse,semicolon:after_default_case:*": { "subcaseMS": 3.100 }, "webgpu:shader,validation,parse,semicolon:after_default_case_break:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,parse,semicolon:after_diagnostic:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,semicolon:after_discard:*": { "subcaseMS": 4.400 }, "webgpu:shader,validation,parse,semicolon:after_enable:*": { "subcaseMS": 1.301 }, "webgpu:shader,validation,parse,semicolon:after_fn_const_assert:*": { "subcaseMS": 1.400 }, @@ -1848,6 +2355,7 @@ "webgpu:shader,validation,parse,semicolon:after_member:*": { "subcaseMS": 4.801 }, "webgpu:shader,validation,parse,semicolon:after_module_const_decl:*": { "subcaseMS": 1.400 }, "webgpu:shader,validation,parse,semicolon:after_module_var_decl:*": { "subcaseMS": 0.901 }, + "webgpu:shader,validation,parse,semicolon:after_requires:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,semicolon:after_return:*": { "subcaseMS": 1.201 }, "webgpu:shader,validation,parse,semicolon:after_struct_decl:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,semicolon:after_switch:*": { "subcaseMS": 1.101 }, @@ -1861,16 +2369,28 @@ "webgpu:shader,validation,parse,semicolon:function_body_single:*": { "subcaseMS": 0.800 }, "webgpu:shader,validation,parse,semicolon:module_scope_multiple:*": { "subcaseMS": 0.900 }, "webgpu:shader,validation,parse,semicolon:module_scope_single:*": { "subcaseMS": 2.100 }, + "webgpu:shader,validation,parse,shadow_builtins:function_param:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_access_mode:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_builtin:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_builtin_atomic:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_builtin_atomic_type:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_builtin_barriers:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_builtin_f16:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_builtin_handle_type:*": { "subcaseMS": 0.000 }, + "webgpu:shader,validation,parse,shadow_builtins:shadow_hides_builtin_texture:*": { "subcaseMS": 0.000 }, "webgpu:shader,validation,parse,source:empty:*": { "subcaseMS": 1.101 }, "webgpu:shader,validation,parse,source:invalid_source:*": { "subcaseMS": 1.100 }, "webgpu:shader,validation,parse,source:valid_source:*": { "subcaseMS": 1.101 }, + "webgpu:shader,validation,parse,statement_behavior:invalid_functions:*": { "subcaseMS": 1.107 }, + "webgpu:shader,validation,parse,statement_behavior:invalid_statements:*": { "subcaseMS": 80.699 }, + "webgpu:shader,validation,parse,statement_behavior:valid_functions:*": { "subcaseMS": 0.978 }, + "webgpu:shader,validation,parse,statement_behavior:valid_statements:*": { "subcaseMS": 15.781 }, "webgpu:shader,validation,parse,unary_ops:all:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,parse,var_and_let:initializer_type:*": { "subcaseMS": 0.900 }, "webgpu:shader,validation,parse,var_and_let:var_access_mode_bad_other_template_contents:*": { "subcaseMS": 4.071 }, "webgpu:shader,validation,parse,var_and_let:var_access_mode_bad_template_delim:*": { "subcaseMS": 1.088 }, "webgpu:shader,validation,shader_io,binding:binding:*": { "subcaseMS": 1.240 }, "webgpu:shader,validation,shader_io,binding:binding_f16:*": { "subcaseMS": 0.500 }, - "webgpu:shader,validation,shader_io,binding:binding_without_group:*": { "subcaseMS": 0.901 }, "webgpu:shader,validation,shader_io,builtins:duplicates:*": { "subcaseMS": 1.913 }, "webgpu:shader,validation,shader_io,builtins:missing_vertex_position:*": { "subcaseMS": 0.975 }, "webgpu:shader,validation,shader_io,builtins:nesting:*": { "subcaseMS": 2.700 }, @@ -1884,7 +2404,6 @@ "webgpu:shader,validation,shader_io,entry_point:no_entry_point_provided:*": { "subcaseMS": 0.801 }, "webgpu:shader,validation,shader_io,group:group:*": { "subcaseMS": 1.355 }, "webgpu:shader,validation,shader_io,group:group_f16:*": { "subcaseMS": 0.400 }, - "webgpu:shader,validation,shader_io,group:group_without_binding:*": { "subcaseMS": 1.100 }, "webgpu:shader,validation,shader_io,group_and_binding:binding_attributes:*": { "subcaseMS": 1.280 }, "webgpu:shader,validation,shader_io,group_and_binding:different_entry_points:*": { "subcaseMS": 1.833 }, "webgpu:shader,validation,shader_io,group_and_binding:function_scope:*": { "subcaseMS": 1.000 }, @@ -1905,13 +2424,16 @@ "webgpu:shader,validation,shader_io,invariant:not_valid_on_user_defined_io:*": { "subcaseMS": 1.100 }, "webgpu:shader,validation,shader_io,invariant:parsing:*": { "subcaseMS": 1.438 }, "webgpu:shader,validation,shader_io,invariant:valid_only_with_vertex_position_builtin:*": { "subcaseMS": 1.461 }, + "webgpu:shader,validation,shader_io,layout_constraints:layout_constraints:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,shader_io,locations:duplicates:*": { "subcaseMS": 1.906 }, "webgpu:shader,validation,shader_io,locations:location_fp16:*": { "subcaseMS": 0.501 }, "webgpu:shader,validation,shader_io,locations:nesting:*": { "subcaseMS": 0.967 }, + "webgpu:shader,validation,shader_io,locations:out_of_order:*": { "subcaseMS": 1.296 }, "webgpu:shader,validation,shader_io,locations:stage_inout:*": { "subcaseMS": 1.850 }, "webgpu:shader,validation,shader_io,locations:type:*": { "subcaseMS": 1.332 }, "webgpu:shader,validation,shader_io,locations:validation:*": { "subcaseMS": 1.296 }, "webgpu:shader,validation,shader_io,size:size:*": { "subcaseMS": 1.218 }, + "webgpu:shader,validation,shader_io,size:size_creation_fixed_footprint:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,shader_io,size:size_fp16:*": { "subcaseMS": 1.500 }, "webgpu:shader,validation,shader_io,size:size_non_struct:*": { "subcaseMS": 0.929 }, "webgpu:shader,validation,shader_io,workgroup_size:workgroup_size:*": { "subcaseMS": 1.227 }, @@ -1921,6 +2443,8 @@ "webgpu:shader,validation,shader_io,workgroup_size:workgroup_size_function:*": { "subcaseMS": 0.800 }, "webgpu:shader,validation,shader_io,workgroup_size:workgroup_size_var:*": { "subcaseMS": 2.101 }, "webgpu:shader,validation,shader_io,workgroup_size:workgroup_size_vertex_shader:*": { "subcaseMS": 1.000 }, + "webgpu:shader,validation,types,alias:any_type:*": { "subcaseMS": 42.329 }, + "webgpu:shader,validation,types,alias:match_non_alias:*": { "subcaseMS": 3.767 }, "webgpu:shader,validation,types,alias:no_direct_recursion:*": { "subcaseMS": 1.450 }, "webgpu:shader,validation,types,alias:no_indirect_recursion:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,types,alias:no_indirect_recursion_via_array_element:*": { "subcaseMS": 1.050 }, @@ -1931,12 +2455,23 @@ "webgpu:shader,validation,types,alias:no_indirect_recursion_via_struct_attribute:*": { "subcaseMS": 1.584 }, "webgpu:shader,validation,types,alias:no_indirect_recursion_via_struct_member:*": { "subcaseMS": 1.000 }, "webgpu:shader,validation,types,alias:no_indirect_recursion_via_vector_element:*": { "subcaseMS": 1.050 }, + "webgpu:shader,validation,types,array:invalid:*": { "subcaseMS": 2.470 }, + "webgpu:shader,validation,types,array:valid:*": { "subcaseMS": 449.293 }, + "webgpu:shader,validation,types,atomics:address_space:*": { "subcaseMS": 1.050 }, + "webgpu:shader,validation,types,atomics:invalid_operations:*": { "subcaseMS": 1.050 }, + "webgpu:shader,validation,types,atomics:type:*": { "subcaseMS": 1.050 }, + "webgpu:shader,validation,types,matrix:invalid:*": { "subcaseMS": 68.086 }, + "webgpu:shader,validation,types,matrix:valid:*": { "subcaseMS": 67.784 }, "webgpu:shader,validation,types,struct:no_direct_recursion:*": { "subcaseMS": 0.951 }, "webgpu:shader,validation,types,struct:no_indirect_recursion:*": { "subcaseMS": 0.901 }, "webgpu:shader,validation,types,struct:no_indirect_recursion_via_array_element:*": { "subcaseMS": 0.901 }, "webgpu:shader,validation,types,struct:no_indirect_recursion_via_array_size:*": { "subcaseMS": 0.900 }, "webgpu:shader,validation,types,struct:no_indirect_recursion_via_struct_attribute:*": { "subcaseMS": 1.467 }, "webgpu:shader,validation,types,struct:no_indirect_recursion_via_struct_member_nested_in_alias:*": { "subcaseMS": 0.950 }, + "webgpu:shader,validation,types,textures:sampled_texture_types:*": { "subcaseMS": 7.634 }, + "webgpu:shader,validation,types,textures:storage_texture_types:*": { "subcaseMS": 167.510 }, + "webgpu:shader,validation,types,textures:texel_formats,as_value:*": { "subcaseMS": 0.518 }, + "webgpu:shader,validation,types,textures:texel_formats:*": { "subcaseMS": 1707.432 }, "webgpu:shader,validation,types,vector:vector:*": { "subcaseMS": 1.295 }, "webgpu:shader,validation,uniformity,uniformity:basics:*": { "subcaseMS": 1.467 }, "webgpu:shader,validation,uniformity,uniformity:binary_expressions:*": { "subcaseMS": 1.758 }, @@ -1948,6 +2483,7 @@ "webgpu:shader,validation,uniformity,uniformity:pointers:*": { "subcaseMS": 1.738 }, "webgpu:shader,validation,uniformity,uniformity:short_circuit_expressions:*": { "subcaseMS": 1.401 }, "webgpu:shader,validation,uniformity,uniformity:unary_expressions:*": { "subcaseMS": 1.279 }, + "webgpu:util,texture,color_space_conversions:util_matches_2d_canvas:*": { "subcaseMS": 1.001 }, "webgpu:util,texture,texel_data:float_texel_data_in_shader:*": { "subcaseMS": 2.042 }, "webgpu:util,texture,texel_data:sint_texel_data_in_shader:*": { "subcaseMS": 2.573 }, "webgpu:util,texture,texel_data:snorm_texel_data_in_shader:*": { "subcaseMS": 4.645 }, @@ -1994,9 +2530,11 @@ "webgpu:web_platform,copyToTexture,image:from_image:*": { "subcaseMS": 21.869 }, "webgpu:web_platform,copyToTexture,video:copy_from_video:*": { "subcaseMS": 25.101 }, "webgpu:web_platform,external_texture,video:importExternalTexture,compute:*": { "subcaseMS": 36.270 }, - "webgpu:web_platform,external_texture,video:importExternalTexture,sample:*": { "subcaseMS": 33.380 }, - "webgpu:web_platform,external_texture,video:importExternalTexture,sampleWithRotationMetadata:*": { "subcaseMS": 34.968 }, + "webgpu:web_platform,external_texture,video:importExternalTexture,sample:*": { "subcaseMS": 34.968 }, "webgpu:web_platform,external_texture,video:importExternalTexture,sampleWithVideoFrameWithVisibleRectParam:*": { "subcaseMS": 29.160 }, - "webgpu:web_platform,worker,worker:worker:*": { "subcaseMS": 245.901 }, + "webgpu:web_platform,external_texture,video:importExternalTexture,sample_non_YUV_video_frame:*": { "subcaseMS": 36.270 }, + "webgpu:web_platform,worker,worker:dedicated_worker:*": { "subcaseMS": 245.901 }, + "webgpu:web_platform,worker,worker:service_worker:*": { "subcaseMS": 102.800 }, + "webgpu:web_platform,worker,worker:shared_worker:*": { "subcaseMS": 26.801 }, "_end": "" } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/multisample_info.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/multisample_info.ts new file mode 100644 index 0000000000..f8d247a3de --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/multisample_info.ts @@ -0,0 +1,75 @@ +/* Data used for multisample tests */ + +const samplePositionToFragmentPosition = (pos: readonly number[]): readonly number[] => + pos.map(v => v / 16); +const samplePositionsToFragmentPositions = ( + positions: readonly (readonly number[])[] +): readonly (readonly number[])[] => positions.map(samplePositionToFragmentPosition); + +// These are sample positions based on a 16x16 grid with 0,0 at the top left. +// For example 8,8 would be a fragment coordinate of 0.5, 0.5 +// Based on: https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels +const kMultisamplingTables = new Map<number, readonly (readonly number[])[]>([ + [1, samplePositionsToFragmentPositions([[8, 8]])], + [ + 2, + samplePositionsToFragmentPositions([ + [4, 4], + [12, 12], + ]), + ], + [ + 4, + samplePositionsToFragmentPositions([ + [6, 2], + [14, 6], + [2, 10], + [10, 14], + ]), + ], + [ + 8, + samplePositionsToFragmentPositions([ + [9, 5], + [7, 11], + [13, 9], + [5, 3], + [3, 13], + [1, 7], + [11, 15], + [15, 1], + ]), + ], + [ + 16, + samplePositionsToFragmentPositions([ + [9, 9], + [7, 5], + [5, 10], + [12, 7], + + [3, 6], + [10, 13], + [13, 11], + [11, 3], + + [6, 14], + [8, 1], + [4, 2], + [2, 12], + + [0, 8], + [15, 4], + [14, 15], + [1, 0], + ]), + ], +]); + +/** + * For a given sampleCount returns an array of 2d fragment offsets + * where each offset is between 0 and 1. + */ +export function getMultisampleFragmentOffsets(sampleCount: number) { + return kMultisamplingTables.get(sampleCount)!; +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/print_environment.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/print_environment.spec.ts new file mode 100644 index 0000000000..44a34c22cb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/print_environment.spec.ts @@ -0,0 +1,70 @@ +export const description = ` + +Examples of writing CTS tests with various features. + +Start here when looking for examples of basic framework usage. +`; + +import { getResourcePath } from '../common/framework/resources.js'; +import { globalTestConfig } from '../common/framework/test_config.js'; +import { makeTestGroup } from '../common/framework/test_group.js'; +import { getDefaultRequestAdapterOptions } from '../common/util/navigator_gpu.js'; + +import { GPUTest } from './gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +/** console.log is disallowed by WPT. Work around it when we're not in WPT. */ +function consoleLogIfNotWPT(x: unknown) { + if (!('step_timeout' in globalThis)) { + const cons = console; + cons.log(x); + } +} + +g.test('info') + .desc( + `Test which prints what global scope (e.g. worker type) it's running in. +Typically, tests will check for the presence of the feature they need (like HTMLCanvasElement) +and skip if it's not available. + +Run this test under various configurations to see different results +(Window, worker scopes, Node, etc.) + +NOTE: If your test runtime elides logs when tests pass, you won't see the prints from this test +in the logs. On non-WPT runtimes, it will also print to the console with console.log. +WPT disallows console.log and doesn't support logs on passing tests, so this does nothing on WPT.` + ) + .fn(async t => { + const adapterInfo = await t.adapter.requestAdapterInfo(); + + const info = JSON.stringify( + { + globalScope: Object.getPrototypeOf(globalThis).constructor.name, + globalTestConfig, + baseResourcePath: getResourcePath(''), + defaultRequestAdapterOptions: getDefaultRequestAdapterOptions(), + adapterInfo, + userAgent: navigator.userAgent, + }, + // Flatten objects with prototype chains into plain objects, using `for..in`. (Otherwise, + // properties from the prototype chain will be ignored and things will print as `{}`.) + (_key, value) => { + if (value === undefined || value === null) return null; + if (typeof value !== 'object') return value; + + const valueObj = value as Record<string, unknown>; + return Object.fromEntries( + (function* () { + for (const key in valueObj) { + yield [key, valueObj[key]]; + } + })() + ); + }, + 2 + ); + + t.info(info); + consoleLogIfNotWPT(info); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/array/index.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/array/index.spec.ts new file mode 100644 index 0000000000..c98cf318a2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/array/index.spec.ts @@ -0,0 +1,354 @@ +export const description = ` +Execution Tests for array indexing expressions +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { + False, + True, + Type, + Value, + array, + f32, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { align } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { allInputSources, basicExpressionBuilder, run } from '../../expression.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('concrete_scalar') + .specURL('https://www.w3.org/TR/WGSL/#array-access-expr') + .desc(`Test indexing of an array of concrete scalars`) + .params(u => + u + .combine( + 'inputSource', + // 'uniform' address space requires array stride to be multiple of 16 bytes + allInputSources.filter(s => s !== 'uniform') + ) + .combine('elementType', ['i32', 'u32', 'f32', 'f16'] as const) + .combine('indexType', ['i32', 'u32'] as const) + ) + .beforeAllSubcases(t => { + if (t.params.elementType === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const indexType = Type[t.params.indexType]; + const cases: Case[] = [ + { + input: [ + array( + /* 0 */ elementType.create(10), + /* 1 */ elementType.create(11), + /* 2 */ elementType.create(12) + ), + indexType.create(0), + ], + expected: elementType.create(10), + }, + { + input: [ + array( + /* 0 */ elementType.create(20), + /* 1 */ elementType.create(21), + /* 2 */ elementType.create(22) + ), + indexType.create(1), + ], + expected: elementType.create(21), + }, + { + input: [ + array( + /* 0 */ elementType.create(30), + /* 1 */ elementType.create(31), + /* 2 */ elementType.create(32) + ), + indexType.create(2), + ], + expected: elementType.create(32), + }, + ]; + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}]`), + [Type.array(3, elementType), indexType], + elementType, + t.params, + cases + ); + }); + +g.test('bool') + .specURL('https://www.w3.org/TR/WGSL/#array-access-expr') + .desc(`Test indexing of an array of booleans`) + .params(u => + u + .combine( + 'inputSource', + // 'uniform' address space requires array stride to be multiple of 16 bytes + allInputSources.filter(s => s !== 'uniform') + ) + .combine('indexType', ['i32', 'u32'] as const) + ) + .fn(async t => { + const indexType = Type[t.params.indexType]; + const cases: Case[] = [ + { + input: [array(True, False, True), indexType.create(0)], + expected: True, + }, + { + input: [array(True, False, True), indexType.create(1)], + expected: False, + }, + { + input: [array(True, False, True), indexType.create(2)], + expected: True, + }, + ]; + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}]`), + [Type.array(3, Type.bool), indexType], + Type.bool, + t.params, + cases + ); + }); + +g.test('abstract_scalar') + .specURL('https://www.w3.org/TR/WGSL/#array-access-expr') + .desc(`Test indexing of an array of scalars`) + .params(u => + u + .combine('elementType', ['abstract-int', 'abstract-float'] as const) + .combine('indexType', ['i32', 'u32'] as const) + ) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const indexType = Type[t.params.indexType]; + const cases: Case[] = [ + { + input: [ + array( + /* 0 */ elementType.create(0x10_00000000), + /* 1 */ elementType.create(0x11_00000000), + /* 2 */ elementType.create(0x12_00000000) + ), + indexType.create(0), + ], + expected: f32(0x10), + }, + { + input: [ + array( + /* 0 */ elementType.create(0x20_00000000), + /* 1 */ elementType.create(0x21_00000000), + /* 2 */ elementType.create(0x22_00000000) + ), + indexType.create(1), + ], + expected: f32(0x21), + }, + { + input: [ + array( + /* 0 */ elementType.create(0x30_00000000), + /* 1 */ elementType.create(0x31_00000000), + /* 2 */ elementType.create(0x32_00000000) + ), + indexType.create(2), + ], + expected: f32(0x32), + }, + ]; + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}] / 0x100000000`), + [Type.array(3, elementType), indexType], + Type.f32, + { inputSource: 'const' }, + cases + ); + }); + +g.test('runtime_sized') + .specURL('https://www.w3.org/TR/WGSL/#array-access-expr') + .desc(`Test indexing of a runtime sized array`) + .params(u => + u + .combine('elementType', [ + 'i32', + 'u32', + 'f32', + 'f16', + 'vec4i', + 'vec2u', + 'vec3f', + 'vec2h', + ] as const) + .combine('indexType', ['i32', 'u32'] as const) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(Type[t.params.elementType]).kind === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const elementType = Type[t.params.elementType]; + const valueArrayType = Type.array(0, elementType); + const indexType = Type[t.params.indexType]; + const indexArrayType = Type.array(0, indexType); + + const wgsl = ` +${scalarTypeOf(elementType).kind === 'f16' ? 'enable f16;' : ''} + +@group(0) @binding(0) var<storage, read> input_values : ${valueArrayType}; +@group(0) @binding(1) var<storage, read> input_indices : ${indexArrayType}; +@group(0) @binding(2) var<storage, read_write> output : ${valueArrayType}; + +@compute @workgroup_size(16) +fn main(@builtin(local_invocation_index) invocation_id : u32) { + let index = input_indices[invocation_id]; + output[invocation_id] = input_values[index]; +} +`; + + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ code: wgsl }), + entryPoint: 'main', + }, + }); + + const values = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]; + const indices = [9, 0, 14, 10, 12, 4, 15, 3, 5, 6, 11, 2, 8, 13, 7, 1]; + + const inputValues = values.map(i => elementType.create(i)); + const inputIndices = indices.map(i => indexType.create(i)); + const expected = indices.map(i => inputValues[i]); + + const bufferSize = (arr: Value[]) => { + let offset = 0; + let alignment = 0; + for (const value of arr) { + alignment = Math.max(alignment, value.type.alignment); + offset = align(offset, value.type.alignment) + value.type.size; + } + return align(offset, alignment); + }; + + const toArray = (arr: Value[]) => { + const array = new Uint8Array(bufferSize(arr)); + let offset = 0; + for (const value of arr) { + offset = align(offset, value.type.alignment); + value.copyTo(array, offset); + offset += value.type.size; + } + return array; + }; + + const inputArrayBuffer = t.makeBufferWithContents(toArray(inputValues), GPUBufferUsage.STORAGE); + const inputIndexBuffer = t.makeBufferWithContents( + toArray(inputIndices), + GPUBufferUsage.STORAGE + ); + const outputBuffer = t.device.createBuffer({ + size: bufferSize(expected), + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: inputArrayBuffer } }, + { binding: 1, resource: { buffer: inputIndexBuffer } }, + { binding: 2, resource: { buffer: outputBuffer } }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(1); + pass.end(); + t.queue.submit([encoder.finish()]); + + t.expectGPUBufferValuesEqual(outputBuffer, toArray(expected)); + }); + +g.test('vector') + .specURL('https://www.w3.org/TR/WGSL/#array-access-expr') + .desc(`Test indexing of an array of vectors`) + .params(u => + u + .combine('inputSource', allInputSources) + .expand('elementType', t => + t.inputSource === 'uniform' + ? (['vec4i', 'vec4u', 'vec4f'] as const) + : (['vec4i', 'vec4u', 'vec4f', 'vec4h'] as const) + ) + .combine('indexType', ['i32', 'u32'] as const) + ) + .beforeAllSubcases(t => { + if (t.params.elementType === 'vec4h') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const indexType = Type[t.params.indexType]; + const cases: Case[] = [ + { + input: [ + array( + /* 0 */ elementType.create([0x10, 0x11, 0x12, 0x13]), + /* 1 */ elementType.create([0x14, 0x15, 0x16, 0x17]), + /* 2 */ elementType.create([0x18, 0x19, 0x1a, 0x1b]) + ), + indexType.create(0), + ], + expected: elementType.create([0x10, 0x11, 0x12, 0x13]), + }, + { + input: [ + array( + /* 0 */ elementType.create([0x20, 0x21, 0x22, 0x23]), + /* 1 */ elementType.create([0x24, 0x25, 0x26, 0x27]), + /* 2 */ elementType.create([0x28, 0x29, 0x2a, 0x2b]) + ), + indexType.create(1), + ], + expected: elementType.create([0x24, 0x25, 0x26, 0x27]), + }, + { + input: [ + array( + /* 0 */ elementType.create([0x30, 0x31, 0x32, 0x33]), + /* 1 */ elementType.create([0x34, 0x35, 0x36, 0x37]), + /* 2 */ elementType.create([0x38, 0x39, 0x3a, 0x3b]) + ), + indexType.create(2), + ], + expected: elementType.create([0x38, 0x39, 0x3a, 0x3b]), + }, + ]; + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}]`), + [Type.array(3, elementType), indexType], + elementType, + t.params, + cases + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/matrix/index.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/matrix/index.spec.ts new file mode 100644 index 0000000000..f6fd05b46f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/matrix/index.spec.ts @@ -0,0 +1,200 @@ +export const description = ` +Execution Tests for matrix indexing expressions +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { + MatrixValue, + ScalarValue, + Type, + abstractFloat, + f32, + vec, +} from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, basicExpressionBuilder, run } from '../../expression.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('concrete_float_column') + .specURL('https://www.w3.org/TR/WGSL/#matrix-access-expr') + .desc(`Test indexing a column vector from a concrete matrix`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('elementType', ['f32', 'f16'] as const) + .combine('indexType', ['i32', 'u32'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.elementType === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const indexType = Type[t.params.indexType]; + const matrixType = Type.mat(t.params.columns, t.params.rows, elementType); + const columnType = Type.vec(t.params.rows, elementType); + const elements: ScalarValue[][] = []; + for (let c = 0; c < t.params.columns; c++) { + const column: ScalarValue[] = []; + for (let r = 0; r < t.params.rows; r++) { + column.push(elementType.create((c + 1) * 10 + (r + 1))); + } + elements.push(column); + } + const vector = new MatrixValue(elements); + const cases: Case[] = []; + for (let c = 0; c < t.params.columns; c++) { + cases.push({ + input: [vector, indexType.create(c)], + expected: vec(...elements[c]), + }); + } + + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}]`), + [matrixType, indexType], + columnType, + t.params, + cases + ); + }); + +g.test('concrete_float_element') + .specURL('https://www.w3.org/TR/WGSL/#matrix-access-expr') + .desc(`Test indexing a single element from a concrete matrix`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('elementType', ['f32', 'f16'] as const) + .combine('indexType', ['i32', 'u32'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.elementType === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const indexType = Type[t.params.indexType]; + const matrixType = Type.mat(t.params.columns, t.params.rows, elementType); + const columnValues: ScalarValue[][] = []; + for (let c = 0; c < t.params.columns; c++) { + const column: ScalarValue[] = []; + for (let r = 0; r < t.params.rows; r++) { + column.push(elementType.create((c + 1) * 10 + (r + 1))); + } + columnValues.push(column); + } + const matrix = new MatrixValue(columnValues); + const cases: Case[] = []; + for (let c = 0; c < t.params.columns; c++) { + for (let r = 0; r < t.params.rows; r++) { + cases.push({ + input: [matrix, indexType.create(c), indexType.create(r)], + expected: columnValues[c][r], + }); + } + } + + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}][${ops[2]}]`), + [matrixType, indexType, indexType], + elementType, + t.params, + cases + ); + }); + +g.test('abstract_float_column') + .specURL('https://www.w3.org/TR/WGSL/#matrix-access-expr') + .desc(`Test indexing a column vector from a abstract-float matrix`) + .params(u => + u + .combine('indexType', ['i32', 'u32'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .fn(async t => { + const indexType = Type[t.params.indexType]; + const matrixType = Type.mat(t.params.columns, t.params.rows, Type.abstractFloat); + const vecfColumnType = Type.vec(t.params.rows, Type.f32); + const values: number[][] = []; + for (let c = 0; c < t.params.columns; c++) { + const column: number[] = []; + for (let r = 0; r < t.params.rows; r++) { + column.push((c + 1) * 10 + (r + 1)); + } + values.push(column); + } + const matrix = new MatrixValue( + values.map(column => column.map(v => abstractFloat(v * 0x100000000))) + ); + const cases: Case[] = []; + for (let c = 0; c < t.params.columns; c++) { + cases.push({ + input: [matrix, indexType.create(c)], + expected: vec(...values[c].map(v => f32(v))), + }); + } + + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}] / 0x100000000`), + [matrixType, indexType], + vecfColumnType, + { inputSource: 'const' }, + cases + ); + }); + +g.test('abstract_float_element') + .specURL('https://www.w3.org/TR/WGSL/#matrix-access-expr') + .desc(`Test indexing a single element from a abstract-float matrix`) + .params(u => + u + .combine('indexType', ['i32', 'u32'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .fn(async t => { + const indexType = Type[t.params.indexType]; + const matrixType = Type.mat(t.params.columns, t.params.rows, Type.abstractFloat); + const values: number[][] = []; + for (let c = 0; c < t.params.columns; c++) { + const column: number[] = []; + for (let r = 0; r < t.params.rows; r++) { + column.push((c + 1) * 10 + (r + 1)); + } + values.push(column); + } + const matrix = new MatrixValue( + values.map(column => column.map(v => abstractFloat(v * 0x100000000))) + ); + const cases: Case[] = []; + for (let c = 0; c < t.params.columns; c++) { + for (let r = 0; r < t.params.rows; r++) { + cases.push({ + input: [matrix, indexType.create(c), indexType.create(r)], + expected: f32(values[c][r]), + }); + } + } + + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}][${ops[2]}] / 0x100000000`), + [matrixType, indexType, indexType], + Type.f32, + { inputSource: 'const' }, + cases + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/structure/index.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/structure/index.spec.ts new file mode 100644 index 0000000000..ae56d11b6b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/structure/index.spec.ts @@ -0,0 +1,508 @@ +export const description = ` +Execution Tests for structure member accessing expressions +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { ScalarKind, Type, Value, u32 } from '../../../../../util/conversion.js'; +import { align } from '../../../../../util/math.js'; +import { toComparator } from '../../expectation.js'; +import { InputSource, structLayout, structStride } from '../../expression.js'; + +export const g = makeTestGroup(GPUTest); + +const kMemberTypes = [ + ['bool'], + ['u32'], + ['vec3f'], + ['i32', 'u32'], + ['i32', 'f16', 'vec4i', 'mat3x2f'], + ['bool', 'u32', 'f16', 'vec3f', 'vec2i'], + ['i32', 'u32', 'f32', 'f16', 'vec3f', 'vec4i'], +] as readonly ScalarKind[][]; + +const kMemberTypesNoBool = kMemberTypes.filter(tys => !tys.includes('bool')); + +async function run( + t: GPUTest, + wgsl: string, + expected: Value[], + input: Value[] | ArrayBufferLike | null, + inputSource: InputSource +) { + const outputBufferSize = structStride( + expected.map(v => v.type), + 'storage_rw' + ); + + const outputBuffer = t.device.createBuffer({ + size: outputBufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + }); + + const bindGroupEntries: GPUBindGroupEntry[] = [ + { + binding: 0, + resource: { buffer: outputBuffer }, + }, + ]; + + if (input !== null) { + let inputData: Uint8Array; + if (input instanceof Array) { + const inputTypes = input.map(v => v.type); + const inputBufferSize = structStride(inputTypes, inputSource); + inputData = new Uint8Array(inputBufferSize); + structLayout(inputTypes, inputSource, m => { + input[m.index].copyTo(inputData, m.offset); + }); + } else { + inputData = new Uint8Array(input); + } + + const inputBuffer = t.makeBufferWithContents( + inputData, + GPUBufferUsage.COPY_SRC | + (inputSource === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE) + ); + + bindGroupEntries.push({ + binding: 1, + resource: { buffer: inputBuffer }, + }); + } + + // build the shader module + const module = t.device.createShaderModule({ code: wgsl }); + + // build the pipeline + const pipeline = await t.device.createComputePipelineAsync({ + layout: 'auto', + compute: { module, entryPoint: 'main' }, + }); + + // build the bind group + const group = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: bindGroupEntries, + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, group); + pass.dispatchWorkgroups(1); + pass.end(); + + t.queue.submit([encoder.finish()]); + + const checkExpectation = (outputData: Uint8Array) => { + // The list of expectation failures + const errs: string[] = []; + + let offset = 0; + for (let i = 0; i < expected.length; i++) { + offset = align(offset, expected[i].type.alignment); + const got = expected[i].type.read(outputData, offset); + const cmp = toComparator(expected[i]).compare(got); + if (!cmp.matched) { + errs.push(`result ${i}:) + returned: ${cmp.got} + expected: ${cmp.expected}`); + } + offset += expected[i].type.size; + } + + return errs.length > 0 ? new Error(errs.join('\n\n')) : undefined; + }; + + t.expectGPUBufferValuesPassCheck(outputBuffer, checkExpectation, { + type: Uint8Array, + typedLength: outputBufferSize, + }); +} + +g.test('buffer') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc(`Test accessing of a value structure in a storage or uniform buffer`) + .params(u => + u + .combine('member_types', kMemberTypesNoBool) + .combine('inputSource', ['uniform', 'storage'] as const) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const values = t.params.member_types.map((ty, i) => Type[ty].create(i)); + const expected = values[t.params.member_index]; + + await run( + t, + ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; +@group(0) @binding(1) var<${t.params.inputSource}> input : MyStruct; + +struct MyStruct { + ${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; + +@workgroup_size(1) @compute +fn main() { + output = input.member_${t.params.member_index}; +} +`, + /* expected */ [expected], + /* input */ values, + /* inputSource */ t.params.inputSource === 'uniform' ? 'uniform' : 'storage_r' + ); + }); + +g.test('buffer_align') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc( + `Test accessing of a value structure in a storage buffer that has members using the @align attribute` + ) + .params(u => + u + .beginSubcases() + .combine('member_index', [0, 1, 2] as const) + .combine('alignments', [ + [1, 1, 1], + [4, 4, 4], + [4, 8, 16], + [8, 4, 16], + [8, 16, 4], + ] as const) + ) + .fn(async t => { + const memberTypes = ['i32', 'u32', 'f32'] as const; + const values = memberTypes.map((ty, i) => Type[ty].create(i)); + const expected = values[t.params.member_index]; + const input = new Uint8Array(64); + let offset = 4; // pre : i32 + for (let i = 0; i < 3; i++) { + offset = align(offset, t.params.alignments[i]); + values[i].copyTo(input, offset); + offset += values[i].type.size; + } + await run( + t, + ` +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; +@group(0) @binding(1) var<storage> input : MyStruct; + +struct MyStruct { + pre : i32, + @align(${t.params.alignments[0]}) member_0 : ${memberTypes[0]}, + @align(${t.params.alignments[1]}) member_1 : ${memberTypes[1]}, + @align(${t.params.alignments[2]}) member_2 : ${memberTypes[2]}, + post : i32, +}; + +@workgroup_size(1) @compute +fn main() { +output = input.member_${t.params.member_index}; +} +`, + /* expected */ [expected], + /* input */ input, + /* inputSource */ 'storage_r' + ); + }); + +g.test('buffer_size') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc( + `Test accessing of a value structure in a storage buffer that has members using the @size attribute` + ) + .params(u => + u + .beginSubcases() + .combine('member_index', [0, 1, 2] as const) + .combine('sizes', [ + [4, 4, 4], + [4, 8, 16], + [8, 4, 16], + [8, 16, 4], + ] as const) + ) + .fn(async t => { + const memberTypes = ['i32', 'u32', 'f32'] as const; + const values = memberTypes.map((ty, i) => Type[ty].create(i)); + const expected = values[t.params.member_index]; + const input = new Uint8Array(64); + let offset = 4; // pre : i32 + for (let i = 0; i < 3; i++) { + offset = align(offset, values[i].type.alignment); + values[i].copyTo(input, offset); + offset += t.params.sizes[i]; + } + await run( + t, + ` +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; +@group(0) @binding(1) var<storage> input : MyStruct; + +struct MyStruct { + pre : i32, + @size(${t.params.sizes[0]}) member_0 : ${memberTypes[0]}, + @size(${t.params.sizes[1]}) member_1 : ${memberTypes[1]}, + @size(${t.params.sizes[2]}) member_2 : ${memberTypes[2]}, + post : i32, +}; + +@workgroup_size(1) @compute +fn main() { +output = input.member_${t.params.member_index}; +} +`, + /* expected */ [expected], + /* input */ input, + /* inputSource */ 'storage_r' + ); + }); + +g.test('buffer_pointer') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc(`Test accessing of a value structure via a pointer to a storage or uniform buffer`) + .params(u => + u + .combine('member_types', kMemberTypesNoBool) + .combine('inputSource', ['uniform', 'storage'] as const) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const values = t.params.member_types.map((ty, i) => Type[ty].create(i)); + const expected = values[t.params.member_index]; + + await run( + t, + ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; +@group(0) @binding(1) var<${t.params.inputSource}> input : MyStruct; + +struct MyStruct { + ${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; + +@workgroup_size(1) @compute +fn main() { + let ptr = &input; + output = (*ptr).member_${t.params.member_index}; +} +`, + /* expected */ [expected], + /* input */ values, + /* inputSource */ t.params.inputSource === 'uniform' ? 'uniform' : 'storage_r' + ); + }); + +g.test('let') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc(`Test accessing of a let structure`) + .params(u => + u + .combine('member_types', kMemberTypes) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const memberType = Type[t.params.member_types[t.params.member_index]]; + const values = t.params.member_types.map((ty, i) => Type[ty].create(i)); + const expected = + memberType === Type.bool + ? u32(values[t.params.member_index].value === true ? 1 : 0) + : values[t.params.member_index]; + + await run( + t, + ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; + +struct MyStruct { + ${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; + +@workgroup_size(1) @compute +fn main() { + let s = MyStruct(${values.map(v => v.wgsl()).join(', ')}); + let v = s.member_${t.params.member_index}; + output = ${memberType === Type.bool ? `select(0u, 1u, v)` : 'v'}; +} +`, + /* expected */ [expected], + /* input */ null, + /* inputSource */ 'const' + ); + }); + +g.test('param') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc(`Test accessing of a parameter structure`) + .params(u => + u + .combine('member_types', kMemberTypes) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const memberType = Type[t.params.member_types[t.params.member_index]]; + const values = t.params.member_types.map((ty, i) => Type[ty].create(i)); + const expected = + memberType === Type.bool + ? u32(values[t.params.member_index].value === true ? 1 : 0) + : values[t.params.member_index]; + + await run( + t, + ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; + +struct MyStruct { + ${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; + +fn f(s : MyStruct) -> ${t.params.member_types[t.params.member_index]} { + return s.member_${t.params.member_index}; +} + +@workgroup_size(1) @compute +fn main() { + let v = f(MyStruct(${values.map(v => v.wgsl()).join(', ')})); + output = ${memberType === Type.bool ? `select(0u, 1u, v)` : 'v'}; +} +`, + /* expected */ [expected], + /* input */ null, + /* inputSource */ 'const' + ); + }); + +g.test('const') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc(`Test accessing of a const value structure`) + .params(u => + u + .combine('member_types', kMemberTypes) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const memberType = Type[t.params.member_types[t.params.member_index]]; + const values = t.params.member_types.map((ty, i) => Type[ty].create(i)); + const expected = + memberType === Type.bool + ? u32(values[t.params.member_index].value === true ? 1 : 0) + : values[t.params.member_index]; + + await run( + t, + ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; + +struct MyStruct { + ${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; + +const S = MyStruct(${values.map(v => v.wgsl()).join(', ')}); + +@workgroup_size(1) @compute +fn main() { + let v = S.member_${t.params.member_index}; + output = ${memberType === Type.bool ? `select(0u, 1u, v)` : 'v'}; +} +`, + /* expected */ [expected], + /* input */ null, + /* inputSource */ 'const' + ); + }); + +g.test('const_nested') + .specURL('https://www.w3.org/TR/WGSL/#struct-access-expr') + .desc(`Test accessing of a const value structure nested in another structure`) + .params(u => + u + .combine('member_types', kMemberTypes) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const memberType = Type[t.params.member_types[t.params.member_index]]; + const values = t.params.member_types.map((ty, i) => Type[ty].create(i)); + const expected = + memberType === Type.bool + ? u32(values[t.params.member_index].value === true ? 1 : 0) + : values[t.params.member_index]; + + await run( + t, + ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +@group(0) @binding(0) var<storage, read_write> output : ${expected.type}; + +struct MyStruct { + ${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; + +struct Outer { + pre : i32, + inner : MyStruct, + post : i32, +} + +const S = Outer(10, MyStruct(${values.map(v => v.wgsl()).join(', ')}), 20); + +@workgroup_size(1) @compute +fn main() { + let v = S.inner.member_${t.params.member_index}; + output = ${memberType === Type.bool ? `select(0u, 1u, v)` : 'v'}; +} +`, + /* expected */ [expected], + /* input */ null, + /* inputSource */ 'const' + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/vector/components.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/vector/components.spec.ts new file mode 100644 index 0000000000..0528337087 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/vector/components.spec.ts @@ -0,0 +1,118 @@ +export const description = ` +Execution Tests for vector component selection expressions +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { ScalarValue, Type, VectorValue, f32 } from '../../../../../util/conversion.js'; +import { allInputSources, basicExpressionBuilder, run } from '../../expression.js'; + +export const g = makeTestGroup(GPUTest); + +/** @returns the full permutation of component indices used to component select a vector of width 'n' */ +function indices(n: number) { + const out: number[][] = []; + for (let width = 1; width < n; width++) { + let generate = (swizzle: number[]) => { + out.push(swizzle); + }; + for (let i = 0; i < width; i++) { + const next = generate; + generate = (swizzle: number[]) => { + for (let j = 0; j < width; j++) { + next([...swizzle, j]); + } + }; + } + generate([]); + } + return out; +} + +g.test('concrete_scalar') + .specURL('https://www.w3.org/TR/WGSL/#vector-access-expr') + .desc(`Test vector component selection of concrete vectors`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('elementType', ['i32', 'u32', 'f32', 'f16', 'bool'] as const) + .combine('width', [2, 3, 4] as const) + .combine('components', ['rgba', 'xyzw'] as const) + .beginSubcases() + .expand('indices', u => indices(u.width)) + ) + .beforeAllSubcases(t => { + if (t.params.elementType === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const vectorType = Type.vec(t.params.width, elementType); + const elementValues = + t.params.elementType === 'bool' ? (i: number) => i & 1 : (i: number) => (i + 1) * 10; + const elements: ScalarValue[] = []; + for (let i = 0; i < t.params.width; i++) { + elements.push(elementType.create(elementValues(i))); + } + const result = (() => { + if (t.params.indices.length === 1) { + return { type: elementType, value: elementType.create(elementValues(0)) }; + } else { + const vec = Type.vec(t.params.indices.length, elementType); + return { type: vec, value: vec.create(t.params.indices.map(i => elementValues(i))) }; + } + })(); + + const components = t.params.indices.map(i => t.params.components[i]).join(''); + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}.${components}`), + [vectorType], + result.type, + t.params, + [{ input: [new VectorValue(elements)], expected: result.value }] + ); + }); + +g.test('abstract_scalar') + .specURL('https://www.w3.org/TR/WGSL/#vector-access-expr') + .desc(`Test vector component selection of abstract numeric vectors`) + .params(u => + u + .combine('elementType', ['abstract-int', 'abstract-float'] as const) + .combine('width', [2, 3, 4] as const) + .combine('components', ['rgba', 'xyzw'] as const) + .beginSubcases() + .expand('indices', u => indices(u.width)) + ) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const vectorType = Type.vec(t.params.width, elementType); + const elementValues = (i: number) => (i + 1) * 0x100000000; + const elements: ScalarValue[] = []; + for (let i = 0; i < t.params.width; i++) { + elements.push(elementType.create(elementValues(i))); + } + const result = (() => { + if (t.params.indices.length === 1) { + return { type: Type.f32, value: f32(elementValues(0) / 0x100000000) }; + } else { + const vec = Type.vec(t.params.indices.length, Type.f32); + return { + type: vec, + value: vec.create(t.params.indices.map(i => elementValues(i) / 0x100000000)), + }; + } + })(); + + const components = t.params.indices.map(i => t.params.components[i]).join(''); + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}.${components} / 0x100000000`), + [vectorType], + result.type, + { inputSource: 'const' }, + [{ input: [new VectorValue(elements)], expected: result.value }] + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/vector/index.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/vector/index.spec.ts new file mode 100644 index 0000000000..28fbd0e86c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/access/vector/index.spec.ts @@ -0,0 +1,87 @@ +export const description = ` +Execution Tests for vector indexing expressions +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { ScalarValue, Type, VectorValue, f32 } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, basicExpressionBuilder, run } from '../../expression.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('concrete_scalar') + .specURL('https://www.w3.org/TR/WGSL/#vector-access-expr') + .desc(`Test indexing of concrete vectors`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('elementType', ['i32', 'u32', 'f32', 'f16', 'bool'] as const) + .combine('indexType', ['i32', 'u32'] as const) + .combine('width', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.elementType === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const indexType = Type[t.params.indexType]; + const vectorType = Type.vec(t.params.width, elementType); + const elements: ScalarValue[] = []; + for (let i = 0; i < t.params.width; i++) { + if (t.params.elementType === 'bool') { + elements.push(elementType.create(i & 1)); + } else { + elements.push(elementType.create((i + 1) * 10)); + } + } + const vector = new VectorValue(elements); + const cases: Case[] = []; + for (let i = 0; i < t.params.width; i++) { + cases.push({ input: [vector, indexType.create(i)], expected: elements[i] }); + } + + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}]`), + [vectorType, indexType], + elementType, + t.params, + cases + ); + }); + +g.test('abstract_scalar') + .specURL('https://www.w3.org/TR/WGSL/#vector-access-expr') + .desc(`Test indexing of abstract numeric vectors`) + .params(u => + u + .combine('elementType', ['abstract-int', 'abstract-float'] as const) + .combine('indexType', ['i32', 'u32'] as const) + .combine('width', [2, 3, 4] as const) + ) + .fn(async t => { + const elementType = Type[t.params.elementType]; + const indexType = Type[t.params.indexType]; + const vectorType = Type.vec(t.params.width, elementType); + const elements: ScalarValue[] = []; + for (let i = 0; i < t.params.width; i++) { + elements.push(elementType.create((i + 1) * 0x100000000)); + } + const vector = new VectorValue(elements); + const cases: Case[] = []; + for (let i = 0; i < t.params.width; i++) { + cases.push({ input: [vector, indexType.create(i)], expected: f32(i + 1) }); + } + + await run( + t, + basicExpressionBuilder(ops => `${ops[0]}[${ops[1]}] / 0x100000000`), + [vectorType, indexType], + Type.f32, + { inputSource: 'const' }, + cases + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_addition.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_addition.cache.ts new file mode 100644 index 0000000000..f5e9d3b481 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_addition.cache.ts @@ -0,0 +1,54 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF64Range, sparseVectorF64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const additionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.additionInterval(e, s))); +}; + +const additionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.additionInterval(s, e))); +}; + +const scalar_cases = { + ['scalar']: () => { + return FP.abstract.generateScalarPairToIntervalCases( + sparseScalarF64Range(), + sparseScalarF64Range(), + 'finite', + FP.abstract.additionInterval + ); + }, +}; + +const vector_scalar_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`vec${dim}_scalar`]: () => { + return FP.abstract.generateVectorScalarToVectorCases( + sparseVectorF64Range(dim), + sparseScalarF64Range(), + 'finite', + additionVectorScalarInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`scalar_vec${dim}`]: () => { + return FP.abstract.generateScalarVectorToVectorCases( + sparseScalarF64Range(), + sparseVectorF64Range(dim), + 'finite', + additionScalarVectorInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_addition', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_addition.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_addition.spec.ts index 0f703f0889..52a07ff328 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_addition.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_addition.spec.ts @@ -1,70 +1,17 @@ export const description = ` -Execution Tests for non-matrix AbstractFloat addition expression +Execution Tests for non-matrix abstract-float addition expression `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF64Range, sparseVectorF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractBinary } from './binary.js'; - -const additionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.additionInterval(e, s))); -}; - -const additionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.additionInterval(s, e))); -}; +import { d } from './af_addition.cache.js'; +import { abstractFloatBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = { - ['scalar']: () => { - return FP.abstract.generateScalarPairToIntervalCases( - sparseF64Range(), - sparseF64Range(), - 'finite', - FP.abstract.additionInterval - ); - }, -}; - -const vector_scalar_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`vec${dim}_scalar`]: () => { - return FP.abstract.generateVectorScalarToVectorCases( - sparseVectorF64Range(dim), - sparseF64Range(), - 'finite', - additionVectorScalarInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`scalar_vec${dim}`]: () => { - return FP.abstract.generateScalarVectorToVectorCases( - sparseF64Range(), - sparseVectorF64Range(dim), - 'finite', - additionScalarVectorInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/af_addition', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -78,9 +25,9 @@ Accuracy: Correctly rounded const cases = await d.get('scalar'); await run( t, - abstractBinary('+'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('+'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -101,9 +48,9 @@ Accuracy: Correctly rounded const cases = await d.get('scalar'); // Using vectorize to generate vector cases based on scalar cases await run( t, - abstractBinary('+'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('+'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -123,9 +70,9 @@ Accuracy: Correctly rounded const cases = await d.get(`vec${dim}_scalar`); await run( t, - abstractBinary('+'), - [TypeVec(dim, TypeAbstractFloat), TypeAbstractFloat], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('+'), + [Type.vec(dim, Type.abstractFloat), Type.abstractFloat], + Type.vec(dim, Type.abstractFloat), t.params, cases ); @@ -145,9 +92,9 @@ Accuracy: Correctly rounded const cases = await d.get(`scalar_vec${dim}`); await run( t, - abstractBinary('+'), - [TypeAbstractFloat, TypeVec(dim, TypeAbstractFloat)], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('+'), + [Type.abstractFloat, Type.vec(dim, Type.abstractFloat)], + Type.vec(dim, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_comparison.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_comparison.cache.ts new file mode 100644 index 0000000000..e000266401 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_comparison.cache.ts @@ -0,0 +1,90 @@ +import { anyOf } from '../../../../util/compare.js'; +import { abstractFloat, bool, ScalarValue } from '../../../../util/conversion.js'; +import { flushSubnormalNumberF64, vectorF64Range } from '../../../../util/math.js'; +import { Case } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +/** + * @returns a test case for the provided left hand & right hand values and truth function. + * Handles quantization and subnormals. + */ +function makeCase( + lhs: number, + rhs: number, + truthFunc: (lhs: ScalarValue, rhs: ScalarValue) => boolean +): Case { + // Subnormal float values may be flushed at any time. + // https://www.w3.org/TR/WGSL/#floating-point-evaluation + const af_lhs = abstractFloat(lhs); + const af_rhs = abstractFloat(rhs); + const lhs_options = new Set([af_lhs, abstractFloat(flushSubnormalNumberF64(lhs))]); + const rhs_options = new Set([af_rhs, abstractFloat(flushSubnormalNumberF64(rhs))]); + const expected: Array<ScalarValue> = []; + lhs_options.forEach(l => { + rhs_options.forEach(r => { + const result = bool(truthFunc(l, r)); + if (!expected.includes(result)) { + expected.push(result); + } + }); + }); + + return { input: [af_lhs, af_rhs], expected: anyOf(...expected) }; +} + +export const d = makeCaseCache('binary/af_logical', { + equals: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) === (rhs.value as number); + }; + + return vectorF64Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + not_equals: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) !== (rhs.value as number); + }; + + return vectorF64Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_than: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) < (rhs.value as number); + }; + + return vectorF64Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_equals: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) <= (rhs.value as number); + }; + + return vectorF64Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_than: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) > (rhs.value as number); + }; + + return vectorF64Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_equals: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) >= (rhs.value as number); + }; + + return vectorF64Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_comparison.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_comparison.spec.ts index 5b8b1637b9..3941e12539 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_comparison.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_comparison.spec.ts @@ -1,110 +1,17 @@ export const description = ` -Execution Tests for the AbstractFloat comparison operations +Execution Tests for the abstract-float comparison operations `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { anyOf } from '../../../../util/compare.js'; -import { - abstractFloat, - bool, - Scalar, - TypeAbstractFloat, - TypeBool, -} from '../../../../util/conversion.js'; -import { flushSubnormalNumberF64, vectorF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, Case, run } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { allInputSources, run } from '../expression.js'; +import { d } from './af_comparison.cache.js'; import { binary } from './binary.js'; export const g = makeTestGroup(GPUTest); -/** - * @returns a test case for the provided left hand & right hand values and truth function. - * Handles quantization and subnormals. - */ -function makeCase( - lhs: number, - rhs: number, - truthFunc: (lhs: Scalar, rhs: Scalar) => boolean -): Case { - // Subnormal float values may be flushed at any time. - // https://www.w3.org/TR/WGSL/#floating-point-evaluation - const af_lhs = abstractFloat(lhs); - const af_rhs = abstractFloat(rhs); - const lhs_options = new Set([af_lhs, abstractFloat(flushSubnormalNumberF64(lhs))]); - const rhs_options = new Set([af_rhs, abstractFloat(flushSubnormalNumberF64(rhs))]); - const expected: Array<Scalar> = []; - lhs_options.forEach(l => { - rhs_options.forEach(r => { - const result = bool(truthFunc(l, r)); - if (!expected.includes(result)) { - expected.push(result); - } - }); - }); - - return { input: [af_lhs, af_rhs], expected: anyOf(...expected) }; -} - -export const d = makeCaseCache('binary/af_logical', { - equals: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) === (rhs.value as number); - }; - - return vectorF64Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - not_equals: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) !== (rhs.value as number); - }; - - return vectorF64Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_than: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) < (rhs.value as number); - }; - - return vectorF64Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_equals: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) <= (rhs.value as number); - }; - - return vectorF64Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_than: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) > (rhs.value as number); - }; - - return vectorF64Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_equals: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) >= (rhs.value as number); - }; - - return vectorF64Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, -}); - g.test('equals') .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') .desc( @@ -120,7 +27,14 @@ Accuracy: Correct result ) .fn(async t => { const cases = await d.get('equals'); - await run(t, binary('=='), [TypeAbstractFloat, TypeAbstractFloat], TypeBool, t.params, cases); + await run( + t, + binary('=='), + [Type.abstractFloat, Type.abstractFloat], + Type.bool, + t.params, + cases + ); }); g.test('not_equals') @@ -138,7 +52,14 @@ Accuracy: Correct result ) .fn(async t => { const cases = await d.get('not_equals'); - await run(t, binary('!='), [TypeAbstractFloat, TypeAbstractFloat], TypeBool, t.params, cases); + await run( + t, + binary('!='), + [Type.abstractFloat, Type.abstractFloat], + Type.bool, + t.params, + cases + ); }); g.test('less_than') @@ -156,7 +77,7 @@ Accuracy: Correct result ) .fn(async t => { const cases = await d.get('less_than'); - await run(t, binary('<'), [TypeAbstractFloat, TypeAbstractFloat], TypeBool, t.params, cases); + await run(t, binary('<'), [Type.abstractFloat, Type.abstractFloat], Type.bool, t.params, cases); }); g.test('less_equals') @@ -174,7 +95,14 @@ Accuracy: Correct result ) .fn(async t => { const cases = await d.get('less_equals'); - await run(t, binary('<='), [TypeAbstractFloat, TypeAbstractFloat], TypeBool, t.params, cases); + await run( + t, + binary('<='), + [Type.abstractFloat, Type.abstractFloat], + Type.bool, + t.params, + cases + ); }); g.test('greater_than') @@ -192,7 +120,7 @@ Accuracy: Correct result ) .fn(async t => { const cases = await d.get('greater_than'); - await run(t, binary('>'), [TypeAbstractFloat, TypeAbstractFloat], TypeBool, t.params, cases); + await run(t, binary('>'), [Type.abstractFloat, Type.abstractFloat], Type.bool, t.params, cases); }); g.test('greater_equals') @@ -210,5 +138,12 @@ Accuracy: Correct result ) .fn(async t => { const cases = await d.get('greater_equals'); - await run(t, binary('>='), [TypeAbstractFloat, TypeAbstractFloat], TypeBool, t.params, cases); + await run( + t, + binary('>='), + [Type.abstractFloat, Type.abstractFloat], + Type.bool, + t.params, + cases + ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_division.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_division.cache.ts new file mode 100644 index 0000000000..ff2fb6a578 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_division.cache.ts @@ -0,0 +1,57 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF64Range, sparseVectorF64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const divisionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + // division has an ulp accuracy, so abstract is only expected to be as accurate as f32 + return FP.abstract.toVector(v.map(e => FP.f32.divisionInterval(e, s))); +}; + +const divisionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + // division has an ulp accuracy, so abstract is only expected to be as accurate as f32 + return FP.abstract.toVector(v.map(e => FP.f32.divisionInterval(s, e))); +}; + +const scalar_cases = { + ['scalar']: () => { + return FP.abstract.generateScalarPairToIntervalCases( + sparseScalarF64Range(), + sparseScalarF64Range(), + 'finite', + // division has an ulp accuracy, so abstract is only expected to be as accurate as f32 + FP.f32.divisionInterval + ); + }, +}; + +const vector_scalar_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`vec${dim}_scalar`]: () => { + return FP.abstract.generateVectorScalarToVectorCases( + sparseVectorF64Range(dim), + sparseScalarF64Range(), + 'finite', + divisionVectorScalarInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`scalar_vec${dim}`]: () => { + return FP.abstract.generateScalarVectorToVectorCases( + sparseScalarF64Range(), + sparseVectorF64Range(dim), + 'finite', + divisionScalarVectorInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_division', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_division.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_division.spec.ts index 4c1765d203..0ebe30b6cc 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_division.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_division.spec.ts @@ -1,70 +1,17 @@ export const description = ` -Execution Tests for non-matrix AbstractFloat division expression +Execution Tests for non-matrix abstract-float division expression `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF64Range, sparseVectorF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractBinary } from './binary.js'; - -const divisionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.divisionInterval(e, s))); -}; - -const divisionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.divisionInterval(s, e))); -}; +import { d } from './af_division.cache.js'; +import { abstractFloatBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = { - ['scalar']: () => { - return FP.abstract.generateScalarPairToIntervalCases( - sparseF64Range(), - sparseF64Range(), - 'finite', - FP.abstract.divisionInterval - ); - }, -}; - -const vector_scalar_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`vec${dim}_scalar`]: () => { - return FP.abstract.generateVectorScalarToVectorCases( - sparseVectorF64Range(dim), - sparseF64Range(), - 'finite', - divisionVectorScalarInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`scalar_vec${dim}`]: () => { - return FP.abstract.generateScalarVectorToVectorCases( - sparseF64Range(), - sparseVectorF64Range(dim), - 'finite', - divisionScalarVectorInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/af_division', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -78,9 +25,9 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get('scalar'); await run( t, - abstractBinary('/'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('/'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -101,9 +48,9 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get('scalar'); // Using vectorize to generate vector cases based on scalar cases await run( t, - abstractBinary('/'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('/'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -123,9 +70,9 @@ Accuracy: Correctly rounded const cases = await d.get(`vec${dim}_scalar`); await run( t, - abstractBinary('/'), - [TypeVec(dim, TypeAbstractFloat), TypeAbstractFloat], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('/'), + [Type.vec(dim, Type.abstractFloat), Type.abstractFloat], + Type.vec(dim, Type.abstractFloat), t.params, cases ); @@ -145,9 +92,9 @@ Accuracy: Correctly rounded const cases = await d.get(`scalar_vec${dim}`); await run( t, - abstractBinary('/'), - [TypeAbstractFloat, TypeVec(dim, TypeAbstractFloat)], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('/'), + [Type.abstractFloat, Type.vec(dim, Type.abstractFloat)], + Type.vec(dim, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_addition.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_addition.cache.ts new file mode 100644 index 0000000000..e89250f57b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_addition.cache.ts @@ -0,0 +1,26 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF64Range } from '../../../../util/math.js'; +import { selectNCases } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR +const mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).map(rows => ({ + [`mat${cols}x${rows}`]: () => { + return selectNCases( + 'binary/af_matrix_addition', + 50, + FP.abstract.generateMatrixPairToMatrixCases( + sparseMatrixF64Range(cols, rows), + sparseMatrixF64Range(cols, rows), + 'finite', + FP.abstract.additionMatrixMatrixInterval + ) + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_matrix_addition', mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_addition.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_addition.spec.ts index 86bddec894..49c746c53e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_addition.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_addition.spec.ts @@ -1,37 +1,17 @@ export const description = ` -Execution Tests for matrix AbstractFloat addition expressions +Execution Tests for matrix abstract-float addition expressions `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractBinary } from './binary.js'; +import { d } from './af_matrix_addition.cache.js'; +import { abstractFloatBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR -const mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).map(rows => ({ - [`mat${cols}x${rows}`]: () => { - return FP.abstract.generateMatrixPairToMatrixCases( - sparseMatrixF64Range(cols, rows), - sparseMatrixF64Range(cols, rows), - 'finite', - FP.abstract.additionMatrixMatrixInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/af_matrix_addition', mat_cases); - g.test('matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -52,9 +32,9 @@ Accuracy: Correctly rounded const cases = await d.get(`mat${cols}x${rows}`); await run( t, - abstractBinary('+'), - [TypeMat(cols, rows, TypeAbstractFloat), TypeMat(cols, rows, TypeAbstractFloat)], - TypeMat(cols, rows, TypeAbstractFloat), + abstractFloatBinary('+'), + [Type.mat(cols, rows, Type.abstractFloat), Type.mat(cols, rows, Type.abstractFloat)], + Type.mat(cols, rows, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_matrix_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_matrix_multiplication.cache.ts new file mode 100644 index 0000000000..78512fbd0d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_matrix_multiplication.cache.ts @@ -0,0 +1,29 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF64Range } from '../../../../util/math.js'; +import { selectNCases } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matKxR_matCxK +const mat_mat_cases = ([2, 3, 4] as const) + .flatMap(k => + ([2, 3, 4] as const).flatMap(cols => + ([2, 3, 4] as const).map(rows => ({ + [`mat${k}x${rows}_mat${cols}x${k}`]: () => { + return selectNCases( + 'binary/af_matrix_matrix_multiplication', + 10, + FP.abstract.generateMatrixPairToMatrixCases( + sparseMatrixF64Range(k, rows), + sparseMatrixF64Range(cols, k), + 'finite', + // Matrix-matrix multiplication has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP.f32.multiplicationMatrixMatrixInterval + ) + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_matrix_matrix_multiplication', mat_mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_matrix_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_matrix_multiplication.spec.ts new file mode 100644 index 0000000000..9320fb1226 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_matrix_multiplication.spec.ts @@ -0,0 +1,45 @@ +export const description = ` +Execution Tests for matrix-matrix AbstractFloat multiplication expression +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { Type } from '../../../../util/conversion.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { d } from './af_matrix_matrix_multiplication.cache.js'; +import { abstractFloatBinary } from './binary.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('matrix_matrix') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x * y, where x is a matrix and y is a matrix +Accuracy: Correctly rounded +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('common_dim', [2, 3, 4] as const) + .combine('x_rows', [2, 3, 4] as const) + .combine('y_cols', [2, 3, 4] as const) + ) + .fn(async t => { + const x_cols = t.params.common_dim; + const x_rows = t.params.x_rows; + const y_cols = t.params.y_cols; + const y_rows = t.params.common_dim; + + const cases = await d.get(`mat${x_cols}x${x_rows}_mat${y_cols}x${y_rows}`); + await run( + t, + abstractFloatBinary('*'), + [Type.mat(x_cols, x_rows, Type.abstractFloat), Type.mat(y_cols, y_rows, Type.abstractFloat)], + Type.mat(y_cols, x_rows, Type.abstractFloat), + t.params, + cases + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_scalar_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_scalar_multiplication.cache.ts new file mode 100644 index 0000000000..9106362970 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_scalar_multiplication.cache.ts @@ -0,0 +1,49 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF64Range, sparseScalarF64Range } from '../../../../util/math.js'; +import { selectNCases } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_scalar +const mat_scalar_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).map(rows => ({ + [`mat${cols}x${rows}_scalar`]: () => { + return selectNCases( + 'binary/af_matrix_scalar_multiplication_mat_scalar', + 50, + FP.abstract.generateMatrixScalarToMatrixCases( + sparseMatrixF64Range(cols, rows), + sparseScalarF64Range(), + 'finite', + FP.abstract.multiplicationMatrixScalarInterval + ) + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: scalar_matCxR +const scalar_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).map(rows => ({ + [`scalar_mat${cols}x${rows}`]: () => { + return selectNCases( + 'binary/af_matrix_scalar_multiplication_scalar_mat', + 50, + FP.abstract.generateScalarMatrixToMatrixCases( + sparseScalarF64Range(), + sparseMatrixF64Range(cols, rows), + 'finite', + FP.abstract.multiplicationScalarMatrixInterval + ) + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_matrix_scalar_multiplication', { + ...mat_scalar_cases, + ...scalar_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_scalar_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_scalar_multiplication.spec.ts new file mode 100644 index 0000000000..c6faabbc84 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_scalar_multiplication.spec.ts @@ -0,0 +1,69 @@ +export const description = ` +Execution Tests for matrix-scalar and scalar-matrix AbstractFloat multiplication expression +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { Type } from '../../../../util/conversion.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { d } from './af_matrix_scalar_multiplication.cache.js'; +import { abstractFloatBinary } from './binary.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('matrix_scalar') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x * y, where x is a matrix and y is a scalar +Accuracy: Correctly rounded +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('cols', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .fn(async t => { + const cols = t.params.cols; + const rows = t.params.rows; + const cases = await d.get(`mat${cols}x${rows}_scalar`); + await run( + t, + abstractFloatBinary('*'), + [Type.mat(cols, rows, Type.abstractFloat), Type.abstractFloat], + Type.mat(cols, rows, Type.abstractFloat), + t.params, + cases + ); + }); + +g.test('scalar_matrix') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x * y, where x is a scalar and y is a matrix +Accuracy: Correctly rounded +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('cols', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .fn(async t => { + const cols = t.params.cols; + const rows = t.params.rows; + const cases = await d.get(`scalar_mat${cols}x${rows}`); + await run( + t, + abstractFloatBinary('*'), + [Type.abstractFloat, Type.mat(cols, rows, Type.abstractFloat)], + Type.mat(cols, rows, Type.abstractFloat), + t.params, + cases + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_subtraction.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_subtraction.cache.ts new file mode 100644 index 0000000000..c3e5e856dc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_subtraction.cache.ts @@ -0,0 +1,26 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF64Range } from '../../../../util/math.js'; +import { selectNCases } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR +const mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).map(rows => ({ + [`mat${cols}x${rows}`]: () => { + return selectNCases( + 'binary/af_matrix_subtraction', + 50, + FP.abstract.generateMatrixPairToMatrixCases( + sparseMatrixF64Range(cols, rows), + sparseMatrixF64Range(cols, rows), + 'finite', + FP.abstract.subtractionMatrixMatrixInterval + ) + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_matrix_subtraction', mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_subtraction.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_subtraction.spec.ts index 849c11611f..9b240fdee9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_subtraction.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_subtraction.spec.ts @@ -1,37 +1,17 @@ export const description = ` -Execution Tests for matrix AbstractFloat subtraction expression +Execution Tests for matrix abstract-float subtraction expression `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractBinary } from './binary.js'; +import { d } from './af_matrix_subtraction.cache.js'; +import { abstractFloatBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR -const mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).map(rows => ({ - [`mat${cols}x${rows}`]: () => { - return FP.abstract.generateMatrixPairToMatrixCases( - sparseMatrixF64Range(cols, rows), - sparseMatrixF64Range(cols, rows), - 'finite', - FP.abstract.subtractionMatrixMatrixInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/af_matrix_subtraction', mat_cases); - g.test('matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -52,9 +32,9 @@ Accuracy: Correctly rounded const cases = await d.get(`mat${cols}x${rows}`); await run( t, - abstractBinary('-'), - [TypeMat(cols, rows, TypeAbstractFloat), TypeMat(cols, rows, TypeAbstractFloat)], - TypeMat(cols, rows, TypeAbstractFloat), + abstractFloatBinary('-'), + [Type.mat(cols, rows, Type.abstractFloat), Type.mat(cols, rows, Type.abstractFloat)], + Type.mat(cols, rows, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_vector_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_vector_multiplication.cache.ts new file mode 100644 index 0000000000..4578eb0ce4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_vector_multiplication.cache.ts @@ -0,0 +1,51 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF64Range, sparseVectorF64Range } from '../../../../util/math.js'; +import { selectNCases } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_vecC +const mat_vec_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).map(rows => ({ + [`mat${cols}x${rows}_vec${cols}`]: () => { + return selectNCases( + 'binary/af_matrix_vector_multiplication_mat_vec', + 50, + FP.abstract.generateMatrixVectorToVectorCases( + sparseMatrixF64Range(cols, rows), + sparseVectorF64Range(cols), + 'finite', + // Matrix-vector multiplication has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP.f32.multiplicationMatrixVectorInterval + ) + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: vecR_matCxR +const vec_mat_cases = ([2, 3, 4] as const) + .flatMap(rows => + ([2, 3, 4] as const).map(cols => ({ + [`vec${rows}_mat${cols}x${rows}`]: () => { + return selectNCases( + 'binary/af_matrix_vector_multiplication_vec_mat', + 50, + FP.abstract.generateVectorMatrixToVectorCases( + sparseVectorF64Range(rows), + sparseMatrixF64Range(cols, rows), + 'finite', + // Vector-matrix multiplication has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP.f32.multiplicationVectorMatrixInterval + ) + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_matrix_vector_multiplication', { + ...mat_vec_cases, + ...vec_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_vector_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_vector_multiplication.spec.ts new file mode 100644 index 0000000000..5db78f8369 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_matrix_vector_multiplication.spec.ts @@ -0,0 +1,69 @@ +export const description = ` +Execution Tests for matrix-vector and vector-matrix AbstractFloat multiplication expression +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { Type } from '../../../../util/conversion.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { d } from './af_matrix_vector_multiplication.cache.js'; +import { abstractFloatBinary } from './binary.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('matrix_vector') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x * y, where x is a matrix and y is a vector +Accuracy: Correctly rounded +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('cols', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .fn(async t => { + const cols = t.params.cols; + const rows = t.params.rows; + const cases = await d.get(`mat${cols}x${rows}_vec${cols}`); + await run( + t, + abstractFloatBinary('*'), + [Type.mat(cols, rows, Type.abstractFloat), Type.vec(cols, Type.abstractFloat)], + Type.vec(rows, Type.abstractFloat), + t.params, + cases + ); + }); + +g.test('vector_matrix') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x * y, where x is a vector and y is is a matrix +Accuracy: Correctly rounded +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('cols', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .fn(async t => { + const cols = t.params.cols; + const rows = t.params.rows; + const cases = await d.get(`vec${rows}_mat${cols}x${rows}`); + await run( + t, + abstractFloatBinary('*'), + [Type.vec(rows, Type.abstractFloat), Type.mat(cols, rows, Type.abstractFloat)], + Type.vec(cols, Type.abstractFloat), + t.params, + cases + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_multiplication.cache.ts new file mode 100644 index 0000000000..e111682cd2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_multiplication.cache.ts @@ -0,0 +1,54 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF64Range, sparseVectorF64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const multiplicationVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.multiplicationInterval(e, s))); +}; + +const multiplicationScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.multiplicationInterval(s, e))); +}; + +const scalar_cases = { + ['scalar']: () => { + return FP.abstract.generateScalarPairToIntervalCases( + sparseScalarF64Range(), + sparseScalarF64Range(), + 'finite', + FP.abstract.multiplicationInterval + ); + }, +}; + +const vector_scalar_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`vec${dim}_scalar`]: () => { + return FP.abstract.generateVectorScalarToVectorCases( + sparseVectorF64Range(dim), + sparseScalarF64Range(), + 'finite', + multiplicationVectorScalarInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`scalar_vec${dim}`]: () => { + return FP.abstract.generateScalarVectorToVectorCases( + sparseScalarF64Range(), + sparseVectorF64Range(dim), + 'finite', + multiplicationScalarVectorInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_multiplication', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_multiplication.spec.ts index 6b15812703..405de758bd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_multiplication.spec.ts @@ -1,70 +1,17 @@ export const description = ` -Execution Tests for non-matrix AbstractFloat multiplication expression +Execution Tests for non-matrix abstract-float multiplication expression `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF64Range, sparseVectorF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractBinary } from './binary.js'; - -const multiplicationVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.multiplicationInterval(e, s))); -}; - -const multiplicationScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.multiplicationInterval(s, e))); -}; +import { d } from './af_multiplication.cache.js'; +import { abstractFloatBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = { - ['scalar']: () => { - return FP.abstract.generateScalarPairToIntervalCases( - sparseF64Range(), - sparseF64Range(), - 'finite', - FP.abstract.multiplicationInterval - ); - }, -}; - -const vector_scalar_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`vec${dim}_scalar`]: () => { - return FP.abstract.generateVectorScalarToVectorCases( - sparseVectorF64Range(dim), - sparseF64Range(), - 'finite', - multiplicationVectorScalarInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`scalar_vec${dim}`]: () => { - return FP.abstract.generateScalarVectorToVectorCases( - sparseF64Range(), - sparseVectorF64Range(dim), - 'finite', - multiplicationScalarVectorInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/af_multiplication', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -78,9 +25,9 @@ Accuracy: Correctly rounded const cases = await d.get('scalar'); await run( t, - abstractBinary('*'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('*'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -101,9 +48,9 @@ Accuracy: Correctly rounded const cases = await d.get('scalar'); // Using vectorize to generate vector cases based on scalar cases await run( t, - abstractBinary('*'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('*'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -123,9 +70,9 @@ Accuracy: Correctly rounded const cases = await d.get(`vec${dim}_scalar`); await run( t, - abstractBinary('*'), - [TypeVec(dim, TypeAbstractFloat), TypeAbstractFloat], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('*'), + [Type.vec(dim, Type.abstractFloat), Type.abstractFloat], + Type.vec(dim, Type.abstractFloat), t.params, cases ); @@ -145,9 +92,9 @@ Accuracy: Correctly rounded const cases = await d.get(`scalar_vec${dim}`); await run( t, - abstractBinary('*'), - [TypeAbstractFloat, TypeVec(dim, TypeAbstractFloat)], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('*'), + [Type.abstractFloat, Type.vec(dim, Type.abstractFloat)], + Type.vec(dim, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_remainder.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_remainder.cache.ts new file mode 100644 index 0000000000..4817298167 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_remainder.cache.ts @@ -0,0 +1,57 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF64Range, sparseVectorF64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const remainderVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + // remainder has an inherited accuracy, so abstract is only expected to be as accurate as f32 + return FP.abstract.toVector(v.map(e => FP.f32.remainderInterval(e, s))); +}; + +const remainderScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + // remainder has an inherited accuracy, so abstract is only expected to be as accurate as f32 + return FP.abstract.toVector(v.map(e => FP.f32.remainderInterval(s, e))); +}; + +const scalar_cases = { + ['scalar']: () => { + return FP.abstract.generateScalarPairToIntervalCases( + sparseScalarF64Range(), + sparseScalarF64Range(), + 'finite', + // remainder has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP.f32.remainderInterval + ); + }, +}; + +const vector_scalar_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`vec${dim}_scalar`]: () => { + return FP.abstract.generateVectorScalarToVectorCases( + sparseVectorF64Range(dim), + sparseScalarF64Range(), + 'finite', + remainderVectorScalarInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`scalar_vec${dim}`]: () => { + return FP.abstract.generateScalarVectorToVectorCases( + sparseScalarF64Range(), + sparseVectorF64Range(dim), + 'finite', + remainderScalarVectorInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_remainder', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_remainder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_remainder.spec.ts index b4ce930bdb..d743f85ed6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_remainder.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_remainder.spec.ts @@ -4,67 +4,14 @@ Execution Tests for non-matrix abstract float remainder expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF64Range, sparseVectorF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractBinary } from './binary.js'; - -const remainderVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.remainderInterval(e, s))); -}; - -const remainderScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.remainderInterval(s, e))); -}; +import { d } from './af_remainder.cache.js'; +import { abstractFloatBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = { - ['scalar']: () => { - return FP.abstract.generateScalarPairToIntervalCases( - sparseF64Range(), - sparseF64Range(), - 'finite', - FP.abstract.remainderInterval - ); - }, -}; - -const vector_scalar_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`vec${dim}_scalar`]: () => { - return FP.abstract.generateVectorScalarToVectorCases( - sparseVectorF64Range(dim), - sparseF64Range(), - 'finite', - remainderVectorScalarInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`scalar_vec${dim}`]: () => { - return FP.abstract.generateScalarVectorToVectorCases( - sparseF64Range(), - sparseVectorF64Range(dim), - 'finite', - remainderScalarVectorInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/af_remainder', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -78,9 +25,9 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get('scalar'); await run( t, - abstractBinary('%'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('%'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -101,9 +48,9 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get('scalar'); // Using vectorize to generate vector cases based on scalar cases await run( t, - abstractBinary('%'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('%'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -123,9 +70,9 @@ Accuracy: Correctly rounded const cases = await d.get(`vec${dim}_scalar`); await run( t, - abstractBinary('%'), - [TypeVec(dim, TypeAbstractFloat), TypeAbstractFloat], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('%'), + [Type.vec(dim, Type.abstractFloat), Type.abstractFloat], + Type.vec(dim, Type.abstractFloat), t.params, cases ); @@ -145,9 +92,9 @@ Accuracy: Correctly rounded const cases = await d.get(`scalar_vec${dim}`); await run( t, - abstractBinary('%'), - [TypeAbstractFloat, TypeVec(dim, TypeAbstractFloat)], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('%'), + [Type.abstractFloat, Type.vec(dim, Type.abstractFloat)], + Type.vec(dim, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_subtraction.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_subtraction.cache.ts new file mode 100644 index 0000000000..ea5107e143 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_subtraction.cache.ts @@ -0,0 +1,54 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF64Range, sparseVectorF64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const subtractionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.subtractionInterval(e, s))); +}; + +const subtractionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.subtractionInterval(s, e))); +}; + +const scalar_cases = { + ['scalar']: () => { + return FP.abstract.generateScalarPairToIntervalCases( + sparseScalarF64Range(), + sparseScalarF64Range(), + 'finite', + FP.abstract.subtractionInterval + ); + }, +}; + +const vector_scalar_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`vec${dim}_scalar`]: () => { + return FP.abstract.generateVectorScalarToVectorCases( + sparseVectorF64Range(dim), + sparseScalarF64Range(), + 'finite', + subtractionVectorScalarInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`scalar_vec${dim}`]: () => { + return FP.abstract.generateScalarVectorToVectorCases( + sparseScalarF64Range(), + sparseVectorF64Range(dim), + 'finite', + subtractionScalarVectorInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_subtraction', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_subtraction.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_subtraction.spec.ts index 00dc66feb9..2874a744da 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_subtraction.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/af_subtraction.spec.ts @@ -1,70 +1,17 @@ export const description = ` -Execution Tests for non-matrix AbstractFloat subtraction expression +Execution Tests for non-matrix abstract-float subtraction expression `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF64Range, sparseVectorF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractBinary } from './binary.js'; - -const subtractionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.subtractionInterval(e, s))); -}; - -const subtractionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.abstract.toVector(v.map(e => FP.abstract.subtractionInterval(s, e))); -}; +import { d } from './af_subtraction.cache.js'; +import { abstractFloatBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = { - ['scalar']: () => { - return FP.abstract.generateScalarPairToIntervalCases( - sparseF64Range(), - sparseF64Range(), - 'finite', - FP.abstract.subtractionInterval - ); - }, -}; - -const vector_scalar_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`vec${dim}_scalar`]: () => { - return FP.abstract.generateVectorScalarToVectorCases( - sparseVectorF64Range(dim), - sparseF64Range(), - 'finite', - subtractionVectorScalarInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .map(dim => ({ - [`scalar_vec${dim}`]: () => { - return FP.abstract.generateScalarVectorToVectorCases( - sparseF64Range(), - sparseVectorF64Range(dim), - 'finite', - subtractionScalarVectorInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/af_subtraction', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -78,9 +25,9 @@ Accuracy: Correctly rounded const cases = await d.get('scalar'); await run( t, - abstractBinary('-'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('-'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -101,9 +48,9 @@ Accuracy: Correctly rounded const cases = await d.get('scalar'); // Using vectorize to generate vector cases based on scalar cases await run( t, - abstractBinary('-'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBinary('-'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -123,9 +70,9 @@ Accuracy: Correctly rounded const cases = await d.get(`vec${dim}_scalar`); await run( t, - abstractBinary('-'), - [TypeVec(dim, TypeAbstractFloat), TypeAbstractFloat], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('-'), + [Type.vec(dim, Type.abstractFloat), Type.abstractFloat], + Type.vec(dim, Type.abstractFloat), t.params, cases ); @@ -145,9 +92,9 @@ Accuracy: Correctly rounded const cases = await d.get(`scalar_vec${dim}`); await run( t, - abstractBinary('-'), - [TypeAbstractFloat, TypeVec(dim, TypeAbstractFloat)], - TypeVec(dim, TypeAbstractFloat), + abstractFloatBinary('-'), + [Type.abstractFloat, Type.vec(dim, Type.abstractFloat)], + Type.vec(dim, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_arithmetic.cache.ts new file mode 100644 index 0000000000..48300e8013 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_arithmetic.cache.ts @@ -0,0 +1,145 @@ +import { kValue } from '../../../../util/constants.js'; +import { sparseI64Range, vectorI64Range } from '../../../../util/math.js'; +import { + generateBinaryToI64Cases, + generateI64VectorBinaryToVectorCases, + generateVectorI64BinaryToVectorCases, +} from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +function ai_add(x: bigint, y: bigint): bigint | undefined { + const result = x + y; + return !kValue.i64.isOOB(result) ? result : undefined; +} + +function ai_div(x: bigint, y: bigint): bigint | undefined { + if (y === 0n) return undefined; + if (x === kValue.i64.negative.min && y === -1n) return undefined; + const result = x / y; + return !kValue.i64.isOOB(result) ? result : undefined; +} + +function ai_mul(x: bigint, y: bigint): bigint | undefined { + const result = x * y; + return !kValue.i64.isOOB(result) ? result : undefined; +} + +function ai_rem(x: bigint, y: bigint): bigint | undefined { + if (y === 0n) return undefined; + if (x === kValue.i64.negative.min && y === -1n) return undefined; + const result = x % y; + return !kValue.i64.isOOB(result) ? result : undefined; +} + +function ai_sub(x: bigint, y: bigint): bigint | undefined { + const result = x - y; + return !kValue.i64.isOOB(result) ? result : undefined; +} + +export const d = makeCaseCache('binary/ai_arithmetic', { + addition: () => { + return generateBinaryToI64Cases(sparseI64Range(), sparseI64Range(), ai_add); + }, + addition_scalar_vector2: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(2), ai_add); + }, + addition_scalar_vector3: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(3), ai_add); + }, + addition_scalar_vector4: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(4), ai_add); + }, + addition_vector2_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(2), sparseI64Range(), ai_add); + }, + addition_vector3_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(3), sparseI64Range(), ai_add); + }, + addition_vector4_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(4), sparseI64Range(), ai_add); + }, + division: () => { + return generateBinaryToI64Cases(sparseI64Range(), sparseI64Range(), ai_div); + }, + division_scalar_vector2: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(2), ai_div); + }, + division_scalar_vector3: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(3), ai_div); + }, + division_scalar_vector4: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(4), ai_div); + }, + division_vector2_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(2), sparseI64Range(), ai_div); + }, + division_vector3_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(3), sparseI64Range(), ai_div); + }, + division_vector4_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(4), sparseI64Range(), ai_div); + }, + multiplication: () => { + return generateBinaryToI64Cases(sparseI64Range(), sparseI64Range(), ai_mul); + }, + multiplication_scalar_vector2: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(2), ai_mul); + }, + multiplication_scalar_vector3: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(3), ai_mul); + }, + multiplication_scalar_vector4: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(4), ai_mul); + }, + multiplication_vector2_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(2), sparseI64Range(), ai_mul); + }, + multiplication_vector3_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(3), sparseI64Range(), ai_mul); + }, + multiplication_vector4_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(4), sparseI64Range(), ai_mul); + }, + remainder: () => { + return generateBinaryToI64Cases(sparseI64Range(), sparseI64Range(), ai_rem); + }, + remainder_scalar_vector2: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(2), ai_rem); + }, + remainder_scalar_vector3: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(3), ai_rem); + }, + remainder_scalar_vector4: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(4), ai_rem); + }, + remainder_vector2_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(2), sparseI64Range(), ai_rem); + }, + remainder_vector3_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(3), sparseI64Range(), ai_rem); + }, + remainder_vector4_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(4), sparseI64Range(), ai_rem); + }, + subtraction: () => { + return generateBinaryToI64Cases(sparseI64Range(), sparseI64Range(), ai_sub); + }, + subtraction_scalar_vector2: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(2), ai_sub); + }, + subtraction_scalar_vector3: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(3), ai_sub); + }, + subtraction_scalar_vector4: () => { + return generateI64VectorBinaryToVectorCases(sparseI64Range(), vectorI64Range(4), ai_sub); + }, + subtraction_vector2_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(2), sparseI64Range(), ai_sub); + }, + subtraction_vector3_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(3), sparseI64Range(), ai_sub); + }, + subtraction_vector4_scalar: () => { + return generateVectorI64BinaryToVectorCases(vectorI64Range(4), sparseI64Range(), ai_sub); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_arithmetic.spec.ts new file mode 100644 index 0000000000..ef211af3ed --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_arithmetic.spec.ts @@ -0,0 +1,303 @@ +export const description = ` +Execution Tests for the abstract int arithmetic binary expression operations +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { Type } from '../../../../util/conversion.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { d } from './ai_arithmetic.cache.js'; +import { abstractIntBinary } from './binary.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('addition') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x + y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('addition'); + await run( + t, + abstractIntBinary('+'), + [Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('addition_scalar_vector') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x + y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_rhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_rhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`addition_scalar_vector${vec_size}`); + await run(t, abstractIntBinary('+'), [Type.abstractInt, vec_type], vec_type, t.params, cases); + }); + +g.test('addition_vector_scalar') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x + y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_lhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_lhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`addition_vector${vec_size}_scalar`); + await run(t, abstractIntBinary('+'), [vec_type, Type.abstractInt], vec_type, t.params, cases); + }); + +g.test('division') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x / y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('division'); + await run( + t, + abstractIntBinary('/'), + [Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('division_scalar_vector') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x / y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_rhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_rhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`division_scalar_vector${vec_size}`); + await run(t, abstractIntBinary('/'), [Type.abstractInt, vec_type], vec_type, t.params, cases); + }); + +g.test('division_vector_scalar') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x / y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_lhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_lhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`division_vector${vec_size}_scalar`); + await run(t, abstractIntBinary('/'), [vec_type, Type.abstractInt], vec_type, t.params, cases); + }); + +g.test('multiplication') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x * y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('multiplication'); + await run( + t, + abstractIntBinary('*'), + [Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('multiplication_scalar_vector') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x * y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_rhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_rhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`multiplication_scalar_vector${vec_size}`); + await run(t, abstractIntBinary('*'), [Type.abstractInt, vec_type], vec_type, t.params, cases); + }); + +g.test('multiplication_vector_scalar') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x * y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_lhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_lhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`multiplication_vector${vec_size}_scalar`); + await run(t, abstractIntBinary('*'), [vec_type, Type.abstractInt], vec_type, t.params, cases); + }); + +g.test('remainder') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x % y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('remainder'); + await run( + t, + abstractIntBinary('%'), + [Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('remainder_scalar_vector') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x % y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_rhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_rhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`remainder_scalar_vector${vec_size}`); + await run(t, abstractIntBinary('%'), [Type.abstractInt, vec_type], vec_type, t.params, cases); + }); + +g.test('remainder_vector_scalar') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x % y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_lhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_lhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`remainder_vector${vec_size}_scalar`); + await run(t, abstractIntBinary('%'), [vec_type, Type.abstractInt], vec_type, t.params, cases); + }); + +g.test('subtraction') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x - y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('subtraction'); + await run( + t, + abstractIntBinary('-'), + [Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('subtraction_scalar_vector') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x - y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_rhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_rhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`subtraction_scalar_vector${vec_size}`); + await run(t, abstractIntBinary('-'), [Type.abstractInt, vec_type], vec_type, t.params, cases); + }); + +g.test('subtraction_vector_scalar') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: x - y +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize_lhs', [2, 3, 4] as const) + ) + .fn(async t => { + const vec_size = t.params.vectorize_lhs; + const vec_type = Type.vec(vec_size, Type.abstractInt); + const cases = await d.get(`subtraction_vector${vec_size}_scalar`); + await run(t, abstractIntBinary('-'), [vec_type, Type.abstractInt], vec_type, t.params, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_comparison.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_comparison.spec.ts new file mode 100644 index 0000000000..899e651054 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/ai_comparison.spec.ts @@ -0,0 +1,124 @@ +export const description = ` +Execution Tests for the abstract-int comparison expressions +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { bool, abstractInt, Type } from '../../../../util/conversion.js'; +import { vectorI64Range } from '../../../../util/math.js'; +import { Case } from '../case.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { binary } from './binary.js'; + +export const g = makeTestGroup(GPUTest); + +/** + * @returns a test case for the provided left hand & right hand values and + * expected boolean result. + */ +function makeCase(lhs: bigint, rhs: bigint, expected_answer: boolean): Case { + return { input: [abstractInt(lhs), abstractInt(rhs)], expected: bool(expected_answer) }; +} + +g.test('equals') + .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') + .desc( + ` +Expression: x == y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = vectorI64Range(2).map(v => makeCase(v[0], v[1], v[0] === v[1])); + await run(t, binary('=='), [Type.abstractInt, Type.abstractInt], Type.bool, t.params, cases); + }); + +g.test('not_equals') + .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') + .desc( + ` +Expression: x != y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = vectorI64Range(2).map(v => makeCase(v[0], v[1], v[0] !== v[1])); + await run(t, binary('!='), [Type.abstractInt, Type.abstractInt], Type.bool, t.params, cases); + }); + +g.test('less_than') + .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') + .desc( + ` +Expression: x < y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = vectorI64Range(2).map(v => makeCase(v[0], v[1], v[0] < v[1])); + await run(t, binary('<'), [Type.abstractInt, Type.abstractInt], Type.bool, t.params, cases); + }); + +g.test('less_equals') + .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') + .desc( + ` +Expression: x <= y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = vectorI64Range(2).map(v => makeCase(v[0], v[1], v[0] <= v[1])); + await run(t, binary('<='), [Type.abstractInt, Type.abstractInt], Type.bool, t.params, cases); + }); + +g.test('greater_than') + .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') + .desc( + ` +Expression: x > y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = vectorI64Range(2).map(v => makeCase(v[0], v[1], v[0] > v[1])); + await run(t, binary('>'), [Type.abstractInt, Type.abstractInt], Type.bool, t.params, cases); + }); + +g.test('greater_equals') + .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') + .desc( + ` +Expression: x >= y +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = vectorI64Range(2).map(v => makeCase(v[0], v[1], v[0] >= v[1])); + await run(t, binary('>='), [Type.abstractInt, Type.abstractInt], Type.bool, t.params, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts index f0b01b839b..4df0c67d78 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts @@ -3,6 +3,7 @@ import { basicExpressionBuilder, compoundAssignmentBuilder, abstractFloatShaderBuilder, + abstractIntShaderBuilder, } from '../expression.js'; /* @returns a ShaderBuilder that evaluates a binary operation */ @@ -16,6 +17,11 @@ export function compoundBinary(op: string): ShaderBuilder { } /* @returns a ShaderBuilder that evaluates a binary operation that returns AbstractFloats */ -export function abstractBinary(op: string): ShaderBuilder { +export function abstractFloatBinary(op: string): ShaderBuilder { return abstractFloatShaderBuilder(values => `(${values.map(v => `(${v})`).join(op)})`); } + +/* @returns a ShaderBuilder that evaluates a binary operation that returns AbstractFloats */ +export function abstractIntBinary(op: string): ShaderBuilder { + return abstractIntShaderBuilder(values => `(${values.map(v => `(${v})`).join(op)})`); +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts index 0d8d775352..b6e9a45e9d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts @@ -3,59 +3,171 @@ Execution Tests for the bitwise binary expression operations `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { assert } from '../../../../../common/util/util.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { i32, scalarType, u32 } from '../../../../util/conversion.js'; -import { allInputSources, run } from '../expression.js'; +import { + abstractIntBits, + i32Bits, + ScalarValue, + scalarType, + u32Bits, +} from '../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../expression.js'; -import { binary, compoundBinary } from './binary.js'; +import { abstractIntBinary, binary, compoundBinary } from './binary.js'; export const g = makeTestGroup(GPUTest); -function makeBitwiseOrCases(inputType: string) { - const V = inputType === 'i32' ? i32 : u32; - const cases = [ - // Static patterns +/** + * Collection of functions and values required to implement bitwise tests for a + * specific scalar type + */ +interface ScalarImpl { + // builder is a mostly a wrapper around type builders like 'i32Bits' that + // handles the (number|bigint) type check. + builder: (bits: bigint | number) => ScalarValue; + size: 32 | 64; +} + +const kScalarImpls = { + i32: { + builder: (bits: bigint | number): ScalarValue => { + assert(typeof bits === 'number'); + return i32Bits(bits); + }, + size: 32, + } as ScalarImpl, + u32: { + builder: (bits: bigint | number): ScalarValue => { + assert(typeof bits === 'number'); + return u32Bits(bits); + }, + size: 32, + } as ScalarImpl, + 'abstract-int': { + builder: (bits: bigint | number): ScalarValue => { + assert(typeof bits === 'bigint'); + return abstractIntBits(bits); + }, + size: 64, + } as ScalarImpl, +}; + +/** Wrapper for converting from input type strings to the appropriate implementation */ +function scalarImplForInputType(inputType: string): ScalarImpl { + assert(inputType === 'i32' || inputType === 'u32' || inputType === 'abstract-int'); + return kScalarImpls[inputType]; +} + +/** Manually calculated bitwise-or cases used a check that the CTS test is correct */ +const kBitwiseOrStaticPatterns = { + 32: [ { - input: [V(0b00000000000000000000000000000000), V(0b00000000000000000000000000000000)], - expected: V(0b00000000000000000000000000000000), + input: [0b00000000000000000000000000000000, 0b00000000000000000000000000000000], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b11111111111111111111111111111111), V(0b00000000000000000000000000000000)], - expected: V(0b11111111111111111111111111111111), + input: [0b11111111111111111111111111111111, 0b00000000000000000000000000000000], + expected: 0b11111111111111111111111111111111, }, { - input: [V(0b00000000000000000000000000000000), V(0b11111111111111111111111111111111)], - expected: V(0b11111111111111111111111111111111), + input: [0b00000000000000000000000000000000, 0b11111111111111111111111111111111], + expected: 0b11111111111111111111111111111111, }, { - input: [V(0b11111111111111111111111111111111), V(0b11111111111111111111111111111111)], - expected: V(0b11111111111111111111111111111111), + input: [0b11111111111111111111111111111111, 0b11111111111111111111111111111111], + expected: 0b11111111111111111111111111111111, }, { - input: [V(0b10100100010010100100010010100100), V(0b00000000000000000000000000000000)], - expected: V(0b10100100010010100100010010100100), + input: [0b10100100010010100100010010100100, 0b00000000000000000000000000000000], + expected: 0b10100100010010100100010010100100, }, { - input: [V(0b00000000000000000000000000000000), V(0b10100100010010100100010010100100)], - expected: V(0b10100100010010100100010010100100), + input: [0b00000000000000000000000000000000, 0b10100100010010100100010010100100], + expected: 0b10100100010010100100010010100100, }, { - input: [V(0b01010010001001010010001001010010), V(0b10100100010010100100010010100100)], - expected: V(0b11110110011011110110011011110110), + input: [0b01010010001001010010001001010010, 0b10100100010010100100010010100100], + expected: 0b11110110011011110110011011110110, }, - ]; - // Permute all combinations of a single bit being set for the LHS and RHS - for (let i = 0; i < 32; i++) { - const lhs = 1 << i; - for (let j = 0; j < 32; j++) { - const rhs = 1 << j; - cases.push({ - input: [V(lhs), V(rhs)], - expected: V(lhs | rhs), + ], + 64: [ + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b1111111111111111111111111111111111111111111111111111111111111111n, + }, + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b1111111111111111111111111111111111111111111111111111111111111111n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b1111111111111111111111111111111111111111111111111111111111111111n, + }, + { + input: [ + 0b1010010001001010010001001010010010100100010010100100010010100100n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b1010010001001010010001001010010010100100010010100100010010100100n, + }, + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b1010010001001010010001001010010010100100010010100100010010100100n, + ], + expected: 0b1010010001001010010001001010010010100100010010100100010010100100n, + }, + { + input: [ + 0b0101001000100101001000100101001010100100010010100100010010100100n, + 0b1010010001001010010001001010010010100100010010100100010010100100n, + ], + expected: 0b1111011001101111011001101111011010100100010010100100010010100100n, + }, + ], +}; + +/** @returns a set of bitwise-or cases for the specific input type */ +function makeBitwiseOrCases(inputType: string) { + const impl = scalarImplForInputType(inputType); + const indices = + impl.size === 64 ? [...Array(impl.size).keys()].map(BigInt) : [...Array(impl.size).keys()]; + + return [ + ...kBitwiseOrStaticPatterns[impl.size].map(c => { + return { + input: c.input.map(impl.builder), + expected: impl.builder(c.expected), + }; + }), + // Permute all combinations of a single bit being set for the LHS and RHS + ...indices.flatMap(i => { + const lhs = typeof i === 'bigint' ? 1n << i : 1 << i; + return indices.map(j => { + const rhs = typeof j === 'bigint' ? 1n << j : 1 << j; + assert(typeof lhs === typeof rhs); + const result = typeof lhs === 'bigint' ? lhs | (rhs as bigint) : lhs | (rhs as number); + return { input: [impl.builder(lhs), impl.builder(rhs)], expected: impl.builder(result) }; }); - } - } - return cases; + }), + ]; } g.test('bitwise_or') @@ -63,22 +175,25 @@ g.test('bitwise_or') .desc( ` e1 | e2: T -T is i32, u32, vecN<i32>, or vecN<u32> +T is i32, u32, abstractInt, vecN<i32>, vecN<u32>, or vecN<abstractInt> Bitwise-or. Component-wise when T is a vector. ` ) .params(u => u - .combine('type', ['i32', 'u32'] as const) + .combine('type', ['i32', 'u32', 'abstract-int'] as const) .combine('inputSource', allInputSources) .combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { + t.skipIf( + t.params.type === 'abstract-int' && !onlyConstInputSource.includes(t.params.inputSource) + ); const type = scalarType(t.params.type); const cases = makeBitwiseOrCases(t.params.type); - - await run(t, binary('|'), [type, type], type, t.params, cases); + const builder = t.params.type === 'abstract-int' ? abstractIntBinary('|') : binary('|'); + await run(t, builder, [type, type], type, t.params, cases); }); g.test('bitwise_or_compound') @@ -104,59 +219,137 @@ Bitwise-or. Component-wise when T is a vector. await run(t, compoundBinary('|='), [type, type], type, t.params, cases); }); -function makeBitwiseAndCases(inputType: string) { - const V = inputType === 'i32' ? i32 : u32; - const cases = [ - // Static patterns +/** Manually calculated bitwise-and cases used a check that the CTS test is correct */ +const kBitwiseAndStaticPatterns = { + 32: [ { - input: [V(0b00000000000000000000000000000000), V(0b00000000000000000000000000000000)], - expected: V(0b00000000000000000000000000000000), + input: [0b00000000000000000000000000000000, 0b00000000000000000000000000000000], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b11111111111111111111111111111111), V(0b00000000000000000000000000000000)], - expected: V(0b00000000000000000000000000000000), + input: [0b11111111111111111111111111111111, 0b00000000000000000000000000000000], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b00000000000000000000000000000000), V(0b11111111111111111111111111111111)], - expected: V(0b00000000000000000000000000000000), + input: [0b00000000000000000000000000000000, 0b11111111111111111111111111111111], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b11111111111111111111111111111111), V(0b11111111111111111111111111111111)], - expected: V(0b11111111111111111111111111111111), + input: [0b11111111111111111111111111111111, 0b11111111111111111111111111111111], + expected: 0b11111111111111111111111111111111, }, { - input: [V(0b10100100010010100100010010100100), V(0b00000000000000000000000000000000)], - expected: V(0b00000000000000000000000000000000), + input: [0b10100100010010100100010010100100, 0b00000000000000000000000000000000], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b10100100010010100100010010100100), V(0b11111111111111111111111111111111)], - expected: V(0b10100100010010100100010010100100), + input: [0b10100100010010100100010010100100, 0b11111111111111111111111111111111], + expected: 0b10100100010010100100010010100100, }, { - input: [V(0b00000000000000000000000000000000), V(0b10100100010010100100010010100100)], - expected: V(0b00000000000000000000000000000000), + input: [0b00000000000000000000000000000000, 0b10100100010010100100010010100100], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b11111111111111111111111111111111), V(0b10100100010010100100010010100100)], - expected: V(0b10100100010010100100010010100100), + input: [0b11111111111111111111111111111111, 0b10100100010010100100010010100100], + expected: 0b10100100010010100100010010100100, }, { - input: [V(0b01010010001001010010001001010010), V(0b01011011101101011011101101011011)], - expected: V(0b01010010001001010010001001010010), + input: [0b01010010001001010010001001010010, 0b01011011101101011011101101011011], + expected: 0b01010010001001010010001001010010, }, - ]; - // Permute all combinations of a single bit being set for the LHS and all but one bit set for the RHS - for (let i = 0; i < 32; i++) { - const lhs = 1 << i; - for (let j = 0; j < 32; j++) { - const rhs = 0xffffffff ^ (1 << j); - cases.push({ - input: [V(lhs), V(rhs)], - expected: V(lhs & rhs), + ], + 64: [ + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b1111111111111111111111111111111111111111111111111111111111111111n, + }, + { + input: [ + 0b1010010001001010010001001010010010100100010010100100010010100100n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b1010010001001010010001001010010010100100010010100100010010100100n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b1010010001001010010001001010010010100100010010100100010010100100n, + }, + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b1010010001001010010001001010010010100100010010100100010010100100n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b1010010001001010010001001010010010100100010010100100010010100100n, + ], + expected: 0b1010010001001010010001001010010010100100010010100100010010100100n, + }, + { + input: [ + 0b0101001000100101001000100101001001010010001001010010001001010010n, + 0b0101101110110101101110110101101101011011101101011011101101011011n, + ], + expected: 0b0101001000100101001000100101001001010010001001010010001001010010n, + }, + ], +}; + +/** @returns a set of bitwise-or cases for the specific input type */ +function makeBitwiseAndCases(inputType: string) { + const impl = scalarImplForInputType(inputType); + const indices = + impl.size === 64 ? [...Array(impl.size).keys()].map(BigInt) : [...Array(impl.size).keys()]; + + return [ + ...kBitwiseAndStaticPatterns[impl.size].map(c => { + return { + input: c.input.map(impl.builder), + expected: impl.builder(c.expected), + }; + }), + // Permute all combinations of a single bit being set for the LHS and all but one bit set for the RHS + ...indices.flatMap(i => { + const lhs = typeof i === 'bigint' ? 1n << i : 1 << i; + return indices.map(j => { + const rhs = typeof j === 'bigint' ? 0xffffffffffffffffn ^ (1n << j) : 0xffffffff ^ (1 << j); + assert(typeof lhs === typeof rhs); + const result = typeof lhs === 'bigint' ? lhs & (rhs as bigint) : lhs & (rhs as number); + return { input: [impl.builder(lhs), impl.builder(rhs)], expected: impl.builder(result) }; }); - } - } - return cases; + }), + ]; } g.test('bitwise_and') @@ -164,21 +357,25 @@ g.test('bitwise_and') .desc( ` e1 & e2: T -T is i32, u32, vecN<i32>, or vecN<u32> +T is i32, u32, AbstractInt, vecN<i32>, vecN<u32>, or vecN<AbstractInt> Bitwise-and. Component-wise when T is a vector. ` ) .params(u => u - .combine('type', ['i32', 'u32'] as const) + .combine('type', ['i32', 'u32', 'abstract-int'] as const) .combine('inputSource', allInputSources) .combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { + t.skipIf( + t.params.type === 'abstract-int' && !onlyConstInputSource.includes(t.params.inputSource) + ); const type = scalarType(t.params.type); const cases = makeBitwiseAndCases(t.params.type); - await run(t, binary('&'), [type, type], type, t.params, cases); + const builder = t.params.type === 'abstract-int' ? abstractIntBinary('&') : binary('&'); + await run(t, builder, [type, type], type, t.params, cases); }); g.test('bitwise_and_compound') @@ -203,59 +400,137 @@ Bitwise-and. Component-wise when T is a vector. await run(t, compoundBinary('&='), [type, type], type, t.params, cases); }); -function makeBitwiseExcluseOrCases(inputType: string) { - const V = inputType === 'i32' ? i32 : u32; - const cases = [ - // Static patterns +/** Manually calculated bitwise-or cases used a check that the CTS test is correct */ +const kBitwiseExclusiveOrStaticPatterns = { + 32: [ { - input: [V(0b00000000000000000000000000000000), V(0b00000000000000000000000000000000)], - expected: V(0b00000000000000000000000000000000), + input: [0b00000000000000000000000000000000, 0b00000000000000000000000000000000], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b11111111111111111111111111111111), V(0b00000000000000000000000000000000)], - expected: V(0b11111111111111111111111111111111), + input: [0b11111111111111111111111111111111, 0b00000000000000000000000000000000], + expected: 0b11111111111111111111111111111111, }, { - input: [V(0b00000000000000000000000000000000), V(0b11111111111111111111111111111111)], - expected: V(0b11111111111111111111111111111111), + input: [0b00000000000000000000000000000000, 0b11111111111111111111111111111111], + expected: 0b11111111111111111111111111111111, }, { - input: [V(0b11111111111111111111111111111111), V(0b11111111111111111111111111111111)], - expected: V(0b00000000000000000000000000000000), + input: [0b11111111111111111111111111111111, 0b11111111111111111111111111111111], + expected: 0b00000000000000000000000000000000, }, { - input: [V(0b10100100010010100100010010100100), V(0b00000000000000000000000000000000)], - expected: V(0b10100100010010100100010010100100), + input: [0b10100100010010100100010010100100, 0b00000000000000000000000000000000], + expected: 0b10100100010010100100010010100100, }, { - input: [V(0b10100100010010100100010010100100), V(0b11111111111111111111111111111111)], - expected: V(0b01011011101101011011101101011011), + input: [0b10100100010010100100010010100100, 0b11111111111111111111111111111111], + expected: 0b01011011101101011011101101011011, }, { - input: [V(0b00000000000000000000000000000000), V(0b10100100010010100100010010100100)], - expected: V(0b10100100010010100100010010100100), + input: [0b00000000000000000000000000000000, 0b10100100010010100100010010100100], + expected: 0b10100100010010100100010010100100, }, { - input: [V(0b11111111111111111111111111111111), V(0b10100100010010100100010010100100)], - expected: V(0b01011011101101011011101101011011), + input: [0b11111111111111111111111111111111, 0b10100100010010100100010010100100], + expected: 0b01011011101101011011101101011011, }, { - input: [V(0b01010010001001010010001001010010), V(0b01011011101101011011101101011011)], - expected: V(0b00001001100100001001100100001001), + input: [0b01010010001001010010001001010010, 0b01011011101101011011101101011011], + expected: 0b00001001100100001001100100001001, }, - ]; - // Permute all combinations of a single bit being set for the LHS and all but one bit set for the RHS - for (let i = 0; i < 32; i++) { - const lhs = 1 << i; - for (let j = 0; j < 32; j++) { - const rhs = 0xffffffff ^ (1 << j); - cases.push({ - input: [V(lhs), V(rhs)], - expected: V(lhs ^ rhs), + ], + 64: [ + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b1111111111111111111111111111111111111111111111111111111111111111n, + }, + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b1111111111111111111111111111111111111111111111111111111111111111n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b0000000000000000000000000000000000000000000000000000000000000000n, + }, + { + input: [ + 0b1010010001001010010001001010010010100100010010100100010010100100n, + 0b0000000000000000000000000000000000000000000000000000000000000000n, + ], + expected: 0b1010010001001010010001001010010010100100010010100100010010100100n, + }, + { + input: [ + 0b1010010001001010010001001010010010100100010010100100010010100100n, + 0b1111111111111111111111111111111111111111111111111111111111111111n, + ], + expected: 0b0101101110110101101110110101101101011011101101011011101101011011n, + }, + { + input: [ + 0b0000000000000000000000000000000000000000000000000000000000000000n, + 0b1010010001001010010001001010010010100100010010100100010010100100n, + ], + expected: 0b1010010001001010010001001010010010100100010010100100010010100100n, + }, + { + input: [ + 0b1111111111111111111111111111111111111111111111111111111111111111n, + 0b1010010001001010010001001010010010100100010010100100010010100100n, + ], + expected: 0b0101101110110101101110110101101101011011101101011011101101011011n, + }, + { + input: [ + 0b0101001000100101001000100101001001010010001001010010001001010010n, + 0b0101101110110101101110110101101101011011101101011011101101011011n, + ], + expected: 0b0000100110010000100110010000100100001001100100001001100100001001n, + }, + ], +}; + +/** @returns a set of bitwise-xor cases for the specific input type */ +function makeBitwiseExclusiveOrCases(inputType: string) { + const impl = scalarImplForInputType(inputType); + const indices = + impl.size === 64 ? [...Array(impl.size).keys()].map(BigInt) : [...Array(impl.size).keys()]; + + return [ + ...kBitwiseExclusiveOrStaticPatterns[impl.size].map(c => { + return { + input: c.input.map(impl.builder), + expected: impl.builder(c.expected), + }; + }), + // Permute all combinations of a single bit being set for the LHS and all but one bit set for the RHS + ...indices.flatMap(i => { + const lhs = typeof i === 'bigint' ? 1n << i : 1 << i; + return indices.map(j => { + const rhs = typeof j === 'bigint' ? 0xffffffffffffffffn ^ (1n << j) : 0xffffffff ^ (1 << j); + assert(typeof lhs === typeof rhs); + const result = typeof lhs === 'bigint' ? lhs ^ (rhs as bigint) : lhs ^ (rhs as number); + return { input: [impl.builder(lhs), impl.builder(rhs)], expected: impl.builder(result) }; }); - } - } - return cases; + }), + ]; } g.test('bitwise_exclusive_or') @@ -263,21 +538,25 @@ g.test('bitwise_exclusive_or') .desc( ` e1 ^ e2: T -T is i32, u32, vecN<i32>, or vecN<u32> +T is i32, u32, abstractInt, vecN<i32>, vecN<u32>, or vecN<abstractInt> Bitwise-exclusive-or. Component-wise when T is a vector. ` ) .params(u => u - .combine('type', ['i32', 'u32'] as const) + .combine('type', ['i32', 'u32', 'abstract-int'] as const) .combine('inputSource', allInputSources) .combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { + t.skipIf( + t.params.type === 'abstract-int' && !onlyConstInputSource.includes(t.params.inputSource) + ); const type = scalarType(t.params.type); - const cases = makeBitwiseExcluseOrCases(t.params.type); - await run(t, binary('^'), [type, type], type, t.params, cases); + const cases = makeBitwiseExclusiveOrCases(t.params.type); + const builder = t.params.type === 'abstract-int' ? abstractIntBinary('^') : binary('^'); + await run(t, builder, [type, type], type, t.params, cases); }); g.test('bitwise_exclusive_or_compound') @@ -298,6 +577,6 @@ Bitwise-exclusive-or. Component-wise when T is a vector. ) .fn(async t => { const type = scalarType(t.params.type); - const cases = makeBitwiseExcluseOrCases(t.params.type); + const cases = makeBitwiseExclusiveOrCases(t.params.type); await run(t, compoundBinary('^='), [type, type], type, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise_shift.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise_shift.spec.ts index 5457b7ceab..e2ed29d3c2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise_shift.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise_shift.spec.ts @@ -4,8 +4,9 @@ Execution Tests for the bitwise shift binary expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { i32, scalarType, ScalarType, TypeU32, u32 } from '../../../../util/conversion.js'; -import { allInputSources, CaseList, run } from '../expression.js'; +import { i32, scalarType, ScalarType, Type, u32 } from '../../../../util/conversion.js'; +import { Case } from '../case.js'; +import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; @@ -65,9 +66,9 @@ function is_valid_const_shift_right(e1: number, e1Type: string, e2: number) { // Returns all cases of shifting e1 left by [0,63]. If `is_const` is true, cases that are // invalid for const eval are not returned. -function generate_shift_left_cases(e1: number, e1Type: string, is_const: boolean): CaseList { +function generate_shift_left_cases(e1: number, e1Type: string, is_const: boolean): Case[] { const V = e1Type === 'i32' ? i32 : u32; - const cases: CaseList = []; + const cases: Case[] = []; for (let shift = 0; shift < 64; ++shift) { const e2 = shift; if (is_const && !is_valid_const_shift_left(e1, e1Type, e2)) { @@ -81,9 +82,9 @@ function generate_shift_left_cases(e1: number, e1Type: string, is_const: boolean // Returns all cases of shifting e1 right by [0,63]. If `is_const` is true, cases that are // invalid for const eval are not returned. -function generate_shift_right_cases(e1: number, e1Type: string, is_const: boolean): CaseList { +function generate_shift_right_cases(e1: number, e1Type: string, is_const: boolean): Case[] { const V = e1Type === 'i32' ? i32 : u32; - const cases: CaseList = []; + const cases: Case[] = []; for (let shift = 0; shift < 64; ++shift) { const e2 = shift; if (is_const && !is_valid_const_shift_right(e1, e1Type, e2)) { @@ -107,7 +108,7 @@ function makeShiftLeftConcreteCases(inputType: string, inputSource: string, type const V = inputType === 'i32' ? i32 : u32; const is_const = inputSource === 'const'; - const cases: CaseList = [ + const cases: Case[] = [ { input: /* */ [V(0b00000000000000000000000000000001), u32(1)], expected: /**/ V(0b00000000000000000000000000000010), @@ -193,7 +194,7 @@ Shift left (shifted value is concrete) .fn(async t => { const type = scalarType(t.params.type); const cases = makeShiftLeftConcreteCases(t.params.type, t.params.inputSource, type); - await run(t, binary('<<'), [type, TypeU32], type, t.params, cases); + await run(t, binary('<<'), [type, Type.u32], type, t.params, cases); }); g.test('shift_left_concrete_compound') @@ -214,14 +215,14 @@ Shift left (shifted value is concrete) .fn(async t => { const type = scalarType(t.params.type); const cases = makeShiftLeftConcreteCases(t.params.type, t.params.inputSource, type); - await run(t, compoundBinary('<<='), [type, TypeU32], type, t.params, cases); + await run(t, compoundBinary('<<='), [type, Type.u32], type, t.params, cases); }); function makeShiftRightConcreteCases(inputType: string, inputSource: string, type: ScalarType) { const V = inputType === 'i32' ? i32 : u32; const is_const = inputSource === 'const'; - const cases: CaseList = [ + const cases: Case[] = [ { input: /* */ [V(0b00000000000000000000000000000001), u32(1)], expected: /**/ V(0b00000000000000000000000000000000), @@ -318,7 +319,7 @@ Shift right (shifted value is concrete) .fn(async t => { const type = scalarType(t.params.type); const cases = makeShiftRightConcreteCases(t.params.type, t.params.inputSource, type); - await run(t, binary('>>'), [type, TypeU32], type, t.params, cases); + await run(t, binary('>>'), [type, Type.u32], type, t.params, cases); }); g.test('shift_right_concrete_compound') @@ -339,5 +340,5 @@ Shift right (shifted value is concrete) .fn(async t => { const type = scalarType(t.params.type); const cases = makeShiftRightConcreteCases(t.params.type, t.params.inputSource, type); - await run(t, compoundBinary('>>='), [type, TypeU32], type, t.params, cases); + await run(t, compoundBinary('>>='), [type, Type.u32], type, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts index e3aa448fe3..0e76f50824 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts @@ -4,7 +4,7 @@ Execution Tests for the boolean binary logical expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { bool, TypeBool } from '../../../../util/conversion.js'; +import { bool, Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; @@ -33,7 +33,7 @@ Logical "and". Component-wise when T is a vector. Evaluates both e1 and e2. { input: [bool(true), bool(true)], expected: bool(true) }, ]; - await run(t, binary('&'), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, binary('&'), [Type.bool, Type.bool], Type.bool, t.params, cases); }); g.test('and_compound') @@ -55,7 +55,7 @@ Logical "and". Component-wise when T is a vector. Evaluates both e1 and e2. { input: [bool(true), bool(true)], expected: bool(true) }, ]; - await run(t, compoundBinary('&='), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, compoundBinary('&='), [Type.bool, Type.bool], Type.bool, t.params, cases); }); g.test('and_short_circuit') @@ -75,7 +75,7 @@ short_circuiting "and". Yields true if both e1 and e2 are true; evaluates e2 onl { input: [bool(true), bool(true)], expected: bool(true) }, ]; - await run(t, binary('&&'), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, binary('&&'), [Type.bool, Type.bool], Type.bool, t.params, cases); }); g.test('or') @@ -97,7 +97,7 @@ Logical "or". Component-wise when T is a vector. Evaluates both e1 and e2. { input: [bool(true), bool(true)], expected: bool(true) }, ]; - await run(t, binary('|'), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, binary('|'), [Type.bool, Type.bool], Type.bool, t.params, cases); }); g.test('or_compound') @@ -119,7 +119,7 @@ Logical "or". Component-wise when T is a vector. Evaluates both e1 and e2. { input: [bool(true), bool(true)], expected: bool(true) }, ]; - await run(t, compoundBinary('|='), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, compoundBinary('|='), [Type.bool, Type.bool], Type.bool, t.params, cases); }); g.test('or_short_circuit') @@ -139,7 +139,7 @@ short_circuiting "and". Yields true if both e1 and e2 are true; evaluates e2 onl { input: [bool(true), bool(true)], expected: bool(true) }, ]; - await run(t, binary('||'), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, binary('||'), [Type.bool, Type.bool], Type.bool, t.params, cases); }); g.test('equals') @@ -161,7 +161,7 @@ Equality. Component-wise when T is a vector. { input: [bool(true), bool(true)], expected: bool(true) }, ]; - await run(t, binary('=='), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, binary('=='), [Type.bool, Type.bool], Type.bool, t.params, cases); }); g.test('not_equals') @@ -183,5 +183,5 @@ Equality. Component-wise when T is a vector. { input: [bool(true), bool(true)], expected: bool(false) }, ]; - await run(t, binary('!='), [TypeBool, TypeBool], TypeBool, t.params, cases); + await run(t, binary('!='), [Type.bool, Type.bool], Type.bool, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_addition.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_addition.cache.ts new file mode 100644 index 0000000000..f179d48a13 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_addition.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF16Range, sparseVectorF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const additionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.additionInterval(e, s))); +}; + +const additionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.additionInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarPairToIntervalCases( + sparseScalarF16Range(), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f16.additionInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateVectorScalarToVectorCases( + sparseVectorF16Range(dim), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + additionVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarVectorToVectorCases( + sparseScalarF16Range(), + sparseVectorF16Range(dim), + nonConst ? 'unfiltered' : 'finite', + additionScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_addition', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_addition.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_addition.spec.ts index 8948f90499..d9aa44bba0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_addition.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_addition.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f16 addition expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF16Range, sparseVectorF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const additionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.additionInterval(e, s))); -}; - -const additionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.additionInterval(s, e))); -}; +import { d } from './f16_addition.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarPairToIntervalCases( - sparseF16Range(), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f16.additionInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorScalarToVectorCases( - sparseVectorF16Range(dim), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - additionVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarVectorToVectorCases( - sparseF16Range(), - sparseVectorF16Range(dim), - nonConst ? 'unfiltered' : 'finite', - additionScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_addition', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -87,7 +28,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('+'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('+'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector') @@ -106,7 +47,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('+'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('+'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('scalar_compound') @@ -127,7 +68,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('+='), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, compoundBinary('+='), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector_scalar') @@ -150,8 +91,8 @@ Accuracy: Correctly rounded await run( t, binary('+'), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -177,8 +118,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('+='), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -204,8 +145,8 @@ Accuracy: Correctly rounded await run( t, binary('+'), - [TypeF16, TypeVec(dim, TypeF16)], - TypeVec(dim, TypeF16), + [Type.f16, Type.vec(dim, Type.f16)], + Type.vec(dim, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_comparison.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_comparison.cache.ts new file mode 100644 index 0000000000..c0c0d4f8a4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_comparison.cache.ts @@ -0,0 +1,144 @@ +import { anyOf } from '../../../../util/compare.js'; +import { bool, f16, ScalarValue } from '../../../../util/conversion.js'; +import { flushSubnormalNumberF16, vectorF16Range } from '../../../../util/math.js'; +import { Case } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +/** + * @returns a test case for the provided left hand & right hand values and truth function. + * Handles quantization and subnormals. + */ +function makeCase( + lhs: number, + rhs: number, + truthFunc: (lhs: ScalarValue, rhs: ScalarValue) => boolean +): Case { + // Subnormal float values may be flushed at any time. + // https://www.w3.org/TR/WGSL/#floating-point-evaluation + const f16_lhs = f16(lhs); + const f16_rhs = f16(rhs); + const lhs_options = new Set([f16_lhs, f16(flushSubnormalNumberF16(lhs))]); + const rhs_options = new Set([f16_rhs, f16(flushSubnormalNumberF16(rhs))]); + const expected: Array<ScalarValue> = []; + lhs_options.forEach(l => { + rhs_options.forEach(r => { + const result = bool(truthFunc(l, r)); + if (!expected.includes(result)) { + expected.push(result); + } + }); + }); + + return { input: [f16_lhs, f16_rhs], expected: anyOf(...expected) }; +} + +export const d = makeCaseCache('binary/f16_logical', { + equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) === (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) === (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + not_equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) !== (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + not_equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) !== (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_than_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) < (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_than_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) < (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) <= (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) <= (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_than_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) > (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_than_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) > (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) >= (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) >= (rhs.value as number); + }; + + return vectorF16Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_comparison.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_comparison.spec.ts index ae7e1675c5..b978cd3c99 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_comparison.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_comparison.spec.ts @@ -4,155 +4,14 @@ Execution Tests for the f16 comparison operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { anyOf } from '../../../../util/compare.js'; -import { bool, f16, Scalar, TypeBool, TypeF16 } from '../../../../util/conversion.js'; -import { flushSubnormalNumberF16, vectorF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, Case, run } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { allInputSources, run } from '../expression.js'; import { binary } from './binary.js'; +import { d } from './f16_comparison.cache.js'; export const g = makeTestGroup(GPUTest); -/** - * @returns a test case for the provided left hand & right hand values and truth function. - * Handles quantization and subnormals. - */ -function makeCase( - lhs: number, - rhs: number, - truthFunc: (lhs: Scalar, rhs: Scalar) => boolean -): Case { - // Subnormal float values may be flushed at any time. - // https://www.w3.org/TR/WGSL/#floating-point-evaluation - const f16_lhs = f16(lhs); - const f16_rhs = f16(rhs); - const lhs_options = new Set([f16_lhs, f16(flushSubnormalNumberF16(lhs))]); - const rhs_options = new Set([f16_rhs, f16(flushSubnormalNumberF16(rhs))]); - const expected: Array<Scalar> = []; - lhs_options.forEach(l => { - rhs_options.forEach(r => { - const result = bool(truthFunc(l, r)); - if (!expected.includes(result)) { - expected.push(result); - } - }); - }); - - return { input: [f16_lhs, f16_rhs], expected: anyOf(...expected) }; -} - -export const d = makeCaseCache('binary/f16_logical', { - equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) === (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) === (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - not_equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) !== (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - not_equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) !== (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_than_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) < (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_than_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) < (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) <= (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) <= (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_than_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) > (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_than_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) > (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) >= (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) >= (rhs.value as number); - }; - - return vectorF16Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, -}); - g.test('equals') .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') .desc( @@ -171,7 +30,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'equals_const' : 'equals_non_const' ); - await run(t, binary('=='), [TypeF16, TypeF16], TypeBool, t.params, cases); + await run(t, binary('=='), [Type.f16, Type.f16], Type.bool, t.params, cases); }); g.test('not_equals') @@ -192,7 +51,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'not_equals_const' : 'not_equals_non_const' ); - await run(t, binary('!='), [TypeF16, TypeF16], TypeBool, t.params, cases); + await run(t, binary('!='), [Type.f16, Type.f16], Type.bool, t.params, cases); }); g.test('less_than') @@ -213,7 +72,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'less_than_const' : 'less_than_non_const' ); - await run(t, binary('<'), [TypeF16, TypeF16], TypeBool, t.params, cases); + await run(t, binary('<'), [Type.f16, Type.f16], Type.bool, t.params, cases); }); g.test('less_equals') @@ -234,7 +93,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'less_equals_const' : 'less_equals_non_const' ); - await run(t, binary('<='), [TypeF16, TypeF16], TypeBool, t.params, cases); + await run(t, binary('<='), [Type.f16, Type.f16], Type.bool, t.params, cases); }); g.test('greater_than') @@ -255,7 +114,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'greater_than_const' : 'greater_than_non_const' ); - await run(t, binary('>'), [TypeF16, TypeF16], TypeBool, t.params, cases); + await run(t, binary('>'), [Type.f16, Type.f16], Type.bool, t.params, cases); }); g.test('greater_equals') @@ -276,5 +135,5 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'greater_equals_const' : 'greater_equals_non_const' ); - await run(t, binary('>='), [TypeF16, TypeF16], TypeBool, t.params, cases); + await run(t, binary('>='), [Type.f16, Type.f16], Type.bool, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_division.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_division.cache.ts new file mode 100644 index 0000000000..95590ca467 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_division.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF16Range, sparseVectorF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const divisionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.divisionInterval(e, s))); +}; + +const divisionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.divisionInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarPairToIntervalCases( + sparseScalarF16Range(), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f16.divisionInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateVectorScalarToVectorCases( + sparseVectorF16Range(dim), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + divisionVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarVectorToVectorCases( + sparseScalarF16Range(), + sparseVectorF16Range(dim), + nonConst ? 'unfiltered' : 'finite', + divisionScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_division', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_division.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_division.spec.ts index c3b8fc04db..8a155024db 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_division.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_division.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f16 division expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF16Range, sparseVectorF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const divisionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.divisionInterval(e, s))); -}; - -const divisionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.divisionInterval(s, e))); -}; +import { d } from './f16_division.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarPairToIntervalCases( - sparseF16Range(), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f16.divisionInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorScalarToVectorCases( - sparseVectorF16Range(dim), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - divisionVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarVectorToVectorCases( - sparseF16Range(), - sparseVectorF16Range(dim), - nonConst ? 'unfiltered' : 'finite', - divisionScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_division', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -87,7 +28,7 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('/'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('/'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector') @@ -106,7 +47,7 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('/'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('/'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('scalar_compound') @@ -127,7 +68,7 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('/='), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, compoundBinary('/='), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector_scalar') @@ -150,8 +91,8 @@ Accuracy: Correctly rounded await run( t, binary('/'), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -177,8 +118,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('/='), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -204,8 +145,8 @@ Accuracy: Correctly rounded await run( t, binary('/'), - [TypeF16, TypeVec(dim, TypeF16)], - TypeVec(dim, TypeF16), + [Type.f16, Type.vec(dim, Type.f16)], + Type.vec(dim, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_addition.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_addition.cache.ts new file mode 100644 index 0000000000..a670b08b07 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_addition.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_[non_]const +const mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateMatrixPairToMatrixCases( + sparseMatrixF16Range(cols, rows), + sparseMatrixF16Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f16.additionMatrixMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_matrix_addition', mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_addition.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_addition.spec.ts index fe64f41503..7c34b0cadd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_addition.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_addition.spec.ts @@ -4,36 +4,14 @@ Execution Tests for matrix f16 addition expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f16_matrix_addition.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_[non_]const -const mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateMatrixPairToMatrixCases( - sparseMatrixF16Range(cols, rows), - sparseMatrixF16Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f16.additionMatrixMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_matrix_addition', mat_cases); - g.test('matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -60,8 +38,8 @@ Accuracy: Correctly rounded await run( t, binary('+'), - [TypeMat(cols, rows, TypeF16), TypeMat(cols, rows, TypeF16)], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f16), Type.mat(cols, rows, Type.f16)], + Type.mat(cols, rows, Type.f16), t.params, cases ); @@ -93,8 +71,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('+='), - [TypeMat(cols, rows, TypeF16), TypeMat(cols, rows, TypeF16)], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f16), Type.mat(cols, rows, Type.f16)], + Type.mat(cols, rows, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_matrix_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_matrix_multiplication.cache.ts new file mode 100644 index 0000000000..a31813abb3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_matrix_multiplication.cache.ts @@ -0,0 +1,25 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matKxR_matCxK_[non_]const +const mat_mat_cases = ([2, 3, 4] as const) + .flatMap(k => + ([2, 3, 4] as const).flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${k}x${rows}_mat${cols}x${k}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateMatrixPairToMatrixCases( + sparseMatrixF16Range(k, rows), + sparseMatrixF16Range(cols, k), + nonConst ? 'unfiltered' : 'finite', + FP.f16.multiplicationMatrixMatrixInterval + ); + }, + })) + ) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_matrix_matrix_multiplication', mat_mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_matrix_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_matrix_multiplication.spec.ts index 0c8b3e8c51..80ca78f7f5 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_matrix_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_matrix_multiplication.spec.ts @@ -4,38 +4,14 @@ Execution Tests for matrix-matrix f16 multiplication expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f16_matrix_matrix_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matKxR_matCxK_[non_]const -const mat_mat_cases = ([2, 3, 4] as const) - .flatMap(k => - ([2, 3, 4] as const).flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${k}x${rows}_mat${cols}x${k}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateMatrixPairToMatrixCases( - sparseMatrixF16Range(k, rows), - sparseMatrixF16Range(cols, k), - nonConst ? 'unfiltered' : 'finite', - FP.f16.multiplicationMatrixMatrixInterval - ); - }, - })) - ) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_matrix_matrix_multiplication', mat_mat_cases); - g.test('matrix_matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -68,8 +44,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeMat(x_cols, x_rows, TypeF16), TypeMat(y_cols, y_rows, TypeF16)], - TypeMat(y_cols, x_rows, TypeF16), + [Type.mat(x_cols, x_rows, Type.f16), Type.mat(y_cols, y_rows, Type.f16)], + Type.mat(y_cols, x_rows, Type.f16), t.params, cases ); @@ -106,8 +82,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeMat(x_cols, x_rows, TypeF16), TypeMat(y_cols, y_rows, TypeF16)], - TypeMat(y_cols, x_rows, TypeF16), + [Type.mat(x_cols, x_rows, Type.f16), Type.mat(y_cols, y_rows, Type.f16)], + Type.mat(y_cols, x_rows, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_scalar_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_scalar_multiplication.cache.ts new file mode 100644 index 0000000000..f902a1c8bc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_scalar_multiplication.cache.ts @@ -0,0 +1,44 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF16Range, sparseScalarF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_scalar_[non_]const +const mat_scalar_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateMatrixScalarToMatrixCases( + sparseMatrixF16Range(cols, rows), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f16.multiplicationMatrixScalarInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: scalar_matCxR_[non_]const +const scalar_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`scalar_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarMatrixToMatrixCases( + sparseScalarF16Range(), + sparseMatrixF16Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f16.multiplicationScalarMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_matrix_scalar_multiplication', { + ...mat_scalar_cases, + ...scalar_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_scalar_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_scalar_multiplication.spec.ts index 29d4700ee6..aa7087738a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_scalar_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_scalar_multiplication.spec.ts @@ -4,57 +4,14 @@ Execution Tests for matrix-scalar and scalar-matrix f16 multiplication expressio import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseF16Range, sparseMatrixF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f16_matrix_scalar_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_scalar_[non_]const -const mat_scalar_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateMatrixScalarToMatrixCases( - sparseMatrixF16Range(cols, rows), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f16.multiplicationMatrixScalarInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: scalar_matCxR_[non_]const -const scalar_mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`scalar_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarMatrixToMatrixCases( - sparseF16Range(), - sparseMatrixF16Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f16.multiplicationScalarMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_matrix_scalar_multiplication', { - ...mat_scalar_cases, - ...scalar_mat_cases, -}); - g.test('matrix_scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -83,8 +40,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeMat(cols, rows, TypeF16), TypeF16], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f16), Type.f16], + Type.mat(cols, rows, Type.f16), t.params, cases ); @@ -118,8 +75,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeMat(cols, rows, TypeF16), TypeF16], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f16), Type.f16], + Type.mat(cols, rows, Type.f16), t.params, cases ); @@ -153,8 +110,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeF16, TypeMat(cols, rows, TypeF16)], - TypeMat(cols, rows, TypeF16), + [Type.f16, Type.mat(cols, rows, Type.f16)], + Type.mat(cols, rows, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_subtraction.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_subtraction.cache.ts new file mode 100644 index 0000000000..a6edcf7fe5 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_subtraction.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_[non_]const +const mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateMatrixPairToMatrixCases( + sparseMatrixF16Range(cols, rows), + sparseMatrixF16Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f16.subtractionMatrixMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_matrix_subtraction', mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_subtraction.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_subtraction.spec.ts index 5b5f6ba04e..e8e13d902a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_subtraction.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_subtraction.spec.ts @@ -4,36 +4,14 @@ Execution Tests for matrix f16 subtraction expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f16_matrix_subtraction.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_[non_]const -const mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateMatrixPairToMatrixCases( - sparseMatrixF16Range(cols, rows), - sparseMatrixF16Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f16.subtractionMatrixMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_matrix_subtraction', mat_cases); - g.test('matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -60,8 +38,8 @@ Accuracy: Correctly rounded await run( t, binary('-'), - [TypeMat(cols, rows, TypeF16), TypeMat(cols, rows, TypeF16)], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f16), Type.mat(cols, rows, Type.f16)], + Type.mat(cols, rows, Type.f16), t.params, cases ); @@ -93,8 +71,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('-='), - [TypeMat(cols, rows, TypeF16), TypeMat(cols, rows, TypeF16)], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f16), Type.mat(cols, rows, Type.f16)], + Type.mat(cols, rows, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_vector_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_vector_multiplication.cache.ts new file mode 100644 index 0000000000..7b822386fe --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_vector_multiplication.cache.ts @@ -0,0 +1,44 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF16Range, sparseVectorF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_vecC_[non_]const +const mat_vec_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_vec${cols}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateMatrixVectorToVectorCases( + sparseMatrixF16Range(cols, rows), + sparseVectorF16Range(cols), + nonConst ? 'unfiltered' : 'finite', + FP.f16.multiplicationMatrixVectorInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: vecR_matCxR_[non_]const +const vec_mat_cases = ([2, 3, 4] as const) + .flatMap(rows => + ([2, 3, 4] as const).flatMap(cols => + ([true, false] as const).map(nonConst => ({ + [`vec${rows}_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateVectorMatrixToVectorCases( + sparseVectorF16Range(rows), + sparseMatrixF16Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f16.multiplicationVectorMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_matrix_vector_multiplication', { + ...mat_vec_cases, + ...vec_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_vector_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_vector_multiplication.spec.ts index 3e916c7fd4..557a7cead8 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_vector_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_matrix_vector_multiplication.spec.ts @@ -4,57 +4,14 @@ Execution Tests for matrix-vector and vector-matrix f16 multiplication expressio import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeMat, TypeVec } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF16Range, sparseVectorF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f16_matrix_vector_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_vecC_[non_]const -const mat_vec_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_vec${cols}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateMatrixVectorToVectorCases( - sparseMatrixF16Range(cols, rows), - sparseVectorF16Range(cols), - nonConst ? 'unfiltered' : 'finite', - FP.f16.multiplicationMatrixVectorInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: vecR_matCxR_[non_]const -const vec_mat_cases = ([2, 3, 4] as const) - .flatMap(rows => - ([2, 3, 4] as const).flatMap(cols => - ([true, false] as const).map(nonConst => ({ - [`vec${rows}_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorMatrixToVectorCases( - sparseVectorF16Range(rows), - sparseMatrixF16Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f16.multiplicationVectorMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_matrix_vector_multiplication', { - ...mat_vec_cases, - ...vec_mat_cases, -}); - g.test('matrix_vector') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -83,8 +40,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeMat(cols, rows, TypeF16), TypeVec(cols, TypeF16)], - TypeVec(rows, TypeF16), + [Type.mat(cols, rows, Type.f16), Type.vec(cols, Type.f16)], + Type.vec(rows, Type.f16), t.params, cases ); @@ -118,8 +75,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeVec(rows, TypeF16), TypeMat(cols, rows, TypeF16)], - TypeVec(cols, TypeF16), + [Type.vec(rows, Type.f16), Type.mat(cols, rows, Type.f16)], + Type.vec(cols, Type.f16), t.params, cases ); @@ -148,8 +105,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeVec(rows, TypeF16), TypeMat(cols, rows, TypeF16)], - TypeVec(cols, TypeF16), + [Type.vec(rows, Type.f16), Type.mat(cols, rows, Type.f16)], + Type.vec(cols, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_multiplication.cache.ts new file mode 100644 index 0000000000..a94f55ccf0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_multiplication.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF16Range, sparseVectorF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const multiplicationVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.multiplicationInterval(e, s))); +}; + +const multiplicationScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.multiplicationInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarPairToIntervalCases( + sparseScalarF16Range(), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f16.multiplicationInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateVectorScalarToVectorCases( + sparseVectorF16Range(dim), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + multiplicationVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarVectorToVectorCases( + sparseScalarF16Range(), + sparseVectorF16Range(dim), + nonConst ? 'unfiltered' : 'finite', + multiplicationScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_multiplication', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_multiplication.spec.ts index 10041fbc17..81339d9266 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_multiplication.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f16 multiplication expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF16Range, sparseVectorF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const multiplicationVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.multiplicationInterval(e, s))); -}; - -const multiplicationScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.multiplicationInterval(s, e))); -}; +import { d } from './f16_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarPairToIntervalCases( - sparseF16Range(), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f16.multiplicationInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorScalarToVectorCases( - sparseVectorF16Range(dim), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - multiplicationVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarVectorToVectorCases( - sparseF16Range(), - sparseVectorF16Range(dim), - nonConst ? 'unfiltered' : 'finite', - multiplicationScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_multiplication', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -87,7 +28,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('*'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('*'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector') @@ -106,7 +47,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('*'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('*'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('scalar_compound') @@ -127,7 +68,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('*='), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, compoundBinary('*='), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector_scalar') @@ -150,8 +91,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -177,8 +118,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -204,8 +145,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeF16, TypeVec(dim, TypeF16)], - TypeVec(dim, TypeF16), + [Type.f16, Type.vec(dim, Type.f16)], + Type.vec(dim, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_remainder.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_remainder.cache.ts new file mode 100644 index 0000000000..2c1cdc0c38 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_remainder.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF16Range, sparseVectorF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const remainderVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.remainderInterval(e, s))); +}; + +const remainderScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.remainderInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarPairToIntervalCases( + sparseScalarF16Range(), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f16.remainderInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateVectorScalarToVectorCases( + sparseVectorF16Range(dim), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + remainderVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarVectorToVectorCases( + sparseScalarF16Range(), + sparseVectorF16Range(dim), + nonConst ? 'unfiltered' : 'finite', + remainderScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_remainder', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_remainder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_remainder.spec.ts index 801b84904b..0fe1cc53c6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_remainder.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_remainder.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f16 remainder expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF16Range, sparseVectorF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const remainderVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.remainderInterval(e, s))); -}; - -const remainderScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.remainderInterval(s, e))); -}; +import { d } from './f16_remainder.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarPairToIntervalCases( - sparseF16Range(), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f16.remainderInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorScalarToVectorCases( - sparseVectorF16Range(dim), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - remainderVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarVectorToVectorCases( - sparseF16Range(), - sparseVectorF16Range(dim), - nonConst ? 'unfiltered' : 'finite', - remainderScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_remainder', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -87,7 +28,7 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('%'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('%'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector') @@ -106,7 +47,7 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('%'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('%'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('scalar_compound') @@ -127,7 +68,7 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('%='), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, compoundBinary('%='), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector_scalar') @@ -150,8 +91,8 @@ Accuracy: Correctly rounded await run( t, binary('%'), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -177,8 +118,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('%='), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -204,8 +145,8 @@ Accuracy: Correctly rounded await run( t, binary('%'), - [TypeF16, TypeVec(dim, TypeF16)], - TypeVec(dim, TypeF16), + [Type.f16, Type.vec(dim, Type.f16)], + Type.vec(dim, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_subtraction.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_subtraction.cache.ts new file mode 100644 index 0000000000..a68b58449b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_subtraction.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF16Range, sparseVectorF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const subtractionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.subtractionInterval(e, s))); +}; + +const subtractionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f16.toVector(v.map(e => FP.f16.subtractionInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarPairToIntervalCases( + sparseScalarF16Range(), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f16.subtractionInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateVectorScalarToVectorCases( + sparseVectorF16Range(dim), + sparseScalarF16Range(), + nonConst ? 'unfiltered' : 'finite', + subtractionVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateScalarVectorToVectorCases( + sparseScalarF16Range(), + sparseVectorF16Range(dim), + nonConst ? 'unfiltered' : 'finite', + subtractionScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f16_subtraction', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_subtraction.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_subtraction.spec.ts index a64d556837..6b29aad6ad 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_subtraction.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f16_subtraction.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f16 subtraction expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF16Range, sparseVectorF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const subtractionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.subtractionInterval(e, s))); -}; - -const subtractionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f16.toVector(v.map(e => FP.f16.subtractionInterval(s, e))); -}; +import { d } from './f16_subtraction.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarPairToIntervalCases( - sparseF16Range(), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f16.subtractionInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorScalarToVectorCases( - sparseVectorF16Range(dim), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - subtractionVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateScalarVectorToVectorCases( - sparseF16Range(), - sparseVectorF16Range(dim), - nonConst ? 'unfiltered' : 'finite', - subtractionScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f16_subtraction', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -87,7 +28,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('-'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('-'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector') @@ -106,7 +47,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('-'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, binary('-'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('scalar_compound') @@ -127,7 +68,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('-='), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, compoundBinary('-='), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('vector_scalar') @@ -150,8 +91,8 @@ Accuracy: Correctly rounded await run( t, binary('-'), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -177,8 +118,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('-='), - [TypeVec(dim, TypeF16), TypeF16], - TypeVec(dim, TypeF16), + [Type.vec(dim, Type.f16), Type.f16], + Type.vec(dim, Type.f16), t.params, cases ); @@ -204,8 +145,8 @@ Accuracy: Correctly rounded await run( t, binary('-'), - [TypeF16, TypeVec(dim, TypeF16)], - TypeVec(dim, TypeF16), + [Type.f16, Type.vec(dim, Type.f16)], + Type.vec(dim, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_addition.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_addition.cache.ts new file mode 100644 index 0000000000..9353671fb0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_addition.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF32Range, sparseVectorF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const additionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.additionInterval(e, s))); +}; + +const additionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.additionInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarPairToIntervalCases( + sparseScalarF32Range(), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f32.additionInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateVectorScalarToVectorCases( + sparseVectorF32Range(dim), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + additionVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarVectorToVectorCases( + sparseScalarF32Range(), + sparseVectorF32Range(dim), + nonConst ? 'unfiltered' : 'finite', + additionScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_addition', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_addition.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_addition.spec.ts index 65739f67ca..9a502ae677 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_addition.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_addition.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f32 addition expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF32Range, sparseVectorF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const additionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.additionInterval(e, s))); -}; - -const additionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.additionInterval(s, e))); -}; +import { d } from './f32_addition.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarPairToIntervalCases( - sparseF32Range(), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f32.additionInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorScalarToVectorCases( - sparseVectorF32Range(dim), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - additionVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarVectorToVectorCases( - sparseF32Range(), - sparseVectorF32Range(dim), - nonConst ? 'unfiltered' : 'finite', - additionScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_addition', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -84,7 +25,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('+'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('+'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector') @@ -100,7 +41,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('+'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('+'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('scalar_compound') @@ -118,7 +59,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('+='), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, compoundBinary('+='), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector_scalar') @@ -138,8 +79,8 @@ Accuracy: Correctly rounded await run( t, binary('+'), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -162,8 +103,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('+='), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -186,8 +127,8 @@ Accuracy: Correctly rounded await run( t, binary('+'), - [TypeF32, TypeVec(dim, TypeF32)], - TypeVec(dim, TypeF32), + [Type.f32, Type.vec(dim, Type.f32)], + Type.vec(dim, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_comparison.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_comparison.cache.ts new file mode 100644 index 0000000000..28fb22e820 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_comparison.cache.ts @@ -0,0 +1,144 @@ +import { anyOf } from '../../../../util/compare.js'; +import { bool, f32, ScalarValue } from '../../../../util/conversion.js'; +import { flushSubnormalNumberF32, vectorF32Range } from '../../../../util/math.js'; +import { Case } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +/** + * @returns a test case for the provided left hand & right hand values and truth function. + * Handles quantization and subnormals. + */ +function makeCase( + lhs: number, + rhs: number, + truthFunc: (lhs: ScalarValue, rhs: ScalarValue) => boolean +): Case { + // Subnormal float values may be flushed at any time. + // https://www.w3.org/TR/WGSL/#floating-point-evaluation + const f32_lhs = f32(lhs); + const f32_rhs = f32(rhs); + const lhs_options = new Set([f32_lhs, f32(flushSubnormalNumberF32(lhs))]); + const rhs_options = new Set([f32_rhs, f32(flushSubnormalNumberF32(rhs))]); + const expected: Array<ScalarValue> = []; + lhs_options.forEach(l => { + rhs_options.forEach(r => { + const result = bool(truthFunc(l, r)); + if (!expected.includes(result)) { + expected.push(result); + } + }); + }); + + return { input: [f32_lhs, f32_rhs], expected: anyOf(...expected) }; +} + +export const d = makeCaseCache('binary/f32_logical', { + equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) === (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) === (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + not_equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) !== (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + not_equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) !== (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_than_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) < (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_than_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) < (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) <= (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + less_equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) <= (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_than_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) > (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_than_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) > (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_equals_non_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) >= (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, + greater_equals_const: () => { + const truthFunc = (lhs: ScalarValue, rhs: ScalarValue): boolean => { + return (lhs.value as number) >= (rhs.value as number); + }; + + return vectorF32Range(2).map(v => { + return makeCase(v[0], v[1], truthFunc); + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_comparison.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_comparison.spec.ts index ef862e7757..42eb8934a4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_comparison.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_comparison.spec.ts @@ -4,155 +4,14 @@ Execution Tests for the f32 comparison operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { anyOf } from '../../../../util/compare.js'; -import { bool, f32, Scalar, TypeBool, TypeF32 } from '../../../../util/conversion.js'; -import { flushSubnormalNumberF32, vectorF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, Case, run } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { allInputSources, run } from '../expression.js'; import { binary } from './binary.js'; +import { d } from './f32_comparison.cache.js'; export const g = makeTestGroup(GPUTest); -/** - * @returns a test case for the provided left hand & right hand values and truth function. - * Handles quantization and subnormals. - */ -function makeCase( - lhs: number, - rhs: number, - truthFunc: (lhs: Scalar, rhs: Scalar) => boolean -): Case { - // Subnormal float values may be flushed at any time. - // https://www.w3.org/TR/WGSL/#floating-point-evaluation - const f32_lhs = f32(lhs); - const f32_rhs = f32(rhs); - const lhs_options = new Set([f32_lhs, f32(flushSubnormalNumberF32(lhs))]); - const rhs_options = new Set([f32_rhs, f32(flushSubnormalNumberF32(rhs))]); - const expected: Array<Scalar> = []; - lhs_options.forEach(l => { - rhs_options.forEach(r => { - const result = bool(truthFunc(l, r)); - if (!expected.includes(result)) { - expected.push(result); - } - }); - }); - - return { input: [f32_lhs, f32_rhs], expected: anyOf(...expected) }; -} - -export const d = makeCaseCache('binary/f32_logical', { - equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) === (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) === (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - not_equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) !== (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - not_equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) !== (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_than_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) < (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_than_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) < (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) <= (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - less_equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) <= (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_than_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) > (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_than_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) > (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_equals_non_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) >= (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, - greater_equals_const: () => { - const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => { - return (lhs.value as number) >= (rhs.value as number); - }; - - return vectorF32Range(2).map(v => { - return makeCase(v[0], v[1], truthFunc); - }); - }, -}); - g.test('equals') .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') .desc( @@ -168,7 +27,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'equals_const' : 'equals_non_const' ); - await run(t, binary('=='), [TypeF32, TypeF32], TypeBool, t.params, cases); + await run(t, binary('=='), [Type.f32, Type.f32], Type.bool, t.params, cases); }); g.test('not_equals') @@ -186,7 +45,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'not_equals_const' : 'not_equals_non_const' ); - await run(t, binary('!='), [TypeF32, TypeF32], TypeBool, t.params, cases); + await run(t, binary('!='), [Type.f32, Type.f32], Type.bool, t.params, cases); }); g.test('less_than') @@ -204,7 +63,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'less_than_const' : 'less_than_non_const' ); - await run(t, binary('<'), [TypeF32, TypeF32], TypeBool, t.params, cases); + await run(t, binary('<'), [Type.f32, Type.f32], Type.bool, t.params, cases); }); g.test('less_equals') @@ -222,7 +81,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'less_equals_const' : 'less_equals_non_const' ); - await run(t, binary('<='), [TypeF32, TypeF32], TypeBool, t.params, cases); + await run(t, binary('<='), [Type.f32, Type.f32], Type.bool, t.params, cases); }); g.test('greater_than') @@ -240,7 +99,7 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'greater_than_const' : 'greater_than_non_const' ); - await run(t, binary('>'), [TypeF32, TypeF32], TypeBool, t.params, cases); + await run(t, binary('>'), [Type.f32, Type.f32], Type.bool, t.params, cases); }); g.test('greater_equals') @@ -258,5 +117,5 @@ Accuracy: Correct result const cases = await d.get( t.params.inputSource === 'const' ? 'greater_equals_const' : 'greater_equals_non_const' ); - await run(t, binary('>='), [TypeF32, TypeF32], TypeBool, t.params, cases); + await run(t, binary('>='), [Type.f32, Type.f32], Type.bool, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_division.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_division.cache.ts new file mode 100644 index 0000000000..017f7a451a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_division.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF32Range, sparseVectorF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const divisionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.divisionInterval(e, s))); +}; + +const divisionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.divisionInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarPairToIntervalCases( + sparseScalarF32Range(), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f32.divisionInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateVectorScalarToVectorCases( + sparseVectorF32Range(dim), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + divisionVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarVectorToVectorCases( + sparseScalarF32Range(), + sparseVectorF32Range(dim), + nonConst ? 'unfiltered' : 'finite', + divisionScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_division', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_division.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_division.spec.ts index bd3793bf8a..bdd71e69eb 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_division.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_division.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f32 division expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF32Range, sparseVectorF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const divisionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.divisionInterval(e, s))); -}; - -const divisionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.divisionInterval(s, e))); -}; +import { d } from './f32_division.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarPairToIntervalCases( - sparseF32Range(), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f32.divisionInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorScalarToVectorCases( - sparseVectorF32Range(dim), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - divisionVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarVectorToVectorCases( - sparseF32Range(), - sparseVectorF32Range(dim), - nonConst ? 'unfiltered' : 'finite', - divisionScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_division', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -84,7 +25,7 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('/'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('/'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector') @@ -100,7 +41,7 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('/'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('/'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('scalar_compound') @@ -118,7 +59,7 @@ Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('/='), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, compoundBinary('/='), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector_scalar') @@ -138,8 +79,8 @@ Accuracy: Correctly rounded await run( t, binary('/'), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -162,8 +103,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('/='), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -186,8 +127,8 @@ Accuracy: Correctly rounded await run( t, binary('/'), - [TypeF32, TypeVec(dim, TypeF32)], - TypeVec(dim, TypeF32), + [Type.f32, Type.vec(dim, Type.f32)], + Type.vec(dim, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_addition.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_addition.cache.ts new file mode 100644 index 0000000000..0f3ced975d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_addition.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_[non_]const +const mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixPairToMatrixCases( + sparseMatrixF32Range(cols, rows), + sparseMatrixF32Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f32.additionMatrixMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_matrix_addition', mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_addition.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_addition.spec.ts index 9f11c3cac1..06a4ac47cc 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_addition.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_addition.spec.ts @@ -4,36 +4,14 @@ Execution Tests for matrix f32 addition expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f32_matrix_addition.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_[non_]const -const mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixPairToMatrixCases( - sparseMatrixF32Range(cols, rows), - sparseMatrixF32Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f32.additionMatrixMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_matrix_addition', mat_cases); - g.test('matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -57,8 +35,8 @@ Accuracy: Correctly rounded await run( t, binary('+'), - [TypeMat(cols, rows, TypeF32), TypeMat(cols, rows, TypeF32)], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f32), Type.mat(cols, rows, Type.f32)], + Type.mat(cols, rows, Type.f32), t.params, cases ); @@ -87,8 +65,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('+='), - [TypeMat(cols, rows, TypeF32), TypeMat(cols, rows, TypeF32)], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f32), Type.mat(cols, rows, Type.f32)], + Type.mat(cols, rows, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_matrix_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_matrix_multiplication.cache.ts new file mode 100644 index 0000000000..cde2d74fdf --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_matrix_multiplication.cache.ts @@ -0,0 +1,25 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matKxR_matCxK_[non_]const +const mat_mat_cases = ([2, 3, 4] as const) + .flatMap(k => + ([2, 3, 4] as const).flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${k}x${rows}_mat${cols}x${k}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixPairToMatrixCases( + sparseMatrixF32Range(k, rows), + sparseMatrixF32Range(cols, k), + nonConst ? 'unfiltered' : 'finite', + FP.f32.multiplicationMatrixMatrixInterval + ); + }, + })) + ) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_matrix_matrix_multiplication', mat_mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_matrix_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_matrix_multiplication.spec.ts index 2c48eab187..1ff7799f46 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_matrix_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_matrix_multiplication.spec.ts @@ -4,38 +4,14 @@ Execution Tests for matrix-matrix f32 multiplication expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f32_matrix_matrix_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matKxR_matCxK_[non_]const -const mat_mat_cases = ([2, 3, 4] as const) - .flatMap(k => - ([2, 3, 4] as const).flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${k}x${rows}_mat${cols}x${k}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixPairToMatrixCases( - sparseMatrixF32Range(k, rows), - sparseMatrixF32Range(cols, k), - nonConst ? 'unfiltered' : 'finite', - FP.f32.multiplicationMatrixMatrixInterval - ); - }, - })) - ) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_matrix_matrix_multiplication', mat_mat_cases); - g.test('matrix_matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -65,8 +41,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeMat(x_cols, x_rows, TypeF32), TypeMat(y_cols, y_rows, TypeF32)], - TypeMat(y_cols, x_rows, TypeF32), + [Type.mat(x_cols, x_rows, Type.f32), Type.mat(y_cols, y_rows, Type.f32)], + Type.mat(y_cols, x_rows, Type.f32), t.params, cases ); @@ -100,8 +76,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeMat(x_cols, x_rows, TypeF32), TypeMat(y_cols, y_rows, TypeF32)], - TypeMat(y_cols, x_rows, TypeF32), + [Type.mat(x_cols, x_rows, Type.f32), Type.mat(y_cols, y_rows, Type.f32)], + Type.mat(y_cols, x_rows, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_scalar_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_scalar_multiplication.cache.ts new file mode 100644 index 0000000000..f17dac31e6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_scalar_multiplication.cache.ts @@ -0,0 +1,44 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF32Range, sparseScalarF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_scalar_[non_]const +const mat_scalar_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixScalarToMatrixCases( + sparseMatrixF32Range(cols, rows), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f32.multiplicationMatrixScalarInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: scalar_matCxR_[non_]const +const scalar_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`scalar_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarMatrixToMatrixCases( + sparseScalarF32Range(), + sparseMatrixF32Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f32.multiplicationScalarMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_matrix_scalar_multiplication', { + ...mat_scalar_cases, + ...scalar_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_scalar_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_scalar_multiplication.spec.ts index f3d36b8382..e8771d19eb 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_scalar_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_scalar_multiplication.spec.ts @@ -4,57 +4,14 @@ Execution Tests for matrix-scalar and scalar-matrix f32 multiplication expressio import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseF32Range, sparseMatrixF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f32_matrix_scalar_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_scalar_[non_]const -const mat_scalar_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixScalarToMatrixCases( - sparseMatrixF32Range(cols, rows), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f32.multiplicationMatrixScalarInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: scalar_matCxR_[non_]const -const scalar_mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`scalar_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarMatrixToMatrixCases( - sparseF32Range(), - sparseMatrixF32Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f32.multiplicationScalarMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_matrix_scalar_multiplication', { - ...mat_scalar_cases, - ...scalar_mat_cases, -}); - g.test('matrix_scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -80,8 +37,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeMat(cols, rows, TypeF32), TypeF32], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f32), Type.f32], + Type.mat(cols, rows, Type.f32), t.params, cases ); @@ -112,8 +69,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeMat(cols, rows, TypeF32), TypeF32], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f32), Type.f32], + Type.mat(cols, rows, Type.f32), t.params, cases ); @@ -144,8 +101,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeF32, TypeMat(cols, rows, TypeF32)], - TypeMat(cols, rows, TypeF32), + [Type.f32, Type.mat(cols, rows, Type.f32)], + Type.mat(cols, rows, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_subtraction.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_subtraction.cache.ts new file mode 100644 index 0000000000..2cd2bc1c6d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_subtraction.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_[non_]const +const mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixPairToMatrixCases( + sparseMatrixF32Range(cols, rows), + sparseMatrixF32Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f32.subtractionMatrixMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_matrix_subtraction', mat_cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_subtraction.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_subtraction.spec.ts index 5f101d9b27..31565ba598 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_subtraction.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_subtraction.spec.ts @@ -4,36 +4,14 @@ Execution Tests for matrix f32 subtraction expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeMat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f32_matrix_subtraction.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_[non_]const -const mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixPairToMatrixCases( - sparseMatrixF32Range(cols, rows), - sparseMatrixF32Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f32.subtractionMatrixMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_matrix_subtraction', mat_cases); - g.test('matrix') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -57,8 +35,8 @@ Accuracy: Correctly rounded await run( t, binary('-'), - [TypeMat(cols, rows, TypeF32), TypeMat(cols, rows, TypeF32)], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f32), Type.mat(cols, rows, Type.f32)], + Type.mat(cols, rows, Type.f32), t.params, cases ); @@ -87,8 +65,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('-='), - [TypeMat(cols, rows, TypeF32), TypeMat(cols, rows, TypeF32)], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f32), Type.mat(cols, rows, Type.f32)], + Type.mat(cols, rows, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_vector_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_vector_multiplication.cache.ts new file mode 100644 index 0000000000..f20b95029e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_vector_multiplication.cache.ts @@ -0,0 +1,44 @@ +import { FP } from '../../../../util/floating_point.js'; +import { sparseMatrixF32Range, sparseVectorF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: matCxR_vecC_[non_]const +const mat_vec_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`mat${cols}x${rows}_vec${cols}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixVectorToVectorCases( + sparseMatrixF32Range(cols, rows), + sparseVectorF32Range(cols), + nonConst ? 'unfiltered' : 'finite', + FP.f32.multiplicationMatrixVectorInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: vecR_matCxR_[non_]const +const vec_mat_cases = ([2, 3, 4] as const) + .flatMap(rows => + ([2, 3, 4] as const).flatMap(cols => + ([true, false] as const).map(nonConst => ({ + [`vec${rows}_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateVectorMatrixToVectorCases( + sparseVectorF32Range(rows), + sparseMatrixF32Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f32.multiplicationVectorMatrixInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_matrix_vector_multiplication', { + ...mat_vec_cases, + ...vec_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_vector_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_vector_multiplication.spec.ts index e6cdf16d92..8cd7ed49fe 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_vector_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_matrix_vector_multiplication.spec.ts @@ -4,57 +4,14 @@ Execution Tests for matrix-vector and vector-matrix f32 multiplication expressio import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeMat, TypeVec } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { sparseMatrixF32Range, sparseVectorF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; +import { d } from './f32_matrix_vector_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: matCxR_vecC_[non_]const -const mat_vec_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`mat${cols}x${rows}_vec${cols}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixVectorToVectorCases( - sparseMatrixF32Range(cols, rows), - sparseVectorF32Range(cols), - nonConst ? 'unfiltered' : 'finite', - FP.f32.multiplicationMatrixVectorInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: vecR_matCxR_[non_]const -const vec_mat_cases = ([2, 3, 4] as const) - .flatMap(rows => - ([2, 3, 4] as const).flatMap(cols => - ([true, false] as const).map(nonConst => ({ - [`vec${rows}_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorMatrixToVectorCases( - sparseVectorF32Range(rows), - sparseMatrixF32Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f32.multiplicationVectorMatrixInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_matrix_vector_multiplication', { - ...mat_vec_cases, - ...vec_mat_cases, -}); - g.test('matrix_vector') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -80,8 +37,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeMat(cols, rows, TypeF32), TypeVec(cols, TypeF32)], - TypeVec(rows, TypeF32), + [Type.mat(cols, rows, Type.f32), Type.vec(cols, Type.f32)], + Type.vec(rows, Type.f32), t.params, cases ); @@ -112,8 +69,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeVec(rows, TypeF32), TypeMat(cols, rows, TypeF32)], - TypeVec(cols, TypeF32), + [Type.vec(rows, Type.f32), Type.mat(cols, rows, Type.f32)], + Type.vec(cols, Type.f32), t.params, cases ); @@ -139,8 +96,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeVec(rows, TypeF32), TypeMat(cols, rows, TypeF32)], - TypeVec(cols, TypeF32), + [Type.vec(rows, Type.f32), Type.mat(cols, rows, Type.f32)], + Type.vec(cols, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_multiplication.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_multiplication.cache.ts new file mode 100644 index 0000000000..6a8c0bd81e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_multiplication.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF32Range, sparseVectorF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const multiplicationVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.multiplicationInterval(e, s))); +}; + +const multiplicationScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.multiplicationInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarPairToIntervalCases( + sparseScalarF32Range(), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f32.multiplicationInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateVectorScalarToVectorCases( + sparseVectorF32Range(dim), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + multiplicationVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarVectorToVectorCases( + sparseScalarF32Range(), + sparseVectorF32Range(dim), + nonConst ? 'unfiltered' : 'finite', + multiplicationScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_multiplication', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_multiplication.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_multiplication.spec.ts index 38da08fd3e..478ca71ef0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_multiplication.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_multiplication.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f32 multiplication expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF32Range, sparseVectorF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const multiplicationVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.multiplicationInterval(e, s))); -}; - -const multiplicationScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.multiplicationInterval(s, e))); -}; +import { d } from './f32_multiplication.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarPairToIntervalCases( - sparseF32Range(), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f32.multiplicationInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorScalarToVectorCases( - sparseVectorF32Range(dim), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - multiplicationVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarVectorToVectorCases( - sparseF32Range(), - sparseVectorF32Range(dim), - nonConst ? 'unfiltered' : 'finite', - multiplicationScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_multiplication', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -84,7 +25,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('*'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('*'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector') @@ -100,7 +41,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('*'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('*'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('scalar_compound') @@ -118,7 +59,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('*='), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, compoundBinary('*='), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector_scalar') @@ -138,8 +79,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -162,8 +103,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('*='), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -186,8 +127,8 @@ Accuracy: Correctly rounded await run( t, binary('*'), - [TypeF32, TypeVec(dim, TypeF32)], - TypeVec(dim, TypeF32), + [Type.f32, Type.vec(dim, Type.f32)], + Type.vec(dim, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_remainder.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_remainder.cache.ts new file mode 100644 index 0000000000..2cb1a16f9d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_remainder.cache.ts @@ -0,0 +1,64 @@ +export const description = ` +Execution Tests for non-matrix f32 remainder expression +`; + +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF32Range, sparseVectorF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const remainderVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.remainderInterval(e, s))); +}; + +const remainderScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.remainderInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarPairToIntervalCases( + sparseScalarF32Range(), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f32.remainderInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateVectorScalarToVectorCases( + sparseVectorF32Range(dim), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + remainderVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarVectorToVectorCases( + sparseScalarF32Range(), + sparseVectorF32Range(dim), + nonConst ? 'unfiltered' : 'finite', + remainderScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_remainder', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_remainder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_remainder.spec.ts index 390a7f3426..3a9acb02e0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_remainder.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_remainder.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f32 remainder expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF32Range, sparseVectorF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const remainderVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.remainderInterval(e, s))); -}; - -const remainderScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.remainderInterval(s, e))); -}; +import { d } from './f32_remainder.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarPairToIntervalCases( - sparseF32Range(), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f32.remainderInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorScalarToVectorCases( - sparseVectorF32Range(dim), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - remainderVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarVectorToVectorCases( - sparseF32Range(), - sparseVectorF32Range(dim), - nonConst ? 'unfiltered' : 'finite', - remainderScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_remainder', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -84,7 +25,7 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('%'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('%'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector') @@ -100,7 +41,7 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('%'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('%'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('scalar_compound') @@ -118,7 +59,7 @@ Accuracy: Derived from x - y * trunc(x/y) const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('%='), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, compoundBinary('%='), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector_scalar') @@ -138,8 +79,8 @@ Accuracy: Correctly rounded await run( t, binary('%'), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -162,8 +103,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('%='), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -186,8 +127,8 @@ Accuracy: Correctly rounded await run( t, binary('%'), - [TypeF32, TypeVec(dim, TypeF32)], - TypeVec(dim, TypeF32), + [Type.f32, Type.vec(dim, Type.f32)], + Type.vec(dim, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_subtraction.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_subtraction.cache.ts new file mode 100644 index 0000000000..519c0b3783 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_subtraction.cache.ts @@ -0,0 +1,60 @@ +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseScalarF32Range, sparseVectorF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const subtractionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.subtractionInterval(e, s))); +}; + +const subtractionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { + return FP.f32.toVector(v.map(e => FP.f32.subtractionInterval(s, e))); +}; + +const scalar_cases = ([true, false] as const) + .map(nonConst => ({ + [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarPairToIntervalCases( + sparseScalarF32Range(), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + FP.f32.subtractionInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const vector_scalar_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateVectorScalarToVectorCases( + sparseVectorF32Range(dim), + sparseScalarF32Range(), + nonConst ? 'unfiltered' : 'finite', + subtractionVectorScalarInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateScalarVectorToVectorCases( + sparseScalarF32Range(), + sparseVectorF32Range(dim), + nonConst ? 'unfiltered' : 'finite', + subtractionScalarVectorInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/f32_subtraction', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_subtraction.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_subtraction.spec.ts index 91e06b7de8..55097390e9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_subtraction.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_subtraction.spec.ts @@ -4,73 +4,14 @@ Execution Tests for non-matrix f32 subtraction expression import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32, TypeVec } from '../../../../util/conversion.js'; -import { FP, FPVector } from '../../../../util/floating_point.js'; -import { sparseF32Range, sparseVectorF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -const subtractionVectorScalarInterval = (v: readonly number[], s: number): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.subtractionInterval(e, s))); -}; - -const subtractionScalarVectorInterval = (s: number, v: readonly number[]): FPVector => { - return FP.f32.toVector(v.map(e => FP.f32.subtractionInterval(s, e))); -}; +import { d } from './f32_subtraction.cache.js'; export const g = makeTestGroup(GPUTest); -const scalar_cases = ([true, false] as const) - .map(nonConst => ({ - [`scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarPairToIntervalCases( - sparseF32Range(), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - FP.f32.subtractionInterval - ); - }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const vector_scalar_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorScalarToVectorCases( - sparseVectorF32Range(dim), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - subtractionVectorScalarInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -const scalar_vector_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`scalar_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateScalarVectorToVectorCases( - sparseF32Range(), - sparseVectorF32Range(dim), - nonConst ? 'unfiltered' : 'finite', - subtractionScalarVectorInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('binary/f32_subtraction', { - ...scalar_cases, - ...vector_scalar_cases, - ...scalar_vector_cases, -}); - g.test('scalar') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -84,7 +25,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, binary('-'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('-'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector') @@ -100,7 +41,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' // Using vectorize to generate vector cases based on scalar cases ); - await run(t, binary('-'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, binary('-'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('scalar_compound') @@ -118,7 +59,7 @@ Accuracy: Correctly rounded const cases = await d.get( t.params.inputSource === 'const' ? 'scalar_const' : 'scalar_non_const' ); - await run(t, compoundBinary('-='), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, compoundBinary('-='), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('vector_scalar') @@ -138,8 +79,8 @@ Accuracy: Correctly rounded await run( t, binary('-'), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -162,8 +103,8 @@ Accuracy: Correctly rounded await run( t, compoundBinary('-='), - [TypeVec(dim, TypeF32), TypeF32], - TypeVec(dim, TypeF32), + [Type.vec(dim, Type.f32), Type.f32], + Type.vec(dim, Type.f32), t.params, cases ); @@ -186,8 +127,8 @@ Accuracy: Correctly rounded await run( t, binary('-'), - [TypeF32, TypeVec(dim, TypeF32)], - TypeVec(dim, TypeF32), + [Type.f32, Type.vec(dim, Type.f32)], + Type.vec(dim, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.cache.ts new file mode 100644 index 0000000000..ca7ca879f5 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.cache.ts @@ -0,0 +1,306 @@ +import { kValue } from '../../../../util/constants.js'; +import { sparseI32Range, vectorI32Range } from '../../../../util/math.js'; +import { + generateBinaryToI32Cases, + generateI32VectorBinaryToVectorCases, + generateVectorI32BinaryToVectorCases, +} from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +function i32_add(x: number, y: number): number | undefined { + return x + y; +} + +function i32_subtract(x: number, y: number): number | undefined { + return x - y; +} + +function i32_multiply(x: number, y: number): number | undefined { + return Math.imul(x, y); +} + +function i32_divide_non_const(x: number, y: number): number | undefined { + if (y === 0) { + return x; + } + if (x === kValue.i32.negative.min && y === -1) { + return x; + } + return x / y; +} + +function i32_divide_const(x: number, y: number): number | undefined { + if (y === 0) { + return undefined; + } + if (x === kValue.i32.negative.min && y === -1) { + return undefined; + } + return x / y; +} + +function i32_remainder_non_const(x: number, y: number): number | undefined { + if (y === 0) { + return 0; + } + if (x === kValue.i32.negative.min && y === -1) { + return 0; + } + return x % y; +} + +function i32_remainder_const(x: number, y: number): number | undefined { + if (y === 0) { + return undefined; + } + if (x === kValue.i32.negative.min && y === -1) { + return undefined; + } + return x % y; +} + +export const d = makeCaseCache('binary/i32_arithmetic', { + addition: () => { + return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_add); + }, + subtraction: () => { + return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_subtract); + }, + multiplication: () => { + return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_multiply); + }, + division_non_const: () => { + return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_divide_non_const); + }, + division_const: () => { + return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_divide_const); + }, + remainder_non_const: () => { + return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_remainder_non_const); + }, + remainder_const: () => { + return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_remainder_const); + }, + addition_scalar_vector2: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(2), i32_add); + }, + addition_scalar_vector3: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(3), i32_add); + }, + addition_scalar_vector4: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(4), i32_add); + }, + addition_vector2_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(2), sparseI32Range(), i32_add); + }, + addition_vector3_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(3), sparseI32Range(), i32_add); + }, + addition_vector4_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(4), sparseI32Range(), i32_add); + }, + subtraction_scalar_vector2: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(2), i32_subtract); + }, + subtraction_scalar_vector3: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(3), i32_subtract); + }, + subtraction_scalar_vector4: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(4), i32_subtract); + }, + subtraction_vector2_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(2), sparseI32Range(), i32_subtract); + }, + subtraction_vector3_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(3), sparseI32Range(), i32_subtract); + }, + subtraction_vector4_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(4), sparseI32Range(), i32_subtract); + }, + multiplication_scalar_vector2: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(2), i32_multiply); + }, + multiplication_scalar_vector3: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(3), i32_multiply); + }, + multiplication_scalar_vector4: () => { + return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(4), i32_multiply); + }, + multiplication_vector2_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(2), sparseI32Range(), i32_multiply); + }, + multiplication_vector3_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(3), sparseI32Range(), i32_multiply); + }, + multiplication_vector4_scalar: () => { + return generateVectorI32BinaryToVectorCases(vectorI32Range(4), sparseI32Range(), i32_multiply); + }, + division_scalar_vector2_non_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(2), + i32_divide_non_const + ); + }, + division_scalar_vector3_non_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(3), + i32_divide_non_const + ); + }, + division_scalar_vector4_non_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(4), + i32_divide_non_const + ); + }, + division_vector2_scalar_non_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(2), + sparseI32Range(), + i32_divide_non_const + ); + }, + division_vector3_scalar_non_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(3), + sparseI32Range(), + i32_divide_non_const + ); + }, + division_vector4_scalar_non_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(4), + sparseI32Range(), + i32_divide_non_const + ); + }, + division_scalar_vector2_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(2), + i32_divide_const + ); + }, + division_scalar_vector3_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(3), + i32_divide_const + ); + }, + division_scalar_vector4_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(4), + i32_divide_const + ); + }, + division_vector2_scalar_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(2), + sparseI32Range(), + i32_divide_const + ); + }, + division_vector3_scalar_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(3), + sparseI32Range(), + i32_divide_const + ); + }, + division_vector4_scalar_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(4), + sparseI32Range(), + i32_divide_const + ); + }, + remainder_scalar_vector2_non_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(2), + i32_remainder_non_const + ); + }, + remainder_scalar_vector3_non_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(3), + i32_remainder_non_const + ); + }, + remainder_scalar_vector4_non_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(4), + i32_remainder_non_const + ); + }, + remainder_vector2_scalar_non_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(2), + sparseI32Range(), + i32_remainder_non_const + ); + }, + remainder_vector3_scalar_non_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(3), + sparseI32Range(), + i32_remainder_non_const + ); + }, + remainder_vector4_scalar_non_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(4), + sparseI32Range(), + i32_remainder_non_const + ); + }, + remainder_scalar_vector2_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(2), + i32_remainder_const + ); + }, + remainder_scalar_vector3_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(3), + i32_remainder_const + ); + }, + remainder_scalar_vector4_const: () => { + return generateI32VectorBinaryToVectorCases( + sparseI32Range(), + vectorI32Range(4), + i32_remainder_const + ); + }, + remainder_vector2_scalar_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(2), + sparseI32Range(), + i32_remainder_const + ); + }, + remainder_vector3_scalar_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(3), + sparseI32Range(), + i32_remainder_const + ); + }, + remainder_vector4_scalar_const: () => { + return generateVectorI32BinaryToVectorCases( + vectorI32Range(4), + sparseI32Range(), + i32_remainder_const + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts index e9b7a2407f..ef8ea00447 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts @@ -4,320 +4,14 @@ Execution Tests for the i32 arithmetic binary expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { kValue } from '../../../../util/constants.js'; -import { TypeI32, TypeVec } from '../../../../util/conversion.js'; -import { sparseI32Range, vectorI32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { - allInputSources, - generateBinaryToI32Cases, - generateI32VectorBinaryToVectorCases, - generateVectorI32BinaryToVectorCases, - run, -} from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -function i32_add(x: number, y: number): number | undefined { - return x + y; -} - -function i32_subtract(x: number, y: number): number | undefined { - return x - y; -} - -function i32_multiply(x: number, y: number): number | undefined { - return Math.imul(x, y); -} - -function i32_divide_non_const(x: number, y: number): number | undefined { - if (y === 0) { - return x; - } - if (x === kValue.i32.negative.min && y === -1) { - return x; - } - return x / y; -} - -function i32_divide_const(x: number, y: number): number | undefined { - if (y === 0) { - return undefined; - } - if (x === kValue.i32.negative.min && y === -1) { - return undefined; - } - return x / y; -} - -function i32_remainder_non_const(x: number, y: number): number | undefined { - if (y === 0) { - return 0; - } - if (x === kValue.i32.negative.min && y === -1) { - return 0; - } - return x % y; -} - -function i32_remainder_const(x: number, y: number): number | undefined { - if (y === 0) { - return undefined; - } - if (x === kValue.i32.negative.min && y === -1) { - return undefined; - } - return x % y; -} +import { d } from './i32_arithmetic.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('binary/i32_arithmetic', { - addition: () => { - return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_add); - }, - subtraction: () => { - return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_subtract); - }, - multiplication: () => { - return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_multiply); - }, - division_non_const: () => { - return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_divide_non_const); - }, - division_const: () => { - return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_divide_const); - }, - remainder_non_const: () => { - return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_remainder_non_const); - }, - remainder_const: () => { - return generateBinaryToI32Cases(sparseI32Range(), sparseI32Range(), i32_remainder_const); - }, - addition_scalar_vector2: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(2), i32_add); - }, - addition_scalar_vector3: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(3), i32_add); - }, - addition_scalar_vector4: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(4), i32_add); - }, - addition_vector2_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(2), sparseI32Range(), i32_add); - }, - addition_vector3_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(3), sparseI32Range(), i32_add); - }, - addition_vector4_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(4), sparseI32Range(), i32_add); - }, - subtraction_scalar_vector2: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(2), i32_subtract); - }, - subtraction_scalar_vector3: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(3), i32_subtract); - }, - subtraction_scalar_vector4: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(4), i32_subtract); - }, - subtraction_vector2_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(2), sparseI32Range(), i32_subtract); - }, - subtraction_vector3_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(3), sparseI32Range(), i32_subtract); - }, - subtraction_vector4_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(4), sparseI32Range(), i32_subtract); - }, - multiplication_scalar_vector2: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(2), i32_multiply); - }, - multiplication_scalar_vector3: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(3), i32_multiply); - }, - multiplication_scalar_vector4: () => { - return generateI32VectorBinaryToVectorCases(sparseI32Range(), vectorI32Range(4), i32_multiply); - }, - multiplication_vector2_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(2), sparseI32Range(), i32_multiply); - }, - multiplication_vector3_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(3), sparseI32Range(), i32_multiply); - }, - multiplication_vector4_scalar: () => { - return generateVectorI32BinaryToVectorCases(vectorI32Range(4), sparseI32Range(), i32_multiply); - }, - division_scalar_vector2_non_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(2), - i32_divide_non_const - ); - }, - division_scalar_vector3_non_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(3), - i32_divide_non_const - ); - }, - division_scalar_vector4_non_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(4), - i32_divide_non_const - ); - }, - division_vector2_scalar_non_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(2), - sparseI32Range(), - i32_divide_non_const - ); - }, - division_vector3_scalar_non_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(3), - sparseI32Range(), - i32_divide_non_const - ); - }, - division_vector4_scalar_non_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(4), - sparseI32Range(), - i32_divide_non_const - ); - }, - division_scalar_vector2_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(2), - i32_divide_const - ); - }, - division_scalar_vector3_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(3), - i32_divide_const - ); - }, - division_scalar_vector4_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(4), - i32_divide_const - ); - }, - division_vector2_scalar_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(2), - sparseI32Range(), - i32_divide_const - ); - }, - division_vector3_scalar_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(3), - sparseI32Range(), - i32_divide_const - ); - }, - division_vector4_scalar_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(4), - sparseI32Range(), - i32_divide_const - ); - }, - remainder_scalar_vector2_non_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(2), - i32_remainder_non_const - ); - }, - remainder_scalar_vector3_non_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(3), - i32_remainder_non_const - ); - }, - remainder_scalar_vector4_non_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(4), - i32_remainder_non_const - ); - }, - remainder_vector2_scalar_non_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(2), - sparseI32Range(), - i32_remainder_non_const - ); - }, - remainder_vector3_scalar_non_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(3), - sparseI32Range(), - i32_remainder_non_const - ); - }, - remainder_vector4_scalar_non_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(4), - sparseI32Range(), - i32_remainder_non_const - ); - }, - remainder_scalar_vector2_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(2), - i32_remainder_const - ); - }, - remainder_scalar_vector3_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(3), - i32_remainder_const - ); - }, - remainder_scalar_vector4_const: () => { - return generateI32VectorBinaryToVectorCases( - sparseI32Range(), - vectorI32Range(4), - i32_remainder_const - ); - }, - remainder_vector2_scalar_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(2), - sparseI32Range(), - i32_remainder_const - ); - }, - remainder_vector3_scalar_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(3), - sparseI32Range(), - i32_remainder_const - ); - }, - remainder_vector4_scalar_const: () => { - return generateVectorI32BinaryToVectorCases( - vectorI32Range(4), - sparseI32Range(), - i32_remainder_const - ); - }, -}); - g.test('addition') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -330,7 +24,7 @@ Expression: x + y ) .fn(async t => { const cases = await d.get('addition'); - await run(t, binary('+'), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, binary('+'), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('addition_compound') @@ -345,7 +39,7 @@ Expression: x += y ) .fn(async t => { const cases = await d.get('addition'); - await run(t, compoundBinary('+='), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, compoundBinary('+='), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('subtraction') @@ -360,7 +54,7 @@ Expression: x - y ) .fn(async t => { const cases = await d.get('subtraction'); - await run(t, binary('-'), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, binary('-'), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('subtraction_compound') @@ -375,7 +69,7 @@ Expression: x -= y ) .fn(async t => { const cases = await d.get('subtraction'); - await run(t, compoundBinary('-='), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, compoundBinary('-='), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('multiplication') @@ -390,7 +84,7 @@ Expression: x * y ) .fn(async t => { const cases = await d.get('multiplication'); - await run(t, binary('*'), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, binary('*'), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('multiplication_compound') @@ -405,7 +99,7 @@ Expression: x *= y ) .fn(async t => { const cases = await d.get('multiplication'); - await run(t, compoundBinary('*='), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, compoundBinary('*='), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('division') @@ -422,7 +116,7 @@ Expression: x / y const cases = await d.get( t.params.inputSource === 'const' ? 'division_const' : 'division_non_const' ); - await run(t, binary('/'), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, binary('/'), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('division_compound') @@ -439,7 +133,7 @@ Expression: x /= y const cases = await d.get( t.params.inputSource === 'const' ? 'division_const' : 'division_non_const' ); - await run(t, compoundBinary('/='), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, compoundBinary('/='), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('remainder') @@ -456,7 +150,7 @@ Expression: x % y const cases = await d.get( t.params.inputSource === 'const' ? 'remainder_const' : 'remainder_non_const' ); - await run(t, binary('%'), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, binary('%'), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('remainder_compound') @@ -473,7 +167,7 @@ Expression: x %= y const cases = await d.get( t.params.inputSource === 'const' ? 'remainder_const' : 'remainder_non_const' ); - await run(t, compoundBinary('%='), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, compoundBinary('%='), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('addition_scalar_vector') @@ -488,9 +182,9 @@ Expression: x + y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`addition_scalar_vector${vec_size}`); - await run(t, binary('+'), [TypeI32, vec_type], vec_type, t.params, cases); + await run(t, binary('+'), [Type.i32, vec_type], vec_type, t.params, cases); }); g.test('addition_vector_scalar') @@ -505,9 +199,9 @@ Expression: x + y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`addition_vector${vec_size}_scalar`); - await run(t, binary('+'), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, binary('+'), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('addition_vector_scalar_compound') @@ -522,9 +216,9 @@ Expression: x += y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`addition_vector${vec_size}_scalar`); - await run(t, compoundBinary('+='), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, compoundBinary('+='), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('subtraction_scalar_vector') @@ -539,9 +233,9 @@ Expression: x - y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`subtraction_scalar_vector${vec_size}`); - await run(t, binary('-'), [TypeI32, vec_type], vec_type, t.params, cases); + await run(t, binary('-'), [Type.i32, vec_type], vec_type, t.params, cases); }); g.test('subtraction_vector_scalar') @@ -556,9 +250,9 @@ Expression: x - y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`subtraction_vector${vec_size}_scalar`); - await run(t, binary('-'), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, binary('-'), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('subtraction_vector_scalar_compound') @@ -573,9 +267,9 @@ Expression: x -= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`subtraction_vector${vec_size}_scalar`); - await run(t, compoundBinary('-='), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, compoundBinary('-='), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('multiplication_scalar_vector') @@ -590,9 +284,9 @@ Expression: x * y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`multiplication_scalar_vector${vec_size}`); - await run(t, binary('*'), [TypeI32, vec_type], vec_type, t.params, cases); + await run(t, binary('*'), [Type.i32, vec_type], vec_type, t.params, cases); }); g.test('multiplication_vector_scalar') @@ -607,9 +301,9 @@ Expression: x * y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`multiplication_vector${vec_size}_scalar`); - await run(t, binary('*'), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, binary('*'), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('multiplication_vector_scalar_compound') @@ -624,9 +318,9 @@ Expression: x *= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const cases = await d.get(`multiplication_vector${vec_size}_scalar`); - await run(t, compoundBinary('*='), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, compoundBinary('*='), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('division_scalar_vector') @@ -641,10 +335,10 @@ Expression: x / y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`division_scalar_vector${vec_size}_${source}`); - await run(t, binary('/'), [TypeI32, vec_type], vec_type, t.params, cases); + await run(t, binary('/'), [Type.i32, vec_type], vec_type, t.params, cases); }); g.test('division_vector_scalar') @@ -659,10 +353,10 @@ Expression: x / y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`division_vector${vec_size}_scalar_${source}`); - await run(t, binary('/'), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, binary('/'), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('division_vector_scalar_compound') @@ -677,10 +371,10 @@ Expression: x /= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`division_vector${vec_size}_scalar_${source}`); - await run(t, compoundBinary('/='), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, compoundBinary('/='), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('remainder_scalar_vector') @@ -695,10 +389,10 @@ Expression: x % y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`remainder_scalar_vector${vec_size}_${source}`); - await run(t, binary('%'), [TypeI32, vec_type], vec_type, t.params, cases); + await run(t, binary('%'), [Type.i32, vec_type], vec_type, t.params, cases); }); g.test('remainder_vector_scalar') @@ -713,10 +407,10 @@ Expression: x % y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`remainder_vector${vec_size}_scalar_${source}`); - await run(t, binary('%'), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, binary('%'), [vec_type, Type.i32], vec_type, t.params, cases); }); g.test('remainder_vector_scalar_compound') @@ -731,8 +425,8 @@ Expression: x %= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeI32); + const vec_type = Type.vec(vec_size, Type.i32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`remainder_vector${vec_size}_scalar_${source}`); - await run(t, compoundBinary('%='), [vec_type, TypeI32], vec_type, t.params, cases); + await run(t, compoundBinary('%='), [vec_type, Type.i32], vec_type, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_comparison.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_comparison.cache.ts new file mode 100644 index 0000000000..de0a6de477 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_comparison.cache.ts @@ -0,0 +1,21 @@ +import { bool, i32 } from '../../../../util/conversion.js'; +import { vectorI32Range } from '../../../../util/math.js'; +import { Case } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +/** + * @returns a test case for the provided left hand & right hand values and + * expected boolean result. + */ +function makeCase(lhs: number, rhs: number, expected_answer: boolean): Case { + return { input: [i32(lhs), i32(rhs)], expected: bool(expected_answer) }; +} + +export const d = makeCaseCache('binary/i32_comparison', { + equals: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] === v[1])), + not_equals: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] !== v[1])), + less_than: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] < v[1])), + less_equal: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] <= v[1])), + greater_than: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] > v[1])), + greater_equal: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] >= v[1])), +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_comparison.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_comparison.spec.ts index dce1a2519e..9b6566c9a4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_comparison.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_comparison.spec.ts @@ -4,32 +4,14 @@ Execution Tests for the i32 comparison expressions import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { i32, bool, TypeBool, TypeI32 } from '../../../../util/conversion.js'; -import { vectorI32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, Case, run } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { allInputSources, run } from '../expression.js'; import { binary } from './binary.js'; +import { d } from './i32_comparison.cache.js'; export const g = makeTestGroup(GPUTest); -/** - * @returns a test case for the provided left hand & right hand values and - * expected boolean result. - */ -function makeCase(lhs: number, rhs: number, expected_answer: boolean): Case { - return { input: [i32(lhs), i32(rhs)], expected: bool(expected_answer) }; -} - -export const d = makeCaseCache('binary/i32_comparison', { - equals: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] === v[1])), - not_equals: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] !== v[1])), - less_than: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] < v[1])), - less_equal: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] <= v[1])), - greater_than: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] > v[1])), - greater_equal: () => vectorI32Range(2).map(v => makeCase(v[0], v[1], v[0] >= v[1])), -}); - g.test('equals') .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') .desc( @@ -42,7 +24,7 @@ Expression: x == y ) .fn(async t => { const cases = await d.get('equals'); - await run(t, binary('=='), [TypeI32, TypeI32], TypeBool, t.params, cases); + await run(t, binary('=='), [Type.i32, Type.i32], Type.bool, t.params, cases); }); g.test('not_equals') @@ -57,7 +39,7 @@ Expression: x != y ) .fn(async t => { const cases = await d.get('not_equals'); - await run(t, binary('!='), [TypeI32, TypeI32], TypeBool, t.params, cases); + await run(t, binary('!='), [Type.i32, Type.i32], Type.bool, t.params, cases); }); g.test('less_than') @@ -72,7 +54,7 @@ Expression: x < y ) .fn(async t => { const cases = await d.get('less_than'); - await run(t, binary('<'), [TypeI32, TypeI32], TypeBool, t.params, cases); + await run(t, binary('<'), [Type.i32, Type.i32], Type.bool, t.params, cases); }); g.test('less_equals') @@ -87,7 +69,7 @@ Expression: x <= y ) .fn(async t => { const cases = await d.get('less_equal'); - await run(t, binary('<='), [TypeI32, TypeI32], TypeBool, t.params, cases); + await run(t, binary('<='), [Type.i32, Type.i32], Type.bool, t.params, cases); }); g.test('greater_than') @@ -102,7 +84,7 @@ Expression: x > y ) .fn(async t => { const cases = await d.get('greater_than'); - await run(t, binary('>'), [TypeI32, TypeI32], TypeBool, t.params, cases); + await run(t, binary('>'), [Type.i32, Type.i32], Type.bool, t.params, cases); }); g.test('greater_equals') @@ -117,5 +99,5 @@ Expression: x >= y ) .fn(async t => { const cases = await d.get('greater_equal'); - await run(t, binary('>='), [TypeI32, TypeI32], TypeBool, t.params, cases); + await run(t, binary('>='), [Type.i32, Type.i32], Type.bool, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.cache.ts new file mode 100644 index 0000000000..91be35a1ee --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.cache.ts @@ -0,0 +1,293 @@ +import { sparseU32Range, vectorU32Range } from '../../../../util/math.js'; +import { + generateBinaryToU32Cases, + generateU32VectorBinaryToVectorCases, + generateVectorU32BinaryToVectorCases, +} from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +function u32_add(x: number, y: number): number | undefined { + return x + y; +} + +function u32_subtract(x: number, y: number): number | undefined { + return x - y; +} + +function u32_multiply(x: number, y: number): number | undefined { + return Math.imul(x, y); +} + +function u32_divide_non_const(x: number, y: number): number | undefined { + if (y === 0) { + return x; + } + return x / y; +} + +function u32_divide_const(x: number, y: number): number | undefined { + if (y === 0) { + return undefined; + } + return x / y; +} + +function u32_remainder_non_const(x: number, y: number): number | undefined { + if (y === 0) { + return 0; + } + return x % y; +} + +function u32_remainder_const(x: number, y: number): number | undefined { + if (y === 0) { + return undefined; + } + return x % y; +} + +export const d = makeCaseCache('binary/u32_arithmetic', { + addition: () => { + return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_add); + }, + subtraction: () => { + return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_subtract); + }, + multiplication: () => { + return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_multiply); + }, + division_non_const: () => { + return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_divide_non_const); + }, + division_const: () => { + return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_divide_const); + }, + remainder_non_const: () => { + return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_remainder_non_const); + }, + remainder_const: () => { + return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_remainder_const); + }, + addition_scalar_vector2: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(2), u32_add); + }, + addition_scalar_vector3: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(3), u32_add); + }, + addition_scalar_vector4: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(4), u32_add); + }, + addition_vector2_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(2), sparseU32Range(), u32_add); + }, + addition_vector3_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(3), sparseU32Range(), u32_add); + }, + addition_vector4_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(4), sparseU32Range(), u32_add); + }, + subtraction_scalar_vector2: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(2), u32_subtract); + }, + subtraction_scalar_vector3: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(3), u32_subtract); + }, + subtraction_scalar_vector4: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(4), u32_subtract); + }, + subtraction_vector2_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(2), sparseU32Range(), u32_subtract); + }, + subtraction_vector3_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(3), sparseU32Range(), u32_subtract); + }, + subtraction_vector4_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(4), sparseU32Range(), u32_subtract); + }, + multiplication_scalar_vector2: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(2), u32_multiply); + }, + multiplication_scalar_vector3: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(3), u32_multiply); + }, + multiplication_scalar_vector4: () => { + return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(4), u32_multiply); + }, + multiplication_vector2_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(2), sparseU32Range(), u32_multiply); + }, + multiplication_vector3_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(3), sparseU32Range(), u32_multiply); + }, + multiplication_vector4_scalar: () => { + return generateVectorU32BinaryToVectorCases(vectorU32Range(4), sparseU32Range(), u32_multiply); + }, + division_scalar_vector2_non_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(2), + u32_divide_non_const + ); + }, + division_scalar_vector3_non_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(3), + u32_divide_non_const + ); + }, + division_scalar_vector4_non_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(4), + u32_divide_non_const + ); + }, + division_vector2_scalar_non_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(2), + sparseU32Range(), + u32_divide_non_const + ); + }, + division_vector3_scalar_non_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(3), + sparseU32Range(), + u32_divide_non_const + ); + }, + division_vector4_scalar_non_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(4), + sparseU32Range(), + u32_divide_non_const + ); + }, + division_scalar_vector2_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(2), + u32_divide_const + ); + }, + division_scalar_vector3_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(3), + u32_divide_const + ); + }, + division_scalar_vector4_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(4), + u32_divide_const + ); + }, + division_vector2_scalar_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(2), + sparseU32Range(), + u32_divide_const + ); + }, + division_vector3_scalar_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(3), + sparseU32Range(), + u32_divide_const + ); + }, + division_vector4_scalar_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(4), + sparseU32Range(), + u32_divide_const + ); + }, + remainder_scalar_vector2_non_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(2), + u32_remainder_non_const + ); + }, + remainder_scalar_vector3_non_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(3), + u32_remainder_non_const + ); + }, + remainder_scalar_vector4_non_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(4), + u32_remainder_non_const + ); + }, + remainder_vector2_scalar_non_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(2), + sparseU32Range(), + u32_remainder_non_const + ); + }, + remainder_vector3_scalar_non_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(3), + sparseU32Range(), + u32_remainder_non_const + ); + }, + remainder_vector4_scalar_non_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(4), + sparseU32Range(), + u32_remainder_non_const + ); + }, + remainder_scalar_vector2_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(2), + u32_remainder_const + ); + }, + remainder_scalar_vector3_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(3), + u32_remainder_const + ); + }, + remainder_scalar_vector4_const: () => { + return generateU32VectorBinaryToVectorCases( + sparseU32Range(), + vectorU32Range(4), + u32_remainder_const + ); + }, + remainder_vector2_scalar_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(2), + sparseU32Range(), + u32_remainder_const + ); + }, + remainder_vector3_scalar_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(3), + sparseU32Range(), + u32_remainder_const + ); + }, + remainder_vector4_scalar_const: () => { + return generateVectorU32BinaryToVectorCases( + vectorU32Range(4), + sparseU32Range(), + u32_remainder_const + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts index 88667e8233..6df16f303c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts @@ -4,307 +4,14 @@ Execution Tests for the u32 arithmetic binary expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeU32, TypeVec } from '../../../../util/conversion.js'; -import { sparseU32Range, vectorU32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { - allInputSources, - generateBinaryToU32Cases, - generateU32VectorBinaryToVectorCases, - generateVectorU32BinaryToVectorCases, - run, -} from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { allInputSources, run } from '../expression.js'; import { binary, compoundBinary } from './binary.js'; - -function u32_add(x: number, y: number): number | undefined { - return x + y; -} - -function u32_subtract(x: number, y: number): number | undefined { - return x - y; -} - -function u32_multiply(x: number, y: number): number | undefined { - return Math.imul(x, y); -} - -function u32_divide_non_const(x: number, y: number): number | undefined { - if (y === 0) { - return x; - } - return x / y; -} - -function u32_divide_const(x: number, y: number): number | undefined { - if (y === 0) { - return undefined; - } - return x / y; -} - -function u32_remainder_non_const(x: number, y: number): number | undefined { - if (y === 0) { - return 0; - } - return x % y; -} - -function u32_remainder_const(x: number, y: number): number | undefined { - if (y === 0) { - return undefined; - } - return x % y; -} +import { d } from './u32_arithmetic.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('binary/u32_arithmetic', { - addition: () => { - return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_add); - }, - subtraction: () => { - return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_subtract); - }, - multiplication: () => { - return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_multiply); - }, - division_non_const: () => { - return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_divide_non_const); - }, - division_const: () => { - return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_divide_const); - }, - remainder_non_const: () => { - return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_remainder_non_const); - }, - remainder_const: () => { - return generateBinaryToU32Cases(sparseU32Range(), sparseU32Range(), u32_remainder_const); - }, - addition_scalar_vector2: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(2), u32_add); - }, - addition_scalar_vector3: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(3), u32_add); - }, - addition_scalar_vector4: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(4), u32_add); - }, - addition_vector2_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(2), sparseU32Range(), u32_add); - }, - addition_vector3_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(3), sparseU32Range(), u32_add); - }, - addition_vector4_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(4), sparseU32Range(), u32_add); - }, - subtraction_scalar_vector2: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(2), u32_subtract); - }, - subtraction_scalar_vector3: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(3), u32_subtract); - }, - subtraction_scalar_vector4: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(4), u32_subtract); - }, - subtraction_vector2_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(2), sparseU32Range(), u32_subtract); - }, - subtraction_vector3_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(3), sparseU32Range(), u32_subtract); - }, - subtraction_vector4_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(4), sparseU32Range(), u32_subtract); - }, - multiplication_scalar_vector2: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(2), u32_multiply); - }, - multiplication_scalar_vector3: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(3), u32_multiply); - }, - multiplication_scalar_vector4: () => { - return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(4), u32_multiply); - }, - multiplication_vector2_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(2), sparseU32Range(), u32_multiply); - }, - multiplication_vector3_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(3), sparseU32Range(), u32_multiply); - }, - multiplication_vector4_scalar: () => { - return generateVectorU32BinaryToVectorCases(vectorU32Range(4), sparseU32Range(), u32_multiply); - }, - division_scalar_vector2_non_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(2), - u32_divide_non_const - ); - }, - division_scalar_vector3_non_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(3), - u32_divide_non_const - ); - }, - division_scalar_vector4_non_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(4), - u32_divide_non_const - ); - }, - division_vector2_scalar_non_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(2), - sparseU32Range(), - u32_divide_non_const - ); - }, - division_vector3_scalar_non_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(3), - sparseU32Range(), - u32_divide_non_const - ); - }, - division_vector4_scalar_non_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(4), - sparseU32Range(), - u32_divide_non_const - ); - }, - division_scalar_vector2_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(2), - u32_divide_const - ); - }, - division_scalar_vector3_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(3), - u32_divide_const - ); - }, - division_scalar_vector4_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(4), - u32_divide_const - ); - }, - division_vector2_scalar_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(2), - sparseU32Range(), - u32_divide_const - ); - }, - division_vector3_scalar_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(3), - sparseU32Range(), - u32_divide_const - ); - }, - division_vector4_scalar_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(4), - sparseU32Range(), - u32_divide_const - ); - }, - remainder_scalar_vector2_non_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(2), - u32_remainder_non_const - ); - }, - remainder_scalar_vector3_non_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(3), - u32_remainder_non_const - ); - }, - remainder_scalar_vector4_non_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(4), - u32_remainder_non_const - ); - }, - remainder_vector2_scalar_non_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(2), - sparseU32Range(), - u32_remainder_non_const - ); - }, - remainder_vector3_scalar_non_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(3), - sparseU32Range(), - u32_remainder_non_const - ); - }, - remainder_vector4_scalar_non_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(4), - sparseU32Range(), - u32_remainder_non_const - ); - }, - remainder_scalar_vector2_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(2), - u32_remainder_const - ); - }, - remainder_scalar_vector3_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(3), - u32_remainder_const - ); - }, - remainder_scalar_vector4_const: () => { - return generateU32VectorBinaryToVectorCases( - sparseU32Range(), - vectorU32Range(4), - u32_remainder_const - ); - }, - remainder_vector2_scalar_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(2), - sparseU32Range(), - u32_remainder_const - ); - }, - remainder_vector3_scalar_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(3), - sparseU32Range(), - u32_remainder_const - ); - }, - remainder_vector4_scalar_const: () => { - return generateVectorU32BinaryToVectorCases( - vectorU32Range(4), - sparseU32Range(), - u32_remainder_const - ); - }, -}); - g.test('addition') .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') .desc( @@ -317,7 +24,7 @@ Expression: x + y ) .fn(async t => { const cases = await d.get('addition'); - await run(t, binary('+'), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, binary('+'), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('addition_compound') @@ -332,7 +39,7 @@ Expression: x += y ) .fn(async t => { const cases = await d.get('addition'); - await run(t, compoundBinary('+='), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, compoundBinary('+='), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('subtraction') @@ -347,7 +54,7 @@ Expression: x - y ) .fn(async t => { const cases = await d.get('subtraction'); - await run(t, binary('-'), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, binary('-'), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('subtraction_compound') @@ -362,7 +69,7 @@ Expression: x -= y ) .fn(async t => { const cases = await d.get('subtraction'); - await run(t, compoundBinary('-='), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, compoundBinary('-='), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('multiplication') @@ -377,7 +84,7 @@ Expression: x * y ) .fn(async t => { const cases = await d.get('multiplication'); - await run(t, binary('*'), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, binary('*'), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('multiplication_compound') @@ -392,7 +99,7 @@ Expression: x *= y ) .fn(async t => { const cases = await d.get('multiplication'); - await run(t, compoundBinary('*='), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, compoundBinary('*='), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('division') @@ -409,7 +116,7 @@ Expression: x / y const cases = await d.get( t.params.inputSource === 'const' ? 'division_const' : 'division_non_const' ); - await run(t, binary('/'), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, binary('/'), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('division_compound') @@ -426,7 +133,7 @@ Expression: x /= y const cases = await d.get( t.params.inputSource === 'const' ? 'division_const' : 'division_non_const' ); - await run(t, compoundBinary('/='), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, compoundBinary('/='), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('remainder') @@ -443,7 +150,7 @@ Expression: x % y const cases = await d.get( t.params.inputSource === 'const' ? 'remainder_const' : 'remainder_non_const' ); - await run(t, binary('%'), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, binary('%'), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('remainder_compound') @@ -460,7 +167,7 @@ Expression: x %= y const cases = await d.get( t.params.inputSource === 'const' ? 'remainder_const' : 'remainder_non_const' ); - await run(t, compoundBinary('%='), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, compoundBinary('%='), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('addition_scalar_vector') @@ -475,9 +182,9 @@ Expression: x + y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`addition_scalar_vector${vec_size}`); - await run(t, binary('+'), [TypeU32, vec_type], vec_type, t.params, cases); + await run(t, binary('+'), [Type.u32, vec_type], vec_type, t.params, cases); }); g.test('addition_vector_scalar') @@ -492,9 +199,9 @@ Expression: x + y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`addition_vector${vec_size}_scalar`); - await run(t, binary('+'), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, binary('+'), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('addition_vector_scalar_compound') @@ -509,9 +216,9 @@ Expression: x += y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`addition_vector${vec_size}_scalar`); - await run(t, compoundBinary('+='), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, compoundBinary('+='), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('subtraction_scalar_vector') @@ -526,9 +233,9 @@ Expression: x - y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`subtraction_scalar_vector${vec_size}`); - await run(t, binary('-'), [TypeU32, vec_type], vec_type, t.params, cases); + await run(t, binary('-'), [Type.u32, vec_type], vec_type, t.params, cases); }); g.test('subtraction_vector_scalar') @@ -543,9 +250,9 @@ Expression: x - y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`subtraction_vector${vec_size}_scalar`); - await run(t, binary('-'), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, binary('-'), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('subtraction_vector_scalar_compound') @@ -560,9 +267,9 @@ Expression: x -= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`subtraction_vector${vec_size}_scalar`); - await run(t, compoundBinary('-='), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, compoundBinary('-='), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('multiplication_scalar_vector') @@ -577,9 +284,9 @@ Expression: x * y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`multiplication_scalar_vector${vec_size}`); - await run(t, binary('*'), [TypeU32, vec_type], vec_type, t.params, cases); + await run(t, binary('*'), [Type.u32, vec_type], vec_type, t.params, cases); }); g.test('multiplication_vector_scalar') @@ -594,9 +301,9 @@ Expression: x * y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`multiplication_vector${vec_size}_scalar`); - await run(t, binary('*'), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, binary('*'), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('multiplication_vector_scalar_compound') @@ -611,9 +318,9 @@ Expression: x *= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const cases = await d.get(`multiplication_vector${vec_size}_scalar`); - await run(t, compoundBinary('*='), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, compoundBinary('*='), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('division_scalar_vector') @@ -628,10 +335,10 @@ Expression: x / y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`division_scalar_vector${vec_size}_${source}`); - await run(t, binary('/'), [TypeU32, vec_type], vec_type, t.params, cases); + await run(t, binary('/'), [Type.u32, vec_type], vec_type, t.params, cases); }); g.test('division_vector_scalar') @@ -646,10 +353,10 @@ Expression: x / y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`division_vector${vec_size}_scalar_${source}`); - await run(t, binary('/'), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, binary('/'), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('division_vector_scalar_compound') @@ -664,10 +371,10 @@ Expression: x /= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`division_vector${vec_size}_scalar_${source}`); - await run(t, compoundBinary('/='), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, compoundBinary('/='), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('remainder_scalar_vector') @@ -682,10 +389,10 @@ Expression: x % y ) .fn(async t => { const vec_size = t.params.vectorize_rhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`remainder_scalar_vector${vec_size}_${source}`); - await run(t, binary('%'), [TypeU32, vec_type], vec_type, t.params, cases); + await run(t, binary('%'), [Type.u32, vec_type], vec_type, t.params, cases); }); g.test('remainder_vector_scalar') @@ -700,10 +407,10 @@ Expression: x % y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`remainder_vector${vec_size}_scalar_${source}`); - await run(t, binary('%'), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, binary('%'), [vec_type, Type.u32], vec_type, t.params, cases); }); g.test('remainder_vector_scalar_compound') @@ -718,8 +425,8 @@ Expression: x %= y ) .fn(async t => { const vec_size = t.params.vectorize_lhs; - const vec_type = TypeVec(vec_size, TypeU32); + const vec_type = Type.vec(vec_size, Type.u32); const source = t.params.inputSource === 'const' ? 'const' : 'non_const'; const cases = await d.get(`remainder_vector${vec_size}_scalar_${source}`); - await run(t, compoundBinary('%='), [vec_type, TypeU32], vec_type, t.params, cases); + await run(t, compoundBinary('%='), [vec_type, Type.u32], vec_type, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_comparison.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_comparison.cache.ts new file mode 100644 index 0000000000..93b0500496 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_comparison.cache.ts @@ -0,0 +1,21 @@ +import { bool, u32 } from '../../../../util/conversion.js'; +import { vectorU32Range } from '../../../../util/math.js'; +import { Case } from '../case.js'; +import { makeCaseCache } from '../case_cache.js'; + +/** + * @returns a test case for the provided left hand & right hand values and + * expected boolean result. + */ +function makeCase(lhs: number, rhs: number, expected_answer: boolean): Case { + return { input: [u32(lhs), u32(rhs)], expected: bool(expected_answer) }; +} + +export const d = makeCaseCache('binary/u32_comparison', { + equals: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] === v[1])), + not_equals: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] !== v[1])), + less_than: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] < v[1])), + less_equal: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] <= v[1])), + greater_than: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] > v[1])), + greater_equal: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] >= v[1])), +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_comparison.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_comparison.spec.ts index 1f693da5fd..5bb6767b01 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_comparison.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_comparison.spec.ts @@ -4,32 +4,14 @@ Execution Tests for the u32 comparison expressions import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { u32, bool, TypeBool, TypeU32 } from '../../../../util/conversion.js'; -import { vectorU32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, Case, run } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { allInputSources, run } from '../expression.js'; import { binary } from './binary.js'; +import { d } from './u32_comparison.cache.js'; export const g = makeTestGroup(GPUTest); -/** - * @returns a test case for the provided left hand & right hand values and - * expected boolean result. - */ -function makeCase(lhs: number, rhs: number, expected_answer: boolean): Case { - return { input: [u32(lhs), u32(rhs)], expected: bool(expected_answer) }; -} - -export const d = makeCaseCache('binary/u32_comparison', { - equals: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] === v[1])), - not_equals: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] !== v[1])), - less_than: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] < v[1])), - less_equal: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] <= v[1])), - greater_than: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] > v[1])), - greater_equal: () => vectorU32Range(2).map(v => makeCase(v[0], v[1], v[0] >= v[1])), -}); - g.test('equals') .specURL('https://www.w3.org/TR/WGSL/#comparison-expr') .desc( @@ -42,7 +24,7 @@ Expression: x == y ) .fn(async t => { const cases = await d.get('equals'); - await run(t, binary('=='), [TypeU32, TypeU32], TypeBool, t.params, cases); + await run(t, binary('=='), [Type.u32, Type.u32], Type.bool, t.params, cases); }); g.test('not_equals') @@ -57,7 +39,7 @@ Expression: x != y ) .fn(async t => { const cases = await d.get('not_equals'); - await run(t, binary('!='), [TypeU32, TypeU32], TypeBool, t.params, cases); + await run(t, binary('!='), [Type.u32, Type.u32], Type.bool, t.params, cases); }); g.test('less_than') @@ -72,7 +54,7 @@ Expression: x < y ) .fn(async t => { const cases = await d.get('less_than'); - await run(t, binary('<'), [TypeU32, TypeU32], TypeBool, t.params, cases); + await run(t, binary('<'), [Type.u32, Type.u32], Type.bool, t.params, cases); }); g.test('less_equals') @@ -87,7 +69,7 @@ Expression: x <= y ) .fn(async t => { const cases = await d.get('less_equal'); - await run(t, binary('<='), [TypeU32, TypeU32], TypeBool, t.params, cases); + await run(t, binary('<='), [Type.u32, Type.u32], Type.bool, t.params, cases); }); g.test('greater_than') @@ -102,7 +84,7 @@ Expression: x > y ) .fn(async t => { const cases = await d.get('greater_than'); - await run(t, binary('>'), [TypeU32, TypeU32], TypeBool, t.params, cases); + await run(t, binary('>'), [Type.u32, Type.u32], Type.bool, t.params, cases); }); g.test('greater_equals') @@ -117,5 +99,5 @@ Expression: x >= y ) .fn(async t => { const cases = await d.get('greater_equal'); - await run(t, binary('>='), [TypeU32, TypeU32], TypeBool, t.params, cases); + await run(t, binary('>='), [Type.u32, Type.u32], Type.bool, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.cache.ts new file mode 100644 index 0000000000..9634ae8f34 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.cache.ts @@ -0,0 +1,26 @@ +import { abstractInt } from '../../../../../util/conversion.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { absBigInt, fullI64Range } from '../../../../../util/math.js'; +import { CaseListBuilder, makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract_float|abstract_int] +const cases: Record<string, CaseListBuilder> = { + ...(['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait === 'abstract' ? 'abstract_float' : trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + 'unfiltered', + FP[trait].absInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}), + abstract_int: () => { + return fullI64Range().map(e => { + return { input: abstractInt(e), expected: abstractInt(absBigInt(e)) }; + }); + }, +}; + +export const d = makeCaseCache('abs', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts index 05d5242f73..75d41ab163 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts @@ -1,14 +1,14 @@ export const description = ` Execution tests for the 'abs' builtin function -S is AbstractInt, i32, or u32 +S is abstract-int, i32, or u32 T is S or vecN<S> @const fn abs(e: T ) -> T The absolute value of e. Component-wise when T is a vector. If e is a signed integral scalar type and evaluates to the largest negative value, then the result is e. If e is an unsigned integral type, then the result is e. -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn abs(e: T ) -> T Returns the absolute value of e (e.g. e with a positive sign bit). @@ -18,47 +18,26 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; import { kBit } from '../../../../../util/constants.js'; -import { - i32Bits, - TypeF32, - TypeF16, - TypeI32, - TypeU32, - u32Bits, - TypeAbstractFloat, -} from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range, fullF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type, i32Bits, u32Bits } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { d } from './abs.cache.js'; +import { abstractFloatBuiltin, abstractIntBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('abs', { - f32: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.absInterval); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'unfiltered', FP.f16.absInterval); - }, - abstract: () => { - return FP.abstract.generateScalarToIntervalCases( - fullF64Range(), - 'unfiltered', - FP.abstract.absInterval - ); - }, -}); - g.test('abstract_int') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') .desc(`abstract int tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_int'); + await run(t, abstractIntBuiltin('abs'), [Type.abstractInt], Type.abstractInt, t.params, cases); + }); g.test('u32') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') @@ -67,7 +46,7 @@ g.test('u32') u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - await run(t, builtin('abs'), [TypeU32], TypeU32, t.params, [ + await run(t, builtin('abs'), [Type.u32], Type.u32, t.params, [ // Min and Max u32 { input: u32Bits(kBit.u32.min), expected: u32Bits(kBit.u32.min) }, { input: u32Bits(kBit.u32.max), expected: u32Bits(kBit.u32.max) }, @@ -114,7 +93,7 @@ g.test('i32') u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - await run(t, builtin('abs'), [TypeI32], TypeI32, t.params, [ + await run(t, builtin('abs'), [Type.i32], Type.i32, t.params, [ // Min and max i32 // If e evaluates to the largest negative value, then the result is e. { input: i32Bits(kBit.i32.negative.min), expected: i32Bits(kBit.i32.negative.min) }, @@ -166,8 +145,15 @@ g.test('abstract_float') .combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - const cases = await d.get('abstract'); - await run(t, abstractBuiltin('abs'), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases); + const cases = await d.get('abstract_float'); + await run( + t, + abstractFloatBuiltin('abs'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); }); g.test('f32') @@ -178,7 +164,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('abs'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('abs'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -192,5 +178,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('abs'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('abs'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.cache.ts new file mode 100644 index 0000000000..466618b6db --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.cache.ts @@ -0,0 +1,24 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + [...linearRange(-1, 1, 100), ...FP[trait].scalarRange()], // acos is defined on [-1, 1] + nonConst ? 'unfiltered' : 'finite', + // acos has an ulp or inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].acosInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('acos', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts index 5755c07905..46089d8576 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'acos' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn acos(e: T ) -> T Returns the arc cosine of e. Component-wise when T is a vector. @@ -9,48 +9,33 @@ Returns the arc cosine of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { linearRange, fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { d } from './acos.cache.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -const f32_inputs = [ - ...linearRange(-1, 1, 100), // acos is defined on [-1, 1] - ...fullF32Range(), -]; - -const f16_inputs = [ - ...linearRange(-1, 1, 100), // acos is defined on [-1, 1] - ...fullF16Range(), -]; - -export const d = makeCaseCache('acos', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.acosInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.acosInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.acosInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.acosInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('acos'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -60,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('acos'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('acos'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -74,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('acos'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('acos'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.cache.ts new file mode 100644 index 0000000000..587131140d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.cache.ts @@ -0,0 +1,24 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + [...biasedRange(1, 2, 100), ...FP[trait].scalarRange()], // x near 1 can be problematic to implement + nonConst ? 'unfiltered' : 'finite', + // acosh has an inherited accuracy, so is only expected to be as accurate as f32 + ...FP[trait !== 'abstract' ? trait : 'f32'].acoshIntervals + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('acosh', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts index cc78ce3eee..531a2479c6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'acosh' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn acosh(e: T ) -> T Returns the hyperbolic arc cosine of e. The result is 0 when e < 1. @@ -13,47 +13,33 @@ Note: The result is not mathematically meaningful when e < 1. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { biasedRange, fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { d } from './acosh.cache.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -const f32_inputs = [ - ...biasedRange(1, 2, 100), // x near 1 can be problematic to implement - ...fullF32Range(), -]; -const f16_inputs = [ - ...biasedRange(1, 2, 100), // x near 1 can be problematic to implement - ...fullF16Range(), -]; - -export const d = makeCaseCache('acosh', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', ...FP.f32.acoshIntervals); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', ...FP.f32.acoshIntervals); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', ...FP.f16.acoshIntervals); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', ...FP.f16.acoshIntervals); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('acosh'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -63,7 +49,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('acosh'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('acosh'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -77,5 +63,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('acosh'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('acosh'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts index 9a2938c1d5..74e072703d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts @@ -10,15 +10,7 @@ Returns true if each component of e is true if e is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - False, - True, - TypeBool, - TypeVec, - vec2, - vec3, - vec4, -} from '../../../../../util/conversion.js'; +import { False, True, Type, vec2, vec3, vec4 } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -36,14 +28,14 @@ g.test('bool') .fn(async t => { const overloads = { scalar: { - type: TypeBool, + type: Type.bool, cases: [ { input: False, expected: False }, { input: True, expected: True }, ], }, vec2: { - type: TypeVec(2, TypeBool), + type: Type.vec(2, Type.bool), cases: [ { input: vec2(False, False), expected: False }, { input: vec2(True, False), expected: False }, @@ -52,7 +44,7 @@ g.test('bool') ], }, vec3: { - type: TypeVec(3, TypeBool), + type: Type.vec(3, Type.bool), cases: [ { input: vec3(False, False, False), expected: False }, { input: vec3(True, False, False), expected: False }, @@ -65,7 +57,7 @@ g.test('bool') ], }, vec4: { - type: TypeVec(4, TypeBool), + type: Type.vec(4, Type.bool), cases: [ { input: vec4(False, False, False, False), expected: False }, { input: vec4(False, True, False, False), expected: False }, @@ -88,5 +80,5 @@ g.test('bool') }; const overload = overloads[t.params.overload]; - await run(t, builtin('all'), [overload.type], TypeBool, t.params, overload.cases); + await run(t, builtin('all'), [overload.type], Type.bool, t.params, overload.cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts index 19ed7d186f..43c599e2aa 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts @@ -10,15 +10,7 @@ Returns true if any component of e is true if e is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - False, - True, - TypeBool, - TypeVec, - vec2, - vec3, - vec4, -} from '../../../../../util/conversion.js'; +import { False, True, Type, vec2, vec3, vec4 } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -36,14 +28,14 @@ g.test('bool') .fn(async t => { const overloads = { scalar: { - type: TypeBool, + type: Type.bool, cases: [ { input: False, expected: False }, { input: True, expected: True }, ], }, vec2: { - type: TypeVec(2, TypeBool), + type: Type.vec(2, Type.bool), cases: [ { input: vec2(False, False), expected: False }, { input: vec2(True, False), expected: True }, @@ -52,7 +44,7 @@ g.test('bool') ], }, vec3: { - type: TypeVec(3, TypeBool), + type: Type.vec(3, Type.bool), cases: [ { input: vec3(False, False, False), expected: False }, { input: vec3(True, False, False), expected: True }, @@ -65,7 +57,7 @@ g.test('bool') ], }, vec4: { - type: TypeVec(4, TypeBool), + type: Type.vec(4, Type.bool), cases: [ { input: vec4(False, False, False, False), expected: False }, { input: vec4(False, True, False, False), expected: True }, @@ -88,5 +80,5 @@ g.test('bool') }; const overload = overloads[t.params.overload]; - await run(t, builtin('any'), [overload.type], TypeBool, t.params, overload.cases); + await run(t, builtin('any'), [overload.type], Type.bool, t.params, overload.cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.cache.ts new file mode 100644 index 0000000000..d9e6280e0d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.cache.ts @@ -0,0 +1,24 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + [...linearRange(-1, 1, 100), ...FP[trait].scalarRange()], // asin is defined on [-1, 1] + nonConst ? 'unfiltered' : 'finite', + // asin has an ulp or inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].asinInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('asin', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts index 8d18ebb303..f368c3838c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'asin' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn asin(e: T ) -> T Returns the arc sine of e. Component-wise when T is a vector. @@ -9,48 +9,33 @@ Returns the arc sine of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { linearRange, fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { d } from './asin.cache.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -const f32_inputs = [ - ...linearRange(-1, 1, 100), // asin is defined on [-1, 1] - ...fullF32Range(), -]; - -const f16_inputs = [ - ...linearRange(-1, 1, 100), // asin is defined on [-1, 1] - ...fullF16Range(), -]; - -export const d = makeCaseCache('asin', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.asinInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.asinInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.asinInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.asinInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('asin'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -60,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('asin'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('asin'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -74,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('asin'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('asin'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.cache.ts new file mode 100644 index 0000000000..4ee66f1ea6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.cache.ts @@ -0,0 +1,18 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + 'unfiltered', + // asinh has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].asinhInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('asinh', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts index 9a8384e090..60867a9bce 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'sinh' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn asinh(e: T ) -> T Returns the hyperbolic arc sine of e. @@ -12,32 +12,33 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { d } from './asinh.cache.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('asinh', { - f32: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.asinhInterval); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'unfiltered', FP.f16.asinhInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float test`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('asinh'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -47,7 +48,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('asinh'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('asinh'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -61,5 +62,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('asinh'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('asinh'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.cache.ts new file mode 100644 index 0000000000..e39e40448e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.cache.ts @@ -0,0 +1,25 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +const known_values = [-Math.sqrt(3), -1, -1 / Math.sqrt(3), 0, 1, 1 / Math.sqrt(3), Math.sqrt(3)]; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + [...known_values, ...FP[trait].scalarRange()], + nonConst ? 'unfiltered' : 'finite', + // atan has an ulp accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].atanInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('atan', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts index 3d0d3e6725..824a7cd0b5 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'atan' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn atan(e: T ) -> T Returns the arc tangent of e. Component-wise when T is a vector. @@ -10,43 +10,33 @@ Returns the arc tangent of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { d } from './atan.cache.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -const known_values = [-Math.sqrt(3), -1, -1 / Math.sqrt(3), 0, 1, 1 / Math.sqrt(3), Math.sqrt(3)]; - -const f32_inputs = [...known_values, ...fullF32Range()]; -const f16_inputs = [...known_values, ...fullF16Range()]; - -export const d = makeCaseCache('atan', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.atanInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.atanInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.atanInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.atanInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('atan'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -62,7 +52,7 @@ TODO(#792): Decide what the ground-truth is for these tests. [1] ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('atan'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('atan'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -76,5 +66,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('atan'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('atan'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.cache.ts new file mode 100644 index 0000000000..be25c2dffb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.cache.ts @@ -0,0 +1,35 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + // Using sparse range since there are N^2 cases being generated, and also including extra values + // around 0, where there is a discontinuity that implementations may behave badly at. + const numeric_range = [ + ...FP[trait].sparseScalarRange(), + ...linearRange( + FP[trait].constants().negative.max, + FP[trait].constants().positive.min, + 10 + ), + ]; + return FP[trait].generateScalarPairToIntervalCases( + numeric_range, + numeric_range, + nonConst ? 'unfiltered' : 'finite', + // atan2 has an ulp accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].atan2Interval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('atan2', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts index fbace73dd2..067d1fdc66 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'atan2' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn atan2(e1: T ,e2: T ) -> T Returns the arc tangent of e1 over e2. Component-wise when T is a vector. @@ -9,47 +9,33 @@ Returns the arc tangent of e1 over e2. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { linearRange, sparseF32Range, sparseF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { d } from './atan2.cache.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -const cases = (['f32', 'f16'] as const) - .flatMap(kind => - ([true, false] as const).map(nonConst => ({ - [`${kind}_${nonConst ? 'non_const' : 'const'}`]: () => { - const fp = FP[kind]; - // Using sparse range since there are N^2 cases being generated, and also including extra values - // around 0, where there is a discontinuity that implementations may behave badly at. - const numeric_range = [ - ...(kind === 'f32' ? sparseF32Range() : sparseF16Range()), - ...linearRange(fp.constants().negative.max, fp.constants().positive.min, 10), - ]; - return fp.generateScalarPairToIntervalCases( - numeric_range, - numeric_range, - nonConst ? 'unfiltered' : 'finite', - fp.atan2Interval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('atan2', cases); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get(`abstract_const`); + await run( + t, + abstractFloatBuiltin('atan2'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -65,7 +51,7 @@ TODO(#792): Decide what the ground-truth is for these tests. [1] ) .fn(async t => { const cases = await d.get(`f32_${t.params.inputSource === 'const' ? 'const' : 'non_const'}`); - await run(t, builtin('atan2'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('atan2'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -79,5 +65,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(`f16_${t.params.inputSource === 'const' ? 'const' : 'non_const'}`); - await run(t, builtin('atan2'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('atan2'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.cache.ts new file mode 100644 index 0000000000..bcd000bf61 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.cache.ts @@ -0,0 +1,32 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + [ + // discontinuity at x = -1 + ...biasedRange(FP[trait].constants().negative.less_than_one, -0.9, 20), + -1, + // discontinuity at x = 1 + ...biasedRange(FP[trait].constants().positive.less_than_one, 0.9, 20), + 1, + ...FP[trait].scalarRange(), + ], + nonConst ? 'unfiltered' : 'finite', + // atanh has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].atanhInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('atanh', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts index 90f322a7ea..644efafd2f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'atanh' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn atanh(e: T ) -> T Returns the hyperbolic arc tangent of e. The result is 0 when abs(e) ≥ 1. @@ -12,54 +12,33 @@ Note: The result is not mathematically meaningful when abs(e) >= 1. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { biasedRange, fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { d } from './atanh.cache.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); -const f32_inputs = [ - ...biasedRange(kValue.f32.negative.less_than_one, -0.9, 20), // discontinuity at x = -1 - -1, - ...biasedRange(kValue.f32.positive.less_than_one, 0.9, 20), // discontinuity at x = 1 - 1, - ...fullF32Range(), -]; -const f16_inputs = [ - ...biasedRange(kValue.f16.negative.less_than_one, -0.9, 20), // discontinuity at x = -1 - -1, - ...biasedRange(kValue.f16.positive.less_than_one, 0.9, 20), // discontinuity at x = 1 - 1, - ...fullF16Range(), -]; - -export const d = makeCaseCache('atanh', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.atanhInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.atanhInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.atanhInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.atanhInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('atanh'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -69,7 +48,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('atanh'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('atanh'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -83,5 +62,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('atanh'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('atanh'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAdd.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAdd.spec.ts index 37d3ce5292..187a555449 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAdd.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAdd.spec.ts @@ -35,7 +35,7 @@ fn atomicAdd(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -72,7 +72,7 @@ fn atomicAdd(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { // Allocate one extra element to ensure it doesn't get modified diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAnd.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAnd.spec.ts index ed5cfa84a3..ad05bd851d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAnd.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicAnd.spec.ts @@ -38,7 +38,7 @@ fn atomicAnd(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -91,7 +91,7 @@ fn atomicAnd(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicCompareExchangeWeak.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicCompareExchangeWeak.spec.ts index 2556bb744b..79e0597af6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicCompareExchangeWeak.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicCompareExchangeWeak.spec.ts @@ -48,7 +48,7 @@ struct __atomic_compare_exchange_result<T> { .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -187,7 +187,7 @@ struct __atomic_compare_exchange_result<T> { .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize; @@ -333,12 +333,17 @@ struct __atomic_compare_exchange_result<T> { .params(u => u .combine('workgroupSize', onlyWorkgroupSizes) // - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize; const scalarType = t.params.scalarType; + t.skipIf( + numInvocations > t.device.limits.maxComputeWorkgroupSizeX, + `${numInvocations} > maxComputeWorkgroupSizeX(${t.device.limits.maxComputeWorkgroupSizeX})` + ); + // Number of times each workgroup attempts to exchange the same value to the same memory address const numWrites = 4; @@ -550,12 +555,17 @@ struct __atomic_compare_exchange_result<T> { .params(u => u .combine('workgroupSize', onlyWorkgroupSizes) // - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize; const scalarType = t.params.scalarType; + t.skipIf( + numInvocations > t.device.limits.maxComputeWorkgroupSizeX, + `${numInvocations} > maxComputeWorkgroupSizeX(${t.device.limits.maxComputeWorkgroupSizeX})` + ); + // Number of times each workgroup attempts to exchange the same value to the same memory address const numWrites = 4; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicExchange.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicExchange.spec.ts index 540ac16b07..00b6ddb7e3 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicExchange.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicExchange.spec.ts @@ -26,7 +26,7 @@ fn atomicExchange(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -125,7 +125,7 @@ fn atomicLoad(atomic_ptr: ptr<AS, atomic<T>, read_write>) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize; @@ -236,7 +236,7 @@ fn atomicExchange(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -350,7 +350,7 @@ fn atomicLoad(atomic_ptr: ptr<AS, atomic<T>, read_write>) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicLoad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicLoad.spec.ts index 2aac7bb9b9..23ca127cae 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicLoad.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicLoad.spec.ts @@ -26,7 +26,7 @@ fn atomicLoad(atomic_ptr: ptr<AS, atomic<T>, read_write>) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -117,7 +117,7 @@ fn atomicLoad(atomic_ptr: ptr<AS, atomic<T>, read_write>) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMax.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMax.spec.ts index 066d673018..86bb8b460d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMax.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMax.spec.ts @@ -35,7 +35,7 @@ fn atomicMax(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -72,7 +72,7 @@ fn atomicMax(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { // Allocate one extra element to ensure it doesn't get modified diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMin.spec.ts index ad880c4182..d9a42d15ee 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMin.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicMin.spec.ts @@ -35,7 +35,7 @@ fn atomicMin(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { // Allocate one extra element to ensure it doesn't get modified @@ -71,7 +71,7 @@ fn atomicMin(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { // Allocate one extra element to ensure it doesn't get modified diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicOr.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicOr.spec.ts index 3892d41b38..8ddba24385 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicOr.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicOr.spec.ts @@ -38,7 +38,7 @@ fn atomicOr(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -90,7 +90,7 @@ fn atomicOr(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicStore.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicStore.spec.ts index 18ff72975d..4d226e312b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicStore.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicStore.spec.ts @@ -32,7 +32,7 @@ fn atomicStore(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -72,7 +72,7 @@ fn atomicStore(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize; @@ -117,7 +117,7 @@ one of the values written. .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -210,7 +210,7 @@ one of the values written. .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(async t => { const numInvocations = t.params.workgroupSize; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicSub.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicSub.spec.ts index 6cea190299..8fdced1df2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicSub.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicSub.spec.ts @@ -35,7 +35,7 @@ fn atomicSub(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -72,7 +72,7 @@ fn atomicSub(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T u .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { // Allocate one extra element to ensure it doesn't get modified diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicXor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicXor.spec.ts index 99192fd9fe..2240043590 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicXor.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/atomicXor.spec.ts @@ -38,7 +38,7 @@ fn atomicXor(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize * t.params.dispatchSize; @@ -91,7 +91,7 @@ fn atomicXor(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T .combine('workgroupSize', workgroupSizes) .combine('dispatchSize', dispatchSizes) .combine('mapId', keysOf(kMapId)) - .combine('scalarType', ['u32', 'i32']) + .combine('scalarType', ['u32', 'i32'] as const) ) .fn(t => { const numInvocations = t.params.workgroupSize; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/harness.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/harness.ts index ed02467f80..e12cf537cd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/harness.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomics/harness.ts @@ -25,15 +25,14 @@ export const kMapId = { }, }; -export function typedArrayCtor(scalarType: string): TypedArrayBufferViewConstructor { +export function typedArrayCtor( + scalarType: 'u32' | 'i32' +): TypedArrayBufferViewConstructor<Uint32Array | Int32Array> { switch (scalarType) { case 'u32': return Uint32Array; case 'i32': return Int32Array; - default: - assert(false, 'Atomic variables can only by u32 or i32'); - return Uint8Array; } } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/bitcast.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/bitcast.cache.ts new file mode 100644 index 0000000000..75dfea0086 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/bitcast.cache.ts @@ -0,0 +1,837 @@ +import { assert } from '../../../../../../common/util/util.js'; +import { Comparator, alwaysPass, anyOf } from '../../../../../util/compare.js'; +import { kBit, kValue } from '../../../../../util/constants.js'; +import { + ScalarValue, + VectorValue, + f16, + f32, + i32, + toVector, + u32, + abstractFloat, + abstractInt, +} from '../../../../../util/conversion.js'; +import { FP, FPInterval } from '../../../../../util/floating_point.js'; +import { + cartesianProduct, + fullI32Range, + fullU32Range, + isFiniteF16, + isFiniteF32, + isSubnormalNumberF16, + isSubnormalNumberF32, + linearRange, + scalarF16Range, + scalarF32Range, +} from '../../../../../util/math.js'; +import { + reinterpretF16AsU16, + reinterpretF32AsI32, + reinterpretF32AsU32, + reinterpretI32AsF32, + reinterpretI32AsU32, + reinterpretU16AsF16, + reinterpretU32AsF32, + reinterpretU32AsI32, +} from '../../../../../util/reinterpret.js'; +import { makeCaseCache } from '../../case_cache.js'; + +const numNaNs = 11; +const f32InfAndNaNInU32: number[] = [ + // Cover NaNs evenly in integer space. + // The positive NaN with the lowest integer representation is the integer + // for infinity, plus one. + // The positive NaN with the highest integer representation is i32.max (!) + ...linearRange(kBit.f32.positive.infinity + 1, kBit.i32.positive.max, numNaNs), + // The negative NaN with the lowest integer representation is the integer + // for negative infinity, plus one. + // The negative NaN with the highest integer representation is u32.max (!) + ...linearRange(kBit.f32.negative.infinity + 1, kBit.u32.max, numNaNs), + kBit.f32.positive.infinity, + kBit.f32.negative.infinity, +]; +const f32InfAndNaNInF32 = f32InfAndNaNInU32.map(u => reinterpretU32AsF32(u)); +const f32InfAndNaNInI32 = f32InfAndNaNInU32.map(u => reinterpretU32AsI32(u)); + +const f32ZerosInU32 = [0, kBit.f32.negative.zero]; +const f32ZerosInF32 = f32ZerosInU32.map(u => reinterpretU32AsF32(u)); +const f32ZerosInI32 = f32ZerosInU32.map(u => reinterpretU32AsI32(u)); +const f32ZerosInterval: FPInterval = new FPInterval('f32', -0.0, 0.0); + +// f32FiniteRange is a list of finite f32s. fullF32Range() already +// has +0, we only need to add -0. +const f32FiniteRange: number[] = [...scalarF32Range(), kValue.f32.negative.zero]; +const f32RangeWithInfAndNaN: number[] = [...f32FiniteRange, ...f32InfAndNaNInF32]; + +// Type.f16 values, finite, Inf/NaN, and zeros. Represented in float and u16. +const f16FiniteInF16: number[] = [...scalarF16Range(), kValue.f16.negative.zero]; +const f16FiniteInU16: number[] = f16FiniteInF16.map(u => reinterpretF16AsU16(u)); + +const f16InfAndNaNInU16: number[] = [ + // Cover NaNs evenly in integer space. + // The positive NaN with the lowest integer representation is the integer + // for infinity, plus one. + // The positive NaN with the highest integer representation is u16 0x7fff i.e. 32767. + ...linearRange(kBit.f16.positive.infinity + 1, 32767, numNaNs).map(v => Math.ceil(v)), + // The negative NaN with the lowest integer representation is the integer + // for negative infinity, plus one. + // The negative NaN with the highest integer representation is u16 0xffff i.e. 65535 + ...linearRange(kBit.f16.negative.infinity + 1, 65535, numNaNs).map(v => Math.floor(v)), + kBit.f16.positive.infinity, + kBit.f16.negative.infinity, +]; +const f16InfAndNaNInF16 = f16InfAndNaNInU16.map(u => reinterpretU16AsF16(u)); + +const f16ZerosInU16 = [kBit.f16.negative.zero, 0]; + +// f16 interval that match +/-0.0. +const f16ZerosInterval: FPInterval = new FPInterval('f16', -0.0, 0.0); + +/** + * @returns an u32 whose lower and higher 16bits are the two elements of the + * given array of two u16 respectively, in little-endian. + */ +function u16x2ToU32(u16x2: readonly number[]): number { + assert(u16x2.length === 2); + // Create a DataView with 4 bytes buffer. + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + // Enforce little-endian. + view.setUint16(0, u16x2[0], true); + view.setUint16(2, u16x2[1], true); + return view.getUint32(0, true); +} + +/** + * @returns an array of two u16, respectively the lower and higher 16bits of + * given u32 in little-endian. + */ +function u32ToU16x2(u32: number): number[] { + // Create a DataView with 4 bytes buffer. + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + // Enforce little-endian. + view.setUint32(0, u32, true); + return [view.getUint16(0, true), view.getUint16(2, true)]; +} + +/** + * @returns a vec2<f16> from an array of two u16, each reinterpreted as f16. + */ +function u16x2ToVec2F16(u16x2: number[]): VectorValue { + assert(u16x2.length === 2); + return toVector(u16x2.map(reinterpretU16AsF16), f16); +} + +/** + * @returns a vec4<f16> from an array of four u16, each reinterpreted as f16. + */ +function u16x4ToVec4F16(u16x4: number[]): VectorValue { + assert(u16x4.length === 4); + return toVector(u16x4.map(reinterpretU16AsF16), f16); +} + +/** + * @returns true if and only if a given u32 can bitcast to a vec2<f16> with all elements + * being finite f16 values. + */ +function canU32BitcastToFiniteVec2F16(u32: number): boolean { + return u32ToU16x2(u32) + .map(u16 => isFiniteF16(reinterpretU16AsF16(u16))) + .reduce((a, b) => a && b, true); +} + +/** + * @returns an array of N elements with the i-th element being an array of len elements + * [a_i, a_((i+1)%N), ..., a_((i+len-1)%N)], for the input array of N element [a_1, ... a_N] + * and the given len. For example, slidingSlice([1, 2, 3], 2) result in + * [[1, 2], [2, 3], [3, 1]]. + * This helper function is used for generating vector cases from scalar values array. + */ +function slidingSlice(input: number[], len: number) { + const result: number[][] = []; + for (let i = 0; i < input.length; i++) { + const sub: number[] = []; + for (let j = 0; j < len; j++) { + sub.push(input[(i + j) % input.length]); + } + result.push(sub); + } + return result; +} + +// vec2<f16> interesting (zeros, Inf, and NaN) values for testing cases. +// vec2<f16> values that has at least one Inf/NaN f16 element, reinterpreted as u32/i32. +const f16Vec2InfAndNaNInU32 = [ + ...cartesianProduct(f16InfAndNaNInU16, [...f16InfAndNaNInU16, ...f16FiniteInU16]), + ...cartesianProduct(f16FiniteInU16, f16InfAndNaNInU16), +].map(u16x2ToU32); +const f16Vec2InfAndNaNInI32 = f16Vec2InfAndNaNInU32.map(u => reinterpretU32AsI32(u)); +// vec2<f16> values with two f16 0.0 element, reinterpreted as u32/i32. +const f16Vec2ZerosInU32 = cartesianProduct(f16ZerosInU16, f16ZerosInU16).map(u16x2ToU32); +const f16Vec2ZerosInI32 = f16Vec2ZerosInU32.map(u => reinterpretU32AsI32(u)); + +// i32/u32/f32 range for bitcasting to vec2<f16> +// u32 values for bitcasting to vec2<f16> finite, Inf, and NaN. +const u32RangeForF16Vec2FiniteInfNaN: number[] = [ + ...fullU32Range(), + ...f16Vec2ZerosInU32, + ...f16Vec2InfAndNaNInU32, +]; +// u32 values for bitcasting to finite only vec2<f16>, used for constant evaluation. +const u32RangeForF16Vec2Finite: number[] = u32RangeForF16Vec2FiniteInfNaN.filter( + canU32BitcastToFiniteVec2F16 +); +// i32 values for bitcasting to vec2<f16> finite, zeros, Inf, and NaN. +const i32RangeForF16Vec2FiniteInfNaN: number[] = [ + ...fullI32Range(), + ...f16Vec2ZerosInI32, + ...f16Vec2InfAndNaNInI32, +]; +// i32 values for bitcasting to finite only vec2<f16>, used for constant evaluation. +const i32RangeForF16Vec2Finite: number[] = i32RangeForF16Vec2FiniteInfNaN.filter(u => + canU32BitcastToFiniteVec2F16(reinterpretI32AsU32(u)) +); +// f32 values with finite/Inf/NaN f32, for bitcasting to vec2<f16> finite, zeros, Inf, and NaN. +const f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN: number[] = [ + ...f32RangeWithInfAndNaN, + ...u32RangeForF16Vec2FiniteInfNaN.map(reinterpretU32AsF32), +]; +// Finite f32 values for bitcasting to finite only vec2<f16>, used for constant evaluation. +const f32FiniteRangeForF16Vec2Finite: number[] = f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN + .filter(isFiniteF32) + .filter(u => canU32BitcastToFiniteVec2F16(reinterpretF32AsU32(u))); + +// vec2<f16> cases for bitcasting to i32/u32/f32, by combining f16 values into pairs +const f16Vec2FiniteInU16x2 = slidingSlice(f16FiniteInU16, 2); +const f16Vec2FiniteInfNanInU16x2 = slidingSlice([...f16FiniteInU16, ...f16InfAndNaNInU16], 2); +// vec4<f16> cases for bitcasting to vec2<i32/u32/f32>, by combining f16 values 4-by-4 +const f16Vec2FiniteInU16x4 = slidingSlice(f16FiniteInU16, 4); +const f16Vec2FiniteInfNanInU16x4 = slidingSlice([...f16FiniteInU16, ...f16InfAndNaNInU16], 4); + +// alwaysPass comparator for i32/u32/f32 cases. For f32/f16 we also use unbound interval, which +// allow per-element unbounded expectation for vector. +const anyF32 = alwaysPass('any f32'); +const anyI32 = alwaysPass('any i32'); +const anyU32 = alwaysPass('any u32'); + +// Unbounded FPInterval +const f32UnboundedInterval = FP.f32.constants().unboundedInterval; +const f16UnboundedInterval = FP.f16.constants().unboundedInterval; + +// i32 and u32 cases for bitcasting to f32. +// i32 cases for bitcasting to f32 finite, zeros, Inf, and NaN. +const i32RangeForF32FiniteInfNaN: number[] = [ + ...fullI32Range(), + ...f32ZerosInI32, + ...f32InfAndNaNInI32, +]; +// i32 cases for bitcasting to f32 finite only. +const i32RangeForF32Finite: number[] = i32RangeForF32FiniteInfNaN.filter(i => + isFiniteF32(reinterpretI32AsF32(i)) +); +// u32 cases for bitcasting to f32 finite, zeros, Inf, and NaN. +const u32RangeForF32FiniteInfNaN: number[] = [ + ...fullU32Range(), + ...f32ZerosInU32, + ...f32InfAndNaNInU32, +]; +// u32 cases for bitcasting to f32 finite only. +const u32RangeForF32Finite: number[] = u32RangeForF32FiniteInfNaN.filter(u => + isFiniteF32(reinterpretU32AsF32(u)) +); + +/** + * @returns a Comparator for checking if a f32 value is a valid + * bitcast conversion from f32. + */ +function bitcastF32ToF32Comparator(f: number): Comparator { + if (!isFiniteF32(f)) return anyF32; + const acceptable: number[] = [f, ...(isSubnormalNumberF32(f) ? f32ZerosInF32 : [])]; + return anyOf(...acceptable.map(f32)); +} + +/** + * @returns a Comparator for checking if a u32 value is a valid + * bitcast conversion from f32. + */ +function bitcastF32ToU32Comparator(f: number): Comparator { + if (!isFiniteF32(f)) return anyU32; + const acceptable: number[] = [ + reinterpretF32AsU32(f), + ...(isSubnormalNumberF32(f) ? f32ZerosInU32 : []), + ]; + return anyOf(...acceptable.map(u32)); +} + +/** + * @returns a Comparator for checking if a i32 value is a valid + * bitcast conversion from f32. + */ +function bitcastF32ToI32Comparator(f: number): Comparator { + if (!isFiniteF32(f)) return anyI32; + const acceptable: number[] = [ + reinterpretF32AsI32(f), + ...(isSubnormalNumberF32(f) ? f32ZerosInI32 : []), + ]; + return anyOf(...acceptable.map(i32)); +} + +/** + * @returns a Comparator for checking if a f32 value is a valid + * bitcast conversion from i32. + */ +function bitcastI32ToF32Comparator(i: number): Comparator { + const f: number = reinterpretI32AsF32(i); + if (!isFiniteF32(f)) return anyI32; + // Positive or negative zero bit pattern map to any zero. + if (f32ZerosInI32.includes(i)) return anyOf(...f32ZerosInF32.map(f32)); + const acceptable: number[] = [f, ...(isSubnormalNumberF32(f) ? f32ZerosInF32 : [])]; + return anyOf(...acceptable.map(f32)); +} + +/** + * @returns a Comparator for checking if a f32 value is a valid + * bitcast conversion from u32. + */ +function bitcastU32ToF32Comparator(u: number): Comparator { + const f: number = reinterpretU32AsF32(u); + if (!isFiniteF32(f)) return anyU32; + // Positive or negative zero bit pattern map to any zero. + if (f32ZerosInU32.includes(u)) return anyOf(...f32ZerosInF32.map(f32)); + const acceptable: number[] = [f, ...(isSubnormalNumberF32(f) ? f32ZerosInF32 : [])]; + return anyOf(...acceptable.map(f32)); +} + +/** + * @returns an array of expected f16 FPInterval for the given bitcasted f16 value, which may be + * subnormal, Inf, or NaN. Test cases that bitcasted to vector of f16 use this function to get + * per-element expectation and build vector expectation using cartesianProduct. + */ +function generateF16ExpectationIntervals(bitcastedF16Value: number): FPInterval[] { + // If the bitcasted f16 value is inf or nan, the result is unbounded + if (!isFiniteF16(bitcastedF16Value)) { + return [f16UnboundedInterval]; + } + // If the casted f16 value is +/-0.0, the result can be one of both. Note that in JS -0.0 === 0.0. + if (bitcastedF16Value === 0.0) { + return [f16ZerosInterval]; + } + const exactInterval = FP.f16.toInterval(bitcastedF16Value); + // If the casted f16 value is subnormal, it also may be flushed to +/-0.0. + return [exactInterval, ...(isSubnormalNumberF16(bitcastedF16Value) ? [f16ZerosInterval] : [])]; +} + +/** + * @returns a Comparator for checking if a f16 value is a valid + * bitcast conversion from f16. + */ +function bitcastF16ToF16Comparator(f: number): Comparator { + if (!isFiniteF16(f)) return anyOf(f16UnboundedInterval); + return anyOf(...generateF16ExpectationIntervals(f)); +} + +/** + * @returns a Comparator for checking if a vec2<f16> is a valid bitcast + * conversion from u32. + */ +function bitcastU32ToVec2F16Comparator(u: number): Comparator { + const bitcastedVec2F16InU16x2 = u32ToU16x2(u).map(reinterpretU16AsF16); + // Generate expection for vec2 f16 result, by generating expected intervals for each elements and + // then do cartesian product. + const expectedIntervalsCombination = cartesianProduct( + ...bitcastedVec2F16InU16x2.map(generateF16ExpectationIntervals) + ); + return anyOf(...expectedIntervalsCombination); +} + +/** + * @returns a Comparator for checking if a vec2<f16> value is a valid + * bitcast conversion from i32. + */ +function bitcastI32ToVec2F16Comparator(i: number): Comparator { + const bitcastedVec2F16InU16x2 = u32ToU16x2(reinterpretI32AsU32(i)).map(reinterpretU16AsF16); + // Generate expection for vec2 f16 result, by generating expected intervals for each elements and + // then do cartesian product. + const expectedIntervalsCombination = cartesianProduct( + ...bitcastedVec2F16InU16x2.map(generateF16ExpectationIntervals) + ); + return anyOf(...expectedIntervalsCombination); +} + +/** + * @returns a Comparator for checking if a vec2<f16> value is a valid + * bitcast conversion from f32. + */ +function bitcastF32ToVec2F16Comparator(f: number): Comparator { + // If input f32 is not finite, it can be evaluated to any value and thus any result f16 vec2 is + // possible. + if (!isFiniteF32(f)) { + return anyOf([f16UnboundedInterval, f16UnboundedInterval]); + } + const bitcastedVec2F16InU16x2 = u32ToU16x2(reinterpretF32AsU32(f)).map(reinterpretU16AsF16); + // Generate expection for vec2 f16 result, by generating expected intervals for each elements and + // then do cartesian product. + const expectedIntervalsCombination = cartesianProduct( + ...bitcastedVec2F16InU16x2.map(generateF16ExpectationIntervals) + ); + return anyOf(...expectedIntervalsCombination); +} + +/** + * @returns a Comparator for checking if a vec4<f16> is a valid + * bitcast conversion from vec2<u32>. + */ +function bitcastVec2U32ToVec4F16Comparator(u32x2: number[]): Comparator { + assert(u32x2.length === 2); + const bitcastedVec4F16InU16x4 = u32x2.flatMap(u32ToU16x2).map(reinterpretU16AsF16); + // Generate expection for vec4 f16 result, by generating expected intervals for each elements and + // then do cartesian product. + const expectedIntervalsCombination = cartesianProduct( + ...bitcastedVec4F16InU16x4.map(generateF16ExpectationIntervals) + ); + return anyOf(...expectedIntervalsCombination); +} + +/** + * @returns a Comparator for checking if a vec4<f16> is a valid + * bitcast conversion from vec2<i32>. + */ +function bitcastVec2I32ToVec4F16Comparator(i32x2: number[]): Comparator { + assert(i32x2.length === 2); + const bitcastedVec4F16InU16x4 = i32x2 + .map(reinterpretI32AsU32) + .flatMap(u32ToU16x2) + .map(reinterpretU16AsF16); + // Generate expection for vec4 f16 result, by generating expected intervals for each elements and + // then do cartesian product. + const expectedIntervalsCombination = cartesianProduct( + ...bitcastedVec4F16InU16x4.map(generateF16ExpectationIntervals) + ); + return anyOf(...expectedIntervalsCombination); +} + +/** + * @returns a Comparator for checking if a vec4<f16> is a valid + * bitcast conversion from vec2<f32>. + */ +function bitcastVec2F32ToVec4F16Comparator(f32x2: number[]): Comparator { + assert(f32x2.length === 2); + const bitcastedVec4F16InU16x4 = f32x2 + .map(reinterpretF32AsU32) + .flatMap(u32ToU16x2) + .map(reinterpretU16AsF16); + // Generate expection for vec4 f16 result, by generating expected intervals for each elements and + // then do cartesian product. + const expectedIntervalsCombination = cartesianProduct( + ...bitcastedVec4F16InU16x4.map(generateF16ExpectationIntervals) + ); + return anyOf(...expectedIntervalsCombination); +} + +// Structure that store the expectations of a single 32bit scalar/element bitcasted from two f16. +interface ExpectionFor32BitsScalarFromF16x2 { + // possibleExpectations is Scalar array if the expectation is for i32/u32 and FPInterval array for + // f32. Note that if the expectation for i32/u32 is unbound, possibleExpectations is meaningless. + possibleExpectations: (ScalarValue | FPInterval)[]; + isUnbounded: boolean; +} + +/** + * @returns the array of possible 16bits, represented in u16, that bitcasted + * from a given finite f16 represented in u16, handling the possible subnormal + * flushing. Used to build up 32bits or larger results. + */ +function possibleBitsInU16FromFiniteF16InU16(f16InU16: number): number[] { + const h = reinterpretU16AsF16(f16InU16); + assert(isFiniteF16(h)); + return [f16InU16, ...(isSubnormalNumberF16(h) ? f16ZerosInU16 : [])]; +} + +/** + * @returns the expectation for a single 32bit scalar bitcasted from given pair of + * f16, result in ExpectionFor32BitsScalarFromF16x2. + */ +function possible32BitScalarIntervalsFromF16x2( + f16x2InU16x2: number[], + type: 'i32' | 'u32' | 'f32' +): ExpectionFor32BitsScalarFromF16x2 { + assert(f16x2InU16x2.length === 2); + let reinterpretFromU32: (x: number) => number; + let expectationsForValue: (x: number) => ScalarValue[] | FPInterval[]; + let unboundedExpectations: FPInterval[] | ScalarValue[]; + if (type === 'u32') { + reinterpretFromU32 = (x: number) => x; + expectationsForValue = x => [u32(x)]; + // Scalar expectation can not express "unbounded" for i32 and u32, so use 0 here as a + // placeholder, and the possibleExpectations should be ignored if the result is unbounded. + unboundedExpectations = [u32(0)]; + } else if (type === 'i32') { + reinterpretFromU32 = (x: number) => reinterpretU32AsI32(x); + expectationsForValue = x => [i32(x)]; + // Scalar expectation can not express "unbounded" for i32 and u32, so use 0 here as a + // placeholder, and the possibleExpectations should be ignored if the result is unbounded. + unboundedExpectations = [i32(0)]; + } else { + assert(type === 'f32'); + reinterpretFromU32 = (x: number) => reinterpretU32AsF32(x); + expectationsForValue = x => { + // Handle the possible Inf/NaN/zeros and subnormal cases for f32 result. + if (!isFiniteF32(x)) { + return [f32UnboundedInterval]; + } + // If the casted f16 value is +/-0.0, the result can be one of both. Note that in JS -0.0 === 0.0. + if (x === 0.0) { + return [f32ZerosInterval]; + } + const exactInterval = FP.f32.toInterval(x); + // If the casted f16 value is subnormal, it also may be flushed to +/-0.0. + return [exactInterval, ...(isSubnormalNumberF32(x) ? [f32ZerosInterval] : [])]; + }; + unboundedExpectations = [f32UnboundedInterval]; + } + // Return unbounded expection if f16 Inf/NaN occurs + if ( + !isFiniteF16(reinterpretU16AsF16(f16x2InU16x2[0])) || + !isFiniteF16(reinterpretU16AsF16(f16x2InU16x2[1])) + ) { + return { possibleExpectations: unboundedExpectations, isUnbounded: true }; + } + const possibleU16Bits = f16x2InU16x2.map(possibleBitsInU16FromFiniteF16InU16); + const possibleExpectations = cartesianProduct(...possibleU16Bits).flatMap< + ScalarValue | FPInterval + >((possibleBitsU16x2: readonly number[]) => { + assert(possibleBitsU16x2.length === 2); + return expectationsForValue(reinterpretFromU32(u16x2ToU32(possibleBitsU16x2))); + }); + return { possibleExpectations, isUnbounded: false }; +} + +/** + * @returns a Comparator for checking if a u32 value is a valid + * bitcast conversion from vec2 f16. + */ +function bitcastVec2F16ToU32Comparator(vec2F16InU16x2: number[]): Comparator { + assert(vec2F16InU16x2.length === 2); + const expectations = possible32BitScalarIntervalsFromF16x2(vec2F16InU16x2, 'u32'); + // Return alwaysPass if result is expected unbounded. + if (expectations.isUnbounded) { + return anyU32; + } + return anyOf(...expectations.possibleExpectations); +} + +/** + * @returns a Comparator for checking if a i32 value is a valid + * bitcast conversion from vec2 f16. + */ +function bitcastVec2F16ToI32Comparator(vec2F16InU16x2: number[]): Comparator { + assert(vec2F16InU16x2.length === 2); + const expectations = possible32BitScalarIntervalsFromF16x2(vec2F16InU16x2, 'i32'); + // Return alwaysPass if result is expected unbounded. + if (expectations.isUnbounded) { + return anyI32; + } + return anyOf(...expectations.possibleExpectations); +} + +/** + * @returns a Comparator for checking if a i32 value is a valid + * bitcast conversion from vec2 f16. + */ +function bitcastVec2F16ToF32Comparator(vec2F16InU16x2: number[]): Comparator { + assert(vec2F16InU16x2.length === 2); + const expectations = possible32BitScalarIntervalsFromF16x2(vec2F16InU16x2, 'f32'); + // Return alwaysPass if result is expected unbounded. + if (expectations.isUnbounded) { + return anyF32; + } + return anyOf(...expectations.possibleExpectations); +} + +/** + * @returns a Comparator for checking if a vec2 u32 value is a valid + * bitcast conversion from vec4 f16. + */ +function bitcastVec4F16ToVec2U32Comparator(vec4F16InU16x4: number[]): Comparator { + assert(vec4F16InU16x4.length === 4); + const expectationsPerElement = [vec4F16InU16x4.slice(0, 2), vec4F16InU16x4.slice(2, 4)].map(e => + possible32BitScalarIntervalsFromF16x2(e, 'u32') + ); + // Return alwaysPass if any element is expected unbounded. Although it may be only one unbounded + // element in the result vector, currently we don't have a way to build a comparator that expect + // only one element of i32/u32 vector unbounded. + if (expectationsPerElement.map(e => e.isUnbounded).reduce((a, b) => a || b, false)) { + return alwaysPass('any vec2<u32>'); + } + return anyOf( + ...cartesianProduct(...expectationsPerElement.map(e => e.possibleExpectations)).map( + e => new VectorValue(e as ScalarValue[]) + ) + ); +} + +/** + * @returns a Comparator for checking if a vec2 i32 value is a valid + * bitcast conversion from vec4 f16. + */ +function bitcastVec4F16ToVec2I32Comparator(vec4F16InU16x4: number[]): Comparator { + assert(vec4F16InU16x4.length === 4); + const expectationsPerElement = [vec4F16InU16x4.slice(0, 2), vec4F16InU16x4.slice(2, 4)].map(e => + possible32BitScalarIntervalsFromF16x2(e, 'i32') + ); + // Return alwaysPass if any element is expected unbounded. Although it may be only one unbounded + // element in the result vector, currently we don't have a way to build a comparator that expect + // only one element of i32/u32 vector unbounded. + if (expectationsPerElement.map(e => e.isUnbounded).reduce((a, b) => a || b, false)) { + return alwaysPass('any vec2<i32>'); + } + return anyOf( + ...cartesianProduct(...expectationsPerElement.map(e => e.possibleExpectations)).map( + e => new VectorValue(e as ScalarValue[]) + ) + ); +} + +/** + * @returns a Comparator for checking if a vec2 f32 value is a valid + * bitcast conversion from vec4 f16. + */ +function bitcastVec4F16ToVec2F32Comparator(vec4F16InU16x4: number[]): Comparator { + assert(vec4F16InU16x4.length === 4); + const expectationsPerElement = [vec4F16InU16x4.slice(0, 2), vec4F16InU16x4.slice(2, 4)].map(e => + possible32BitScalarIntervalsFromF16x2(e, 'f32') + ); + return anyOf( + ...cartesianProduct(...expectationsPerElement.map(e => e.possibleExpectations)).map(e => [ + e[0] as FPInterval, + e[1] as FPInterval, + ]) + ); +} + +export const d = makeCaseCache('bitcast', { + // Identity Cases + i32_to_i32: () => fullI32Range().map(e => ({ input: i32(e), expected: i32(e) })), + u32_to_u32: () => fullU32Range().map(e => ({ input: u32(e), expected: u32(e) })), + f32_inf_nan_to_f32: () => + f32RangeWithInfAndNaN.map(e => ({ + input: f32(e), + expected: bitcastF32ToF32Comparator(e), + })), + f32_to_f32: () => + f32FiniteRange.map(e => ({ input: f32(e), expected: bitcastF32ToF32Comparator(e) })), + f16_inf_nan_to_f16: () => + [...f16FiniteInF16, ...f16InfAndNaNInF16].map(e => ({ + input: f16(e), + expected: bitcastF16ToF16Comparator(e), + })), + f16_to_f16: () => + f16FiniteInF16.map(e => ({ input: f16(e), expected: bitcastF16ToF16Comparator(e) })), + + // i32,u32,f32,Abstract to different i32,u32,f32 + i32_to_u32: () => fullI32Range().map(e => ({ input: i32(e), expected: u32(e) })), + i32_to_f32: () => + i32RangeForF32Finite.map(e => ({ + input: i32(e), + expected: bitcastI32ToF32Comparator(e), + })), + ai_to_i32: () => fullI32Range().map(e => ({ input: abstractInt(BigInt(e)), expected: i32(e) })), + ai_to_u32: () => fullU32Range().map(e => ({ input: abstractInt(BigInt(e)), expected: u32(e) })), + ai_to_f32: () => + // AbstractInt is converted to i32, because there is no explicit overload + i32RangeForF32Finite.map(e => ({ + input: abstractInt(BigInt(e)), + expected: bitcastI32ToF32Comparator(e), + })), + i32_to_f32_inf_nan: () => + i32RangeForF32FiniteInfNaN.map(e => ({ + input: i32(e), + expected: bitcastI32ToF32Comparator(e), + })), + u32_to_i32: () => fullU32Range().map(e => ({ input: u32(e), expected: i32(e) })), + u32_to_f32: () => + u32RangeForF32Finite.map(e => ({ + input: u32(e), + expected: bitcastU32ToF32Comparator(e), + })), + u32_to_f32_inf_nan: () => + u32RangeForF32FiniteInfNaN.map(e => ({ + input: u32(e), + expected: bitcastU32ToF32Comparator(e), + })), + f32_inf_nan_to_i32: () => + f32RangeWithInfAndNaN.map(e => ({ + input: f32(e), + expected: bitcastF32ToI32Comparator(e), + })), + f32_to_i32: () => + f32FiniteRange.map(e => ({ input: f32(e), expected: bitcastF32ToI32Comparator(e) })), + + f32_inf_nan_to_u32: () => + f32RangeWithInfAndNaN.map(e => ({ + input: f32(e), + expected: bitcastF32ToU32Comparator(e), + })), + f32_to_u32: () => + f32FiniteRange.map(e => ({ input: f32(e), expected: bitcastF32ToU32Comparator(e) })), + + // i32,u32,f32,AbstractInt to vec2<f16> + u32_to_vec2_f16_inf_nan: () => + u32RangeForF16Vec2FiniteInfNaN.map(e => ({ + input: u32(e), + expected: bitcastU32ToVec2F16Comparator(e), + })), + u32_to_vec2_f16: () => + u32RangeForF16Vec2Finite.map(e => ({ + input: u32(e), + expected: bitcastU32ToVec2F16Comparator(e), + })), + i32_to_vec2_f16_inf_nan: () => + i32RangeForF16Vec2FiniteInfNaN.map(e => ({ + input: i32(e), + expected: bitcastI32ToVec2F16Comparator(e), + })), + i32_to_vec2_f16: () => + i32RangeForF16Vec2Finite.map(e => ({ + input: i32(e), + expected: bitcastI32ToVec2F16Comparator(e), + })), + ai_to_vec2_f16: () => + // AbstractInt is converted to i32, because there is no explicit overload + i32RangeForF16Vec2Finite.map(e => ({ + input: abstractInt(BigInt(e)), + expected: bitcastI32ToVec2F16Comparator(e), + })), + f32_inf_nan_to_vec2_f16_inf_nan: () => + f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN.map(e => ({ + input: f32(e), + expected: bitcastF32ToVec2F16Comparator(e), + })), + f32_to_vec2_f16: () => + f32FiniteRangeForF16Vec2Finite.map(e => ({ + input: f32(e), + expected: bitcastF32ToVec2F16Comparator(e), + })), + af_to_vec2_f16: () => + f32FiniteRangeForF16Vec2Finite.map(e => ({ + input: abstractFloat(e), + expected: bitcastF32ToVec2F16Comparator(e), + })), + + // vec2<i32>, vec2<u32>, vec2<f32>, vec2<AbstractInt> to vec4<f16> + vec2_i32_to_vec4_f16_inf_nan: () => + slidingSlice(i32RangeForF16Vec2FiniteInfNaN, 2).map(e => ({ + input: toVector(e, i32), + expected: bitcastVec2I32ToVec4F16Comparator(e), + })), + vec2_i32_to_vec4_f16: () => + slidingSlice(i32RangeForF16Vec2Finite, 2).map(e => ({ + input: toVector(e, i32), + expected: bitcastVec2I32ToVec4F16Comparator(e), + })), + vec2_ai_to_vec4_f16: () => + // AbstractInt is converted to i32, because there is no explicit overload + slidingSlice(i32RangeForF16Vec2Finite, 2).map(e => ({ + input: toVector(e, (n: number) => abstractInt(BigInt(n))), + expected: bitcastVec2I32ToVec4F16Comparator(e), + })), + vec2_u32_to_vec4_f16_inf_nan: () => + slidingSlice(u32RangeForF16Vec2FiniteInfNaN, 2).map(e => ({ + input: toVector(e, u32), + expected: bitcastVec2U32ToVec4F16Comparator(e), + })), + vec2_u32_to_vec4_f16: () => + slidingSlice(u32RangeForF16Vec2Finite, 2).map(e => ({ + input: toVector(e, u32), + expected: bitcastVec2U32ToVec4F16Comparator(e), + })), + vec2_f32_inf_nan_to_vec4_f16_inf_nan: () => + slidingSlice(f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN, 2).map(e => ({ + input: toVector(e, f32), + expected: bitcastVec2F32ToVec4F16Comparator(e), + })), + vec2_f32_to_vec4_f16: () => + slidingSlice(f32FiniteRangeForF16Vec2Finite, 2).map(e => ({ + input: toVector(e, f32), + expected: bitcastVec2F32ToVec4F16Comparator(e), + })), + vec2_af_to_vec4_f16: () => + slidingSlice(f32FiniteRangeForF16Vec2Finite, 2).map(e => ({ + input: toVector(e, abstractFloat), + expected: bitcastVec2F32ToVec4F16Comparator(e), + })), + + // vec2<f16> to i32, u32, f32 + vec2_f16_to_u32: () => + f16Vec2FiniteInU16x2.map(e => ({ + input: u16x2ToVec2F16(e), + expected: bitcastVec2F16ToU32Comparator(e), + })), + vec2_f16_inf_nan_to_u32: () => + f16Vec2FiniteInfNanInU16x2.map(e => ({ + input: u16x2ToVec2F16(e), + expected: bitcastVec2F16ToU32Comparator(e), + })), + vec2_f16_to_i32: () => + f16Vec2FiniteInU16x2.map(e => ({ + input: u16x2ToVec2F16(e), + expected: bitcastVec2F16ToI32Comparator(e), + })), + vec2_f16_inf_nan_to_i32: () => + f16Vec2FiniteInfNanInU16x2.map(e => ({ + input: u16x2ToVec2F16(e), + expected: bitcastVec2F16ToI32Comparator(e), + })), + vec2_f16_to_f32_finite: () => + f16Vec2FiniteInU16x2 + .filter(u16x2 => isFiniteF32(reinterpretU32AsF32(u16x2ToU32(u16x2)))) + .map(e => ({ + input: u16x2ToVec2F16(e), + expected: bitcastVec2F16ToF32Comparator(e), + })), + vec2_f16_inf_nan_to_f32: () => + f16Vec2FiniteInfNanInU16x2.map(e => ({ + input: u16x2ToVec2F16(e), + expected: bitcastVec2F16ToF32Comparator(e), + })), + + // vec4<f16> to vec2 of i32, u32, f32 + vec4_f16_to_vec2_u32: () => + f16Vec2FiniteInU16x4.map(e => ({ + input: u16x4ToVec4F16(e), + expected: bitcastVec4F16ToVec2U32Comparator(e), + })), + vec4_f16_inf_nan_to_vec2_u32: () => + f16Vec2FiniteInfNanInU16x4.map(e => ({ + input: u16x4ToVec4F16(e), + expected: bitcastVec4F16ToVec2U32Comparator(e), + })), + vec4_f16_to_vec2_i32: () => + f16Vec2FiniteInU16x4.map(e => ({ + input: u16x4ToVec4F16(e), + expected: bitcastVec4F16ToVec2I32Comparator(e), + })), + vec4_f16_inf_nan_to_vec2_i32: () => + f16Vec2FiniteInfNanInU16x4.map(e => ({ + input: u16x4ToVec4F16(e), + expected: bitcastVec4F16ToVec2I32Comparator(e), + })), + vec4_f16_to_vec2_f32_finite: () => + f16Vec2FiniteInU16x4 + .filter( + u16x4 => + isFiniteF32(reinterpretU32AsF32(u16x2ToU32(u16x4.slice(0, 2)))) && + isFiniteF32(reinterpretU32AsF32(u16x2ToU32(u16x4.slice(2, 4)))) + ) + .map(e => ({ + input: u16x4ToVec4F16(e), + expected: bitcastVec4F16ToVec2F32Comparator(e), + })), + vec4_f16_inf_nan_to_vec2_f32: () => + f16Vec2FiniteInfNanInU16x4.map(e => ({ + input: u16x4ToVec4F16(e), + expected: bitcastVec4F16ToVec2F32Comparator(e), + })), +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/bitcast.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/bitcast.spec.ts index 390129f2c7..02acf98ef4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/bitcast.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/bitcast.spec.ts @@ -11,6 +11,9 @@ S is i32, u32, f32 T is i32, u32, f32, and T is not S Reinterpretation of bits. Beware non-normal f32 values. +@const @must_use fn bitcast<u32>(e : Type.abstractInt) -> T +@const @must_use fn bitcast<vecN<u32>>(e : vecN<Type.abstractInt>) -> T + @const @must_use fn bitcast<T>(e: vec2<f16> ) -> T @const @must_use fn bitcast<vec2<T>>(e: vec4<f16> ) -> vec2<T> @const @must_use fn bitcast<vec2<f16>>(e: T ) -> vec2<f16> @@ -20,823 +23,26 @@ T is i32, u32, f32 import { TestParams } from '../../../../../../common/framework/fixture.js'; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; -import { assert } from '../../../../../../common/util/util.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { Comparator, alwaysPass, anyOf } from '../../../../../util/compare.js'; -import { kBit, kValue } from '../../../../../util/constants.js'; +import { anyOf } from '../../../../../util/compare.js'; import { f32, - i32, u32, - f16, - TypeF32, - TypeI32, - TypeU32, - TypeF16, - TypeVec, - Vector, - Scalar, - toVector, + i32, + abstractFloat, + uint32ToFloat32, + u32Bits, + Type, } from '../../../../../util/conversion.js'; -import { FPInterval, FP } from '../../../../../util/floating_point.js'; -import { - fullF32Range, - fullI32Range, - fullU32Range, - fullF16Range, - linearRange, - isSubnormalNumberF32, - isSubnormalNumberF16, - cartesianProduct, - isFiniteF32, - isFiniteF16, -} from '../../../../../util/math.js'; -import { - reinterpretI32AsF32, - reinterpretI32AsU32, - reinterpretF32AsI32, - reinterpretF32AsU32, - reinterpretU32AsF32, - reinterpretU32AsI32, - reinterpretU16AsF16, - reinterpretF16AsU16, -} from '../../../../../util/reinterpret.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run, ShaderBuilder } from '../../expression.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { scalarF32Range } from '../../../../../util/math.js'; +import { ShaderBuilder, allInputSources, onlyConstInputSource, run } from '../../expression.js'; +import { d } from './bitcast.cache.js'; import { builtinWithPredeclaration } from './builtin.js'; export const g = makeTestGroup(GPUTest); -const numNaNs = 11; -const f32InfAndNaNInU32: number[] = [ - // Cover NaNs evenly in integer space. - // The positive NaN with the lowest integer representation is the integer - // for infinity, plus one. - // The positive NaN with the highest integer representation is i32.max (!) - ...linearRange(kBit.f32.positive.infinity + 1, kBit.i32.positive.max, numNaNs), - // The negative NaN with the lowest integer representation is the integer - // for negative infinity, plus one. - // The negative NaN with the highest integer representation is u32.max (!) - ...linearRange(kBit.f32.negative.infinity + 1, kBit.u32.max, numNaNs), - kBit.f32.positive.infinity, - kBit.f32.negative.infinity, -]; -const f32InfAndNaNInF32 = f32InfAndNaNInU32.map(u => reinterpretU32AsF32(u)); -const f32InfAndNaNInI32 = f32InfAndNaNInU32.map(u => reinterpretU32AsI32(u)); - -const f32ZerosInU32 = [0, kBit.f32.negative.zero]; -const f32ZerosInF32 = f32ZerosInU32.map(u => reinterpretU32AsF32(u)); -const f32ZerosInI32 = f32ZerosInU32.map(u => reinterpretU32AsI32(u)); -const f32ZerosInterval: FPInterval = new FPInterval('f32', -0.0, 0.0); - -// f32FiniteRange is a list of finite f32s. fullF32Range() already -// has +0, we only need to add -0. -const f32FiniteRange: number[] = [...fullF32Range(), kValue.f32.negative.zero]; -const f32RangeWithInfAndNaN: number[] = [...f32FiniteRange, ...f32InfAndNaNInF32]; - -// F16 values, finite, Inf/NaN, and zeros. Represented in float and u16. -const f16FiniteInF16: number[] = [...fullF16Range(), kValue.f16.negative.zero]; -const f16FiniteInU16: number[] = f16FiniteInF16.map(u => reinterpretF16AsU16(u)); - -const f16InfAndNaNInU16: number[] = [ - // Cover NaNs evenly in integer space. - // The positive NaN with the lowest integer representation is the integer - // for infinity, plus one. - // The positive NaN with the highest integer representation is u16 0x7fff i.e. 32767. - ...linearRange(kBit.f16.positive.infinity + 1, 32767, numNaNs).map(v => Math.ceil(v)), - // The negative NaN with the lowest integer representation is the integer - // for negative infinity, plus one. - // The negative NaN with the highest integer representation is u16 0xffff i.e. 65535 - ...linearRange(kBit.f16.negative.infinity + 1, 65535, numNaNs).map(v => Math.floor(v)), - kBit.f16.positive.infinity, - kBit.f16.negative.infinity, -]; -const f16InfAndNaNInF16 = f16InfAndNaNInU16.map(u => reinterpretU16AsF16(u)); - -const f16ZerosInU16 = [kBit.f16.negative.zero, 0]; - -// f16 interval that match +/-0.0. -const f16ZerosInterval: FPInterval = new FPInterval('f16', -0.0, 0.0); - -/** - * @returns an u32 whose lower and higher 16bits are the two elements of the - * given array of two u16 respectively, in little-endian. - */ -function u16x2ToU32(u16x2: readonly number[]): number { - assert(u16x2.length === 2); - // Create a DataView with 4 bytes buffer. - const buffer = new ArrayBuffer(4); - const view = new DataView(buffer); - // Enforce little-endian. - view.setUint16(0, u16x2[0], true); - view.setUint16(2, u16x2[1], true); - return view.getUint32(0, true); -} - -/** - * @returns an array of two u16, respectively the lower and higher 16bits of - * given u32 in little-endian. - */ -function u32ToU16x2(u32: number): number[] { - // Create a DataView with 4 bytes buffer. - const buffer = new ArrayBuffer(4); - const view = new DataView(buffer); - // Enforce little-endian. - view.setUint32(0, u32, true); - return [view.getUint16(0, true), view.getUint16(2, true)]; -} - -/** - * @returns a vec2<f16> from an array of two u16, each reinterpreted as f16. - */ -function u16x2ToVec2F16(u16x2: number[]): Vector { - assert(u16x2.length === 2); - return toVector(u16x2.map(reinterpretU16AsF16), f16); -} - -/** - * @returns a vec4<f16> from an array of four u16, each reinterpreted as f16. - */ -function u16x4ToVec4F16(u16x4: number[]): Vector { - assert(u16x4.length === 4); - return toVector(u16x4.map(reinterpretU16AsF16), f16); -} - -/** - * @returns true if and only if a given u32 can bitcast to a vec2<f16> with all elements - * being finite f16 values. - */ -function canU32BitcastToFiniteVec2F16(u32: number): boolean { - return u32ToU16x2(u32) - .map(u16 => isFiniteF16(reinterpretU16AsF16(u16))) - .reduce((a, b) => a && b, true); -} - -/** - * @returns an array of N elements with the i-th element being an array of len elements - * [a_i, a_((i+1)%N), ..., a_((i+len-1)%N)], for the input array of N element [a_1, ... a_N] - * and the given len. For example, slidingSlice([1, 2, 3], 2) result in - * [[1, 2], [2, 3], [3, 1]]. - * This helper function is used for generating vector cases from scalar values array. - */ -function slidingSlice(input: number[], len: number) { - const result: number[][] = []; - for (let i = 0; i < input.length; i++) { - const sub: number[] = []; - for (let j = 0; j < len; j++) { - sub.push(input[(i + j) % input.length]); - } - result.push(sub); - } - return result; -} - -// vec2<f16> interesting (zeros, Inf, and NaN) values for testing cases. -// vec2<f16> values that has at least one Inf/NaN f16 element, reinterpreted as u32/i32. -const f16Vec2InfAndNaNInU32 = [ - ...cartesianProduct(f16InfAndNaNInU16, [...f16InfAndNaNInU16, ...f16FiniteInU16]), - ...cartesianProduct(f16FiniteInU16, f16InfAndNaNInU16), -].map(u16x2ToU32); -const f16Vec2InfAndNaNInI32 = f16Vec2InfAndNaNInU32.map(u => reinterpretU32AsI32(u)); -// vec2<f16> values with two f16 0.0 element, reinterpreted as u32/i32. -const f16Vec2ZerosInU32 = cartesianProduct(f16ZerosInU16, f16ZerosInU16).map(u16x2ToU32); -const f16Vec2ZerosInI32 = f16Vec2ZerosInU32.map(u => reinterpretU32AsI32(u)); - -// i32/u32/f32 range for bitcasting to vec2<f16> -// u32 values for bitcasting to vec2<f16> finite, Inf, and NaN. -const u32RangeForF16Vec2FiniteInfNaN: number[] = [ - ...fullU32Range(), - ...f16Vec2ZerosInU32, - ...f16Vec2InfAndNaNInU32, -]; -// u32 values for bitcasting to finite only vec2<f16>, used for constant evaluation. -const u32RangeForF16Vec2Finite: number[] = u32RangeForF16Vec2FiniteInfNaN.filter( - canU32BitcastToFiniteVec2F16 -); -// i32 values for bitcasting to vec2<f16> finite, zeros, Inf, and NaN. -const i32RangeForF16Vec2FiniteInfNaN: number[] = [ - ...fullI32Range(), - ...f16Vec2ZerosInI32, - ...f16Vec2InfAndNaNInI32, -]; -// i32 values for bitcasting to finite only vec2<f16>, used for constant evaluation. -const i32RangeForF16Vec2Finite: number[] = i32RangeForF16Vec2FiniteInfNaN.filter(u => - canU32BitcastToFiniteVec2F16(reinterpretI32AsU32(u)) -); -// f32 values with finite/Inf/NaN f32, for bitcasting to vec2<f16> finite, zeros, Inf, and NaN. -const f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN: number[] = [ - ...f32RangeWithInfAndNaN, - ...u32RangeForF16Vec2FiniteInfNaN.map(reinterpretU32AsF32), -]; -// Finite f32 values for bitcasting to finite only vec2<f16>, used for constant evaluation. -const f32FiniteRangeForF16Vec2Finite: number[] = f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN - .filter(isFiniteF32) - .filter(u => canU32BitcastToFiniteVec2F16(reinterpretF32AsU32(u))); - -// vec2<f16> cases for bitcasting to i32/u32/f32, by combining f16 values into pairs -const f16Vec2FiniteInU16x2 = slidingSlice(f16FiniteInU16, 2); -const f16Vec2FiniteInfNanInU16x2 = slidingSlice([...f16FiniteInU16, ...f16InfAndNaNInU16], 2); -// vec4<f16> cases for bitcasting to vec2<i32/u32/f32>, by combining f16 values 4-by-4 -const f16Vec2FiniteInU16x4 = slidingSlice(f16FiniteInU16, 4); -const f16Vec2FiniteInfNanInU16x4 = slidingSlice([...f16FiniteInU16, ...f16InfAndNaNInU16], 4); - -// alwaysPass comparator for i32/u32/f32 cases. For f32/f16 we also use unbound interval, which -// allow per-element unbounded expectation for vector. -const anyF32 = alwaysPass('any f32'); -const anyI32 = alwaysPass('any i32'); -const anyU32 = alwaysPass('any u32'); - -// Unbounded FPInterval -const f32UnboundedInterval = FP.f32.constants().unboundedInterval; -const f16UnboundedInterval = FP.f16.constants().unboundedInterval; - -// i32 and u32 cases for bitcasting to f32. -// i32 cases for bitcasting to f32 finite, zeros, Inf, and NaN. -const i32RangeForF32FiniteInfNaN: number[] = [ - ...fullI32Range(), - ...f32ZerosInI32, - ...f32InfAndNaNInI32, -]; -// i32 cases for bitcasting to f32 finite only. -const i32RangeForF32Finite: number[] = i32RangeForF32FiniteInfNaN.filter(i => - isFiniteF32(reinterpretI32AsF32(i)) -); -// u32 cases for bitcasting to f32 finite, zeros, Inf, and NaN. -const u32RangeForF32FiniteInfNaN: number[] = [ - ...fullU32Range(), - ...f32ZerosInU32, - ...f32InfAndNaNInU32, -]; -// u32 cases for bitcasting to f32 finite only. -const u32RangeForF32Finite: number[] = u32RangeForF32FiniteInfNaN.filter(u => - isFiniteF32(reinterpretU32AsF32(u)) -); - -/** - * @returns a Comparator for checking if a f32 value is a valid - * bitcast conversion from f32. - */ -function bitcastF32ToF32Comparator(f: number): Comparator { - if (!isFiniteF32(f)) return anyF32; - const acceptable: number[] = [f, ...(isSubnormalNumberF32(f) ? f32ZerosInF32 : [])]; - return anyOf(...acceptable.map(f32)); -} - -/** - * @returns a Comparator for checking if a u32 value is a valid - * bitcast conversion from f32. - */ -function bitcastF32ToU32Comparator(f: number): Comparator { - if (!isFiniteF32(f)) return anyU32; - const acceptable: number[] = [ - reinterpretF32AsU32(f), - ...(isSubnormalNumberF32(f) ? f32ZerosInU32 : []), - ]; - return anyOf(...acceptable.map(u32)); -} - -/** - * @returns a Comparator for checking if a i32 value is a valid - * bitcast conversion from f32. - */ -function bitcastF32ToI32Comparator(f: number): Comparator { - if (!isFiniteF32(f)) return anyI32; - const acceptable: number[] = [ - reinterpretF32AsI32(f), - ...(isSubnormalNumberF32(f) ? f32ZerosInI32 : []), - ]; - return anyOf(...acceptable.map(i32)); -} - -/** - * @returns a Comparator for checking if a f32 value is a valid - * bitcast conversion from i32. - */ -function bitcastI32ToF32Comparator(i: number): Comparator { - const f: number = reinterpretI32AsF32(i); - if (!isFiniteF32(f)) return anyI32; - // Positive or negative zero bit pattern map to any zero. - if (f32ZerosInI32.includes(i)) return anyOf(...f32ZerosInF32.map(f32)); - const acceptable: number[] = [f, ...(isSubnormalNumberF32(f) ? f32ZerosInF32 : [])]; - return anyOf(...acceptable.map(f32)); -} - -/** - * @returns a Comparator for checking if a f32 value is a valid - * bitcast conversion from u32. - */ -function bitcastU32ToF32Comparator(u: number): Comparator { - const f: number = reinterpretU32AsF32(u); - if (!isFiniteF32(f)) return anyU32; - // Positive or negative zero bit pattern map to any zero. - if (f32ZerosInU32.includes(u)) return anyOf(...f32ZerosInF32.map(f32)); - const acceptable: number[] = [f, ...(isSubnormalNumberF32(f) ? f32ZerosInF32 : [])]; - return anyOf(...acceptable.map(f32)); -} - -/** - * @returns an array of expected f16 FPInterval for the given bitcasted f16 value, which may be - * subnormal, Inf, or NaN. Test cases that bitcasted to vector of f16 use this function to get - * per-element expectation and build vector expectation using cartesianProduct. - */ -function generateF16ExpectationIntervals(bitcastedF16Value: number): FPInterval[] { - // If the bitcasted f16 value is inf or nan, the result is unbounded - if (!isFiniteF16(bitcastedF16Value)) { - return [f16UnboundedInterval]; - } - // If the casted f16 value is +/-0.0, the result can be one of both. Note that in JS -0.0 === 0.0. - if (bitcastedF16Value === 0.0) { - return [f16ZerosInterval]; - } - const exactInterval = FP.f16.toInterval(bitcastedF16Value); - // If the casted f16 value is subnormal, it also may be flushed to +/-0.0. - return [exactInterval, ...(isSubnormalNumberF16(bitcastedF16Value) ? [f16ZerosInterval] : [])]; -} - -/** - * @returns a Comparator for checking if a f16 value is a valid - * bitcast conversion from f16. - */ -function bitcastF16ToF16Comparator(f: number): Comparator { - if (!isFiniteF16(f)) return anyOf(f16UnboundedInterval); - return anyOf(...generateF16ExpectationIntervals(f)); -} - -/** - * @returns a Comparator for checking if a vec2<f16> is a valid bitcast - * conversion from u32. - */ -function bitcastU32ToVec2F16Comparator(u: number): Comparator { - const bitcastedVec2F16InU16x2 = u32ToU16x2(u).map(reinterpretU16AsF16); - // Generate expection for vec2 f16 result, by generating expected intervals for each elements and - // then do cartesian product. - const expectedIntervalsCombination = cartesianProduct( - ...bitcastedVec2F16InU16x2.map(generateF16ExpectationIntervals) - ); - return anyOf(...expectedIntervalsCombination); -} - -/** - * @returns a Comparator for checking if a vec2<f16> value is a valid - * bitcast conversion from i32. - */ -function bitcastI32ToVec2F16Comparator(i: number): Comparator { - const bitcastedVec2F16InU16x2 = u32ToU16x2(reinterpretI32AsU32(i)).map(reinterpretU16AsF16); - // Generate expection for vec2 f16 result, by generating expected intervals for each elements and - // then do cartesian product. - const expectedIntervalsCombination = cartesianProduct( - ...bitcastedVec2F16InU16x2.map(generateF16ExpectationIntervals) - ); - return anyOf(...expectedIntervalsCombination); -} - -/** - * @returns a Comparator for checking if a vec2<f16> value is a valid - * bitcast conversion from f32. - */ -function bitcastF32ToVec2F16Comparator(f: number): Comparator { - // If input f32 is not finite, it can be evaluated to any value and thus any result f16 vec2 is - // possible. - if (!isFiniteF32(f)) { - return anyOf([f16UnboundedInterval, f16UnboundedInterval]); - } - const bitcastedVec2F16InU16x2 = u32ToU16x2(reinterpretF32AsU32(f)).map(reinterpretU16AsF16); - // Generate expection for vec2 f16 result, by generating expected intervals for each elements and - // then do cartesian product. - const expectedIntervalsCombination = cartesianProduct( - ...bitcastedVec2F16InU16x2.map(generateF16ExpectationIntervals) - ); - return anyOf(...expectedIntervalsCombination); -} - -/** - * @returns a Comparator for checking if a vec4<f16> is a valid - * bitcast conversion from vec2<u32>. - */ -function bitcastVec2U32ToVec4F16Comparator(u32x2: number[]): Comparator { - assert(u32x2.length === 2); - const bitcastedVec4F16InU16x4 = u32x2.flatMap(u32ToU16x2).map(reinterpretU16AsF16); - // Generate expection for vec4 f16 result, by generating expected intervals for each elements and - // then do cartesian product. - const expectedIntervalsCombination = cartesianProduct( - ...bitcastedVec4F16InU16x4.map(generateF16ExpectationIntervals) - ); - return anyOf(...expectedIntervalsCombination); -} - -/** - * @returns a Comparator for checking if a vec4<f16> is a valid - * bitcast conversion from vec2<i32>. - */ -function bitcastVec2I32ToVec4F16Comparator(i32x2: number[]): Comparator { - assert(i32x2.length === 2); - const bitcastedVec4F16InU16x4 = i32x2 - .map(reinterpretI32AsU32) - .flatMap(u32ToU16x2) - .map(reinterpretU16AsF16); - // Generate expection for vec4 f16 result, by generating expected intervals for each elements and - // then do cartesian product. - const expectedIntervalsCombination = cartesianProduct( - ...bitcastedVec4F16InU16x4.map(generateF16ExpectationIntervals) - ); - return anyOf(...expectedIntervalsCombination); -} - -/** - * @returns a Comparator for checking if a vec4<f16> is a valid - * bitcast conversion from vec2<f32>. - */ -function bitcastVec2F32ToVec4F16Comparator(f32x2: number[]): Comparator { - assert(f32x2.length === 2); - const bitcastedVec4F16InU16x4 = f32x2 - .map(reinterpretF32AsU32) - .flatMap(u32ToU16x2) - .map(reinterpretU16AsF16); - // Generate expection for vec4 f16 result, by generating expected intervals for each elements and - // then do cartesian product. - const expectedIntervalsCombination = cartesianProduct( - ...bitcastedVec4F16InU16x4.map(generateF16ExpectationIntervals) - ); - return anyOf(...expectedIntervalsCombination); -} - -// Structure that store the expectations of a single 32bit scalar/element bitcasted from two f16. -interface ExpectionFor32BitsScalarFromF16x2 { - // possibleExpectations is Scalar array if the expectation is for i32/u32 and FPInterval array for - // f32. Note that if the expectation for i32/u32 is unbound, possibleExpectations is meaningless. - possibleExpectations: (Scalar | FPInterval)[]; - isUnbounded: boolean; -} - -/** - * @returns the array of possible 16bits, represented in u16, that bitcasted - * from a given finite f16 represented in u16, handling the possible subnormal - * flushing. Used to build up 32bits or larger results. - */ -function possibleBitsInU16FromFiniteF16InU16(f16InU16: number): number[] { - const h = reinterpretU16AsF16(f16InU16); - assert(isFiniteF16(h)); - return [f16InU16, ...(isSubnormalNumberF16(h) ? f16ZerosInU16 : [])]; -} - -/** - * @returns the expectation for a single 32bit scalar bitcasted from given pair of - * f16, result in ExpectionFor32BitsScalarFromF16x2. - */ -function possible32BitScalarIntervalsFromF16x2( - f16x2InU16x2: number[], - type: 'i32' | 'u32' | 'f32' -): ExpectionFor32BitsScalarFromF16x2 { - assert(f16x2InU16x2.length === 2); - let reinterpretFromU32: (x: number) => number; - let expectationsForValue: (x: number) => Scalar[] | FPInterval[]; - let unboundedExpectations: FPInterval[] | Scalar[]; - if (type === 'u32') { - reinterpretFromU32 = (x: number) => x; - expectationsForValue = x => [u32(x)]; - // Scalar expectation can not express "unbounded" for i32 and u32, so use 0 here as a - // placeholder, and the possibleExpectations should be ignored if the result is unbounded. - unboundedExpectations = [u32(0)]; - } else if (type === 'i32') { - reinterpretFromU32 = (x: number) => reinterpretU32AsI32(x); - expectationsForValue = x => [i32(x)]; - // Scalar expectation can not express "unbounded" for i32 and u32, so use 0 here as a - // placeholder, and the possibleExpectations should be ignored if the result is unbounded. - unboundedExpectations = [i32(0)]; - } else { - assert(type === 'f32'); - reinterpretFromU32 = (x: number) => reinterpretU32AsF32(x); - expectationsForValue = x => { - // Handle the possible Inf/NaN/zeros and subnormal cases for f32 result. - if (!isFiniteF32(x)) { - return [f32UnboundedInterval]; - } - // If the casted f16 value is +/-0.0, the result can be one of both. Note that in JS -0.0 === 0.0. - if (x === 0.0) { - return [f32ZerosInterval]; - } - const exactInterval = FP.f32.toInterval(x); - // If the casted f16 value is subnormal, it also may be flushed to +/-0.0. - return [exactInterval, ...(isSubnormalNumberF32(x) ? [f32ZerosInterval] : [])]; - }; - unboundedExpectations = [f32UnboundedInterval]; - } - // Return unbounded expection if f16 Inf/NaN occurs - if ( - !isFiniteF16(reinterpretU16AsF16(f16x2InU16x2[0])) || - !isFiniteF16(reinterpretU16AsF16(f16x2InU16x2[1])) - ) { - return { possibleExpectations: unboundedExpectations, isUnbounded: true }; - } - const possibleU16Bits = f16x2InU16x2.map(possibleBitsInU16FromFiniteF16InU16); - const possibleExpectations = cartesianProduct(...possibleU16Bits).flatMap<Scalar | FPInterval>( - (possibleBitsU16x2: readonly number[]) => { - assert(possibleBitsU16x2.length === 2); - return expectationsForValue(reinterpretFromU32(u16x2ToU32(possibleBitsU16x2))); - } - ); - return { possibleExpectations, isUnbounded: false }; -} - -/** - * @returns a Comparator for checking if a u32 value is a valid - * bitcast conversion from vec2 f16. - */ -function bitcastVec2F16ToU32Comparator(vec2F16InU16x2: number[]): Comparator { - assert(vec2F16InU16x2.length === 2); - const expectations = possible32BitScalarIntervalsFromF16x2(vec2F16InU16x2, 'u32'); - // Return alwaysPass if result is expected unbounded. - if (expectations.isUnbounded) { - return anyU32; - } - return anyOf(...expectations.possibleExpectations); -} - -/** - * @returns a Comparator for checking if a i32 value is a valid - * bitcast conversion from vec2 f16. - */ -function bitcastVec2F16ToI32Comparator(vec2F16InU16x2: number[]): Comparator { - assert(vec2F16InU16x2.length === 2); - const expectations = possible32BitScalarIntervalsFromF16x2(vec2F16InU16x2, 'i32'); - // Return alwaysPass if result is expected unbounded. - if (expectations.isUnbounded) { - return anyI32; - } - return anyOf(...expectations.possibleExpectations); -} - -/** - * @returns a Comparator for checking if a i32 value is a valid - * bitcast conversion from vec2 f16. - */ -function bitcastVec2F16ToF32Comparator(vec2F16InU16x2: number[]): Comparator { - assert(vec2F16InU16x2.length === 2); - const expectations = possible32BitScalarIntervalsFromF16x2(vec2F16InU16x2, 'f32'); - // Return alwaysPass if result is expected unbounded. - if (expectations.isUnbounded) { - return anyF32; - } - return anyOf(...expectations.possibleExpectations); -} - -/** - * @returns a Comparator for checking if a vec2 u32 value is a valid - * bitcast conversion from vec4 f16. - */ -function bitcastVec4F16ToVec2U32Comparator(vec4F16InU16x4: number[]): Comparator { - assert(vec4F16InU16x4.length === 4); - const expectationsPerElement = [vec4F16InU16x4.slice(0, 2), vec4F16InU16x4.slice(2, 4)].map(e => - possible32BitScalarIntervalsFromF16x2(e, 'u32') - ); - // Return alwaysPass if any element is expected unbounded. Although it may be only one unbounded - // element in the result vector, currently we don't have a way to build a comparator that expect - // only one element of i32/u32 vector unbounded. - if (expectationsPerElement.map(e => e.isUnbounded).reduce((a, b) => a || b, false)) { - return alwaysPass('any vec2<u32>'); - } - return anyOf( - ...cartesianProduct(...expectationsPerElement.map(e => e.possibleExpectations)).map( - e => new Vector(e as Scalar[]) - ) - ); -} - -/** - * @returns a Comparator for checking if a vec2 i32 value is a valid - * bitcast conversion from vec4 f16. - */ -function bitcastVec4F16ToVec2I32Comparator(vec4F16InU16x4: number[]): Comparator { - assert(vec4F16InU16x4.length === 4); - const expectationsPerElement = [vec4F16InU16x4.slice(0, 2), vec4F16InU16x4.slice(2, 4)].map(e => - possible32BitScalarIntervalsFromF16x2(e, 'i32') - ); - // Return alwaysPass if any element is expected unbounded. Although it may be only one unbounded - // element in the result vector, currently we don't have a way to build a comparator that expect - // only one element of i32/u32 vector unbounded. - if (expectationsPerElement.map(e => e.isUnbounded).reduce((a, b) => a || b, false)) { - return alwaysPass('any vec2<i32>'); - } - return anyOf( - ...cartesianProduct(...expectationsPerElement.map(e => e.possibleExpectations)).map( - e => new Vector(e as Scalar[]) - ) - ); -} - -/** - * @returns a Comparator for checking if a vec2 f32 value is a valid - * bitcast conversion from vec4 f16. - */ -function bitcastVec4F16ToVec2F32Comparator(vec4F16InU16x4: number[]): Comparator { - assert(vec4F16InU16x4.length === 4); - const expectationsPerElement = [vec4F16InU16x4.slice(0, 2), vec4F16InU16x4.slice(2, 4)].map(e => - possible32BitScalarIntervalsFromF16x2(e, 'f32') - ); - return anyOf( - ...cartesianProduct(...expectationsPerElement.map(e => e.possibleExpectations)).map(e => [ - e[0] as FPInterval, - e[1] as FPInterval, - ]) - ); -} - -export const d = makeCaseCache('bitcast', { - // Identity Cases - i32_to_i32: () => fullI32Range().map(e => ({ input: i32(e), expected: i32(e) })), - u32_to_u32: () => fullU32Range().map(e => ({ input: u32(e), expected: u32(e) })), - f32_inf_nan_to_f32: () => - f32RangeWithInfAndNaN.map(e => ({ - input: f32(e), - expected: bitcastF32ToF32Comparator(e), - })), - f32_to_f32: () => - f32FiniteRange.map(e => ({ input: f32(e), expected: bitcastF32ToF32Comparator(e) })), - f16_inf_nan_to_f16: () => - [...f16FiniteInF16, ...f16InfAndNaNInF16].map(e => ({ - input: f16(e), - expected: bitcastF16ToF16Comparator(e), - })), - f16_to_f16: () => - f16FiniteInF16.map(e => ({ input: f16(e), expected: bitcastF16ToF16Comparator(e) })), - - // i32,u32,f32 to different i32,u32,f32 - i32_to_u32: () => fullI32Range().map(e => ({ input: i32(e), expected: u32(e) })), - i32_to_f32: () => - i32RangeForF32Finite.map(e => ({ - input: i32(e), - expected: bitcastI32ToF32Comparator(e), - })), - i32_to_f32_inf_nan: () => - i32RangeForF32FiniteInfNaN.map(e => ({ - input: i32(e), - expected: bitcastI32ToF32Comparator(e), - })), - u32_to_i32: () => fullU32Range().map(e => ({ input: u32(e), expected: i32(e) })), - u32_to_f32: () => - u32RangeForF32Finite.map(e => ({ - input: u32(e), - expected: bitcastU32ToF32Comparator(e), - })), - u32_to_f32_inf_nan: () => - u32RangeForF32FiniteInfNaN.map(e => ({ - input: u32(e), - expected: bitcastU32ToF32Comparator(e), - })), - f32_inf_nan_to_i32: () => - f32RangeWithInfAndNaN.map(e => ({ - input: f32(e), - expected: bitcastF32ToI32Comparator(e), - })), - f32_to_i32: () => - f32FiniteRange.map(e => ({ input: f32(e), expected: bitcastF32ToI32Comparator(e) })), - - f32_inf_nan_to_u32: () => - f32RangeWithInfAndNaN.map(e => ({ - input: f32(e), - expected: bitcastF32ToU32Comparator(e), - })), - f32_to_u32: () => - f32FiniteRange.map(e => ({ input: f32(e), expected: bitcastF32ToU32Comparator(e) })), - - // i32,u32,f32 to vec2<f16> - u32_to_vec2_f16_inf_nan: () => - u32RangeForF16Vec2FiniteInfNaN.map(e => ({ - input: u32(e), - expected: bitcastU32ToVec2F16Comparator(e), - })), - u32_to_vec2_f16: () => - u32RangeForF16Vec2Finite.map(e => ({ - input: u32(e), - expected: bitcastU32ToVec2F16Comparator(e), - })), - i32_to_vec2_f16_inf_nan: () => - i32RangeForF16Vec2FiniteInfNaN.map(e => ({ - input: i32(e), - expected: bitcastI32ToVec2F16Comparator(e), - })), - i32_to_vec2_f16: () => - i32RangeForF16Vec2Finite.map(e => ({ - input: i32(e), - expected: bitcastI32ToVec2F16Comparator(e), - })), - f32_inf_nan_to_vec2_f16_inf_nan: () => - f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN.map(e => ({ - input: f32(e), - expected: bitcastF32ToVec2F16Comparator(e), - })), - f32_to_vec2_f16: () => - f32FiniteRangeForF16Vec2Finite.map(e => ({ - input: f32(e), - expected: bitcastF32ToVec2F16Comparator(e), - })), - - // vec2<i32>, vec2<u32>, vec2<f32> to vec4<f16> - vec2_i32_to_vec4_f16_inf_nan: () => - slidingSlice(i32RangeForF16Vec2FiniteInfNaN, 2).map(e => ({ - input: toVector(e, i32), - expected: bitcastVec2I32ToVec4F16Comparator(e), - })), - vec2_i32_to_vec4_f16: () => - slidingSlice(i32RangeForF16Vec2Finite, 2).map(e => ({ - input: toVector(e, i32), - expected: bitcastVec2I32ToVec4F16Comparator(e), - })), - vec2_u32_to_vec4_f16_inf_nan: () => - slidingSlice(u32RangeForF16Vec2FiniteInfNaN, 2).map(e => ({ - input: toVector(e, u32), - expected: bitcastVec2U32ToVec4F16Comparator(e), - })), - vec2_u32_to_vec4_f16: () => - slidingSlice(u32RangeForF16Vec2Finite, 2).map(e => ({ - input: toVector(e, u32), - expected: bitcastVec2U32ToVec4F16Comparator(e), - })), - vec2_f32_inf_nan_to_vec4_f16_inf_nan: () => - slidingSlice(f32RangeWithInfAndNaNForF16Vec2FiniteInfNaN, 2).map(e => ({ - input: toVector(e, f32), - expected: bitcastVec2F32ToVec4F16Comparator(e), - })), - vec2_f32_to_vec4_f16: () => - slidingSlice(f32FiniteRangeForF16Vec2Finite, 2).map(e => ({ - input: toVector(e, f32), - expected: bitcastVec2F32ToVec4F16Comparator(e), - })), - - // vec2<f16> to i32, u32, f32 - vec2_f16_to_u32: () => - f16Vec2FiniteInU16x2.map(e => ({ - input: u16x2ToVec2F16(e), - expected: bitcastVec2F16ToU32Comparator(e), - })), - vec2_f16_inf_nan_to_u32: () => - f16Vec2FiniteInfNanInU16x2.map(e => ({ - input: u16x2ToVec2F16(e), - expected: bitcastVec2F16ToU32Comparator(e), - })), - vec2_f16_to_i32: () => - f16Vec2FiniteInU16x2.map(e => ({ - input: u16x2ToVec2F16(e), - expected: bitcastVec2F16ToI32Comparator(e), - })), - vec2_f16_inf_nan_to_i32: () => - f16Vec2FiniteInfNanInU16x2.map(e => ({ - input: u16x2ToVec2F16(e), - expected: bitcastVec2F16ToI32Comparator(e), - })), - vec2_f16_to_f32_finite: () => - f16Vec2FiniteInU16x2 - .filter(u16x2 => isFiniteF32(reinterpretU32AsF32(u16x2ToU32(u16x2)))) - .map(e => ({ - input: u16x2ToVec2F16(e), - expected: bitcastVec2F16ToF32Comparator(e), - })), - vec2_f16_inf_nan_to_f32: () => - f16Vec2FiniteInfNanInU16x2.map(e => ({ - input: u16x2ToVec2F16(e), - expected: bitcastVec2F16ToF32Comparator(e), - })), - - // vec4<f16> to vec2 of i32, u32, f32 - vec4_f16_to_vec2_u32: () => - f16Vec2FiniteInU16x4.map(e => ({ - input: u16x4ToVec4F16(e), - expected: bitcastVec4F16ToVec2U32Comparator(e), - })), - vec4_f16_inf_nan_to_vec2_u32: () => - f16Vec2FiniteInfNanInU16x4.map(e => ({ - input: u16x4ToVec4F16(e), - expected: bitcastVec4F16ToVec2U32Comparator(e), - })), - vec4_f16_to_vec2_i32: () => - f16Vec2FiniteInU16x4.map(e => ({ - input: u16x4ToVec4F16(e), - expected: bitcastVec4F16ToVec2I32Comparator(e), - })), - vec4_f16_inf_nan_to_vec2_i32: () => - f16Vec2FiniteInfNanInU16x4.map(e => ({ - input: u16x4ToVec4F16(e), - expected: bitcastVec4F16ToVec2I32Comparator(e), - })), - vec4_f16_to_vec2_f32_finite: () => - f16Vec2FiniteInU16x4 - .filter( - u16x4 => - isFiniteF32(reinterpretU32AsF32(u16x2ToU32(u16x4.slice(0, 2)))) && - isFiniteF32(reinterpretU32AsF32(u16x2ToU32(u16x4.slice(2, 4)))) - ) - .map(e => ({ - input: u16x4ToVec4F16(e), - expected: bitcastVec4F16ToVec2F32Comparator(e), - })), - vec4_f16_inf_nan_to_vec2_f32: () => - f16Vec2FiniteInfNanInU16x4.map(e => ({ - input: u16x4ToVec4F16(e), - expected: bitcastVec4F16ToVec2F32Comparator(e), - })), -}); - /** * @returns a ShaderBuilder that generates a call to bitcast, * using appropriate destination type, which optionally can be @@ -865,7 +71,7 @@ g.test('i32_to_i32') ) .fn(async t => { const cases = await d.get('i32_to_i32'); - await run(t, bitcastBuilder('i32', t.params), [TypeI32], TypeI32, t.params, cases); + await run(t, bitcastBuilder('i32', t.params), [Type.i32], Type.i32, t.params, cases); }); g.test('u32_to_u32') @@ -879,7 +85,7 @@ g.test('u32_to_u32') ) .fn(async t => { const cases = await d.get('u32_to_u32'); - await run(t, bitcastBuilder('u32', t.params), [TypeU32], TypeU32, t.params, cases); + await run(t, bitcastBuilder('u32', t.params), [Type.u32], Type.u32, t.params, cases); }); g.test('f32_to_f32') @@ -896,7 +102,7 @@ g.test('f32_to_f32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'f32_to_f32' : 'f32_inf_nan_to_f32' ); - await run(t, bitcastBuilder('f32', t.params), [TypeF32], TypeF32, t.params, cases); + await run(t, bitcastBuilder('f32', t.params), [Type.f32], Type.f32, t.params, cases); }); // To i32 from u32, f32 @@ -911,7 +117,7 @@ g.test('u32_to_i32') ) .fn(async t => { const cases = await d.get('u32_to_i32'); - await run(t, bitcastBuilder('i32', t.params), [TypeU32], TypeI32, t.params, cases); + await run(t, bitcastBuilder('i32', t.params), [Type.u32], Type.i32, t.params, cases); }); g.test('f32_to_i32') @@ -928,7 +134,7 @@ g.test('f32_to_i32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'f32_to_i32' : 'f32_inf_nan_to_i32' ); - await run(t, bitcastBuilder('i32', t.params), [TypeF32], TypeI32, t.params, cases); + await run(t, bitcastBuilder('i32', t.params), [Type.f32], Type.i32, t.params, cases); }); // To u32 from i32, f32 @@ -943,7 +149,7 @@ g.test('i32_to_u32') ) .fn(async t => { const cases = await d.get('i32_to_u32'); - await run(t, bitcastBuilder('u32', t.params), [TypeI32], TypeU32, t.params, cases); + await run(t, bitcastBuilder('u32', t.params), [Type.i32], Type.u32, t.params, cases); }); g.test('f32_to_u32') @@ -960,7 +166,7 @@ g.test('f32_to_u32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'f32_to_u32' : 'f32_inf_nan_to_u32' ); - await run(t, bitcastBuilder('u32', t.params), [TypeF32], TypeU32, t.params, cases); + await run(t, bitcastBuilder('u32', t.params), [Type.f32], Type.u32, t.params, cases); }); // To f32 from i32, u32 @@ -978,7 +184,7 @@ g.test('i32_to_f32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'i32_to_f32' : 'i32_to_f32_inf_nan' ); - await run(t, bitcastBuilder('f32', t.params), [TypeI32], TypeF32, t.params, cases); + await run(t, bitcastBuilder('f32', t.params), [Type.i32], Type.f32, t.params, cases); }); g.test('u32_to_f32') @@ -995,7 +201,7 @@ g.test('u32_to_f32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'u32_to_f32' : 'u32_to_f32_inf_nan' ); - await run(t, bitcastBuilder('f32', t.params), [TypeU32], TypeF32, t.params, cases); + await run(t, bitcastBuilder('f32', t.params), [Type.u32], Type.f32, t.params, cases); }); // 16 bit types @@ -1020,7 +226,7 @@ g.test('f16_to_f16') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'f16_to_f16' : 'f16_inf_nan_to_f16' ); - await run(t, bitcastBuilder('f16', t.params), [TypeF16], TypeF16, t.params, cases); + await run(t, bitcastBuilder('f16', t.params), [Type.f16], Type.f16, t.params, cases); }); // f16: 32-bit scalar numeric to vec2<f16> @@ -1036,14 +242,7 @@ g.test('i32_to_vec2h') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'i32_to_vec2_f16' : 'i32_to_vec2_f16_inf_nan' ); - await run( - t, - bitcastBuilder('vec2<f16>', t.params), - [TypeI32], - TypeVec(2, TypeF16), - t.params, - cases - ); + await run(t, bitcastBuilder('vec2<f16>', t.params), [Type.i32], Type.vec2h, t.params, cases); }); g.test('u32_to_vec2h') @@ -1058,14 +257,7 @@ g.test('u32_to_vec2h') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'u32_to_vec2_f16' : 'u32_to_vec2_f16_inf_nan' ); - await run( - t, - bitcastBuilder('vec2<f16>', t.params), - [TypeU32], - TypeVec(2, TypeF16), - t.params, - cases - ); + await run(t, bitcastBuilder('vec2<f16>', t.params), [Type.u32], Type.vec2h, t.params, cases); }); g.test('f32_to_vec2h') @@ -1080,14 +272,7 @@ g.test('f32_to_vec2h') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'f32_to_vec2_f16' : 'f32_inf_nan_to_vec2_f16_inf_nan' ); - await run( - t, - bitcastBuilder('vec2<f16>', t.params), - [TypeF32], - TypeVec(2, TypeF16), - t.params, - cases - ); + await run(t, bitcastBuilder('vec2<f16>', t.params), [Type.f32], Type.vec2h, t.params, cases); }); // f16: vec2<32-bit scalar numeric> to vec4<f16> @@ -1103,14 +288,7 @@ g.test('vec2i_to_vec4h') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'vec2_i32_to_vec4_f16' : 'vec2_i32_to_vec4_f16_inf_nan' ); - await run( - t, - bitcastBuilder('vec4<f16>', t.params), - [TypeVec(2, TypeI32)], - TypeVec(4, TypeF16), - t.params, - cases - ); + await run(t, bitcastBuilder('vec4<f16>', t.params), [Type.vec2i], Type.vec4h, t.params, cases); }); g.test('vec2u_to_vec4h') @@ -1125,14 +303,7 @@ g.test('vec2u_to_vec4h') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'vec2_u32_to_vec4_f16' : 'vec2_u32_to_vec4_f16_inf_nan' ); - await run( - t, - bitcastBuilder('vec4<f16>', t.params), - [TypeVec(2, TypeU32)], - TypeVec(4, TypeF16), - t.params, - cases - ); + await run(t, bitcastBuilder('vec4<f16>', t.params), [Type.vec2u], Type.vec4h, t.params, cases); }); g.test('vec2f_to_vec4h') @@ -1149,14 +320,7 @@ g.test('vec2f_to_vec4h') ? 'vec2_f32_to_vec4_f16' : 'vec2_f32_inf_nan_to_vec4_f16_inf_nan' ); - await run( - t, - bitcastBuilder('vec4<f16>', t.params), - [TypeVec(2, TypeF32)], - TypeVec(4, TypeF16), - t.params, - cases - ); + await run(t, bitcastBuilder('vec4<f16>', t.params), [Type.vec2f], Type.vec4h, t.params, cases); }); // f16: vec2<f16> to 32-bit scalar numeric @@ -1172,7 +336,7 @@ g.test('vec2h_to_i32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'vec2_f16_to_i32' : 'vec2_f16_inf_nan_to_i32' ); - await run(t, bitcastBuilder('i32', t.params), [TypeVec(2, TypeF16)], TypeI32, t.params, cases); + await run(t, bitcastBuilder('i32', t.params), [Type.vec2h], Type.i32, t.params, cases); }); g.test('vec2h_to_u32') @@ -1187,7 +351,7 @@ g.test('vec2h_to_u32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'vec2_f16_to_u32' : 'vec2_f16_inf_nan_to_u32' ); - await run(t, bitcastBuilder('u32', t.params), [TypeVec(2, TypeF16)], TypeU32, t.params, cases); + await run(t, bitcastBuilder('u32', t.params), [Type.vec2h], Type.u32, t.params, cases); }); g.test('vec2h_to_f32') @@ -1202,7 +366,7 @@ g.test('vec2h_to_f32') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'vec2_f16_to_f32_finite' : 'vec2_f16_inf_nan_to_f32' ); - await run(t, bitcastBuilder('f32', t.params), [TypeVec(2, TypeF16)], TypeF32, t.params, cases); + await run(t, bitcastBuilder('f32', t.params), [Type.vec2h], Type.f32, t.params, cases); }); // f16: vec4<f16> to vec2<32-bit scalar numeric> @@ -1218,14 +382,7 @@ g.test('vec4h_to_vec2i') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'vec4_f16_to_vec2_i32' : 'vec4_f16_inf_nan_to_vec2_i32' ); - await run( - t, - bitcastBuilder('vec2<i32>', t.params), - [TypeVec(4, TypeF16)], - TypeVec(2, TypeI32), - t.params, - cases - ); + await run(t, bitcastBuilder('vec2<i32>', t.params), [Type.vec4h], Type.vec2i, t.params, cases); }); g.test('vec4h_to_vec2u') @@ -1240,14 +397,7 @@ g.test('vec4h_to_vec2u') // Infinities and NaNs are errors in const-eval. t.params.inputSource === 'const' ? 'vec4_f16_to_vec2_u32' : 'vec4_f16_inf_nan_to_vec2_u32' ); - await run( - t, - bitcastBuilder('vec2<u32>', t.params), - [TypeVec(4, TypeF16)], - TypeVec(2, TypeU32), - t.params, - cases - ); + await run(t, bitcastBuilder('vec2<u32>', t.params), [Type.vec4h], Type.vec2u, t.params, cases); }); g.test('vec4h_to_vec2f') @@ -1264,12 +414,230 @@ g.test('vec4h_to_vec2f') ? 'vec4_f16_to_vec2_f32_finite' : 'vec4_f16_inf_nan_to_vec2_f32' ); + await run(t, bitcastBuilder('vec2<f32>', t.params), [Type.vec4h], Type.vec2f, t.params, cases); + }); + +// Abstract Float +g.test('af_to_f32') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract float to f32 tests`) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = scalarF32Range().map(u => { + const res = FP['f32'].addFlushedIfNeeded([u]).map(f => { + return f32(f); + }); + return { + input: abstractFloat(u), + expected: anyOf(...res), + }; + }); + + await run(t, bitcastBuilder('f32', t.params), [Type.abstractFloat], Type.f32, t.params, cases); + }); + +g.test('af_to_i32') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract float to i32 tests`) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const values = [ + 0, + 1, + 10, + 256, + u32Bits(0b11111111011111111111111111111111).value, + u32Bits(0b11111111010000000000000000000000).value, + u32Bits(0b11111110110000000000000000000000).value, + u32Bits(0b11111101110000000000000000000000).value, + u32Bits(0b11111011110000000000000000000000).value, + u32Bits(0b11110111110000000000000000000000).value, + u32Bits(0b11101111110000000000000000000000).value, + u32Bits(0b11011111110000000000000000000000).value, + u32Bits(0b10111111110000000000000000000000).value, + u32Bits(0b01111111011111111111111111111111).value, + u32Bits(0b01111111010000000000000000000000).value, + u32Bits(0b01111110110000000000000000000000).value, + u32Bits(0b01111101110000000000000000000000).value, + u32Bits(0b01111011110000000000000000000000).value, + u32Bits(0b01110111110000000000000000000000).value, + u32Bits(0b01101111110000000000000000000000).value, + u32Bits(0b01011111110000000000000000000000).value, + u32Bits(0b00111111110000000000000000000000).value, + ]; + + const cases = values.map(u => { + return { + input: abstractFloat(uint32ToFloat32(u)), + expected: i32(u), + }; + }); + + await run(t, bitcastBuilder('i32', t.params), [Type.abstractFloat], Type.i32, t.params, cases); + }); + +g.test('af_to_u32') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract float to u32 tests`) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const values = [ + 0, + 1, + 10, + 256, + u32Bits(0b11111111011111111111111111111111).value, + u32Bits(0b11111111010000000000000000000000).value, + u32Bits(0b11111110110000000000000000000000).value, + u32Bits(0b11111101110000000000000000000000).value, + u32Bits(0b11111011110000000000000000000000).value, + u32Bits(0b11110111110000000000000000000000).value, + u32Bits(0b11101111110000000000000000000000).value, + u32Bits(0b11011111110000000000000000000000).value, + u32Bits(0b10111111110000000000000000000000).value, + u32Bits(0b01111111011111111111111111111111).value, + u32Bits(0b01111111010000000000000000000000).value, + u32Bits(0b01111110110000000000000000000000).value, + u32Bits(0b01111101110000000000000000000000).value, + u32Bits(0b01111011110000000000000000000000).value, + u32Bits(0b01110111110000000000000000000000).value, + u32Bits(0b01101111110000000000000000000000).value, + u32Bits(0b01011111110000000000000000000000).value, + u32Bits(0b00111111110000000000000000000000).value, + ]; + + const cases = values.map(u => { + return { + input: abstractFloat(uint32ToFloat32(u)), + expected: u32(u), + }; + }); + + await run(t, bitcastBuilder('u32', t.params), [Type.abstractFloat], Type.u32, t.params, cases); + }); + +g.test('af_to_vec2f16') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract float to f16 tests`) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase('shader-f16'); + }) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('af_to_vec2_f16'); + + await run( + t, + bitcastBuilder('vec2<f16>', t.params), + [Type.abstractFloat], + Type.vec2h, + t.params, + cases + ); + }); + +g.test('vec2af_to_vec4f16') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract float to f16 tests`) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase('shader-f16'); + }) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('vec2_af_to_vec4_f16'); + + await run( + t, + bitcastBuilder('vec4<f16>', t.params), + [Type.vec(2, Type.abstractFloat)], + Type.vec4h, + t.params, + cases + ); + }); + +// Abstract Int +g.test('ai_to_i32') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract int to i32 tests`) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('alias', [false, true]) + ) + .fn(async t => { + const cases = await d.get('ai_to_i32'); + await run(t, bitcastBuilder('i32', t.params), [Type.abstractInt], Type.i32, t.params, cases); + }); + +g.test('ai_to_u32') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract int to u32 tests`) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('alias', [false, true]) + ) + .fn(async t => { + const cases = await d.get('ai_to_u32'); + await run(t, bitcastBuilder('u32', t.params), [Type.abstractInt], Type.u32, t.params, cases); + }); + +g.test('ai_to_f32') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast abstract int to f32 tests`) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('alias', [false, true]) + ) + .fn(async t => { + const cases = await d.get('ai_to_f32'); + await run(t, bitcastBuilder('f32', t.params), [Type.abstractInt], Type.f32, t.params, cases); + }); + +g.test('ai_to_vec2h') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast ai to vec2h tests`) + .params(u => u.combine('inputSource', onlyConstInputSource).combine('alias', [false, true])) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase('shader-f16'); + }) + .fn(async t => { + const cases = await d.get('ai_to_vec2_f16'); await run( t, - bitcastBuilder('vec2<f32>', t.params), - [TypeVec(4, TypeF16)], - TypeVec(2, TypeF32), + bitcastBuilder('vec2<f16>', t.params), + [Type.abstractInt], + Type.vec2h, t.params, cases ); }); + +g.test('vec2ai_to_vec4h') + .specURL('https://www.w3.org/TR/WGSL/#bitcast-builtin') + .desc(`bitcast vec2ai to vec4h tests`) + .params(u => u.combine('inputSource', onlyConstInputSource).combine('alias', [false, true])) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase('shader-f16'); + }) + .fn(async t => { + const cases = await d.get('vec2_ai_to_vec4_f16'); + await run(t, bitcastBuilder('vec4<f16>', t.params), [Type.vec2ai], Type.vec4h, t.params, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts index 282feea703..0afd8c3980 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts @@ -1,5 +1,6 @@ import { abstractFloatShaderBuilder, + abstractIntShaderBuilder, basicExpressionBuilder, basicExpressionWithPredeclarationBuilder, ShaderBuilder, @@ -11,10 +12,15 @@ export function builtin(name: string): ShaderBuilder { } /* @returns a ShaderBuilder that calls the builtin with the given name that returns AbstractFloats */ -export function abstractBuiltin(name: string): ShaderBuilder { +export function abstractFloatBuiltin(name: string): ShaderBuilder { return abstractFloatShaderBuilder(values => `${name}(${values.join(', ')})`); } +/* @returns a ShaderBuilder that calls the builtin with the given name that returns AbstractInts */ +export function abstractIntBuiltin(name: string): ShaderBuilder { + return abstractIntShaderBuilder(values => `${name}(${values.join(', ')})`); +} + /* @returns a ShaderBuilder that calls the builtin with the given name and has given predeclaration */ export function builtinWithPredeclaration(name: string, predeclaration: string): ShaderBuilder { return basicExpressionWithPredeclarationBuilder( diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.cache.ts new file mode 100644 index 0000000000..c0178d9b83 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.cache.ts @@ -0,0 +1,26 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +const kSmallMagnitudeTestValues = [0.1, 0.9, 1.0, 1.1, 1.9, -0.1, -0.9, -1.0, -1.1, -1.9]; + +// See https://github.com/gpuweb/cts/issues/2766 for details +const kIssue2766Value = { + f32: 0x8000_0000, + f16: 0x8000, + abstract: 0x8000_0000_0000_0000, +}; + +// Cases: [f32|f16] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [...kSmallMagnitudeTestValues, kIssue2766Value[trait], ...FP[trait].scalarRange()], + 'unfiltered', + FP[trait].ceilInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('ceil', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts index 6cdf90986b..842875f094 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'ceil' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn ceil(e: T ) -> T Returns the ceiling of e. Component-wise when T is a vector. @@ -10,70 +10,33 @@ Returns the ceiling of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './ceil.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('ceil', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - // Small positive numbers - 0.1, - 0.9, - 1.0, - 1.1, - 1.9, - // Small negative numbers - -0.1, - -0.9, - -1.0, - -1.1, - -1.9, - 0x80000000, // https://github.com/gpuweb/cts/issues/2766 - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.ceilInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - // Small positive numbers - 0.1, - 0.9, - 1.0, - 1.1, - 1.9, - // Small negative numbers - -0.1, - -0.9, - -1.0, - -1.1, - -1.9, - 0x8000, // https://github.com/gpuweb/cts/issues/2766 - ...fullF16Range(), - ], - 'unfiltered', - FP.f16.ceilInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('ceil'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -83,7 +46,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('ceil'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('ceil'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -97,5 +60,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('ceil'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('ceil'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.cache.ts new file mode 100644 index 0000000000..909d15e7e7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.cache.ts @@ -0,0 +1,131 @@ +import { kValue } from '../../../../../util/constants.js'; +import { ScalarType, Type } from '../../../../../util/conversion.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { maxBigInt, minBigInt } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +const u32Values = [0, 1, 2, 3, 0x70000000, 0x80000000, kValue.u32.max]; + +const i32Values = [ + kValue.i32.negative.min, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + 0x70000000, + kValue.i32.positive.max, +]; + +const abstractFloatValues = [ + kValue.i64.negative.min, + -3n, + -2n, + -1n, + 0n, + 1n, + 2n, + 3n, + 0x70000000n, + kValue.i64.positive.max, +]; + +/** @returns a set of clamp test cases from an ascending list of concrete integer values */ +function generateConcreteIntegerTestCases( + test_values: Array<number>, + type: ScalarType, + stage: 'const' | 'non_const' +): Array<Case> { + return test_values.flatMap(low => + test_values.flatMap(high => + stage === 'const' && low > high + ? [] + : test_values.map(e => ({ + input: [type.create(e), type.create(low), type.create(high)], + expected: type.create(Math.min(Math.max(e, low), high)), + })) + ) + ); +} + +/** @returns a set of clamp test cases from an ascending list of abstract integer values */ +function generateAbstractIntegerTestCases(test_values: Array<bigint>): Array<Case> { + return test_values.flatMap(low => + test_values.flatMap(high => + low > high + ? [] + : test_values.map(e => ({ + input: [ + Type.abstractInt.create(e), + Type.abstractInt.create(low), + Type.abstractInt.create(high), + ], + expected: Type.abstractInt.create(minBigInt(maxBigInt(e, low), high)), + })) + ) + ); +} + +function generateFloatTestCases( + test_values: readonly number[], + trait: 'f32' | 'f16' | 'abstract', + stage: 'const' | 'non_const' +): Array<Case> { + return test_values.flatMap(low => + test_values.flatMap(high => + stage === 'const' && low > high + ? [] + : test_values.flatMap(e => { + const c = FP[trait].makeScalarTripleToIntervalCase( + e, + low, + high, + stage === 'const' ? 'finite' : 'unfiltered', + ...FP[trait].clampIntervals + ); + return c === undefined ? [] : [c]; + }) + ) + ); +} + +// Cases: [f32|f16|abstract]_[non_]const +// abstract_non_const is empty and unused +const fp_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return generateFloatTestCases( + FP[trait].sparseScalarRange(), + trait, + nonConst ? 'non_const' : 'const' + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('clamp', { + u32_non_const: () => { + return generateConcreteIntegerTestCases(u32Values, Type.u32, 'non_const'); + }, + u32_const: () => { + return generateConcreteIntegerTestCases(u32Values, Type.u32, 'const'); + }, + i32_non_const: () => { + return generateConcreteIntegerTestCases(i32Values, Type.i32, 'non_const'); + }, + i32_const: () => { + return generateConcreteIntegerTestCases(i32Values, Type.i32, 'const'); + }, + abstract_int: () => { + return generateAbstractIntegerTestCases(abstractFloatValues); + }, + ...fp_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts index 0113fd656f..0b524bccf0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts @@ -1,12 +1,12 @@ export const description = ` Execution tests for the 'clamp' builtin function -S is AbstractInt, i32, or u32 +S is abstract-int, i32, or u32 T is S or vecN<S> @const fn clamp(e: T , low: T, high: T) -> T Returns min(max(e,low),high). Component-wise when T is a vector. -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const clamp(e: T , low: T , high: T) -> T Returns either min(max(e,low),high), or the median of the three values e, low, high. @@ -15,117 +15,33 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { - ScalarType, - TypeF32, - TypeF16, - TypeI32, - TypeU32, - TypeAbstractFloat, -} from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { sparseF32Range, sparseF16Range, sparseF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, onlyConstInputSource, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, abstractIntBuiltin, builtin } from './builtin.js'; +import { d } from './clamp.cache.js'; export const g = makeTestGroup(GPUTest); -const u32Values = [0, 1, 2, 3, 0x70000000, 0x80000000, kValue.u32.max]; - -const i32Values = [ - kValue.i32.negative.min, - -3, - -2, - -1, - 0, - 1, - 2, - 3, - 0x70000000, - kValue.i32.positive.max, -]; - -export const d = makeCaseCache('clamp', { - u32_non_const: () => { - return generateIntegerTestCases(u32Values, TypeU32, 'non-const'); - }, - u32_const: () => { - return generateIntegerTestCases(u32Values, TypeU32, 'const'); - }, - i32_non_const: () => { - return generateIntegerTestCases(i32Values, TypeI32, 'non-const'); - }, - i32_const: () => { - return generateIntegerTestCases(i32Values, TypeI32, 'const'); - }, - f32_const: () => { - return generateFloatTestCases(sparseF32Range(), 'f32', 'const'); - }, - f32_non_const: () => { - return generateFloatTestCases(sparseF32Range(), 'f32', 'non-const'); - }, - f16_const: () => { - return generateFloatTestCases(sparseF16Range(), 'f16', 'const'); - }, - f16_non_const: () => { - return generateFloatTestCases(sparseF16Range(), 'f16', 'non-const'); - }, - abstract: () => { - return generateFloatTestCases(sparseF64Range(), 'abstract', 'const'); - }, -}); - -/** @returns a set of clamp test cases from an ascending list of integer values */ -function generateIntegerTestCases( - test_values: Array<number>, - type: ScalarType, - stage: 'const' | 'non-const' -): Array<Case> { - return test_values.flatMap(low => - test_values.flatMap(high => - stage === 'const' && low > high - ? [] - : test_values.map(e => ({ - input: [type.create(e), type.create(low), type.create(high)], - expected: type.create(Math.min(Math.max(e, low), high)), - })) - ) - ); -} - -function generateFloatTestCases( - test_values: readonly number[], - trait: 'f32' | 'f16' | 'abstract', - stage: 'const' | 'non-const' -): Array<Case> { - return test_values.flatMap(low => - test_values.flatMap(high => - stage === 'const' && low > high - ? [] - : test_values.flatMap(e => { - const c = FP[trait].makeScalarTripleToIntervalCase( - e, - low, - high, - stage === 'const' ? 'finite' : 'unfiltered', - ...FP[trait].clampIntervals - ); - return c === undefined ? [] : [c]; - }) - ) - ); -} - g.test('abstract_int') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') .desc(`abstract int tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_int'); + await run( + t, + abstractIntBuiltin('clamp'), + [Type.abstractInt, Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); g.test('u32') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') @@ -135,7 +51,7 @@ g.test('u32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const'); - await run(t, builtin('clamp'), [TypeU32, TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, builtin('clamp'), [Type.u32, Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('i32') @@ -146,7 +62,7 @@ g.test('i32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'i32_const' : 'i32_non_const'); - await run(t, builtin('clamp'), [TypeI32, TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, builtin('clamp'), [Type.i32, Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('abstract_float') @@ -158,12 +74,12 @@ g.test('abstract_float') .combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - const cases = await d.get('abstract'); + const cases = await d.get('abstract_const'); await run( t, - abstractBuiltin('clamp'), - [TypeAbstractFloat, TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBuiltin('clamp'), + [Type.abstractFloat, Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -177,7 +93,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('clamp'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('clamp'), [Type.f32, Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -191,5 +107,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('clamp'), [TypeF16, TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('clamp'), [Type.f16, Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.cache.ts new file mode 100644 index 0000000000..11c9f50a1f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [ + // Well-defined accuracy range + ...linearRange(-Math.PI, Math.PI, 100), + ...FP[trait].scalarRange(), + ], + trait === 'abstract' ? 'finite' : 'unfiltered', + // cos has an absolute accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].cosInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('cos', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts index 723bca2efd..5675f220bf 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'cos' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn cos(e: T ) -> T Returns the cosine of e. Component-wise when T is a vector. @@ -9,48 +9,33 @@ Returns the cosine of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './cos.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('cos', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - // Well-defined accuracy range - ...linearRange(-Math.PI, Math.PI, 1000), - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.cosInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - // Well-defined accuracy range - ...linearRange(-Math.PI, Math.PI, 1000), - ...fullF16Range(), - ], - 'unfiltered', - FP.f16.cosInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('cos'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -66,7 +51,7 @@ TODO(#792): Decide what the ground-truth is for these tests. [1] ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('cos'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('cos'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -80,5 +65,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('cos'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('cos'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.cache.ts new file mode 100644 index 0000000000..0c90f178df --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + nonConst ? 'unfiltered' : 'finite', + // cosh has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].coshInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('cosh', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts index 37fb961c98..d043df9527 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'cosh' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn cosh(e: T ) -> T Returns the hyperbolic cosine of e. Component-wise when T is a vector @@ -9,38 +9,33 @@ Returns the hyperbolic cosine of e. Component-wise when T is a vector import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './cosh.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('cosh', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'finite', FP.f32.coshInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.coshInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'finite', FP.f16.coshInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'unfiltered', FP.f16.coshInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('cosh'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -50,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('cosh'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('cosh'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -64,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('cosh'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('cosh'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts index cfae4bb6e0..ea0c38ae58 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts @@ -12,7 +12,7 @@ Also known as "clz" in some languages. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeU32, u32Bits, u32, TypeI32, i32Bits, i32 } from '../../../../../util/conversion.js'; +import { Type, u32Bits, u32, i32Bits, i32 } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -27,7 +27,7 @@ g.test('u32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('countLeadingZeros'), [TypeU32], TypeU32, cfg, [ + await run(t, builtin('countLeadingZeros'), [Type.u32], Type.u32, cfg, [ // Zero { input: u32Bits(0b00000000000000000000000000000000), expected: u32(32) }, @@ -142,7 +142,7 @@ g.test('i32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('countLeadingZeros'), [TypeI32], TypeI32, cfg, [ + await run(t, builtin('countLeadingZeros'), [Type.i32], Type.i32, cfg, [ // Zero { input: i32Bits(0b00000000000000000000000000000000), expected: i32(32) }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts index f0be916285..1937e04283 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts @@ -11,7 +11,7 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeU32, u32Bits, u32, TypeI32, i32Bits, i32 } from '../../../../../util/conversion.js'; +import { Type, u32Bits, u32, i32Bits, i32 } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -26,7 +26,7 @@ g.test('u32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('countOneBits'), [TypeU32], TypeU32, cfg, [ + await run(t, builtin('countOneBits'), [Type.u32], Type.u32, cfg, [ // Zero { input: u32Bits(0b00000000000000000000000000000000), expected: u32(0) }, @@ -141,7 +141,7 @@ g.test('i32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('countOneBits'), [TypeI32], TypeI32, cfg, [ + await run(t, builtin('countOneBits'), [Type.i32], Type.i32, cfg, [ // Zero { input: i32Bits(0b00000000000000000000000000000000), expected: i32(0) }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts index d0b3198f49..3392a47810 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts @@ -12,7 +12,7 @@ Also known as "ctz" in some languages. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { i32, i32Bits, TypeI32, u32, TypeU32, u32Bits } from '../../../../../util/conversion.js'; +import { i32, i32Bits, Type, u32, u32Bits } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -27,7 +27,7 @@ g.test('u32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('countTrailingZeros'), [TypeU32], TypeU32, cfg, [ + await run(t, builtin('countTrailingZeros'), [Type.u32], Type.u32, cfg, [ // Zero { input: u32Bits(0b00000000000000000000000000000000), expected: u32(32) }, @@ -142,7 +142,7 @@ g.test('i32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('countTrailingZeros'), [TypeI32], TypeI32, cfg, [ + await run(t, builtin('countTrailingZeros'), [Type.i32], Type.i32, cfg, [ // Zero { input: i32Bits(0b00000000000000000000000000000000), expected: i32(32) }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.cache.ts new file mode 100644 index 0000000000..396789b4ed --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.cache.ts @@ -0,0 +1,25 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +// abstract_non_const is empty and not used +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateVectorPairToVectorCases( + FP[trait].vectorRange(3), + FP[trait].vectorRange(3), + nonConst ? 'unfiltered' : 'finite', + // cross has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].crossInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('cross', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts index 2b0b3e58ce..cc32537076 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts @@ -1,77 +1,32 @@ export const description = ` Execution tests for the 'cross' builtin function -T is AbstractFloat, f32, or f16 +T is abstract-float, f32, or f16 @const fn cross(e1: vec3<T> ,e2: vec3<T>) -> vec3<T> Returns the cross product of e1 and e2. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeF16, TypeF32, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { sparseVectorF64Range, vectorF16Range, vectorF32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './cross.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('cross', { - f32_const: () => { - return FP.f32.generateVectorPairToVectorCases( - vectorF32Range(3), - vectorF32Range(3), - 'finite', - FP.f32.crossInterval - ); - }, - f32_non_const: () => { - return FP.f32.generateVectorPairToVectorCases( - vectorF32Range(3), - vectorF32Range(3), - 'unfiltered', - FP.f32.crossInterval - ); - }, - f16_const: () => { - return FP.f16.generateVectorPairToVectorCases( - vectorF16Range(3), - vectorF16Range(3), - 'finite', - FP.f16.crossInterval - ); - }, - f16_non_const: () => { - return FP.f16.generateVectorPairToVectorCases( - vectorF16Range(3), - vectorF16Range(3), - 'unfiltered', - FP.f16.crossInterval - ); - }, - abstract: () => { - return FP.abstract.generateVectorPairToVectorCases( - sparseVectorF64Range(3), - sparseVectorF64Range(3), - 'finite', - FP.abstract.crossInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => u.combine('inputSource', onlyConstInputSource)) .fn(async t => { - const cases = await d.get('abstract'); + const cases = await d.get('abstract_const'); await run( t, - abstractBuiltin('cross'), - [TypeVec(3, TypeAbstractFloat), TypeVec(3, TypeAbstractFloat)], - TypeVec(3, TypeAbstractFloat), + abstractFloatBuiltin('cross'), + [Type.vec(3, Type.abstractFloat), Type.vec(3, Type.abstractFloat)], + Type.vec(3, Type.abstractFloat), t.params, cases ); @@ -83,14 +38,7 @@ g.test('f32') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run( - t, - builtin('cross'), - [TypeVec(3, TypeF32), TypeVec(3, TypeF32)], - TypeVec(3, TypeF32), - t.params, - cases - ); + await run(t, builtin('cross'), [Type.vec3f, Type.vec3f], Type.vec3f, t.params, cases); }); g.test('f16') @@ -102,12 +50,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run( - t, - builtin('cross'), - [TypeVec(3, TypeF16), TypeVec(3, TypeF16)], - TypeVec(3, TypeF16), - t.params, - cases - ); + await run(t, builtin('cross'), [Type.vec3h, Type.vec3h], Type.vec3h, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.cache.ts new file mode 100644 index 0000000000..16abe41f34 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.cache.ts @@ -0,0 +1,24 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +// abstract_non_const is empty and not used +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + nonConst ? 'unfiltered' : 'finite', + // degrees has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].degreesInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('degrees', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts index f82153ffca..e8589c9933 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'degrees' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<T> @const fn degrees(e1: T ) -> T Converts radians to degrees, approximating e1 × 180 ÷ π. Component-wise when T is a vector @@ -9,46 +9,14 @@ Converts radians to degrees, approximating e1 × 180 ÷ π. Component-wise when import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeF16, TypeF32 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF16Range, fullF32Range, fullF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './degrees.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('degrees', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'finite', FP.f32.degreesInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases( - fullF32Range(), - 'unfiltered', - FP.f32.degreesInterval - ); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'finite', FP.f16.degreesInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases( - fullF16Range(), - 'unfiltered', - FP.f16.degreesInterval - ); - }, - abstract: () => { - return FP.abstract.generateScalarToIntervalCases( - fullF64Range(), - 'finite', - FP.abstract.degreesInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) @@ -58,12 +26,12 @@ g.test('abstract_float') .combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - const cases = await d.get('abstract'); + const cases = await d.get('abstract_const'); await run( t, - abstractBuiltin('degrees'), - [TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBuiltin('degrees'), + [Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -77,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('degrees'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('degrees'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -91,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('degrees'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('degrees'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/derivatives.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/derivatives.cache.ts new file mode 100644 index 0000000000..ebd414f395 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/derivatives.cache.ts @@ -0,0 +1,14 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { sparseScalarF32Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('derivatives', { + scalar: () => { + return FP.f32.generateScalarPairToIntervalCases( + sparseScalarF32Range(), + sparseScalarF32Range(), + 'unfiltered', + FP.f32.subtractionInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/derivatives.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/derivatives.ts new file mode 100644 index 0000000000..2af4a48096 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/derivatives.ts @@ -0,0 +1,215 @@ +import { GPUTest } from '../../../../../gpu_test.js'; +import { Type, Value } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { toComparator } from '../../expectation.js'; +import { packScalarsToVector } from '../../expression.js'; + +/** + * Run a test for a derivative builtin function. + * @param t the GPUTest + * @param cases list of test cases to run + * @param builtin the builtin function to test + * @param non_uniform_discard if true, one of each pair of invocations will discard + * @param vectorize if defined, the vector width to use (2, 3, or 4) + */ +export function runDerivativeTest( + t: GPUTest, + cases: Case[], + builtin: string, + non_uniform_discard: boolean, + vectorize?: number +) { + // If the 'vectorize' config option was provided, pack the cases into vectors. + let type: Type = Type.f32; + if (vectorize !== undefined) { + const packed = packScalarsToVector([type, type], type, cases, vectorize); + cases = packed.cases; + type = packed.resultType; + } + + //////////////////////////////////////////////////////////////// + // The two input values for a given case are distributed to two different invocations in a quad. + // We will populate a storage buffer with these input values laid out sequentially: + // [ case_0_input_1, case_0_input_0, case_1_input_1, case_1_input_0, ...] + // + // The render pipeline will be launched several times over a viewport size of (2, 2). Each draw + // call will execute a single quad (four fragment invocation), which will exercise two test cases. + // Each of these draw calls will use a different instance index, which is forwarded to the + // fragment shader. Each invocation will determine its index into the storage buffer using its + // fragment position and the instance index for that draw call. + // + // Consider two draw calls that test 4 cases (c_0, c_1, c_2, c_3). + // + // For derivatives along the 'x' direction, the mapping from fragment position to case input is: + // Quad 0: | c_0_i_1 | c_0_i_0 | Quad 1: | c_2_i_1 | c_2_i_0 | + // | c_1_i_1 | c_1_i_0 | | c_3_i_1 | c_3_i_0 | + // + // For derivatives along the 'y' direction, the mapping from fragment position to case input is: + // Quad 0: | c_0_i_1 | c_1_i_1 | Quad 1: | c_2_i_1 | c_3_i_1 | + // | c_0_i_0 | c_1_i_0 | | c_2_i_0 | c_3_i_0 | + // + //////////////////////////////////////////////////////////////// + + // Determine the direction of the derivative ('x' or 'y') from the builtin name. + const dir = builtin[3]; + + // Determine the WGSL type to use in the shader, and the stride in bytes between values. + let valueStride = 4; + let wgslType = 'f32'; + if (vectorize) { + wgslType = `vec${vectorize}f`; + valueStride = vectorize * 4; + if (vectorize === 3) { + valueStride = 16; + } + } + + // Define a vertex shader that draws a triangle over the full viewport, and a fragment shader that + // calls the derivative builtin with a value loaded from that fragment's index into the storage + // buffer (determined using the quad index and fragment position, as described above). + const code = ` +struct CaseInfo { + @builtin(position) position: vec4f, + @location(0) @interpolate(flat) quad_idx: u32, +} + +@vertex +fn vert(@builtin(vertex_index) vertex_idx: u32, + @builtin(instance_index) instance_idx: u32) -> CaseInfo { + const kVertices = array( + vec2f(-2, -2), + vec2f( 2, -2), + vec2f( 0, 2), + ); + return CaseInfo(vec4(kVertices[vertex_idx], 0, 1), instance_idx); +} + +@group(0) @binding(0) var<storage, read> inputs : array<${wgslType}>; +@group(0) @binding(1) var<storage, read_write> outputs : array<${wgslType}>; + +@fragment +fn frag(info : CaseInfo) { + let case_idx = u32(info.position.${dir === 'x' ? 'y' : 'x'}); + let inv_idx = u32(info.position.${dir}); + let index = info.quad_idx*4 + case_idx*2 + inv_idx; + let input = inputs[index]; + ${non_uniform_discard ? 'if inv_idx == 0 { discard; }' : ''} + outputs[index] = ${builtin}(input); +} +`; + + // Create the render pipeline. + const module = t.device.createShaderModule({ code }); + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { module, targets: [{ format: 'rgba8unorm', writeMask: 0 }] }, + }); + + // Create storage buffers to hold the inputs and outputs. + const bufferSize = cases.length * 2 * valueStride; + const inputBuffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.STORAGE, + mappedAtCreation: true, + }); + const outputBuffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + + // Populate the input storage buffer with case input values. + const valuesData = new Uint8Array(inputBuffer.getMappedRange()); + for (let i = 0; i < cases.length; i++) { + const inputs = cases[i].input as ReadonlyArray<Value>; + inputs[0].copyTo(valuesData, (i * 2 + 1) * valueStride); + inputs[1].copyTo(valuesData, i * 2 * valueStride); + } + inputBuffer.unmap(); + + // Create a bind group for the storage buffers. + const group = t.device.createBindGroup({ + entries: [ + { binding: 0, resource: { buffer: inputBuffer } }, + { binding: 1, resource: { buffer: outputBuffer } }, + ], + layout: pipeline.getBindGroupLayout(0), + }); + + // Create a texture to use as a color attachment. + // We only need this for launching the desired number of fragment invocations. + const colorAttachment = t.device.createTexture({ + size: { width: 2, height: 2 }, + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + // Submit the render pass to the device. + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorAttachment.createView(), + loadOp: 'clear', + storeOp: 'discard', + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, group); + for (let quad = 0; quad < cases.length / 2; quad++) { + pass.draw(3, 1, undefined, quad); + } + pass.end(); + t.queue.submit([encoder.finish()]); + + // Check the outputs match the expected results. + t.expectGPUBufferValuesPassCheck( + outputBuffer, + (outputData: Uint8Array) => { + for (let i = 0; i < cases.length; i++) { + const c = cases[i]; + + // Both invocations involved in the derivative should get the same result. + for (let d = 0; d < 2; d++) { + if (non_uniform_discard && d === 0) { + continue; + } + + const index = (i * 2 + d) * valueStride; + const result = type.read(outputData, index); + const cmp = toComparator(c.expected).compare(result); + if (!cmp.matched) { + // If this is a coarse derivative, the implementation is also allowed to calculate only + // one of the two derivatives and return that result to all of the invocations. + if (!builtin.endsWith('Fine')) { + const c0 = cases[i % 2 === 0 ? i + 1 : i - 1]; + const cmp0 = toComparator(c0.expected).compare(result); + if (!cmp0.matched) { + return new Error(` + 1st pair: (${(c.input as Value[]).join(', ')}) + expected: ${cmp.expected} + + 2nd pair: (${(c0.input as Value[]).join(', ')}) + expected: ${cmp0.expected} + + returned: ${result}`); + } + } else { + return new Error(` + inputs: (${(c.input as Value[]).join(', ')}) + expected: ${cmp.expected} + + returned: ${result}`); + } + } + } + } + return undefined; + }, + { + type: Uint8Array, + typedLength: bufferSize, + } + ); +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.cache.ts new file mode 100644 index 0000000000..ccd073bce5 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.cache.ts @@ -0,0 +1,99 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Accuracy for determinant is only defined for e, where e is an integer and +// |e| < quadroot(2**21) [~38], +// due to computational complexity of calculating the general solution for 4x4, +// so custom matrices are used. +// +// Note: For 2x2 and 3x3 the limits are squareroot and cuberoot instead of +// quadroot, but using the tighter 4x4 limits for all cases for simplicity. +const kDeterminantValues = [-38, -10, -5, -1, 0, 1, 5, 10, 38]; + +const kDeterminantMatrixValues = { + 2: kDeterminantValues.map((f, idx) => [ + [idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx], + [idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx], + ]), + 3: kDeterminantValues.map((f, idx) => [ + [idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx], + [idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx], + [idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx], + ]), + 4: kDeterminantValues.map((f, idx) => [ + [ + idx % 16 === 0 ? f : idx, + idx % 16 === 1 ? f : -idx, + idx % 16 === 2 ? f : idx, + idx % 16 === 3 ? f : -idx, + ], + [ + idx % 16 === 4 ? f : -idx, + idx % 16 === 5 ? f : idx, + idx % 16 === 6 ? f : -idx, + idx % 16 === 7 ? f : idx, + ], + [ + idx % 16 === 8 ? f : idx, + idx % 16 === 9 ? f : -idx, + idx % 16 === 10 ? f : idx, + idx % 16 === 11 ? f : -idx, + ], + [ + idx % 16 === 12 ? f : -idx, + idx % 16 === 13 ? f : idx, + idx % 16 === 14 ? f : -idx, + idx % 16 === 15 ? f : idx, + ], + ]), +}; + +// Cases: f32_matDxD_[non_]const +const f32_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`f32_mat${dim}x${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixToScalarCases( + kDeterminantMatrixValues[dim], + nonConst ? 'unfiltered' : 'finite', + FP.f32.determinantInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: f16_matDxD_[non_]const +const f16_cases = ([2, 3, 4] as const) + .flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`f16_mat${dim}x${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f16.generateMatrixToScalarCases( + kDeterminantMatrixValues[dim], + nonConst ? 'unfiltered' : 'finite', + FP.f16.determinantInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: abstract_matDxD +const abstract_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`abstract_mat${dim}x${dim}`]: () => { + return FP.abstract.generateMatrixToScalarCases( + kDeterminantMatrixValues[dim], + 'finite', + // determinant has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP.f32.determinantInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('determinant', { + ...f32_cases, + ...f16_cases, + ...abstract_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts index f08f4f0b6b..638af80aca 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts @@ -1,109 +1,37 @@ export const description = ` Execution tests for the 'determinant' builtin function -T is AbstractFloat, f32, or f16 +T is abstract-float, f32, or f16 @const determinant(e: matCxC<T> ) -> T Returns the determinant of e. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeMat } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './determinant.cache.js'; export const g = makeTestGroup(GPUTest); -// Accuracy for determinant is only defined for e, where e is an integer and -// |e| < quadroot(2**21) [~38], -// due to computational complexity of calculating the general solution for 4x4, -// so custom matrices are used. -// -// Note: For 2x2 and 3x3 the limits are squareroot and cuberoot instead of -// quadroot, but using the tighter 4x4 limits for all cases for simplicity. -const kDeterminantValues = [-38, -10, -5, -1, 0, 1, 5, 10, 38]; - -const kDeterminantMatrixValues = { - 2: kDeterminantValues.map((f, idx) => [ - [idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx], - [idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx], - ]), - 3: kDeterminantValues.map((f, idx) => [ - [idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx], - [idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx], - [idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx], - ]), - 4: kDeterminantValues.map((f, idx) => [ - [ - idx % 16 === 0 ? f : idx, - idx % 16 === 1 ? f : -idx, - idx % 16 === 2 ? f : idx, - idx % 16 === 3 ? f : -idx, - ], - [ - idx % 16 === 4 ? f : -idx, - idx % 16 === 5 ? f : idx, - idx % 16 === 6 ? f : -idx, - idx % 16 === 7 ? f : idx, - ], - [ - idx % 16 === 8 ? f : idx, - idx % 16 === 9 ? f : -idx, - idx % 16 === 10 ? f : idx, - idx % 16 === 11 ? f : -idx, - ], - [ - idx % 16 === 12 ? f : -idx, - idx % 16 === 13 ? f : idx, - idx % 16 === 14 ? f : -idx, - idx % 16 === 15 ? f : idx, - ], - ]), -}; - -// Cases: f32_matDxD_[non_]const -const f32_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`f32_mat${dim}x${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixToScalarCases( - kDeterminantMatrixValues[dim], - nonConst ? 'unfiltered' : 'finite', - FP.f32.determinantInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_matDxD_[non_]const -const f16_cases = ([2, 3, 4] as const) - .flatMap(dim => - ([true, false] as const).map(nonConst => ({ - [`f16_mat${dim}x${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateMatrixToScalarCases( - kDeterminantMatrixValues[dim], - nonConst ? 'unfiltered' : 'finite', - FP.f16.determinantInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('determinant', { - ...f32_cases, - ...f16_cases, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions') .desc(`abstract float tests`) - .params(u => u.combine('inputSource', allInputSources).combine('dimension', [2, 3, 4] as const)) - .unimplemented(); + .params(u => u.combine('inputSource', onlyConstInputSource).combine('dim', [2, 3, 4] as const)) + .fn(async t => { + const dim = t.params.dim; + const cases = await d.get(`abstract_mat${dim}x${dim}`); + await run( + t, + abstractFloatBuiltin('determinant'), + [Type.mat(dim, dim, Type.abstractFloat)], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions') @@ -116,7 +44,7 @@ g.test('f32') ? `f32_mat${dim}x${dim}_const` : `f32_mat${dim}x${dim}_non_const` ); - await run(t, builtin('determinant'), [TypeMat(dim, dim, TypeF32)], TypeF32, t.params, cases); + await run(t, builtin('determinant'), [Type.mat(dim, dim, Type.f32)], Type.f32, t.params, cases); }); g.test('f16') @@ -133,5 +61,5 @@ g.test('f16') ? `f16_mat${dim}x${dim}_const` : `f16_mat${dim}x${dim}_non_const` ); - await run(t, builtin('determinant'), [TypeMat(dim, dim, TypeF16)], TypeF16, t.params, cases); + await run(t, builtin('determinant'), [Type.mat(dim, dim, Type.f16)], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.cache.ts new file mode 100644 index 0000000000..0b37190cf7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.cache.ts @@ -0,0 +1,49 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const scalar_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarPairToIntervalCases( + FP[trait].scalarRange(), + FP[trait].scalarRange(), + nonConst ? 'unfiltered' : 'finite', + // distance has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].distanceInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: [f32|f16|abstract]_vecN_[non_]const +const vec_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`${trait}_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateVectorPairToIntervalCases( + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + nonConst ? 'unfiltered' : 'finite', + // distance has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].distanceInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('distance', { + ...scalar_cases, + ...vec_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts index 13cddf6403..5f03c49319 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'distance' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn distance(e1: T ,e2: T ) -> f32 Returns the distance between e1 and e2 (e.g. length(e1-e2)). @@ -10,97 +10,77 @@ Returns the distance between e1 and e2 (e.g. length(e1-e2)). import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { - fullF32Range, - fullF16Range, - sparseVectorF32Range, - sparseVectorF16Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; - -import { builtin } from './builtin.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; + +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './distance.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: f32_vecN_[non_]const -const f32_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f32_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorPairToIntervalCases( - sparseVectorF32Range(n), - sparseVectorF32Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f32.distanceInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_vecN_[non_]const -const f16_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f16_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorPairToIntervalCases( - sparseVectorF16Range(n), - sparseVectorF16Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f16.distanceInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('distance', { - f32_const: () => { - return FP.f32.generateScalarPairToIntervalCases( - fullF32Range(), - fullF32Range(), - 'finite', - FP.f32.distanceInterval - ); - }, - f32_non_const: () => { - return FP.f32.generateScalarPairToIntervalCases( - fullF32Range(), - fullF32Range(), - 'unfiltered', - FP.f32.distanceInterval +g.test('abstract_float') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc(`abstract float tests`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('distance'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, + t.params, + cases ); - }, - ...f32_vec_cases, - f16_const: () => { - return FP.f16.generateScalarPairToIntervalCases( - fullF16Range(), - fullF16Range(), - 'finite', - FP.f16.distanceInterval + }); + +g.test('abstract_float_vec2') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_const'); + await run( + t, + abstractFloatBuiltin('distance'), + [Type.vec2af, Type.vec2af], + Type.abstractFloat, + t.params, + cases ); - }, - f16_non_const: () => { - return FP.f16.generateScalarPairToIntervalCases( - fullF16Range(), - fullF16Range(), - 'unfiltered', - FP.f16.distanceInterval + }); + +g.test('abstract_float_vec3') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_const'); + await run( + t, + abstractFloatBuiltin('distance'), + [Type.vec3af, Type.vec3af], + Type.abstractFloat, + t.params, + cases ); - }, - ...f16_vec_cases, -}); + }); -g.test('abstract_float') - .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') - .desc(`abstract float tests`) - .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) - ) - .unimplemented(); +g.test('abstract_float_vec4') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_const'); + await run( + t, + abstractFloatBuiltin('distance'), + [Type.vec4af, Type.vec4af], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') @@ -108,7 +88,7 @@ g.test('f32') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('distance'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('distance'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f32_vec2') @@ -119,14 +99,7 @@ g.test('f32_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const' ); - await run( - t, - builtin('distance'), - [TypeVec(2, TypeF32), TypeVec(2, TypeF32)], - TypeF32, - t.params, - cases - ); + await run(t, builtin('distance'), [Type.vec2f, Type.vec2f], Type.f32, t.params, cases); }); g.test('f32_vec3') @@ -137,14 +110,7 @@ g.test('f32_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const' ); - await run( - t, - builtin('distance'), - [TypeVec(3, TypeF32), TypeVec(3, TypeF32)], - TypeF32, - t.params, - cases - ); + await run(t, builtin('distance'), [Type.vec3f, Type.vec3f], Type.f32, t.params, cases); }); g.test('f32_vec4') @@ -155,14 +121,7 @@ g.test('f32_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const' ); - await run( - t, - builtin('distance'), - [TypeVec(4, TypeF32), TypeVec(4, TypeF32)], - TypeF32, - t.params, - cases - ); + await run(t, builtin('distance'), [Type.vec4f, Type.vec4f], Type.f32, t.params, cases); }); g.test('f16') @@ -174,7 +133,7 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('distance'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('distance'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('f16_vec2') @@ -188,14 +147,7 @@ g.test('f16_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec2_const' : 'f16_vec2_non_const' ); - await run( - t, - builtin('distance'), - [TypeVec(2, TypeF16), TypeVec(2, TypeF16)], - TypeF16, - t.params, - cases - ); + await run(t, builtin('distance'), [Type.vec2h, Type.vec2h], Type.f16, t.params, cases); }); g.test('f16_vec3') @@ -209,14 +161,7 @@ g.test('f16_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec3_const' : 'f16_vec3_non_const' ); - await run( - t, - builtin('distance'), - [TypeVec(3, TypeF16), TypeVec(3, TypeF16)], - TypeF16, - t.params, - cases - ); + await run(t, builtin('distance'), [Type.vec3h, Type.vec3h], Type.f16, t.params, cases); }); g.test('f16_vec4') @@ -230,12 +175,5 @@ g.test('f16_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec4_const' : 'f16_vec4_non_const' ); - await run( - t, - builtin('distance'), - [TypeVec(4, TypeF16), TypeVec(4, TypeF16)], - TypeF16, - t.params, - cases - ); + await run(t, builtin('distance'), [Type.vec4h, Type.vec4h], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.cache.ts new file mode 100644 index 0000000000..6d1f22def1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.cache.ts @@ -0,0 +1,118 @@ +import { ROArrayArray } from '../../../../../../common/util/types.js'; +import { assert } from '../../../../../../common/util/util.js'; +import { kValue } from '../../../../../util/constants.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { + calculatePermutations, + sparseVectorI32Range, + sparseVectorI64Range, + sparseVectorU32Range, + vectorI32Range, + vectorI64Range, + vectorU32Range, +} from '../../../../../util/math.js'; +import { + generateVectorVectorToI32Cases, + generateVectorVectorToI64Cases, + generateVectorVectorToU32Cases, +} from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +function ai_dot(x: bigint[], y: bigint[]): bigint | undefined { + assert(x.length === y.length, 'Cannot calculate dot for vectors of different lengths'); + const multiplications = x.map((_, idx) => x[idx] * y[idx]); + if (multiplications.some(kValue.i64.isOOB)) return undefined; + + const result = multiplications.reduce((prev, curr) => prev + curr); + if (kValue.i64.isOOB(result)) return undefined; + + // The spec does not state the ordering of summation, so all the + // permutations are calculated and the intermediate results checked for + // going OOB. vec2 does not need permutations, since a + b === b + a. + // All the end results should be the same regardless of the order if the + // intermediate additions stay inbounds. + if (x.length !== 2) { + let wentOOB: boolean = false; + const permutations: ROArrayArray<bigint> = calculatePermutations(multiplications); + permutations.forEach(p => { + if (!wentOOB) { + p.reduce((prev, curr) => { + const next = prev + curr; + if (kValue.i64.isOOB(next)) { + wentOOB = true; + } + return next; + }); + } + }); + + if (wentOOB) return undefined; + } + + return !kValue.i64.isOOB(result) ? result : undefined; +} + +function ci_dot(x: number[], y: number[]): number | undefined { + assert(x.length === y.length, 'Cannot calculate dot for vectors of different lengths'); + return x.reduce((prev, _, idx) => prev + Math.imul(x[idx], y[idx]), 0); +} + +// Cases: [f32|f16|abstract]_vecN_[non_]const +const float_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(N => + ([true, false] as const).map(nonConst => ({ + [`${trait === 'abstract' ? 'abstract_float' : trait}_vec${N}_${ + nonConst ? 'non_const' : 'const' + }`]: () => { + // Emit an empty array for not const abstract float, since they will never be run + if (trait === 'abstract' && nonConst) { + return []; + } + // vec3 and vec4 require calculating all possible permutations, so their runtime is much + // longer per test, so only using sparse vectors for them. + return FP[trait].generateVectorPairToIntervalCases( + N === 2 ? FP[trait].vectorRange(2) : FP[trait].sparseVectorRange(N), + N === 2 ? FP[trait].vectorRange(2) : FP[trait].sparseVectorRange(N), + nonConst ? 'unfiltered' : 'finite', + // dot has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].dotInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const cases = { + ...float_cases, + abstract_int_vec2: () => { + return generateVectorVectorToI64Cases(vectorI64Range(2), vectorI64Range(2), ai_dot); + }, + abstract_int_vec3: () => { + return generateVectorVectorToI64Cases(sparseVectorI64Range(3), sparseVectorI64Range(3), ai_dot); + }, + abstract_int_vec4: () => { + return generateVectorVectorToI64Cases(sparseVectorI64Range(4), sparseVectorI64Range(4), ai_dot); + }, + i32_vec2: () => { + return generateVectorVectorToI32Cases(vectorI32Range(2), vectorI32Range(2), ci_dot); + }, + i32_vec3: () => { + return generateVectorVectorToI32Cases(sparseVectorI32Range(3), sparseVectorI32Range(3), ci_dot); + }, + i32_vec4: () => { + return generateVectorVectorToI32Cases(sparseVectorI32Range(4), sparseVectorI32Range(4), ci_dot); + }, + u32_vec2: () => { + return generateVectorVectorToU32Cases(vectorU32Range(2), vectorU32Range(2), ci_dot); + }, + u32_vec3: () => { + return generateVectorVectorToU32Cases(sparseVectorU32Range(3), sparseVectorU32Range(3), ci_dot); + }, + u32_vec4: () => { + return generateVectorVectorToU32Cases(sparseVectorU32Range(4), sparseVectorU32Range(4), ci_dot); + }, +}; + +export const d = makeCaseCache('dot', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts index 2726546183..188409bf21 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts @@ -1,87 +1,182 @@ export const description = ` Execution tests for the 'dot' builtin function -T is AbstractInt, AbstractFloat, i32, u32, f32, or f16 +T is Type.abstractInt, Type.abstractFloat, i32, u32, f32, or f16 @const fn dot(e1: vecN<T>,e2: vecN<T>) -> T Returns the dot product of e1 and e2. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { sparseVectorF32Range, vectorF32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, abstractIntBuiltin, builtin } from './builtin.js'; +import { d } from './dot.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: [f32|f16]_vecN_[non_]const -const cases = (['f32', 'f16'] as const) - .flatMap(trait => - ([2, 3, 4] as const).flatMap(N => - ([true, false] as const).map(nonConst => ({ - [`${trait}_vec${N}_${nonConst ? 'non_const' : 'const'}`]: () => { - // vec3 and vec4 require calculating all possible permutations, so their runtime is much - // longer per test, so only using sparse vectors for them. - return FP[trait].generateVectorPairToIntervalCases( - N === 2 ? vectorF32Range(2) : sparseVectorF32Range(N), - N === 2 ? vectorF32Range(2) : sparseVectorF32Range(N), - nonConst ? 'unfiltered' : 'finite', - FP[trait].dotInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('dot', cases); - -g.test('abstract_int') - .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') - .desc(`abstract int tests`) +g.test('abstract_int_vec2') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`abstract integer tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_int_vec2'); + await run( + t, + abstractIntBuiltin('dot'), + [Type.vec(2, Type.abstractInt), Type.vec(2, Type.abstractInt)], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('abstract_int_vec3') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`abstract integer tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_int_vec3'); + await run( + t, + abstractIntBuiltin('dot'), + [Type.vec(3, Type.abstractInt), Type.vec(3, Type.abstractInt)], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('abstract_int_vec4') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`abstract integer tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_int_vec4'); + await run( + t, + abstractIntBuiltin('dot'), + [Type.vec(4, Type.abstractInt), Type.vec(4, Type.abstractInt)], + Type.abstractInt, + t.params, + cases + ); + }); + +g.test('i32_vec2') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`i32 tests using vec2s`) .params(u => u.combine('inputSource', allInputSources)) - .unimplemented(); + .fn(async t => { + const cases = await d.get('i32_vec2'); + await run(t, builtin('dot'), [Type.vec2i, Type.vec2i], Type.i32, t.params, cases); + }); -g.test('i32') +g.test('i32_vec3') .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') - .desc(`i32 tests`) + .desc(`i32 tests using vec3s`) .params(u => u.combine('inputSource', allInputSources)) - .unimplemented(); + .fn(async t => { + const cases = await d.get('i32_vec3'); + await run(t, builtin('dot'), [Type.vec3i, Type.vec3i], Type.i32, t.params, cases); + }); -g.test('u32') +g.test('i32_vec4') .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') - .desc(`u32 tests`) + .desc(`i32 tests using vec4s`) .params(u => u.combine('inputSource', allInputSources)) - .unimplemented(); + .fn(async t => { + const cases = await d.get('i32_vec4'); + await run(t, builtin('dot'), [Type.vec4i, Type.vec4i], Type.i32, t.params, cases); + }); -g.test('abstract_float') +g.test('u32_vec2') .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') - .desc(`abstract float test`) + .desc(`u32 tests using vec2s`) .params(u => u.combine('inputSource', allInputSources)) - .unimplemented(); + .fn(async t => { + const cases = await d.get('u32_vec2'); + await run(t, builtin('dot'), [Type.vec2u, Type.vec2u], Type.u32, t.params, cases); + }); -g.test('f32_vec2') +g.test('u32_vec3') .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') - .desc(`f32 tests using vec2s`) + .desc(`u32 tests using vec3s`) .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { - const cases = await d.get( - t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const' + const cases = await d.get('u32_vec3'); + await run(t, builtin('dot'), [Type.vec3u, Type.vec3u], Type.u32, t.params, cases); + }); + +g.test('u32_vec4') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`u32 tests using vec4s`) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cases = await d.get('u32_vec4'); + await run(t, builtin('dot'), [Type.vec4u, Type.vec4u], Type.u32, t.params, cases); + }); + +g.test('abstract_float_vec2') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`abstract float tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_float_vec2_const'); + await run( + t, + abstractFloatBuiltin('dot'), + [Type.vec(2, Type.abstractFloat), Type.vec(2, Type.abstractFloat)], + Type.abstractFloat, + t.params, + cases ); + }); + +g.test('abstract_float_vec3') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`abstract float tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_float_vec3_const'); await run( t, - builtin('dot'), - [TypeVec(2, TypeF32), TypeVec(2, TypeF32)], - TypeF32, + abstractFloatBuiltin('dot'), + [Type.vec(3, Type.abstractFloat), Type.vec(3, Type.abstractFloat)], + Type.abstractFloat, t.params, cases ); }); +g.test('abstract_float_vec4') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`abstract float tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_float_vec4_const'); + await run( + t, + abstractFloatBuiltin('dot'), + [Type.vec(4, Type.abstractFloat), Type.vec(4, Type.abstractFloat)], + Type.abstractFloat, + t.params, + cases + ); + }); + +g.test('f32_vec2') + .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') + .desc(`f32 tests using vec2s`) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cases = await d.get( + t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const' + ); + await run(t, builtin('dot'), [Type.vec2f, Type.vec2f], Type.f32, t.params, cases); + }); + g.test('f32_vec3') .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions') .desc(`f32 tests using vec3s`) @@ -90,14 +185,7 @@ g.test('f32_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const' ); - await run( - t, - builtin('dot'), - [TypeVec(3, TypeF32), TypeVec(3, TypeF32)], - TypeF32, - t.params, - cases - ); + await run(t, builtin('dot'), [Type.vec3f, Type.vec3f], Type.f32, t.params, cases); }); g.test('f32_vec4') @@ -108,14 +196,7 @@ g.test('f32_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const' ); - await run( - t, - builtin('dot'), - [TypeVec(4, TypeF32), TypeVec(4, TypeF32)], - TypeF32, - t.params, - cases - ); + await run(t, builtin('dot'), [Type.vec4f, Type.vec4f], Type.f32, t.params, cases); }); g.test('f16_vec2') @@ -129,14 +210,7 @@ g.test('f16_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec2_const' : 'f16_vec2_non_const' ); - await run( - t, - builtin('dot'), - [TypeVec(2, TypeF16), TypeVec(2, TypeF16)], - TypeF16, - t.params, - cases - ); + await run(t, builtin('dot'), [Type.vec2h, Type.vec2h], Type.f16, t.params, cases); }); g.test('f16_vec3') @@ -150,14 +224,7 @@ g.test('f16_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec3_const' : 'f16_vec3_non_const' ); - await run( - t, - builtin('dot'), - [TypeVec(3, TypeF16), TypeVec(3, TypeF16)], - TypeF16, - t.params, - cases - ); + await run(t, builtin('dot'), [Type.vec3h, Type.vec3h], Type.f16, t.params, cases); }); g.test('f16_vec4') @@ -171,12 +238,5 @@ g.test('f16_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec4_const' : 'f16_vec4_non_const' ); - await run( - t, - builtin('dot'), - [TypeVec(4, TypeF16), TypeVec(4, TypeF16)], - TypeF16, - t.params, - cases - ); + await run(t, builtin('dot'), [Type.vec4h, Type.vec4h], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot4I8Packed.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot4I8Packed.spec.ts new file mode 100644 index 0000000000..de537c473e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot4I8Packed.spec.ts @@ -0,0 +1,74 @@ +export const description = ` +Execution tests for the 'dot4I8Packed' builtin function + +@const fn dot4I8Packed(e1: u32 ,e2: u32) -> i32 +e1 and e2 are interpreted as vectors with four 8-bit signed integer components. Return the signed +integer dot product of these two vectors. Each component is sign-extended to i32 before performing +the multiply, and then the add operations are done in WGSL i32 with wrapping behaviour. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { Type, i32, u32 } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#dot4I8Packed-builtin') + .desc( + ` +@const fn dot4I8Packed(e1: u32, e2: u32) -> i32 + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const dot4I8Packed = (e1: number, e2: number) => { + let result = 0; + for (let i = 0; i < 4; ++i) { + let e1_i = (e1 >> (i * 8)) & 0xff; + if (e1_i >= 128) { + e1_i -= 256; + } + let e2_i = (e2 >> (i * 8)) & 0xff; + if (e2_i >= 128) { + e2_i -= 256; + } + result += e1_i * e2_i; + } + return result; + }; + + const testInputs = [ + // dot({0, 0, 0, 0}, {0, 0, 0, 0}) + [0, 0], + // dot({127, 127, 127, 127}, {127, 127, 127, 127}) + [0x7f7f7f7f, 0x7f7f7f7f], + // dot({-128, -128, -128, -128}, {-128, -128, -128, -128}) + [0x80808080, 0x80808080], + // dot({127, 127, 127, 127}, {-128, -128, -128, -128}) + [0x7f7f7f7f, 0x80808080], + // dot({1, 2, 3, 4}, {5, 6, 7, 8}) + [0x01020304, 0x05060708], + // dot({1, 2, 3, 4}, {-1, -2, -3, -4}) + [0x01020304, 0xfffefdfc], + // dot({-5, -6, -7, -8}, {5, 6, 7, 8}) + [0xfbfaf9f8, 0x05060708], + // dot({-9, -10, -11, -12}, {-13, -14, -15, -16}) + [0xf7f6f5f4, 0xf3f2f1f0], + ] as const; + + const makeCase = (x: number, y: number): Case => { + return { input: [u32(x), u32(y)], expected: i32(dot4I8Packed(x, y)) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(...(v as [number, number]))]; + }); + + await run(t, builtin('dot4I8Packed'), [Type.u32, Type.u32], Type.i32, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot4U8Packed.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot4U8Packed.spec.ts new file mode 100644 index 0000000000..a12a3d0123 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot4U8Packed.spec.ts @@ -0,0 +1,59 @@ +export const description = ` +Execution tests for the 'dot4U8Packed' builtin function + +@const fn dot4U8Packed(e1: u32 ,e2: u32) -> u32 +e1 and e2 are interpreted as vectors with four 8-bit unsigned integer components. Return the +unsigned integer dot product of these two vectors. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { Type, u32 } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#dot4U8Packed-builtin') + .desc( + ` +@const fn dot4U8Packed(e1: u32, e2: u32) -> u32 + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const dot4U8Packed = (e1: number, e2: number) => { + let result = 0; + for (let i = 0; i < 4; ++i) { + const e1_i = (e1 >> (i * 8)) & 0xff; + const e2_i = (e2 >> (i * 8)) & 0xff; + result += e1_i * e2_i; + } + return result; + }; + + const testInputs = [ + // dot({0, 0, 0, 0}, {0, 0, 0, 0}) + [0, 0], + // dot({255u, 255u, 255u, 255u}, {255u, 255u, 255u, 255u}) + [0xffffffff, 0xffffffff], + // dot({1u, 2u, 3u, 4u}, {5u, 6u, 7u, 8u}) + [0x01020304, 0x05060708], + // dot({120u, 90u, 60u, 30u}, {50u, 100u, 150u, 200u}) + [0x785a3c1e, 0x326496c8], + ] as const; + + const makeCase = (x: number, y: number): Case => { + return { input: [u32(x), u32(y)], expected: u32(dot4U8Packed(x, y)) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(...(v as [number, number]))]; + }); + + await run(t, builtin('dot4U8Packed'), [Type.u32, Type.u32], Type.u32, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts index 287a51c699..d922603a9f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts @@ -10,14 +10,22 @@ The result is the same as either dpdxFine(e) or dpdxCoarse(e). import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { allInputSources } from '../../expression.js'; + +import { d } from './derivatives.cache.js'; +import { runDerivativeTest } from './derivatives.js'; export const g = makeTestGroup(GPUTest); +const builtin = 'dpdx'; + g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') - .desc(`f32 tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('non_uniform_discard', [false, true]) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('scalar'); + runDerivativeTest(t, cases, builtin, t.params.non_uniform_discard, t.params.vectorize); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts index 67a75bb010..488f7e5440 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts @@ -9,14 +9,22 @@ This may result in fewer unique positions that dpdxFine(e). import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { allInputSources } from '../../expression.js'; + +import { d } from './derivatives.cache.js'; +import { runDerivativeTest } from './derivatives.js'; export const g = makeTestGroup(GPUTest); +const builtin = 'dpdxCoarse'; + g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') - .desc(`f32 tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('non_uniform_discard', [false, true]) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('scalar'); + runDerivativeTest(t, cases, builtin, t.params.non_uniform_discard, t.params.vectorize); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts index 91d65b990b..180aec2ec4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts @@ -8,14 +8,22 @@ Returns the partial derivative of e with respect to window x coordinates. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { allInputSources } from '../../expression.js'; + +import { d } from './derivatives.cache.js'; +import { runDerivativeTest } from './derivatives.js'; export const g = makeTestGroup(GPUTest); +const builtin = 'dpdxFine'; + g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') - .desc(`f32 tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('non_uniform_discard', [false, true]) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('scalar'); + runDerivativeTest(t, cases, builtin, t.params.non_uniform_discard, t.params.vectorize); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts index 0cd9cafdb9..94df913d5c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts @@ -9,14 +9,22 @@ The result is the same as either dpdyFine(e) or dpdyCoarse(e). import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { allInputSources } from '../../expression.js'; + +import { d } from './derivatives.cache.js'; +import { runDerivativeTest } from './derivatives.js'; export const g = makeTestGroup(GPUTest); +const builtin = 'dpdy'; + g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') - .desc(`f32 tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('non_uniform_discard', [false, true]) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('scalar'); + runDerivativeTest(t, cases, builtin, t.params.non_uniform_discard, t.params.vectorize); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts index f06869fdc2..2974475a6c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts @@ -9,14 +9,22 @@ This may result in fewer unique positions that dpdyFine(e). import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { allInputSources } from '../../expression.js'; + +import { d } from './derivatives.cache.js'; +import { runDerivativeTest } from './derivatives.js'; export const g = makeTestGroup(GPUTest); +const builtin = 'dpdyCoarse'; + g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') - .desc(`f32 test`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('non_uniform_discard', [false, true]) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('scalar'); + runDerivativeTest(t, cases, builtin, t.params.non_uniform_discard, t.params.vectorize); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts index e09761de95..5772024cc6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts @@ -8,14 +8,22 @@ Returns the partial derivative of e with respect to window y coordinates. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { allInputSources } from '../../expression.js'; + +import { d } from './derivatives.cache.js'; +import { runDerivativeTest } from './derivatives.js'; export const g = makeTestGroup(GPUTest); +const builtin = 'dpdyFine'; + g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') - .desc(`f32 tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('non_uniform_discard', [false, true]) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('scalar'); + runDerivativeTest(t, cases, builtin, t.params.non_uniform_discard, t.params.vectorize); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.cache.ts new file mode 100644 index 0000000000..5ecc8b2d97 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.cache.ts @@ -0,0 +1,44 @@ +import { kValue } from '../../../../../util/constants.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange, linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// floor(ln(max f32 value)) = 88, so exp(88) will be within range of a f32, but exp(89) will not +// floor(ln(max f64 value)) = 709, so exp(709) can be handled by the testing framework, but exp(710) will misbehave +const f32_inputs = [ + 0, // Returns 1 by definition + -89, // Returns subnormal value + kValue.f32.negative.min, // Closest to returning 0 as possible + ...biasedRange(kValue.f32.negative.max, -88, 100), + ...biasedRange(kValue.f32.positive.min, 88, 100), + ...linearRange(89, 709, 10), // Overflows f32, but not f64 +]; + +// floor(ln(max f16 value)) = 11, so exp(11) will be within range of a f16, but exp(12) will not +const f16_inputs = [ + 0, // Returns 1 by definition + -12, // Returns subnormal value + kValue.f16.negative.min, // Closest to returning 0 as possible + ...biasedRange(kValue.f16.negative.max, -11, 100), + ...biasedRange(kValue.f16.positive.min, 11, 100), + ...linearRange(12, 709, 10), // Overflows f16, but not f64 +]; + +export const d = makeCaseCache('exp', { + f32_const: () => { + return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.expInterval); + }, + f32_non_const: () => { + return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.expInterval); + }, + f16_const: () => { + return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.expInterval); + }, + f16_non_const: () => { + return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.expInterval); + }, + abstract: () => { + // exp has an ulp accuracy, so is only expected to be as accurate as f32 + return FP.abstract.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.expInterval); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts index 8b1ced3cab..e6bf65fe4f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'exp' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn exp(e1: T ) -> T Returns the natural exponentiation of e1 (e.g. e^e1). Component-wise when T is a vector. @@ -9,60 +9,33 @@ Returns the natural exponentiation of e1 (e.g. e^e1). Component-wise when T is a import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { biasedRange, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './exp.cache.js'; export const g = makeTestGroup(GPUTest); -// floor(ln(max f32 value)) = 88, so exp(88) will be within range of a f32, but exp(89) will not -// floor(ln(max f64 value)) = 709, so exp(709) can be handled by the testing framework, but exp(710) will misbehave -const f32_inputs = [ - 0, // Returns 1 by definition - -89, // Returns subnormal value - kValue.f32.negative.min, // Closest to returning 0 as possible - ...biasedRange(kValue.f32.negative.max, -88, 100), - ...biasedRange(kValue.f32.positive.min, 88, 100), - ...linearRange(89, 709, 10), // Overflows f32, but not f64 -]; - -// floor(ln(max f16 value)) = 11, so exp(11) will be within range of a f16, but exp(12) will not -const f16_inputs = [ - 0, // Returns 1 by definition - -12, // Returns subnormal value - kValue.f16.negative.min, // Closest to returning 0 as possible - ...biasedRange(kValue.f16.negative.max, -11, 100), - ...biasedRange(kValue.f16.positive.min, 11, 100), - ...linearRange(12, 709, 10), // Overflows f16, but not f64 -]; - -export const d = makeCaseCache('exp', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.expInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.expInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.expInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.expInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('exp'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -72,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('exp'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('exp'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -86,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('exp'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('exp'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.cache.ts new file mode 100644 index 0000000000..e2d0f1c16c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.cache.ts @@ -0,0 +1,44 @@ +import { kValue } from '../../../../../util/constants.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange, linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// floor(log2(max f32 value)) = 127, so exp2(127) will be within range of a f32, but exp2(128) will not +// floor(ln(max f64 value)) = 1023, so exp2(1023) can be handled by the testing framework, but exp2(1024) will misbehave +const f32_inputs = [ + 0, // Returns 1 by definition + -128, // Returns subnormal value + kValue.f32.negative.min, // Closest to returning 0 as possible + ...biasedRange(kValue.f32.negative.max, -127, 100), + ...biasedRange(kValue.f32.positive.min, 127, 100), + ...linearRange(128, 1023, 10), // Overflows f32, but not f64 +]; + +// floor(log2(max f16 value)) = 15, so exp2(15) will be within range of a f16, but exp2(15) will not +const f16_inputs = [ + 0, // Returns 1 by definition + -16, // Returns subnormal value + kValue.f16.negative.min, // Closest to returning 0 as possible + ...biasedRange(kValue.f16.negative.max, -15, 100), + ...biasedRange(kValue.f16.positive.min, 15, 100), + ...linearRange(16, 1023, 10), // Overflows f16, but not f64 +]; + +export const d = makeCaseCache('exp2', { + f32_const: () => { + return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.exp2Interval); + }, + f32_non_const: () => { + return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.exp2Interval); + }, + f16_const: () => { + return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.exp2Interval); + }, + f16_non_const: () => { + return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.exp2Interval); + }, + abstract: () => { + // exp2 has an ulp accuracy, so is only expected to be as accurate as f32 + return FP.abstract.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.exp2Interval); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts index 67e123cb30..f47d2e5066 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'exp2' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn exp2(e: T ) -> T Returns 2 raised to the power e (e.g. 2^e). Component-wise when T is a vector. @@ -9,60 +9,33 @@ Returns 2 raised to the power e (e.g. 2^e). Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { biasedRange, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './exp2.cache.js'; export const g = makeTestGroup(GPUTest); -// floor(log2(max f32 value)) = 127, so exp2(127) will be within range of a f32, but exp2(128) will not -// floor(ln(max f64 value)) = 1023, so exp2(1023) can be handled by the testing framework, but exp2(1024) will misbehave -const f32_inputs = [ - 0, // Returns 1 by definition - -128, // Returns subnormal value - kValue.f32.negative.min, // Closest to returning 0 as possible - ...biasedRange(kValue.f32.negative.max, -127, 100), - ...biasedRange(kValue.f32.positive.min, 127, 100), - ...linearRange(128, 1023, 10), // Overflows f32, but not f64 -]; - -// floor(log2(max f16 value)) = 15, so exp2(15) will be within range of a f16, but exp2(15) will not -const f16_inputs = [ - 0, // Returns 1 by definition - -16, // Returns subnormal value - kValue.f16.negative.min, // Closest to returning 0 as possible - ...biasedRange(kValue.f16.negative.max, -15, 100), - ...biasedRange(kValue.f16.positive.min, 15, 100), - ...linearRange(16, 1023, 10), // Overflows f16, but not f64 -]; - -export const d = makeCaseCache('exp2', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.exp2Interval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.exp2Interval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.exp2Interval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.exp2Interval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('exp2'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -72,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('exp2'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('exp2'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -86,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('exp2'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('exp2'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts index d535bf5d74..ef04b661bd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts @@ -33,17 +33,7 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - i32Bits, - TypeI32, - u32, - TypeU32, - u32Bits, - vec2, - vec3, - vec4, - TypeVec, -} from '../../../../../util/conversion.js'; +import { i32Bits, Type, u32, u32Bits, vec2, vec3, vec4 } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -57,7 +47,7 @@ g.test('u32') .fn(async t => { const cfg: Config = t.params; - const T = t.params.width === 1 ? TypeU32 : TypeVec(t.params.width, TypeU32); + const T = t.params.width === 1 ? Type.u32 : Type.vec(t.params.width, Type.u32); const V = (x: number, y?: number, z?: number, w?: number) => { y = y === undefined ? x : y; @@ -193,7 +183,7 @@ g.test('u32') ); } - await run(t, builtin('extractBits'), [T, TypeU32, TypeU32], T, cfg, cases); + await run(t, builtin('extractBits'), [T, Type.u32, Type.u32], T, cfg, cases); }); g.test('i32') @@ -203,7 +193,7 @@ g.test('i32') .fn(async t => { const cfg: Config = t.params; - const T = t.params.width === 1 ? TypeI32 : TypeVec(t.params.width, TypeI32); + const T = t.params.width === 1 ? Type.i32 : Type.vec(t.params.width, Type.i32); const V = (x: number, y?: number, z?: number, w?: number) => { y = y === undefined ? x : y; @@ -333,5 +323,5 @@ g.test('i32') ); } - await run(t, builtin('extractBits'), [T, TypeU32, TypeU32], T, cfg, cases); + await run(t, builtin('extractBits'), [T, Type.u32, Type.u32], T, cfg, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.cache.ts new file mode 100644 index 0000000000..17a5d0fd8f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.cache.ts @@ -0,0 +1,125 @@ +import { ROArrayArray } from '../../../../../../common/util/types.js'; +import { anyOf } from '../../../../../util/compare.js'; +import { toVector } from '../../../../../util/conversion.js'; +import { FP, FPKind, FPVector } from '../../../../../util/floating_point.js'; +import { cartesianProduct } from '../../../../../util/math.js'; +import { Case, selectNCases } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; +import { IntervalFilter } from '../../interval_filter.js'; + +// Using a bespoke implementation of make*Case and generate*Cases here +// since faceForwardIntervals is the only builtin with the API signature +// (vec, vec, vec) -> vec +// +// Additionally faceForward has significant complexities around it due to the +// fact that `dot` is calculated in its operation, but the result of dot isn't +// used to calculate the builtin's result. + +/** + * @returns a Case for `faceForward` + * @param argumentKind what kind of floating point numbers being operated on + * @param parameterKind what kind of floating point operation should be performed, + * should be the same as argumentKind, except for abstract + * @param x the `x` param for the case + * @param y the `y` param for the case + * @param z the `z` param for the case + * @param check what interval checking to apply + * */ +function makeCase( + argumentKind: FPKind, + parameterKind: FPKind, + x: readonly number[], + y: readonly number[], + z: readonly number[], + check: IntervalFilter +): Case | undefined { + const fp = FP[argumentKind]; + x = x.map(fp.quantize); + y = y.map(fp.quantize); + z = z.map(fp.quantize); + + const results = FP[parameterKind].faceForwardIntervals(x, y, z); + if (check === 'finite' && results.some(r => r === undefined)) { + return undefined; + } + + // Stripping the undefined results, since undefined is used to signal that an OOB + // could occur within the calculation that isn't reflected in the result + // intervals. + const define_results = results.filter((r): r is FPVector => r !== undefined); + + return { + input: [ + toVector(x, fp.scalarBuilder), + toVector(y, fp.scalarBuilder), + toVector(z, fp.scalarBuilder), + ], + expected: anyOf(...define_results), + }; +} + +/** + * @returns an array of Cases for `faceForward` + * @param argumentKind what kind of floating point numbers being operated on + * @param parameterKind what kind of floating point operation should be performed, + * should be the same as argumentKind, except for abstract + * @param xs array of inputs to try for the `x` param + * @param ys array of inputs to try for the `y` param + * @param zs array of inputs to try for the `z` param + * @param check what interval checking to apply + */ +function generateCases( + argumentKind: FPKind, + parameterKind: FPKind, + xs: ROArrayArray<number>, + ys: ROArrayArray<number>, + zs: ROArrayArray<number>, + check: IntervalFilter +): Case[] { + // Cannot use `cartesianProduct` here due to heterogeneous param types + return cartesianProduct(xs, ys, zs) + .map(e => makeCase(argumentKind, parameterKind, e[0], e[1], e[2], check)) + .filter((c): c is Case => c !== undefined); +} + +// Cases: [f32|f16|abstract]_vecN_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`${trait}_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + if (trait !== 'abstract') { + return generateCases( + trait, + trait, + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + nonConst ? 'unfiltered' : 'finite' + ); + } else { + // Restricting the number of cases, because a vector of abstract floats needs to be returned, which is costly. + return selectNCases( + 'faceForward', + 20, + generateCases( + trait, + // faceForward has an inherited accuracy, so is only expected to be as accurate as f32 + 'f32', + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + nonConst ? 'unfiltered' : 'finite' + ) + ); + } + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('faceForward', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts index 6b6794fb9f..096abe034f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts @@ -1,142 +1,68 @@ export const description = ` Execution tests for the 'faceForward' builtin function -T is vecN<AbstractFloat>, vecN<f32>, or vecN<f16> +T is vecN<Type.abstractFloat>, vecN<f32>, or vecN<f16> @const fn faceForward(e1: T ,e2: T ,e3: T ) -> T Returns e1 if dot(e2,e3) is negative, and -e1 otherwise. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; -import { ROArrayArray } from '../../../../../../common/util/types.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { anyOf } from '../../../../../util/compare.js'; -import { toVector, TypeF32, TypeF16, TypeVec } from '../../../../../util/conversion.js'; -import { FP, FPKind, FPVector } from '../../../../../util/floating_point.js'; -import { - cartesianProduct, - sparseVectorF32Range, - sparseVectorF16Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, IntervalFilter, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './faceForward.cache.js'; export const g = makeTestGroup(GPUTest); -// Using a bespoke implementation of make*Case and generate*Cases here -// since faceForwardIntervals is the only builtin with the API signature -// (vec, vec, vec) -> vec -// -// Additionally faceForward has significant complexities around it due to the -// fact that `dot` is calculated in it s operation, but the result of dot isn't -// used to calculate the builtin's result. - -/** - * @returns a Case for `faceForward` - * @param kind what kind of floating point numbers being operated on - * @param x the `x` param for the case - * @param y the `y` param for the case - * @param z the `z` param for the case - * @param check what interval checking to apply - * */ -function makeCase( - kind: FPKind, - x: readonly number[], - y: readonly number[], - z: readonly number[], - check: IntervalFilter -): Case | undefined { - const fp = FP[kind]; - x = x.map(fp.quantize); - y = y.map(fp.quantize); - z = z.map(fp.quantize); - - const results = FP[kind].faceForwardIntervals(x, y, z); - if (check === 'finite' && results.some(r => r === undefined)) { - return undefined; - } - - // Stripping the undefined results, since undefined is used to signal that an OOB - // could occur within the calculation that isn't reflected in the result - // intervals. - const define_results = results.filter((r): r is FPVector => r !== undefined); - - return { - input: [ - toVector(x, fp.scalarBuilder), - toVector(y, fp.scalarBuilder), - toVector(z, fp.scalarBuilder), - ], - expected: anyOf(...define_results), - }; -} - -/** - * @returns an array of Cases for `faceForward` - * @param kind what kind of floating point numbers being operated on - * @param xs array of inputs to try for the `x` param - * @param ys array of inputs to try for the `y` param - * @param zs array of inputs to try for the `z` param - * @param check what interval checking to apply - */ -function generateCases( - kind: FPKind, - xs: ROArrayArray<number>, - ys: ROArrayArray<number>, - zs: ROArrayArray<number>, - check: IntervalFilter -): Case[] { - // Cannot use `cartesianProduct` here due to heterogeneous param types - return cartesianProduct(xs, ys, zs) - .map(e => makeCase(kind, e[0], e[1], e[2], check)) - .filter((c): c is Case => c !== undefined); -} - -// Cases: f32_vecN_[non_]const -const f32_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f32_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return generateCases( - 'f32', - sparseVectorF32Range(n), - sparseVectorF32Range(n), - sparseVectorF32Range(n), - nonConst ? 'unfiltered' : 'finite' - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_vecN_[non_]const -const f16_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f16_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return generateCases( - 'f16', - sparseVectorF16Range(n), - sparseVectorF16Range(n), - sparseVectorF16Range(n), - nonConst ? 'unfiltered' : 'finite' - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); +g.test('abstract_float_vec2') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_const'); + await run( + t, + abstractFloatBuiltin('faceForward'), + [Type.vec2af, Type.vec2af, Type.vec2af], + Type.vec2af, + t.params, + cases + ); + }); -export const d = makeCaseCache('faceForward', { - ...f32_vec_cases, - ...f16_vec_cases, -}); +g.test('abstract_float_vec3') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_const'); + await run( + t, + abstractFloatBuiltin('faceForward'), + [Type.vec3af, Type.vec3af, Type.vec3af], + Type.vec3af, + t.params, + cases + ); + }); -g.test('abstract_float') - .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') - .desc(`abstract float tests`) - .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const)) - .unimplemented(); +g.test('abstract_float_vec4') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_const'); + await run( + t, + abstractFloatBuiltin('faceForward'), + [Type.vec4af, Type.vec4af, Type.vec4af], + Type.vec4af, + t.params, + cases + ); + }); g.test('f32_vec2') .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') @@ -149,8 +75,8 @@ g.test('f32_vec2') await run( t, builtin('faceForward'), - [TypeVec(2, TypeF32), TypeVec(2, TypeF32), TypeVec(2, TypeF32)], - TypeVec(2, TypeF32), + [Type.vec2f, Type.vec2f, Type.vec2f], + Type.vec2f, t.params, cases ); @@ -167,8 +93,8 @@ g.test('f32_vec3') await run( t, builtin('faceForward'), - [TypeVec(3, TypeF32), TypeVec(3, TypeF32), TypeVec(3, TypeF32)], - TypeVec(3, TypeF32), + [Type.vec3f, Type.vec3f, Type.vec3f], + Type.vec3f, t.params, cases ); @@ -185,8 +111,8 @@ g.test('f32_vec4') await run( t, builtin('faceForward'), - [TypeVec(4, TypeF32), TypeVec(4, TypeF32), TypeVec(4, TypeF32)], - TypeVec(4, TypeF32), + [Type.vec4f, Type.vec4f, Type.vec4f], + Type.vec4f, t.params, cases ); @@ -206,8 +132,8 @@ g.test('f16_vec2') await run( t, builtin('faceForward'), - [TypeVec(2, TypeF16), TypeVec(2, TypeF16), TypeVec(2, TypeF16)], - TypeVec(2, TypeF16), + [Type.vec2h, Type.vec2h, Type.vec2h], + Type.vec2h, t.params, cases ); @@ -227,8 +153,8 @@ g.test('f16_vec3') await run( t, builtin('faceForward'), - [TypeVec(3, TypeF16), TypeVec(3, TypeF16), TypeVec(3, TypeF16)], - TypeVec(3, TypeF16), + [Type.vec3h, Type.vec3h, Type.vec3h], + Type.vec3h, t.params, cases ); @@ -248,8 +174,8 @@ g.test('f16_vec4') await run( t, builtin('faceForward'), - [TypeVec(4, TypeF16), TypeVec(4, TypeF16), TypeVec(4, TypeF16)], - TypeVec(4, TypeF16), + [Type.vec4h, Type.vec4h, Type.vec4h], + Type.vec4h, t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts index 26216563cd..9248b1e2bf 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts @@ -16,7 +16,7 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { i32, i32Bits, TypeI32, u32, TypeU32, u32Bits } from '../../../../../util/conversion.js'; +import { i32, i32Bits, Type, u32, u32Bits } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -31,7 +31,7 @@ g.test('u32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('firstLeadingBit'), [TypeU32], TypeU32, cfg, [ + await run(t, builtin('firstLeadingBit'), [Type.u32], Type.u32, cfg, [ // Zero { input: u32Bits(0b00000000000000000000000000000000), expected: u32(-1) }, @@ -146,7 +146,7 @@ g.test('i32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('firstLeadingBit'), [TypeI32], TypeI32, cfg, [ + await run(t, builtin('firstLeadingBit'), [Type.i32], Type.i32, cfg, [ // Zero { input: i32Bits(0b00000000000000000000000000000000), expected: i32(-1) }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts index 5c65f59d28..a8dd27ee87 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts @@ -12,7 +12,7 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { i32, i32Bits, TypeI32, u32, TypeU32, u32Bits } from '../../../../../util/conversion.js'; +import { i32, i32Bits, Type, u32, u32Bits } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -27,7 +27,7 @@ g.test('u32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('firstTrailingBit'), [TypeU32], TypeU32, cfg, [ + await run(t, builtin('firstTrailingBit'), [Type.u32], Type.u32, cfg, [ // Zero { input: u32Bits(0b00000000000000000000000000000000), expected: u32(-1) }, @@ -142,7 +142,7 @@ g.test('i32') ) .fn(async t => { const cfg: Config = t.params; - await run(t, builtin('firstTrailingBit'), [TypeI32], TypeI32, cfg, [ + await run(t, builtin('firstTrailingBit'), [Type.i32], Type.i32, cfg, [ // Zero { input: i32Bits(0b00000000000000000000000000000000), expected: i32(-1) }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.cache.ts new file mode 100644 index 0000000000..0af966cfd2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.cache.ts @@ -0,0 +1,26 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +const kSmallMagnitudeTestValues = [0.1, 0.9, 1.0, 1.1, 1.9, -0.1, -0.9, -1.0, -1.1, -1.9]; + +// See https://github.com/gpuweb/cts/issues/2766 for details +const kIssue2766Value = { + abstract: 0x8000_0000_0000_0000, + f32: 0x8000_0000, + f16: 0x8000, +}; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [...kSmallMagnitudeTestValues, kIssue2766Value[trait], ...FP[trait].scalarRange()], + 'unfiltered', + FP[trait].floorInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('floor', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts index 873a6772c3..26cffe5d10 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'floor' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn floor(e: T ) -> T Returns the floor of e. Component-wise when T is a vector. @@ -9,54 +9,14 @@ Returns the floor of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeAbstractFloat } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range, fullF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './floor.cache.js'; export const g = makeTestGroup(GPUTest); -const kSmallMagnitudeTestValues = [0.1, 0.9, 1.0, 1.1, 1.9, -0.1, -0.9, -1.0, -1.1, -1.9]; - -export const d = makeCaseCache('floor', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - ...kSmallMagnitudeTestValues, - ...fullF32Range(), - 0x8000_0000, // https://github.com/gpuweb/cts/issues/2766 - ], - 'unfiltered', - FP.f32.floorInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - ...kSmallMagnitudeTestValues, - ...fullF16Range(), - 0x8000, // https://github.com/gpuweb/cts/issues/2766 - ], - 'unfiltered', - FP.f16.floorInterval - ); - }, - abstract: () => { - return FP.abstract.generateScalarToIntervalCases( - [ - ...kSmallMagnitudeTestValues, - ...fullF64Range(), - 0x8000_0000_0000_0000, // https://github.com/gpuweb/cts/issues/2766 - ], - 'unfiltered', - FP.abstract.floorInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) @@ -67,7 +27,14 @@ g.test('abstract_float') ) .fn(async t => { const cases = await d.get('abstract'); - await run(t, abstractBuiltin('floor'), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases); + await run( + t, + abstractFloatBuiltin('floor'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); }); g.test('f32') @@ -78,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('floor'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('floor'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -92,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('floor'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('floor'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.cache.ts new file mode 100644 index 0000000000..20a61ce837 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.cache.ts @@ -0,0 +1,26 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +// abstract_non_const is empty and not used +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarTripleToIntervalCases( + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + nonConst ? 'unfiltered' : 'finite', + // fma has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].fmaInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('fma', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts index 701f9d7ca9..620792d420 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'fma' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn fma(e1: T ,e2: T ,e3: T ) -> T Returns e1 * e2 + e3. Component-wise when T is a vector. @@ -9,64 +9,14 @@ Returns e1 * e2 + e3. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeAbstractFloat } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { sparseF32Range, sparseF16Range, sparseF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './fma.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('fma', { - f32_const: () => { - return FP.f32.generateScalarTripleToIntervalCases( - sparseF32Range(), - sparseF32Range(), - sparseF32Range(), - 'finite', - FP.f32.fmaInterval - ); - }, - f32_non_const: () => { - return FP.f32.generateScalarTripleToIntervalCases( - sparseF32Range(), - sparseF32Range(), - sparseF32Range(), - 'unfiltered', - FP.f32.fmaInterval - ); - }, - f16_const: () => { - return FP.f16.generateScalarTripleToIntervalCases( - sparseF16Range(), - sparseF16Range(), - sparseF16Range(), - 'finite', - FP.f16.fmaInterval - ); - }, - f16_non_const: () => { - return FP.f16.generateScalarTripleToIntervalCases( - sparseF16Range(), - sparseF16Range(), - sparseF16Range(), - 'unfiltered', - FP.f16.fmaInterval - ); - }, - abstract: () => { - return FP.abstract.generateScalarTripleToIntervalCases( - sparseF64Range(), - sparseF64Range(), - sparseF64Range(), - 'finite', - FP.abstract.fmaInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) @@ -76,12 +26,12 @@ g.test('abstract_float') .combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - const cases = await d.get('abstract'); + const cases = await d.get('abstract_const'); await run( t, - abstractBuiltin('fma'), - [TypeAbstractFloat, TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBuiltin('fma'), + [Type.abstractFloat, Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -95,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('fma'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('fma'), [Type.f32, Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -109,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('fma'), [TypeF16, TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('fma'), [Type.f16, Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.cache.ts new file mode 100644 index 0000000000..5d0933554b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.cache.ts @@ -0,0 +1,50 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +const kCommonValues = [ + 0.5, // 0.5 -> 0.5 + 0.9, // ~0.9 -> ~0.9 + 1, // 1 -> 0 + 2, // 2 -> 0 + 1.11, // ~1.11 -> ~0.11 + -0.1, // ~-0.1 -> ~0.9 + -0.5, // -0.5 -> 0.5 + -0.9, // ~-0.9 -> ~0.1 + -1, // -1 -> 0 + -2, // -2 -> 0 + -1.11, // ~-1.11 -> ~0.89 +]; + +const kTraitSpecificValues = { + f32: [ + 10.0001, // ~10.0001 -> ~0.0001 + -10.0001, // -10.0001 -> ~0.9999 + 0x8000_0000, // https://github.com/gpuweb/cts/issues/2766 + ], + f16: [ + 10.0078125, // 10.0078125 -> 0.0078125 + -10.0078125, // -10.0078125 -> 0.9921875 + 658.5, // 658.5 -> 0.5 + 0x8000, // https://github.com/gpuweb/cts/issues/2766 + ], + abstract: [ + 10.0001, // ~10.0001 -> ~0.0001 + -10.0001, // -10.0001 -> ~0.9999 + 0x8000_0000, // https://github.com/gpuweb/cts/issues/2766 + ], +}; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [...kCommonValues, ...kTraitSpecificValues[trait], ...FP[trait].scalarRange()], + trait === 'abstract' ? 'finite' : 'unfiltered', + FP[trait].fractInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('fract', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts index 44ea31fde2..f1f7279c0b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'fract' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn fract(e: T ) -> T Returns the fractional part of e, computed as e - floor(e). @@ -10,72 +10,33 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './fract.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('fract', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - 0.5, // 0.5 -> 0.5 - 0.9, // ~0.9 -> ~0.9 - 1, // 1 -> 0 - 2, // 2 -> 0 - 1.11, // ~1.11 -> ~0.11 - 10.0001, // ~10.0001 -> ~0.0001 - -0.1, // ~-0.1 -> ~0.9 - -0.5, // -0.5 -> 0.5 - -0.9, // ~-0.9 -> ~0.1 - -1, // -1 -> 0 - -2, // -2 -> 0 - -1.11, // ~-1.11 -> ~0.89 - -10.0001, // -10.0001 -> ~0.9999 - 0x80000000, // https://github.com/gpuweb/cts/issues/2766 - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.fractInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - 0.5, // 0.5 -> 0.5 - 0.9, // ~0.9 -> ~0.9 - 1, // 1 -> 0 - 2, // 2 -> 0 - 1.11, // ~1.11 -> ~0.11 - 10.0078125, // 10.0078125 -> 0.0078125 - -0.1, // ~-0.1 -> ~0.9 - -0.5, // -0.5 -> 0.5 - -0.9, // ~-0.9 -> ~0.1 - -1, // -1 -> 0 - -2, // -2 -> 0 - -1.11, // ~-1.11 -> ~0.89 - -10.0078125, // -10.0078125 -> 0.9921875 - 658.5, // 658.5 -> 0.5 - ...fullF16Range(), - ], - 'unfiltered', - FP.f16.fractInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('fract'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -85,7 +46,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('fract'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('fract'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -99,5 +60,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('fract'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('fract'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.cache.ts new file mode 100644 index 0000000000..2211e2adc9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.cache.ts @@ -0,0 +1,103 @@ +import { skipUndefined } from '../../../../../util/compare.js'; +import { + ScalarValue, + VectorValue, + i32, + toVector, + abstractInt, +} from '../../../../../util/conversion.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { frexp } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +/* @returns a fract Case for a given scalar or vector input */ +function makeCaseFract(v: number | readonly number[], trait: 'f32' | 'f16' | 'abstract'): Case { + const fp = FP[trait]; + let toInput: (n: readonly number[]) => ScalarValue | VectorValue; + let toOutput: (n: readonly number[]) => ScalarValue | VectorValue; + if (v instanceof Array) { + // Input is vector + toInput = (n: readonly number[]) => toVector(n, fp.scalarBuilder); + toOutput = (n: readonly number[]) => toVector(n, fp.scalarBuilder); + } else { + // Input is scalar, also wrap it in an array. + v = [v]; + toInput = (n: readonly number[]) => fp.scalarBuilder(n[0]); + toOutput = (n: readonly number[]) => fp.scalarBuilder(n[0]); + } + + v = v.map(fp.quantize); + if (v.some(e => e !== 0 && fp.isSubnormal(e))) { + return { input: toInput(v), expected: skipUndefined(undefined) }; + } + + const fs = v.map(e => { + return frexp(e, trait !== 'abstract' ? trait : 'f64').fract; + }); + + return { input: toInput(v), expected: toOutput(fs) }; +} + +/* @returns an exp Case for a given scalar or vector input */ +function makeCaseExp(v: number | readonly number[], trait: 'f32' | 'f16' | 'abstract'): Case { + const fp = FP[trait]; + let toInput: (n: readonly number[]) => ScalarValue | VectorValue; + let toOutput: (n: readonly number[]) => ScalarValue | VectorValue; + if (v instanceof Array) { + // Input is vector + toInput = (n: readonly number[]) => toVector(n, fp.scalarBuilder); + toOutput = (n: readonly number[]) => + toVector(n, trait !== 'abstract' ? i32 : (n: number) => abstractInt(BigInt(n))); + } else { + // Input is scalar, also wrap it in an array. + v = [v]; + toInput = (n: readonly number[]) => fp.scalarBuilder(n[0]); + toOutput = (n: readonly number[]) => + trait !== 'abstract' ? i32(n[0]) : abstractInt(BigInt(n[0])); + } + + v = v.map(fp.quantize); + if (v.some(e => e !== 0 && fp.isSubnormal(e))) { + return { input: toInput(v), expected: skipUndefined(undefined) }; + } + + const fs = v.map(e => { + return frexp(e, trait !== 'abstract' ? trait : 'f64').exp; + }); + + return { input: toInput(v), expected: toOutput(fs) }; +} + +// Cases: [f32|f16]_vecN_[exp|whole] +const vec_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + (['exp', 'fract'] as const).map(portion => ({ + [`${trait}_vec${dim}_${portion}`]: () => { + return FP[trait] + .vectorRange(dim) + .map(v => (portion === 'exp' ? makeCaseExp(v, trait) : makeCaseFract(v, trait))); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: [f32|f16]_[exp|whole] +const scalar_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + (['exp', 'fract'] as const).map(portion => ({ + [`${trait}_${portion}`]: () => { + return FP[trait] + .scalarRange() + .map(v => (portion === 'exp' ? makeCaseExp(v, trait) : makeCaseFract(v, trait))); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('frexp', { + ...scalar_cases, + ...vec_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts index ffe672b08c..8f07f3990d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts @@ -15,34 +15,19 @@ The magnitude of the significand is in the range of [0.5, 1.0) or 0. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { skipUndefined } from '../../../../../util/compare.js'; -import { - i32, - Scalar, - toVector, - TypeF32, - TypeF16, - TypeI32, - TypeVec, - Vector, -} from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { - frexp, - fullF16Range, - fullF32Range, - vectorF16Range, - vectorF32Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { + ShaderBuilder, allInputSources, basicExpressionBuilder, - Case, run, - ShaderBuilder, + abstractFloatShaderBuilder, + abstractIntShaderBuilder, + onlyConstInputSource, } from '../../expression.js'; +import { d } from './frexp.cache.js'; + export const g = makeTestGroup(GPUTest); /* @returns an ShaderBuilder that evaluates frexp and returns .fract from the result structure */ @@ -55,112 +40,159 @@ function expBuilder(): ShaderBuilder { return basicExpressionBuilder(value => `frexp(${value}).exp`); } -/* @returns a fract Case for a given scalar or vector input */ -function makeVectorCaseFract(v: number | readonly number[], trait: 'f32' | 'f16'): Case { - const fp = FP[trait]; - let toInput: (n: readonly number[]) => Scalar | Vector; - let toOutput: (n: readonly number[]) => Scalar | Vector; - if (v instanceof Array) { - // Input is vector - toInput = (n: readonly number[]) => toVector(n, fp.scalarBuilder); - toOutput = (n: readonly number[]) => toVector(n, fp.scalarBuilder); - } else { - // Input is scalar, also wrap it in an array. - v = [v]; - toInput = (n: readonly number[]) => fp.scalarBuilder(n[0]); - toOutput = (n: readonly number[]) => fp.scalarBuilder(n[0]); - } - - v = v.map(fp.quantize); - if (v.some(e => e !== 0 && fp.isSubnormal(e))) { - return { input: toInput(v), expected: skipUndefined(undefined) }; - } - - const fs = v.map(e => { - return frexp(e, trait).fract; +/* @returns an ShaderBuilder that evaluates frexp and returns .fract from the result structure, for abstract inputs */ +function abstractFractBuilder(): ShaderBuilder { + return abstractFloatShaderBuilder(value => `frexp(${value}).fract`); +} + +/* @returns an ShaderBuilder that evaluates frexp and returns .exp from the result structure, for abstract inputs */ +function abstractExpBuilder(): ShaderBuilder { + return abstractIntShaderBuilder(value => `frexp(${value}).exp`); +} + +g.test('abstract_float_fract') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is AbstractFloat + +struct __frexp_result_abstract { + fract : AbstractFloat, // fract part + exp : AbstractInt // exponent part +} +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_fract'); + await run(t, abstractFractBuilder(), [Type.abstractFloat], Type.abstractFloat, t.params, cases); + }); + +g.test('abstract_float_exp') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is AbstractFloat + +struct __frexp_result_abstract { + fract : AbstractFloat, // fract part + exp : AbstractInt // exponent part +} +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_exp'); + await run(t, abstractExpBuilder(), [Type.abstractFloat], Type.abstractInt, t.params, cases); + }); + +g.test('abstract_float_vec2_fract') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is vec2<AbstractFloat> + +struct __frexp_result_vec2_abstract { + fract : vec2<AbstractFloat>, // fract part + exp : vec2<AbstractInt> // exponent part +} +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_fract'); + await run(t, abstractFractBuilder(), [Type.vec2af], Type.vec2af, t.params, cases); + }); + +g.test('abstract_float_vec2_exp') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is vec2<AbstractFloat> + +struct __frexp_result_vec2_abstract { + fract : vec2<AbstractFloat>, // fractional part + exp : vec2<AbstractInt> // exponent part +} +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_exp'); + await run(t, abstractExpBuilder(), [Type.vec2af], Type.vec2ai, t.params, cases); }); - return { input: toInput(v), expected: toOutput(fs) }; +g.test('abstract_float_vec3_fract') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is vec3<AbstractFloat> + +struct __frexp_result_vec3_abstract { + fract : vec3<AbstractFloat>, // fractional part + exp : vec3<AbstractInt> // exponent part } +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_fract'); + await run(t, abstractFractBuilder(), [Type.vec3af], Type.vec3af, t.params, cases); + }); -/* @returns an exp Case for a given scalar or vector input */ -function makeVectorCaseExp(v: number | readonly number[], trait: 'f32' | 'f16'): Case { - const fp = FP[trait]; - let toInput: (n: readonly number[]) => Scalar | Vector; - let toOutput: (n: readonly number[]) => Scalar | Vector; - if (v instanceof Array) { - // Input is vector - toInput = (n: readonly number[]) => toVector(n, fp.scalarBuilder); - toOutput = (n: readonly number[]) => toVector(n, i32); - } else { - // Input is scalar, also wrap it in an array. - v = [v]; - toInput = (n: readonly number[]) => fp.scalarBuilder(n[0]); - toOutput = (n: readonly number[]) => i32(n[0]); - } - - v = v.map(fp.quantize); - if (v.some(e => e !== 0 && fp.isSubnormal(e))) { - return { input: toInput(v), expected: skipUndefined(undefined) }; - } - - const fs = v.map(e => { - return frexp(e, trait).exp; +g.test('abstract_float_vec3_exp') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is vec3<AbstractFloat> + +struct __frexp_result_vec3_abstract { + fract : vec3<AbstractFloat>, // fractional part + exp : vec3<AbstractInt> // exponent part +} +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_exp'); + await run(t, abstractExpBuilder(), [Type.vec3af], Type.vec3ai, t.params, cases); }); - return { input: toInput(v), expected: toOutput(fs) }; +g.test('abstract_float_vec4_fract') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is vec4<AbstractFloat> + +struct __frexp_result_vec4_abstract { + fract : vec4<AbstractFloat>, // fractional part + exp : vec4<AbstractInt> // exponent part } +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_fract'); + await run(t, abstractFractBuilder(), [Type.vec4af], Type.vec4af, t.params, cases); + }); + +g.test('abstract_float_vec4_exp') + .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') + .desc( + ` +T is vec4<AbstractFloat> -export const d = makeCaseCache('frexp', { - f32_fract: () => { - return fullF32Range().map(v => makeVectorCaseFract(v, 'f32')); - }, - f32_exp: () => { - return fullF32Range().map(v => makeVectorCaseExp(v, 'f32')); - }, - f32_vec2_fract: () => { - return vectorF32Range(2).map(v => makeVectorCaseFract(v, 'f32')); - }, - f32_vec2_exp: () => { - return vectorF32Range(2).map(v => makeVectorCaseExp(v, 'f32')); - }, - f32_vec3_fract: () => { - return vectorF32Range(3).map(v => makeVectorCaseFract(v, 'f32')); - }, - f32_vec3_exp: () => { - return vectorF32Range(3).map(v => makeVectorCaseExp(v, 'f32')); - }, - f32_vec4_fract: () => { - return vectorF32Range(4).map(v => makeVectorCaseFract(v, 'f32')); - }, - f32_vec4_exp: () => { - return vectorF32Range(4).map(v => makeVectorCaseExp(v, 'f32')); - }, - f16_fract: () => { - return fullF16Range().map(v => makeVectorCaseFract(v, 'f16')); - }, - f16_exp: () => { - return fullF16Range().map(v => makeVectorCaseExp(v, 'f16')); - }, - f16_vec2_fract: () => { - return vectorF16Range(2).map(v => makeVectorCaseFract(v, 'f16')); - }, - f16_vec2_exp: () => { - return vectorF16Range(2).map(v => makeVectorCaseExp(v, 'f16')); - }, - f16_vec3_fract: () => { - return vectorF16Range(3).map(v => makeVectorCaseFract(v, 'f16')); - }, - f16_vec3_exp: () => { - return vectorF16Range(3).map(v => makeVectorCaseExp(v, 'f16')); - }, - f16_vec4_fract: () => { - return vectorF16Range(4).map(v => makeVectorCaseFract(v, 'f16')); - }, - f16_vec4_exp: () => { - return vectorF16Range(4).map(v => makeVectorCaseExp(v, 'f16')); - }, -}); +struct __frexp_result_vec4_abstract { + fract : vec4<AbstractFloat>, // fractional part + exp : vec4<AbstractInt> // exponent part +} +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_exp'); + await run(t, abstractExpBuilder(), [Type.vec4af], Type.vec4ai, t.params, cases); + }); g.test('f32_fract') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -177,7 +209,7 @@ struct __frexp_result_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_fract'); - await run(t, fractBuilder(), [TypeF32], TypeF32, t.params, cases); + await run(t, fractBuilder(), [Type.f32], Type.f32, t.params, cases); }); g.test('f32_exp') @@ -195,7 +227,7 @@ struct __frexp_result_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_exp'); - await run(t, expBuilder(), [TypeF32], TypeI32, t.params, cases); + await run(t, expBuilder(), [Type.f32], Type.i32, t.params, cases); }); g.test('f32_vec2_fract') @@ -213,7 +245,7 @@ struct __frexp_result_vec2_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec2_fract'); - await run(t, fractBuilder(), [TypeVec(2, TypeF32)], TypeVec(2, TypeF32), t.params, cases); + await run(t, fractBuilder(), [Type.vec2f], Type.vec2f, t.params, cases); }); g.test('f32_vec2_exp') @@ -231,7 +263,7 @@ struct __frexp_result_vec2_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec2_exp'); - await run(t, expBuilder(), [TypeVec(2, TypeF32)], TypeVec(2, TypeI32), t.params, cases); + await run(t, expBuilder(), [Type.vec2f], Type.vec2i, t.params, cases); }); g.test('f32_vec3_fract') @@ -249,7 +281,7 @@ struct __frexp_result_vec3_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec3_fract'); - await run(t, fractBuilder(), [TypeVec(3, TypeF32)], TypeVec(3, TypeF32), t.params, cases); + await run(t, fractBuilder(), [Type.vec3f], Type.vec3f, t.params, cases); }); g.test('f32_vec3_exp') @@ -267,7 +299,7 @@ struct __frexp_result_vec3_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec3_exp'); - await run(t, expBuilder(), [TypeVec(3, TypeF32)], TypeVec(3, TypeI32), t.params, cases); + await run(t, expBuilder(), [Type.vec3f], Type.vec3i, t.params, cases); }); g.test('f32_vec4_fract') @@ -285,7 +317,7 @@ struct __frexp_result_vec4_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec4_fract'); - await run(t, fractBuilder(), [TypeVec(4, TypeF32)], TypeVec(4, TypeF32), t.params, cases); + await run(t, fractBuilder(), [Type.vec4f], Type.vec4f, t.params, cases); }); g.test('f32_vec4_exp') @@ -303,7 +335,7 @@ struct __frexp_result_vec4_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec4_exp'); - await run(t, expBuilder(), [TypeVec(4, TypeF32)], TypeVec(4, TypeI32), t.params, cases); + await run(t, expBuilder(), [Type.vec4f], Type.vec4i, t.params, cases); }); g.test('f16_fract') @@ -324,7 +356,7 @@ struct __frexp_result_f16 { }) .fn(async t => { const cases = await d.get('f16_fract'); - await run(t, fractBuilder(), [TypeF16], TypeF16, t.params, cases); + await run(t, fractBuilder(), [Type.f16], Type.f16, t.params, cases); }); g.test('f16_exp') @@ -345,7 +377,7 @@ struct __frexp_result_f16 { }) .fn(async t => { const cases = await d.get('f16_exp'); - await run(t, expBuilder(), [TypeF16], TypeI32, t.params, cases); + await run(t, expBuilder(), [Type.f16], Type.i32, t.params, cases); }); g.test('f16_vec2_fract') @@ -366,7 +398,7 @@ struct __frexp_result_vec2_f16 { }) .fn(async t => { const cases = await d.get('f16_vec2_fract'); - await run(t, fractBuilder(), [TypeVec(2, TypeF16)], TypeVec(2, TypeF16), t.params, cases); + await run(t, fractBuilder(), [Type.vec2h], Type.vec2h, t.params, cases); }); g.test('f16_vec2_exp') @@ -387,7 +419,7 @@ struct __frexp_result_vec2_f16 { }) .fn(async t => { const cases = await d.get('f16_vec2_exp'); - await run(t, expBuilder(), [TypeVec(2, TypeF16)], TypeVec(2, TypeI32), t.params, cases); + await run(t, expBuilder(), [Type.vec2h], Type.vec2i, t.params, cases); }); g.test('f16_vec3_fract') @@ -408,7 +440,7 @@ struct __frexp_result_vec3_f16 { }) .fn(async t => { const cases = await d.get('f16_vec3_fract'); - await run(t, fractBuilder(), [TypeVec(3, TypeF16)], TypeVec(3, TypeF16), t.params, cases); + await run(t, fractBuilder(), [Type.vec3h], Type.vec3h, t.params, cases); }); g.test('f16_vec3_exp') @@ -429,7 +461,7 @@ struct __frexp_result_vec3_f16 { }) .fn(async t => { const cases = await d.get('f16_vec3_exp'); - await run(t, expBuilder(), [TypeVec(3, TypeF16)], TypeVec(3, TypeI32), t.params, cases); + await run(t, expBuilder(), [Type.vec3h], Type.vec3i, t.params, cases); }); g.test('f16_vec4_fract') @@ -450,7 +482,7 @@ struct __frexp_result_vec4_f16 { }) .fn(async t => { const cases = await d.get('f16_vec4_fract'); - await run(t, fractBuilder(), [TypeVec(4, TypeF16)], TypeVec(4, TypeF16), t.params, cases); + await run(t, fractBuilder(), [Type.vec4h], Type.vec4h, t.params, cases); }); g.test('f16_vec4_exp') @@ -471,5 +503,5 @@ struct __frexp_result_vec4_f16 { }) .fn(async t => { const cases = await d.get('f16_vec4_exp'); - await run(t, expBuilder(), [TypeVec(4, TypeF16)], TypeVec(4, TypeI32), t.params, cases); + await run(t, expBuilder(), [Type.vec4h], Type.vec4i, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts index 1068e76252..b3eb65781d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts @@ -18,17 +18,7 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - i32Bits, - TypeI32, - u32, - TypeU32, - u32Bits, - vec2, - vec3, - vec4, - TypeVec, -} from '../../../../../util/conversion.js'; +import { i32Bits, Type, u32, u32Bits, vec2, vec3, vec4 } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -46,8 +36,8 @@ g.test('integer') ) .fn(async t => { const cfg: Config = t.params; - const scalarType = t.params.signed ? TypeI32 : TypeU32; - const T = t.params.width === 1 ? scalarType : TypeVec(t.params.width, scalarType); + const scalarType = t.params.signed ? Type.i32 : Type.u32; + const T = t.params.width === 1 ? scalarType : Type.vec(t.params.width, scalarType); const V = (x: number, y?: number, z?: number, w?: number) => { y = y === undefined ? x : y; @@ -382,5 +372,5 @@ g.test('integer') ); } - await run(t, builtin('insertBits'), [T, T, TypeU32, TypeU32], T, cfg, cases); + await run(t, builtin('insertBits'), [T, T, Type.u32, Type.u32], T, cfg, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.cache.ts new file mode 100644 index 0000000000..e50d6d4866 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.cache.ts @@ -0,0 +1,44 @@ +import { kValue } from '../../../../../util/constants.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange, linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('inverseSqrt', { + f32: () => { + return FP.f32.generateScalarToIntervalCases( + [ + // 0 < x <= 1 linearly spread + ...linearRange(kValue.f32.positive.min, 1, 100), + // 1 <= x < 2^32, biased towards 1 + ...biasedRange(1, 2 ** 32, 1000), + ], + 'unfiltered', + FP.f32.inverseSqrtInterval + ); + }, + f16: () => { + return FP.f16.generateScalarToIntervalCases( + [ + // 0 < x <= 1 linearly spread + ...linearRange(kValue.f16.positive.min, 1, 100), + // 1 <= x < 2^15, biased towards 1 + ...biasedRange(1, 2 ** 15, 1000), + ], + 'unfiltered', + FP.f16.inverseSqrtInterval + ); + }, + abstract: () => { + return FP.abstract.generateScalarToIntervalCases( + [ + // 0 < x <= 1 linearly spread + ...linearRange(kValue.f64.positive.min, 1, 100), + // 1 <= x < 2^64, biased towards 1, only using 100 steps, because af tests are more expensive per case + ...biasedRange(1, 2 ** 64, 100), + ], + 'finite', + // inverseSqrt has an ulp accuracy, so is only expected to be as accurate as f32 + FP.f32.inverseSqrtInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts index 3e83816387..954718de6c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'inverseSqrt' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn inverseSqrt(e: T ) -> T Returns the reciprocal of sqrt(e). Component-wise when T is a vector. @@ -9,51 +9,33 @@ Returns the reciprocal of sqrt(e). Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { biasedRange, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './inversesqrt.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('inverseSqrt', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - // 0 < x <= 1 linearly spread - ...linearRange(kValue.f32.positive.min, 1, 100), - // 1 <= x < 2^32, biased towards 1 - ...biasedRange(1, 2 ** 32, 1000), - ], - 'unfiltered', - FP.f32.inverseSqrtInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - // 0 < x <= 1 linearly spread - ...linearRange(kValue.f16.positive.min, 1, 100), - // 1 <= x < 2^15, biased towards 1 - ...biasedRange(1, 2 ** 15, 1000), - ], - 'unfiltered', - FP.f16.inverseSqrtInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('inverseSqrt'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -63,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('inverseSqrt'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('inverseSqrt'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -77,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('inverseSqrt'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('inverseSqrt'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.cache.ts new file mode 100644 index 0000000000..cf9194319b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.cache.ts @@ -0,0 +1,61 @@ +import { assert } from '../../../../../../common/util/util.js'; +import { anyOf } from '../../../../../util/compare.js'; +import { abstractInt, i32 } from '../../../../../util/conversion.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange, quantizeToI32, sparseI32Range } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// ldexpInterval's return interval doesn't cover the flush-to-zero cases when e2 + bias <= 0, thus +// special examination is required. +// See the comment block on ldexpInterval for more details +// e2 is an integer (i32) while e1 is float. +const makeCase = (trait: 'f32' | 'f16' | 'abstract', e1: number, e2: number): Case => { + const FPTrait = FP[trait]; + e1 = FPTrait.quantize(e1); + // e2 should be in i32 range for the convenience. + assert(-2147483648 <= e2 && e2 <= 2147483647, 'e2 should be in i32 range'); + e2 = quantizeToI32(e2); + + const expected = FPTrait.ldexpInterval(e1, e2); + + const e2_scalar = trait === 'abstract' ? abstractInt(BigInt(e2)) : i32(e2); + // Result may be zero if e2 + bias <= 0 + if (e2 + FPTrait.constants().bias <= 0) { + return { + input: [FPTrait.scalarBuilder(e1), e2_scalar], + expected: anyOf(expected, FPTrait.constants().zeroInterval), + }; + } + + return { input: [FPTrait.scalarBuilder(e1), e2_scalar], expected }; +}; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (nonConst) { + if (trait === 'abstract') { + return []; + } + return FP[trait] + .sparseScalarRange() + .flatMap(e1 => sparseI32Range().map(e2 => makeCase(trait, e1, e2))); + } + const bias = FP[trait].constants().bias; + // const + return FP[trait] + .sparseScalarRange() + .flatMap(e1 => + biasedRange(-bias - 10, bias + 1, 10).flatMap(e2 => + FP[trait].isFinite(e1 * 2 ** quantizeToI32(e2)) ? makeCase(trait, e1, e2) : [] + ) + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('ldexp', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts index 3829867752..d6cebfef6d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts @@ -1,10 +1,10 @@ export const description = ` Execution tests for the 'ldexp' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> -K is AbstractInt, i32 +K is Type.abstractInt, i32 I is K or vecN<K>, where I is a scalar if T is a scalar, or a vector when T is a vector @@ -13,77 +13,15 @@ Returns e1 * 2^e2. Component-wise when T is a vector. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; -import { assert } from '../../../../../../common/util/util.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { anyOf } from '../../../../../util/compare.js'; -import { i32, TypeF32, TypeF16, TypeI32 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { - biasedRange, - quantizeToI32, - sparseF32Range, - sparseI32Range, - sparseF16Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './ldexp.cache.js'; export const g = makeTestGroup(GPUTest); -const bias = { - f32: 127, - f16: 15, -} as const; - -// ldexpInterval's return interval doesn't cover the flush-to-zero cases when e2 + bias <= 0, thus -// special examination is required. -// See the comment block on ldexpInterval for more details -// e2 is an integer (i32) while e1 is float. -const makeCase = (trait: 'f32' | 'f16', e1: number, e2: number): Case => { - const FPTrait = FP[trait]; - e1 = FPTrait.quantize(e1); - // e2 should be in i32 range for the convinience. - assert(-2147483648 <= e2 && e2 <= 2147483647, 'e2 should be in i32 range'); - e2 = quantizeToI32(e2); - - const expected = FPTrait.ldexpInterval(e1, e2); - - // Result may be zero if e2 + bias <= 0 - if (e2 + bias[trait] <= 0) { - return { - input: [FPTrait.scalarBuilder(e1), i32(e2)], - expected: anyOf(expected, FPTrait.constants().zeroInterval), - }; - } - - return { input: [FPTrait.scalarBuilder(e1), i32(e2)], expected }; -}; - -export const d = makeCaseCache('ldexp', { - f32_non_const: () => { - return sparseF32Range().flatMap(e1 => sparseI32Range().map(e2 => makeCase('f32', e1, e2))); - }, - f32_const: () => { - return sparseF32Range().flatMap(e1 => - biasedRange(-bias.f32 - 10, bias.f32 + 1, 10).flatMap(e2 => - FP.f32.isFinite(e1 * 2 ** quantizeToI32(e2)) ? makeCase('f32', e1, e2) : [] - ) - ); - }, - f16_non_const: () => { - return sparseF16Range().flatMap(e1 => sparseI32Range().map(e2 => makeCase('f16', e1, e2))); - }, - f16_const: () => { - return sparseF16Range().flatMap(e1 => - biasedRange(-bias.f16 - 10, bias.f16 + 1, 10).flatMap(e2 => - FP.f16.isFinite(e1 * 2 ** quantizeToI32(e2)) ? makeCase('f16', e1, e2) : [] - ) - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc( @@ -91,9 +29,21 @@ g.test('abstract_float') ` ) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('ldexp'), + [Type.abstractFloat, Type.abstractInt], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -103,7 +53,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('ldexp'), [TypeF32, TypeI32], TypeF32, t.params, cases); + await run(t, builtin('ldexp'), [Type.f32, Type.i32], Type.f32, t.params, cases); }); g.test('f16') @@ -117,5 +67,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('ldexp'), [TypeF16, TypeI32], TypeF16, t.params, cases); + await run(t, builtin('ldexp'), [Type.f16, Type.i32], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.cache.ts new file mode 100644 index 0000000000..e7a2ee22ba --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.cache.ts @@ -0,0 +1,42 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const scalar_cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + trait !== 'abstract' ? 'unfiltered' : 'finite', + // length has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].lengthInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: [f32|f16|abstract]_vecN_[non_]const +const vec_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`${trait}_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateVectorToIntervalCases( + FP[trait].vectorRange(dim), + nonConst ? 'unfiltered' : 'finite', + // length has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].lengthInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('length', { + ...scalar_cases, + ...vec_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts index 85c1f85169..735c8468b7 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'length' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn length(e: T ) -> f32 Returns the length of e (e.g. abs(e) if T is a scalar, or sqrt(e[0]^2 + e[1]^2 + ...) if T is a vector). @@ -9,77 +9,77 @@ Returns the length of e (e.g. abs(e) if T is a scalar, or sqrt(e[0]^2 + e[1]^2 + import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { - fullF32Range, - fullF16Range, - vectorF32Range, - vectorF16Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; - -import { builtin } from './builtin.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; + +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './length.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: f32_vecN_[non_]const -const f32_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f32_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorToIntervalCases( - vectorF32Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f32.lengthInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_vecN_[non_]const -const f16_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f16_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorToIntervalCases( - vectorF16Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f16.lengthInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('length', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - fullF32Range(), - 'unfiltered', - FP.f32.lengthInterval +g.test('abstract_float') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract_float tests`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('length'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); + +g.test('abstract_float_vec2') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_const'); + await run( + t, + abstractFloatBuiltin('length'), + [Type.vec2af], + Type.abstractFloat, + t.params, + cases ); - }, - ...f32_vec_cases, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - fullF16Range(), - 'unfiltered', - FP.f16.lengthInterval + }); + +g.test('abstract_float_vec3') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract_float tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_const'); + await run( + t, + abstractFloatBuiltin('length'), + [Type.vec3af], + Type.abstractFloat, + t.params, + cases ); - }, - ...f16_vec_cases, -}); + }); -g.test('abstract_float') +g.test('abstract_float_vec4') .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') - .desc(`abstract float tests`) - .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) - ) - .unimplemented(); + .desc(`abstract_float tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_const'); + await run( + t, + abstractFloatBuiltin('length'), + [Type.vec4af], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') @@ -87,7 +87,7 @@ g.test('f32') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('length'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('length'), [Type.f32], Type.f32, t.params, cases); }); g.test('f32_vec2') @@ -98,7 +98,7 @@ g.test('f32_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const' ); - await run(t, builtin('length'), [TypeVec(2, TypeF32)], TypeF32, t.params, cases); + await run(t, builtin('length'), [Type.vec2f], Type.f32, t.params, cases); }); g.test('f32_vec3') @@ -109,7 +109,7 @@ g.test('f32_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const' ); - await run(t, builtin('length'), [TypeVec(3, TypeF32)], TypeF32, t.params, cases); + await run(t, builtin('length'), [Type.vec3f], Type.f32, t.params, cases); }); g.test('f32_vec4') @@ -120,7 +120,7 @@ g.test('f32_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const' ); - await run(t, builtin('length'), [TypeVec(4, TypeF32)], TypeF32, t.params, cases); + await run(t, builtin('length'), [Type.vec4f], Type.f32, t.params, cases); }); g.test('f16') @@ -132,7 +132,7 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('length'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('length'), [Type.f16], Type.f16, t.params, cases); }); g.test('f16_vec2') @@ -146,7 +146,7 @@ g.test('f16_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec2_const' : 'f16_vec2_non_const' ); - await run(t, builtin('length'), [TypeVec(2, TypeF16)], TypeF16, t.params, cases); + await run(t, builtin('length'), [Type.vec2h], Type.f16, t.params, cases); }); g.test('f16_vec3') @@ -160,7 +160,7 @@ g.test('f16_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec3_const' : 'f16_vec3_non_const' ); - await run(t, builtin('length'), [TypeVec(3, TypeF16)], TypeF16, t.params, cases); + await run(t, builtin('length'), [Type.vec3h], Type.f16, t.params, cases); }); g.test('f16_vec4') @@ -174,5 +174,5 @@ g.test('f16_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec4_const' : 'f16_vec4_non_const' ); - await run(t, builtin('length'), [TypeVec(4, TypeF16)], TypeF16, t.params, cases); + await run(t, builtin('length'), [Type.vec4h], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.cache.ts new file mode 100644 index 0000000000..76b8bfa1af --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.cache.ts @@ -0,0 +1,30 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange, linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// log's accuracy is defined in three regions { [0, 0.5), [0.5, 2.0], (2.0, +∞] } +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + [ + ...linearRange(FP[trait].constants().positive.min, 0.5, 20), + ...linearRange(0.5, 2.0, 20), + ...biasedRange(2.0, 2 ** 32, 1000), + ...FP[trait].scalarRange(), + ], + nonConst ? 'unfiltered' : 'finite', + // log has an absolute or ulp accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].logInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('log', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts index ac60e2b1bc..99b4a6983e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'log' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn log(e: T ) -> T Returns the natural logarithm of e. Component-wise when T is a vector. @@ -9,53 +9,33 @@ Returns the natural logarithm of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { biasedRange, fullF32Range, fullF16Range, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './log.cache.js'; export const g = makeTestGroup(GPUTest); -// log's accuracy is defined in three regions { [0, 0.5), [0.5, 2.0], (2.0, +∞] } -const f32_inputs = [ - ...linearRange(kValue.f32.positive.min, 0.5, 20), - ...linearRange(0.5, 2.0, 20), - ...biasedRange(2.0, 2 ** 32, 1000), - ...fullF32Range(), -]; -const f16_inputs = [ - ...linearRange(kValue.f16.positive.min, 0.5, 20), - ...linearRange(0.5, 2.0, 20), - ...biasedRange(2.0, 2 ** 32, 1000), - ...fullF16Range(), -]; - -export const d = makeCaseCache('log', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.logInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.logInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.logInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.logInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('log'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -71,7 +51,7 @@ TODO(#792): Decide what the ground-truth is for these tests. [1] ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('log'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('log'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -85,5 +65,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('log'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('log'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.cache.ts new file mode 100644 index 0000000000..d7781f328d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.cache.ts @@ -0,0 +1,30 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { biasedRange, linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// log2's accuracy is defined in three regions { [0, 0.5), [0.5, 2.0], (2.0, +∞] } +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + [ + ...linearRange(FP[trait].constants().positive.min, 0.5, 20), + ...linearRange(0.5, 2.0, 20), + ...biasedRange(2.0, 2 ** 32, 1000), + ...FP[trait].scalarRange(), + ], + nonConst ? 'unfiltered' : 'finite', + // log2 has an absolute or ulp accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].log2Interval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('log2', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts index 37931579b9..dc623a6784 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'log2' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn log2(e: T ) -> T Returns the base-2 logarithm of e. Component-wise when T is a vector. @@ -9,53 +9,33 @@ Returns the base-2 logarithm of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { biasedRange, fullF32Range, fullF16Range, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './log2.cache.js'; export const g = makeTestGroup(GPUTest); -// log2's accuracy is defined in three regions { [0, 0.5), [0.5, 2.0], (2.0, +∞] } -const f32_inputs = [ - ...linearRange(kValue.f32.positive.min, 0.5, 20), - ...linearRange(0.5, 2.0, 20), - ...biasedRange(2.0, 2 ** 32, 1000), - ...fullF32Range(), -]; -const f16_inputs = [ - ...linearRange(kValue.f16.positive.min, 0.5, 20), - ...linearRange(0.5, 2.0, 20), - ...biasedRange(2.0, 2 ** 32, 1000), - ...fullF16Range(), -]; - -export const d = makeCaseCache('log2', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'finite', FP.f32.log2Interval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(f32_inputs, 'unfiltered', FP.f32.log2Interval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'finite', FP.f16.log2Interval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(f16_inputs, 'unfiltered', FP.f16.log2Interval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('log2'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -71,7 +51,7 @@ TODO(#792): Decide what the ground-truth is for these tests. [1] ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('log2'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('log2'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -85,5 +65,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('log2'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('log2'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.cache.ts new file mode 100644 index 0000000000..72def03dab --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.cache.ts @@ -0,0 +1,18 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarPairToIntervalCases( + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + 'unfiltered', + FP[trait].maxInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('max', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts index 6654b4951c..ee7cb0d674 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts @@ -1,12 +1,12 @@ export const description = ` Execution tests for the 'max' builtin function -S is AbstractInt, i32, or u32 +S is abstract-int, i32, or u32 T is S or vecN<S> @const fn max(e1: T ,e2: T) -> T Returns e2 if e1 is less than e2, and e1 otherwise. Component-wise when T is a vector. -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is vecN<S> @const fn max(e1: T ,e2: T) -> T Returns e2 if e1 is less than e2, and e1 otherwise. @@ -18,72 +18,50 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - i32, - TypeF32, - TypeF16, - TypeI32, - TypeU32, - u32, - TypeAbstractFloat, -} from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range, sparseF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, onlyConstInputSource, run } from '../../expression.js'; - -import { abstractBuiltin, builtin } from './builtin.js'; +import { Type, i32, u32, abstractInt } from '../../../../../util/conversion.js'; +import { maxBigInt } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; + +import { abstractFloatBuiltin, abstractIntBuiltin, builtin } from './builtin.js'; +import { d } from './max.cache.js'; /** Generate set of max test cases from list of interesting values */ -function generateTestCases( - values: Array<number>, - makeCase: (x: number, y: number) => Case -): Array<Case> { - const cases = new Array<Case>(); - values.forEach(e => { - values.forEach(f => { - cases.push(makeCase(e, f)); +function generateTestCases<Type>(values: Type[], makeCase: (x: Type, y: Type) => Case): Case[] { + return values.flatMap(e => { + return values.map(f => { + return makeCase(e, f); }); }); - return cases; } export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('max', { - f32: () => { - return FP.f32.generateScalarPairToIntervalCases( - fullF32Range(), - fullF32Range(), - 'unfiltered', - FP.f32.maxInterval - ); - }, - f16: () => { - return FP.f16.generateScalarPairToIntervalCases( - fullF16Range(), - fullF16Range(), - 'unfiltered', - FP.f16.maxInterval - ); - }, - abstract: () => { - return FP.abstract.generateScalarPairToIntervalCases( - sparseF64Range(), - sparseF64Range(), - 'unfiltered', - FP.abstract.maxInterval - ); - }, -}); - g.test('abstract_int') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') .desc(`abstract int tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const makeCase = (x: bigint, y: bigint): Case => { + return { input: [abstractInt(x), abstractInt(y)], expected: abstractInt(maxBigInt(x, y)) }; + }; + + const test_values = [-0x70000000n, -2n, -1n, 0n, 1n, 2n, 0x70000000n]; + const cases = generateTestCases(test_values, makeCase); + + await run( + t, + abstractIntBuiltin('max'), + [Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); g.test('u32') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') @@ -96,10 +74,10 @@ g.test('u32') return { input: [u32(x), u32(y)], expected: u32(Math.max(x, y)) }; }; - const test_values: Array<number> = [0, 1, 2, 0x70000000, 0x80000000, 0xffffffff]; + const test_values: number[] = [0, 1, 2, 0x70000000, 0x80000000, 0xffffffff]; const cases = generateTestCases(test_values, makeCase); - await run(t, builtin('max'), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, builtin('max'), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('i32') @@ -113,10 +91,10 @@ g.test('i32') return { input: [i32(x), i32(y)], expected: i32(Math.max(x, y)) }; }; - const test_values: Array<number> = [-0x70000000, -2, -1, 0, 1, 2, 0x70000000]; + const test_values: number[] = [-0x70000000, -2, -1, 0, 1, 2, 0x70000000]; const cases = generateTestCases(test_values, makeCase); - await run(t, builtin('max'), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, builtin('max'), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('abstract_float') @@ -131,9 +109,9 @@ g.test('abstract_float') const cases = await d.get('abstract'); await run( t, - abstractBuiltin('max'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBuiltin('max'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -147,7 +125,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('max'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('max'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -161,5 +139,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('max'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('max'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.cache.ts new file mode 100644 index 0000000000..cddca325f0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.cache.ts @@ -0,0 +1,18 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarPairToIntervalCases( + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + 'unfiltered', + FP[trait].minInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('min', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts index 6c05319546..ac63641399 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts @@ -1,12 +1,12 @@ export const description = ` Execution tests for the 'min' builtin function -S is AbstractInt, i32, or u32 +S is abstract-int, i32, or u32 T is S or vecN<S> @const fn min(e1: T ,e2: T) -> T Returns e1 if e1 is less than e2, and e2 otherwise. Component-wise when T is a vector. -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn min(e1: T ,e2: T) -> T Returns e2 if e2 is less than e1, and e1 otherwise. @@ -17,72 +17,50 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - i32, - TypeF32, - TypeF16, - TypeI32, - TypeU32, - u32, - TypeAbstractFloat, -} from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range, sparseF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, onlyConstInputSource, run } from '../../expression.js'; - -import { abstractBuiltin, builtin } from './builtin.js'; +import { Type, i32, u32, abstractInt } from '../../../../../util/conversion.js'; +import { minBigInt } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -export const g = makeTestGroup(GPUTest); +import { abstractFloatBuiltin, abstractIntBuiltin, builtin } from './builtin.js'; +import { d } from './min.cache.js'; -export const d = makeCaseCache('min', { - f32: () => { - return FP.f32.generateScalarPairToIntervalCases( - fullF32Range(), - fullF32Range(), - 'unfiltered', - FP.f32.minInterval - ); - }, - f16: () => { - return FP.f16.generateScalarPairToIntervalCases( - fullF16Range(), - fullF16Range(), - 'unfiltered', - FP.f16.minInterval - ); - }, - abstract: () => { - return FP.abstract.generateScalarPairToIntervalCases( - sparseF64Range(), - sparseF64Range(), - 'unfiltered', - FP.abstract.minInterval - ); - }, -}); +export const g = makeTestGroup(GPUTest); /** Generate set of min test cases from list of interesting values */ -function generateTestCases( - values: Array<number>, - makeCase: (x: number, y: number) => Case -): Array<Case> { - const cases = new Array<Case>(); - values.forEach(e => { - values.forEach(f => { - cases.push(makeCase(e, f)); +function generateTestCases<Type>(values: Type[], makeCase: (x: Type, y: Type) => Case): Case[] { + return values.flatMap(e => { + return values.map(f => { + return makeCase(e, f); }); }); - return cases; } g.test('abstract_int') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') .desc(`abstract int tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const makeCase = (x: bigint, y: bigint): Case => { + return { input: [abstractInt(x), abstractInt(y)], expected: abstractInt(minBigInt(x, y)) }; + }; + + const test_values = [-0x70000000n, -2n, -1n, 0n, 1n, 2n, 0x70000000n]; + const cases = generateTestCases(test_values, makeCase); + + await run( + t, + abstractIntBuiltin('min'), + [Type.abstractInt, Type.abstractInt], + Type.abstractInt, + t.params, + cases + ); + }); g.test('u32') .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions') @@ -95,10 +73,10 @@ g.test('u32') return { input: [u32(x), u32(y)], expected: u32(Math.min(x, y)) }; }; - const test_values: Array<number> = [0, 1, 2, 0x70000000, 0x80000000, 0xffffffff]; + const test_values: number[] = [0, 1, 2, 0x70000000, 0x80000000, 0xffffffff]; const cases = generateTestCases(test_values, makeCase); - await run(t, builtin('min'), [TypeU32, TypeU32], TypeU32, t.params, cases); + await run(t, builtin('min'), [Type.u32, Type.u32], Type.u32, t.params, cases); }); g.test('i32') @@ -112,10 +90,10 @@ g.test('i32') return { input: [i32(x), i32(y)], expected: i32(Math.min(x, y)) }; }; - const test_values: Array<number> = [-0x70000000, -2, -1, 0, 1, 2, 0x70000000]; + const test_values: number[] = [-0x70000000, -2, -1, 0, 1, 2, 0x70000000]; const cases = generateTestCases(test_values, makeCase); - await run(t, builtin('min'), [TypeI32, TypeI32], TypeI32, t.params, cases); + await run(t, builtin('min'), [Type.i32, Type.i32], Type.i32, t.params, cases); }); g.test('abstract_float') @@ -130,9 +108,9 @@ g.test('abstract_float') const cases = await d.get('abstract'); await run( t, - abstractBuiltin('min'), - [TypeAbstractFloat, TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBuiltin('min'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -146,7 +124,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('min'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('min'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -160,5 +138,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('min'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('min'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.cache.ts new file mode 100644 index 0000000000..be221297b0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.cache.ts @@ -0,0 +1,56 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { selectNCases } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +// abstract_non_const is empty and unused +const scalar_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + const cases = FP[trait].generateScalarTripleToIntervalCases( + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + nonConst ? 'unfiltered' : 'finite', + // mix has an inherited accuracy, so abstract is only expected to be as accurate as f32 + ...FP[trait !== 'abstract' ? trait : 'f32'].mixIntervals + ); + return selectNCases('mix_scalar', trait === 'abstract' ? 50 : cases.length, cases); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: [f32|f16]_vecN_scalar_[non_]const +// abstract_vecN_non_const is empty and unused +const vec_scalar_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`${trait}_vec${dim}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + const cases = FP[trait].generateVectorPairScalarToVectorComponentWiseCase( + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + FP[trait].sparseScalarRange(), + nonConst ? 'unfiltered' : 'finite', + // mix has an inherited accuracy, so abstract is only expected to be as accurate as f32 + ...FP[trait !== 'abstract' ? trait : 'f32'].mixIntervals + ); + return selectNCases('mix_vector', trait === 'abstract' ? 50 : cases.length, cases); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('mix', { + ...scalar_cases, + ...vec_scalar_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts index 95e9f6b310..0005ab5c0e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts @@ -1,12 +1,12 @@ export const description = ` Execution tests for the 'mix' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn mix(e1: T, e2: T, e3: T) -> T Returns the linear blend of e1 and e2 (e.g. e1*(1-e3)+e2*e3). Component-wise when T is a vector. -T is AbstractFloat, f32, or f16 +T is abstract-float, f32, or f16 T2 is vecN<T> @const fn mix(e1: T2, e2: T2, e3: T) -> T2 Returns the component-wise linear blend of e1 and e2, using scalar blending factor e3 for each component. @@ -16,121 +16,81 @@ Same as mix(e1,e2,T2(e3)). import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeVec, TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { - sparseF32Range, - sparseF16Range, - sparseVectorF32Range, - sparseVectorF16Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './mix.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: f32_vecN_scalar_[non_]const -const f32_vec_scalar_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f32_vec${n}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorPairScalarToVectorComponentWiseCase( - sparseVectorF32Range(n), - sparseVectorF32Range(n), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite', - ...FP.f32.mixIntervals - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_vecN_scalar_[non_]const -const f16_vec_scalar_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f16_vec${n}_scalar_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorPairScalarToVectorComponentWiseCase( - sparseVectorF16Range(n), - sparseVectorF16Range(n), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite', - ...FP.f16.mixIntervals - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('mix', { - f32_const: () => { - return FP.f32.generateScalarTripleToIntervalCases( - sparseF32Range(), - sparseF32Range(), - sparseF32Range(), - 'finite', - ...FP.f32.mixIntervals - ); - }, - f32_non_const: () => { - return FP.f32.generateScalarTripleToIntervalCases( - sparseF32Range(), - sparseF32Range(), - sparseF32Range(), - 'unfiltered', - ...FP.f32.mixIntervals - ); - }, - ...f32_vec_scalar_cases, - f16_const: () => { - return FP.f16.generateScalarTripleToIntervalCases( - sparseF16Range(), - sparseF16Range(), - sparseF16Range(), - 'finite', - ...FP.f16.mixIntervals - ); - }, - f16_non_const: () => { - return FP.f16.generateScalarTripleToIntervalCases( - sparseF16Range(), - sparseF16Range(), - sparseF16Range(), - 'unfiltered', - ...FP.f16.mixIntervals - ); - }, - ...f16_vec_scalar_cases, -}); - g.test('abstract_float_matching') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract_float test with matching third param`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('mix'), + [Type.abstractFloat, Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('abstract_float_nonmatching_vec2') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract_float tests with two vec2<abstract_float> params and scalar third param`) - .params(u => u.combine('inputSource', allInputSources)) - .unimplemented(); + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_scalar_const'); + await run( + t, + abstractFloatBuiltin('mix'), + [Type.vec(2, Type.abstractFloat), Type.vec(2, Type.abstractFloat), Type.abstractFloat], + Type.vec(2, Type.abstractFloat), + t.params, + cases + ); + }); g.test('abstract_float_nonmatching_vec3') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract_float tests with two vec3<abstract_float> params and scalar third param`) - .params(u => u.combine('inputSource', allInputSources)) - .unimplemented(); + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_scalar_const'); + await run( + t, + abstractFloatBuiltin('mix'), + [Type.vec(3, Type.abstractFloat), Type.vec(3, Type.abstractFloat), Type.abstractFloat], + Type.vec(3, Type.abstractFloat), + t.params, + cases + ); + }); g.test('abstract_float_nonmatching_vec4') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract_float tests with two vec4<abstract_float> params and scalar third param`) - .params(u => u.combine('inputSource', allInputSources)) - .unimplemented(); + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_scalar_const'); + await run( + t, + abstractFloatBuiltin('mix'), + [Type.vec(4, Type.abstractFloat), Type.vec(4, Type.abstractFloat), Type.abstractFloat], + Type.vec(4, Type.abstractFloat), + t.params, + cases + ); + }); g.test('f32_matching') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -140,7 +100,7 @@ g.test('f32_matching') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('mix'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('mix'), [Type.f32, Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f32_nonmatching_vec2') @@ -151,14 +111,7 @@ g.test('f32_nonmatching_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec2_scalar_const' : 'f32_vec2_scalar_non_const' ); - await run( - t, - builtin('mix'), - [TypeVec(2, TypeF32), TypeVec(2, TypeF32), TypeF32], - TypeVec(2, TypeF32), - t.params, - cases - ); + await run(t, builtin('mix'), [Type.vec2f, Type.vec2f, Type.f32], Type.vec2f, t.params, cases); }); g.test('f32_nonmatching_vec3') @@ -169,14 +122,7 @@ g.test('f32_nonmatching_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec3_scalar_const' : 'f32_vec3_scalar_non_const' ); - await run( - t, - builtin('mix'), - [TypeVec(3, TypeF32), TypeVec(3, TypeF32), TypeF32], - TypeVec(3, TypeF32), - t.params, - cases - ); + await run(t, builtin('mix'), [Type.vec3f, Type.vec3f, Type.f32], Type.vec3f, t.params, cases); }); g.test('f32_nonmatching_vec4') @@ -187,14 +133,7 @@ g.test('f32_nonmatching_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec4_scalar_const' : 'f32_vec4_scalar_non_const' ); - await run( - t, - builtin('mix'), - [TypeVec(4, TypeF32), TypeVec(4, TypeF32), TypeF32], - TypeVec(4, TypeF32), - t.params, - cases - ); + await run(t, builtin('mix'), [Type.vec4f, Type.vec4f, Type.f32], Type.vec4f, t.params, cases); }); g.test('f16_matching') @@ -208,7 +147,7 @@ g.test('f16_matching') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('mix'), [TypeF16, TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('mix'), [Type.f16, Type.f16, Type.f16], Type.f16, t.params, cases); }); g.test('f16_nonmatching_vec2') @@ -222,14 +161,7 @@ g.test('f16_nonmatching_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec2_scalar_const' : 'f16_vec2_scalar_non_const' ); - await run( - t, - builtin('mix'), - [TypeVec(2, TypeF16), TypeVec(2, TypeF16), TypeF16], - TypeVec(2, TypeF16), - t.params, - cases - ); + await run(t, builtin('mix'), [Type.vec2h, Type.vec2h, Type.f16], Type.vec2h, t.params, cases); }); g.test('f16_nonmatching_vec3') @@ -243,14 +175,7 @@ g.test('f16_nonmatching_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec3_scalar_const' : 'f16_vec3_scalar_non_const' ); - await run( - t, - builtin('mix'), - [TypeVec(3, TypeF16), TypeVec(3, TypeF16), TypeF16], - TypeVec(3, TypeF16), - t.params, - cases - ); + await run(t, builtin('mix'), [Type.vec3h, Type.vec3h, Type.f16], Type.vec3h, t.params, cases); }); g.test('f16_nonmatching_vec4') @@ -264,12 +189,5 @@ g.test('f16_nonmatching_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec4_scalar_const' : 'f16_vec4_scalar_non_const' ); - await run( - t, - builtin('mix'), - [TypeVec(4, TypeF16), TypeVec(4, TypeF16), TypeF16], - TypeVec(4, TypeF16), - t.params, - cases - ); + await run(t, builtin('mix'), [Type.vec4h, Type.vec4h, Type.f16], Type.vec4h, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.cache.ts new file mode 100644 index 0000000000..1a76de56bb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.cache.ts @@ -0,0 +1,75 @@ +import { toVector } from '../../../../../util/conversion.js'; +import { FP, FPKind } from '../../../../../util/floating_point.js'; +import { Case } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +/** @returns a fract Case for a scalar vector input */ +function makeScalarCaseFract(kind: FPKind, n: number): Case { + const fp = FP[kind]; + n = fp.quantize(n); + const result = fp.modfInterval(n).fract; + + return { input: fp.scalarBuilder(n), expected: result }; +} + +/** @returns a whole Case for a scalar vector input */ +function makeScalarCaseWhole(kind: FPKind, n: number): Case { + const fp = FP[kind]; + n = fp.quantize(n); + const result = fp.modfInterval(n).whole; + + return { input: fp.scalarBuilder(n), expected: result }; +} + +/** @returns a fract Case for a given vector input */ +function makeVectorCaseFract(kind: FPKind, v: readonly number[]): Case { + const fp = FP[kind]; + v = v.map(fp.quantize); + const fs = v.map(e => { + return fp.modfInterval(e).fract; + }); + + return { input: toVector(v, fp.scalarBuilder), expected: fs }; +} + +/** @returns a whole Case for a given vector input */ +function makeVectorCaseWhole(kind: FPKind, v: readonly number[]): Case { + const fp = FP[kind]; + v = v.map(fp.quantize); + const ws = v.map(e => { + return fp.modfInterval(e).whole; + }); + + return { input: toVector(v, fp.scalarBuilder), expected: ws }; +} + +// Cases: [f32|f16|abstract]_[fract|whole] +const scalar_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(kind => + (['whole', 'fract'] as const).map(portion => ({ + [`${kind}_${portion}`]: () => { + const makeCase = portion === 'whole' ? makeScalarCaseWhole : makeScalarCaseFract; + return FP[kind].scalarRange().map(makeCase.bind(null, kind)); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: [f32|f16|abstract]_vecN_[fract|whole] +const vec_cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(kind => + ([2, 3, 4] as const).flatMap(n => + (['whole', 'fract'] as const).map(portion => ({ + [`${kind}_vec${n}_${portion}`]: () => { + const makeCase = portion === 'whole' ? makeVectorCaseWhole : makeVectorCaseFract; + return FP[kind].vectorRange(n).map(makeCase.bind(null, kind)); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('modf', { + ...scalar_cases, + ...vec_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts index 1a3d8a2850..6c988008f8 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts @@ -1,13 +1,13 @@ export const description = ` Execution tests for the 'modf' builtin function -T is f32 or f16 or AbstractFloat +T is f32 or f16 or Type.abstractFloat @const fn modf(e:T) -> result_struct Splits |e| into fractional and whole number parts. The whole part is (|e| % 1.0), and the fractional part is |e| minus the whole part. Returns the result_struct for the given type. -S is f32 or f16 or AbstractFloat +S is f32 or f16 or Type.abstractFloat T is vecN<S> @const fn modf(e:T) -> result_struct Splits the components of |e| into fractional and whole number parts. @@ -18,33 +18,18 @@ Returns the result_struct for the given type. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - toVector, - TypeAbstractFloat, - TypeF16, - TypeF32, - TypeVec, -} from '../../../../../util/conversion.js'; -import { FP, FPKind } from '../../../../../util/floating_point.js'; -import { - fullF16Range, - fullF32Range, - fullF64Range, - vectorF16Range, - vectorF32Range, - vectorF64Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { abstractFloatShaderBuilder, allInputSources, basicExpressionBuilder, - Case, onlyConstInputSource, run, ShaderBuilder, } from '../../expression.js'; +import { d } from './modf.cache.js'; + export const g = makeTestGroup(GPUTest); /** @returns an ShaderBuilder that evaluates modf and returns .whole from the result structure */ @@ -67,101 +52,6 @@ function abstractFractBuilder(): ShaderBuilder { return abstractFloatShaderBuilder(value => `modf(${value}).fract`); } -/** @returns a fract Case for a scalar vector input */ -function makeScalarCaseFract(kind: FPKind, n: number): Case { - const fp = FP[kind]; - n = fp.quantize(n); - const result = fp.modfInterval(n).fract; - - return { input: fp.scalarBuilder(n), expected: result }; -} - -/** @returns a whole Case for a scalar vector input */ -function makeScalarCaseWhole(kind: FPKind, n: number): Case { - const fp = FP[kind]; - n = fp.quantize(n); - const result = fp.modfInterval(n).whole; - - return { input: fp.scalarBuilder(n), expected: result }; -} - -/** @returns a fract Case for a given vector input */ -function makeVectorCaseFract(kind: FPKind, v: readonly number[]): Case { - const fp = FP[kind]; - v = v.map(fp.quantize); - const fs = v.map(e => { - return fp.modfInterval(e).fract; - }); - - return { input: toVector(v, fp.scalarBuilder), expected: fs }; -} - -/** @returns a whole Case for a given vector input */ -function makeVectorCaseWhole(kind: FPKind, v: readonly number[]): Case { - const fp = FP[kind]; - v = v.map(fp.quantize); - const ws = v.map(e => { - return fp.modfInterval(e).whole; - }); - - return { input: toVector(v, fp.scalarBuilder), expected: ws }; -} - -const scalar_range = { - f32: fullF32Range(), - f16: fullF16Range(), - abstract: fullF64Range(), -}; - -const vector_range = { - f32: { - 2: vectorF32Range(2), - 3: vectorF32Range(3), - 4: vectorF32Range(4), - }, - f16: { - 2: vectorF16Range(2), - 3: vectorF16Range(3), - 4: vectorF16Range(4), - }, - abstract: { - 2: vectorF64Range(2), - 3: vectorF64Range(3), - 4: vectorF64Range(4), - }, -}; - -// Cases: [f32|f16|abstract]_[fract|whole] -const scalar_cases = (['f32', 'f16', 'abstract'] as const) - .flatMap(kind => - (['whole', 'fract'] as const).map(portion => ({ - [`${kind}_${portion}`]: () => { - const makeCase = portion === 'whole' ? makeScalarCaseWhole : makeScalarCaseFract; - return scalar_range[kind].map(makeCase.bind(null, kind)); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: [f32|f16|abstract]_vecN_[fract|whole] -const vec_cases = (['f32', 'f16', 'abstract'] as const) - .flatMap(kind => - ([2, 3, 4] as const).flatMap(n => - (['whole', 'fract'] as const).map(portion => ({ - [`${kind}_vec${n}_${portion}`]: () => { - const makeCase = portion === 'whole' ? makeVectorCaseWhole : makeVectorCaseFract; - return vector_range[kind][n].map(makeCase.bind(null, kind)); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('modf', { - ...scalar_cases, - ...vec_cases, -}); - g.test('f32_fract') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc( @@ -177,7 +67,7 @@ struct __modf_result_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_fract'); - await run(t, fractBuilder(), [TypeF32], TypeF32, t.params, cases); + await run(t, fractBuilder(), [Type.f32], Type.f32, t.params, cases); }); g.test('f32_whole') @@ -195,7 +85,7 @@ struct __modf_result_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_whole'); - await run(t, wholeBuilder(), [TypeF32], TypeF32, t.params, cases); + await run(t, wholeBuilder(), [Type.f32], Type.f32, t.params, cases); }); g.test('f32_vec2_fract') @@ -213,7 +103,7 @@ struct __modf_result_vec2_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec2_fract'); - await run(t, fractBuilder(), [TypeVec(2, TypeF32)], TypeVec(2, TypeF32), t.params, cases); + await run(t, fractBuilder(), [Type.vec2f], Type.vec2f, t.params, cases); }); g.test('f32_vec2_whole') @@ -231,7 +121,7 @@ struct __modf_result_vec2_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec2_whole'); - await run(t, wholeBuilder(), [TypeVec(2, TypeF32)], TypeVec(2, TypeF32), t.params, cases); + await run(t, wholeBuilder(), [Type.vec2f], Type.vec2f, t.params, cases); }); g.test('f32_vec3_fract') @@ -249,7 +139,7 @@ struct __modf_result_vec3_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec3_fract'); - await run(t, fractBuilder(), [TypeVec(3, TypeF32)], TypeVec(3, TypeF32), t.params, cases); + await run(t, fractBuilder(), [Type.vec3f], Type.vec3f, t.params, cases); }); g.test('f32_vec3_whole') @@ -267,7 +157,7 @@ struct __modf_result_vec3_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec3_whole'); - await run(t, wholeBuilder(), [TypeVec(3, TypeF32)], TypeVec(3, TypeF32), t.params, cases); + await run(t, wholeBuilder(), [Type.vec3f], Type.vec3f, t.params, cases); }); g.test('f32_vec4_fract') @@ -285,7 +175,7 @@ struct __modf_result_vec4_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec4_fract'); - await run(t, fractBuilder(), [TypeVec(4, TypeF32)], TypeVec(4, TypeF32), t.params, cases); + await run(t, fractBuilder(), [Type.vec4f], Type.vec4f, t.params, cases); }); g.test('f32_vec4_whole') @@ -303,7 +193,7 @@ struct __modf_result_vec4_f32 { .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get('f32_vec4_whole'); - await run(t, wholeBuilder(), [TypeVec(4, TypeF32)], TypeVec(4, TypeF32), t.params, cases); + await run(t, wholeBuilder(), [Type.vec4f], Type.vec4f, t.params, cases); }); g.test('f16_fract') @@ -324,7 +214,7 @@ struct __modf_result_f16 { }) .fn(async t => { const cases = await d.get('f16_fract'); - await run(t, fractBuilder(), [TypeF16], TypeF16, t.params, cases); + await run(t, fractBuilder(), [Type.f16], Type.f16, t.params, cases); }); g.test('f16_whole') @@ -345,7 +235,7 @@ struct __modf_result_f16 { }) .fn(async t => { const cases = await d.get('f16_whole'); - await run(t, wholeBuilder(), [TypeF16], TypeF16, t.params, cases); + await run(t, wholeBuilder(), [Type.f16], Type.f16, t.params, cases); }); g.test('f16_vec2_fract') @@ -366,7 +256,7 @@ struct __modf_result_vec2_f16 { }) .fn(async t => { const cases = await d.get('f16_vec2_fract'); - await run(t, fractBuilder(), [TypeVec(2, TypeF16)], TypeVec(2, TypeF16), t.params, cases); + await run(t, fractBuilder(), [Type.vec2h], Type.vec2h, t.params, cases); }); g.test('f16_vec2_whole') @@ -387,7 +277,7 @@ struct __modf_result_vec2_f16 { }) .fn(async t => { const cases = await d.get('f16_vec2_whole'); - await run(t, wholeBuilder(), [TypeVec(2, TypeF16)], TypeVec(2, TypeF16), t.params, cases); + await run(t, wholeBuilder(), [Type.vec2h], Type.vec2h, t.params, cases); }); g.test('f16_vec3_fract') @@ -408,7 +298,7 @@ struct __modf_result_vec3_f16 { }) .fn(async t => { const cases = await d.get('f16_vec3_fract'); - await run(t, fractBuilder(), [TypeVec(3, TypeF16)], TypeVec(3, TypeF16), t.params, cases); + await run(t, fractBuilder(), [Type.vec3h], Type.vec3h, t.params, cases); }); g.test('f16_vec3_whole') @@ -429,7 +319,7 @@ struct __modf_result_vec3_f16 { }) .fn(async t => { const cases = await d.get('f16_vec3_whole'); - await run(t, wholeBuilder(), [TypeVec(3, TypeF16)], TypeVec(3, TypeF16), t.params, cases); + await run(t, wholeBuilder(), [Type.vec3h], Type.vec3h, t.params, cases); }); g.test('f16_vec4_fract') @@ -450,7 +340,7 @@ struct __modf_result_vec4_f16 { }) .fn(async t => { const cases = await d.get('f16_vec4_fract'); - await run(t, fractBuilder(), [TypeVec(4, TypeF16)], TypeVec(4, TypeF16), t.params, cases); + await run(t, fractBuilder(), [Type.vec4h], Type.vec4h, t.params, cases); }); g.test('f16_vec4_whole') @@ -471,43 +361,43 @@ struct __modf_result_vec4_f16 { }) .fn(async t => { const cases = await d.get('f16_vec4_whole'); - await run(t, wholeBuilder(), [TypeVec(4, TypeF16)], TypeVec(4, TypeF16), t.params, cases); + await run(t, wholeBuilder(), [Type.vec4h], Type.vec4h, t.params, cases); }); g.test('abstract_fract') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc( ` -T is AbstractFloat +T is abstract-float struct __modf_result_abstract { - fract : AbstractFloat, // fractional part - whole : AbstractFloat // whole part + fract : Type.abstractFloat, // fractional part + whole : Type.abstractFloat // whole part } ` ) .params(u => u.combine('inputSource', onlyConstInputSource)) .fn(async t => { const cases = await d.get('abstract_fract'); - await run(t, abstractFractBuilder(), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases); + await run(t, abstractFractBuilder(), [Type.abstractFloat], Type.abstractFloat, t.params, cases); }); g.test('abstract_whole') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc( ` -T is AbstractFloat +T is abstract-float struct __modf_result_abstract { - fract : AbstractFloat, // fractional part - whole : AbstractFloat // whole part + fract : Type.abstractFloat, // fractional part + whole : Type.abstractFloat // whole part } ` ) .params(u => u.combine('inputSource', onlyConstInputSource)) .fn(async t => { const cases = await d.get('abstract_whole'); - await run(t, abstractWholeBuilder(), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases); + await run(t, abstractWholeBuilder(), [Type.abstractFloat], Type.abstractFloat, t.params, cases); }); g.test('abstract_vec2_fract') @@ -528,8 +418,8 @@ struct __modf_result_vec2_abstract { await run( t, abstractFractBuilder(), - [TypeVec(2, TypeAbstractFloat)], - TypeVec(2, TypeAbstractFloat), + [Type.vec(2, Type.abstractFloat)], + Type.vec(2, Type.abstractFloat), t.params, cases ); @@ -553,8 +443,8 @@ struct __modf_result_vec2_abstract { await run( t, abstractWholeBuilder(), - [TypeVec(2, TypeAbstractFloat)], - TypeVec(2, TypeAbstractFloat), + [Type.vec(2, Type.abstractFloat)], + Type.vec(2, Type.abstractFloat), t.params, cases ); @@ -578,8 +468,8 @@ struct __modf_result_vec3_abstract { await run( t, abstractFractBuilder(), - [TypeVec(3, TypeAbstractFloat)], - TypeVec(3, TypeAbstractFloat), + [Type.vec(3, Type.abstractFloat)], + Type.vec(3, Type.abstractFloat), t.params, cases ); @@ -603,8 +493,8 @@ struct __modf_result_vec3_abstract { await run( t, abstractWholeBuilder(), - [TypeVec(3, TypeAbstractFloat)], - TypeVec(3, TypeAbstractFloat), + [Type.vec(3, Type.abstractFloat)], + Type.vec(3, Type.abstractFloat), t.params, cases ); @@ -628,8 +518,8 @@ struct __modf_result_vec4_abstract { await run( t, abstractFractBuilder(), - [TypeVec(4, TypeAbstractFloat)], - TypeVec(4, TypeAbstractFloat), + [Type.vec(4, Type.abstractFloat)], + Type.vec(4, Type.abstractFloat), t.params, cases ); @@ -653,8 +543,8 @@ struct __modf_result_vec4_abstract { await run( t, abstractWholeBuilder(), - [TypeVec(4, TypeAbstractFloat)], - TypeVec(4, TypeAbstractFloat), + [Type.vec(4, Type.abstractFloat)], + Type.vec(4, Type.abstractFloat), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.cache.ts new file mode 100644 index 0000000000..7da5a43c22 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.cache.ts @@ -0,0 +1,25 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_vecN_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`${trait}_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateVectorToVectorCases( + FP[trait].vectorRange(dim), + nonConst ? 'unfiltered' : 'finite', + // normalize has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].normalizeInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('normalize', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts index 615617b448..06be8a125e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts @@ -1,65 +1,47 @@ export const description = ` Execution tests for the 'normalize' builtin function -T is AbstractFloat, f32, or f16 +T is abstract-float, f32, or f16 @const fn normalize(e: vecN<T> ) -> vecN<T> Returns a unit vector in the same direction as e. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { vectorF32Range, vectorF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './normalize.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: f32_vecN_[non_]const -const f32_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f32_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorToVectorCases( - vectorF32Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f32.normalizeInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_vecN_[non_]const -const f16_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f16_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorToVectorCases( - vectorF16Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f16.normalizeInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); +g.test('abstract_float_vec2') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_const'); + await run(t, abstractFloatBuiltin('normalize'), [Type.vec2af], Type.vec2af, t.params, cases); + }); -export const d = makeCaseCache('normalize', { - ...f32_vec_cases, - ...f16_vec_cases, -}); +g.test('abstract_float_vec3') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_const'); + await run(t, abstractFloatBuiltin('normalize'), [Type.vec3af], Type.vec3af, t.params, cases); + }); -g.test('abstract_float') - .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') - .desc(`abstract float tests`) - .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) - ) - .unimplemented(); +g.test('abstract_float_vec4') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_const'); + await run(t, abstractFloatBuiltin('normalize'), [Type.vec4af], Type.vec4af, t.params, cases); + }); g.test('f32_vec2') .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') @@ -69,7 +51,7 @@ g.test('f32_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const' ); - await run(t, builtin('normalize'), [TypeVec(2, TypeF32)], TypeVec(2, TypeF32), t.params, cases); + await run(t, builtin('normalize'), [Type.vec2f], Type.vec2f, t.params, cases); }); g.test('f32_vec3') @@ -80,7 +62,7 @@ g.test('f32_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const' ); - await run(t, builtin('normalize'), [TypeVec(3, TypeF32)], TypeVec(3, TypeF32), t.params, cases); + await run(t, builtin('normalize'), [Type.vec3f], Type.vec3f, t.params, cases); }); g.test('f32_vec4') @@ -91,7 +73,7 @@ g.test('f32_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const' ); - await run(t, builtin('normalize'), [TypeVec(4, TypeF32)], TypeVec(4, TypeF32), t.params, cases); + await run(t, builtin('normalize'), [Type.vec4f], Type.vec4f, t.params, cases); }); g.test('f16_vec2') @@ -105,7 +87,7 @@ g.test('f16_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec2_const' : 'f16_vec2_non_const' ); - await run(t, builtin('normalize'), [TypeVec(2, TypeF16)], TypeVec(2, TypeF16), t.params, cases); + await run(t, builtin('normalize'), [Type.vec2h], Type.vec2h, t.params, cases); }); g.test('f16_vec3') @@ -119,7 +101,7 @@ g.test('f16_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec3_const' : 'f16_vec3_non_const' ); - await run(t, builtin('normalize'), [TypeVec(3, TypeF16)], TypeVec(3, TypeF16), t.params, cases); + await run(t, builtin('normalize'), [Type.vec3h], Type.vec3h, t.params, cases); }); g.test('f16_vec4') @@ -133,5 +115,5 @@ g.test('f16_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec4_const' : 'f16_vec4_non_const' ); - await run(t, builtin('normalize'), [TypeVec(4, TypeF16)], TypeVec(4, TypeF16), t.params, cases); + await run(t, builtin('normalize'), [Type.vec4h], Type.vec4h, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.cache.ts new file mode 100644 index 0000000000..9cd7824cd9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.cache.ts @@ -0,0 +1,55 @@ +import { anyOf, skipUndefined } from '../../../../../util/compare.js'; +import { f32, pack2x16float, u32, vec2 } from '../../../../../util/conversion.js'; +import { cartesianProduct, quantizeToF32, scalarF32Range } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// pack2x16float has somewhat unusual behaviour, specifically around how it is +// supposed to behave when values go OOB and when they are considered to have +// gone OOB, so has its own bespoke implementation. + +/** + * @returns a Case for `pack2x16float` + * @param param0 first param for the case + * @param param1 second param for the case + * @param filter_undefined should inputs that cause an undefined expectation be + * filtered out, needed for const-eval + */ +function makeCase(param0: number, param1: number, filter_undefined: boolean): Case | undefined { + param0 = quantizeToF32(param0); + param1 = quantizeToF32(param1); + + const results = pack2x16float(param0, param1); + if (filter_undefined && results.some(r => r === undefined)) { + return undefined; + } + + return { + input: [vec2(f32(param0), f32(param1))], + expected: anyOf( + ...results.map(r => (r === undefined ? skipUndefined(undefined) : skipUndefined(u32(r)))) + ), + }; +} + +/** + * @returns an array of Cases for `pack2x16float` + * @param param0s array of inputs to try for the first param + * @param param1s array of inputs to try for the second param + * @param filter_undefined should inputs that cause an undefined expectation be + * filtered out, needed for const-eval + */ +function generateCases(param0s: number[], param1s: number[], filter_undefined: boolean): Case[] { + return cartesianProduct(param0s, param1s) + .map(e => makeCase(e[0], e[1], filter_undefined)) + .filter((c): c is Case => c !== undefined); +} + +export const d = makeCaseCache('pack2x16float', { + f32_const: () => { + return generateCases(scalarF32Range(), scalarF32Range(), true); + }, + f32_non_const: () => { + return generateCases(scalarF32Range(), scalarF32Range(), false); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts index 790e54720c..5ba6993427 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts @@ -6,74 +6,14 @@ which is then placed in bits 16 × i through 16 × i + 15 of the result. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { anyOf, skipUndefined } from '../../../../../util/compare.js'; -import { - f32, - pack2x16float, - TypeF32, - TypeU32, - TypeVec, - u32, - vec2, -} from '../../../../../util/conversion.js'; -import { cartesianProduct, fullF32Range, quantizeToF32 } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; +import { d } from './pack2x16float.cache.js'; export const g = makeTestGroup(GPUTest); -// pack2x16float has somewhat unusual behaviour, specifically around how it is -// supposed to behave when values go OOB and when they are considered to have -// gone OOB, so has its own bespoke implementation. - -/** - * @returns a Case for `pack2x16float` - * @param param0 first param for the case - * @param param1 second param for the case - * @param filter_undefined should inputs that cause an undefined expectation be - * filtered out, needed for const-eval - */ -function makeCase(param0: number, param1: number, filter_undefined: boolean): Case | undefined { - param0 = quantizeToF32(param0); - param1 = quantizeToF32(param1); - - const results = pack2x16float(param0, param1); - if (filter_undefined && results.some(r => r === undefined)) { - return undefined; - } - - return { - input: [vec2(f32(param0), f32(param1))], - expected: anyOf( - ...results.map(r => (r === undefined ? skipUndefined(undefined) : skipUndefined(u32(r)))) - ), - }; -} - -/** - * @returns an array of Cases for `pack2x16float` - * @param param0s array of inputs to try for the first param - * @param param1s array of inputs to try for the second param - * @param filter_undefined should inputs that cause an undefined expectation be - * filtered out, needed for const-eval - */ -function generateCases(param0s: number[], param1s: number[], filter_undefined: boolean): Case[] { - return cartesianProduct(param0s, param1s) - .map(e => makeCase(e[0], e[1], filter_undefined)) - .filter((c): c is Case => c !== undefined); -} - -export const d = makeCaseCache('pack2x16float', { - f32_const: () => { - return generateCases(fullF32Range(), fullF32Range(), true); - }, - f32_non_const: () => { - return generateCases(fullF32Range(), fullF32Range(), false); - }, -}); - g.test('pack') .specURL('https://www.w3.org/TR/WGSL/#pack-builtin-functions') .desc( @@ -84,5 +24,5 @@ g.test('pack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('pack2x16float'), [TypeVec(2, TypeF32)], TypeU32, t.params, cases); + await run(t, builtin('pack2x16float'), [Type.vec2f], Type.u32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts index 54bb21f6c6..1bcca2f73f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts @@ -8,17 +8,10 @@ bits 16 × i through 16 × i + 15 of the result. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; import { kValue } from '../../../../../util/constants.js'; -import { - f32, - pack2x16snorm, - TypeF32, - TypeU32, - TypeVec, - u32, - vec2, -} from '../../../../../util/conversion.js'; +import { f32, pack2x16snorm, u32, vec2, Type } from '../../../../../util/conversion.js'; import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js'; -import { allInputSources, Case, run } from '../../expression.js'; +import { Case } from '../../case.js'; +import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -51,5 +44,5 @@ g.test('pack') ]; }); - await run(t, builtin('pack2x16snorm'), [TypeVec(2, TypeF32)], TypeU32, t.params, cases); + await run(t, builtin('pack2x16snorm'), [Type.vec2f], Type.u32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts index a875a9c7e1..334d106482 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts @@ -8,17 +8,10 @@ bits 16 × i through 16 × i + 15 of the result. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; import { kValue } from '../../../../../util/constants.js'; -import { - f32, - pack2x16unorm, - TypeF32, - TypeU32, - TypeVec, - u32, - vec2, -} from '../../../../../util/conversion.js'; +import { f32, pack2x16unorm, u32, vec2, Type } from '../../../../../util/conversion.js'; import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js'; -import { allInputSources, Case, run } from '../../expression.js'; +import { Case } from '../../case.js'; +import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -51,5 +44,5 @@ g.test('pack') ]; }); - await run(t, builtin('pack2x16unorm'), [TypeVec(2, TypeF32)], TypeU32, t.params, cases); + await run(t, builtin('pack2x16unorm'), [Type.vec2f], Type.u32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts index de0463e9fc..fbe362ea45 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts @@ -8,18 +8,10 @@ bits 8 × i through 8 × i + 7 of the result. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; import { kValue } from '../../../../../util/constants.js'; -import { - f32, - pack4x8snorm, - Scalar, - TypeF32, - TypeU32, - TypeVec, - u32, - vec4, -} from '../../../../../util/conversion.js'; +import { f32, pack4x8snorm, ScalarValue, u32, vec4, Type } from '../../../../../util/conversion.js'; import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js'; -import { allInputSources, Case, run } from '../../expression.js'; +import { Case } from '../../case.js'; +import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -35,7 +27,12 @@ g.test('pack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const makeCase = (vals: [number, number, number, number]): Case => { - const vals_f32 = new Array<Scalar>(4) as [Scalar, Scalar, Scalar, Scalar]; + const vals_f32 = new Array<ScalarValue>(4) as [ + ScalarValue, + ScalarValue, + ScalarValue, + ScalarValue, + ]; for (const idx in vals) { vals[idx] = quantizeToF32(vals[idx]); vals_f32[idx] = f32(vals[idx]); @@ -56,5 +53,5 @@ g.test('pack') ]; }); - await run(t, builtin('pack4x8snorm'), [TypeVec(4, TypeF32)], TypeU32, t.params, cases); + await run(t, builtin('pack4x8snorm'), [Type.vec4f], Type.u32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts index b670e92fbb..c7d62e722b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts @@ -8,18 +8,10 @@ bits 8 × i through 8 × i + 7 of the result. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; import { kValue } from '../../../../../util/constants.js'; -import { - f32, - pack4x8unorm, - Scalar, - TypeF32, - TypeU32, - TypeVec, - u32, - vec4, -} from '../../../../../util/conversion.js'; +import { f32, pack4x8unorm, ScalarValue, u32, vec4, Type } from '../../../../../util/conversion.js'; import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js'; -import { allInputSources, Case, run } from '../../expression.js'; +import { Case } from '../../case.js'; +import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -35,7 +27,12 @@ g.test('pack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const makeCase = (vals: [number, number, number, number]): Case => { - const vals_f32 = new Array<Scalar>(4) as [Scalar, Scalar, Scalar, Scalar]; + const vals_f32 = new Array<ScalarValue>(4) as [ + ScalarValue, + ScalarValue, + ScalarValue, + ScalarValue, + ]; for (const idx in vals) { vals[idx] = quantizeToF32(vals[idx]); vals_f32[idx] = f32(vals[idx]); @@ -56,5 +53,5 @@ g.test('pack') ]; }); - await run(t, builtin('pack4x8unorm'), [TypeVec(4, TypeF32)], TypeU32, t.params, cases); + await run(t, builtin('pack4x8unorm'), [Type.vec4f], Type.u32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xI8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xI8.spec.ts new file mode 100644 index 0000000000..94aa67f0ae --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xI8.spec.ts @@ -0,0 +1,69 @@ +export const description = ` +Execution tests for the 'pack4xI8' builtin function + +@const fn pack4xI8(e: vec4<i32>) -> u32 +Pack the lower 8 bits of each component of e into a u32 value and drop all the unused bits. +Component e[i] of the input is mapped to bits (8 * i) through (8 * (i + 7)) of the result. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { u32, toVector, i32, Type } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#pack4xI8-builtin') + .desc( + ` +@const fn pack4xI8(e: vec4<i32>) -> u32 + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const pack4xI8 = (vals: readonly [number, number, number, number]) => { + const result = new Uint32Array(1); + for (let i = 0; i < 4; ++i) { + result[0] |= (vals[i] & 0xff) << (i * 8); + } + return result[0]; + }; + + const testInputs = [ + [0, 0, 0, 0], + [1, 2, 3, 4], + [-1, 2, 3, 4], + [1, -2, 3, 4], + [1, 2, -3, 4], + [1, 2, 3, -4], + [-1, -2, 3, 4], + [-1, 2, -3, 4], + [-1, 2, 3, -4], + [1, -2, -3, 4], + [1, -2, 3, -4], + [1, 2, -3, -4], + [-1, -2, -3, 4], + [-1, -2, 3, -4], + [-1, 2, -3, -4], + [1, -2, -3, -4], + [-1, -2, -3, -4], + [127, 128, -128, -129], + [128, 128, -128, -128], + [32767, 32768, -32768, -32769], + ] as const; + + const makeCase = (vals: readonly [number, number, number, number]): Case => { + return { input: [toVector(vals, i32)], expected: u32(pack4xI8(vals)) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(v)]; + }); + + await run(t, builtin('pack4xI8'), [Type.vec4i], Type.u32, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xI8Clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xI8Clamp.spec.ts new file mode 100644 index 0000000000..4968ed1e04 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xI8Clamp.spec.ts @@ -0,0 +1,73 @@ +export const description = ` +Execution tests for the 'pack4xI8Clamp' builtin function + +@const fn pack4xI8Clamp(e: vec4<i32>) -> u32 +Clamp each component of e in the range [-128, 127] and then pack the lower 8 bits of each component +into a u32 value. Component e[i] of the input is mapped to bits (8 * i) through (8 * (i + 7)) of the +result. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { u32, toVector, i32, Type } from '../../../../../util/conversion.js'; +import { clamp } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#pack4xI8Clamp-builtin') + .desc( + ` +@const fn pack4xI8Clamp(e: vec4<i32>) -> u32 + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const pack4xI8Clamp = (vals: readonly [number, number, number, number]) => { + const result = new Uint32Array(1); + for (let i = 0; i < 4; ++i) { + const clampedValue = clamp(vals[i], { min: -128, max: 127 }); + result[0] |= (clampedValue & 0xff) << (i * 8); + } + return result[0]; + }; + + const testInputs = [ + [0, 0, 0, 0], + [1, 2, 3, 4], + [-1, 2, 3, 4], + [1, -2, 3, 4], + [1, 2, -3, 4], + [1, 2, 3, -4], + [-1, -2, 3, 4], + [-1, 2, -3, 4], + [-1, 2, 3, -4], + [1, -2, -3, 4], + [1, -2, 3, -4], + [1, 2, -3, -4], + [-1, -2, -3, 4], + [-1, -2, 3, -4], + [-1, 2, -3, -4], + [1, -2, -3, -4], + [-1, -2, -3, -4], + [126, 127, 128, 129], + [-130, -129, -128, -127], + [127, 128, -128, -129], + [32767, 32768, -32768, -32769], + ] as const; + + const makeCase = (vals: readonly [number, number, number, number]): Case => { + return { input: [toVector(vals, i32)], expected: u32(pack4xI8Clamp(vals)) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(v)]; + }); + + await run(t, builtin('pack4xI8Clamp'), [Type.vec4i], Type.u32, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xU8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xU8.spec.ts new file mode 100644 index 0000000000..9d08e88d44 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xU8.spec.ts @@ -0,0 +1,54 @@ +export const description = ` +Execution tests for the 'pack4xU8' builtin function + +@const fn pack4xU8(e: vec4<u32>) -> u32 +Pack the lower 8 bits of each component of e into a u32 value and drop all the unused bits. +Component e[i] of the input is mapped to bits (8 * i) through (8 * (i + 7)) of the result. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { u32, toVector, Type } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#pack4xU8-builtin') + .desc( + ` +@const fn pack4xU8(e: vec4<u32>) -> u32 + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const pack4xU8 = (vals: readonly [number, number, number, number]) => { + const result = new Uint32Array(1); + for (let i = 0; i < 4; ++i) { + result[0] |= (vals[i] & 0xff) << (i * 8); + } + return result[0]; + }; + + const testInputs = [ + [0, 0, 0, 0], + [1, 2, 3, 4], + [255, 255, 255, 255], + [254, 255, 256, 257], + [65535, 65536, 255, 254], + ] as const; + + const makeCase = (vals: readonly [number, number, number, number]): Case => { + return { input: [toVector(vals, u32)], expected: u32(pack4xU8(vals)) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(v)]; + }); + + await run(t, builtin('pack4xU8'), [Type.vec4u], Type.u32, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xU8Clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xU8Clamp.spec.ts new file mode 100644 index 0000000000..ffecf9b877 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4xU8Clamp.spec.ts @@ -0,0 +1,57 @@ +export const description = ` +Execution tests for the 'pack4xU8Clamp' builtin function + +@const fn pack4xU8Clamp(e: vec4<u32>) -> u32 +Clamp each component of e in the range of [0, 255] and then pack the lower 8 bits of each component +into a u32 value. Component e[i] of the input is mapped to bits (8 * i) through (8 * (i + 7)) of the +result. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { u32, toVector, Type } from '../../../../../util/conversion.js'; +import { clamp } from '../../../../../util/math.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#pack4xU8Clamp-builtin') + .desc( + ` +@const fn pack4xU8Clamp(e: vec4<u32>) -> u32 + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const pack4xU8Clamp = (vals: readonly [number, number, number, number]) => { + const result = new Uint32Array(1); + for (let i = 0; i < 4; ++i) { + const clampedValue = clamp(vals[i], { min: 0, max: 255 }); + result[0] |= clampedValue << (i * 8); + } + return result[0]; + }; + + const testInputs = [ + [0, 0, 0, 0], + [1, 2, 3, 4], + [255, 255, 255, 255], + [254, 255, 256, 257], + [65535, 65536, 255, 254], + ] as const; + + const makeCase = (vals: readonly [number, number, number, number]): Case => { + return { input: [toVector(vals, u32)], expected: u32(pack4xU8Clamp(vals)) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(v)]; + }); + + await run(t, builtin('pack4xU8Clamp'), [Type.vec4u], Type.u32, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.cache.ts new file mode 100644 index 0000000000..54777e702f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.cache.ts @@ -0,0 +1,24 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarPairToIntervalCases( + FP[trait].scalarRange(), + FP[trait].scalarRange(), + nonConst ? 'unfiltered' : 'finite', + // pow has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].powInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('pow', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts index f9b4fe1cfa..84e9649c96 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'pow' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn pow(e1: T ,e2: T ) -> T Returns e1 raised to the power e2. Component-wise when T is a vector. @@ -9,58 +9,33 @@ Returns e1 raised to the power e2. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './pow.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('pow', { - f32_const: () => { - return FP.f32.generateScalarPairToIntervalCases( - fullF32Range(), - fullF32Range(), - 'finite', - FP.f32.powInterval - ); - }, - f32_non_const: () => { - return FP.f32.generateScalarPairToIntervalCases( - fullF32Range(), - fullF32Range(), - 'unfiltered', - FP.f32.powInterval - ); - }, - f16_const: () => { - return FP.f16.generateScalarPairToIntervalCases( - fullF16Range(), - fullF16Range(), - 'finite', - FP.f16.powInterval - ); - }, - f16_non_const: () => { - return FP.f16.generateScalarPairToIntervalCases( - fullF16Range(), - fullF16Range(), - 'unfiltered', - FP.f16.powInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('pow'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -70,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('pow'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('pow'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -84,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('pow'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('pow'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.cache.ts new file mode 100644 index 0000000000..91aa845d29 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.cache.ts @@ -0,0 +1,41 @@ +import { kValue } from '../../../../../util/constants.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { scalarF16Range, scalarF32Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('quantizeToF16', { + f32_const: () => { + return FP.f32.generateScalarToIntervalCases( + [ + kValue.f16.negative.min, + kValue.f16.negative.max, + kValue.f16.negative.subnormal.min, + kValue.f16.negative.subnormal.max, + kValue.f16.positive.subnormal.min, + kValue.f16.positive.subnormal.max, + kValue.f16.positive.min, + kValue.f16.positive.max, + ...scalarF16Range(), + ], + 'finite', + FP.f32.quantizeToF16Interval + ); + }, + f32_non_const: () => { + return FP.f32.generateScalarToIntervalCases( + [ + kValue.f16.negative.min, + kValue.f16.negative.max, + kValue.f16.negative.subnormal.min, + kValue.f16.negative.subnormal.max, + kValue.f16.positive.subnormal.min, + kValue.f16.positive.subnormal.max, + kValue.f16.positive.min, + kValue.f16.positive.max, + ...scalarF32Range(), + ], + 'unfiltered', + FP.f32.quantizeToF16Interval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts index b37d4c5afb..0aa9669e93 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts @@ -10,54 +10,14 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { kValue } from '../../../../../util/constants.js'; -import { TypeF32 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF16Range, fullF32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; +import { d } from './quantizeToF16.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('quantizeToF16', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases( - [ - kValue.f16.negative.min, - kValue.f16.negative.max, - kValue.f16.negative.subnormal.min, - kValue.f16.negative.subnormal.max, - kValue.f16.positive.subnormal.min, - kValue.f16.positive.subnormal.max, - kValue.f16.positive.min, - kValue.f16.positive.max, - ...fullF16Range(), - ], - 'finite', - FP.f32.quantizeToF16Interval - ); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases( - [ - kValue.f16.negative.min, - kValue.f16.negative.max, - kValue.f16.negative.subnormal.min, - kValue.f16.negative.subnormal.max, - kValue.f16.positive.subnormal.min, - kValue.f16.positive.subnormal.max, - kValue.f16.positive.min, - kValue.f16.positive.max, - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.quantizeToF16Interval - ); - }, -}); - g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`f32 tests`) @@ -66,5 +26,5 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('quantizeToF16'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('quantizeToF16'), [Type.f32], Type.f32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.cache.ts new file mode 100644 index 0000000000..8ed0fbbd2b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.cache.ts @@ -0,0 +1,18 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + trait !== 'abstract' ? 'unfiltered' : 'finite', + // radians has an inherited accuracy, so abstract is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].radiansInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('radians', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts index 63ae45b656..a405807ec0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'radians' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn radians(e1: T ) -> T Converts degrees to radians, approximating e1 * π / 180. @@ -10,40 +10,14 @@ Component-wise when T is a vector import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeF16, TypeF32 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF16Range, fullF32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './radians.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('radians', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - fullF32Range(), - 'unfiltered', - FP.f32.radiansInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - fullF16Range(), - 'unfiltered', - FP.f16.radiansInterval - ); - }, - abstract: () => { - return FP.abstract.generateScalarToIntervalCases( - fullF16Range(), - 'unfiltered', - FP.abstract.radiansInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) @@ -56,9 +30,9 @@ g.test('abstract_float') const cases = await d.get('abstract'); await run( t, - abstractBuiltin('radians'), - [TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBuiltin('radians'), + [Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -72,7 +46,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('radians'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('radians'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -86,5 +60,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('radians'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('radians'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.cache.ts new file mode 100644 index 0000000000..ca57226f3a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.cache.ts @@ -0,0 +1,26 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_vecN_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`${trait}_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateVectorPairToVectorCases( + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + nonConst ? 'unfiltered' : 'finite', + // reflect has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].reflectInterval + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('reflect', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts index 2614c4e686..e36558d30c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'reflect' builtin function -T is vecN<AbstractFloat>, vecN<f32>, or vecN<f16> +T is vecN<Type.abstractFloat>, vecN<f32>, or vecN<f16> @const fn reflect(e1: T, e2: T ) -> T For the incident vector e1 and surface orientation e2, returns the reflection direction e1-2*dot(e2,e1)*e2. @@ -9,58 +9,61 @@ direction e1-2*dot(e2,e1)*e2. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { sparseVectorF32Range, sparseVectorF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './reflect.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: f32_vecN_[non_]const -const f32_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f32_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateVectorPairToVectorCases( - sparseVectorF32Range(n), - sparseVectorF32Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f32.reflectInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_vecN_[non_]const -const f16_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f16_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateVectorPairToVectorCases( - sparseVectorF16Range(n), - sparseVectorF16Range(n), - nonConst ? 'unfiltered' : 'finite', - FP.f16.reflectInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); +g.test('abstract_float_vec2') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_const'); + await run( + t, + abstractFloatBuiltin('reflect'), + [Type.vec2af, Type.vec2af], + Type.vec2af, + t.params, + cases + ); + }); -export const d = makeCaseCache('reflect', { - ...f32_vec_cases, - ...f16_vec_cases, -}); +g.test('abstract_float_vec3') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_const'); + await run( + t, + abstractFloatBuiltin('reflect'), + [Type.vec3af, Type.vec3af], + Type.vec3af, + t.params, + cases + ); + }); -g.test('abstract_float') - .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') - .desc(`abstract float tests`) - .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const)) - .unimplemented(); +g.test('abstract_float_vec4') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_const'); + await run( + t, + abstractFloatBuiltin('reflect'), + [Type.vec4af, Type.vec4af], + Type.vec4af, + t.params, + cases + ); + }); g.test('f32_vec2') .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') @@ -70,14 +73,7 @@ g.test('f32_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const' ); - await run( - t, - builtin('reflect'), - [TypeVec(2, TypeF32), TypeVec(2, TypeF32)], - TypeVec(2, TypeF32), - t.params, - cases - ); + await run(t, builtin('reflect'), [Type.vec2f, Type.vec2f], Type.vec2f, t.params, cases); }); g.test('f32_vec3') @@ -88,14 +84,7 @@ g.test('f32_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const' ); - await run( - t, - builtin('reflect'), - [TypeVec(3, TypeF32), TypeVec(3, TypeF32)], - TypeVec(3, TypeF32), - t.params, - cases - ); + await run(t, builtin('reflect'), [Type.vec3f, Type.vec3f], Type.vec3f, t.params, cases); }); g.test('f32_vec4') @@ -106,14 +95,7 @@ g.test('f32_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const' ); - await run( - t, - builtin('reflect'), - [TypeVec(4, TypeF32), TypeVec(4, TypeF32)], - TypeVec(4, TypeF32), - t.params, - cases - ); + await run(t, builtin('reflect'), [Type.vec4f, Type.vec4f], Type.vec4f, t.params, cases); }); g.test('f16_vec2') @@ -127,14 +109,7 @@ g.test('f16_vec2') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec2_const' : 'f16_vec2_non_const' ); - await run( - t, - builtin('reflect'), - [TypeVec(2, TypeF16), TypeVec(2, TypeF16)], - TypeVec(2, TypeF16), - t.params, - cases - ); + await run(t, builtin('reflect'), [Type.vec2h, Type.vec2h], Type.vec2h, t.params, cases); }); g.test('f16_vec3') @@ -148,14 +123,7 @@ g.test('f16_vec3') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec3_const' : 'f16_vec3_non_const' ); - await run( - t, - builtin('reflect'), - [TypeVec(3, TypeF16), TypeVec(3, TypeF16)], - TypeVec(3, TypeF16), - t.params, - cases - ); + await run(t, builtin('reflect'), [Type.vec3h, Type.vec3h], Type.vec3h, t.params, cases); }); g.test('f16_vec4') @@ -169,12 +137,5 @@ g.test('f16_vec4') const cases = await d.get( t.params.inputSource === 'const' ? 'f16_vec4_const' : 'f16_vec4_non_const' ); - await run( - t, - builtin('reflect'), - [TypeVec(4, TypeF16), TypeVec(4, TypeF16)], - TypeVec(4, TypeF16), - t.params, - cases - ); + await run(t, builtin('reflect'), [Type.vec4h, Type.vec4h], Type.vec4h, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.cache.ts new file mode 100644 index 0000000000..a759f5b669 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.cache.ts @@ -0,0 +1,116 @@ +import { ROArrayArray } from '../../../../../../common/util/types.js'; +import { toVector } from '../../../../../util/conversion.js'; +import { FP, FPKind } from '../../../../../util/floating_point.js'; +import { Case, selectNCases } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; +import { IntervalFilter } from '../../interval_filter.js'; + +// Using a bespoke implementation of make*Case and generate*Cases here +// since refract is the only builtin with the API signature +// (vec, vec, scalar) -> vec + +/** + * @returns a Case for `refract` + * @param argumentKind what kind of floating point numbers being operated on + * @param parameterKind what kind of floating point operation should be performed, + * should be the same as argumentKind, except for abstract + * @param i the `i` param for the case + * @param s the `s` param for the case + * @param r the `r` param for the case + * @param check what interval checking to apply + * */ +function makeCase( + argumentKind: FPKind, + parameterKind: FPKind, + i: readonly number[], + s: readonly number[], + r: number, + check: IntervalFilter +): Case | undefined { + const fp = FP[argumentKind]; + i = i.map(fp.quantize); + s = s.map(fp.quantize); + r = fp.quantize(r); + + const vectors = FP[parameterKind].refractInterval(i, s, r); + if (check === 'finite' && vectors.some(e => !e.isFinite())) { + return undefined; + } + + return { + input: [toVector(i, fp.scalarBuilder), toVector(s, fp.scalarBuilder), fp.scalarBuilder(r)], + expected: vectors, + }; +} + +/** + * @returns an array of Cases for `refract` + * @param argumentKind what kind of floating point numbers being operated on + * @param parameterKind what kind of floating point operation should be performed, + * should be the same as argumentKind, except for abstract + * @param param_is array of inputs to try for the `i` param + * @param param_ss array of inputs to try for the `s` param + * @param param_rs array of inputs to try for the `r` param + * @param check what interval checking to apply + */ +function generateCases( + argumentKind: FPKind, + parameterKind: FPKind, + param_is: ROArrayArray<number>, + param_ss: ROArrayArray<number>, + param_rs: readonly number[], + check: IntervalFilter +): Case[] { + // Cannot use `cartesianProduct` here due to heterogeneous param types + return param_is + .flatMap(i => { + return param_ss.flatMap(s => { + return param_rs.map(r => { + return makeCase(argumentKind, parameterKind, i, s, r, check); + }); + }); + }) + .filter((c): c is Case => c !== undefined); +} + +// Cases: [f32|f16|abstract]_vecN_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(dim => + ([true, false] as const).map(nonConst => ({ + [`${trait}_vec${dim}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + if (trait !== 'abstract') { + return generateCases( + trait, + trait, + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + FP[trait].sparseScalarRange(), + nonConst ? 'unfiltered' : 'finite' + ); + } else { + // Restricting the number of cases, because a vector of abstract floats needs to be returned, which is costly. + return selectNCases( + 'faceForward', + 20, + generateCases( + trait, + // refract has an inherited accuracy, so is only expected to be as accurate as f32 + 'f32', + FP[trait].sparseVectorRange(dim), + FP[trait].sparseVectorRange(dim), + FP[trait].sparseScalarRange(), + nonConst ? 'unfiltered' : 'finite' + ) + ); + } + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('refract', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts index be1a76b437..5b51f30eee 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts @@ -2,7 +2,7 @@ export const description = ` Execution tests for the 'refract' builtin function T is vecN<I> -I is AbstractFloat, f32, or f16 +I is abstract-float, f32, or f16 @const fn refract(e1: T ,e2: T ,e3: I ) -> T For the incident vector e1 and surface normal e2, and the ratio of indices of refraction e3, let k = 1.0 -e3*e3* (1.0 - dot(e2,e1) * dot(e2,e1)). @@ -11,129 +11,62 @@ vector e3*e1- (e3* dot(e2,e1) + sqrt(k)) *e2. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; -import { ROArrayArray } from '../../../../../../common/util/types.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { toVector, TypeF32, TypeF16, TypeVec } from '../../../../../util/conversion.js'; -import { FP, FPKind } from '../../../../../util/floating_point.js'; -import { - sparseVectorF32Range, - sparseVectorF16Range, - sparseF32Range, - sparseF16Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, IntervalFilter, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './refract.cache.js'; export const g = makeTestGroup(GPUTest); -// Using a bespoke implementation of make*Case and generate*Cases here -// since refract is the only builtin with the API signature -// (vec, vec, scalar) -> vec - -/** - * @returns a Case for `refract` - * @param kind what type of floating point numbers to operate on - * @param i the `i` param for the case - * @param s the `s` param for the case - * @param r the `r` param for the case - * @param check what interval checking to apply - * */ -function makeCase( - kind: FPKind, - i: readonly number[], - s: readonly number[], - r: number, - check: IntervalFilter -): Case | undefined { - const fp = FP[kind]; - i = i.map(fp.quantize); - s = s.map(fp.quantize); - r = fp.quantize(r); - - const vectors = fp.refractInterval(i, s, r); - if (check === 'finite' && vectors.some(e => !e.isFinite())) { - return undefined; - } - - return { - input: [toVector(i, fp.scalarBuilder), toVector(s, fp.scalarBuilder), fp.scalarBuilder(r)], - expected: fp.refractInterval(i, s, r), - }; -} - -/** - * @returns an array of Cases for `refract` - * @param kind what type of floating point numbers to operate on - * @param param_is array of inputs to try for the `i` param - * @param param_ss array of inputs to try for the `s` param - * @param param_rs array of inputs to try for the `r` param - * @param check what interval checking to apply - */ -function generateCases( - kind: FPKind, - param_is: ROArrayArray<number>, - param_ss: ROArrayArray<number>, - param_rs: readonly number[], - check: IntervalFilter -): Case[] { - // Cannot use `cartesianProduct` here due to heterogeneous param types - return param_is - .flatMap(i => { - return param_ss.flatMap(s => { - return param_rs.map(r => { - return makeCase(kind, i, s, r, check); - }); - }); - }) - .filter((c): c is Case => c !== undefined); -} - -// Cases: f32_vecN_[non_]const -const f32_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f32_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return generateCases( - 'f32', - sparseVectorF32Range(n), - sparseVectorF32Range(n), - sparseF32Range(), - nonConst ? 'unfiltered' : 'finite' - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_vecN_[non_]const -const f16_vec_cases = ([2, 3, 4] as const) - .flatMap(n => - ([true, false] as const).map(nonConst => ({ - [`f16_vec${n}_${nonConst ? 'non_const' : 'const'}`]: () => { - return generateCases( - 'f16', - sparseVectorF16Range(n), - sparseVectorF16Range(n), - sparseF16Range(), - nonConst ? 'unfiltered' : 'finite' - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); +g.test('abstract_float_vec2') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec2s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec2_const'); + await run( + t, + abstractFloatBuiltin('refract'), + [Type.vec2af, Type.vec2af, Type.abstractFloat], + Type.vec2af, + t.params, + cases + ); + }); -export const d = makeCaseCache('refract', { - ...f32_vec_cases, - ...f16_vec_cases, -}); +g.test('abstract_float_vec3') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec3s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec3_const'); + await run( + t, + abstractFloatBuiltin('refract'), + [Type.vec3af, Type.vec3af, Type.abstractFloat], + Type.vec3af, + t.params, + cases + ); + }); -g.test('abstract_float') - .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') - .desc(`abstract float tests`) - .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const)) - .unimplemented(); +g.test('abstract_float_vec4') + .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') + .desc(`abstract float tests using vec4s`) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract_vec4_const'); + await run( + t, + abstractFloatBuiltin('refract'), + [Type.vec4af, Type.vec4af, Type.abstractFloat], + Type.vec4af, + t.params, + cases + ); + }); g.test('f32_vec2') .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions') @@ -146,8 +79,8 @@ g.test('f32_vec2') await run( t, builtin('refract'), - [TypeVec(2, TypeF32), TypeVec(2, TypeF32), TypeF32], - TypeVec(2, TypeF32), + [Type.vec2f, Type.vec2f, Type.f32], + Type.vec2f, t.params, cases ); @@ -164,8 +97,8 @@ g.test('f32_vec3') await run( t, builtin('refract'), - [TypeVec(3, TypeF32), TypeVec(3, TypeF32), TypeF32], - TypeVec(3, TypeF32), + [Type.vec3f, Type.vec3f, Type.f32], + Type.vec3f, t.params, cases ); @@ -182,8 +115,8 @@ g.test('f32_vec4') await run( t, builtin('refract'), - [TypeVec(4, TypeF32), TypeVec(4, TypeF32), TypeF32], - TypeVec(4, TypeF32), + [Type.vec4f, Type.vec4f, Type.f32], + Type.vec4f, t.params, cases ); @@ -203,8 +136,8 @@ g.test('f16_vec2') await run( t, builtin('refract'), - [TypeVec(2, TypeF16), TypeVec(2, TypeF16), TypeF16], - TypeVec(2, TypeF16), + [Type.vec2h, Type.vec2h, Type.f16], + Type.vec2h, t.params, cases ); @@ -224,8 +157,8 @@ g.test('f16_vec3') await run( t, builtin('refract'), - [TypeVec(3, TypeF16), TypeVec(3, TypeF16), TypeF16], - TypeVec(3, TypeF16), + [Type.vec3h, Type.vec3h, Type.f16], + Type.vec3h, t.params, cases ); @@ -245,8 +178,8 @@ g.test('f16_vec4') await run( t, builtin('refract'), - [TypeVec(4, TypeF16), TypeVec(4, TypeF16), TypeF16], - TypeVec(4, TypeF16), + [Type.vec4h, Type.vec4h, Type.f16], + Type.vec4h, t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts index 6acb359822..e235e62a52 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts @@ -10,7 +10,7 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeU32, u32Bits, TypeI32, i32Bits } from '../../../../../util/conversion.js'; +import { u32Bits, i32Bits, Type } from '../../../../../util/conversion.js'; import { allInputSources, Config, run } from '../../expression.js'; import { builtin } from './builtin.js'; @@ -26,7 +26,7 @@ g.test('u32') .fn(async t => { const cfg: Config = t.params; // prettier-ignore - await run(t, builtin('reverseBits'), [TypeU32], TypeU32, cfg, [ + await run(t, builtin('reverseBits'), [Type.u32], Type.u32, cfg, [ // Zero { input: u32Bits(0b00000000000000000000000000000000), expected: u32Bits(0b00000000000000000000000000000000) }, @@ -142,7 +142,7 @@ g.test('i32') .fn(async t => { const cfg: Config = t.params; // prettier-ignore - await run(t, builtin('reverseBits'), [TypeI32], TypeI32, cfg, [ + await run(t, builtin('reverseBits'), [Type.i32], Type.i32, cfg, [ // Zero { input: i32Bits(0b00000000000000000000000000000000), expected: i32Bits(0b00000000000000000000000000000000) }, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.cache.ts new file mode 100644 index 0000000000..e5383b2075 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.cache.ts @@ -0,0 +1,24 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// See https://github.com/gpuweb/cts/issues/2766 for details +const kIssue2766Value = { + abstract: 0x8000_0000_0000_0000, + f32: 0x8000_0000, + f16: 0x8000, +}; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [kIssue2766Value[trait], ...FP[trait].scalarRange()], + 'unfiltered', + FP[trait].roundInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('round', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts index bd40ed4b2a..eeaf41b381 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'round' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn round(e: T) -> T Result is the integer k nearest to e, as a floating point value. @@ -12,46 +12,33 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './round.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('round', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - 0x80000000, // https://github.com/gpuweb/cts/issues/2766, - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.roundInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - 0x8000, // https://github.com/gpuweb/cts/issues/2766 - ...fullF16Range(), - ], - 'unfiltered', - FP.f16.roundInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('round'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -61,7 +48,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('round'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('round'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -75,5 +62,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('round'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('round'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.cache.ts new file mode 100644 index 0000000000..4a4ffeee30 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.cache.ts @@ -0,0 +1,18 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [...linearRange(0.0, 1.0, 20), ...FP[trait].scalarRange()], + 'unfiltered', + FP[trait].saturateInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('saturate', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts index 2f16502921..79c61e4eec 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'saturate' builtin function -S is AbstractFloat, f32, or f16 +S is abstract-float, f32, or f16 T is S or vecN<S> @const fn saturate(e: T) -> T Returns clamp(e, 0.0, 1.0). Component-wise when T is a vector. @@ -9,52 +9,14 @@ Returns clamp(e, 0.0, 1.0). Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeF16, TypeF32 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF16Range, fullF32Range, fullF64Range, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './saturate.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('saturate', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - // Non-clamped values - ...linearRange(0.0, 1.0, 20), - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.saturateInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - // Non-clamped values - ...linearRange(0.0, 1.0, 20), - ...fullF16Range(), - ], - 'unfiltered', - FP.f16.saturateInterval - ); - }, - abstract: () => { - return FP.abstract.generateScalarToIntervalCases( - [ - // Non-clamped values - ...linearRange(0.0, 1.0, 20), - ...fullF64Range(), - ], - 'unfiltered', - FP.abstract.saturateInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) @@ -67,9 +29,9 @@ g.test('abstract_float') const cases = await d.get('abstract'); await run( t, - abstractBuiltin('saturate'), - [TypeAbstractFloat], - TypeAbstractFloat, + abstractFloatBuiltin('saturate'), + [Type.abstractFloat], + Type.abstractFloat, t.params, cases ); @@ -82,7 +44,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('saturate'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('saturate'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -96,5 +58,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('saturate'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('saturate'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts index c64f989f42..63accbc2d4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts @@ -14,12 +14,6 @@ import { makeTestGroup } from '../../../../../../common/framework/test_group.js' import { GPUTest } from '../../../../../gpu_test.js'; import { VectorType, - TypeVec, - TypeBool, - TypeF32, - TypeF16, - TypeI32, - TypeU32, f32, f16, i32, @@ -31,11 +25,14 @@ import { vec3, vec4, abstractFloat, - TypeAbstractFloat, + abstractInt, + ScalarValue, + Type, } from '../../../../../util/conversion.js'; -import { run, CaseList, allInputSources } from '../../expression.js'; +import { Case } from '../../case.js'; +import { run, allInputSources } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, abstractIntBuiltin, builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); @@ -43,32 +40,47 @@ function makeBool(n: number) { return bool((n & 1) === 1); } -type scalarKind = 'b' | 'af' | 'f' | 'h' | 'i' | 'u'; +type scalarKind = 'b' | 'af' | 'f' | 'h' | 'ai' | 'i' | 'u'; const dataType = { b: { - type: TypeBool, - constructor: makeBool, + type: Type.bool, + scalar_builder: makeBool, + shader_builder: builtin('select'), }, af: { - type: TypeAbstractFloat, - constructor: abstractFloat, + type: Type.abstractFloat, + scalar_builder: abstractFloat, + shader_builder: abstractFloatBuiltin('select'), }, f: { - type: TypeF32, - constructor: f32, + type: Type.f32, + scalar_builder: f32, + shader_builder: builtin('select'), }, h: { - type: TypeF16, - constructor: f16, + type: Type.f16, + scalar_builder: f16, + shader_builder: builtin('select'), + }, + ai: { + type: Type.abstractInt, + // Only ints are used in the tests below, so the conversion to bigint will + // be safe. If a non-int is passed in this will Error. + scalar_builder: (v: number): ScalarValue => { + return abstractInt(BigInt(v)); + }, + shader_builder: abstractIntBuiltin('select'), }, i: { - type: TypeI32, - constructor: i32, + type: Type.i32, + scalar_builder: i32, + shader_builder: builtin('select'), }, u: { - type: TypeU32, - constructor: u32, + type: Type.u32, + scalar_builder: u32, + shader_builder: builtin('select'), }, }; @@ -78,7 +90,7 @@ g.test('scalar') .params(u => u .combine('inputSource', allInputSources) - .combine('component', ['b', 'af', 'f', 'h', 'i', 'u'] as const) + .combine('component', ['b', 'af', 'f', 'h', 'ai', 'i', 'u'] as const) .combine('overload', ['scalar', 'vec2', 'vec3', 'vec4'] as const) ) .beforeAllSubcases(t => { @@ -86,10 +98,11 @@ g.test('scalar') t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); } t.skipIf(t.params.component === 'af' && t.params.inputSource !== 'const'); + t.skipIf(t.params.component === 'ai' && t.params.inputSource !== 'const'); }) .fn(async t => { - const componentType = dataType[t.params.component as scalarKind].type; - const cons = dataType[t.params.component as scalarKind].constructor; + const componentType = dataType[t.params.component].type; + const scalar_builder = dataType[t.params.component].scalar_builder; // Create the scalar values that will be selected from, either as scalars // or vectors. @@ -97,39 +110,40 @@ g.test('scalar') // Each boolean will select between c[k] and c[k+4]. Those values must // always compare as different. The tricky case is boolean, where the parity // has to be different, i.e. c[k]-c[k+4] must be odd. - const c = [0, 1, 2, 3, 5, 6, 7, 8].map(i => cons(i)); + const scalars = [0, 1, 2, 3, 5, 6, 7, 8].map(i => scalar_builder(i)); + // Now form vectors that will have different components from each other. - const v2a = vec2(c[0], c[1]); - const v2b = vec2(c[4], c[5]); - const v3a = vec3(c[0], c[1], c[2]); - const v3b = vec3(c[4], c[5], c[6]); - const v4a = vec4(c[0], c[1], c[2], c[3]); - const v4b = vec4(c[4], c[5], c[6], c[7]); + const v2a = vec2(scalars[0], scalars[1]); + const v2b = vec2(scalars[4], scalars[5]); + const v3a = vec3(scalars[0], scalars[1], scalars[2]); + const v3b = vec3(scalars[4], scalars[5], scalars[6]); + const v4a = vec4(scalars[0], scalars[1], scalars[2], scalars[3]); + const v4b = vec4(scalars[4], scalars[5], scalars[6], scalars[7]); const overloads = { scalar: { type: componentType, cases: [ - { input: [c[0], c[1], False], expected: c[0] }, - { input: [c[0], c[1], True], expected: c[1] }, + { input: [scalars[0], scalars[1], False], expected: scalars[0] }, + { input: [scalars[0], scalars[1], True], expected: scalars[1] }, ], }, vec2: { - type: TypeVec(2, componentType), + type: Type.vec(2, componentType), cases: [ { input: [v2a, v2b, False], expected: v2a }, { input: [v2a, v2b, True], expected: v2b }, ], }, vec3: { - type: TypeVec(3, componentType), + type: Type.vec(3, componentType), cases: [ { input: [v3a, v3b, False], expected: v3a }, { input: [v3a, v3b, True], expected: v3b }, ], }, vec4: { - type: TypeVec(4, componentType), + type: Type.vec(4, componentType), cases: [ { input: [v4a, v4b, False], expected: v4a }, { input: [v4a, v4b, True], expected: v4b }, @@ -140,8 +154,8 @@ g.test('scalar') await run( t, - t.params.component === 'af' ? abstractBuiltin('select') : builtin('select'), - [overload.type, overload.type, TypeBool], + dataType[t.params.component as scalarKind].shader_builder, + [overload.type, overload.type, Type.bool], overload.type, t.params, overload.cases @@ -154,7 +168,7 @@ g.test('vector') .params(u => u .combine('inputSource', allInputSources) - .combine('component', ['b', 'af', 'f', 'h', 'i', 'u'] as const) + .combine('component', ['b', 'af', 'f', 'h', 'ai', 'i', 'u'] as const) .combine('overload', ['vec2', 'vec3', 'vec4'] as const) ) .beforeAllSubcases(t => { @@ -162,29 +176,30 @@ g.test('vector') t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); } t.skipIf(t.params.component === 'af' && t.params.inputSource !== 'const'); + t.skipIf(t.params.component === 'ai' && t.params.inputSource !== 'const'); }) .fn(async t => { - const componentType = dataType[t.params.component as scalarKind].type; - const cons = dataType[t.params.component as scalarKind].constructor; + const componentType = dataType[t.params.component].type; + const scalar_builder = dataType[t.params.component].scalar_builder; // Create the scalar values that will be selected from. // // Each boolean will select between c[k] and c[k+4]. Those values must // always compare as different. The tricky case is boolean, where the parity // has to be different, i.e. c[k]-c[k+4] must be odd. - const c = [0, 1, 2, 3, 5, 6, 7, 8].map(i => cons(i)); + const scalars = [0, 1, 2, 3, 5, 6, 7, 8].map(i => scalar_builder(i)); const T = True; const F = False; - let tests: { dataType: VectorType; boolType: VectorType; cases: CaseList }; + let tests: { dataType: VectorType; boolType: VectorType; cases: Case[] }; switch (t.params.overload) { case 'vec2': { - const a = vec2(c[0], c[1]); - const b = vec2(c[4], c[5]); + const a = vec2(scalars[0], scalars[1]); + const b = vec2(scalars[4], scalars[5]); tests = { - dataType: TypeVec(2, componentType), - boolType: TypeVec(2, TypeBool), + dataType: Type.vec(2, componentType), + boolType: Type.vec(2, Type.bool), cases: [ { input: [a, b, vec2(F, F)], expected: vec2(a.x, a.y) }, { input: [a, b, vec2(F, T)], expected: vec2(a.x, b.y) }, @@ -195,11 +210,11 @@ g.test('vector') break; } case 'vec3': { - const a = vec3(c[0], c[1], c[2]); - const b = vec3(c[4], c[5], c[6]); + const a = vec3(scalars[0], scalars[1], scalars[2]); + const b = vec3(scalars[4], scalars[5], scalars[6]); tests = { - dataType: TypeVec(3, componentType), - boolType: TypeVec(3, TypeBool), + dataType: Type.vec(3, componentType), + boolType: Type.vec(3, Type.bool), cases: [ { input: [a, b, vec3(F, F, F)], expected: vec3(a.x, a.y, a.z) }, { input: [a, b, vec3(F, F, T)], expected: vec3(a.x, a.y, b.z) }, @@ -214,11 +229,11 @@ g.test('vector') break; } case 'vec4': { - const a = vec4(c[0], c[1], c[2], c[3]); - const b = vec4(c[4], c[5], c[6], c[7]); + const a = vec4(scalars[0], scalars[1], scalars[2], scalars[3]); + const b = vec4(scalars[4], scalars[5], scalars[6], scalars[7]); tests = { - dataType: TypeVec(4, componentType), - boolType: TypeVec(4, TypeBool), + dataType: Type.vec(4, componentType), + boolType: Type.vec(4, Type.bool), cases: [ { input: [a, b, vec4(F, F, F, F)], expected: vec4(a.x, a.y, a.z, a.w) }, { input: [a, b, vec4(F, F, F, T)], expected: vec4(a.x, a.y, a.z, b.w) }, @@ -244,7 +259,7 @@ g.test('vector') await run( t, - t.params.component === 'af' ? abstractBuiltin('select') : builtin('select'), + dataType[t.params.component].shader_builder, [tests.dataType, tests.dataType, tests.boolType], tests.dataType, t.params, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.cache.ts new file mode 100644 index 0000000000..09f4de19ac --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.cache.ts @@ -0,0 +1,31 @@ +import { abstractInt, i32 } from '../../../../../util/conversion.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { fullI32Range, fullI64Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const fp_cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait === 'abstract' ? 'abstract_float' : trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + 'unfiltered', + FP[trait].signInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('sign', { + ...fp_cases, + i32: () => + fullI32Range().map(i => { + const signFunc = (i: number): number => (i < 0 ? -1 : i > 0 ? 1 : 0); + return { input: [i32(i)], expected: i32(signFunc(i)) }; + }), + abstract_int: () => + fullI64Range().map(i => { + const signFunc = (i: bigint): bigint => (i < 0n ? -1n : i > 0n ? 1n : 0n); + return { input: [abstractInt(i)], expected: abstractInt(signFunc(i)) }; + }), +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts index a147acf6fb..f7d2e3ccfd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'sign' builtin function -S is AbstractFloat, AbstractInt, i32, f32, f16 +S is abstract-float, Type.abstractInt, i32, f32, f16 T is S or vecN<S> @const fn sign(e: T ) -> T Returns the sign of e. Component-wise when T is a vector. @@ -9,48 +9,14 @@ Returns the sign of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { - i32, - TypeF32, - TypeF16, - TypeI32, - TypeAbstractFloat, -} from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { - fullF32Range, - fullF16Range, - fullI32Range, - fullF64Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, abstractIntBuiltin, builtin } from './builtin.js'; +import { d } from './sign.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('sign', { - f32: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.signInterval); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'unfiltered', FP.f16.signInterval); - }, - abstract_float: () => { - return FP.abstract.generateScalarToIntervalCases( - fullF64Range(), - 'unfiltered', - FP.abstract.signInterval - ); - }, - i32: () => - fullI32Range().map(i => { - const signFunc = (i: number): number => (i < 0 ? -1 : i > 0 ? 1 : 0); - return { input: [i32(i)], expected: i32(signFunc(i)) }; - }), -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#sign-builtin') .desc(`abstract float tests`) @@ -61,16 +27,28 @@ g.test('abstract_float') ) .fn(async t => { const cases = await d.get('abstract_float'); - await run(t, abstractBuiltin('sign'), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases); + await run( + t, + abstractFloatBuiltin('sign'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); }); g.test('abstract_int') .specURL('https://www.w3.org/TR/WGSL/#sign-builtin') .desc(`abstract int tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_int'); + await run(t, abstractIntBuiltin('sign'), [Type.abstractInt], Type.abstractInt, t.params, cases); + }); g.test('i32') .specURL('https://www.w3.org/TR/WGSL/#sign-builtin') @@ -80,7 +58,7 @@ g.test('i32') ) .fn(async t => { const cases = await d.get('i32'); - await run(t, builtin('sign'), [TypeI32], TypeI32, t.params, cases); + await run(t, builtin('sign'), [Type.i32], Type.i32, t.params, cases); }); g.test('f32') @@ -91,7 +69,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('sign'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('sign'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -105,5 +83,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('sign'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('sign'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.cache.ts new file mode 100644 index 0000000000..74acf6a62c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [ + // Well-defined accuracy range + ...linearRange(-Math.PI, Math.PI, 100), + ...FP[trait].scalarRange(), + ], + trait === 'abstract' ? 'finite' : 'unfiltered', + // sin has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].sinInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('sin', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts index 4ab3ae7a3d..fc706c5d63 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'sin' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn sin(e: T ) -> T Returns the sine of e. Component-wise when T is a vector. @@ -9,48 +9,33 @@ Returns the sine of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './sin.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('sin', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - // Well-defined accuracy range - ...linearRange(-Math.PI, Math.PI, 1000), - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.sinInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - // Well-defined accuracy range - ...linearRange(-Math.PI, Math.PI, 1000), - ...fullF16Range(), - ], - 'unfiltered', - FP.f16.sinInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('sin'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -66,7 +51,7 @@ TODO(#792): Decide what the ground-truth is for these tests. [1] ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('sin'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('sin'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -80,5 +65,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('sin'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('sin'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.cache.ts new file mode 100644 index 0000000000..aba275581d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + nonConst ? 'unfiltered' : 'finite', + // sinh has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].sinhInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('sinh', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts index d9b93a3dc8..5ffe628de3 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'sinh' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn sinh(e: T ) -> T Returns the hyperbolic sine of e. Component-wise when T is a vector. @@ -9,38 +9,33 @@ Returns the hyperbolic sine of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './sinh.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('sinh', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'finite', FP.f32.sinhInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.sinhInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'finite', FP.f16.sinhInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'unfiltered', FP.f16.sinhInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('sinh'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -50,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('sinh'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('sinh'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -64,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('sinh'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('sinh'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.cache.ts new file mode 100644 index 0000000000..4ba7615b43 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.cache.ts @@ -0,0 +1,25 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarTripleToIntervalCases( + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + FP[trait].sparseScalarRange(), + nonConst ? 'unfiltered' : 'finite', + // smoothstep has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].smoothStepInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('smoothstep', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts index 20d2a4edbc..42d8d09ff5 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'smoothstep' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn smoothstep(low: T , high: T , x: T ) -> T Returns the smooth Hermite interpolation between 0 and 1. @@ -11,62 +11,33 @@ For scalar T, the result is t * t * (3.0 - 2.0 * t), where t = clamp((x - low) / import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { sparseF32Range, sparseF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './smoothstep.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('smoothstep', { - f32_const: () => { - return FP.f32.generateScalarTripleToIntervalCases( - sparseF32Range(), - sparseF32Range(), - sparseF32Range(), - 'finite', - FP.f32.smoothStepInterval - ); - }, - f32_non_const: () => { - return FP.f32.generateScalarTripleToIntervalCases( - sparseF32Range(), - sparseF32Range(), - sparseF32Range(), - 'unfiltered', - FP.f32.smoothStepInterval - ); - }, - f16_const: () => { - return FP.f16.generateScalarTripleToIntervalCases( - sparseF16Range(), - sparseF16Range(), - sparseF16Range(), - 'finite', - FP.f16.smoothStepInterval - ); - }, - f16_non_const: () => { - return FP.f16.generateScalarTripleToIntervalCases( - sparseF16Range(), - sparseF16Range(), - sparseF16Range(), - 'unfiltered', - FP.f16.smoothStepInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('smoothstep'), + [Type.abstractFloat, Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -76,7 +47,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('smoothstep'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('smoothstep'), [Type.f32, Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -90,5 +61,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('smoothstep'), [TypeF16, TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('smoothstep'), [Type.f16, Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.cache.ts new file mode 100644 index 0000000000..2151b2730a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16]_[non_]const +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([true, false] as const).map(nonConst => ({ + [`${trait}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + nonConst ? 'unfiltered' : 'finite', + // sqrt has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].sqrtInterval + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('sqrt', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts index a092438043..3d6c4390e2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'sqrt' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn sqrt(e: T ) -> T Returns the square root of e. Component-wise when T is a vector. @@ -9,38 +9,33 @@ Returns the square root of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './sqrt.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('sqrt', { - f32_const: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'finite', FP.f32.sqrtInterval); - }, - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.sqrtInterval); - }, - f16_const: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'finite', FP.f16.sqrtInterval); - }, - f16_non_const: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'unfiltered', FP.f16.sqrtInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract_const'); + await run( + t, + abstractFloatBuiltin('sqrt'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -50,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, builtin('sqrt'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('sqrt'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -64,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f16_const' : 'f16_non_const'); - await run(t, builtin('sqrt'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('sqrt'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.cache.ts new file mode 100644 index 0000000000..28d1e1952b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.cache.ts @@ -0,0 +1,41 @@ +import { anyOf } from '../../../../../util/compare.js'; +import { FP } from '../../../../../util/floating_point.js'; +import { Case } from '../../case.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// stepInterval's return value can't always be interpreted as a single acceptance +// interval, valid result may be 0.0 or 1.0 or both of them, but will never be a +// value in interval (0.0, 1.0). +// See the comment block on stepInterval for more details +const makeCase = (trait: 'f32' | 'f16' | 'abstract', edge: number, x: number): Case => { + const FPTrait = FP[trait]; + edge = FPTrait.quantize(edge); + x = FPTrait.quantize(x); + const expected = FPTrait.stepInterval(edge, x); + + // [0, 0], [1, 1], or [-∞, +∞] cases + if (expected.isPoint() || !expected.isFinite()) { + return { input: [FPTrait.scalarBuilder(edge), FPTrait.scalarBuilder(x)], expected }; + } + + // [0, 1] case, valid result is either 0.0 or 1.0. + const zeroInterval = FPTrait.toInterval(0); + const oneInterval = FPTrait.toInterval(1); + return { + input: [FPTrait.scalarBuilder(edge), FPTrait.scalarBuilder(x)], + expected: anyOf(zeroInterval, oneInterval), + }; +}; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait] + .sparseScalarRange() + .flatMap(edge => FP[trait].sparseScalarRange().map(x => makeCase(trait, edge, x))); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('step', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts index 752e2676e6..fd76ee16c1 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'step' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn step(edge: T ,x: T ) -> T Returns 1.0 if edge ≤ x, and 0.0 otherwise. Component-wise when T is a vector. @@ -9,57 +9,33 @@ Returns 1.0 if edge ≤ x, and 0.0 otherwise. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { anyOf } from '../../../../../util/compare.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, Case, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './step.cache.js'; export const g = makeTestGroup(GPUTest); -// stepInterval's return value can't always be interpreted as a single acceptance -// interval, valid result may be 0.0 or 1.0 or both of them, but will never be a -// value in interval (0.0, 1.0). -// See the comment block on stepInterval for more details -const makeCase = (trait: 'f32' | 'f16', edge: number, x: number): Case => { - const FPTrait = FP[trait]; - edge = FPTrait.quantize(edge); - x = FPTrait.quantize(x); - const expected = FPTrait.stepInterval(edge, x); - - // [0, 0], [1, 1], or [-∞, +∞] cases - if (expected.isPoint() || !expected.isFinite()) { - return { input: [FPTrait.scalarBuilder(edge), FPTrait.scalarBuilder(x)], expected }; - } - - // [0, 1] case, valid result is either 0.0 or 1.0. - const zeroInterval = FPTrait.toInterval(0); - const oneInterval = FPTrait.toInterval(1); - return { - input: [FPTrait.scalarBuilder(edge), FPTrait.scalarBuilder(x)], - expected: anyOf(zeroInterval, oneInterval), - }; -}; - -export const d = makeCaseCache('step', { - f32: () => { - return fullF32Range().flatMap(edge => fullF32Range().map(x => makeCase('f32', edge, x))); - }, - f16: () => { - return fullF16Range().flatMap(edge => fullF16Range().map(x => makeCase('f16', edge, x))); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('step'), + [Type.abstractFloat, Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -69,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('step'), [TypeF32, TypeF32], TypeF32, t.params, cases); + await run(t, builtin('step'), [Type.f32, Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -83,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('step'), [TypeF16, TypeF16], TypeF16, t.params, cases); + await run(t, builtin('step'), [Type.f16, Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.cache.ts new file mode 100644 index 0000000000..8d8e0d980b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.cache.ts @@ -0,0 +1,23 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { linearRange } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + [ + // Well-defined accuracy range + ...linearRange(-Math.PI, Math.PI, 100), + ...FP[trait].scalarRange(), + ], + 'unfiltered', + // tan has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].tanInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('tan', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts index be3bdee046..7b682d0968 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'tan' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn tan(e: T ) -> T Returns the tangent of e. Component-wise when T is a vector. @@ -9,48 +9,33 @@ Returns the tangent of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range, linearRange } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './tan.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('tan', { - f32: () => { - return FP.f32.generateScalarToIntervalCases( - [ - // Defined accuracy range - ...linearRange(-Math.PI, Math.PI, 100), - ...fullF32Range(), - ], - 'unfiltered', - FP.f32.tanInterval - ); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases( - [ - // Defined accuracy range - ...linearRange(-Math.PI, Math.PI, 100), - ...fullF16Range(), - ], - 'unfiltered', - FP.f16.tanInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('tan'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -60,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('tan'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('tan'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -74,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('tan'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('tan'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.cache.ts new file mode 100644 index 0000000000..5bada7fef5 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.cache.ts @@ -0,0 +1,18 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + 'unfiltered', + // tanh has an inherited accuracy, so is only expected to be as accurate as f32 + FP[trait !== 'abstract' ? trait : 'f32'].tanhInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('tanh', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts index 3aca5b924b..17978926a3 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'tanh' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn tanh(e: T ) -> T Returns the hyperbolic tangent of e. Component-wise when T is a vector. @@ -9,32 +9,33 @@ Returns the hyperbolic tangent of e. Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeF16 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF16Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; -import { allInputSources, run } from '../../expression.js'; +import { Type } from '../../../../../util/conversion.js'; +import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './tanh.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('tanh', { - f32: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.tanhInterval); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases(fullF16Range(), 'unfiltered', FP.f16.tanhInterval); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const cases = await d.get('abstract'); + await run( + t, + abstractFloatBuiltin('tanh'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); + }); g.test('f32') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') @@ -44,7 +45,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('tanh'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('tanh'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -58,5 +59,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('tanh'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('tanh'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimension.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimension.spec.ts deleted file mode 100644 index 0ecb9964cf..0000000000 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimension.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -export const description = ` -Execution tests for the 'textureDimension' builtin function - -The dimensions of the texture in texels. -For textures based on cubes, the results are the dimensions of each face of the cube. -Cube faces are square, so the x and y components of the result are equal. -If level is outside the range [0, textureNumLevels(t)) then any valid value for the return type may be returned. -`; - -import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; -import { GPUTest } from '../../../../../gpu_test.js'; - -export const g = makeTestGroup(GPUTest); - -g.test('sampled') - .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') - .desc( - ` -T: f32, i32, u32 - -fn textureDimensions(t: texture_1d<T>) -> u32 -fn textureDimensions(t: texture_1d<T>, level: u32) -> u32 -fn textureDimensions(t: texture_2d<T>) -> vec2<u32> -fn textureDimensions(t: texture_2d<T>, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_2d_array<T>) -> vec2<u32> -fn textureDimensions(t: texture_2d_array<T>, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_3d<T>) -> vec3<u32> -fn textureDimensions(t: texture_3d<T>, level: u32) -> vec3<u32> -fn textureDimensions(t: texture_cube<T>) -> vec2<u32> -fn textureDimensions(t: texture_cube<T>, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_cube_array<T>) -> vec2<u32> -fn textureDimensions(t: texture_cube_array<T>, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_multisampled_2d<T>)-> vec2<u32> - -Parameters: - * t: the sampled texture - * level: - - The mip level, with level 0 containing a full size version of the texture. - - If omitted, the dimensions of level 0 are returned. -` - ) - .params(u => - u - .combine('texture_type', [ - 'texture_1d', - 'texture_2d', - 'texture_2d_array', - 'texture_3d', - 'texture_cube', - 'texture_cube_array', - 'texture_multisampled_2d', - ] as const) - .beginSubcases() - .combine('sampled_type', ['f32-only', 'i32', 'u32'] as const) - .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const) - ) - .unimplemented(); - -g.test('depth') - .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') - .desc( - ` -fn textureDimensions(t: texture_depth_2d) -> vec2<u32> -fn textureDimensions(t: texture_depth_2d, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_depth_2d_array) -> vec2<u32> -fn textureDimensions(t: texture_depth_2d_array, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_depth_cube) -> vec2<u32> -fn textureDimensions(t: texture_depth_cube, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_depth_cube_array) -> vec2<u32> -fn textureDimensions(t: texture_depth_cube_array, level: u32) -> vec2<u32> -fn textureDimensions(t: texture_depth_multisampled_2d)-> vec2<u32> - -Parameters: - * t: the depth or multisampled texture - * level: - - The mip level, with level 0 containing a full size version of the texture. - - If omitted, the dimensions of level 0 are returned. -` - ) - .params(u => - u - .combine('texture_type', [ - 'texture_depth_2d', - 'texture_depth_2d_array', - 'texture_depth_cube', - 'texture_depth_cube_array', - 'texture_depth_multisampled_2d', - ]) - .beginSubcases() - .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const) - ) - .unimplemented(); - -g.test('storage') - .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') - .desc( - ` -F: rgba8unorm - rgba8snorm - rgba8uint - rgba8sint - rgba16uint - rgba16sint - rgba16float - r32uint - r32sint - r32float - rg32uint - rg32sint - rg32float - rgba32uint - rgba32sint - rgba32float -A: read, write, read_write - -fn textureDimensions(t: texture_storage_1d<F,A>) -> u32 -fn textureDimensions(t: texture_storage_2d<F,A>) -> vec2<u32> -fn textureDimensions(t: texture_storage_2d_array<F,A>) -> vec2<u32> -fn textureDimensions(t: texture_storage_3d<F,A>) -> vec3<u32> - -Parameters: - * t: the storage texture -` - ) - .params(u => - u - .combine('texel_format', [ - 'rgba8unorm', - 'rgba8snorm', - 'rgba8uint', - 'rgba8sint', - 'rgba16uint', - 'rgba16sint', - 'rgba16float', - 'r32uint', - 'r32sint', - 'r32float', - 'rg32uint', - 'rg32sint', - 'rg32float', - 'rgba32uint', - 'rgba32sint', - 'rgba32float', - ] as const) - .beginSubcases() - .combine('access_mode', ['read', 'write', 'read_write'] as const) - ) - .unimplemented(); - -g.test('external') - .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') - .desc( - ` -fn textureDimensions(t: texture_external) -> vec2<u32> - -Parameters: - * t: the external texture -` - ) - .unimplemented(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimensions.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimensions.spec.ts new file mode 100644 index 0000000000..1e025ca618 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimensions.spec.ts @@ -0,0 +1,518 @@ +export const description = ` +Execution tests for the 'textureDimensions' builtin function + +The dimensions of the texture in texels. +For textures based on cubes, the results are the dimensions of each face of the cube. +Cube faces are square, so the x and y components of the result are equal. +If level is outside the range [0, textureNumLevels(t)) then any valid value for the return type may be returned. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { + kAllTextureFormats, + kColorTextureFormats, + kTextureFormatInfo, + sampleTypeForFormatAndAspect, + textureDimensionAndFormatCompatible, +} from '../../../../../format_info.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { align } from '../../../../../util/math.js'; + +export const g = makeTestGroup(GPUTest); + +/// The maximum number of texture mipmap levels to test. +/// Keep this small to reduce memory and test permutations. +const kMaxMipsForTest = 3; + +/// The maximum number of texture samples to test. +const kMaxSamplesForTest = 4; + +/// All the possible GPUTextureViewDimensions. +const kAllViewDimensions: readonly GPUTextureViewDimension[] = [ + '1d', + '2d', + '2d-array', + '3d', + 'cube', + 'cube-array', +] as const; + +/** @returns the aspects to test for the given format */ +function aspectsForFormat(format: GPUTextureFormat): readonly GPUTextureAspect[] { + const formatInfo = kTextureFormatInfo[format]; + if (formatInfo.depth !== undefined && formatInfo.stencil !== undefined) { + return ['depth-only', 'stencil-only']; + } + return ['all']; +} + +/** @returns the sample counts to test for the given format */ +function samplesForFormat(format: GPUTextureFormat): readonly number[] { + const info = kTextureFormatInfo[format]; + return info.multisample ? [1, kMaxSamplesForTest] : [1]; +} + +/** + * @returns a list of number of texture mipmap levels to test, given the format, view dimensions and + * number of samples. + */ +function textureMipCount(params: { + format: GPUTextureFormat; + dimensions: GPUTextureViewDimension; + samples?: number; +}): readonly number[] { + if (params.samples !== undefined && params.samples !== 1) { + // https://www.w3.org/TR/webgpu/#texture-creation + // If descriptor.sampleCount > 1: descriptor.mipLevelCount must be 1. + return [1]; + } + if (textureDimensionsForViewDimensions(params.dimensions) === '1d') { + // https://www.w3.org/TR/webgpu/#dom-gputexturedimension-2d + // Only "2d" textures may have mipmaps, be multisampled, use a compressed or depth/stencil + // format, and be used as a render attachment. + return [1]; + } + return [1, kMaxMipsForTest]; +} + +/** + * @returns a list of GPUTextureViewDescriptor.baseMipLevel to test, give the texture mipmap count. + */ +function baseMipLevel(params: { textureMipCount: number }): readonly number[] { + const out: number[] = []; + for (let i = 0; i < params.textureMipCount; i++) { + out.push(i); + } + return out; +} + +/** + * @returns the argument values for the textureDimensions() `level` parameter to test. + * An `undefined` represents a call to textureDimensions() without the level argument. + */ +function textureDimensionsLevel(params: { + samples?: number; + textureMipCount: number; + baseMipLevel: number; +}): readonly (number | undefined)[] { + if (params.samples !== undefined && params.samples > 1) { + return [undefined]; // textureDimensions() overload with `level` not available. + } + const out: (number | undefined)[] = [undefined]; + for (let i = 0; i < params.textureMipCount - params.baseMipLevel; i++) { + out.push(i); + } + return out; +} + +/** @returns the GPUTextureViewDimensions to test for the format and number of samples */ +function viewDimensions(params: { + format: GPUTextureFormat; + samples?: number; +}): readonly GPUTextureViewDimension[] { + if (params.samples !== undefined && params.samples > 1) { + // https://www.w3.org/TR/webgpu/#dom-gputexturedimension-2d + // Only 2d textures can be multisampled + return ['2d']; + } + + return kAllViewDimensions.filter(dim => + textureDimensionAndFormatCompatible(textureDimensionsForViewDimensions(dim), params.format) + ); +} + +/** @returns the GPUTextureDimension for the GPUTextureViewDimension */ +function textureDimensionsForViewDimensions(dim: GPUTextureViewDimension): GPUTextureDimension { + switch (dim) { + case '1d': + return '1d'; + case '2d': + case '2d-array': + case 'cube': + case 'cube-array': + return '2d'; + case '3d': + return '3d'; + } +} + +/** TestValues holds the texture size and expected return value of textureDimensions() */ +type TestValues = { + /** The value to pass to GPUTextureDescriptor.size, when creating the texture */ + size: number[]; + /** The expected result of calling textureDimensions() */ + expected: number[]; +}; + +/** @returns The TestValues to use for the given texture dimensions and format */ +function testValues(params: { + dimensions: GPUTextureViewDimension; + format: GPUTextureFormat; + baseMipLevel: number; + textureDimensionsLevel?: number; +}): TestValues { + // The minimum dimension length, given the number of mipmap levels that are being tested. + const kMinLen = 1 << kMaxMipsForTest; + const kNumCubeFaces = 6; + + const formatInfo = kTextureFormatInfo[params.format]; + const bw = formatInfo.blockWidth; + const bh = formatInfo.blockHeight; + let mip = params.baseMipLevel; + if (params.textureDimensionsLevel !== undefined) { + mip += params.textureDimensionsLevel; + } + + // Magic constants to multiply the minimum texture dimensions with, to provide + // different dimension values in the test. These could be parameterized, but + // these are currently fixed to reduce the number of test parameterizations. + const kMultipleA = 2; + const kMultipleB = 3; + const kMultipleC = 4; + + switch (params.dimensions) { + case '1d': { + const w = align(kMinLen, bw) * kMultipleA; + return { size: [w], expected: [w >>> mip] }; + } + case '2d': { + const w = align(kMinLen, bw) * kMultipleA; + const h = align(kMinLen, bh) * kMultipleB; + return { size: [w, h], expected: [w >>> mip, h >>> mip] }; + } + case '2d-array': { + const w = align(kMinLen, bw) * kMultipleC; + const h = align(kMinLen, bh) * kMultipleB; + return { size: [w, h, 4], expected: [w >>> mip, h >>> mip] }; + } + case '3d': { + const w = align(kMinLen, bw) * kMultipleA; + const h = align(kMinLen, bh) * kMultipleB; + const d = kMinLen * kMultipleC; + return { + size: [w, h, d], + expected: [w >>> mip, h >>> mip, d >>> mip], + }; + } + case 'cube': { + const l = align(kMinLen, bw) * align(kMinLen, bh) * kMultipleB; + return { + size: [l, l, kNumCubeFaces], + expected: [l >>> mip, l >>> mip], + }; + } + case 'cube-array': { + const l = align(kMinLen, bw) * align(kMinLen, bh) * kMultipleC; + return { + size: [l, l, kNumCubeFaces * 3], + expected: [l >>> mip, l >>> mip], + }; + } + } +} + +/** + * Builds a shader module with the texture view bound to the WGSL texture with the given WGSL type, + * which calls textureDimensions(), assigning the result to an output buffer. + * This shader is executed with a compute shader, and the output buffer is compared to + * `values.expected`. + */ +function run( + t: GPUTest, + view: GPUTextureView, + textureType: string, + levelArg: number | undefined, + values: TestValues +) { + const outputType = values.expected.length > 1 ? `vec${values.expected.length}u` : 'u32'; + const wgsl = ` +@group(0) @binding(0) var texture : ${textureType}; +@group(0) @binding(1) var<storage, read_write> output : ${outputType}; + +@compute @workgroup_size(1) +fn main() { +output = ${ + levelArg !== undefined + ? `textureDimensions(texture, ${levelArg})` + : 'textureDimensions(texture)' + }; +} +`; + const module = t.device.createShaderModule({ + code: wgsl, + }); + const pipeline = t.device.createComputePipeline({ + compute: { module }, + layout: 'auto', + }); + const outputBuffer = t.device.createBuffer({ + size: 32, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE, + }); + const bindgroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: view }, + { binding: 1, resource: { buffer: outputBuffer } }, + ], + }); + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindgroup); + pass.dispatchWorkgroups(1); + pass.end(); + t.device.queue.submit([encoder.finish()]); + + t.expectGPUBufferValuesEqual(outputBuffer, new Uint32Array(values.expected)); +} + +/** @returns true if the GPUTextureViewDimension is valid for a storage texture */ +function dimensionsValidForStorage(dimensions: GPUTextureViewDimension) { + switch (dimensions) { + case '1d': + case '2d': + case '2d-array': + case '3d': + return true; + default: + return false; + } +} + +g.test('sampled_and_multisampled') + .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') + .desc( + ` +T: f32, i32, u32 + +fn textureDimensions(t: texture_1d<T>) -> u32 +fn textureDimensions(t: texture_1d<T>, level: u32) -> u32 +fn textureDimensions(t: texture_2d<T>) -> vec2<u32> +fn textureDimensions(t: texture_2d<T>, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_2d_array<T>) -> vec2<u32> +fn textureDimensions(t: texture_2d_array<T>, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_3d<T>) -> vec3<u32> +fn textureDimensions(t: texture_3d<T>, level: u32) -> vec3<u32> +fn textureDimensions(t: texture_cube<T>) -> vec2<u32> +fn textureDimensions(t: texture_cube<T>, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_cube_array<T>) -> vec2<u32> +fn textureDimensions(t: texture_cube_array<T>, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_multisampled_2d<T>)-> vec2<u32> + +Parameters: + * t: the sampled texture + * level: + - The mip level, with level 0 containing a full size version of the texture. + - If omitted, the dimensions of level 0 are returned. +` + ) + .params(u => + u + .combine('format', kAllTextureFormats) + .unless(p => kTextureFormatInfo[p.format].color?.type === 'unfilterable-float') + .expand('aspect', u => aspectsForFormat(u.format)) + .expand('samples', u => samplesForFormat(u.format)) + .beginSubcases() + .expand('dimensions', viewDimensions) + .expand('textureMipCount', textureMipCount) + .expand('baseMipLevel', baseMipLevel) + .expand('textureDimensionsLevel', textureDimensionsLevel) + ) + .beforeAllSubcases(t => { + const info = kTextureFormatInfo[t.params.format]; + t.skipIfTextureFormatNotSupported(t.params.format); + t.selectDeviceOrSkipTestCase(info.feature); + }) + .fn(t => { + t.skipIfTextureViewDimensionNotSupported(t.params.dimensions); + const values = testValues(t.params); + const texture = t.device.createTexture({ + size: values.size, + dimension: textureDimensionsForViewDimensions(t.params.dimensions), + ...(t.isCompatibility && { textureBindingViewDimension: t.params.dimensions }), + usage: + t.params.samples === 1 + ? GPUTextureUsage.TEXTURE_BINDING + : GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, + format: t.params.format, + sampleCount: t.params.samples, + mipLevelCount: t.params.textureMipCount, + }); + const textureView = texture.createView({ + dimension: t.params.dimensions, + aspect: t.params.aspect, + baseMipLevel: t.params.baseMipLevel, + }); + + function wgslSampledTextureType(): string { + const base = t.params.samples !== 1 ? 'texture_multisampled' : 'texture'; + const dimensions = t.params.dimensions.replace('-', '_'); + const sampleType = sampleTypeForFormatAndAspect(t.params.format, t.params.aspect); + switch (sampleType) { + case 'depth': + case 'float': + return `${base}_${dimensions}<f32>`; + case 'uint': + return `${base}_${dimensions}<u32>`; + case 'sint': + return `${base}_${dimensions}<i32>`; + case 'unfilterable-float': + throw new Error(`'${t.params.format}' does not support sampling`); + } + } + + run(t, textureView, wgslSampledTextureType(), t.params.textureDimensionsLevel, values); + }); + +g.test('depth') + .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') + .desc( + ` +fn textureDimensions(t: texture_depth_2d) -> vec2<u32> +fn textureDimensions(t: texture_depth_2d, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_depth_2d_array) -> vec2<u32> +fn textureDimensions(t: texture_depth_2d_array, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_depth_cube) -> vec2<u32> +fn textureDimensions(t: texture_depth_cube, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_depth_cube_array) -> vec2<u32> +fn textureDimensions(t: texture_depth_cube_array, level: u32) -> vec2<u32> +fn textureDimensions(t: texture_depth_multisampled_2d)-> vec2<u32> + +Parameters: + * t: the depth or multisampled texture + * level: + - The mip level, with level 0 containing a full size version of the texture. + - If omitted, the dimensions of level 0 are returned. +` + ) + .params(u => + u + .combine('format', kAllTextureFormats) + .filter(p => !!kTextureFormatInfo[p.format].depth) + .expand('aspect', u => aspectsForFormat(u.format)) + .unless(u => u.aspect === 'stencil-only') + .expand('samples', u => samplesForFormat(u.format)) + .beginSubcases() + .expand('dimensions', viewDimensions) + .expand('textureMipCount', textureMipCount) + .expand('baseMipLevel', baseMipLevel) + .expand('textureDimensionsLevel', textureDimensionsLevel) + ) + .beforeAllSubcases(t => { + const info = kTextureFormatInfo[t.params.format]; + t.skipIfTextureFormatNotSupported(t.params.format); + t.selectDeviceOrSkipTestCase(info.feature); + }) + .fn(t => { + t.skipIfTextureViewDimensionNotSupported(t.params.dimensions); + const values = testValues(t.params); + const texture = t.device.createTexture({ + size: values.size, + dimension: textureDimensionsForViewDimensions(t.params.dimensions), + ...(t.isCompatibility && { textureBindingViewDimension: t.params.dimensions }), + usage: + t.params.samples === 1 + ? GPUTextureUsage.TEXTURE_BINDING + : GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, + format: t.params.format, + sampleCount: t.params.samples, + mipLevelCount: t.params.textureMipCount, + }); + const textureView = texture.createView({ + dimension: t.params.dimensions, + aspect: t.params.aspect, + baseMipLevel: t.params.baseMipLevel, + }); + + function wgslDepthTextureType(): string { + const base = t.params.samples !== 1 ? 'texture_depth_multisampled' : 'texture_depth'; + const dimensions = t.params.dimensions.replace('-', '_'); + return `${base}_${dimensions}`; + } + + run(t, textureView, wgslDepthTextureType(), t.params.textureDimensionsLevel, values); + }); + +g.test('storage') + .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') + .desc( + ` +F: rgba8unorm + rgba8snorm + rgba8uint + rgba8sint + rgba16uint + rgba16sint + rgba16float + r32uint + r32sint + r32float + rg32uint + rg32sint + rg32float + rgba32uint + rgba32sint + rgba32float +A: read, write, read_write + +fn textureDimensions(t: texture_storage_1d<F,A>) -> u32 +fn textureDimensions(t: texture_storage_2d<F,A>) -> vec2<u32> +fn textureDimensions(t: texture_storage_2d_array<F,A>) -> vec2<u32> +fn textureDimensions(t: texture_storage_3d<F,A>) -> vec3<u32> + +Parameters: + * t: the storage texture +` + ) + .params(u => + u + .combine('format', kColorTextureFormats) + .filter(p => kTextureFormatInfo[p.format].color?.storage === true) + .expand('aspect', u => aspectsForFormat(u.format)) + .beginSubcases() + .expand('dimensions', u => viewDimensions(u).filter(dimensionsValidForStorage)) + .expand('textureMipCount', textureMipCount) + .expand('baseMipLevel', baseMipLevel) + ) + .beforeAllSubcases(t => { + const info = kTextureFormatInfo[t.params.format]; + t.skipIfTextureFormatNotSupported(t.params.format); + t.skipIfTextureFormatNotUsableAsStorageTexture(t.params.format); + t.selectDeviceOrSkipTestCase(info.feature); + }) + .fn(t => { + const values = testValues(t.params); + const texture = t.device.createTexture({ + size: values.size, + dimension: textureDimensionsForViewDimensions(t.params.dimensions), + usage: GPUTextureUsage.STORAGE_BINDING, + format: t.params.format, + mipLevelCount: t.params.textureMipCount, + }); + const textureView = texture.createView({ + dimension: t.params.dimensions, + aspect: t.params.aspect, + mipLevelCount: 1, + baseMipLevel: t.params.baseMipLevel, + }); + + function wgslStorageTextureType(): string { + const dimensions = t.params.dimensions.replace('-', '_'); + return `texture_storage_${dimensions}<${t.params.format}, write>`; + } + + run(t, textureView, wgslStorageTextureType(), undefined, values); + }); + +g.test('external') + .specURL('https://www.w3.org/TR/WGSL/#texturedimensions') + .desc( + ` +fn textureDimensions(t: texture_external) -> vec2<u32> + +Parameters: + * t: the external texture +` + ) + .unimplemented(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts index f5b01dfc63..7c743576e9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts @@ -1,26 +1,24 @@ export const description = ` Samples a texture. - -Must only be used in a fragment shader stage. -Must only be invoked in uniform control flow. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; -import { GPUTest } from '../../../../../gpu_test.js'; +import { kEncodableTextureFormats, kTextureFormatInfo } from '../../../../../format_info.js'; +import { GPUTest, TextureTestMixin } from '../../../../../gpu_test.js'; +import { hashU32 } from '../../../../../util/math.js'; +import { kTexelRepresentationInfo } from '../../../../../util/texture/texel_data.js'; +import { + vec2, + createRandomTexelView, + TextureCall, + putDataInTextureThenDrawAndCheckResults, + generateSamplePoints, + kSamplePointMethods, +} from './texture_utils.js'; import { generateCoordBoundaries, generateOffsets } from './utils.js'; -export const g = makeTestGroup(GPUTest); - -g.test('stage') - .specURL('https://www.w3.org/TR/WGSL/#texturesample') - .desc( - ` -Tests that 'textureSample' can only be called in 'fragment' shaders. -` - ) - .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const)) - .unimplemented(); +export const g = makeTestGroup(TextureTestMixin(GPUTest)); g.test('control_flow') .specURL('https://www.w3.org/TR/WGSL/#texturesample') @@ -70,11 +68,74 @@ Parameters: Values outside of this range will result in a shader-creation error. ` ) - .paramsSubcasesOnly(u => + .params(u => u - .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const) - .combine('coords', generateCoordBoundaries(2)) - .combine('offset', generateOffsets(2)) + .combine('format', kEncodableTextureFormats) + .filter(t => { + const type = kTextureFormatInfo[t.format].color?.type; + return type === 'float' || type === 'unfilterable-float'; + }) + .combine('sample_points', kSamplePointMethods) + .combine('addressModeU', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const) + .combine('addressModeV', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const) + .combine('minFilter', ['nearest', 'linear'] as const) + .combine('offset', [false, true] as const) + ) + .beforeAllSubcases(t => { + const format = kTexelRepresentationInfo[t.params.format]; + t.skipIfTextureFormatNotSupported(t.params.format); + const hasFloat32 = format.componentOrder.some(c => { + const info = format.componentInfo[c]!; + return info.dataType === 'float' && info.bitLength === 32; + }); + if (hasFloat32) { + t.selectDeviceOrSkipTestCase('float32-filterable'); + } + }) + .fn(async t => { + const descriptor: GPUTextureDescriptor = { + format: t.params.format, + size: { width: 8, height: 8 }, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }; + const texelView = createRandomTexelView(descriptor); + const calls: TextureCall<vec2>[] = generateSamplePoints(50, t.params.minFilter === 'nearest', { + method: t.params.sample_points, + textureWidth: 8, + textureHeight: 8, + }).map((c, i) => { + const hash = hashU32(i) & 0xff; + return { + builtin: 'textureSample', + coordType: 'f', + coords: c, + offset: t.params.offset ? [(hash & 15) - 8, (hash >> 4) - 8] : undefined, + }; + }); + const sampler: GPUSamplerDescriptor = { + addressModeU: t.params.addressModeU, + addressModeV: t.params.addressModeV, + minFilter: t.params.minFilter, + magFilter: t.params.minFilter, + }; + const res = await putDataInTextureThenDrawAndCheckResults( + t.device, + { texels: texelView, descriptor }, + sampler, + calls + ); + t.expectOK(res); + }); + +g.test('sampled_2d_coords,derivatives') + .specURL('https://www.w3.org/TR/WGSL/#texturesample') + .desc( + ` +fn textureSample(t: texture_2d<f32>, s: sampler, coords: vec2<f32>) -> vec4<f32> +fn textureSample(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<f32> + +test mip level selection based on derivatives + ` ) .unimplemented(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts index 786bce4830..1c61c1a5f2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts @@ -2,8 +2,6 @@ export const description = ` Execution tests for the 'textureSampleBias' builtin function Samples a texture with a bias to the mip level. -Must only be used in a fragment shader stage. -Must only be invoked in uniform control flow. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; @@ -13,26 +11,6 @@ import { generateCoordBoundaries, generateOffsets } from './utils.js'; export const g = makeTestGroup(GPUTest); -g.test('stage') - .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias') - .desc( - ` -Tests that 'textureSampleBias' can only be called in 'fragment' shaders. -` - ) - .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const)) - .unimplemented(); - -g.test('control_flow') - .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias') - .desc( - ` -Tests that 'textureSampleBias' can only be called in uniform control flow. -` - ) - .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const)) - .unimplemented(); - g.test('sampled_2d_coords') .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias') .desc( diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts index 9f723fac2e..eae5098257 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts @@ -1,8 +1,5 @@ export const description = ` Samples a depth texture and compares the sampled depth values against a reference value. - -Must only be used in a fragment shader stage. -Must only be invoked in uniform control flow. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; @@ -12,26 +9,6 @@ import { generateCoordBoundaries, generateOffsets } from './utils.js'; export const g = makeTestGroup(GPUTest); -g.test('stage') - .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare') - .desc( - ` -Tests that 'textureSampleCompare' can only be called in 'fragment' shaders. -` - ) - .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const)) - .unimplemented(); - -g.test('control_flow') - .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare') - .desc( - ` -Tests that 'textureSampleCompare' can only be called in uniform control flow. -` - ) - .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const)) - .unimplemented(); - g.test('2d_coords') .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare') .desc( diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts new file mode 100644 index 0000000000..fe2da53d5a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts @@ -0,0 +1,809 @@ +import { assert, range, unreachable } from '../../../../../../common/util/util.js'; +import { EncodableTextureFormat } from '../../../../../format_info.js'; +import { float32ToUint32 } from '../../../../../util/conversion.js'; +import { align, clamp, hashU32, lerp, quantizeToF32 } from '../../../../../util/math.js'; +import { + kTexelRepresentationInfo, + PerTexelComponent, + TexelRepresentationInfo, +} from '../../../../../util/texture/texel_data.js'; +import { TexelView } from '../../../../../util/texture/texel_view.js'; +import { createTextureFromTexelView } from '../../../../../util/texture.js'; +import { reifyExtent3D } from '../../../../../util/unions.js'; + +function getLimitValue(v: number) { + switch (v) { + case Number.POSITIVE_INFINITY: + return 1000; + case Number.NEGATIVE_INFINITY: + return -1000; + default: + return v; + } +} + +function getValueBetweenMinAndMaxTexelValueInclusive( + rep: TexelRepresentationInfo, + normalized: number +) { + return lerp( + getLimitValue(rep.numericRange!.min), + getLimitValue(rep.numericRange!.max), + normalized + ); +} + +/** + * Creates a TexelView filled with random values. + */ +export function createRandomTexelView(info: { + format: GPUTextureFormat; + size: GPUExtent3D; +}): TexelView { + const rep = kTexelRepresentationInfo[info.format as EncodableTextureFormat]; + const generator = (coords: Required<GPUOrigin3DDict>): Readonly<PerTexelComponent<number>> => { + const texel: PerTexelComponent<number> = {}; + for (const component of rep.componentOrder) { + const rnd = hashU32(coords.x, coords.y, coords.z, component.charCodeAt(0)); + const normalized = clamp(rnd / 0xffffffff, { min: 0, max: 1 }); + texel[component] = getValueBetweenMinAndMaxTexelValueInclusive(rep, normalized); + } + return quantize(texel, rep); + }; + return TexelView.fromTexelsAsColors(info.format as EncodableTextureFormat, generator); +} + +export type vec2 = [number, number]; +export type vec3 = [number, number, number]; +export type vec4 = [number, number, number, number]; +export type Dimensionality = number | vec2 | vec3; + +type TextureCallArgKeys = keyof TextureCallArgs<number>; +const kTextureCallArgNames: TextureCallArgKeys[] = [ + 'coords', + 'mipLevel', + 'arrayIndex', + 'ddx', + 'ddy', + 'offset', +]; + +export interface TextureCallArgs<T extends Dimensionality> { + coords?: T; + mipLevel?: number; + arrayIndex?: number; + ddx?: T; + ddy?: T; + offset?: T; +} + +export interface TextureCall<T extends Dimensionality> extends TextureCallArgs<T> { + builtin: 'textureSample' | 'textureLoad'; + coordType: 'f'; +} + +function toArray(coords: Dimensionality): number[] { + if (coords instanceof Array) { + return coords; + } + return [coords]; +} + +function quantize(texel: PerTexelComponent<number>, repl: TexelRepresentationInfo) { + return repl.bitsToNumber(repl.unpackBits(new Uint8Array(repl.pack(repl.encode(texel))))); +} + +function apply(a: number[], b: number[], op: (x: number, y: number) => number) { + assert(a.length === b.length, `apply(${a}, ${b}): arrays must have same length`); + return a.map((v, i) => op(v, b[i])); +} + +const add = (a: number[], b: number[]) => apply(a, b, (x, y) => x + y); + +export interface Texture { + texels: TexelView; + descriptor: GPUTextureDescriptor; +} + +/** + * Returns the expect value for a WGSL builtin texture function + */ +export function expected<T extends Dimensionality>( + call: TextureCall<T>, + texture: Texture, + sampler: GPUSamplerDescriptor +): PerTexelComponent<number> { + const rep = kTexelRepresentationInfo[texture.texels.format]; + const textureExtent = reifyExtent3D(texture.descriptor.size); + const textureSize = [textureExtent.width, textureExtent.height, textureExtent.depthOrArrayLayers]; + const addressMode = [ + sampler.addressModeU ?? 'clamp-to-edge', + sampler.addressModeV ?? 'clamp-to-edge', + sampler.addressModeW ?? 'clamp-to-edge', + ]; + + const load = (at: number[]) => + texture.texels.color({ + x: Math.floor(at[0]), + y: Math.floor(at[1] ?? 0), + z: Math.floor(at[2] ?? 0), + }); + + switch (call.builtin) { + case 'textureSample': { + const coords = toArray(call.coords!); + + // convert normalized to absolute texel coordinate + // ┌───┬───┬───┬───┐ + // │ a │ │ │ │ norm: a = 1/8, b = 7/8 + // ├───┼───┼───┼───┤ abs: a = 0, b = 3 + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ b │ + // └───┴───┴───┴───┘ + let at = coords.map((v, i) => v * textureSize[i] - 0.5); + + // Apply offset in whole texel units + if (call.offset !== undefined) { + at = add(at, toArray(call.offset)); + } + + const samples: { at: number[]; weight: number }[] = []; + + const filter = sampler.minFilter; + switch (filter) { + case 'linear': { + // 'p0' is the lower texel for 'at' + const p0 = at.map(v => Math.floor(v)); + // 'p1' is the higher texel for 'at' + const p1 = p0.map(v => v + 1); + + // interpolation weights for p0 and p1 + const p1W = at.map((v, i) => v - p0[i]); + const p0W = p1W.map(v => 1 - v); + + switch (coords.length) { + case 1: + samples.push({ at: p0, weight: p0W[0] }); + samples.push({ at: p1, weight: p1W[0] }); + break; + case 2: { + samples.push({ at: p0, weight: p0W[0] * p0W[1] }); + samples.push({ at: [p1[0], p0[1]], weight: p1W[0] * p0W[1] }); + samples.push({ at: [p0[0], p1[1]], weight: p0W[0] * p1W[1] }); + samples.push({ at: p1, weight: p1W[0] * p1W[1] }); + break; + } + } + break; + } + case 'nearest': { + const p = at.map(v => Math.round(quantizeToF32(v))); + samples.push({ at: p, weight: 1 }); + break; + } + default: + unreachable(); + } + + const out: PerTexelComponent<number> = {}; + const ss = []; + for (const sample of samples) { + // Apply sampler address mode + const c = sample.at.map((v, i) => { + switch (addressMode[i]) { + case 'clamp-to-edge': + return clamp(v, { min: 0, max: textureSize[i] - 1 }); + case 'mirror-repeat': { + const n = Math.floor(v / textureSize[i]); + v = v - n * textureSize[i]; + return (n & 1) !== 0 ? textureSize[i] - v - 1 : v; + } + case 'repeat': + return v - Math.floor(v / textureSize[i]) * textureSize[i]; + default: + unreachable(); + } + }); + const v = load(c); + ss.push(v); + for (const component of rep.componentOrder) { + out[component] = (out[component] ?? 0) + v[component]! * sample.weight; + } + } + + return out; + } + case 'textureLoad': { + return load(toArray(call.coords!)); + } + } +} + +/** + * Puts random data in a texture, generates a shader that implements `calls` + * such that each call's result is written to the next consecutive texel of + * a rgba32float texture. It then checks the result of each call matches + * the expected result. + */ +export async function putDataInTextureThenDrawAndCheckResults<T extends Dimensionality>( + device: GPUDevice, + texture: Texture, + sampler: GPUSamplerDescriptor, + calls: TextureCall<T>[] +) { + const results = await doTextureCalls(device, texture, sampler, calls); + const errs: string[] = []; + const rep = kTexelRepresentationInfo[texture.texels.format]; + for (let callIdx = 0; callIdx < calls.length; callIdx++) { + const call = calls[callIdx]; + const got = results[callIdx]; + const expect = expected(call, texture, sampler); + + const gULP = rep.bitsToULPFromZero(rep.numberToBits(got)); + const eULP = rep.bitsToULPFromZero(rep.numberToBits(expect)); + for (const component of rep.componentOrder) { + const g = got[component]!; + const e = expect[component]!; + const absDiff = Math.abs(g - e); + const ulpDiff = Math.abs(gULP[component]! - eULP[component]!); + const relDiff = absDiff / Math.max(Math.abs(g), Math.abs(e)); + if (ulpDiff > 3 && relDiff > 0.03) { + const desc = describeTextureCall(call); + errs.push(`component was not as expected: + call: ${desc} + component: ${component} + got: ${g} + expected: ${e} + abs diff: ${absDiff.toFixed(4)} + rel diff: ${(relDiff * 100).toFixed(2)}% + ulp diff: ${ulpDiff} + sample points: +`); + const expectedSamplePoints = [ + 'expected:', + ...(await identifySamplePoints(texture.descriptor, (texels: TexelView) => { + return Promise.resolve( + expected(call, { texels, descriptor: texture.descriptor }, sampler) + ); + })), + ]; + const gotSamplePoints = [ + 'got:', + ...(await identifySamplePoints( + texture.descriptor, + async (texels: TexelView) => + ( + await doTextureCalls(device, { texels, descriptor: texture.descriptor }, sampler, [ + call, + ]) + )[0] + )), + ]; + errs.push(layoutTwoColumns(expectedSamplePoints, gotSamplePoints).join('\n')); + errs.push('', ''); + } + } + } + + return errs.length > 0 ? new Error(errs.join('\n')) : undefined; +} + +/** + * Generates a text art grid showing which texels were sampled + * followed by a list of the samples and the weights used for each + * component. + * + * Example: + * + * 0 1 2 3 4 5 6 7 + * ┌───┬───┬───┬───┬───┬───┬───┬───┐ + * 0 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 1 │ │ │ │ │ │ │ │ a │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 2 │ │ │ │ │ │ │ │ b │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 3 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 4 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 5 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 6 │ │ │ │ │ │ │ │ │ + * ├───┼───┼───┼───┼───┼───┼───┼───┤ + * 7 │ │ │ │ │ │ │ │ │ + * └───┴───┴───┴───┴───┴───┴───┴───┘ + * a: at: [7, 1], weights: [R: 0.75000] + * b: at: [7, 2], weights: [R: 0.25000] + */ +async function identifySamplePoints( + info: GPUTextureDescriptor, + run: (texels: TexelView) => Promise<PerTexelComponent<number>> +) { + const textureSize = reifyExtent3D(info.size); + const numTexels = textureSize.width * textureSize.height; + const rep = kTexelRepresentationInfo[info.format as EncodableTextureFormat]; + + // Identify all the texels that are sampled, and their weights. + const sampledTexelWeights = new Map<number, PerTexelComponent<number>>(); + const unclassifiedStack = [new Set<number>(range(numTexels, v => v))]; + while (unclassifiedStack.length > 0) { + // Pop the an unclassified texels stack + const unclassified = unclassifiedStack.pop()!; + + // Split unclassified texels evenly into two new sets + const setA = new Set<number>(); + const setB = new Set<number>(); + [...unclassified.keys()].forEach((t, i) => ((i & 1) === 0 ? setA : setB).add(t)); + + // Push setB to the unclassified texels stack + if (setB.size > 0) { + unclassifiedStack.push(setB); + } + + // See if any of the texels in setA were sampled. + const results = await run( + TexelView.fromTexelsAsColors( + info.format as EncodableTextureFormat, + (coords: Required<GPUOrigin3DDict>): Readonly<PerTexelComponent<number>> => { + const isCandidate = setA.has(coords.x + coords.y * textureSize.width); + const texel: PerTexelComponent<number> = {}; + for (const component of rep.componentOrder) { + texel[component] = isCandidate ? 1 : 0; + } + return texel; + } + ) + ); + if (rep.componentOrder.some(c => results[c] !== 0)) { + // One or more texels of setA were sampled. + if (setA.size === 1) { + // We identified a specific texel was sampled. + // As there was only one texel in the set, results holds the sampling weights. + setA.forEach(texel => sampledTexelWeights.set(texel, results)); + } else { + // More than one texel in the set. Needs splitting. + unclassifiedStack.push(setA); + } + } + } + + // ┌───┬───┬───┬───┐ + // │ a │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ │ + // ├───┼───┼───┼───┤ + // │ │ │ │ b │ + // └───┴───┴───┴───┘ + const letter = (idx: number) => String.fromCharCode(97 + idx); // 97: 'a' + const orderedTexelIndices: number[] = []; + const lines: string[] = []; + { + let line = ' '; + for (let x = 0; x < textureSize.width; x++) { + line += ` ${x} `; + } + lines.push(line); + } + { + let line = ' ┌'; + for (let x = 0; x < textureSize.width; x++) { + line += x === textureSize.width - 1 ? '───┐' : '───┬'; + } + lines.push(line); + } + for (let y = 0; y < textureSize.height; y++) { + { + let line = `${y} │`; + for (let x = 0; x < textureSize.width; x++) { + const texelIdx = x + y * textureSize.height; + const weight = sampledTexelWeights.get(texelIdx); + if (weight !== undefined) { + line += ` ${letter(orderedTexelIndices.length)} │`; + orderedTexelIndices.push(texelIdx); + } else { + line += ' │'; + } + } + lines.push(line); + } + if (y < textureSize.height - 1) { + let line = ' ├'; + for (let x = 0; x < textureSize.width; x++) { + line += x === textureSize.width - 1 ? '───┤' : '───┼'; + } + lines.push(line); + } + } + { + let line = ' └'; + for (let x = 0; x < textureSize.width; x++) { + line += x === textureSize.width - 1 ? '───┘' : '───┴'; + } + lines.push(line); + } + + orderedTexelIndices.forEach((texelIdx, i) => { + const weights = sampledTexelWeights.get(texelIdx)!; + const y = Math.floor(texelIdx / textureSize.width); + const x = texelIdx - y * textureSize.height; + const w = rep.componentOrder.map(c => `${c}: ${weights[c]?.toFixed(5)}`).join(', '); + lines.push(`${letter(i)}: at: [${x}, ${y}], weights: [${w}]`); + }); + return lines; +} + +function layoutTwoColumns(columnA: string[], columnB: string[]) { + const widthA = Math.max(...columnA.map(l => l.length)); + const lines = Math.max(columnA.length, columnB.length); + const out: string[] = new Array<string>(lines); + for (let line = 0; line < lines; line++) { + const a = columnA[line] ?? ''; + const b = columnB[line] ?? ''; + out[line] = `${a}${' '.repeat(widthA - a.length)} | ${b}`; + } + return out; +} + +export const kSamplePointMethods = ['texel-centre', 'spiral'] as const; +export type SamplePointMethods = (typeof kSamplePointMethods)[number]; + +/** + * Generates an array of coordinates at which to sample a texture. + */ +export function generateSamplePoints( + n: number, + nearest: boolean, + args: + | { + method: 'texel-centre'; + textureWidth: number; + textureHeight: number; + } + | { + method: 'spiral'; + radius?: number; + loops?: number; + textureWidth: number; + textureHeight: number; + } +) { + const out: vec2[] = []; + switch (args.method) { + case 'texel-centre': { + for (let i = 0; i < n; i++) { + const r = hashU32(i); + const x = Math.floor(lerp(0, args.textureWidth - 1, (r & 0xffff) / 0xffff)) + 0.5; + const y = Math.floor(lerp(0, args.textureHeight - 1, (r >>> 16) / 0xffff)) + 0.5; + out.push([x / args.textureWidth, y / args.textureHeight]); + } + break; + } + case 'spiral': { + for (let i = 0; i < n; i++) { + const f = i / (Math.max(n, 2) - 1); + const r = (args.radius ?? 1.5) * f; + const a = (args.loops ?? 2) * 2 * Math.PI * f; + out.push([0.5 + r * Math.cos(a), 0.5 + r * Math.sin(a)]); + } + break; + } + } + // Samplers across devices use different methods to interpolate. + // Quantizing the texture coordinates seems to hit coords that produce + // comparable results to our computed results. + // Note: This value works with 8x8 textures. Other sizes have not been tested. + // Values that worked for reference: + // Win 11, NVidia 2070 Super: 16 + // Linux, AMD Radeon Pro WX 3200: 256 + // MacOS, M1 Mac: 256 + const kSubdivisionsPerTexel = 4; + const q = [args.textureWidth * kSubdivisionsPerTexel, args.textureHeight * kSubdivisionsPerTexel]; + return out.map( + c => + c.map((v, i) => { + // Quantize to kSubdivisionsPerPixel + const v1 = Math.floor(v * q[i]); + // If it's nearest and we're on the edge of a texel then move us off the edge + // since the edge could choose one texel or another in nearest mode + const v2 = nearest && v1 % kSubdivisionsPerTexel === 0 ? v1 + 1 : v1; + // Convert back to texture coords + return v2 / q[i]; + }) as vec2 + ); +} + +function wgslTypeFor(data: Dimensionality, type: 'f' | 'i' | 'u'): string { + if (data instanceof Array) { + switch (data.length) { + case 2: + return `vec2${type}`; + case 3: + return `vec3${type}`; + } + } + return '${type}32'; +} + +function wgslExpr(data: number | vec2 | vec3 | vec4): string { + if (data instanceof Array) { + switch (data.length) { + case 2: + return `vec2(${data.map(v => v.toString()).join(', ')})`; + case 3: + return `vec3(${data.map(v => v.toString()).join(', ')})`; + } + } + return data.toString(); +} + +function binKey<T extends Dimensionality>(call: TextureCall<T>): string { + const keys: string[] = []; + for (const name of kTextureCallArgNames) { + const value = call[name]; + if (value !== undefined) { + if (name === 'offset') { + // offset must be a constant expression + keys.push(`${name}: ${wgslExpr(value)}`); + } else { + keys.push(`${name}: ${wgslTypeFor(value, call.coordType)}`); + } + } + } + return `${call.builtin}(${keys.join(', ')})`; +} + +function buildBinnedCalls<T extends Dimensionality>(calls: TextureCall<T>[]) { + const args: string[] = ['T']; // All texture builtins take the texture as the first argument + const fields: string[] = []; + const data: number[] = []; + + const prototype = calls[0]; + if (prototype.builtin.startsWith('textureSample')) { + // textureSample*() builtins take a sampler as the second argument + args.push('S'); + } + + for (const name of kTextureCallArgNames) { + const value = prototype[name]; + if (value !== undefined) { + if (name === 'offset') { + args.push(`/* offset */ ${wgslExpr(value)}`); + } else { + args.push(`args.${name}`); + fields.push(`@align(16) ${name} : ${wgslTypeFor(value, prototype.coordType)}`); + } + } + } + + for (const call of calls) { + for (const name of kTextureCallArgNames) { + const value = call[name]; + assert( + (prototype[name] === undefined) === (value === undefined), + 'texture calls are not binned correctly' + ); + if (value !== undefined && name !== 'offset') { + const bitcastToU32 = (value: number) => { + if (calls[0].coordType === 'f') { + return float32ToUint32(value); + } + return value; + }; + if (value instanceof Array) { + for (const c of value) { + data.push(bitcastToU32(c)); + } + } else { + data.push(bitcastToU32(value)); + } + // All fields are aligned to 16 bytes. + while ((data.length & 3) !== 0) { + data.push(0); + } + } + } + } + + const expr = `${prototype.builtin}(${args.join(', ')})`; + + return { expr, fields, data }; +} + +function binCalls<T extends Dimensionality>(calls: TextureCall<T>[]): number[][] { + const map = new Map<string, number>(); // key to bin index + const bins: number[][] = []; + calls.forEach((call, callIdx) => { + const key = binKey(call); + const binIdx = map.get(key); + if (binIdx === undefined) { + map.set(key, bins.length); + bins.push([callIdx]); + } else { + bins[binIdx].push(callIdx); + } + }); + return bins; +} + +export function describeTextureCall<T extends Dimensionality>(call: TextureCall<T>): string { + const args: string[] = ['texture: T']; + if (call.builtin.startsWith('textureSample')) { + args.push('sampler: S'); + } + for (const name of kTextureCallArgNames) { + const value = call[name]; + if (value !== undefined) { + args.push(`${name}: ${wgslExpr(value)}`); + } + } + return `${call.builtin}(${args.join(', ')})`; +} + +/** + * Given a list of "calls", each one of which has a texture coordinate, + * generates a fragment shader that uses the fragment position as an index + * (position.y * 256 + position.x) That index is then used to look up a + * coordinate from a storage buffer which is used to call the WGSL texture + * function to read/sample the texture, and then write to an rgba32float + * texture. We then read the rgba32float texture for the per "call" results. + * + * Calls are "binned" by call parameters. Each bin has its own structure and + * field in the storage buffer. This allows the calls to be non-homogenous and + * each have their own data type for coordinates. + */ +export async function doTextureCalls<T extends Dimensionality>( + device: GPUDevice, + texture: Texture, + sampler: GPUSamplerDescriptor, + calls: TextureCall<T>[] +) { + let structs = ''; + let body = ''; + let dataFields = ''; + const data: number[] = []; + let callCount = 0; + const binned = binCalls(calls); + binned.forEach((binCalls, binIdx) => { + const b = buildBinnedCalls(binCalls.map(callIdx => calls[callIdx])); + structs += `struct Args${binIdx} { + ${b.fields.join(', \n')} +} +`; + dataFields += ` args${binIdx} : array<Args${binIdx}, ${binCalls.length}>, +`; + body += ` + { + let is_active = (frag_idx >= ${callCount}) & (frag_idx < ${callCount + binCalls.length}); + let args = data.args${binIdx}[frag_idx - ${callCount}]; + let call = ${b.expr}; + result = select(result, call, is_active); + } +`; + callCount += binCalls.length; + data.push(...b.data); + }); + + const dataBuffer = device.createBuffer({ + size: data.length * 4, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + }); + device.queue.writeBuffer(dataBuffer, 0, new Uint32Array(data)); + + const rtWidth = 256; + const renderTarget = device.createTexture({ + format: 'rgba32float', + size: { width: rtWidth, height: Math.ceil(calls.length / rtWidth) }, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const code = ` +${structs} + +struct Data { +${dataFields} +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4f { + let positions = array( + vec4f(-1, 1, 0, 1), vec4f( 1, 1, 0, 1), + vec4f(-1, -1, 0, 1), vec4f( 1, -1, 0, 1), + ); + return positions[vertex_index]; +} + +@group(0) @binding(0) var T : texture_2d<f32>; +@group(0) @binding(1) var S : sampler; +@group(0) @binding(2) var<storage> data : Data; + +@fragment +fn fs_main(@builtin(position) frag_pos : vec4f) -> @location(0) vec4f { + let frag_idx = u32(frag_pos.x) + u32(frag_pos.y) * ${renderTarget.width}; + var result : vec4f; +${body} + return result; +} +`; + const shaderModule = device.createShaderModule({ code }); + + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { module: shaderModule, entryPoint: 'vs_main' }, + fragment: { + module: shaderModule, + entryPoint: 'fs_main', + targets: [{ format: renderTarget.format }], + }, + primitive: { topology: 'triangle-strip', cullMode: 'none' }, + }); + + const gpuTexture = createTextureFromTexelView(device, texture.texels, texture.descriptor); + const gpuSampler = device.createSampler(sampler); + + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: gpuTexture.createView() }, + { binding: 1, resource: gpuSampler }, + { binding: 2, resource: { buffer: dataBuffer } }, + ], + }); + + const bytesPerRow = align(16 * renderTarget.width, 256); + const resultBuffer = device.createBuffer({ + size: renderTarget.height * bytesPerRow, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const encoder = device.createCommandEncoder(); + + const renderPass = encoder.beginRenderPass({ + colorAttachments: [{ view: renderTarget.createView(), loadOp: 'clear', storeOp: 'store' }], + }); + + renderPass.setPipeline(pipeline); + renderPass.setBindGroup(0, bindGroup); + renderPass.draw(4); + renderPass.end(); + encoder.copyTextureToBuffer( + { texture: renderTarget }, + { buffer: resultBuffer, bytesPerRow }, + { width: renderTarget.width, height: renderTarget.height } + ); + device.queue.submit([encoder.finish()]); + + await resultBuffer.mapAsync(GPUMapMode.READ); + + const view = TexelView.fromTextureDataByReference( + renderTarget.format as EncodableTextureFormat, + new Uint8Array(resultBuffer.getMappedRange()), + { + bytesPerRow, + rowsPerImage: renderTarget.height, + subrectOrigin: [0, 0, 0], + subrectSize: [renderTarget.width, renderTarget.height], + } + ); + + let outIdx = 0; + const out = new Array<PerTexelComponent<number>>(calls.length); + for (const bin of binned) { + for (const callIdx of bin) { + const x = outIdx % rtWidth; + const y = Math.floor(outIdx / rtWidth); + out[callIdx] = view.color({ x, y, z: 0 }); + outIdx++; + } + } + + renderTarget.destroy(); + gpuTexture.destroy(); + resultBuffer.destroy(); + + return out; +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.cache.ts new file mode 100644 index 0000000000..cb89e2d194 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.cache.ts @@ -0,0 +1,27 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract]_matCxR_[non_]const +// abstract_matCxR_non_const is empty and not used +const cases = (['f32', 'f16', 'abstract'] as const) + .flatMap(trait => + ([2, 3, 4] as const).flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`${trait}_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + if (trait === 'abstract' && nonConst) { + return []; + } + return FP[trait].generateMatrixToMatrixCases( + FP[trait].sparseMatrixRange(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP[trait].transposeInterval + ); + }, + })) + ) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('transpose', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts index 6fd4887f35..7a96906cfa 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts @@ -1,82 +1,21 @@ export const description = ` Execution tests for the 'transpose' builtin function -T is AbstractFloat, f32, or f16 +T is abstract-float, f32, or f16 @const transpose(e: matRxC<T> ) -> matCxR<T> Returns the transpose of e. `; import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeF16, TypeF32, TypeMat } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { - sparseMatrixF16Range, - sparseMatrixF32Range, - sparseMatrixF64Range, -} from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './transpose.cache.js'; export const g = makeTestGroup(GPUTest); -// Cases: f32_matCxR_[non_]const -const f32_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`f32_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixToMatrixCases( - sparseMatrixF32Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f32.transposeInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_matCxR_[non_]const -const f16_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`f16_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f16.generateMatrixToMatrixCases( - sparseMatrixF16Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f16.transposeInterval - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: abstract_matCxR -const abstract_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).map(rows => ({ - [`abstract_mat${cols}x${rows}`]: () => { - return FP.abstract.generateMatrixToMatrixCases( - sparseMatrixF64Range(cols, rows), - 'finite', - FP.abstract.transposeInterval - ); - }, - })) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('transpose', { - ...f32_cases, - ...f16_cases, - ...abstract_cases, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions') .desc(`abstract float tests`) @@ -89,12 +28,12 @@ g.test('abstract_float') .fn(async t => { const cols = t.params.cols; const rows = t.params.rows; - const cases = await d.get(`abstract_mat${cols}x${rows}`); + const cases = await d.get(`abstract_mat${cols}x${rows}_const`); await run( t, - abstractBuiltin('transpose'), - [TypeMat(cols, rows, TypeAbstractFloat)], - TypeMat(rows, cols, TypeAbstractFloat), + abstractFloatBuiltin('transpose'), + [Type.mat(cols, rows, Type.abstractFloat)], + Type.mat(rows, cols, Type.abstractFloat), t.params, cases ); @@ -120,8 +59,8 @@ g.test('f32') await run( t, builtin('transpose'), - [TypeMat(cols, rows, TypeF32)], - TypeMat(rows, cols, TypeF32), + [Type.mat(cols, rows, Type.f32)], + Type.mat(rows, cols, Type.f32), t.params, cases ); @@ -150,8 +89,8 @@ g.test('f16') await run( t, builtin('transpose'), - [TypeMat(cols, rows, TypeF16)], - TypeMat(rows, cols, TypeF16), + [Type.mat(cols, rows, Type.f16)], + Type.mat(rows, cols, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.cache.ts new file mode 100644 index 0000000000..061c95b07f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.cache.ts @@ -0,0 +1,17 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { makeCaseCache } from '../../case_cache.js'; + +// Cases: [f32|f16|abstract] +const cases = (['f32', 'f16', 'abstract'] as const) + .map(trait => ({ + [`${trait}`]: () => { + return FP[trait].generateScalarToIntervalCases( + FP[trait].scalarRange(), + 'unfiltered', + FP[trait].truncInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('trunc', cases); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts index 63cd8470f5..9d05709fc6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts @@ -1,7 +1,7 @@ export const description = ` Execution tests for the 'trunc' builtin function -S is AbstractFloat, f32, f16 +S is abstract-float, f32, f16 T is S or vecN<S> @const fn trunc(e: T ) -> T Returns the nearest whole number whose absolute value is less than or equal to e. @@ -10,32 +10,14 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeAbstractFloat, TypeF16, TypeF32 } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullF32Range, fullF64Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, onlyConstInputSource, run } from '../../expression.js'; -import { abstractBuiltin, builtin } from './builtin.js'; +import { abstractFloatBuiltin, builtin } from './builtin.js'; +import { d } from './trunc.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('trunc', { - f32: () => { - return FP.f32.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f32.truncInterval); - }, - f16: () => { - return FP.f16.generateScalarToIntervalCases(fullF32Range(), 'unfiltered', FP.f16.truncInterval); - }, - abstract: () => { - return FP.abstract.generateScalarToIntervalCases( - fullF64Range(), - 'unfiltered', - FP.abstract.truncInterval - ); - }, -}); - g.test('abstract_float') .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions') .desc(`abstract float tests`) @@ -46,7 +28,14 @@ g.test('abstract_float') ) .fn(async t => { const cases = await d.get('abstract'); - await run(t, abstractBuiltin('trunc'), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases); + await run( + t, + abstractFloatBuiltin('trunc'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases + ); }); g.test('f32') @@ -57,7 +46,7 @@ g.test('f32') ) .fn(async t => { const cases = await d.get('f32'); - await run(t, builtin('trunc'), [TypeF32], TypeF32, t.params, cases); + await run(t, builtin('trunc'), [Type.f32], Type.f32, t.params, cases); }); g.test('f16') @@ -71,5 +60,5 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, builtin('trunc'), [TypeF16], TypeF16, t.params, cases); + await run(t, builtin('trunc'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.cache.ts new file mode 100644 index 0000000000..79a7a568d2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.cache.ts @@ -0,0 +1,20 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { fullU32Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('unpack2x16float', { + u32_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'finite', + FP.f32.unpack2x16floatInterval + ); + }, + u32_non_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'unfiltered', + FP.f32.unpack2x16floatInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts index 4a0bf075e9..e145afca50 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts @@ -7,33 +7,14 @@ interpretation of bits 16×i through 16×i+15 of e as an IEEE-754 binary16 value import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullU32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; +import { d } from './unpack2x16float.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unpack2x16float', { - u32_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'finite', - FP.f32.unpack2x16floatInterval - ); - }, - u32_non_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'unfiltered', - FP.f32.unpack2x16floatInterval - ); - }, -}); - g.test('unpack') .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions') .desc( @@ -44,5 +25,5 @@ g.test('unpack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const'); - await run(t, builtin('unpack2x16float'), [TypeU32], TypeVec(2, TypeF32), t.params, cases); + await run(t, builtin('unpack2x16float'), [Type.u32], Type.vec2f, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.cache.ts new file mode 100644 index 0000000000..89dfb475d3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.cache.ts @@ -0,0 +1,20 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { fullU32Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('unpack2x16snorm', { + u32_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'finite', + FP.f32.unpack2x16snormInterval + ); + }, + u32_non_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'unfiltered', + FP.f32.unpack2x16snormInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts index 195cfd9a01..059a5664f9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts @@ -7,33 +7,14 @@ of bits 16×i through 16×i+15 of e as a twos-complement signed integer. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullU32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; +import { d } from './unpack2x16snorm.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unpack2x16snorm', { - u32_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'finite', - FP.f32.unpack2x16snormInterval - ); - }, - u32_non_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'unfiltered', - FP.f32.unpack2x16snormInterval - ); - }, -}); - g.test('unpack') .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions') .desc( @@ -44,5 +25,5 @@ g.test('unpack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const'); - await run(t, builtin('unpack2x16snorm'), [TypeU32], TypeVec(2, TypeF32), t.params, cases); + await run(t, builtin('unpack2x16snorm'), [Type.u32], Type.vec2f, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.cache.ts new file mode 100644 index 0000000000..7f0fe84af6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.cache.ts @@ -0,0 +1,20 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { fullU32Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('unpack2x16unorm', { + u32_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'finite', + FP.f32.unpack2x16unormInterval + ); + }, + u32_non_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'unfiltered', + FP.f32.unpack2x16unormInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts index 16b4e6397c..971f384023 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts @@ -7,33 +7,14 @@ Component i of the result is v ÷ 65535, where v is the interpretation of bits import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullU32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; +import { d } from './unpack2x16unorm.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unpack2x16unorm', { - u32_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'finite', - FP.f32.unpack2x16unormInterval - ); - }, - u32_non_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'unfiltered', - FP.f32.unpack2x16unormInterval - ); - }, -}); - g.test('unpack') .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions') .desc( @@ -44,5 +25,5 @@ g.test('unpack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const'); - await run(t, builtin('unpack2x16unorm'), [TypeU32], TypeVec(2, TypeF32), t.params, cases); + await run(t, builtin('unpack2x16unorm'), [Type.u32], Type.vec2f, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.cache.ts new file mode 100644 index 0000000000..3a4790f188 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.cache.ts @@ -0,0 +1,20 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { fullU32Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('unpack4x8snorm', { + u32_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'finite', + FP.f32.unpack4x8snormInterval + ); + }, + u32_non_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'unfiltered', + FP.f32.unpack4x8snormInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts index 7ea8d51918..d6e533621b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts @@ -7,33 +7,14 @@ bits 8×i through 8×i+7 of e as a twos-complement signed integer. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullU32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; +import { d } from './unpack4x8snorm.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unpack4x8snorm', { - u32_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'finite', - FP.f32.unpack4x8snormInterval - ); - }, - u32_non_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'unfiltered', - FP.f32.unpack4x8snormInterval - ); - }, -}); - g.test('unpack') .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions') .desc( @@ -44,5 +25,5 @@ g.test('unpack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const'); - await run(t, builtin('unpack4x8snorm'), [TypeU32], TypeVec(4, TypeF32), t.params, cases); + await run(t, builtin('unpack4x8snorm'), [Type.u32], Type.vec4f, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.cache.ts new file mode 100644 index 0000000000..21390f74b1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.cache.ts @@ -0,0 +1,20 @@ +import { FP } from '../../../../../util/floating_point.js'; +import { fullU32Range } from '../../../../../util/math.js'; +import { makeCaseCache } from '../../case_cache.js'; + +export const d = makeCaseCache('unpack4x8unorm', { + u32_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'finite', + FP.f32.unpack4x8unormInterval + ); + }, + u32_non_const: () => { + return FP.f32.generateU32ToIntervalCases( + fullU32Range(), + 'unfiltered', + FP.f32.unpack4x8unormInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts index bf54d23c12..026043da1a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts @@ -7,33 +7,14 @@ through 8×i+7 of e as an unsigned integer. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js'; -import { FP } from '../../../../../util/floating_point.js'; -import { fullU32Range } from '../../../../../util/math.js'; -import { makeCaseCache } from '../../case_cache.js'; +import { Type } from '../../../../../util/conversion.js'; import { allInputSources, run } from '../../expression.js'; import { builtin } from './builtin.js'; +import { d } from './unpack4x8unorm.cache.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unpack4x8unorm', { - u32_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'finite', - FP.f32.unpack4x8unormInterval - ); - }, - u32_non_const: () => { - return FP.f32.generateU32ToIntervalCases( - fullU32Range(), - 'unfiltered', - FP.f32.unpack4x8unormInterval - ); - }, -}); - g.test('unpack') .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions') .desc( @@ -44,5 +25,5 @@ g.test('unpack') .params(u => u.combine('inputSource', allInputSources)) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const'); - await run(t, builtin('unpack4x8unorm'), [TypeU32], TypeVec(4, TypeF32), t.params, cases); + await run(t, builtin('unpack4x8unorm'), [Type.u32], Type.vec4f, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4xI8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4xI8.spec.ts new file mode 100644 index 0000000000..4f7e4ed3a7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4xI8.spec.ts @@ -0,0 +1,56 @@ +export const description = ` +Execution tests for the 'unpack4xI8' builtin function + +@const fn unpack4xI8(e: u32) -> vec4<i32> +e is interpreted as a vector with four 8-bit signed integer components. Unpack e into a vec4<i32> +with sign extension. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { u32, toVector, i32, Type } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#unpack4xI8-builtin') + .desc( + ` +@const fn unpack4xI8(e: u32) -> vec4<i32> + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const unpack4xI8 = (e: number) => { + const result: [number, number, number, number] = [0, 0, 0, 0]; + for (let i = 0; i < 4; ++i) { + let intValue = (e >> (8 * i)) & 0xff; + if (intValue > 127) { + intValue -= 256; + } + result[i] = intValue; + } + return result; + }; + + const testInputs = [ + 0, 0x01020304, 0xfcfdfeff, 0x040302ff, 0x0403fe01, 0x04fd0201, 0xfc030201, 0xfcfdfe01, + 0xfcfd02ff, 0xfc03feff, 0x04fdfeff, 0x0403feff, 0x04fd02ff, 0xfc0302ff, 0x04fdfe01, + 0xfc03fe01, 0xfcfd0201, 0x80817f7e, + ] as const; + + const makeCase = (e: number): Case => { + return { input: [u32(e)], expected: toVector(unpack4xI8(e), i32) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(v)]; + }); + + await run(t, builtin('unpack4xI8'), [Type.u32], Type.vec4i, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4xU8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4xU8.spec.ts new file mode 100644 index 0000000000..09849030eb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4xU8.spec.ts @@ -0,0 +1,48 @@ +export const description = ` +Execution tests for the 'unpack4xU8' builtin function + +@const fn unpack4xU8(e: u32) -> vec4<u32> +e is interpreted as a vector with four 8-bit unsigned integer components. Unpack e into a vec4<u32> +with zero extension. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { u32, toVector, Type } from '../../../../../util/conversion.js'; +import { Case } from '../../case.js'; +import { allInputSources, Config, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic') + .specURL('https://www.w3.org/TR/WGSL/#unpack4xU8-builtin') + .desc( + ` +@const fn unpack4xU8(e: u32) -> vec4<u32> + ` + ) + .params(u => u.combine('inputSource', allInputSources)) + .fn(async t => { + const cfg: Config = t.params; + + const unpack4xU8 = (e: number) => { + const result: [number, number, number, number] = [0, 0, 0, 0]; + for (let i = 0; i < 4; ++i) { + result[i] = (e >> (8 * i)) & 0xff; + } + return result; + }; + + const testInputs = [0, 0x08060402, 0xffffffff, 0xfefdfcfb] as const; + + const makeCase = (e: number): Case => { + return { input: [u32(e)], expected: toVector(unpack4xU8(e), u32) }; + }; + const cases: Array<Case> = testInputs.flatMap(v => { + return [makeCase(v)]; + }); + + await run(t, builtin('unpack4xU8'), [Type.u32], Type.vec4u, cfg, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/workgroupUniformLoad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/workgroupUniformLoad.spec.ts new file mode 100644 index 0000000000..099b54146d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/workgroupUniformLoad.spec.ts @@ -0,0 +1,182 @@ +export const description = ` +Executes a control barrier synchronization function that affects memory and atomic operations in the workgroup address space. +`; + +// NOTE: The control barrier executed by this builtin is tested in the memory_model tests. + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { + TypedArrayBufferView, + TypedArrayBufferViewConstructor, + iterRange, +} from '../../../../../../common/util/util.js'; +import { GPUTest } from '../../../../../gpu_test.js'; +import { checkElementsEqualGenerated } from '../../../../../util/check_contents.js'; + +export const g = makeTestGroup(GPUTest); + +interface TypeConfig { + // The value to store the workgroup variable. + store_val: string; + // The expected values once the variable has been copied back to the host. + expected: TypedArrayBufferView; + // The type used for the host-visible buffer, if different from the workgroup variable. + host_type?: string; + // A type conversion function, if the types are different. + to_host?: (x: string) => string; + // Any additional module-scope declarations needed by the type. + decls?: string; +} + +// A list of types configurations used for the workgroup variable. +const kTypes: Record<string, TypeConfig> = { + bool: { + store_val: `true`, + expected: new Uint32Array([1]), + host_type: 'u32', + to_host: (x: string) => `u32(${x})`, + }, + u32: { + store_val: `42`, + expected: new Uint32Array([42]), + }, + vec4u: { + store_val: `vec4u(42, 1, 0xffffffff, 777)`, + expected: new Uint32Array([42, 1, 0xffffffff, 777]), + }, + mat3x2f: { + store_val: `mat3x2(42, 1, 65536, -42, -1, -65536)`, + expected: new Float32Array([42, 1, 65536, -42, -1, -65536]), + }, + 'array<u32, 4>': { + store_val: `array(42, 1, 0xffffffff, 777)`, + expected: new Uint32Array([42, 1, 0xffffffff, 777]), + }, + SimpleStruct: { + decls: 'struct SimpleStruct { a: u32, b: u32, c: u32, d: u32, }', + store_val: `SimpleStruct(42, 1, 0xffffffff, 777)`, + expected: new Uint32Array([42, 1, 0xffffffff, 777]), + }, + ComplexStruct: { + decls: `struct Inner { v: vec4u, } + struct ComplexStruct { + a: array<Inner, 4>, + @size(28) b: vec4u, + c: u32 + } + const v = vec4(42, 1, 0xffffffff, 777); + const rhs = ComplexStruct( + array(Inner(v.xyzw), Inner(v.yzwx), Inner(v.zwxy), Inner(v.wxyz)), + v.xzxz, + 0x12345678, + );`, + store_val: `rhs`, + expected: new Uint32Array([ + // v.xyzw + 42, 1, 0xffffffff, 777, + // v.yzwx + 1, 0xffffffff, 777, 42, + // v.zwxy + 0xffffffff, 777, 42, 1, + // v.wxyz + 777, 42, 1, 0xffffffff, + // v.xzxz + 42, 0xffffffff, 42, 0xffffffff, + // 12 bytes of padding + 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0x12345678, + ]), + }, +}; + +g.test('types') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#workgroupUniformLoad-builtin') + .desc( + `Test that the result of a workgroupUniformLoad is the value previously stored to the workgroup variable, for a variety of types. + ` + ) + .params(u => + u.combine('type', keysOf(kTypes)).combine('wgsize', [ + [1, 1], + [3, 7], + [1, 128], + [16, 16], + ]) + ) + .fn(t => { + const type = kTypes[t.params.type]; + const wgsize_x = t.params.wgsize[0]; + const wgsize_y = t.params.wgsize[1]; + const num_invocations = wgsize_x * wgsize_y; + const num_words_per_invocation = type.expected.length; + const total_host_words = num_invocations * num_words_per_invocation; + + t.skipIf( + num_invocations > t.device.limits.maxComputeInvocationsPerWorkgroup, + `num_invocations (${num_invocations}) > maxComputeInvocationsPerWorkgroup (${t.device.limits.maxComputeInvocationsPerWorkgroup})` + ); + + let load = `workgroupUniformLoad(&wgvar)`; + if (type.to_host) { + load = type.to_host(load); + } + + // Construct a shader that stores a value to workgroup variable and then loads it using + // workgroupUniformLoad() in every invocation, copying the results back to a storage buffer. + const code = ` + ${type.decls ? type.decls : ''} + + @group(0) @binding(0) var<storage, read_write> buffer : array<${ + type.host_type ? type.host_type : t.params.type + }, ${num_invocations}>; + + var<workgroup> wgvar : ${t.params.type}; + + @compute @workgroup_size(${wgsize_x}, ${wgsize_y}) + fn main(@builtin(local_invocation_index) lid: u32) { + if (lid == ${num_invocations - 1}) { + wgvar = ${type.store_val}; + } + buffer[lid] = ${load}; + } + `; + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ code }), + entryPoint: 'main', + }, + }); + + // Allocate a buffer and fill it with 0xdeadbeef values. + const outputBuffer = t.makeBufferWithContents( + new Uint32Array([...iterRange(total_host_words, _i => 0xdeadbeef)]), + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC + ); + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [{ binding: 0, resource: { buffer: outputBuffer } }], + }); + + // Run the shader. + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(1); + pass.end(); + t.queue.submit([encoder.finish()]); + + // Check that the output matches the expected values for each invocation. + t.expectGPUBufferValuesPassCheck( + outputBuffer, + data => + checkElementsEqualGenerated(data, i => { + return Number(type.expected[i % num_words_per_invocation]); + }), + { + type: type.expected.constructor as TypedArrayBufferViewConstructor, + typedLength: total_host_words, + } + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/user/ptr_params.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/user/ptr_params.spec.ts new file mode 100644 index 0000000000..87c3b9f2e1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/user/ptr_params.spec.ts @@ -0,0 +1,849 @@ +export const description = ` +User function call tests for pointer parameters. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../../gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +function wgslTypeDecl(kind: 'vec4i' | 'array' | 'struct') { + switch (kind) { + case 'vec4i': + return ` +alias T = vec4i; +`; + case 'array': + return ` +alias T = array<vec4f, 3>; +`; + case 'struct': + return ` +struct S { +a : i32, +b : u32, +c : i32, +d : u32, +} +alias T = S; +`; + } +} + +function valuesForType(kind: 'vec4i' | 'array' | 'struct') { + switch (kind) { + case 'vec4i': + return new Uint32Array([1, 2, 3, 4]); + case 'array': + return new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + case 'struct': + return new Uint32Array([1, 2, 3, 4]); + } +} + +function run( + t: GPUTest, + wgsl: string, + inputUsage: 'uniform' | 'storage', + input: Uint32Array | Float32Array, + expected: Uint32Array | Float32Array +) { + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ code: wgsl }), + entryPoint: 'main', + }, + }); + + const inputBuffer = t.makeBufferWithContents( + input, + inputUsage === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE + ); + + const outputBuffer = t.device.createBuffer({ + size: expected.buffer.byteLength, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: inputBuffer } }, + { binding: 1, resource: { buffer: outputBuffer } }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(1); + pass.end(); + t.queue.submit([encoder.finish()]); + + t.expectGPUBufferValuesEqual(outputBuffer, expected); +} + +g.test('read_full_object') + .desc('Test a pointer parameter can be read by a callee function') + .params(u => + u + .combine('address_space', ['function', 'private', 'workgroup', 'storage', 'uniform'] as const) + .combine('call_indirection', [0, 1, 2] as const) + .combine('type', ['vec4i', 'array', 'struct'] as const) + ) + .fn(t => { + switch (t.params.address_space) { + case 'workgroup': + case 'storage': + case 'uniform': + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + + const main: string = { + function: ` +@compute @workgroup_size(1) +fn main() { + var F : T = input; + f0(&F); +} +`, + private: ` +var<private> P : T; +@compute @workgroup_size(1) +fn main() { + P = input; + f0(&P); +} +`, + workgroup: ` +var<workgroup> W : T; +@compute @workgroup_size(1) +fn main() { + W = input; + f0(&W); +} +`, + storage: ` +@compute @workgroup_size(1) +fn main() { + f0(&input); +} +`, + uniform: ` +@compute @workgroup_size(1) +fn main() { + f0(&input); +} +`, + }[t.params.address_space]; + + let call_chain = ''; + for (let i = 0; i < t.params.call_indirection; i++) { + call_chain += ` +fn f${i}(p : ptr<${t.params.address_space}, T>) { + f${i + 1}(p); +} +`; + } + + const inputVar: string = + t.params.address_space === 'uniform' + ? `@binding(0) @group(0) var<uniform> input : T;` + : `@binding(0) @group(0) var<storage, read> input : T;`; + + const wgsl = ` +${wgslTypeDecl(t.params.type)} + +${inputVar} + +@binding(1) @group(0) var<storage, read_write> output : T; + +fn f${t.params.call_indirection}(p : ptr<${t.params.address_space}, T>) { + output = *p; +} + +${call_chain} + +${main} +`; + + const values = valuesForType(t.params.type); + + run(t, wgsl, t.params.address_space === 'uniform' ? 'uniform' : 'storage', values, values); + }); + +g.test('read_ptr_to_member') + .desc('Test a pointer parameter to a member of a structure can be read by a callee function') + .params(u => + u.combine('address_space', ['function', 'private', 'workgroup', 'storage', 'uniform'] as const) + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const main: string = { + function: ` +@compute @workgroup_size(1) +fn main() { + var v : S = input; + output = f0(&v); +} +`, + private: ` +var<private> P : S; +@compute @workgroup_size(1) +fn main() { + P = input; + output = f0(&P); +} +`, + workgroup: ` +var<workgroup> W : S; +@compute @workgroup_size(1) +fn main() { + W = input; + output = f0(&W); +} +`, + storage: ` +@compute @workgroup_size(1) +fn main() { + output = f0(&input); +} +`, + uniform: ` +@compute @workgroup_size(1) +fn main() { + output = f0(&input); +} +`, + }[t.params.address_space]; + + const inputVar: string = + t.params.address_space === 'uniform' + ? `@binding(0) @group(0) var<uniform> input : S;` + : `@binding(0) @group(0) var<storage, read> input : S;`; + + const wgsl = ` +struct S { + a : vec4i, + b : T, + c : vec4i, +} + +struct T { + a : vec4i, + b : vec4i, +} + + +${inputVar} +@binding(1) @group(0) var<storage, read_write> output : T; + +fn f2(p : ptr<${t.params.address_space}, T>) -> T { + return *p; +} + +fn f1(p : ptr<${t.params.address_space}, S>) -> T { + return f2(&(*p).b); +} + +fn f0(p : ptr<${t.params.address_space}, S>) -> T { + return f1(p); +} + +${main} +`; + + // prettier-ignore + const input = new Uint32Array([ + /* S.a */ 1, 2, 3, 4, + /* S.b.a */ 5, 6, 7, 8, + /* S.b.b */ 9, 10, 11, 12, + /* S.c */ 13, 14, 15, 16, + ]); + + // prettier-ignore + const expected = new Uint32Array([ + /* S.b.a */ 5, 6, 7, 8, + /* S.b.b */ 9, 10, 11, 12, + ]); + + run(t, wgsl, t.params.address_space === 'uniform' ? 'uniform' : 'storage', input, expected); + }); + +g.test('read_ptr_to_element') + .desc('Test a pointer parameter to an element of an array can be read by a callee function') + .params(u => + u.combine('address_space', ['function', 'private', 'workgroup', 'storage', 'uniform'] as const) + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const main: string = { + function: ` +@compute @workgroup_size(1) +fn main() { + var v : T = input; + output = f0(&v); +} +`, + private: ` +var<private> P : T; +@compute @workgroup_size(1) +fn main() { + P = input; + output = f0(&P); +} +`, + workgroup: ` +var<workgroup> W : T; +@compute @workgroup_size(1) +fn main() { + W = input; + output = f0(&W); +} +`, + storage: ` +@compute @workgroup_size(1) +fn main() { + output = f0(&input); +} +`, + uniform: ` +@compute @workgroup_size(1) +fn main() { + output = f0(&input); +} +`, + }[t.params.address_space]; + + const inputVar: string = + t.params.address_space === 'uniform' + ? `@binding(0) @group(0) var<uniform> input : T;` + : `@binding(0) @group(0) var<storage, read> input : T;`; + + const wgsl = ` +alias T3 = vec4i; +alias T2 = array<T3, 2>; +alias T1 = array<T2, 3>; +alias T = array<T1, 2>; + +${inputVar} +@binding(1) @group(0) var<storage, read_write> output : T3; + +fn f2(p : ptr<${t.params.address_space}, T2>) -> T3 { + return (*p)[1]; +} + +fn f1(p : ptr<${t.params.address_space}, T1>) -> T3 { + return f2(&(*p)[0]) + f2(&(*p)[2]); +} + +fn f0(p : ptr<${t.params.address_space}, T>) -> T3 { + return f1(&(*p)[0]); +} + +${main} +`; + + // prettier-ignore + const input = new Uint32Array([ + /* [0][0][0] */ 1, 2, 3, 4, + /* [0][0][1] */ 5, 6, 7, 8, + /* [0][1][0] */ 9, 10, 11, 12, + /* [0][1][1] */ 13, 14, 15, 16, + /* [0][2][0] */ 17, 18, 19, 20, + /* [0][2][1] */ 21, 22, 23, 24, + /* [1][0][0] */ 25, 26, 27, 28, + /* [1][0][1] */ 29, 30, 31, 32, + /* [1][1][0] */ 33, 34, 35, 36, + /* [1][1][1] */ 37, 38, 39, 40, + /* [1][2][0] */ 41, 42, 43, 44, + /* [1][2][1] */ 45, 46, 47, 48, + ]); + const expected = new Uint32Array([/* [0][0][1] + [0][2][1] */ 5 + 21, 6 + 22, 7 + 23, 8 + 24]); + + run(t, wgsl, t.params.address_space === 'uniform' ? 'uniform' : 'storage', input, expected); + }); + +g.test('write_full_object') + .desc('Test a pointer parameter can be written to by a callee function') + .params(u => + u + .combine('address_space', ['function', 'private', 'workgroup', 'storage'] as const) + .combine('call_indirection', [0, 1, 2] as const) + .combine('type', ['vec4i', 'array', 'struct'] as const) + ) + .fn(t => { + switch (t.params.address_space) { + case 'workgroup': + case 'storage': + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + + const ptr = + t.params.address_space === 'storage' + ? `ptr<storage, T, read_write>` + : `ptr<${t.params.address_space}, T>`; + + const main: string = { + function: ` +@compute @workgroup_size(1) +fn main() { + var F : T; + f0(&F); + output = F; +} +`, + private: ` +var<private> P : T; +@compute @workgroup_size(1) +fn main() { + f0(&P); + output = P; +} +`, + workgroup: ` +var<workgroup> W : T; +@compute @workgroup_size(1) +fn main() { + f0(&W); + output = W; +} +`, + storage: ` +@compute @workgroup_size(1) +fn main() { + f0(&output); +} +`, + }[t.params.address_space]; + + let call_chain = ''; + for (let i = 0; i < t.params.call_indirection; i++) { + call_chain += ` +fn f${i}(p : ${ptr}) { + f${i + 1}(p); +} +`; + } + + const wgsl = ` +${wgslTypeDecl(t.params.type)} + +@binding(0) @group(0) var<uniform> input : T; +@binding(1) @group(0) var<storage, read_write> output : T; + +fn f${t.params.call_indirection}(p : ${ptr}) { + *p = input; +} + +${call_chain} + +${main} +`; + + const values = valuesForType(t.params.type); + + run(t, wgsl, 'uniform', values, values); + }); + +g.test('write_ptr_to_member') + .desc( + 'Test a pointer parameter to a member of a structure can be written to by a callee function' + ) + .params(u => u.combine('address_space', ['function', 'private', 'workgroup', 'storage'] as const)) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const main: string = { + function: ` +@compute @workgroup_size(1) +fn main() { + var v : S; + f0(&v); + output = v; +} +`, + private: ` +var<private> P : S; +@compute @workgroup_size(1) +fn main() { + f0(&P); + output = P; +} +`, + workgroup: ` +var<workgroup> W : S; +@compute @workgroup_size(1) +fn main() { + f0(&W); + output = W; +} +`, + storage: ` +@compute @workgroup_size(1) +fn main() { + f1(&output); +} +`, + }[t.params.address_space]; + + const ptr = (ty: string) => + t.params.address_space === 'storage' + ? `ptr<storage, ${ty}, read_write>` + : `ptr<${t.params.address_space}, ${ty}>`; + + const wgsl = ` +struct S { + a : vec4i, + b : T, + c : vec4i, +} + +struct T { + a : vec4i, + b : vec4i, +} + + +@binding(0) @group(0) var<storage> input : T; +@binding(1) @group(0) var<storage, read_write> output : S; + +fn f2(p : ${ptr('T')}) { + *p = input; +} + +fn f1(p : ${ptr('S')}) { + f2(&(*p).b); +} + +fn f0(p : ${ptr('S')}) { + f1(p); +} + +${main} +`; + + // prettier-ignore + const input = new Uint32Array([ + /* S.b.a */ 5, 6, 7, 8, + /* S.b.b */ 9, 10, 11, 12, + ]); + + // prettier-ignore + const expected = new Uint32Array([ + /* S.a */ 0, 0, 0, 0, + /* S.b.a */ 5, 6, 7, 8, + /* S.b.b */ 9, 10, 11, 12, + /* S.c */ 0, 0, 0, 0, + ]); + + run(t, wgsl, 'storage', input, expected); + }); + +g.test('write_ptr_to_element') + .desc('Test a pointer parameter to an element of an array can be written to by a callee function') + .params(u => u.combine('address_space', ['function', 'private', 'workgroup', 'storage'] as const)) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const main: string = { + function: ` +@compute @workgroup_size(1) +fn main() { + var v : T; + f0(&v); + output = v; +} +`, + private: ` +var<private> P : T; +@compute @workgroup_size(1) +fn main() { + f0(&P); + output = P; +} +`, + workgroup: ` +var<workgroup> W : T; +@compute @workgroup_size(1) +fn main() { + f0(&W); + output = W; +} +`, + storage: ` +@compute @workgroup_size(1) +fn main() { + f0(&output); +} +`, + }[t.params.address_space]; + + const ptr = (ty: string) => + t.params.address_space === 'storage' + ? `ptr<storage, ${ty}, read_write>` + : `ptr<${t.params.address_space}, ${ty}>`; + + const wgsl = ` +alias T3 = vec4i; +alias T2 = array<T3, 2>; +alias T1 = array<T2, 3>; +alias T = array<T1, 2>; + +@binding(0) @group(0) var<storage, read> input : T3; +@binding(1) @group(0) var<storage, read_write> output : T; + +fn f2(p : ${ptr('T2')}) { + (*p)[1] = input; +} + +fn f1(p : ${ptr('T1')}) { + f2(&(*p)[0]); + f2(&(*p)[2]); +} + +fn f0(p : ${ptr('T')}) { + f1(&(*p)[0]); +} + +${main} +`; + + const input = new Uint32Array([1, 2, 3, 4]); + + // prettier-ignore + const expected = new Uint32Array([ + /* [0][0][0] */ 0, 0, 0, 0, + /* [0][0][1] */ 1, 2, 3, 4, + /* [0][1][0] */ 0, 0, 0, 0, + /* [0][1][1] */ 0, 0, 0, 0, + /* [0][2][0] */ 0, 0, 0, 0, + /* [0][2][1] */ 1, 2, 3, 4, + /* [1][0][0] */ 0, 0, 0, 0, + /* [1][0][1] */ 0, 0, 0, 0, + /* [1][1][0] */ 0, 0, 0, 0, + /* [1][1][1] */ 0, 0, 0, 0, + /* [1][2][0] */ 0, 0, 0, 0, + /* [1][2][1] */ 0, 0, 0, 0, + ]); + + run(t, wgsl, 'storage', input, expected); + }); + +g.test('atomic_ptr_to_element') + .desc( + 'Test a pointer parameter to an atomic<i32> of an array can be read from and written to by a callee function' + ) + .params(u => u.combine('address_space', ['workgroup', 'storage'] as const)) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const main: string = { + workgroup: ` +var<workgroup> W_input : T; +var<workgroup> W_output : T; +@compute @workgroup_size(1) +fn main() { + // Copy input -> W_input + for (var i = 0; i < 2; i++) { + for (var j = 0; j < 3; j++) { + for (var k = 0; k < 2; k++) { + atomicStore(&W_input[k][j][i], atomicLoad(&input[k][j][i])); + } + } + } + + f0(&W_input, &W_output); + + // Copy W_output -> output + for (var i = 0; i < 2; i++) { + for (var j = 0; j < 3; j++) { + for (var k = 0; k < 2; k++) { + atomicStore(&output[k][j][i], atomicLoad(&W_output[k][j][i])); + } + } + } +} +`, + storage: ` +@compute @workgroup_size(1) +fn main() { + f0(&input, &output); +} +`, + }[t.params.address_space]; + + const ptr = (ty: string) => + t.params.address_space === 'storage' + ? `ptr<storage, ${ty}, read_write>` + : `ptr<${t.params.address_space}, ${ty}>`; + + const wgsl = ` +alias T3 = atomic<i32>; +alias T2 = array<T3, 2>; +alias T1 = array<T2, 3>; +alias T = array<T1, 2>; + +@binding(0) @group(0) var<storage, read_write> input : T; +@binding(1) @group(0) var<storage, read_write> output : T; + +fn f2(in : ${ptr('T2')}, out : ${ptr('T2')}) { + let v = atomicLoad(&(*in)[0]); + atomicStore(&(*out)[1], v); +} + +fn f1(in : ${ptr('T1')}, out : ${ptr('T1')}) { + f2(&(*in)[0], &(*out)[1]); + f2(&(*in)[2], &(*out)[0]); +} + +fn f0(in : ${ptr('T')}, out : ${ptr('T')}) { + f1(&(*in)[1], &(*out)[0]); +} + +${main} +`; + + // prettier-ignore + const input = new Uint32Array([ + /* [0][0][0] */ 1, + /* [0][0][1] */ 2, + /* [0][1][0] */ 3, + /* [0][1][1] */ 4, + /* [0][2][0] */ 5, + /* [0][2][1] */ 6, + /* [1][0][0] */ 7, // -> [0][1][1] + /* [1][0][1] */ 8, + /* [1][1][0] */ 9, + /* [1][1][1] */ 10, + /* [1][2][0] */ 11, // -> [0][0][1] + /* [1][2][1] */ 12, + ]); + + // prettier-ignore + const expected = new Uint32Array([ + /* [0][0][0] */ 0, + /* [0][0][1] */ 11, + /* [0][1][0] */ 0, + /* [0][1][1] */ 7, + /* [0][2][0] */ 0, + /* [0][2][1] */ 0, + /* [1][0][0] */ 0, + /* [1][0][1] */ 0, + /* [1][1][0] */ 0, + /* [1][1][1] */ 0, + /* [1][2][0] */ 0, + /* [1][2][1] */ 0, + ]); + + run(t, wgsl, 'storage', input, expected); + }); + +g.test('array_length') + .desc( + 'Test a pointer parameter to a runtime sized array can be used by arrayLength() in a callee function' + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const wgsl = ` +@binding(0) @group(0) var<storage, read> arr : array<u32>; +@binding(1) @group(0) var<storage, read_write> output : u32; + +fn f2(p : ptr<storage, array<u32>, read>) -> u32 { + return arrayLength(p); +} + +fn f1(p : ptr<storage, array<u32>, read>) -> u32 { + return f2(p); +} + +fn f0(p : ptr<storage, array<u32>, read>) -> u32 { + return f1(p); +} + +@compute @workgroup_size(1) +fn main() { + output = f0(&arr); +} +`; + + const input = new Uint32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + const expected = new Uint32Array([12]); + + run(t, wgsl, 'storage', input, expected); + }); + +g.test('mixed_ptr_parameters') + .desc('Test that functions can accept multiple, mixed pointer parameters') + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const wgsl = ` +@binding(0) @group(0) var<uniform> input : array<vec4i, 4>; +@binding(1) @group(0) var<storage, read_write> output : array<vec4i, 4>; + +fn sum(f : ptr<function, i32>, + w : ptr<workgroup, atomic<i32>>, + p : ptr<private, i32>, + u : ptr<uniform, vec4i>) -> vec4i { + + return vec4(*f + atomicLoad(w) + *p) + *u; +} + +struct S { + i : i32, +} + +var<private> P0 = S(0); +var<private> P1 = S(10); +var<private> P2 = 20; +var<private> P3 = 30; + +struct T { + i : atomic<i32>, +} + +var<workgroup> W0 : T; +var<workgroup> W1 : atomic<i32>; +var<workgroup> W2 : T; +var<workgroup> W3 : atomic<i32>; + +@compute @workgroup_size(1) +fn main() { + atomicStore(&W0.i, 0); + atomicStore(&W1, 100); + atomicStore(&W2.i, 200); + atomicStore(&W3, 300); + + var F = array(0, 1000, 2000, 3000); + + output[0] = sum(&F[2], &W3, &P1.i, &input[0]); // vec4(2310) + vec4(1, 2, 3, 4) + output[1] = sum(&F[1], &W2.i, &P0.i, &input[1]); // vec4(1200) + vec4(4, 3, 2, 1) + output[2] = sum(&F[3], &W0.i, &P3, &input[2]); // vec4(3030) + vec4(2, 4, 1, 3) + output[3] = sum(&F[2], &W1, &P2, &input[3]); // vec4(2120) + vec4(4, 1, 2, 3) +} +`; + + // prettier-ignore + const input = new Uint32Array([ + /* [0] */ 1, 2, 3, 4, + /* [1] */ 4, 3, 2, 1, + /* [2] */ 2, 4, 1, 3, + /* [3] */ 4, 1, 2, 3, + ]); + + // prettier-ignore + const expected = new Uint32Array([ + /* [0] */ 2311, 2312, 2313, 2314, + /* [1] */ 1204, 1203, 1202, 1201, + /* [2] */ 3032, 3034, 3031, 3033, + /* [3] */ 2124, 2121, 2122, 2123, + ]); + + run(t, wgsl, 'uniform', input, expected); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case.ts new file mode 100644 index 0000000000..d837b1c32f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case.ts @@ -0,0 +1,440 @@ +import { crc32 } from '../../../../common/util/crc32.js'; +import { ROArrayArray } from '../../../../common/util/types.js'; +import { assert } from '../../../../common/util/util.js'; +import { + abstractInt, + i32, + ScalarBuilder, + u32, + Value, + VectorValue, +} from '../../../util/conversion.js'; +import { + cartesianProduct, + QuantizeFunc, + quantizeToI32, + quantizeToI64, + quantizeToU32, +} from '../../../util/math.js'; + +import { Expectation } from './expectation.js'; + +function notUndefined<T>(value: T | undefined): value is T { + return value !== undefined; +} + +/** Case is a single expression test case. */ +export type Case = { + // The input value(s) + input: Value | ReadonlyArray<Value>; + // The expected result, or function to check the result + expected: Expectation; +}; + +/** + * Filters a given set of Cases down to a target number of cases by + * randomly selecting which Cases to return. + * + * The selection algorithm is deterministic and stable for a case's + * inputs. + * + * This means that if a specific case is selected is not affected by the + * presence of other cases in the list, so in theory it is possible to create a + * pathological set of cases such that all or not of the cases are selected + * in spite of the target number. + * + * This is a trade-off from guaranteeing stability of the selected cases over + * small changes, so the target number of cases is more of a suggestion. It is + * still guaranteed that if you set n0 < n1, then the invocation with n0 will + * return at most the number of cases that n1 does, it just isn't guaranteed to + * be less. + * + * @param dis is a string provided for additional hashing information to avoid + * systemic bias in the selection process across different test + * suites. Specifically every Case with the same input values being + * included or skipped regardless of the operation that they are + * testing. This string should be something like the name of the case + * cache the values are for or the operation under test. + * @param n number of cases targeted be returned. Expected to be a positive + * integer. If equal or greater than the number of cases, then all the + * cases are returned. 0 is not allowed, since it is likely a + * programming error, because if the caller intentionally wants 0 + * items, they can just use []. + * @param cases list of Cases to be selected from. + */ +export function selectNCases(dis: string, n: number, cases: Case[]): Case[] { + assert(n > 0 && Math.round(n) === n, `n ${n} is expected to be a positive integer`); + const count = cases.length; + if (n >= count) { + return cases; + } + const dis_crc32 = crc32(dis); + return cases.filter( + c => Math.trunc((n / count) * 0xffff_ffff) > (crc32(c.input.toString()) ^ dis_crc32) >>> 0 + ); +} + +/** + * A function that performs a binary operation on x and y, and returns the + * expected result. + */ +export interface BinaryOp<T> { + (x: T, y: T): T | undefined; +} + +/** + * A function that performs a vector-vector operation on x and y, and returns the + * expected result. + */ +export interface VectorVectorToScalarOp<T> { + (x: T[], y: T[]): T | undefined; +} + +/** + * @returns a Case for the input params with op applied + * @param scalar scalar param + * @param vector vector param (2, 3, or 4 elements) + * @param op the op to apply to scalar and vector + * @param quantize function to quantize all values in vectors and scalars + * @param scalarize function to convert numbers to Scalars + */ +function makeScalarVectorBinaryToVectorCase<T>( + scalar: T, + vector: readonly T[], + op: BinaryOp<T>, + quantize: QuantizeFunc<T>, + scalarize: ScalarBuilder<T> +): Case | undefined { + scalar = quantize(scalar); + vector = vector.map(quantize); + const result = vector.map(v => op(scalar, v)); + if (result.includes(undefined)) { + return undefined; + } + return { + input: [scalarize(scalar), new VectorValue(vector.map(scalarize))], + expected: new VectorValue(result.filter(notUndefined).map(scalarize)), + }; +} + +/** + * @returns array of Case for the input params with op applied + * @param scalars array of scalar params + * @param vectors array of vector params (2, 3, or 4 elements) + * @param op the op to apply to each pair of scalar and vector + * @param quantize function to quantize all values in vectors and scalars + * @param scalarize function to convert numbers to Scalars + */ +function generateScalarVectorBinaryToVectorCases<T>( + scalars: readonly T[], + vectors: ROArrayArray<T>, + op: BinaryOp<T>, + quantize: QuantizeFunc<T>, + scalarize: ScalarBuilder<T> +): Case[] { + return scalars.flatMap(s => { + return vectors + .map(v => { + return makeScalarVectorBinaryToVectorCase(s, v, op, quantize, scalarize); + }) + .filter(notUndefined); + }); +} + +/** + * @returns a Case for the input params with op applied + * @param vector vector param (2, 3, or 4 elements) + * @param scalar scalar param + * @param op the op to apply to vector and scalar + * @param quantize function to quantize all values in vectors and scalars + * @param scalarize function to convert numbers to Scalars + */ +function makeVectorScalarBinaryToVectorCase<T>( + vector: readonly T[], + scalar: T, + op: BinaryOp<T>, + quantize: QuantizeFunc<T>, + scalarize: ScalarBuilder<T> +): Case | undefined { + vector = vector.map(quantize); + scalar = quantize(scalar); + const result = vector.map(v => op(v, scalar)); + if (result.includes(undefined)) { + return undefined; + } + return { + input: [new VectorValue(vector.map(scalarize)), scalarize(scalar)], + expected: new VectorValue(result.filter(notUndefined).map(scalarize)), + }; +} + +/** + * @returns array of Case for the input params with op applied + * @param vectors array of vector params (2, 3, or 4 elements) + * @param scalars array of scalar params + * @param op the op to apply to each pair of vector and scalar + * @param quantize function to quantize all values in vectors and scalars + * @param scalarize function to convert numbers to Scalars + */ +function generateVectorScalarBinaryToVectorCases<T>( + vectors: ROArrayArray<T>, + scalars: readonly T[], + op: BinaryOp<T>, + quantize: QuantizeFunc<T>, + scalarize: ScalarBuilder<T> +): Case[] { + return scalars.flatMap(s => { + return vectors + .map(v => { + return makeVectorScalarBinaryToVectorCase(v, s, op, quantize, scalarize); + }) + .filter(notUndefined); + }); +} + +/** + * @returns array of Case for the input params with op applied + * @param scalars array of scalar params + * @param vectors array of vector params (2, 3, or 4 elements) + * @param op he op to apply to each pair of scalar and vector + */ +export function generateU32VectorBinaryToVectorCases( + scalars: readonly number[], + vectors: ROArrayArray<number>, + op: BinaryOp<number> +): Case[] { + return generateScalarVectorBinaryToVectorCases(scalars, vectors, op, quantizeToU32, u32); +} + +/** + * @returns array of Case for the input params with op applied + * @param vectors array of vector params (2, 3, or 4 elements) + * @param scalars array of scalar params + * @param op he op to apply to each pair of vector and scalar + */ +export function generateVectorU32BinaryToVectorCases( + vectors: ROArrayArray<number>, + scalars: readonly number[], + op: BinaryOp<number> +): Case[] { + return generateVectorScalarBinaryToVectorCases(vectors, scalars, op, quantizeToU32, u32); +} + +/** + * @returns array of Case for the input params with op applied + * @param scalars array of scalar params + * @param vectors array of vector params (2, 3, or 4 elements) + * @param op he op to apply to each pair of scalar and vector + */ +export function generateI32VectorBinaryToVectorCases( + scalars: readonly number[], + vectors: ROArrayArray<number>, + op: BinaryOp<number> +): Case[] { + return generateScalarVectorBinaryToVectorCases(scalars, vectors, op, quantizeToI32, i32); +} + +/** + * @returns array of Case for the input params with op applied + * @param vectors array of vector params (2, 3, or 4 elements) + * @param scalars array of scalar params + * @param op he op to apply to each pair of vector and scalar + */ +export function generateVectorI32BinaryToVectorCases( + vectors: ROArrayArray<number>, + scalars: readonly number[], + op: BinaryOp<number> +): Case[] { + return generateVectorScalarBinaryToVectorCases(vectors, scalars, op, quantizeToI32, i32); +} + +/** + * @returns array of Case for the input params with op applied + * @param scalars array of scalar params + * @param vectors array of vector params (2, 3, or 4 elements) + * @param op he op to apply to each pair of scalar and vector + */ +export function generateI64VectorBinaryToVectorCases( + scalars: readonly bigint[], + vectors: ROArrayArray<bigint>, + op: BinaryOp<bigint> +): Case[] { + return generateScalarVectorBinaryToVectorCases(scalars, vectors, op, quantizeToI64, abstractInt); +} + +/** + * @returns array of Case for the input params with op applied + * @param vectors array of vector params (2, 3, or 4 elements) + * @param scalars array of scalar params + * @param op he op to apply to each pair of vector and scalar + */ +export function generateVectorI64BinaryToVectorCases( + vectors: ROArrayArray<bigint>, + scalars: readonly bigint[], + op: BinaryOp<bigint> +): Case[] { + return generateVectorScalarBinaryToVectorCases(vectors, scalars, op, quantizeToI64, abstractInt); +} + +/** + * @returns array of Case for the input params with op applied + * @param param0s array of inputs to try for the first param + * @param param1s array of inputs to try for the second param + * @param op callback called on each pair of inputs to produce each case + * @param quantize function to quantize all values + * @param scalarize function to convert numbers to Scalars + */ +function generateScalarBinaryToScalarCases<T>( + param0s: readonly T[], + param1s: readonly T[], + op: BinaryOp<T>, + quantize: QuantizeFunc<T>, + scalarize: ScalarBuilder<T> +): Case[] { + param0s = param0s.map(quantize); + param1s = param1s.map(quantize); + return cartesianProduct(param0s, param1s).reduce((cases, e) => { + const expected = op(e[0], e[1]); + if (expected !== undefined) { + cases.push({ input: [scalarize(e[0]), scalarize(e[1])], expected: scalarize(expected) }); + } + return cases; + }, new Array<Case>()); +} + +/** + * @returns an array of Cases for operations over a range of inputs + * @param param0s array of inputs to try for the first param + * @param param1s array of inputs to try for the second param + * @param op callback called on each pair of inputs to produce each case + */ +export function generateBinaryToI32Cases( + param0s: readonly number[], + param1s: readonly number[], + op: BinaryOp<number> +) { + return generateScalarBinaryToScalarCases(param0s, param1s, op, quantizeToI32, i32); +} + +/** + * @returns an array of Cases for operations over a range of inputs + * @param param0s array of inputs to try for the first param + * @param param1s array of inputs to try for the second param + * @param op callback called on each pair of inputs to produce each case + */ +export function generateBinaryToU32Cases( + param0s: readonly number[], + param1s: readonly number[], + op: BinaryOp<number> +) { + return generateScalarBinaryToScalarCases(param0s, param1s, op, quantizeToU32, u32); +} + +/** + * @returns an array of Cases for operations over a range of inputs + * @param param0s array of inputs to try for the first param + * @param param1s array of inputs to try for the second param + * @param op callback called on each pair of inputs to produce each case + */ +export function generateBinaryToI64Cases( + param0s: readonly bigint[], + param1s: readonly bigint[], + op: BinaryOp<bigint> +) { + return generateScalarBinaryToScalarCases(param0s, param1s, op, quantizeToI64, abstractInt); +} + +/** + * @returns a Case for the input params with op applied + * @param param0 vector param (2, 3, or 4 elements) for the first param + * @param param1 vector param (2, 3, or 4 elements) for the second param + * @param op the op to apply to each pair of vectors + * @param quantize function to quantize all values in vectors and scalars + * @param scalarize function to convert numbers to Scalars + */ +function makeVectorVectorToScalarCase<T>( + param0: readonly T[], + param1: readonly T[], + op: VectorVectorToScalarOp<T>, + quantize: QuantizeFunc<T>, + scalarize: ScalarBuilder<T> +): Case | undefined { + const param0_quantized = param0.map(quantize); + const param1_quantized = param1.map(quantize); + const result = op(param0_quantized, param1_quantized); + if (result === undefined) return undefined; + + return { + input: [ + new VectorValue(param0_quantized.map(scalarize)), + new VectorValue(param1_quantized.map(scalarize)), + ], + expected: scalarize(result), + }; +} + +/** + * @returns array of Case for the input params with op applied + * @param param0s array of vector params (2, 3, or 4 elements) for the first param + * @param param1s array of vector params (2, 3, or 4 elements) for the second param + * @param op the op to apply to each pair of vectors + * @param quantize function to quantize all values in vectors and scalars + * @param scalarize function to convert numbers to Scalars + */ +function generateVectorVectorToScalarCases<T>( + param0s: ROArrayArray<T>, + param1s: ROArrayArray<T>, + op: VectorVectorToScalarOp<T>, + quantize: QuantizeFunc<T>, + scalarize: ScalarBuilder<T> +): Case[] { + return param0s.flatMap(param0 => { + return param1s + .map(param1 => { + return makeVectorVectorToScalarCase(param0, param1, op, quantize, scalarize); + }) + .filter(notUndefined); + }); +} + +/** + * @returns array of Case for the input params with op applied + * @param param0s array of vector params (2, 3, or 4 elements) for the first param + * @param param1s array of vector params (2, 3, or 4 elements) for the second param + * @param op the op to apply to each pair of vectors + */ +export function generateVectorVectorToI32Cases( + param0s: ROArrayArray<number>, + param1s: ROArrayArray<number>, + op: VectorVectorToScalarOp<number> +): Case[] { + return generateVectorVectorToScalarCases(param0s, param1s, op, quantizeToI32, i32); +} + +/** + * @returns array of Case for the input params with op applied + * @param param0s array of vector params (2, 3, or 4 elements) for the first param + * @param param1s array of vector params (2, 3, or 4 elements) for the second param + * @param op the op to apply to each pair of vectors + */ +export function generateVectorVectorToU32Cases( + param0s: ROArrayArray<number>, + param1s: ROArrayArray<number>, + op: VectorVectorToScalarOp<number> +): Case[] { + return generateVectorVectorToScalarCases(param0s, param1s, op, quantizeToU32, u32); +} + +/** + * @returns array of Case for the input params with op applied + * @param param0s array of vector params (2, 3, or 4 elements) for the first param + * @param param1s array of vector params (2, 3, or 4 elements) for the second param + * @param op the op to apply to each pair of vectors + */ +export function generateVectorVectorToI64Cases( + param0s: ROArrayArray<bigint>, + param1s: ROArrayArray<bigint>, + op: VectorVectorToScalarOp<bigint> +): Case[] { + return generateVectorVectorToScalarCases(param0s, param1s, op, quantizeToI64, abstractInt); +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts index ff82792d64..345183c5d5 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts @@ -3,21 +3,22 @@ import { unreachable } from '../../../../common/util/util.js'; import BinaryStream from '../../../util/binary_stream.js'; import { deserializeComparator, serializeComparator } from '../../../util/compare.js'; import { - Scalar, - Vector, - serializeValue, - deserializeValue, - Matrix, + MatrixValue, Value, + VectorValue, + deserializeValue, + isScalarValue, + serializeValue, } from '../../../util/conversion.js'; import { - deserializeFPInterval, FPInterval, + deserializeFPInterval, serializeFPInterval, } from '../../../util/floating_point.js'; import { flatten2DArray, unflatten2DArray } from '../../../util/math.js'; -import { Case, CaseList, Expectation, isComparator } from './expression.js'; +import { Case } from './case.js'; +import { Expectation, isComparator } from './expectation.js'; enum SerializedExpectationKind { Value, @@ -30,7 +31,7 @@ enum SerializedExpectationKind { /** serializeExpectation() serializes an Expectation to a BinaryStream */ export function serializeExpectation(s: BinaryStream, e: Expectation) { - if (e instanceof Scalar || e instanceof Vector || e instanceof Matrix) { + if (isScalarValue(e) || e instanceof VectorValue || e instanceof MatrixValue) { s.writeU8(SerializedExpectationKind.Value); serializeValue(s, e); return; @@ -122,27 +123,27 @@ export function deserializeCase(s: BinaryStream): Case { return { input, expected }; } -/** CaseListBuilder is a function that builds a CaseList */ -export type CaseListBuilder = () => CaseList; +/** CaseListBuilder is a function that builds a list of cases, Case[] */ +export type CaseListBuilder = () => Case[]; /** - * CaseCache is a cache of CaseList. + * CaseCache is a cache of Case[]. * CaseCache implements the Cacheable interface, so the cases can be pre-built * and stored in the data cache, reducing computation costs at CTS runtime. */ -export class CaseCache implements Cacheable<Record<string, CaseList>> { +export class CaseCache implements Cacheable<Record<string, Case[]>> { /** * Constructor * @param name the name of the cache. This must be globally unique. * @param builders a Record of case-list name to case-list builder. */ constructor(name: string, builders: Record<string, CaseListBuilder>) { - this.path = `webgpu/shader/execution/case-cache/${name}.bin`; + this.path = `webgpu/shader/execution/${name}.bin`; this.builders = builders; } /** get() returns the list of cases with the given name */ - public async get(name: string): Promise<CaseList> { + public async get(name: string): Promise<Case[]> { const data = await dataCache.fetch(this); return data[name]; } @@ -151,8 +152,8 @@ export class CaseCache implements Cacheable<Record<string, CaseList>> { * build() implements the Cacheable.build interface. * @returns the data. */ - build(): Promise<Record<string, CaseList>> { - const built: Record<string, CaseList> = {}; + build(): Promise<Record<string, Case[]>> { + const built: Record<string, Case[]> = {}; for (const name in this.builders) { const cases = this.builders[name](); built[name] = cases; @@ -164,7 +165,7 @@ export class CaseCache implements Cacheable<Record<string, CaseList>> { * serialize() implements the Cacheable.serialize interface. * @returns the serialized data. */ - serialize(data: Record<string, CaseList>): Uint8Array { + serialize(data: Record<string, Case[]>): Uint8Array { const maxSize = 32 << 20; // 32MB - max size for a file const stream = new BinaryStream(new ArrayBuffer(maxSize)); stream.writeU32(Object.keys(data).length); @@ -179,9 +180,9 @@ export class CaseCache implements Cacheable<Record<string, CaseList>> { * deserialize() implements the Cacheable.deserialize interface. * @returns the deserialize data. */ - deserialize(array: Uint8Array): Record<string, CaseList> { + deserialize(array: Uint8Array): Record<string, Case[]> { const s = new BinaryStream(array.buffer); - const casesByName: Record<string, CaseList> = {}; + const casesByName: Record<string, Case[]> = {}; const numRecords = s.readU32(); for (let i = 0; i < numRecords; i++) { const name = s.readString(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/constructor/non_zero.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/constructor/non_zero.spec.ts new file mode 100644 index 0000000000..c28d3a8edb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/constructor/non_zero.spec.ts @@ -0,0 +1,797 @@ +export const description = ` +Execution Tests for value constructors from components +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { + ArrayValue, + MatrixType, + ScalarKind, + Type, + Value, + VectorType, + VectorValue, + scalarTypeOf, + vec2, + vec3, +} from '../../../../util/conversion.js'; +import { FP } from '../../../../util/floating_point.js'; +import { allInputSources, basicExpressionBuilder, run } from '../expression.js'; + +export const g = makeTestGroup(GPUTest); + +/** @returns true if 'v' is 'min' or 'max' */ +function isMinOrMax(v: number | 'min' | 'max') { + return v === 'min' || v === 'max'; +} + +/** A list of concrete types to test for the given abstract-numeric type */ +const kConcreteTypesForAbstractType = { + 'abstract-float': ['f32', 'f16'] as const, + 'abstract-int': ['f32', 'f16', 'i32', 'u32'] as const, + 'vec3<abstract-int>': ['vec3f', 'vec3h', 'vec3i', 'vec3u'] as const, + 'vec4<abstract-float>': ['vec4f', 'vec4h'] as const, + 'mat2x3<abstract-float>': ['mat2x3f', 'mat2x3h'] as const, +}; + +/** + * @returns the lowest finite value for 'kind' if 'v' is 'min', + * the highest finite value for 'kind' if 'v' is 'max', + * otherwise returns 'v' + */ +function valueFor(v: number | 'min' | 'max', kind: 'bool' | 'i32' | 'u32' | 'f32' | 'f16') { + if (!isMinOrMax(v)) { + return v as number; + } + switch (kind) { + case 'bool': + return v === 'min' ? 0 : 1; + case 'i32': + return v === 'min' ? -0x80000000 : 0x7fffffff; + case 'u32': + return v === 'min' ? 0 : 0xffffffff; + case 'f32': + return v === 'min' ? FP['f32'].constants().negative.min : FP['f32'].constants().positive.max; + case 'f16': + return v === 'min' ? FP['f16'].constants().negative.min : FP['f16'].constants().positive.max; + } +} + +g.test('scalar_identity') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a scalar constructed from a value of the same type produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16'] as const) + .combine('value', ['min', 'max', 1, 2, 5, 100] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + t.skipIf(t.params.type === 'bool' && !isMinOrMax(t.params.value)); + }) + .fn(async t => { + const type = Type[t.params.type]; + const value = valueFor(t.params.value, t.params.type); + await run( + t, + basicExpressionBuilder(ops => `${type}(${ops[0]})`), + [type], + type, + t.params, + [{ input: [type.create(value)], expected: type.create(value) }] + ); + }); + +g.test('vector_identity') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a vector constructed from a value of the same type produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16'] as const) + .combine('width', [2, 3, 4] as const) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.type]; + const vectorType = Type.vec(t.params.width, elementType); + const elements: number[] = []; + const fn = t.params.infer_type ? `vec${t.params.width}` : `${vectorType}`; + for (let i = 0; i < t.params.width; i++) { + if (t.params.type === 'bool') { + elements.push(i & 1); + } else { + elements.push((i + 1) * 10); + } + } + + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops[0]})`), + [vectorType], + vectorType, + t.params, + [ + { + input: vectorType.create(elements), + expected: vectorType.create(elements), + }, + ] + ); + }); + +g.test('concrete_vector_splat') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a vector constructed from a single concrete scalar produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16'] as const) + .combine('value', ['min', 'max', 1, 2, 5, 100] as const) + .combine('width', [2, 3, 4] as const) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + t.skipIf(t.params.type === 'bool' && !isMinOrMax(t.params.value)); + }) + .fn(async t => { + const value = valueFor(t.params.value, t.params.type); + const elementType = Type[t.params.type]; + const vectorType = Type.vec(t.params.width, elementType); + const fn = t.params.infer_type ? `vec${t.params.width}` : `${vectorType}`; + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops[0]})`), + [elementType], + vectorType, + t.params, + [{ input: [elementType.create(value)], expected: vectorType.create(value) }] + ); + }); + +g.test('abstract_vector_splat') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a vector constructed from a single abstract scalar produces the expected value`) + .params(u => + u + .combine('abstract_type', ['abstract-int', 'abstract-float'] as const) + .expand('concrete_type', t => kConcreteTypesForAbstractType[t.abstract_type]) + .combine('value', [1, 2, 5, 100] as const) + .combine('width', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.concrete_type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const suffix = t.params.abstract_type === 'abstract-float' ? '.0' : ''; + const concreteElementType = Type[t.params.concrete_type]; + const concreteVectorType = Type.vec(t.params.width, concreteElementType); + const fn = `vec${t.params.width}`; + await run( + t, + basicExpressionBuilder(_ => `${fn}(${t.params.value * 0x100000000}${suffix}) / 0x100000000`), + [], + concreteVectorType, + { inputSource: 'const' }, + [{ input: [], expected: concreteVectorType.create(t.params.value) }] + ); + }); + +g.test('concrete_vector_elements') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a vector constructed from concrete element values produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16'] as const) + .combine('width', [2, 3, 4] as const) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.type]; + const vectorType = Type.vec(t.params.width, elementType); + const elements: number[] = []; + const fn = t.params.infer_type ? `vec${t.params.width}` : `${vectorType}`; + for (let i = 0; i < t.params.width; i++) { + if (t.params.type === 'bool') { + elements.push(i & 1); + } else { + elements.push((i + 1) * 10); + } + } + + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops.join(', ')})`), + elements.map(e => elementType), + vectorType, + t.params, + [ + { + input: elements.map(v => elementType.create(v)), + expected: vectorType.create(elements), + }, + ] + ); + }); + +g.test('abstract_vector_elements') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a vector constructed from abstract element values produces the expected value`) + .params(u => + u + .combine('abstract_type', ['abstract-int', 'abstract-float'] as const) + .expand('concrete_type', t => kConcreteTypesForAbstractType[t.abstract_type]) + .combine('width', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.concrete_type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const suffix = t.params.abstract_type === 'abstract-float' ? '.0' : ''; + const concreteElementType = Type[t.params.concrete_type]; + const concreteVectorType = Type.vec(t.params.width, concreteElementType); + const fn = `vec${t.params.width}`; + const elements: number[] = []; + for (let i = 0; i < t.params.width; i++) { + elements.push((i + 1) * 10); + } + await run( + t, + basicExpressionBuilder( + _ => `${fn}(${elements.map(v => `${v * 0x100000000}${suffix}`).join(', ')}) / 0x100000000` + ), + [], + concreteVectorType, + { inputSource: 'const' }, + [{ input: [], expected: concreteVectorType.create(elements) }] + ); + }); + +const kMixSignatures = [ + '2s', // [vec2, scalar] + 's2', // [scalar, vec2] + '2ss', // [vec2, scalar, scalar] + 's2s', // [scalar, vec2, scalar] + 'ss2', // [scalar, scalar, vec2 ] + '22', // [vec2, vec2] + '3s', // [vec3, scalar] + 's3', // [scalar, vec3] +] as const; + +g.test('concrete_vector_mix') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc( + `Test that a vector constructed from a mix of concrete element values and sub-vectors produces the expected value` + ) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16'] as const) + .combine('signature', kMixSignatures) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.type]; + let width = 0; + const elementValue = (i: number) => (t.params.type === 'bool' ? i & 1 : (i + 1) * 10); + const elements: number[] = []; + const nextValue = () => { + const value = elementValue(width++); + elements.push(value); + return elementType.create(value); + }; + const args: Value[] = []; + for (const c of t.params.signature) { + switch (c) { + case '2': + args.push(vec2(nextValue(), nextValue())); + break; + case '3': + args.push(vec3(nextValue(), nextValue(), nextValue())); + break; + case 's': + args.push(nextValue()); + break; + } + } + const vectorType = Type.vec(width, elementType); + const fn = t.params.infer_type ? `vec${width}` : `${vectorType}`; + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops.join(', ')})`), + args.map(e => e.type), + vectorType, + t.params, + [ + { + input: args, + expected: vectorType.create(elements), + }, + ] + ); + }); + +g.test('abstract_vector_mix') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc( + `Test that a vector constructed from a mix of abstract element values and sub-vectors produces the expected value` + ) + .params(u => + u + .combine('abstract_type', ['abstract-int', 'abstract-float'] as const) + .expand('concrete_type', t => kConcreteTypesForAbstractType[t.abstract_type]) + .combine('signature', kMixSignatures) + ) + .beforeAllSubcases(t => { + if (t.params.concrete_type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + let width = 0; + const suffix = t.params.abstract_type === 'abstract-float' ? '.0' : ''; + const concreteElementType = Type[t.params.concrete_type]; + const elementValue = (i: number) => (i + 1) * 10; + const elements: number[] = []; + const nextValue = () => { + const value = elementValue(width++); + elements.push(value); + return `${value * 0x100000000}${suffix}`; + }; + const args: string[] = []; + for (const c of t.params.signature) { + switch (c) { + case '2': + args.push(`vec2(${nextValue()}, ${nextValue()})`); + break; + case '3': + args.push(`vec3(${nextValue()}, ${nextValue()}, ${nextValue()})`); + break; + case 's': + args.push(`${nextValue()}`); + break; + } + } + const concreteVectorType = Type.vec(width, concreteElementType); + const fn = `vec${width}`; + await run( + t, + basicExpressionBuilder(_ => `${fn}(${args.join(', ')}) / 0x100000000`), + [], + concreteVectorType, + { inputSource: 'const' }, + [ + { + input: [], + expected: concreteVectorType.create(elements), + }, + ] + ); + }); + +g.test('matrix_identity') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a matrix constructed from a value of the same type produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['f32', 'f16'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.type]; + const matrixType = Type.mat(t.params.columns, t.params.rows, elementType); + const elements: number[] = []; + for (let column = 0; column < t.params.columns; column++) { + for (let row = 0; row < t.params.rows; row++) { + elements.push((column + 1) * 10 + (row + 1)); + } + } + const fn = t.params.infer_type ? `mat${t.params.columns}x${t.params.rows}` : `${matrixType}`; + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops[0]})`), + [matrixType], + matrixType, + t.params, + [ + { + input: matrixType.create(elements), + expected: matrixType.create(elements), + }, + ] + ); + }); + +g.test('concrete_matrix_elements') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a matrix constructed from concrete element values produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['f32', 'f16'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.type]; + const matrixType = Type.mat(t.params.columns, t.params.rows, elementType); + const elements: number[] = []; + for (let column = 0; column < t.params.columns; column++) { + for (let row = 0; row < t.params.rows; row++) { + elements.push((column + 1) * 10 + (row + 1)); + } + } + const fn = t.params.infer_type ? `mat${t.params.columns}x${t.params.rows}` : `${matrixType}`; + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops.join(', ')})`), + elements.map(e => elementType), + matrixType, + t.params, + [ + { + input: elements.map(e => elementType.create(e)), + expected: matrixType.create(elements), + }, + ] + ); + }); + +g.test('abstract_matrix_elements') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a matrix constructed from concrete element values produces the expected value`) + .params(u => + u + .combine('concrete_type', ['f32', 'f16'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.concrete_type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const concreteElementType = Type[t.params.concrete_type]; + const concreteMatrixType = Type.mat(t.params.columns, t.params.rows, concreteElementType); + const elements: number[] = []; + for (let column = 0; column < t.params.columns; column++) { + for (let row = 0; row < t.params.rows; row++) { + elements.push((column + 1) * 10 + (row + 1)); + } + } + const fn = `mat${t.params.columns}x${t.params.rows}`; + await run( + t, + basicExpressionBuilder( + _ => `${fn}(${elements.map(v => `${v * 0x100000000}.0`).join(', ')}) * (1.0 / 0x100000000)` + ), + [], + concreteMatrixType, + { inputSource: 'const' }, + [ + { + input: [], + expected: concreteMatrixType.create(elements), + }, + ] + ); + }); + +g.test('concrete_matrix_column_vectors') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a matrix constructed from concrete column vectors produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['f32', 'f16'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.type]; + const columnType = Type.vec(t.params.rows, elementType); + const matrixType = Type.mat(t.params.columns, t.params.rows, elementType); + const elements: number[] = []; + const columnVectors: VectorValue[] = []; + for (let column = 0; column < t.params.columns; column++) { + const columnElements: number[] = []; + for (let row = 0; row < t.params.rows; row++) { + const v = (column + 1) * 10 + (row + 1); + elements.push(v); + columnElements.push(v); + } + columnVectors.push(columnType.create(columnElements)); + } + const fn = t.params.infer_type ? `mat${t.params.columns}x${t.params.rows}` : `${matrixType}`; + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops.join(', ')})`), + columnVectors.map(v => v.type), + matrixType, + t.params, + [ + { + input: columnVectors, + expected: matrixType.create(elements), + }, + ] + ); + }); + +g.test('abstract_matrix_column_vectors') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that a matrix constructed from abstract column vectors produces the expected value`) + .params(u => + u + .combine('concrete_type', ['f32', 'f16'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.concrete_type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const concreteElementType = Type[t.params.concrete_type]; + const concreteMatrixType = Type.mat(t.params.columns, t.params.rows, concreteElementType); + const elements: number[] = []; + const columnVectors: string[] = []; + for (let column = 0; column < t.params.columns; column++) { + const columnElements: string[] = []; + for (let row = 0; row < t.params.rows; row++) { + const v = (column + 1) * 10 + (row + 1); + elements.push(v); + columnElements.push(`${v * 0x100000000}`); + } + columnVectors.push(`vec${t.params.rows}(${columnElements.join(', ')})`); + } + const fn = `mat${t.params.columns}x${t.params.rows}`; + await run( + t, + basicExpressionBuilder(_ => `${fn}(${columnVectors.join(', ')}) * (1.0 / 0x100000000)`), + [], + concreteMatrixType, + { inputSource: 'const' }, + [ + { + input: [], + expected: concreteMatrixType.create(elements), + }, + ] + ); + }); + +g.test('concrete_array_elements') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that an array constructed from concrete element values produces the expected value`) + .params(u => + u + .combine('inputSource', allInputSources) + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16', 'vec3f', 'vec4i'] as const) + .combine('length', [1, 5, 10] as const) + .combine('infer_type', [false, true] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const elementType = Type[t.params.type]; + const arrayType = Type.array(t.params.length, elementType); + const elements: number[] = []; + for (let i = 0; i < t.params.length; i++) { + elements.push((i + 1) * 10); + } + const fn = t.params.infer_type ? `array` : `${arrayType}`; + await run( + t, + basicExpressionBuilder(ops => `${fn}(${ops.join(', ')})`), + elements.map(e => elementType), + arrayType, + t.params, + [ + { + input: elements.map(e => elementType.create(e)), + expected: arrayType.create(elements), + }, + ] + ); + }); + +g.test('abstract_array_elements') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that an array constructed from element values produces the expected value`) + .params(u => + u + .combine('abstract_type', [ + 'abstract-int', + 'abstract-float', + 'vec3<abstract-int>', + 'vec4<abstract-float>', + 'mat2x3<abstract-float>', + ] as const) + .expand('concrete_type', t => kConcreteTypesForAbstractType[t.abstract_type]) + .combine('length', [1, 5, 10] as const) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(Type[t.params.concrete_type]).kind === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const count = t.params.length; + const concreteElementType = Type[t.params.concrete_type]; + const concreteArrayType = Type.array(count, concreteElementType); + const elements: { args: string; value: Value }[] = []; + let i = 0; + const nextValue = () => ++i * 10; + for (let i = 0; i < count; i++) { + switch (t.params.abstract_type) { + case 'abstract-int': { + const value = nextValue(); + elements.push({ args: `${value}`, value: concreteElementType.create(value) }); + break; + } + case 'abstract-float': { + const value = nextValue(); + elements.push({ args: `${value}.0`, value: concreteElementType.create(value) }); + break; + } + case 'vec3<abstract-int>': { + const x = nextValue(); + const y = nextValue(); + const z = nextValue(); + elements.push({ + args: `vec3(${x}, ${y}, ${z})`, + value: (concreteElementType as VectorType).create([x, y, z]), + }); + break; + } + case 'vec4<abstract-float>': { + const x = nextValue(); + const y = nextValue(); + const z = nextValue(); + const w = nextValue(); + elements.push({ + args: `vec4(${x}.0, ${y}.0, ${z}.0, ${w}.0)`, + value: (concreteElementType as VectorType).create([x, y, z, w]), + }); + break; + } + case 'mat2x3<abstract-float>': { + const e00 = nextValue(); + const e01 = nextValue(); + const e02 = nextValue(); + const e10 = nextValue(); + const e11 = nextValue(); + const e12 = nextValue(); + elements.push({ + args: `mat2x3(vec3(${e00}.0, ${e01}.0, ${e02}.0), vec3(${e10}.0, ${e11}.0, ${e12}.0))`, + value: (concreteElementType as MatrixType).create([e00, e01, e02, e10, e11, e12]), + }); + break; + } + } + } + const fn = `array`; + await run( + t, + basicExpressionBuilder(_ => `${fn}(${elements.map(e => e.args).join(', ')})`), + [], + concreteArrayType, + { inputSource: 'const' }, + [ + { + input: [], + expected: new ArrayValue(elements.map(e => e.value)), + }, + ] + ); + }); + +g.test('structure') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc(`Test that an structure constructed from element values produces the expected value`) + .params(u => + u + .combine('member_types', [ + ['bool'], + ['u32'], + ['vec3f'], + ['i32', 'u32'], + ['i32', 'f16', 'vec4i', 'mat3x2f'], + ['bool', 'u32', 'f16', 'vec3f', 'vec2i'], + ['i32', 'u32', 'f32', 'f16', 'vec3f', 'vec4i'], + ] as readonly ScalarKind[][]) + .combine('nested', [false, true]) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const memberType = Type[t.params.member_types[t.params.member_index]]; + const values = t.params.member_types.map((ty, i) => Type[ty].create(i)); + + const builder = basicExpressionBuilder(ops => + t.params.nested + ? `OuterStruct(10, MyStruct(${ops.join(', ')}), 20).inner.member_${t.params.member_index}` + : `MyStruct(${ops.join(', ')}).member_${t.params.member_index}` + ); + await run( + t, + (parameterTypes, resultType, cases, inputSource) => { + return ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +${builder(parameterTypes, resultType, cases, inputSource)} + +struct MyStruct { +${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; +struct OuterStruct { + pre : i32, + inner : MyStruct, + post : i32, +}; +`; + }, + t.params.member_types.map(ty => Type[ty]), + memberType, + { inputSource: 'const' }, + [{ input: values, expected: values[t.params.member_index] }] + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/constructor/zero_value.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/constructor/zero_value.spec.ts new file mode 100644 index 0000000000..5899021550 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/constructor/zero_value.spec.ts @@ -0,0 +1,162 @@ +export const description = ` +Execution Tests for zero value constructors +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { ScalarKind, Type } from '../../../../util/conversion.js'; +import { basicExpressionBuilder, run } from '../expression.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('scalar') + .specURL('https://www.w3.org/TR/WGSL/#zero-value-builtin-function') + .desc(`Test that a zero value scalar constructor produces the expected zero value`) + .params(u => u.combine('type', ['bool', 'i32', 'u32', 'f32', 'f16'] as const)) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const type = Type[t.params.type]; + await run( + t, + basicExpressionBuilder(ops => `${type}()`), + [], + type, + { inputSource: 'const' }, + [{ input: [], expected: type.create(0) }] + ); + }); + +g.test('vector') + .specURL('https://www.w3.org/TR/WGSL/#zero-value-builtin-function') + .desc(`Test that a zero value vector constructor produces the expected zero value`) + .params(u => + u + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16'] as const) + .combine('width', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const type = Type.vec(t.params.width, Type[t.params.type]); + await run( + t, + basicExpressionBuilder(ops => `${type}()`), + [], + type, + { inputSource: 'const' }, + [{ input: [], expected: type.create(0) }] + ); + }); + +g.test('matrix') + .specURL('https://www.w3.org/TR/WGSL/#zero-value-builtin-function') + .desc(`Test that a zero value matrix constructor produces the expected zero value`) + .params(u => + u + .combine('type', ['f32', 'f16'] as const) + .combine('columns', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const type = Type.mat(t.params.columns, t.params.rows, Type[t.params.type]); + await run( + t, + basicExpressionBuilder(ops => `${type}()`), + [], + type, + { inputSource: 'const' }, + [{ input: [], expected: type.create(0) }] + ); + }); + +g.test('array') + .specURL('https://www.w3.org/TR/WGSL/#zero-value-builtin-function') + .desc(`Test that a zero value matrix constructor produces the expected zero value`) + .params(u => + u + .combine('type', ['bool', 'i32', 'u32', 'f32', 'f16', 'vec3f', 'vec4i'] as const) + .combine('length', [1, 5, 10] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const type = Type.array(t.params.length, Type[t.params.type]); + await run( + t, + basicExpressionBuilder(ops => `${type}()`), + [], + type, + { inputSource: 'const' }, + [{ input: [], expected: type.create(0) }] + ); + }); + +g.test('structure') + .specURL('https://www.w3.org/TR/WGSL/#zero-value-builtin-function') + .desc(`Test that an structure constructed from element values produces the expected value`) + .params(u => + u + .combine('member_types', [ + ['bool'], + ['u32'], + ['vec3f'], + ['i32', 'u32'], + ['i32', 'f16', 'vec4i', 'mat3x2f'], + ['bool', 'u32', 'f16', 'vec3f', 'vec2i'], + ['i32', 'u32', 'f32', 'f16', 'vec3f', 'vec4i'], + ] as readonly ScalarKind[][]) + .combine('nested', [false, true]) + .beginSubcases() + .expand('member_index', t => t.member_types.map((_, i) => i)) + ) + .beforeAllSubcases(t => { + if (t.params.member_types.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(async t => { + const memberType = Type[t.params.member_types[t.params.member_index]]; + const builder = basicExpressionBuilder(_ => + t.params.nested + ? `OuterStruct().inner.member_${t.params.member_index}` + : `MyStruct().member_${t.params.member_index}` + ); + await run( + t, + (parameterTypes, resultType, cases, inputSource) => { + return ` +${t.params.member_types.includes('f16') ? 'enable f16;' : ''} + +${builder(parameterTypes, resultType, cases, inputSource)} + +struct MyStruct { +${t.params.member_types.map((ty, i) => ` member_${i} : ${ty},`).join('\n')} +}; +struct OuterStruct { + pre : i32, + inner : MyStruct, + post : i32, +}; +`; + }, + [], + memberType, + { inputSource: 'const' }, + [{ input: [], expected: memberType.create(0) }] + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expectation.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expectation.ts new file mode 100644 index 0000000000..955fa68138 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expectation.ts @@ -0,0 +1,38 @@ +import { ROArrayArray } from '../../../../common/util/types.js'; +import { Comparator, compare } from '../../../util/compare.js'; +import { + ArrayValue, + MatrixValue, + Value, + VectorValue, + isScalarValue, +} from '../../../util/conversion.js'; +import { FPInterval } from '../../../util/floating_point.js'; + +export type Expectation = + | Value + | FPInterval + | readonly FPInterval[] + | ROArrayArray<FPInterval> + | Comparator; + +/** @returns if this Expectation actually a Comparator */ +export function isComparator(e: Expectation): e is Comparator { + return !( + e instanceof FPInterval || + isScalarValue(e) || + e instanceof VectorValue || + e instanceof MatrixValue || + e instanceof ArrayValue || + e instanceof Array + ); +} + +/** @returns the input if it is already a Comparator, otherwise wraps it in a 'value' comparator */ +export function toComparator(input: Expectation): Comparator { + if (isComparator(input)) { + return input; + } + + return { compare: got => compare(got, input as Value), kind: 'value' }; +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts index f85516f29b..be8f1fd7fd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts @@ -1,70 +1,25 @@ import { globalTestConfig } from '../../../../common/framework/test_config.js'; -import { ROArrayArray } from '../../../../common/util/types.js'; import { assert, objectEquals, unreachable } from '../../../../common/util/util.js'; import { GPUTest } from '../../../gpu_test.js'; -import { compare, Comparator, ComparatorImpl } from '../../../util/compare.js'; +import { Comparator, ComparatorImpl } from '../../../util/compare.js'; import { kValue } from '../../../util/constants.js'; import { + MatrixType, + ScalarValue, ScalarType, - Scalar, Type, - TypeVec, - TypeU32, - Value, - Vector, VectorType, - u32, - i32, - Matrix, - MatrixType, - ScalarBuilder, + Value, + VectorValue, + isAbstractType, scalarTypeOf, + ArrayType, + elementTypeOf, } from '../../../util/conversion.js'; -import { FPInterval } from '../../../util/floating_point.js'; -import { - cartesianProduct, - QuantizeFunc, - quantizeToI32, - quantizeToU32, -} from '../../../util/math.js'; - -export type Expectation = - | Value - | FPInterval - | readonly FPInterval[] - | ROArrayArray<FPInterval> - | Comparator; - -/** @returns if this Expectation actually a Comparator */ -export function isComparator(e: Expectation): e is Comparator { - return !( - e instanceof FPInterval || - e instanceof Scalar || - e instanceof Vector || - e instanceof Matrix || - e instanceof Array - ); -} - -/** @returns the input if it is already a Comparator, otherwise wraps it in a 'value' comparator */ -export function toComparator(input: Expectation): Comparator { - if (isComparator(input)) { - return input; - } - - return { compare: got => compare(got, input as Value), kind: 'value' }; -} - -/** Case is a single expression test case. */ -export type Case = { - // The input value(s) - input: Value | ReadonlyArray<Value>; - // The expected result, or function to check the result - expected: Expectation; -}; +import { align } from '../../../util/math.js'; -/** CaseList is a list of Cases */ -export type CaseList = Array<Case>; +import { Case } from './case.js'; +import { toComparator } from './expectation.js'; /** The input value source */ export type InputSource = @@ -79,6 +34,9 @@ export const allInputSources: InputSource[] = ['const', 'uniform', 'storage_r', /** Just constant input source */ export const onlyConstInputSource: InputSource[] = ['const']; +/** All input sources except const */ +export const allButConstInputSource: InputSource[] = ['uniform', 'storage_r', 'storage_rw']; + /** Configuration for running a expression test */ export type Config = { // Where the input values are read from @@ -92,127 +50,157 @@ export type Config = { vectorize?: number; }; -// Helper for returning the stride for a given Type -function valueStride(ty: Type): number { - // AbstractFloats are passed out of the shader via a struct of 2x u32s and - // unpacking containers as arrays - if (scalarTypeOf(ty).kind === 'abstract-float') { - if (ty instanceof ScalarType) { - return 16; - } - if (ty instanceof VectorType) { - if (ty.width === 2) { - return 16; - } - // vec3s have padding to make them the same size as vec4s - return 32; - } - if (ty instanceof MatrixType) { - switch (ty.cols) { - case 2: - switch (ty.rows) { - case 2: - return 32; - case 3: - return 64; - case 4: - return 64; - } - break; - case 3: - switch (ty.rows) { - case 2: - return 48; - case 3: - return 96; - case 4: - return 96; - } - break; - case 4: - switch (ty.rows) { - case 2: - return 64; - case 3: - return 128; - case 4: - return 128; - } - break; - } +/** + * @returns the size and alignment in bytes of the type 'ty', taking into + * consideration storage alignment constraints and abstract numerics, which are + * encoded as a struct of holding two u32s. + */ +function sizeAndAlignmentOf(ty: Type, source: InputSource): { size: number; alignment: number } { + if (ty instanceof ScalarType) { + if (ty.kind === 'abstract-float' || ty.kind === 'abstract-int') { + // AbstractFloats and AbstractInts are passed out of the shader via structs of + // 2x u32s and unpacking containers as arrays + return { size: 8, alignment: 8 }; } - unreachable(`AbstractFloats have not yet been implemented for ${ty.toString()}`); + return { size: ty.size, alignment: ty.alignment }; + } + + if (ty instanceof VectorType) { + const out = sizeAndAlignmentOf(ty.elementType, source); + const n = ty.width === 3 ? 4 : ty.width; + out.size *= n; + out.alignment *= n; + return out; } if (ty instanceof MatrixType) { - switch (ty.cols) { - case 2: - switch (ty.rows) { - case 2: - return 16; - case 3: - return 32; - case 4: - return 32; - } - break; - case 3: - switch (ty.rows) { - case 2: - return 32; - case 3: - return 64; - case 4: - return 64; - } - break; - case 4: - switch (ty.rows) { - case 2: - return 32; - case 3: - return 64; - case 4: - return 64; - } - break; + const out = sizeAndAlignmentOf(ty.elementType, source); + const n = ty.rows === 3 ? 4 : ty.rows; + out.size *= n * ty.cols; + out.alignment *= n; + return out; + } + + if (ty instanceof ArrayType) { + const out = sizeAndAlignmentOf(ty.elementType, source); + if (source === 'uniform') { + out.alignment = align(out.alignment, 16); } - unreachable( - `Attempted to get stride length for a matrix with dimensions (${ty.cols}x${ty.rows}), which isn't currently handled` - ); + out.size *= ty.count; + return out; + } + + unreachable(`unhandled type: ${ty}`); +} + +/** + * @returns the stride in bytes of the type 'ty', taking into consideration abstract numerics, + * which are encoded as a struct of 2 x u32. + */ +function strideOf(ty: Type, source: InputSource): number { + const sizeAndAlign = sizeAndAlignmentOf(ty, source); + return align(sizeAndAlign.size, sizeAndAlign.alignment); +} + +/** + * Calls 'callback' with the layout information of each structure member with the types 'members'. + * @returns the byte size, stride and alignment of the structure. + */ +export function structLayout( + members: Type[], + source: InputSource, + callback?: (m: { + index: number; + type: Type; + size: number; + alignment: number; + offset: number; + }) => void +): { size: number; stride: number; alignment: number } { + let offset = 0; + let alignment = 1; + for (let i = 0; i < members.length; i++) { + const member = members[i]; + const sizeAndAlign = sizeAndAlignmentOf(member, source); + offset = align(offset, sizeAndAlign.alignment); + if (callback) { + callback({ + index: i, + type: member, + size: sizeAndAlign.size, + alignment: sizeAndAlign.alignment, + offset, + }); + } + offset += sizeAndAlign.size; + alignment = Math.max(alignment, sizeAndAlign.alignment); } - // Handles scalars and vectors - return 16; + if (source === 'uniform') { + alignment = align(alignment, 16); + } + + const size = offset; + const stride = align(size, alignment); + return { size, stride, alignment }; +} + +/** @returns the stride in bytes between two consecutive structures with the given members */ +export function structStride(members: Type[], source: InputSource): number { + return structLayout(members, source).stride; } -// Helper for summing up all of the stride values for an array of Types -function valueStrides(tys: Type[]): number { - return tys.map(valueStride).reduce((sum, c) => sum + c); +/** @returns the WGSL to describe the structure members in 'members' */ +function wgslMembers(members: Type[], source: InputSource, memberName: (i: number) => string) { + const lines: string[] = []; + const layout = structLayout(members, source, m => { + lines.push(` @size(${m.size}) ${memberName(lines.length)} : ${m.type},`); + }); + const padding = layout.stride - layout.size; + if (padding > 0) { + // Pad with a 'f16' if the padding requires an odd multiple of 2 bytes. + // This is required as 'i32' has an alignment and size of 4 bytes. + const ty = (padding & 2) !== 0 ? 'f16' : 'i32'; + lines.push(` @size(${padding}) padding : ${ty},`); + } + return lines.join('\n'); } // Helper for returning the WGSL storage type for the given Type. function storageType(ty: Type): Type { if (ty instanceof ScalarType) { assert(ty.kind !== 'f64', `No storage type defined for 'f64' values`); + assert(ty.kind !== 'abstract-int', `Custom handling is implemented for 'abstract-int' values`); assert( ty.kind !== 'abstract-float', `Custom handling is implemented for 'abstract-float' values` ); if (ty.kind === 'bool') { - return TypeU32; + return Type.u32; } } if (ty instanceof VectorType) { - return TypeVec(ty.width, storageType(ty.elementType) as ScalarType); + return Type.vec(ty.width, storageType(ty.elementType) as ScalarType); + } + if (ty instanceof ArrayType) { + return Type.array(ty.count, storageType(ty.elementType)); } return ty; } +/** Structure used to hold [from|to]Storage conversion helpers */ +type TypeConversionHelpers = { + // The module-scope WGSL to emit with the shader. + wgsl: string; + // A function that generates a unique WGSL identifier. + uniqueID: () => string; +}; + // Helper for converting a value of the type 'ty' from the storage type. -function fromStorage(ty: Type, expr: string): string { +function fromStorage(ty: Type, expr: string, helpers: TypeConversionHelpers): string { if (ty instanceof ScalarType) { - assert(ty.kind !== 'abstract-float', `AbstractFloat values should not be in input storage`); + assert(ty.kind !== 'abstract-int', `'abstract-int' values should not be in input storage`); + assert(ty.kind !== 'abstract-float', `'abstract-float' values should not be in input storage`); assert(ty.kind !== 'f64', `'No storage type defined for 'f64' values`); if (ty.kind === 'bool') { return `${expr} != 0u`; @@ -220,23 +208,46 @@ function fromStorage(ty: Type, expr: string): string { } if (ty instanceof VectorType) { assert( + ty.elementType.kind !== 'abstract-int', + `'abstract-int' values cannot appear in input storage` + ); + assert( ty.elementType.kind !== 'abstract-float', - `AbstractFloat values cannot appear in input storage` + `'abstract-float' values cannot appear in input storage` ); assert(ty.elementType.kind !== 'f64', `'No storage type defined for 'f64' values`); if (ty.elementType.kind === 'bool') { - return `${expr} != vec${ty.width}<u32>(0u)`; + return `(${expr} != vec${ty.width}<u32>(0u))`; } } + if (ty instanceof ArrayType && elementTypeOf(ty) === Type.bool) { + // array<u32, N> -> array<bool, N> + const conv = helpers.uniqueID(); + const inTy = Type.array(ty.count, Type.u32); + helpers.wgsl += ` +fn ${conv}(in : ${inTy}) -> ${ty} { + var out : ${ty}; + for (var i = 0; i < ${ty.count}; i++) { + out[i] = in[i] != 0; + } + return out; +} +`; + return `${conv}(${expr})`; + } return expr; } // Helper for converting a value of the type 'ty' to the storage type. -function toStorage(ty: Type, expr: string): string { +function toStorage(ty: Type, expr: string, helpers: TypeConversionHelpers): string { if (ty instanceof ScalarType) { assert( + ty.kind !== 'abstract-int', + `'abstract-int' values have custom code for writing to storage` + ); + assert( ty.kind !== 'abstract-float', - `AbstractFloat values have custom code for writing to storage` + `'abstract-float' values have custom code for writing to storage` ); assert(ty.kind !== 'f64', `No storage type defined for 'f64' values`); if (ty.kind === 'bool') { @@ -245,14 +256,33 @@ function toStorage(ty: Type, expr: string): string { } if (ty instanceof VectorType) { assert( + ty.elementType.kind !== 'abstract-int', + `'abstract-int' values have custom code for writing to storage` + ); + assert( ty.elementType.kind !== 'abstract-float', - `AbstractFloat values have custom code for writing to storage` + `'abstract-float' values have custom code for writing to storage` ); assert(ty.elementType.kind !== 'f64', `'No storage type defined for 'f64' values`); if (ty.elementType.kind === 'bool') { return `select(vec${ty.width}<u32>(0u), vec${ty.width}<u32>(1u), ${expr})`; } } + if (ty instanceof ArrayType && elementTypeOf(ty) === Type.bool) { + // array<bool, N> -> array<u32, N> + const conv = helpers.uniqueID(); + const outTy = Type.array(ty.count, Type.u32); + helpers.wgsl += ` +fn ${conv}(in : ${ty}) -> ${outTy} { + var out : ${outTy}; + for (var i = 0; i < ${ty.count}; i++) { + out[i] = select(0u, 1u, in[i]); + } + return out; +} +`; + return `${conv}(${expr})`; + } return expr; } @@ -296,7 +326,7 @@ export async function run( parameterTypes: Array<Type>, resultType: Type, cfg: Config = { inputSource: 'storage_r' }, - cases: CaseList, + cases: Case[], batch_size?: number ) { // If the 'vectorize' config option was provided, pack the cases into vectors. @@ -325,12 +355,13 @@ export async function run( // 2k appears to be a sweet-spot when benchmarking. return Math.floor( Math.min(1024 * 2, t.device.limits.maxUniformBufferBindingSize) / - valueStrides(parameterTypes) + structStride(parameterTypes, cfg.inputSource) ); case 'storage_r': case 'storage_rw': return Math.floor( - t.device.limits.maxStorageBufferBindingSize / valueStrides(parameterTypes) + t.device.limits.maxStorageBufferBindingSize / + structStride(parameterTypes, cfg.inputSource) ); } })(); @@ -353,7 +384,7 @@ export async function run( } }; - const processBatch = async (batchCases: CaseList) => { + const processBatch = async (batchCases: Case[]) => { const checkBatch = await submitBatch( t, shaderBuilder, @@ -404,12 +435,13 @@ async function submitBatch( shaderBuilder: ShaderBuilder, parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource, pipelineCache: PipelineCache ): Promise<() => void> { // Construct a buffer to hold the results of the expression tests - const outputBufferSize = cases.length * valueStride(resultType); + const outputStride = structStride([resultType], 'storage_rw'); + const outputBufferSize = align(cases.length * outputStride, 4); const outputBuffer = t.device.createBuffer({ size: outputBufferSize, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, @@ -444,7 +476,7 @@ async function submitBatch( // Read the outputs from the output buffer const outputs = new Array<Value>(cases.length); for (let i = 0; i < cases.length; i++) { - outputs[i] = resultType.read(outputData, i * valueStride(resultType)); + outputs[i] = resultType.read(outputData, i * outputStride); } // The list of expectation failures @@ -498,7 +530,7 @@ function map<T, U>(v: T | readonly T[], fn: (value: T, index?: number) => U): U[ export type ShaderBuilder = ( parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource ) => string; @@ -507,10 +539,13 @@ export type ShaderBuilder = ( */ function wgslOutputs(resultType: Type, count: number): string { let output_struct = undefined; - if (scalarTypeOf(resultType).kind !== 'abstract-float') { + if ( + scalarTypeOf(resultType).kind !== 'abstract-float' && + scalarTypeOf(resultType).kind !== 'abstract-int' + ) { output_struct = ` struct Output { - @size(${valueStride(resultType)}) value : ${storageType(resultType)} + @size(${strideOf(resultType, 'storage_rw')}) value : ${storageType(resultType)} };`; } else { if (resultType instanceof ScalarType) { @@ -520,7 +555,7 @@ struct Output { }; struct Output { - @size(${valueStride(resultType)}) value: AF, + @size(${strideOf(resultType, 'storage_rw')}) value: AF, };`; } if (resultType instanceof VectorType) { @@ -531,7 +566,7 @@ struct Output { }; struct Output { - @size(${valueStride(resultType)}) value: array<AF, ${dim}>, + @size(${strideOf(resultType, 'storage_rw')}) value: array<AF, ${dim}>, };`; } @@ -544,7 +579,7 @@ struct Output { }; struct Output { - @size(${valueStride(resultType)}) value: array<array<AF, ${rows}>, ${cols}>, + @size(${strideOf(resultType, 'storage_rw')}) value: array<array<AF, ${rows}>, ${cols}>, };`; } @@ -562,7 +597,7 @@ struct Output { function wgslValuesArray( parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], expressionBuilder: ExpressionBuilder ): string { return ` @@ -612,19 +647,28 @@ function basicExpressionShaderBody( expressionBuilder: ExpressionBuilder, parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource ): string { assert( + scalarTypeOf(resultType).kind !== 'abstract-int', + `abstractIntShaderBuilder should be used when result type is 'abstract-int'` + ); + assert( scalarTypeOf(resultType).kind !== 'abstract-float', - `abstractFloatShaderBuilder should be used when result type is 'abstract-float` + `abstractFloatShaderBuilder should be used when result type is 'abstract-float'` ); + let nextUniqueIDSuffix = 0; + const convHelpers: TypeConversionHelpers = { + wgsl: '', + uniqueID: () => `cts_symbol_${nextUniqueIDSuffix++}`, + }; if (inputSource === 'const') { ////////////////////////////////////////////////////////////////////////// // Constant eval ////////////////////////////////////////////////////////////////////////// let body = ''; - if (parameterTypes.some(ty => scalarTypeOf(ty).kind === 'abstract-float')) { + if (parameterTypes.some(ty => isAbstractType(elementTypeOf(ty)))) { // Directly assign the expression to the output, to avoid an // intermediate store, which will concretize the value early body = cases @@ -632,7 +676,8 @@ function basicExpressionShaderBody( (c, i) => ` outputs[${i}].value = ${toStorage( resultType, - expressionBuilder(map(c.input, v => v.wgsl())) + expressionBuilder(map(c.input, v => v.wgsl())), + convHelpers )};` ) .join('\n '); @@ -640,47 +685,60 @@ function basicExpressionShaderBody( body = cases .map((_, i) => { const value = `values[${i}]`; - return ` outputs[${i}].value = ${toStorage(resultType, value)};`; + return ` outputs[${i}].value = ${toStorage(resultType, value, convHelpers)};`; }) .join('\n '); } else { body = ` for (var i = 0u; i < ${cases.length}; i++) { - outputs[i].value = ${toStorage(resultType, `values[i]`)}; + outputs[i].value = ${toStorage(resultType, `values[i]`, convHelpers)}; }`; } + // If params are abstract, we will assign them directly to the storage array, so skip the values array. + let valuesArray = ''; + if (!parameterTypes.some(isAbstractType)) { + valuesArray = wgslValuesArray(parameterTypes, resultType, cases, expressionBuilder); + } + return ` ${wgslOutputs(resultType, cases.length)} -${wgslValuesArray(parameterTypes, resultType, cases, expressionBuilder)} +${valuesArray} + +${convHelpers.wgsl} @compute @workgroup_size(1) fn main() { ${body} -}`; +} +`; } else { ////////////////////////////////////////////////////////////////////////// // Runtime eval ////////////////////////////////////////////////////////////////////////// // returns the WGSL expression to load the ith parameter of the given type from the input buffer - const paramExpr = (ty: Type, i: number) => fromStorage(ty, `inputs[i].param${i}`); + const paramExpr = (ty: Type, i: number) => fromStorage(ty, `inputs[i].param${i}`, convHelpers); // resolves to the expression that calls the builtin - const expr = toStorage(resultType, expressionBuilder(parameterTypes.map(paramExpr))); + const expr = toStorage( + resultType, + expressionBuilder(parameterTypes.map(paramExpr)), + convHelpers + ); return ` struct Input { -${parameterTypes - .map((ty, i) => ` @size(${valueStride(ty)}) param${i} : ${storageType(ty)},`) - .join('\n')} -}; +${wgslMembers(parameterTypes.map(storageType), inputSource, i => `param${i}`)} +} ${wgslOutputs(resultType, cases.length)} ${wgslInputVar(inputSource, cases.length)} +${convHelpers.wgsl} + @compute @workgroup_size(1) fn main() { for (var i = 0; i < ${cases.length}; i++) { @@ -699,7 +757,7 @@ export function basicExpressionBuilder(expressionBuilder: ExpressionBuilder): Sh return ( parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource ) => { return `\ @@ -722,7 +780,7 @@ export function basicExpressionWithPredeclarationBuilder( return ( parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource ) => { return `\ @@ -742,7 +800,7 @@ export function compoundAssignmentBuilder(op: string): ShaderBuilder { return ( parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource ) => { ////////////////////////////////////////////////////////////////////////// @@ -807,8 +865,7 @@ ${wgslHeader(parameterTypes, resultType)} ${wgslOutputs(resultType, cases.length)} struct Input { - @size(${valueStride(lhsType)}) lhs : ${storageType(lhsType)}, - @size(${valueStride(rhsType)}) rhs : ${storageType(rhsType)}, +${wgslMembers([lhsType, rhsType].map(storageType), inputSource, i => ['lhs', 'rhs'][i])} } ${wgslInputVar(inputSource, cases.length)} @@ -969,10 +1026,10 @@ export function abstractFloatShaderBuilder(expressionBuilder: ExpressionBuilder) return ( parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource ) => { - assert(inputSource === 'const', 'AbstractFloat results are only defined for const-eval'); + assert(inputSource === 'const', `'abstract-float' results are only defined for const-eval`); assert( scalarTypeOf(resultType).kind === 'abstract-float', `Expected resultType of 'abstract-float', received '${scalarTypeOf(resultType).kind}' instead` @@ -998,6 +1055,90 @@ ${body} } /** + * @returns a string that extracts the value of an AbstractInt into an output + * destination + * @param expr expression for an AbstractInt value, if working with vectors, + * this string needs to include indexing into the container. + * @param case_idx index in the case output array to assign the result + * @param accessor string representing how access to the AbstractInt that needs + * to be operated on. + * For scalars this should be left as ''. + * For vectors this will be an indexing operation, + * i.e. '[i]' + */ +function abstractIntSnippet(expr: string, case_idx: number, accessor: string = ''): string { + // AbstractInts are i64s under the hood. WebGPU does not support + // putting i64s in buffers, or any 64-bit simple types, so the result needs to + // be split up into u32 bitfields + // + // Since there is no 64-bit data type that can be used as an element for a + // vector or a matrix in WGSL, the testing framework needs to pass the u32s + // via a struct with two u32s, and deconstruct vectors into arrays. + // + // This is complicated by the fact that user defined functions cannot + // take/return AbstractInts, and AbstractInts cannot be stored in + // variables, so the code cannot just inject a simple utility function + // at the top of the shader, instead this snippet needs to be inlined + // everywhere the test needs to return an AbstractInt. + return ` { + outputs[${case_idx}].value${accessor}.high = bitcast<u32>(i32(${expr}${accessor} >> 32)) & 0xFFFFFFFF; + const low_sign = (${expr}${accessor} & (1 << 31)); + outputs[${case_idx}].value${accessor}.low = bitcast<u32>((${expr}${accessor} & 0x7FFFFFFF)) | low_sign; + }`; +} + +/** @returns a string for a specific case that has a AbstractInt result */ +function abstractIntCaseBody(expr: string, resultType: Type, i: number): string { + if (resultType instanceof ScalarType) { + return abstractIntSnippet(expr, i); + } + + if (resultType instanceof VectorType) { + return [...Array(resultType.width).keys()] + .map(idx => abstractIntSnippet(expr, i, `[${idx}]`)) + .join(' \n'); + } + + unreachable(`Results of type '${resultType}' not yet implemented`); +} + +/** + * @returns a ShaderBuilder that builds a test shader hands AbstractInt results. + * @param expressionBuilder an expression builder that will return AbstractInts + */ +export function abstractIntShaderBuilder(expressionBuilder: ExpressionBuilder): ShaderBuilder { + return ( + parameterTypes: Array<Type>, + resultType: Type, + cases: Case[], + inputSource: InputSource + ) => { + assert(inputSource === 'const', `'abstract-int' results are only defined for const-eval`); + assert( + scalarTypeOf(resultType).kind === 'abstract-int', + `Expected resultType of 'abstract-int', received '${scalarTypeOf(resultType).kind}' instead` + ); + + const body = cases + .map((c, i) => { + const expr = `${expressionBuilder(map(c.input, v => v.wgsl()))}`; + return abstractIntCaseBody(expr, resultType, i); + }) + .join('\n '); + + return ` +${wgslHeader(parameterTypes, resultType)} + +${wgslOutputs(resultType, cases.length)} + +@compute @workgroup_size(1) +fn main() { +${body} +}`; + }; +} + +/** * Constructs and returns a GPUComputePipeline and GPUBindGroup for running a * batch of test cases. If a pre-created pipeline can be found in * `pipelineCache`, then this may be returned instead of creating a new @@ -1016,7 +1157,7 @@ async function buildPipeline( shaderBuilder: ShaderBuilder, parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], inputSource: InputSource, outputBuffer: GPUBuffer, pipelineCache: PipelineCache @@ -1060,27 +1201,23 @@ async function buildPipeline( // Input values come from a uniform or storage buffer // size in bytes of the input buffer - const inputSize = cases.length * valueStrides(parameterTypes); + const caseStride = structStride(parameterTypes, inputSource); + const inputSize = align(cases.length * caseStride, 4); // Holds all the parameter values for all cases const inputData = new Uint8Array(inputSize); // Pack all the input parameter values into the inputData buffer - { - const caseStride = valueStrides(parameterTypes); - for (let caseIdx = 0; caseIdx < cases.length; caseIdx++) { - const caseBase = caseIdx * caseStride; - let offset = caseBase; - for (let paramIdx = 0; paramIdx < parameterTypes.length; paramIdx++) { - const params = cases[caseIdx].input; - if (params instanceof Array) { - params[paramIdx].copyTo(inputData, offset); - } else { - params.copyTo(inputData, offset); - } - offset += valueStride(parameterTypes[paramIdx]); + for (let caseIdx = 0; caseIdx < cases.length; caseIdx++) { + const offset = caseIdx * caseStride; + structLayout(parameterTypes, inputSource, m => { + const arg = cases[caseIdx].input; + if (arg instanceof Array) { + arg[m.index].copyTo(inputData, offset + m.offset); + } else { + arg.copyTo(inputData, offset + m.offset); } - } + }); } // build the compute pipeline, if the shader hasn't been compiled already. @@ -1123,12 +1260,12 @@ async function buildPipeline( * If `cases.length` is not a multiple of `vectorWidth`, then the last scalar * test case value is repeated to fill the vector value. */ -function packScalarsToVector( +export function packScalarsToVector( parameterTypes: Array<Type>, resultType: Type, - cases: CaseList, + cases: Case[], vectorWidth: number -): { cases: CaseList; parameterTypes: Array<Type>; resultType: Type } { +): { cases: Case[]; parameterTypes: Array<Type>; resultType: Type } { // Validate that the parameters and return type are all vectorizable for (let i = 0; i < parameterTypes.length; i++) { const ty = parameterTypes[i]; @@ -1145,22 +1282,22 @@ function packScalarsToVector( } const packedCases: Array<Case> = []; - const packedParameterTypes = parameterTypes.map(p => TypeVec(vectorWidth, p as ScalarType)); - const packedResultType = new VectorType(vectorWidth, resultType); + const packedParameterTypes = parameterTypes.map(p => Type.vec(vectorWidth, p as ScalarType)); + const packedResultType = Type.vec(vectorWidth, resultType); const clampCaseIdx = (idx: number) => Math.min(idx, cases.length - 1); let caseIdx = 0; while (caseIdx < cases.length) { // Construct the vectorized inputs from the scalar cases - const packedInputs = new Array<Vector>(parameterTypes.length); + const packedInputs = new Array<VectorValue>(parameterTypes.length); for (let paramIdx = 0; paramIdx < parameterTypes.length; paramIdx++) { - const inputElements = new Array<Scalar>(vectorWidth); + const inputElements = new Array<ScalarValue>(vectorWidth); for (let i = 0; i < vectorWidth; i++) { const input = cases[clampCaseIdx(caseIdx + i)].input; - inputElements[i] = (input instanceof Array ? input[paramIdx] : input) as Scalar; + inputElements[i] = (input instanceof Array ? input[paramIdx] : input) as ScalarValue; } - packedInputs[paramIdx] = new Vector(inputElements); + packedInputs[paramIdx] = new VectorValue(inputElements); } // Gather the comparators for the packed cases @@ -1174,7 +1311,7 @@ function packScalarsToVector( const gElements = new Array<string>(vectorWidth); const eElements = new Array<string>(vectorWidth); for (let i = 0; i < vectorWidth; i++) { - const d = cmp_impls[i]((got as Vector).elements[i]); + const d = cmp_impls[i]((got as VectorValue).elements[i]); matched = matched && d.matched; gElements[i] = d.got; eElements[i] = d.expected; @@ -1199,238 +1336,3 @@ function packScalarsToVector( resultType: packedResultType, }; } - -/** - * Indicates bounds that acceptance intervals need to be within to avoid inputs - * being filtered out. This is used for const-eval tests, since going OOB will - * cause a validation error not an execution error. - */ -export type IntervalFilter = - | 'finite' // Expected to be finite in the interval numeric space - | 'unfiltered'; // No expectations - -/** - * A function that performs a binary operation on x and y, and returns the expected - * result. - */ -export interface BinaryOp { - (x: number, y: number): number | undefined; -} - -/** - * @returns array of Case for the input params with op applied - * @param param0s array of inputs to try for the first param - * @param param1s array of inputs to try for the second param - * @param op callback called on each pair of inputs to produce each case - * @param quantize function to quantize all values - * @param scalarize function to convert numbers to Scalars - */ -function generateScalarBinaryToScalarCases( - param0s: readonly number[], - param1s: readonly number[], - op: BinaryOp, - quantize: QuantizeFunc, - scalarize: ScalarBuilder -): Case[] { - param0s = param0s.map(quantize); - param1s = param1s.map(quantize); - return cartesianProduct(param0s, param1s).reduce((cases, e) => { - const expected = op(e[0], e[1]); - if (expected !== undefined) { - cases.push({ input: [scalarize(e[0]), scalarize(e[1])], expected: scalarize(expected) }); - } - return cases; - }, new Array<Case>()); -} - -/** - * @returns an array of Cases for operations over a range of inputs - * @param param0s array of inputs to try for the first param - * @param param1s array of inputs to try for the second param - * @param op callback called on each pair of inputs to produce each case - */ -export function generateBinaryToI32Cases( - param0s: readonly number[], - param1s: readonly number[], - op: BinaryOp -) { - return generateScalarBinaryToScalarCases(param0s, param1s, op, quantizeToI32, i32); -} - -/** - * @returns an array of Cases for operations over a range of inputs - * @param param0s array of inputs to try for the first param - * @param param1s array of inputs to try for the second param - * @param op callback called on each pair of inputs to produce each case - */ -export function generateBinaryToU32Cases( - param0s: readonly number[], - param1s: readonly number[], - op: BinaryOp -) { - return generateScalarBinaryToScalarCases(param0s, param1s, op, quantizeToU32, u32); -} - -/** - * @returns a Case for the input params with op applied - * @param scalar scalar param - * @param vector vector param (2, 3, or 4 elements) - * @param op the op to apply to scalar and vector - * @param quantize function to quantize all values in vectors and scalars - * @param scalarize function to convert numbers to Scalars - */ -function makeScalarVectorBinaryToVectorCase( - scalar: number, - vector: readonly number[], - op: BinaryOp, - quantize: QuantizeFunc, - scalarize: ScalarBuilder -): Case | undefined { - scalar = quantize(scalar); - vector = vector.map(quantize); - const result = vector.map(v => op(scalar, v)); - if (result.includes(undefined)) { - return undefined; - } - return { - input: [scalarize(scalar), new Vector(vector.map(scalarize))], - expected: new Vector((result as readonly number[]).map(scalarize)), - }; -} - -/** - * @returns array of Case for the input params with op applied - * @param scalars array of scalar params - * @param vectors array of vector params (2, 3, or 4 elements) - * @param op the op to apply to each pair of scalar and vector - * @param quantize function to quantize all values in vectors and scalars - * @param scalarize function to convert numbers to Scalars - */ -function generateScalarVectorBinaryToVectorCases( - scalars: readonly number[], - vectors: ROArrayArray<number>, - op: BinaryOp, - quantize: QuantizeFunc, - scalarize: ScalarBuilder -): Case[] { - const cases = new Array<Case>(); - scalars.forEach(s => { - vectors.forEach(v => { - const c = makeScalarVectorBinaryToVectorCase(s, v, op, quantize, scalarize); - if (c !== undefined) { - cases.push(c); - } - }); - }); - return cases; -} - -/** - * @returns a Case for the input params with op applied - * @param vector vector param (2, 3, or 4 elements) - * @param scalar scalar param - * @param op the op to apply to vector and scalar - * @param quantize function to quantize all values in vectors and scalars - * @param scalarize function to convert numbers to Scalars - */ -function makeVectorScalarBinaryToVectorCase( - vector: readonly number[], - scalar: number, - op: BinaryOp, - quantize: QuantizeFunc, - scalarize: ScalarBuilder -): Case | undefined { - vector = vector.map(quantize); - scalar = quantize(scalar); - const result = vector.map(v => op(v, scalar)); - if (result.includes(undefined)) { - return undefined; - } - return { - input: [new Vector(vector.map(scalarize)), scalarize(scalar)], - expected: new Vector((result as readonly number[]).map(scalarize)), - }; -} - -/** - * @returns array of Case for the input params with op applied - * @param vectors array of vector params (2, 3, or 4 elements) - * @param scalars array of scalar params - * @param op the op to apply to each pair of vector and scalar - * @param quantize function to quantize all values in vectors and scalars - * @param scalarize function to convert numbers to Scalars - */ -function generateVectorScalarBinaryToVectorCases( - vectors: ROArrayArray<number>, - scalars: readonly number[], - op: BinaryOp, - quantize: QuantizeFunc, - scalarize: ScalarBuilder -): Case[] { - const cases = new Array<Case>(); - scalars.forEach(s => { - vectors.forEach(v => { - const c = makeVectorScalarBinaryToVectorCase(v, s, op, quantize, scalarize); - if (c !== undefined) { - cases.push(c); - } - }); - }); - return cases; -} - -/** - * @returns array of Case for the input params with op applied - * @param scalars array of scalar params - * @param vectors array of vector params (2, 3, or 4 elements) - * @param op he op to apply to each pair of scalar and vector - */ -export function generateU32VectorBinaryToVectorCases( - scalars: readonly number[], - vectors: ROArrayArray<number>, - op: BinaryOp -): Case[] { - return generateScalarVectorBinaryToVectorCases(scalars, vectors, op, quantizeToU32, u32); -} - -/** - * @returns array of Case for the input params with op applied - * @param vectors array of vector params (2, 3, or 4 elements) - * @param scalars array of scalar params - * @param op he op to apply to each pair of vector and scalar - */ -export function generateVectorU32BinaryToVectorCases( - vectors: ROArrayArray<number>, - scalars: readonly number[], - op: BinaryOp -): Case[] { - return generateVectorScalarBinaryToVectorCases(vectors, scalars, op, quantizeToU32, u32); -} - -/** - * @returns array of Case for the input params with op applied - * @param scalars array of scalar params - * @param vectors array of vector params (2, 3, or 4 elements) - * @param op he op to apply to each pair of scalar and vector - */ -export function generateI32VectorBinaryToVectorCases( - scalars: readonly number[], - vectors: ROArrayArray<number>, - op: BinaryOp -): Case[] { - return generateScalarVectorBinaryToVectorCases(scalars, vectors, op, quantizeToI32, i32); -} - -/** - * @returns array of Case for the input params with op applied - * @param vectors array of vector params (2, 3, or 4 elements) - * @param scalars array of scalar params - * @param op he op to apply to each pair of vector and scalar - */ -export function generateVectorI32BinaryToVectorCases( - vectors: ROArrayArray<number>, - scalars: readonly number[], - op: BinaryOp -): Case[] { - return generateVectorScalarBinaryToVectorCases(vectors, scalars, op, quantizeToI32, i32); -} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/interval_filter.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/interval_filter.ts new file mode 100644 index 0000000000..7471247e54 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/interval_filter.ts @@ -0,0 +1,8 @@ +/** + * Indicates bounds that acceptance intervals need to be within to avoid inputs + * being filtered out. This is used for const-eval tests, since going OOB will + * cause a validation error not an execution error. + */ +export type IntervalFilter = + | 'finite' // Expected to be finite in the interval numeric space + | 'unfiltered'; // No expectations diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/precedence.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/precedence.spec.ts new file mode 100644 index 0000000000..643bda657e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/precedence.spec.ts @@ -0,0 +1,113 @@ +export const description = ` +Execution tests for operator precedence. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { GPUTest } from '../../../gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +// The list of test cases and their expected results. +interface Expression { + expr: string; + result: number; +} +const kExpressions: Record<string, Expression> = { + add_mul: { expr: 'kThree + kSeven * kEleven', result: 80 }, + mul_add: { expr: 'kThree * kSeven + kEleven', result: 32 }, + sub_neg: { expr: 'kThree - - kSeven', result: 10 }, + neg_shl: { expr: '- kThree << u32(kSeven)', result: -384 }, + neg_shr: { expr: '- kThree >> u32(kSeven)', result: -1 }, + neg_add: { expr: '- kThree + kSeven', result: 4 }, + neg_mul: { expr: '- kThree * kSeven', result: -21 }, + neg_and: { expr: '- kThree & kSeven', result: 5 }, + neg_or: { expr: '- kThree | kSeven', result: -1 }, + neg_xor: { expr: '- kThree ^ kSeven', result: -6 }, + comp_add: { expr: '~ kThree + kSeven', result: 3 }, + mul_deref: { expr: 'kThree * * ptr_five', result: 15 }, + not_and: { expr: 'i32(! kFalse && kFalse)', result: 0 }, + not_or: { expr: 'i32(! kTrue || kTrue)', result: 1 }, + eq_and: { expr: 'i32(kFalse == kTrue && kFalse)', result: 0 }, + and_eq: { expr: 'i32(kFalse && kTrue == kFalse)', result: 0 }, + eq_or: { expr: 'i32(kFalse == kFalse || kTrue)', result: 1 }, + or_eq: { expr: 'i32(kTrue || kFalse == kFalse)', result: 1 }, + add_swizzle: { expr: '(vec + vec . y) . z', result: 8 }, +}; + +g.test('precedence') + .desc( + ` + Test that operator precedence rules are correctly implemented. + ` + ) + .params(u => + u + .combine('expr', keysOf(kExpressions)) + .combine('decl', ['literal', 'const', 'override', 'var<private>']) + .combine('strip_spaces', [false, true]) + ) + .fn(t => { + const expr = kExpressions[t.params.expr]; + + let decl = t.params.decl; + let expr_wgsl = expr.expr; + if (t.params.decl === 'literal') { + decl = 'const'; + expr_wgsl = expr_wgsl.replace(/kThree/g, '3'); + expr_wgsl = expr_wgsl.replace(/kSeven/g, '7'); + expr_wgsl = expr_wgsl.replace(/kEleven/g, '11'); + expr_wgsl = expr_wgsl.replace(/kFalse/g, 'false'); + expr_wgsl = expr_wgsl.replace(/kTrue/g, 'true'); + } + if (t.params.strip_spaces) { + expr_wgsl = expr_wgsl.replace(/ /g, ''); + } + const wgsl = ` + @group(0) @binding(0) var<storage, read_write> buffer : i32; + + ${decl} kFalse = false; + ${decl} kTrue = true; + + ${decl} kThree = 3; + ${decl} kSeven = 7; + ${decl} kEleven = 11; + + @compute @workgroup_size(1) + fn main() { + var five = 5; + var vec = vec4(1, kThree, 5, kSeven); + let ptr_five = &five; + + buffer = ${expr_wgsl}; + } + `; + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ code: wgsl }), + }, + }); + + // Allocate a buffer and fill it with 0xdeadbeef. + const outputBuffer = t.makeBufferWithContents( + new Uint32Array([0xdeadbeef]), + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC + ); + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [{ binding: 0, resource: { buffer: outputBuffer } }], + }); + + // Run the shader. + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(1); + pass.end(); + t.queue.submit([encoder.finish()]); + + // Check that the result is as expected. + t.expectGPUBufferValuesEqual(outputBuffer, new Int32Array([expr.result])); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/address_of_and_indirection.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/address_of_and_indirection.spec.ts new file mode 100644 index 0000000000..c4961558e0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/address_of_and_indirection.spec.ts @@ -0,0 +1,171 @@ +export const description = ` +Execution Tests for unary address-of and indirection (dereference) +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../common/util/data_tables.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { ScalarKind, scalarType } from '../../../../util/conversion.js'; +import { sparseScalarF32Range } from '../../../../util/math.js'; +import { + allButConstInputSource, + basicExpressionWithPredeclarationBuilder, + run, +} from '../expression.js'; + +export const g = makeTestGroup(GPUTest); + +// All the ways to deref an expression +const kDerefCases = { + deref_address_of_identifier: { + wgsl: '(*(&a))', + requires_pointer_composite_access: false, + }, + deref_pointer: { + wgsl: '(*p)', + requires_pointer_composite_access: false, + }, + address_of_identifier: { + wgsl: '(&a)', + requires_pointer_composite_access: true, + }, + pointer: { + wgsl: 'p', + requires_pointer_composite_access: true, + }, +}; + +g.test('deref') + .specURL('https://www.w3.org/TR/WGSL/#indirection') + .desc( + ` +Expression: *e + +Pointer expression dereference. +` + ) + .params(u => + u + .combine('inputSource', allButConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('scalarType', ['bool', 'u32', 'i32', 'f32', 'f16'] as ScalarKind[]) + .combine('derefType', keysOf(kDerefCases)) + .filter(p => !kDerefCases[p.derefType].requires_pointer_composite_access) + ) + .beforeAllSubcases(t => { + if (t.params.scalarType === 'f16') { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + } + }) + .fn(async t => { + const ty = scalarType(t.params.scalarType); + const cases = sparseScalarF32Range().map(e => { + return { input: ty.create(e), expected: ty.create(e) }; + }); + const elemType = ty.kind; + const type = t.params.vectorize ? `vec${t.params.vectorize}<${elemType}>` : elemType; + const shaderBuilder = basicExpressionWithPredeclarationBuilder( + value => `get_dereferenced_value(${value})`, + `fn get_dereferenced_value(value: ${type}) -> ${type} { + var a = value; + let p = &a; + return ${kDerefCases[t.params.derefType].wgsl}; + }` + ); + await run(t, shaderBuilder, [ty], ty, t.params, cases); + }); + +g.test('deref_index') + .specURL('https://www.w3.org/TR/WGSL/#logical-expr') + .desc( + ` +Expression: (*e)[index] + +Pointer expression dereference as lhs of index accessor expression +` + ) + .params(u => + u + .combine('inputSource', allButConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('scalarType', ['bool', 'u32', 'i32', 'f32', 'f16'] as ScalarKind[]) + .combine('derefType', keysOf(kDerefCases)) + ) + .beforeAllSubcases(t => { + if (t.params.scalarType === 'f16') { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + } + }) + .fn(async t => { + if ( + kDerefCases[t.params.derefType].requires_pointer_composite_access && + !t.hasLanguageFeature('pointer_composite_access') + ) { + return; + } + + const ty = scalarType(t.params.scalarType); + const cases = sparseScalarF32Range().map(e => { + return { input: ty.create(e), expected: ty.create(e) }; + }); + const elemType = ty.kind; + const type = t.params.vectorize ? `vec${t.params.vectorize}<${elemType}>` : elemType; + const shaderBuilder = basicExpressionWithPredeclarationBuilder( + value => `get_dereferenced_value(${value})`, + `fn get_dereferenced_value(value: ${type}) -> ${type} { + var a = array<${type}, 1>(value); + let p = &a; + return ${kDerefCases[t.params.derefType].wgsl}[0]; + }` + ); + await run(t, shaderBuilder, [ty], ty, t.params, cases); + }); + +g.test('deref_member') + .specURL('https://www.w3.org/TR/WGSL/#logical-expr') + .desc( + ` +Expression: (*e).member + +Pointer expression dereference as lhs of member accessor expression +` + ) + .params(u => + u + .combine('inputSource', allButConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + .combine('scalarType', ['bool', 'u32', 'i32', 'f32', 'f16'] as ScalarKind[]) + .combine('derefType', keysOf(kDerefCases)) + ) + .beforeAllSubcases(t => { + if (t.params.scalarType === 'f16') { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + } + }) + .fn(async t => { + if ( + kDerefCases[t.params.derefType].requires_pointer_composite_access && + !t.hasLanguageFeature('pointer_composite_access') + ) { + return; + } + + const ty = scalarType(t.params.scalarType); + const cases = sparseScalarF32Range().map(e => { + return { input: ty.create(e), expected: ty.create(e) }; + }); + const elemType = ty.kind; + const type = t.params.vectorize ? `vec${t.params.vectorize}<${elemType}>` : elemType; + const shaderBuilder = basicExpressionWithPredeclarationBuilder( + value => `get_dereferenced_value(${value})`, + `struct S { + m : ${type} + } + fn get_dereferenced_value(value: ${type}) -> ${type} { + var a = S(value); + let p = &a; + return ${kDerefCases[t.params.derefType].wgsl}.m; + }` + ); + await run(t, shaderBuilder, [ty], ty, t.params, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_arithmetic.cache.ts new file mode 100644 index 0000000000..4f274e8922 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_arithmetic.cache.ts @@ -0,0 +1,13 @@ +import { FP } from '../../../../util/floating_point.js'; +import { scalarF64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/af_arithmetic', { + negation: () => { + return FP.abstract.generateScalarToIntervalCases( + scalarF64Range({ neg_norm: 250, neg_sub: 20, pos_sub: 20, pos_norm: 250 }), + 'unfiltered', + FP.abstract.negationInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_arithmetic.spec.ts index 182c0d76a9..686d4b7c4a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_arithmetic.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_arithmetic.spec.ts @@ -1,29 +1,17 @@ export const description = ` -Execution Tests for AbstractFloat arithmetic unary expression operations +Execution Tests for Type.abstractFloat arithmetic unary expression operations `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeAbstractFloat } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { fullF64Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { onlyConstInputSource, run } from '../expression.js'; -import { abstractUnary } from './unary.js'; +import { d } from './af_arithmetic.cache.js'; +import { abstractFloatUnary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/af_arithmetic', { - negation: () => { - return FP.abstract.generateScalarToIntervalCases( - fullF64Range({ neg_norm: 250, neg_sub: 20, pos_sub: 20, pos_norm: 250 }), - 'unfiltered', - FP.abstract.negationInterval - ); - }, -}); - g.test('negation') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -39,5 +27,13 @@ Accuracy: Correctly rounded ) .fn(async t => { const cases = await d.get('negation'); - await run(t, abstractUnary('-'), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases, 1); + await run( + t, + abstractFloatUnary('-'), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases, + 1 + ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_assignment.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_assignment.cache.ts new file mode 100644 index 0000000000..7c607927fe --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_assignment.cache.ts @@ -0,0 +1,51 @@ +import { kValue } from '../../../../util/constants.js'; +import { abstractFloat } from '../../../../util/conversion.js'; +import { FP } from '../../../../util/floating_point.js'; +import { + isSubnormalNumberF64, + limitedScalarF64Range, + scalarF64Range, +} from '../../../../util/math.js'; +import { reinterpretU64AsF64 } from '../../../../util/reinterpret.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/af_assignment', { + abstract: () => { + const inputs = [ + // Values that are useful for debugging the underlying framework/shader code, since it cannot be directly unit tested. + 0, + 0.5, + 0.5, + 1, + -1, + reinterpretU64AsF64(0x7000_0000_0000_0001n), // smallest magnitude negative subnormal with non-zero mantissa + reinterpretU64AsF64(0x0000_0000_0000_0001n), // smallest magnitude positive subnormal with non-zero mantissa + reinterpretU64AsF64(0x600a_aaaa_5555_5555n), // negative subnormal with obvious pattern + reinterpretU64AsF64(0x000a_aaaa_5555_5555n), // positive subnormal with obvious pattern + reinterpretU64AsF64(0x0010_0000_0000_0001n), // smallest magnitude negative normal with non-zero mantissa + reinterpretU64AsF64(0x0010_0000_0000_0001n), // smallest magnitude positive normal with non-zero mantissa + reinterpretU64AsF64(0xf555_5555_aaaa_aaaan), // negative normal with obvious pattern + reinterpretU64AsF64(0x5555_5555_aaaa_aaaan), // positive normal with obvious pattern + reinterpretU64AsF64(0xffef_ffff_ffff_ffffn), // largest magnitude negative normal + reinterpretU64AsF64(0x7fef_ffff_ffff_ffffn), // largest magnitude positive normal + // WebGPU implementation stressing values + ...scalarF64Range(), + ]; + return inputs.map(f => { + return { + input: abstractFloat(f), + expected: isSubnormalNumberF64(f) ? abstractFloat(0) : abstractFloat(f), + }; + }); + }, + f32: () => { + return limitedScalarF64Range(kValue.f32.negative.min, kValue.f32.positive.max).map(f => { + return { input: abstractFloat(f), expected: FP.f32.correctlyRoundedInterval(f) }; + }); + }, + f16: () => { + return limitedScalarF64Range(kValue.f16.negative.min, kValue.f16.positive.max).map(f => { + return { input: abstractFloat(f), expected: FP.f16.correctlyRoundedInterval(f) }; + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_assignment.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_assignment.spec.ts index 141d87d0f2..001c47e117 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_assignment.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/af_assignment.spec.ts @@ -4,20 +4,17 @@ Execution Tests for assignment of AbstractFloats import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { kValue } from '../../../../util/constants.js'; -import { abstractFloat, TypeAbstractFloat, TypeF16, TypeF32 } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { filteredF64Range, fullF64Range, isSubnormalNumberF64 } from '../../../../util/math.js'; -import { reinterpretU64AsF64 } from '../../../../util/reinterpret.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { + ShaderBuilder, abstractFloatShaderBuilder, basicExpressionBuilder, onlyConstInputSource, run, - ShaderBuilder, } from '../expression.js'; +import { d } from './af_assignment.cache.js'; + function concrete_assignment(): ShaderBuilder { return basicExpressionBuilder(value => `${value}`); } @@ -28,47 +25,6 @@ function abstract_assignment(): ShaderBuilder { export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/af_assignment', { - abstract: () => { - const inputs = [ - // Values that are useful for debugging the underlying framework/shader code, since it cannot be directly unit tested. - 0, - 0.5, - 0.5, - 1, - -1, - reinterpretU64AsF64(0x7000_0000_0000_0001n), // smallest magnitude negative subnormal with non-zero mantissa - reinterpretU64AsF64(0x0000_0000_0000_0001n), // smallest magnitude positive subnormal with non-zero mantissa - reinterpretU64AsF64(0x600a_aaaa_5555_5555n), // negative subnormal with obvious pattern - reinterpretU64AsF64(0x000a_aaaa_5555_5555n), // positive subnormal with obvious pattern - reinterpretU64AsF64(0x0010_0000_0000_0001n), // smallest magnitude negative normal with non-zero mantissa - reinterpretU64AsF64(0x0010_0000_0000_0001n), // smallest magnitude positive normal with non-zero mantissa - reinterpretU64AsF64(0xf555_5555_aaaa_aaaan), // negative normal with obvious pattern - reinterpretU64AsF64(0x5555_5555_aaaa_aaaan), // positive normal with obvious pattern - reinterpretU64AsF64(0xffef_ffff_ffff_ffffn), // largest magnitude negative normal - reinterpretU64AsF64(0x7fef_ffff_ffff_ffffn), // largest magnitude positive normal - // WebGPU implementation stressing values - ...fullF64Range(), - ]; - return inputs.map(f => { - return { - input: abstractFloat(f), - expected: isSubnormalNumberF64(f) ? abstractFloat(0) : abstractFloat(f), - }; - }); - }, - f32: () => { - return filteredF64Range(kValue.f32.negative.min, kValue.f32.positive.max).map(f => { - return { input: abstractFloat(f), expected: FP.f32.correctlyRoundedInterval(f) }; - }); - }, - f16: () => { - return filteredF64Range(kValue.f16.negative.min, kValue.f16.positive.max).map(f => { - return { input: abstractFloat(f), expected: FP.f16.correctlyRoundedInterval(f) }; - }); - }, -}); - g.test('abstract') .specURL('https://www.w3.org/TR/WGSL/#floating-point-conversion') .desc( @@ -79,7 +35,15 @@ testing that extracting abstract floats works .params(u => u.combine('inputSource', onlyConstInputSource)) .fn(async t => { const cases = await d.get('abstract'); - await run(t, abstract_assignment(), [TypeAbstractFloat], TypeAbstractFloat, t.params, cases, 1); + await run( + t, + abstract_assignment(), + [Type.abstractFloat], + Type.abstractFloat, + t.params, + cases, + 1 + ); }); g.test('f32') @@ -92,7 +56,7 @@ concretizing to f32 .params(u => u.combine('inputSource', onlyConstInputSource)) .fn(async t => { const cases = await d.get('f32'); - await run(t, concrete_assignment(), [TypeAbstractFloat], TypeF32, t.params, cases); + await run(t, concrete_assignment(), [Type.abstractFloat], Type.f32, t.params, cases); }); g.test('f16') @@ -108,5 +72,5 @@ concretizing to f16 .params(u => u.combine('inputSource', onlyConstInputSource)) .fn(async t => { const cases = await d.get('f16'); - await run(t, concrete_assignment(), [TypeAbstractFloat], TypeF16, t.params, cases); + await run(t, concrete_assignment(), [Type.abstractFloat], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_arithmetic.cache.ts new file mode 100644 index 0000000000..904c0bc700 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_arithmetic.cache.ts @@ -0,0 +1,11 @@ +import { abstractInt } from '../../../../util/conversion.js'; +import { fullI64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/ai_arithmetic', { + negation: () => { + return fullI64Range().map(e => { + return { input: abstractInt(e), expected: abstractInt(-e) }; + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_arithmetic.spec.ts new file mode 100644 index 0000000000..625a38b2d5 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_arithmetic.spec.ts @@ -0,0 +1,30 @@ +export const description = ` +Execution Tests for the abstract integer arithmetic unary expression operations +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { Type } from '../../../../util/conversion.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { d } from './ai_arithmetic.cache.js'; +import { abstractIntUnary } from './unary.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('negation') + .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr') + .desc( + ` +Expression: -x +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('negation'); + await run(t, abstractIntUnary('-'), [Type.abstractInt], Type.abstractInt, t.params, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_assignment.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_assignment.cache.ts new file mode 100644 index 0000000000..0fa4f2efc2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_assignment.cache.ts @@ -0,0 +1,21 @@ +import { abstractInt, i32, u32 } from '../../../../util/conversion.js'; +import { fullI32Range, fullI64Range, fullU32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/ai_assignment', { + abstract: () => { + return fullI64Range().map(n => { + return { input: abstractInt(n), expected: abstractInt(n) }; + }); + }, + i32: () => { + return fullI32Range().map(n => { + return { input: abstractInt(BigInt(n)), expected: i32(n) }; + }); + }, + u32: () => { + return fullU32Range().map(n => { + return { input: abstractInt(BigInt(n)), expected: u32(n) }; + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_assignment.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_assignment.spec.ts new file mode 100644 index 0000000000..fe1ba2d9fc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_assignment.spec.ts @@ -0,0 +1,65 @@ +export const description = ` +Execution Tests for assignment of AbstractInts +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { Type } from '../../../../util/conversion.js'; +import { + ShaderBuilder, + abstractIntShaderBuilder, + basicExpressionBuilder, + onlyConstInputSource, + run, +} from '../expression.js'; + +import { d } from './ai_assignment.cache.js'; + +function concrete_assignment(): ShaderBuilder { + return basicExpressionBuilder(value => `${value}`); +} + +function abstract_assignment(): ShaderBuilder { + return abstractIntShaderBuilder(value => `${value}`); +} + +export const g = makeTestGroup(GPUTest); + +g.test('abstract') + .specURL('https://www.w3.org/TR/WGSL/#abstract-types') + .desc( + ` +testing that extracting abstract ints works +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('abstract'); + await run(t, abstract_assignment(), [Type.abstractInt], Type.abstractInt, t.params, cases, 1); + }); + +g.test('i32') + .specURL('https://www.w3.org/TR/WGSL/#i32-builtin') + .desc( + ` +concretizing to i32 +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('i32'); + await run(t, concrete_assignment(), [Type.abstractInt], Type.i32, t.params, cases); + }); + +g.test('u32') + .specURL('https://www.w3.org/TR/WGSL/#u32-builtin') + .desc( + ` +concretizing to u32 +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('u32'); + await run(t, concrete_assignment(), [Type.abstractInt], Type.u32, t.params, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_complement.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_complement.spec.ts new file mode 100644 index 0000000000..507ae219cb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/ai_complement.spec.ts @@ -0,0 +1,32 @@ +export const description = ` +Execution Tests for the Type.abstractInt bitwise complement operation +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { abstractInt, Type } from '../../../../util/conversion.js'; +import { fullI64Range } from '../../../../util/math.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { abstractIntUnary } from './unary.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('complement') + .specURL('https://www.w3.org/TR/WGSL/#bit-expr') + .desc( + ` +Expression: ~x +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = fullI64Range().map(e => { + return { input: abstractInt(e), expected: abstractInt(~e) }; + }); + await run(t, abstractIntUnary('~'), [Type.abstractInt], Type.abstractInt, t.params, cases); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_conversion.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_conversion.cache.ts new file mode 100644 index 0000000000..5b16c49c42 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_conversion.cache.ts @@ -0,0 +1,54 @@ +import { anyOf } from '../../../../util/compare.js'; +import { ScalarValue, bool, f16, f32, i32, u32 } from '../../../../util/conversion.js'; +import { + fullI32Range, + fullU32Range, + isSubnormalNumberF16, + isSubnormalNumberF32, + scalarF16Range, + scalarF32Range, +} from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/bool_conversion', { + bool: () => { + return [ + { input: bool(true), expected: bool(true) }, + { input: bool(false), expected: bool(false) }, + ]; + }, + u32: () => { + return fullU32Range().map(u => { + return { input: u32(u), expected: u === 0 ? bool(false) : bool(true) }; + }); + }, + i32: () => { + return fullI32Range().map(i => { + return { input: i32(i), expected: i === 0 ? bool(false) : bool(true) }; + }); + }, + f32: () => { + return scalarF32Range().map(f => { + const expected: ScalarValue[] = []; + if (f !== 0) { + expected.push(bool(true)); + } + if (isSubnormalNumberF32(f)) { + expected.push(bool(false)); + } + return { input: f32(f), expected: anyOf(...expected) }; + }); + }, + f16: () => { + return scalarF16Range().map(f => { + const expected: ScalarValue[] = []; + if (f !== 0) { + expected.push(bool(true)); + } + if (isSubnormalNumberF16(f)) { + expected.push(bool(false)); + } + return { input: f16(f), expected: anyOf(...expected) }; + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_conversion.spec.ts index 8fcfed339f..55d01d3eda 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_conversion.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_conversion.spec.ts @@ -4,78 +4,14 @@ Execution Tests for the boolean conversion operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { anyOf } from '../../../../util/compare.js'; -import { - bool, - f32, - f16, - i32, - Scalar, - TypeBool, - TypeF32, - TypeF16, - TypeI32, - TypeU32, - u32, -} from '../../../../util/conversion.js'; -import { - fullF32Range, - fullF16Range, - fullI32Range, - fullU32Range, - isSubnormalNumberF32, - isSubnormalNumberF16, -} from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, run, ShaderBuilder } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { ShaderBuilder, allInputSources, run } from '../expression.js'; +import { d } from './bool_conversion.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/bool_conversion', { - bool: () => { - return [ - { input: bool(true), expected: bool(true) }, - { input: bool(false), expected: bool(false) }, - ]; - }, - u32: () => { - return fullU32Range().map(u => { - return { input: u32(u), expected: u === 0 ? bool(false) : bool(true) }; - }); - }, - i32: () => { - return fullI32Range().map(i => { - return { input: i32(i), expected: i === 0 ? bool(false) : bool(true) }; - }); - }, - f32: () => { - return fullF32Range().map(f => { - const expected: Scalar[] = []; - if (f !== 0) { - expected.push(bool(true)); - } - if (isSubnormalNumberF32(f)) { - expected.push(bool(false)); - } - return { input: f32(f), expected: anyOf(...expected) }; - }); - }, - f16: () => { - return fullF16Range().map(f => { - const expected: Scalar[] = []; - if (f !== 0) { - expected.push(bool(true)); - } - if (isSubnormalNumberF16(f)) { - expected.push(bool(false)); - } - return { input: f16(f), expected: anyOf(...expected) }; - }); - }, -}); - /** Generate expression builder based on how the test case is to be vectorized */ function vectorizeToExpression(vectorize: undefined | 2 | 3 | 4): ShaderBuilder { return vectorize === undefined ? unary('bool') : unary(`vec${vectorize}<bool>`); @@ -95,7 +31,14 @@ Identity operation ) .fn(async t => { const cases = await d.get('bool'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeBool], TypeBool, t.params, cases); + await run( + t, + vectorizeToExpression(t.params.vectorize), + [Type.bool], + Type.bool, + t.params, + cases + ); }); g.test('u32') @@ -113,7 +56,7 @@ The result is false if e is 0, and true otherwise. ) .fn(async t => { const cases = await d.get('u32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeU32], TypeBool, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.u32], Type.bool, t.params, cases); }); g.test('i32') @@ -131,7 +74,7 @@ The result is false if e is 0, and true otherwise. ) .fn(async t => { const cases = await d.get('i32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeI32], TypeBool, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.i32], Type.bool, t.params, cases); }); g.test('f32') @@ -149,7 +92,7 @@ The result is false if e is 0.0 or -0.0, and true otherwise. ) .fn(async t => { const cases = await d.get('f32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF32], TypeBool, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f32], Type.bool, t.params, cases); }); g.test('f16') @@ -170,5 +113,5 @@ The result is false if e is 0.0 or -0.0, and true otherwise. }) .fn(async t => { const cases = await d.get('f16'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF16], TypeBool, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f16], Type.bool, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts index 01eaaab43a..58d4b9fc0f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts @@ -4,7 +4,7 @@ Execution Tests for the boolean unary logical expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { bool, TypeBool } from '../../../../util/conversion.js'; +import { bool, Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; import { unary } from './unary.js'; @@ -29,5 +29,5 @@ Logical negation. The result is true when e is false and false when e is true. C { input: bool(false), expected: bool(true) }, ]; - await run(t, unary('!'), [TypeBool], TypeBool, t.params, cases); + await run(t, unary('!'), [Type.bool], Type.bool, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_arithmetic.cache.ts new file mode 100644 index 0000000000..7d9ee35eec --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_arithmetic.cache.ts @@ -0,0 +1,13 @@ +import { FP } from '../../../../util/floating_point.js'; +import { scalarF16Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/f16_arithmetic', { + negation: () => { + return FP.f16.generateScalarToIntervalCases( + scalarF16Range({ neg_norm: 250, neg_sub: 20, pos_sub: 20, pos_norm: 250 }), + 'unfiltered', + FP.f16.negationInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_arithmetic.spec.ts index 83d7579c07..813bbf7a64 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_arithmetic.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_arithmetic.spec.ts @@ -4,26 +4,14 @@ Execution Tests for the f16 arithmetic unary expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF16 } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { fullF16Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; +import { d } from './f16_arithmetic.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/f16_arithmetic', { - negation: () => { - return FP.f16.generateScalarToIntervalCases( - fullF16Range({ neg_norm: 250, neg_sub: 20, pos_sub: 20, pos_norm: 250 }), - 'unfiltered', - FP.f16.negationInterval - ); - }, -}); - g.test('negation') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -40,5 +28,5 @@ Accuracy: Correctly rounded }) .fn(async t => { const cases = await d.get('negation'); - await run(t, unary('-'), [TypeF16], TypeF16, t.params, cases); + await run(t, unary('-'), [Type.f16], Type.f16, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_conversion.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_conversion.cache.ts new file mode 100644 index 0000000000..bb0eb091dd --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_conversion.cache.ts @@ -0,0 +1,135 @@ +import { abstractInt, bool, f16, i32, u32 } from '../../../../util/conversion.js'; +import { FP, FPInterval } from '../../../../util/floating_point.js'; +import { fullI32Range, fullI64Range, fullU32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +const f16FiniteRangeInterval = new FPInterval( + 'f16', + FP.f16.constants().negative.min, + FP.f16.constants().positive.max +); + +// Cases: f32_matCxR_[non_]const +// Note that f32 values may be not exactly representable in f16 and/or out of range. +const f32_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`f32_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixToMatrixCases( + FP.f32.sparseMatrixRange(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f16.correctlyRoundedMatrix + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: f16_matCxR_[non_]const +const f16_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`f16_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + // Input matrix is of f16 types, use f16.generateMatrixToMatrixCases. + return FP.f16.generateMatrixToMatrixCases( + FP.f16.sparseMatrixRange(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f16.correctlyRoundedMatrix + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: abstract_float_matCxR +// Note that abstract float values may be not exactly representable in f16 +// and/or out of range. +const abstract_float_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).map(rows => ({ + [`abstract_float_mat${cols}x${rows}`]: () => { + return FP.abstract.generateMatrixToMatrixCases( + FP.abstract.sparseMatrixRange(cols, rows), + 'finite', + FP.f16.correctlyRoundedMatrix + ); + }, + })) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('unary/f16_conversion', { + bool: () => { + return [ + { input: bool(true), expected: f16(1.0) }, + { input: bool(false), expected: f16(0.0) }, + ]; + }, + u32_non_const: () => { + return [...fullU32Range(), 65504].map(u => { + return { input: u32(u), expected: FP.f16.correctlyRoundedInterval(u) }; + }); + }, + u32_const: () => { + return [...fullU32Range(), 65504] + .filter(v => f16FiniteRangeInterval.contains(v)) + .map(u => { + return { input: u32(u), expected: FP.f16.correctlyRoundedInterval(u) }; + }); + }, + i32_non_const: () => { + return [...fullI32Range(), 65504, -65504].map(i => { + return { input: i32(i), expected: FP.f16.correctlyRoundedInterval(i) }; + }); + }, + i32_const: () => { + return [...fullI32Range(), 65504, -65504] + .filter(v => f16FiniteRangeInterval.contains(v)) + .map(i => { + return { input: i32(i), expected: FP.f16.correctlyRoundedInterval(i) }; + }); + }, + abstract_int: () => { + return [...fullI64Range(), 65504n, -65504n] + .filter(v => f16FiniteRangeInterval.contains(Number(v))) + .map(i => { + return { input: abstractInt(i), expected: FP.f16.correctlyRoundedInterval(Number(i)) }; + }); + }, + // Note that f32 values may be not exactly representable in f16 and/or out of range. + f32_non_const: () => { + return FP.f32.generateScalarToIntervalCases( + [...FP.f32.scalarRange(), 65535.996, -65535.996], + 'unfiltered', + FP.f16.correctlyRoundedInterval + ); + }, + f32_const: () => { + return FP.f32.generateScalarToIntervalCases( + [...FP.f32.scalarRange(), 65535.996, -65535.996], + 'finite', + FP.f16.correctlyRoundedInterval + ); + }, + // Note that abstract float values may be not exactly representable in f16. + abstract_float: () => { + return FP.abstract.generateScalarToIntervalCases( + [...FP.abstract.scalarRange(), 65535.996, -65535.996], + 'finite', + FP.f16.correctlyRoundedInterval + ); + }, + // All f16 values are exactly representable in f16. + f16: () => { + return FP.f16.scalarRange().map(f => { + return { input: f16(f), expected: FP.f16.correctlyRoundedInterval(f) }; + }); + }, + ...f32_mat_cases, + ...f16_mat_cases, + ...abstract_float_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_conversion.spec.ts index 9eb84f0270..92bd9c6a07 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_conversion.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f16_conversion.spec.ts @@ -4,132 +4,14 @@ Execution Tests for the f32 conversion operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { - bool, - f16, - i32, - TypeBool, - TypeF32, - TypeF16, - TypeI32, - TypeMat, - TypeU32, - u32, -} from '../../../../util/conversion.js'; -import { FP, FPInterval } from '../../../../util/floating_point.js'; -import { - fullF32Range, - fullF16Range, - fullI32Range, - fullU32Range, - sparseMatrixF32Range, - sparseMatrixF16Range, -} from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, run, ShaderBuilder } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { ShaderBuilder, allInputSources, run, onlyConstInputSource } from '../expression.js'; +import { d } from './f16_conversion.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -const f16FiniteRangeInterval = new FPInterval( - 'f32', - FP.f16.constants().negative.min, - FP.f16.constants().positive.max -); - -// Cases: f32_matCxR_[non_]const -// Note that f32 values may be not exactly representable in f16 and/or out of range. -const f32_mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`f32_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixToMatrixCases( - sparseMatrixF32Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f16.correctlyRoundedMatrix - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_matCxR_[non_]const -const f16_mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`f16_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - // Input matrix is of f16 types, use f16.generateMatrixToMatrixCases. - return FP.f16.generateMatrixToMatrixCases( - sparseMatrixF16Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f16.correctlyRoundedMatrix - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('unary/f16_conversion', { - bool: () => { - return [ - { input: bool(true), expected: f16(1.0) }, - { input: bool(false), expected: f16(0.0) }, - ]; - }, - u32_non_const: () => { - return [...fullU32Range(), 65504].map(u => { - return { input: u32(u), expected: FP.f16.correctlyRoundedInterval(u) }; - }); - }, - u32_const: () => { - return [...fullU32Range(), 65504] - .filter(v => f16FiniteRangeInterval.contains(v)) - .map(u => { - return { input: u32(u), expected: FP.f16.correctlyRoundedInterval(u) }; - }); - }, - i32_non_const: () => { - return [...fullI32Range(), 65504, -65504].map(i => { - return { input: i32(i), expected: FP.f16.correctlyRoundedInterval(i) }; - }); - }, - i32_const: () => { - return [...fullI32Range(), 65504, -65504] - .filter(v => f16FiniteRangeInterval.contains(v)) - .map(i => { - return { input: i32(i), expected: FP.f16.correctlyRoundedInterval(i) }; - }); - }, - // Note that f32 values may be not exactly representable in f16 and/or out of range. - f32_non_const: () => { - return FP.f32.generateScalarToIntervalCases( - [...fullF32Range(), 65535.996, -65535.996], - 'unfiltered', - FP.f16.correctlyRoundedInterval - ); - }, - f32_const: () => { - return FP.f32.generateScalarToIntervalCases( - [...fullF32Range(), 65535.996, -65535.996], - 'finite', - FP.f16.correctlyRoundedInterval - ); - }, - // All f16 values are exactly representable in f16. - f16: () => { - return fullF16Range().map(f => { - return { input: f16(f), expected: FP.f16.correctlyRoundedInterval(f) }; - }); - }, - ...f32_mat_cases, - ...f16_mat_cases, -}); - /** Generate a ShaderBuilder based on how the test case is to be vectorized */ function vectorizeToExpression(vectorize: undefined | 2 | 3 | 4): ShaderBuilder { return vectorize === undefined ? unary('f16') : unary(`vec${vectorize}<f16>`); @@ -157,7 +39,7 @@ The result is 1.0 if e is true and 0.0 otherwise }) .fn(async t => { const cases = await d.get('bool'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeBool], TypeF16, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.bool], Type.f16, t.params, cases); }); g.test('u32') @@ -177,7 +59,7 @@ Converted to f16, +/-Inf if out of range }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeU32], TypeF16, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.u32], Type.f16, t.params, cases); }); g.test('i32') @@ -197,7 +79,36 @@ Converted to f16, +/-Inf if out of range }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'i32_const' : 'i32_non_const'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeI32], TypeF16, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.i32], Type.f16, t.params, cases); + }); + +g.test('abstract_int') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc( + ` +f16(e), where e is an AbstractInt + +Converted to f16, +/-Inf if out of range +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + }) + .fn(async t => { + const cases = await d.get('abstract_int'); + await run( + t, + vectorizeToExpression(t.params.vectorize), + [Type.abstractInt], + Type.f16, + t.params, + cases + ); }); g.test('f32') @@ -217,7 +128,7 @@ Correctly rounded to f16 }) .fn(async t => { const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF32], TypeF16, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f32], Type.f16, t.params, cases); }); g.test('f32_mat') @@ -243,8 +154,8 @@ g.test('f32_mat') await run( t, matrixExperession(cols, rows), - [TypeMat(cols, rows, TypeF32)], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f32)], + Type.mat(cols, rows, Type.f16), t.params, cases ); @@ -267,7 +178,7 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF16], TypeF16, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f16], Type.f16, t.params, cases); }); g.test('f16_mat') @@ -293,8 +204,63 @@ g.test('f16_mat') await run( t, matrixExperession(cols, rows), - [TypeMat(cols, rows, TypeF16)], - TypeMat(cols, rows, TypeF16), + [Type.mat(cols, rows, Type.f16)], + Type.mat(cols, rows, Type.f16), + t.params, + cases + ); + }); + +g.test('abstract_float') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc( + ` +f16(e), where e is an AbstractFloat + +Correctly rounded to f16 +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + }) + .fn(async t => { + const cases = await d.get('abstract_float'); + await run( + t, + vectorizeToExpression(t.params.vectorize), + [Type.abstractFloat], + Type.f16, + t.params, + cases + ); + }); + +g.test('abstract_float_mat') + .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions') + .desc(`AbstractFloat matrix to f16 matrix tests`) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('cols', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + }) + .fn(async t => { + const cols = t.params.cols; + const rows = t.params.rows; + const cases = await d.get(`abstract_float_mat${cols}x${rows}`); + await run( + t, + matrixExperession(cols, rows), + [Type.mat(cols, rows, Type.abstractFloat)], + Type.mat(cols, rows, Type.f16), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.cache.ts new file mode 100644 index 0000000000..b23ed3216b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.cache.ts @@ -0,0 +1,13 @@ +import { FP } from '../../../../util/floating_point.js'; +import { scalarF32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/f32_arithmetic', { + negation: () => { + return FP.f32.generateScalarToIntervalCases( + scalarF32Range({ neg_norm: 250, neg_sub: 20, pos_sub: 20, pos_norm: 250 }), + 'unfiltered', + FP.f32.negationInterval + ); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts index f53cff46d8..3bb48705e8 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts @@ -4,26 +4,14 @@ Execution Tests for the f32 arithmetic unary expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { TypeF32 } from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { fullF32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; +import { d } from './f32_arithmetic.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/f32_arithmetic', { - negation: () => { - return FP.f32.generateScalarToIntervalCases( - fullF32Range({ neg_norm: 250, neg_sub: 20, pos_sub: 20, pos_norm: 250 }), - 'unfiltered', - FP.f32.negationInterval - ); - }, -}); - g.test('negation') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -37,5 +25,5 @@ Accuracy: Correctly rounded ) .fn(async t => { const cases = await d.get('negation'); - await run(t, unary('-'), [TypeF32], TypeF32, t.params, cases); + await run(t, unary('-'), [Type.f32], Type.f32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_conversion.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_conversion.cache.ts new file mode 100644 index 0000000000..f61435f07c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_conversion.cache.ts @@ -0,0 +1,79 @@ +import { bool, f16, f32, i32, u32 } from '../../../../util/conversion.js'; +import { FP } from '../../../../util/floating_point.js'; +import { + fullI32Range, + fullU32Range, + scalarF16Range, + scalarF32Range, + sparseMatrixF16Range, + sparseMatrixF32Range, +} from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +// Cases: f32_matCxR_[non_]const +const f32_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`f32_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + return FP.f32.generateMatrixToMatrixCases( + sparseMatrixF32Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f32.correctlyRoundedMatrix + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +// Cases: f16_matCxR_[non_]const +// Note that all f16 values are exactly representable in f32. +const f16_mat_cases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + ([true, false] as const).map(nonConst => ({ + [`f16_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { + // Input matrix is of f16 types, use f16.generateMatrixToMatrixCases. + return FP.f16.generateMatrixToMatrixCases( + sparseMatrixF16Range(cols, rows), + nonConst ? 'unfiltered' : 'finite', + FP.f32.correctlyRoundedMatrix + ); + }, + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('unary/f32_conversion', { + bool: () => { + return [ + { input: bool(true), expected: f32(1.0) }, + { input: bool(false), expected: f32(0.0) }, + ]; + }, + u32: () => { + return fullU32Range().map(u => { + return { input: u32(u), expected: FP.f32.correctlyRoundedInterval(u) }; + }); + }, + i32: () => { + return fullI32Range().map(i => { + return { input: i32(i), expected: FP.f32.correctlyRoundedInterval(i) }; + }); + }, + f32: () => { + return scalarF32Range().map(f => { + return { input: f32(f), expected: FP.f32.correctlyRoundedInterval(f) }; + }); + }, + // All f16 values are exactly representable in f32. + f16: () => { + return scalarF16Range().map(f => { + return { input: f16(f), expected: FP.f32.correctlyRoundedInterval(f) }; + }); + }, + ...f32_mat_cases, + ...f16_mat_cases, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_conversion.spec.ts index 223b13c2d5..464fdee44e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_conversion.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_conversion.spec.ts @@ -4,103 +4,14 @@ Execution Tests for the f32 conversion operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { - bool, - f32, - f16, - i32, - TypeBool, - TypeF32, - TypeF16, - TypeI32, - TypeMat, - TypeU32, - u32, -} from '../../../../util/conversion.js'; -import { FP } from '../../../../util/floating_point.js'; -import { - fullF32Range, - fullF16Range, - fullI32Range, - fullU32Range, - sparseMatrixF32Range, - sparseMatrixF16Range, -} from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, run, ShaderBuilder } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { ShaderBuilder, allInputSources, run } from '../expression.js'; +import { d } from './f32_conversion.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -// Cases: f32_matCxR_[non_]const -const f32_mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`f32_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - return FP.f32.generateMatrixToMatrixCases( - sparseMatrixF32Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f32.correctlyRoundedMatrix - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -// Cases: f16_matCxR_[non_]const -// Note that all f16 values are exactly representable in f32. -const f16_mat_cases = ([2, 3, 4] as const) - .flatMap(cols => - ([2, 3, 4] as const).flatMap(rows => - ([true, false] as const).map(nonConst => ({ - [`f16_mat${cols}x${rows}_${nonConst ? 'non_const' : 'const'}`]: () => { - // Input matrix is of f16 types, use f16.generateMatrixToMatrixCases. - return FP.f16.generateMatrixToMatrixCases( - sparseMatrixF16Range(cols, rows), - nonConst ? 'unfiltered' : 'finite', - FP.f32.correctlyRoundedMatrix - ); - }, - })) - ) - ) - .reduce((a, b) => ({ ...a, ...b }), {}); - -export const d = makeCaseCache('unary/f32_conversion', { - bool: () => { - return [ - { input: bool(true), expected: f32(1.0) }, - { input: bool(false), expected: f32(0.0) }, - ]; - }, - u32: () => { - return fullU32Range().map(u => { - return { input: u32(u), expected: FP.f32.correctlyRoundedInterval(u) }; - }); - }, - i32: () => { - return fullI32Range().map(i => { - return { input: i32(i), expected: FP.f32.correctlyRoundedInterval(i) }; - }); - }, - f32: () => { - return fullF32Range().map(f => { - return { input: f32(f), expected: FP.f32.correctlyRoundedInterval(f) }; - }); - }, - // All f16 values are exactly representable in f32. - f16: () => { - return fullF16Range().map(f => { - return { input: f16(f), expected: FP.f32.correctlyRoundedInterval(f) }; - }); - }, - ...f32_mat_cases, - ...f16_mat_cases, -}); - /** Generate a ShaderBuilder based on how the test case is to be vectorized */ function vectorizeToExpression(vectorize: undefined | 2 | 3 | 4): ShaderBuilder { return vectorize === undefined ? unary('f32') : unary(`vec${vectorize}<f32>`); @@ -125,7 +36,7 @@ The result is 1.0 if e is true and 0.0 otherwise ) .fn(async t => { const cases = await d.get('bool'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeBool], TypeF32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.bool], Type.f32, t.params, cases); }); g.test('u32') @@ -142,7 +53,7 @@ Converted to f32 ) .fn(async t => { const cases = await d.get('u32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeU32], TypeF32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.u32], Type.f32, t.params, cases); }); g.test('i32') @@ -159,7 +70,7 @@ Converted to f32 ) .fn(async t => { const cases = await d.get('i32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeI32], TypeF32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.i32], Type.f32, t.params, cases); }); g.test('f32') @@ -176,7 +87,7 @@ Identity operation ) .fn(async t => { const cases = await d.get('f32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF32], TypeF32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f32], Type.f32, t.params, cases); }); g.test('f32_mat') @@ -199,8 +110,8 @@ g.test('f32_mat') await run( t, matrixExperession(cols, rows), - [TypeMat(cols, rows, TypeF32)], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f32)], + Type.mat(cols, rows, Type.f32), t.params, cases ); @@ -223,7 +134,7 @@ g.test('f16') }) .fn(async t => { const cases = await d.get('f16'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF16], TypeF32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f16], Type.f32, t.params, cases); }); g.test('f16_mat') @@ -249,8 +160,8 @@ g.test('f16_mat') await run( t, matrixExperession(cols, rows), - [TypeMat(cols, rows, TypeF16)], - TypeMat(cols, rows, TypeF32), + [Type.mat(cols, rows, Type.f16)], + Type.mat(cols, rows, Type.f32), t.params, cases ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.cache.ts new file mode 100644 index 0000000000..b7206bcf45 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.cache.ts @@ -0,0 +1,11 @@ +import { i32 } from '../../../../util/conversion.js'; +import { fullI32Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/i32_arithmetic', { + negation: () => { + return fullI32Range().map(e => { + return { input: i32(e), expected: i32(-e) }; + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts index 14519b8967..a7d16a96cd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts @@ -4,23 +4,14 @@ Execution Tests for the i32 arithmetic unary expression operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { i32, TypeI32 } from '../../../../util/conversion.js'; -import { fullI32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; +import { Type } from '../../../../util/conversion.js'; import { allInputSources, run } from '../expression.js'; +import { d } from './i32_arithmetic.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/i32_arithmetic', { - negation: () => { - return fullI32Range().map(e => { - return { input: i32(e), expected: i32(-e) }; - }); - }, -}); - g.test('negation') .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') .desc( @@ -33,5 +24,5 @@ Expression: -x ) .fn(async t => { const cases = await d.get('negation'); - await run(t, unary('-'), [TypeI32], TypeI32, t.params, cases); + await run(t, unary('-'), [Type.i32], Type.i32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_complement.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_complement.spec.ts index e8bda51b51..01e5728d70 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_complement.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_complement.spec.ts @@ -4,23 +4,14 @@ Execution Tests for the i32 bitwise complement operation import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { i32, TypeI32 } from '../../../../util/conversion.js'; +import { i32, Type } from '../../../../util/conversion.js'; import { fullI32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; import { allInputSources, run } from '../expression.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/i32_complement', { - complement: () => { - return fullI32Range().map(e => { - return { input: i32(e), expected: i32(~e) }; - }); - }, -}); - g.test('i32_complement') .specURL('https://www.w3.org/TR/WGSL/#bit-expr') .desc( @@ -32,6 +23,8 @@ Expression: ~x u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - const cases = await d.get('complement'); - await run(t, unary('~'), [TypeI32], TypeI32, t.params, cases); + const cases = fullI32Range().map(e => { + return { input: i32(e), expected: i32(~e) }; + }); + await run(t, unary('~'), [Type.i32], Type.i32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_conversion.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_conversion.cache.ts new file mode 100644 index 0000000000..1c2d01548d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_conversion.cache.ts @@ -0,0 +1,116 @@ +import { kValue } from '../../../../util/constants.js'; +import { + abstractFloat, + abstractInt, + bool, + f16, + f32, + i32, + u32, +} from '../../../../util/conversion.js'; +import { + fullI32Range, + fullU32Range, + quantizeToF16, + quantizeToF32, + scalarF16Range, + scalarF32Range, + scalarF64Range, +} from '../../../../util/math.js'; +import { reinterpretU32AsI32 } from '../../../../util/reinterpret.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/i32_conversion', { + bool: () => { + return [ + { input: bool(true), expected: i32(1) }, + { input: bool(false), expected: i32(0) }, + ]; + }, + abstractInt: () => { + return fullI32Range().map(i => { + return { input: abstractInt(BigInt(i)), expected: i32(i) }; + }); + }, + u32: () => { + return fullU32Range().map(u => { + return { input: u32(u), expected: i32(reinterpretU32AsI32(u)) }; + }); + }, + i32: () => { + return fullI32Range().map(i => { + return { input: i32(i), expected: i32(i) }; + }); + }, + abstractFloat: () => { + return scalarF64Range().map(f => { + // Handles zeros and subnormals + if (Math.abs(f) < 1.0) { + return { input: abstractFloat(f), expected: i32(0) }; + } + + if (f <= kValue.i32.negative.min) { + return { input: abstractFloat(f), expected: i32(kValue.i32.negative.min) }; + } + + if (f >= kValue.i32.positive.max) { + return { input: abstractFloat(f), expected: i32(kValue.i32.positive.max) }; + } + + // All i32s are representable as f64, and both AbstractFloat and number + // are f64 internally, so there is no need for special casing like f32 and + // f16 below. + return { input: abstractFloat(f), expected: i32(Math.trunc(f)) }; + }); + }, + f32: () => { + return scalarF32Range().map(f => { + // Handles zeros and subnormals + if (Math.abs(f) < 1.0) { + return { input: f32(f), expected: i32(0) }; + } + + if (f <= kValue.i32.negative.min) { + return { input: f32(f), expected: i32(kValue.i32.negative.min) }; + } + + if (f >= kValue.i32.positive.max) { + return { input: f32(f), expected: i32(kValue.i32.positive.max) }; + } + + // All f32 no larger than 2^24 has a precise interger part and a fractional part, just need + // to trunc towards 0 for the result integer. + if (Math.abs(f) <= 2 ** 24) { + return { input: f32(f), expected: i32(Math.trunc(f)) }; + } + + // All f32s between 2 ** 24 and kValue.i32.negative.min/.positive.max are + // integers, so in theory one could use them directly, expect that number + // is actually f64 internally, so they need to be quantized to f32 first. + // Cannot just use trunc here, since that might produce a i32 value that + // is precise in f64, but not in f32. + return { input: f32(f), expected: i32(quantizeToF32(f)) }; + }); + }, + f16: () => { + // Note that finite f16 values are always in range of i32. + return scalarF16Range().map(f => { + // Handles zeros and subnormals + if (Math.abs(f) < 1.0) { + return { input: f16(f), expected: i32(0) }; + } + + // All f16 no larger than <= 2^12 has a precise interger part and a fractional part, just need + // to trunc towards 0 for the result integer. + if (Math.abs(f) <= 2 ** 12) { + return { input: f16(f), expected: i32(Math.trunc(f)) }; + } + + // All f16s larger than 2 ** 12 are integers, so in theory one could use them directly, expect + // that number is actually f64 internally, so they need to be quantized to f16 first. + // Cannot just use trunc here, since that might produce a i32 value that is precise in f64, + // but not in f16. + return { input: f16(f), expected: i32(quantizeToF16(f)) }; + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_conversion.spec.ts index a77aa0e4d3..b47ffe0d07 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_conversion.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_conversion.spec.ts @@ -4,104 +4,14 @@ Execution Tests for the i32 conversion operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { kValue } from '../../../../util/constants.js'; -import { - bool, - f32, - f16, - i32, - TypeBool, - TypeF32, - TypeF16, - TypeI32, - TypeU32, - u32, -} from '../../../../util/conversion.js'; -import { - fullF32Range, - fullF16Range, - fullI32Range, - fullU32Range, - quantizeToF32, - quantizeToF16, -} from '../../../../util/math.js'; -import { reinterpretU32AsI32 } from '../../../../util/reinterpret.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, run, ShaderBuilder } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { ShaderBuilder, allInputSources, run, onlyConstInputSource } from '../expression.js'; +import { d } from './i32_conversion.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/i32_conversion', { - bool: () => { - return [ - { input: bool(true), expected: i32(1) }, - { input: bool(false), expected: i32(0) }, - ]; - }, - u32: () => { - return fullU32Range().map(u => { - return { input: u32(u), expected: i32(reinterpretU32AsI32(u)) }; - }); - }, - i32: () => { - return fullI32Range().map(i => { - return { input: i32(i), expected: i32(i) }; - }); - }, - f32: () => { - return fullF32Range().map(f => { - // Handles zeros and subnormals - if (Math.abs(f) < 1.0) { - return { input: f32(f), expected: i32(0) }; - } - - if (f <= kValue.i32.negative.min) { - return { input: f32(f), expected: i32(kValue.i32.negative.min) }; - } - - if (f >= kValue.i32.positive.max) { - return { input: f32(f), expected: i32(kValue.i32.positive.max) }; - } - - // All f32 no larger than 2^24 has a precise interger part and a fractional part, just need - // to trunc towards 0 for the result integer. - if (Math.abs(f) <= 2 ** 24) { - return { input: f32(f), expected: i32(Math.trunc(f)) }; - } - - // All f32s between 2 ** 24 and kValue.i32.negative.min/.positive.max are - // integers, so in theory one could use them directly, expect that number - // is actually f64 internally, so they need to be quantized to f32 first. - // Cannot just use trunc here, since that might produce a i32 value that - // is precise in f64, but not in f32. - return { input: f32(f), expected: i32(quantizeToF32(f)) }; - }); - }, - f16: () => { - // Note that finite f16 values are always in range of i32. - return fullF16Range().map(f => { - // Handles zeros and subnormals - if (Math.abs(f) < 1.0) { - return { input: f16(f), expected: i32(0) }; - } - - // All f16 no larger than <= 2^12 has a precise interger part and a fractional part, just need - // to trunc towards 0 for the result integer. - if (Math.abs(f) <= 2 ** 12) { - return { input: f16(f), expected: i32(Math.trunc(f)) }; - } - - // All f16s larger than 2 ** 12 are integers, so in theory one could use them directly, expect - // that number is actually f64 internally, so they need to be quantized to f16 first. - // Cannot just use trunc here, since that might produce a i32 value that is precise in f64, - // but not in f16. - return { input: f16(f), expected: i32(quantizeToF16(f)) }; - }); - }, -}); - /** Generate a ShaderBuilder based on how the test case is to be vectorized */ function vectorizeToExpression(vectorize: undefined | 2 | 3 | 4): ShaderBuilder { return vectorize === undefined ? unary('i32') : unary(`vec${vectorize}<i32>`); @@ -121,7 +31,7 @@ The result is 1u if e is true and 0u otherwise ) .fn(async t => { const cases = await d.get('bool'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeBool], TypeI32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.bool], Type.i32, t.params, cases); }); g.test('u32') @@ -138,7 +48,7 @@ Reinterpretation of bits ) .fn(async t => { const cases = await d.get('u32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeU32], TypeI32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.u32], Type.i32, t.params, cases); }); g.test('i32') @@ -155,7 +65,7 @@ Identity operation ) .fn(async t => { const cases = await d.get('i32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeI32], TypeI32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.i32], Type.i32, t.params, cases); }); g.test('f32') @@ -172,7 +82,7 @@ e is converted to i32, rounding towards zero ) .fn(async t => { const cases = await d.get('f32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF32], TypeI32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f32], Type.i32, t.params, cases); }); g.test('f16') @@ -192,5 +102,57 @@ e is converted to u32, rounding towards zero }) .fn(async t => { const cases = await d.get('f16'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF16], TypeI32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f16], Type.i32, t.params, cases); + }); + +g.test('abstract_int') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc( + ` +i32(e), where e is an AbstractInt + +Identity operation if e is in bounds for i32, otherwise shader creation error +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('abstractInt'); + await run( + t, + vectorizeToExpression(t.params.vectorize), + [Type.abstractInt], + Type.i32, + t.params, + cases + ); + }); + +g.test('abstract_float') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc( + ` +i32(e), where e is an AbstractFloat + +e is converted to i32, rounding towards zero +` + ) + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('abstractFloat'); + await run( + t, + vectorizeToExpression(t.params.vectorize), + [Type.abstractFloat], + Type.i32, + t.params, + cases + ); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_complement.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_complement.spec.ts index 446e0918bd..74251a32c6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_complement.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_complement.spec.ts @@ -4,23 +4,14 @@ Execution Tests for the u32 bitwise complement operation import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { u32, TypeU32 } from '../../../../util/conversion.js'; +import { Type, u32 } from '../../../../util/conversion.js'; import { fullU32Range } from '../../../../util/math.js'; -import { makeCaseCache } from '../case_cache.js'; import { allInputSources, run } from '../expression.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/u32_complement', { - complement: () => { - return fullU32Range().map(e => { - return { input: u32(e), expected: u32(~e) }; - }); - }, -}); - g.test('u32_complement') .specURL('https://www.w3.org/TR/WGSL/#bit-expr') .desc( @@ -32,6 +23,8 @@ Expression: ~x u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) ) .fn(async t => { - const cases = await d.get('complement'); - await run(t, unary('~'), [TypeU32], TypeU32, t.params, cases); + const cases = fullU32Range().map(e => { + return { input: u32(e), expected: u32(~e) }; + }); + await run(t, unary('~'), [Type.u32], Type.u32, t.params, cases); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_conversion.cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_conversion.cache.ts new file mode 100644 index 0000000000..1ef307810f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_conversion.cache.ts @@ -0,0 +1,107 @@ +import { kValue } from '../../../../util/constants.js'; +import { + abstractFloat, + abstractInt, + bool, + f16, + f32, + i32, + u32, +} from '../../../../util/conversion.js'; +import { + fullI32Range, + fullU32Range, + quantizeToF16, + quantizeToF32, + scalarF16Range, + scalarF32Range, + scalarF64Range, +} from '../../../../util/math.js'; +import { reinterpretI32AsU32 } from '../../../../util/reinterpret.js'; +import { makeCaseCache } from '../case_cache.js'; + +export const d = makeCaseCache('unary/u32_conversion', { + bool: () => { + return [ + { input: bool(true), expected: u32(1) }, + { input: bool(false), expected: u32(0) }, + ]; + }, + abstractInt: () => { + return fullU32Range().map(u => { + return { input: abstractInt(BigInt(u)), expected: u32(u) }; + }); + }, + u32: () => { + return fullU32Range().map(u => { + return { input: u32(u), expected: u32(u) }; + }); + }, + i32: () => { + return fullI32Range().map(i => { + return { input: i32(i), expected: u32(reinterpretI32AsU32(i)) }; + }); + }, + abstractFloat: () => { + return [...scalarF64Range(), -1].map(f => { + // Handles zeros, subnormals, and negatives + if (f < 1.0) { + return { input: abstractFloat(f), expected: u32(0) }; + } + + if (f >= kValue.u32.max) { + return { input: abstractFloat(f), expected: u32(kValue.u32.max) }; + } + + // All u32s are representable as f64s and number is a f64 internally, so + // no need for special handling like is done for f32 and f16 below. + return { input: abstractFloat(f), expected: u32(Math.floor(f)) }; + }); + }, + f32: () => { + return scalarF32Range().map(f => { + // Handles zeros, subnormals, and negatives + if (f < 1.0) { + return { input: f32(f), expected: u32(0) }; + } + + if (f >= kValue.u32.max) { + return { input: f32(f), expected: u32(kValue.u32.max) }; + } + + // All f32 no larger than 2^24 has a precise integer part and a fractional + // part, just need to trunc towards 0 for the result integer. + if (f <= 2 ** 24) { + return { input: f32(f), expected: u32(Math.floor(f)) }; + } + + // All f32s between 2 ** 24 and kValue.u32.max are integers, so in theory + // one could use them directly, expect that number is actually f64 + // internally, so they need to be quantized to f32 first. + // Cannot just use floor here, since that might produce a u32 value that + // is precise in f64, but not in f32. + return { input: f32(f), expected: u32(quantizeToF32(f)) }; + }); + }, + f16: () => { + // Note that all positive finite f16 values are in range of u32. + return scalarF16Range().map(f => { + // Handles zeros, subnormals, and negatives + if (f < 1.0) { + return { input: f16(f), expected: u32(0) }; + } + + // All f16 no larger than <= 2^12 has a precise integer part and a + // fractional part, just need to trunc towards 0 for the result integer. + if (f <= 2 ** 12) { + return { input: f16(f), expected: u32(Math.trunc(f)) }; + } + + // All f16s larger than 2 ** 12 are integers, so in theory one could use + // them directly, expect that number is actually f64 internally, so they + // need to be quantized to f16 first.Cannot just use trunc here, since + // that might produce a u32 value that is precise in f64, but not in f16. + return { input: f16(f), expected: u32(quantizeToF16(f)) }; + }); + }, +}); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_conversion.spec.ts index 87dc6e7a5d..6d342afffb 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_conversion.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/u32_conversion.spec.ts @@ -4,100 +4,14 @@ Execution Tests for the u32 conversion operations import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; -import { kValue } from '../../../../util/constants.js'; -import { - bool, - f32, - f16, - i32, - TypeBool, - TypeF32, - TypeF16, - TypeI32, - TypeU32, - u32, -} from '../../../../util/conversion.js'; -import { - fullF32Range, - fullF16Range, - fullI32Range, - fullU32Range, - quantizeToF32, - quantizeToF16, -} from '../../../../util/math.js'; -import { reinterpretI32AsU32 } from '../../../../util/reinterpret.js'; -import { makeCaseCache } from '../case_cache.js'; -import { allInputSources, run, ShaderBuilder } from '../expression.js'; +import { Type } from '../../../../util/conversion.js'; +import { ShaderBuilder, allInputSources, run, onlyConstInputSource } from '../expression.js'; +import { d } from './u32_conversion.cache.js'; import { unary } from './unary.js'; export const g = makeTestGroup(GPUTest); -export const d = makeCaseCache('unary/u32_conversion', { - bool: () => { - return [ - { input: bool(true), expected: u32(1) }, - { input: bool(false), expected: u32(0) }, - ]; - }, - u32: () => { - return fullU32Range().map(u => { - return { input: u32(u), expected: u32(u) }; - }); - }, - i32: () => { - return fullI32Range().map(i => { - return { input: i32(i), expected: u32(reinterpretI32AsU32(i)) }; - }); - }, - f32: () => { - return fullF32Range().map(f => { - // Handles zeros, subnormals, and negatives - if (f < 1.0) { - return { input: f32(f), expected: u32(0) }; - } - - if (f >= kValue.u32.max) { - return { input: f32(f), expected: u32(kValue.u32.max) }; - } - - // All f32 no larger than 2^24 has a precise interger part and a fractional part, just need - // to trunc towards 0 for the result integer. - if (f <= 2 ** 24) { - return { input: f32(f), expected: u32(Math.floor(f)) }; - } - - // All f32s between 2 ** 24 and kValue.u32.max are integers, so in theory - // one could use them directly, expect that number is actually f64 - // internally, so they need to be quantized to f32 first. - // Cannot just use floor here, since that might produce a u32 value that - // is precise in f64, but not in f32. - return { input: f32(f), expected: u32(quantizeToF32(f)) }; - }); - }, - f16: () => { - // Note that all positive finite f16 values are in range of u32. - return fullF16Range().map(f => { - // Handles zeros, subnormals, and negatives - if (f < 1.0) { - return { input: f16(f), expected: u32(0) }; - } - - // All f16 no larger than <= 2^12 has a precise interger part and a fractional part, just need - // to trunc towards 0 for the result integer. - if (f <= 2 ** 12) { - return { input: f16(f), expected: u32(Math.trunc(f)) }; - } - - // All f16s larger than 2 ** 12 are integers, so in theory one could use them directly, expect - // that number is actually f64 internally, so they need to be quantized to f16 first. - // Cannot just use trunc here, since that might produce a u32 value that is precise in f64, - // but not in f16. - return { input: f16(f), expected: u32(quantizeToF16(f)) }; - }); - }, -}); - /** Generate a ShaderBuilder based on how the test case is to be vectorized */ function vectorizeToExpression(vectorize: undefined | 2 | 3 | 4): ShaderBuilder { return vectorize === undefined ? unary('u32') : unary(`vec${vectorize}<u32>`); @@ -117,7 +31,7 @@ The result is 1u if e is true and 0u otherwise ) .fn(async t => { const cases = await d.get('bool'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeBool], TypeU32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.bool], Type.u32, t.params, cases); }); g.test('u32') @@ -134,7 +48,7 @@ Identity operation ) .fn(async t => { const cases = await d.get('u32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeU32], TypeU32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.u32], Type.u32, t.params, cases); }); g.test('i32') @@ -151,7 +65,7 @@ Reinterpretation of bits ) .fn(async t => { const cases = await d.get('i32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeI32], TypeU32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.i32], Type.u32, t.params, cases); }); g.test('f32') @@ -168,7 +82,7 @@ e is converted to u32, rounding towards zero ) .fn(async t => { const cases = await d.get('f32'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF32], TypeU32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f32], Type.u32, t.params, cases); }); g.test('f16') @@ -188,7 +102,7 @@ e is converted to u32, rounding towards zero }) .fn(async t => { const cases = await d.get('f16'); - await run(t, vectorizeToExpression(t.params.vectorize), [TypeF16], TypeU32, t.params, cases); + await run(t, vectorizeToExpression(t.params.vectorize), [Type.f16], Type.u32, t.params, cases); }); g.test('abstract_int') @@ -201,6 +115,44 @@ Identity operation if the e can be represented in u32, otherwise it produces a s ` ) .params(u => - u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('abstractInt'); + await run( + t, + vectorizeToExpression(t.params.vectorize), + [Type.abstractInt], + Type.u32, + t.params, + cases + ); + }); + +g.test('abstract_float') + .specURL('https://www.w3.org/TR/WGSL/#value-constructor-builtin-function') + .desc( + ` +u32(e), where e is an AbstractFloat + +e is converted to u32, rounding towards zero +` ) - .unimplemented(); + .params(u => + u + .combine('inputSource', onlyConstInputSource) + .combine('vectorize', [undefined, 2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('abstractFloat'); + await run( + t, + vectorizeToExpression(t.params.vectorize), + [Type.abstractFloat], + Type.u32, + t.params, + cases + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts index 160e465178..109e6c1421 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts @@ -1,5 +1,6 @@ import { abstractFloatShaderBuilder, + abstractIntShaderBuilder, basicExpressionBuilder, ShaderBuilder, } from '../expression.js'; @@ -10,6 +11,11 @@ export function unary(op: string): ShaderBuilder { } /* @returns a ShaderBuilder that evaluates a prefix unary operation that returns AbstractFloats */ -export function abstractUnary(op: string): ShaderBuilder { +export function abstractFloatUnary(op: string): ShaderBuilder { return abstractFloatShaderBuilder(value => `${op}(${value})`); } + +/* @returns a ShaderBuilder that evaluates a prefix unary operation that returns AbstractInts */ +export function abstractIntUnary(op: string): ShaderBuilder { + return abstractIntShaderBuilder(value => `${op}(${value})`); +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/call.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/call.spec.ts index b86134a58c..a146f5742f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/call.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/call.spec.ts @@ -81,3 +81,116 @@ fn c() { }`, })); }); + +g.test('arg_eval') + .desc('Test that arguments are evaluated left to right') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + a(b(), c(), d()); + ${f.expect_order(5)} +`, + extra: ` +fn a(p1 : u32, p2 : u32, p3 : u32) { + ${f.expect_order(4)} +} +fn b() -> u32 { + ${f.expect_order(1)} + return 0; +} +fn c() -> u32 { + ${f.expect_order(2)} + return 0; +} +fn d() -> u32 { + ${f.expect_order(3)} + return 0; +}`, + })); + }); + +g.test('arg_eval_logical_and') + .desc('Test that arguments are evaluated left to right') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + a(b(${f.value(1)}) && c()); + a(b(${f.value(0)}) && c()); + ${f.expect_order(6)} +`, + extra: ` +fn a(p : bool) { + ${f.expect_order(3, 5)} +} +fn b(x : i32) -> bool { + ${f.expect_order(1, 4)} + return x == 1; +} +fn c() -> bool { + ${f.expect_order(2)} + return true; +}`, + })); + }); + +g.test('arg_eval_logical_or') + .desc('Test that arguments are evaluated left to right') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + a(b(${f.value(1)}) || c()); + a(b(${f.value(0)}) || c()); + ${f.expect_order(6)} +`, + extra: ` +fn a(p : bool) { + ${f.expect_order(3, 5)} +} +fn b(x : i32) -> bool { + ${f.expect_order(1, 4)} + return x == 0; +} +fn c() -> bool { + ${f.expect_order(2)} + return true; +}`, + })); + }); + +g.test('arg_eval_pointers') + .desc('Test that arguments are evaluated left to right') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + var x : i32 = ${f.value(0)}; + ${f.expect_order(0)} + _ = c(&x); + a(b(&x), c(&x)); + ${f.expect_order(5)} +`, + extra: ` +fn a(p1 : i32, p2 : i32) { + ${f.expect_order(4)} +} +fn b(p : ptr<function, i32>) -> i32 { + (*p)++; + ${f.expect_order(2)} + return 0; +} +fn c(p : ptr<function, i32>) -> i32 { + if (*p == 1) { + ${f.expect_order(3)} + } else { + ${f.expect_order(1)} + } + return 0; +}`, + })); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/for.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/for.spec.ts index 898b8a0e04..5cb7de66c1 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/for.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/for.spec.ts @@ -269,3 +269,53 @@ g.test('nested_for_continue') ` ); }); + +g.test('for_logical_and_condition') + .desc('Test flow control for a for-loop with a logical and condition') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + for (var i = ${f.value(0)}; a(i) && b(i); i++) { + ${f.expect_order(3, 6)} + } + ${f.expect_order(8)} + `, + extra: ` +fn a(i : i32) -> bool { + ${f.expect_order(1, 4, 7)} + return i < ${f.value(2)}; +} +fn b(i : i32) -> bool { + ${f.expect_order(2, 5)} + return i < ${f.value(5)}; +} + `, + })); + }); + +g.test('for_logical_or_condition') + .desc('Test flow control for a for-loop with a logical or condition') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + for (var i = ${f.value(0)}; a(i) || b(i); i++) { + ${f.expect_order(2, 4, 7, 10)} + } + ${f.expect_order(13)} + `, + extra: ` +fn a(i : i32) -> bool { + ${f.expect_order(1, 3, 5, 8, 11)} + return i < ${f.value(2)}; +} +fn b(i : i32) -> bool { + ${f.expect_order(6, 9, 12)} + return i < ${f.value(4)}; +} + `, + })); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/loop.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/loop.spec.ts index 18d0e5d1ee..6dd99d137d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/loop.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/loop.spec.ts @@ -123,3 +123,63 @@ g.test('nested_loops') ` ); }); + +g.test('loop_break_if_logical_and_condition') + .desc('Test flow control for a loop with a logical and break if') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + var i = ${f.value(0)}; + loop { + ${f.expect_order(1, 4, 7)} + continuing { + i++; + break if !(a(i) && b(i)); + } + } + ${f.expect_order(9)} + `, + extra: ` +fn a(i : i32) -> bool { + ${f.expect_order(2, 5, 8)} + return i < ${f.value(3)}; +} +fn b(i : i32) -> bool { + ${f.expect_order(3, 6)} + return i < ${f.value(5)}; +} + `, + })); + }); + +g.test('loop_break_if_logical_or_condition') + .desc('Test flow control for a loop with a logical or break if') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + var i = ${f.value(0)}; + loop { + ${f.expect_order(1, 3, 6, 9)} + continuing { + i++; + break if !(a(i) || b(i)); + } + } + ${f.expect_order(12)} + `, + extra: ` +fn a(i : i32) -> bool { + ${f.expect_order(2, 4, 7, 10)} + return i < ${f.value(2)}; +} +fn b(i : i32) -> bool { + ${f.expect_order(5, 8, 11)} + return i < ${f.value(4)}; +} + `, + })); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/switch.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/switch.spec.ts index e500729614..772cd6a875 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/switch.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/switch.spec.ts @@ -154,3 +154,36 @@ ${f.expect_order(2)} ` ); }); + +g.test('switch_inside_loop_with_continue') + .desc('Test that flow control executes correct for a switch calling continue inside a loop') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest( + t, + f => ` +${f.expect_order(0)} +var i = ${f.value(0)}; +loop { + switch (i) { + case 1: { + ${f.expect_order(4)} + continue; + } + default: { + ${f.expect_order(1)} + break; + } + } + ${f.expect_order(2)} + + continuing { + ${f.expect_order(3, 5)} + i++; + break if i >= 2; + } +} +${f.expect_order(6)} +` + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/while.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/while.spec.ts index 88ce6838a5..5ce6097a3a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/while.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/flow_control/while.spec.ts @@ -138,3 +138,57 @@ g.test('while_nested_continue') ` ); }); + +g.test('while_logical_and_condition') + .desc('Test flow control for a while-loop with a logical and condition') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + var i = ${f.value(0)}; + while (a(i) && b(i)) { + ${f.expect_order(3, 6)} + i++; + } + ${f.expect_order(8)} + `, + extra: ` +fn a(i : i32) -> bool { + ${f.expect_order(1, 4, 7)} + return i < ${f.value(2)}; +} +fn b(i : i32) -> bool { + ${f.expect_order(2, 5)} + return i < ${f.value(5)}; +} + `, + })); + }); + +g.test('while_logical_or_condition') + .desc('Test flow control for a while-loop with a logical or condition') + .params(u => u.combine('preventValueOptimizations', [true, false])) + .fn(t => { + runFlowControlTest(t, f => ({ + entrypoint: ` + ${f.expect_order(0)} + var i = ${f.value(0)}; + while (a(i) || b(i)) { + ${f.expect_order(2, 4, 7, 10)} + i++; + } + ${f.expect_order(13)} + `, + extra: ` +fn a(i : i32) -> bool { + ${f.expect_order(1, 3, 5, 8, 11)} + return i < ${f.value(2)}; +} +fn b(i : i32) -> bool { + ${f.expect_order(6, 9, 12)} + return i < ${f.value(4)}; +} + `, + })); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_layout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_layout.spec.ts new file mode 100644 index 0000000000..a58a31449f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_layout.spec.ts @@ -0,0 +1,1059 @@ +export const description = `Test memory layout requirements`; + +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { keysOf } from '../../../common/util/data_tables.js'; +import { iterRange } from '../../../common/util/util.js'; +import { GPUTest } from '../../gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +interface LayoutCase { + type: string; + decl?: string; + read_assign: string; + write_assign: string; + offset: number; + f16?: boolean; + f32?: boolean; + skip_uniform?: boolean; +} + +const kLayoutCases: Record<string, LayoutCase> = { + vec2u_align8: { + type: `S_vec2u_align`, + decl: `struct S_vec2u_align { + x : u32, + y : vec2u, + }`, + read_assign: `out = in.y[1]`, + write_assign: `out.y[1] = in`, + offset: 12, + }, + vec3u_align16: { + type: `S_vec3u_align`, + decl: `struct S_vec3u_align { + x : u32, + y : vec3u, + }`, + read_assign: `out = in.y[2]`, + write_assign: `out.y[2] = in`, + offset: 24, + }, + vec4u_align16: { + type: `S_vec4u_align`, + decl: `struct S_vec4u_align { + x : u32, + y : vec4u, + }`, + read_assign: `out = in.y[0]`, + write_assign: `out.y[0] = in`, + offset: 16, + }, + struct_align32: { + type: `S_align32`, + decl: `struct S_align32 { + x : u32, + @align(32) y : u32, + }`, + read_assign: `out = in.y;`, + write_assign: `out.y = in`, + offset: 32, + }, + vec2h_align4: { + type: `S_vec2h_align`, + decl: `struct S_vec2h_align { + x : f16, + y : vec2h, + }`, + read_assign: `out = u32(in.y[0])`, + write_assign: `out.y[0] = f16(in)`, + offset: 4, + f16: true, + }, + vec3h_align8: { + type: `S_vec3h_align`, + decl: `struct S_vec3h_align { + x : f16, + y : vec3h, + }`, + read_assign: `out = u32(in.y[2])`, + write_assign: `out.y[2] = f16(in)`, + offset: 12, + f16: true, + }, + vec4h_align8: { + type: `S_vec4h_align`, + decl: `struct S_vec4h_align { + x : f16, + y : vec4h, + }`, + read_assign: `out = u32(in.y[2])`, + write_assign: `out.y[2] = f16(in)`, + offset: 12, + f16: true, + }, + vec2f_align8: { + type: `S_vec2f_align`, + decl: `struct S_vec2f_align { + x : u32, + y : vec2f, + }`, + read_assign: `out = u32(in.y[1])`, + write_assign: `out.y[1] = f32(in)`, + offset: 12, + f32: true, + }, + vec3f_align16: { + type: `S_vec3f_align`, + decl: `struct S_vec3f_align { + x : u32, + y : vec3f, + }`, + read_assign: `out = u32(in.y[2])`, + write_assign: `out.y[2] = f32(in)`, + offset: 24, + f32: true, + }, + vec4f_align16: { + type: `S_vec4f_align`, + decl: `struct S_vec4f_align { + x : u32, + y : vec4f, + }`, + read_assign: `out = u32(in.y[0])`, + write_assign: `out.y[0] = f32(in)`, + offset: 16, + f32: true, + }, + vec3i_size12: { + type: `S_vec3i_size`, + decl: `struct S_vec3i_size { + x : vec3i, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 12, + }, + vec3h_size6: { + type: `S_vec3h_size`, + decl: `struct S_vec3h_size { + x : vec3h, + y : f16, + z : f16, + }`, + read_assign: `out = u32(in.z)`, + write_assign: `out.z = f16(in)`, + offset: 8, + f16: true, + }, + size80: { + type: `S_size80`, + decl: `struct S_size80 { + @size(80) x : u32, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 80, + }, + atomic_align4: { + type: `S_atomic_align`, + decl: `struct S_atomic_align { + x : u32, + y : atomic<u32>, + }`, + read_assign: `out = atomicLoad(&in.y)`, + write_assign: `atomicStore(&out.y, in)`, + offset: 4, + }, + atomic_size4: { + type: `S_atomic_size`, + decl: `struct S_atomic_size { + x : atomic<u32>, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 4, + }, + mat2x2f_align8: { + type: `S_mat2x2f_align`, + decl: `struct S_mat2x2f_align { + x : u32, + y : mat2x2f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 8, + f32: true, + }, + mat3x2f_align8: { + type: `S_mat3x2f_align`, + decl: `struct S_mat3x2f_align { + x : u32, + y : mat3x2f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 8, + f32: true, + }, + mat4x2f_align8: { + type: `S_mat4x2f_align`, + decl: `struct S_mat4x2f_align { + x : u32, + y : mat4x2f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 8, + f32: true, + }, + mat2x3f_align16: { + type: `S_mat2x3f_align`, + decl: `struct S_mat2x3f_align { + x : u32, + y : mat2x3f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 16, + f32: true, + }, + mat3x3f_align16: { + type: `S_mat3x3f_align`, + decl: `struct S_mat3x3f_align { + x : u32, + y : mat3x3f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 16, + f32: true, + }, + mat4x3f_align16: { + type: `S_mat4x3f_align`, + decl: `struct S_mat4x3f_align { + x : u32, + y : mat4x3f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 16, + f32: true, + }, + mat2x4f_align16: { + type: `S_mat2x4f_align`, + decl: `struct S_mat2x4f_align { + x : u32, + y : mat2x4f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 16, + f32: true, + }, + mat3x4f_align16: { + type: `S_mat3x4f_align`, + decl: `struct S_mat3x4f_align { + x : u32, + y : mat3x4f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 16, + f32: true, + }, + mat4x4f_align16: { + type: `S_mat4x4f_align`, + decl: `struct S_mat4x4f_align { + x : u32, + y : mat4x4f, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f32(in)`, + offset: 16, + f32: true, + }, + mat2x2h_align4: { + type: `S_mat2x2h_align`, + decl: `struct S_mat2x2h_align { + x : u32, + y : mat2x2h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 4, + f16: true, + }, + mat3x2h_align4: { + type: `S_mat3x2h_align`, + decl: `struct S_mat3x2h_align { + x : u32, + y : mat3x2h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 4, + f16: true, + }, + mat4x2h_align4: { + type: `S_mat4x2h_align`, + decl: `struct S_mat4x2h_align { + x : u32, + y : mat4x2h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 4, + f16: true, + }, + mat2x3h_align8: { + type: `S_mat2x3h_align`, + decl: `struct S_mat2x3h_align { + x : u32, + y : mat2x3h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 8, + f16: true, + }, + mat3x3h_align8: { + type: `S_mat3x3h_align`, + decl: `struct S_mat3x3h_align { + x : u32, + y : mat2x3h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 8, + f16: true, + }, + mat4x3h_align8: { + type: `S_mat4x3h_align`, + decl: `struct S_mat4x3h_align { + x : u32, + y : mat4x3h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 8, + f16: true, + }, + mat2x4h_align8: { + type: `S_mat2x4h_align`, + decl: `struct S_mat2x4h_align { + x : u32, + y : mat2x4h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 8, + f16: true, + }, + mat3x4h_align8: { + type: `S_mat3x4h_align`, + decl: `struct S_mat3x4h_align { + x : u32, + y : mat3x4h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 8, + f16: true, + }, + mat4x4h_align8: { + type: `S_mat4x4h_align`, + decl: `struct S_mat4x4h_align { + x : u32, + y : mat4x4h, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 8, + f16: true, + }, + mat2x2f_size: { + type: `S_mat2x2f_size`, + decl: `struct S_mat2x2f_size { + x : mat2x2f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 16, + }, + mat3x2f_size: { + type: `S_mat3x2f_size`, + decl: `struct S_mat3x2f_size { + x : mat3x2f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 24, + }, + mat4x2f_size: { + type: `S_mat4x2f_size`, + decl: `struct S_mat4x2f_size { + x : mat4x2f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 32, + }, + mat2x3f_size: { + type: `S_mat2x3f_size`, + decl: `struct S_mat2x3f_size { + x : mat2x3f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 32, + }, + mat3x3f_size: { + type: `S_mat3x3f_size`, + decl: `struct S_mat3x3f_size { + x : mat3x3f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 48, + }, + mat4x3f_size: { + type: `S_mat4x3f_size`, + decl: `struct S_mat4x3f_size { + x : mat4x3f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 64, + }, + mat2x4f_size: { + type: `S_mat2x4f_size`, + decl: `struct S_mat2x4f_size { + x : mat2x4f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 32, + }, + mat3x4f_size: { + type: `S_mat3x4f_size`, + decl: `struct S_mat3x4f_size { + x : mat3x4f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 48, + }, + mat4x4f_size: { + type: `S_mat4x4f_size`, + decl: `struct S_mat4x4f_size { + x : mat4x4f, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 64, + }, + mat2x2h_size: { + type: `S_mat2x2h_size`, + decl: `struct S_mat2x2h_size { + x : mat2x2h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 8, + f16: true, + }, + mat3x2h_size: { + type: `S_mat3x2h_size`, + decl: `struct S_mat3x2h_size { + x : mat3x2h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 12, + f16: true, + }, + mat4x2h_size: { + type: `S_mat4x2h_size`, + decl: `struct S_mat4x2h_size { + x : mat4x2h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 16, + f16: true, + }, + mat2x3h_size: { + type: `S_mat2x3h_size`, + decl: `struct S_mat2x3h_size { + x : mat2x3h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 16, + f16: true, + }, + mat3x3h_size: { + type: `S_mat3x3h_size`, + decl: `struct S_mat3x3h_size { + x : mat3x3h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 24, + f16: true, + }, + mat4x3h_size: { + type: `S_mat4x3h_size`, + decl: `struct S_mat4x3h_size { + x : mat4x3h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 32, + f16: true, + }, + mat2x4h_size: { + type: `S_mat2x4h_size`, + decl: `struct S_mat2x4h_size { + x : mat2x4h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 16, + f16: true, + }, + mat3x4h_size: { + type: `S_mat3x4h_size`, + decl: `struct S_mat3x4h_size { + x : mat3x4h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 24, + f16: true, + }, + mat4x4h_size: { + type: `S_mat4x4h_size`, + decl: `struct S_mat4x4h_size { + x : mat4x4h, + y : f16, + }`, + read_assign: `out = u32(in.y)`, + write_assign: `out.y = f16(in)`, + offset: 32, + f16: true, + }, + struct_align_vec2i: { + type: `S_struct_align_vec2i`, + decl: `struct Inner { + x : u32, + y : vec2i, + } + struct S_struct_align_vec2i { + x : u32, + y : Inner, + }`, + read_assign: `out = in.y.x`, + write_assign: `out.y.x = in`, + offset: 8, + skip_uniform: true, + }, + struct_align_vec3i: { + type: `S_struct_align_vec3i`, + decl: `struct Inner { + x : u32, + y : vec3i, + } + struct S_struct_align_vec3i { + x : u32, + y : Inner, + }`, + read_assign: `out = in.y.x`, + write_assign: `out.y.x = in`, + offset: 16, + }, + struct_align_vec4i: { + type: `S_struct_align_vec4i`, + decl: `struct Inner { + x : u32, + y : vec4i, + } + struct S_struct_align_vec4i { + x : u32, + y : Inner, + }`, + read_assign: `out = in.y.x`, + write_assign: `out.y.x = in`, + offset: 16, + }, + struct_align_vec2h: { + type: `S_struct_align_vec2h`, + decl: `struct Inner { + x : f16, + y : vec2h, + } + struct S_struct_align_vec2h { + x : f16, + y : Inner, + }`, + read_assign: `out = u32(in.y.x)`, + write_assign: `out.y.x = f16(in)`, + offset: 4, + f16: true, + skip_uniform: true, + }, + struct_align_vec3h: { + type: `S_struct_align_vec3h`, + decl: `struct Inner { + x : f16, + y : vec3h, + } + struct S_struct_align_vec3h { + x : f16, + y : Inner, + }`, + read_assign: `out = u32(in.y.x)`, + write_assign: `out.y.x = f16(in)`, + offset: 8, + f16: true, + skip_uniform: true, + }, + struct_align_vec4h: { + type: `S_struct_align_vec4h`, + decl: `struct Inner { + x : f16, + y : vec4h, + } + struct S_struct_align_vec4h { + x : f16, + y : Inner, + }`, + read_assign: `out = u32(in.y.x)`, + write_assign: `out.y.x = f16(in)`, + offset: 8, + f16: true, + skip_uniform: true, + }, + struct_size_roundup: { + type: `S_struct_size_roundup`, + decl: `struct Inner { + x : vec3u, + } + struct S_struct_size_roundup { + x : Inner, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 16, + }, + struct_inner_size: { + type: `S_struct_inner_size`, + decl: `struct Inner { + @size(112) x : u32, + } + struct S_struct_inner_size { + x : Inner, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 112, + }, + struct_inner_align: { + type: `S_struct_inner_align`, + decl: `struct Inner { + @align(64) x : u32, + } + struct S_struct_inner_align { + x : u32, + y : Inner, + }`, + read_assign: `out = in.y.x`, + write_assign: `out.y.x = in`, + offset: 64, + }, + struct_inner_size_and_align: { + type: `S_struct_inner_size_and_align`, + decl: `struct Inner { + @align(32) @size(33) x : u32, + } + struct S_struct_inner_size_and_align { + x : Inner, + y : Inner, + }`, + read_assign: `out = in.y.x`, + write_assign: `out.y.x = in`, + offset: 64, + }, + struct_override_size: { + type: `S_struct_override_size`, + decl: `struct Inner { + @size(32) x : u32, + } + struct S_struct_override_size { + @size(64) x : Inner, + y : u32, + }`, + read_assign: `out = in.y`, + write_assign: `out.y = in`, + offset: 64, + }, + struct_double_align: { + type: `S_struct_double_align`, + decl: `struct Inner { + x : u32, + @align(32) y : u32, + } + struct S_struct_double_align { + x : u32, + @align(64) y : Inner, + }`, + read_assign: `out = in.y.y`, + write_assign: `out.y.y = in`, + offset: 96, + }, + array_vec3u_align: { + type: `S_array_vec3u_align`, + decl: `struct S_array_vec3u_align { + x : u32, + y : array<vec3u, 2>, + }`, + read_assign: `out = in.y[0][0]`, + write_assign: `out.y[0][0] = in`, + offset: 16, + }, + array_vec3h_align: { + type: `S_array_vec3h_align`, + decl: `struct S_array_vec3h_align { + x : f16, + y : array<vec3h, 2>, + }`, + read_assign: `out = u32(in.y[0][0])`, + write_assign: `out.y[0][0] = f16(in)`, + offset: 8, + f16: true, + skip_uniform: true, + }, + array_vec3u_stride: { + type: `S_array_vec3u_stride`, + decl: `struct S_array_vec3u_stride { + x : array<vec3u, 4>, + }`, + read_assign: `out = in.x[1][0]`, + write_assign: `out.x[1][0] = in`, + offset: 16, + }, + array_vec3h_stride: { + type: `S_array_vec3h_stride`, + decl: `struct S_array_vec3h_stride { + x : array<vec3h, 4>, + }`, + read_assign: `out = u32(in.x[1][0])`, + write_assign: `out.x[1][0] = f16(in)`, + offset: 8, + f16: true, + skip_uniform: true, + }, + array_stride_size: { + type: `array<S_stride, 4>`, + decl: `struct S_stride { + @size(16) x : u32, + }`, + read_assign: `out = in[2].x`, + write_assign: `out[2].x = in`, + offset: 32, + }, +}; + +g.test('read_layout') + .desc('Test reading memory layouts') + .params(u => + u + .combine('case', keysOf(kLayoutCases)) + .combine('aspace', ['storage', 'uniform', 'workgroup', 'function', 'private'] as const) + .beginSubcases() + ) + .beforeAllSubcases(t => { + const testcase = kLayoutCases[t.params.case]; + if (testcase.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + // Don't test atomics in workgroup due to initialization boilerplate. + t.skipIf( + testcase.type.includes('atomic') && t.params.aspace !== 'storage', + `Skipping atomic test for non-storage address space` + ); + + t.skipIf( + testcase.skip_uniform === true && t.params.aspace === 'uniform', + `Uniform requires 16 byte alignment` + ); + }) + .fn(t => { + const testcase = kLayoutCases[t.params.case]; + let code = ` +${testcase.f16 ? 'enable f16;' : ''} +${testcase.decl} + +@group(0) @binding(1) +var<storage, read_write> out : u32; +`; + + if (t.params.aspace === 'uniform') { + code += `@group(0) @binding(0) + var<${t.params.aspace}> in : ${testcase.type};`; + } else if (t.params.aspace === 'storage') { + // Use read_write for input data to support atomics. + code += `@group(0) @binding(0) + var<${t.params.aspace}, read_write> in : ${testcase.type};`; + } else { + code += `@group(0) @binding(0) + var<storage> pre_in : ${testcase.type};`; + if (t.params.aspace === 'workgroup') { + code += ` + var<workgroup> in : ${testcase.type};`; + } else if (t.params.aspace === 'private') { + code += ` + var<private> in : ${testcase.type};`; + } + } + + code += ` +@compute @workgroup_size(1,1,1) +fn main() { +`; + + if ( + t.params.aspace === 'workgroup' || + t.params.aspace === 'function' || + t.params.aspace === 'private' + ) { + if (t.params.aspace === 'function') { + code += `var in : ${testcase.type};\n`; + } + code += `in = pre_in;`; + if (t.params.aspace === 'workgroup') { + code += `workgroupBarrier();\n`; + } + } + + code += `\n${testcase.read_assign};\n}`; + + let usage = GPUBufferUsage.COPY_SRC; + if (t.params.aspace === 'uniform') { + usage |= GPUBufferUsage.UNIFORM; + } else { + usage |= GPUBufferUsage.STORAGE; + } + + // Magic number is 42 in various representations. + const inMagicNumber = testcase.f16 ? 0x5140 : testcase.f32 ? 0x42280000 : 42; + const in_buffer = t.makeBufferWithContents( + new Uint32Array([ + ...iterRange(128, x => { + if (x * 4 === testcase.offset) { + return inMagicNumber; + } else { + return 0; + } + }), + ]), + usage + ); + t.trackForCleanup(in_buffer); + + const out_buffer = t.makeBufferWithContents( + new Uint32Array([...iterRange(1, x => 0)]), + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST + ); + t.trackForCleanup(out_buffer); + + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: 'main', + }, + }); + + const bg = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: in_buffer, + }, + }, + { + binding: 1, + resource: { + buffer: out_buffer, + }, + }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bg); + pass.dispatchWorkgroups(1, 1, 1); + pass.end(); + t.queue.submit([encoder.finish()]); + + t.expectGPUBufferValuesEqual(out_buffer, new Uint32Array([42])); + }); + +g.test('write_layout') + .desc('Test writing memory layouts') + .params(u => + u + .combine('case', keysOf(kLayoutCases)) + .combine('aspace', ['storage', 'workgroup', 'function', 'private'] as const) + .beginSubcases() + ) + .beforeAllSubcases(t => { + const testcase = kLayoutCases[t.params.case]; + if (testcase.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + // Don't test atomics in workgroup due to initialization boilerplate. + t.skipIf( + testcase.type.includes('atomic') && t.params.aspace !== 'storage', + `Skipping atomic test for non-storage address space` + ); + }) + .fn(t => { + const testcase = kLayoutCases[t.params.case]; + let code = ` +${testcase.f16 ? 'enable f16;' : ''} +${testcase.decl} + +@group(0) @binding(0) +var<storage> in : u32; +`; + + if (t.params.aspace === 'storage') { + code += `@group(0) @binding(1) + var<storage, read_write> out : ${testcase.type};\n`; + } else { + code += `@group(0) @binding(1) + var<storage, read_write> post_out : ${testcase.type};\n`; + + if (t.params.aspace === 'workgroup') { + code += `var<workgroup> out : ${testcase.type};\n`; + } else if (t.params.aspace === 'private') { + code += `var<private> out : ${testcase.type};\n`; + } + } + + code += ` +@compute @workgroup_size(1,1,1) +fn main() { +`; + + if (t.params.aspace === 'function') { + code += `var out : ${testcase.type};\n`; + } + + code += `${testcase.write_assign};\n`; + if ( + t.params.aspace === 'workgroup' || + t.params.aspace === 'function' || + t.params.aspace === 'private' + ) { + if (t.params.aspace === 'workgroup') { + code += `workgroupBarrier();\n`; + } + code += `post_out = out;`; + } + + code += `\n}`; + + const in_buffer = t.makeBufferWithContents( + new Uint32Array([42]), + GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE + ); + t.trackForCleanup(in_buffer); + + const out_buffer = t.makeBufferWithContents( + new Uint32Array([...iterRange(128, x => 0)]), + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST + ); + t.trackForCleanup(out_buffer); + + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: 'main', + }, + }); + + const bg = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: in_buffer, + }, + }, + { + binding: 1, + resource: { + buffer: out_buffer, + }, + }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bg); + pass.dispatchWorkgroups(1, 1, 1); + pass.end(); + t.queue.submit([encoder.finish()]); + + // Magic number is 42 in various representations. + const outMagicNumber = testcase.f16 ? 0x5140 : testcase.f32 ? 0x42280000 : 42; + const expect = new Uint32Array([ + ...iterRange(128, x => { + if (x * 4 === testcase.offset) { + return outMagicNumber; + } else { + return 0; + } + }), + ]); + + t.expectGPUBufferValuesEqual(out_buffer, expect); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts index 478ae28a7a..72fb69e293 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts @@ -43,22 +43,33 @@ const memoryModelTestParams: MemoryModelTestParams = { numBehaviors: 2, }; -// The two kinds of non-atomic accesses tested. +// The three kinds of non-atomic accesses tested. // rw: read -> barrier -> write // wr: write -> barrier -> read // ww: write -> barrier -> write type AccessPair = 'rw' | 'wr' | 'ww'; // Test the non-atomic memory types. -const kMemTypes = [MemoryType.NonAtomicStorageClass, MemoryType.NonAtomicWorkgroupClass] as const; +const kMemTypes = [ + MemoryType.NonAtomicStorageClass, + MemoryType.NonAtomicWorkgroupClass, + MemoryType.NonAtomicTextureClass, +] as const; const storageMemoryBarrierStoreLoadTestCode = ` test_locations.value[x_0] = 1; - workgroupBarrier(); + storageBarrier(); let r0 = u32(test_locations.value[x_1]); atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0); `; +const textureMemoryBarrierStoreLoadTestCode = ` + textureStore(texture_locations, indexToCoord(x_0), vec4u(1)); + textureBarrier(); + let r0 = textureLoad(texture_locations, indexToCoord(x_1)).x; + atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0); +`; + const workgroupMemoryBarrierStoreLoadTestCode = ` wg_test_locations[x_0] = 1; workgroupBarrier(); @@ -66,13 +77,27 @@ const workgroupMemoryBarrierStoreLoadTestCode = ` atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0); `; +const workgroupUniformLoadMemoryBarrierStoreLoadTestCode = ` + wg_test_locations[x_0] = 1; + _ = workgroupUniformLoad(&placeholder_wg_var); + let r0 = u32(wg_test_locations[x_1]); + atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0); +`; + const storageMemoryBarrierLoadStoreTestCode = ` let r0 = u32(test_locations.value[x_0]); - workgroupBarrier(); + storageBarrier(); test_locations.value[x_1] = 1; atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0); `; +const textureMemoryBarrierLoadStoreTestCode = ` + let r0 = textureLoad(texture_locations, indexToCoord(x_0)).x; + textureBarrier(); + textureStore(texture_locations, indexToCoord(x_1), vec4u(1)); + atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0); +`; + const workgroupMemoryBarrierLoadStoreTestCode = ` let r0 = u32(wg_test_locations[x_0]); workgroupBarrier(); @@ -80,12 +105,27 @@ const workgroupMemoryBarrierLoadStoreTestCode = ` atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0); `; +const workgroupUniformLoadMemoryBarrierLoadStoreTestCode = ` + let r0 = u32(wg_test_locations[x_0]); + _ = workgroupUniformLoad(&placeholder_wg_var); + wg_test_locations[x_1] = 1; + atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0); +`; + const storageMemoryBarrierStoreStoreTestCode = ` test_locations.value[x_0] = 1; storageBarrier(); test_locations.value[x_1] = 2; `; +const textureMemoryBarrierStoreStoreTestCode = ` + textureStore(texture_locations, indexToCoord(x_0), vec4u(1)); + textureBarrier(); + textureStore(texture_locations, indexToCoord(x_1), vec4u(2)); + textureBarrier(); + test_locations.value[x_1] = textureLoad(texture_locations, indexToCoord(x_1)).x; +`; + const workgroupMemoryBarrierStoreStoreTestCode = ` wg_test_locations[x_0] = 1; workgroupBarrier(); @@ -94,20 +134,56 @@ const workgroupMemoryBarrierStoreStoreTestCode = ` test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1] = wg_test_locations[x_1]; `; -function getTestCode(p: { memType: MemoryType; accessPair: AccessPair }): string { +const workgroupUniformLoadMemoryBarrierStoreStoreTestCode = ` + wg_test_locations[x_0] = 1; + _ = workgroupUniformLoad(&placeholder_wg_var); + wg_test_locations[x_1] = 2; + _ = workgroupUniformLoad(&placeholder_wg_var); + test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1] = wg_test_locations[x_1]; +`; + +function getTestCode(p: { + memType: MemoryType; + accessPair: AccessPair; + normalBarrier: boolean; +}): string { switch (p.accessPair) { - case 'rw': - return p.memType === MemoryType.NonAtomicStorageClass - ? storageMemoryBarrierLoadStoreTestCode - : workgroupMemoryBarrierLoadStoreTestCode; - case 'wr': - return p.memType === MemoryType.NonAtomicStorageClass - ? storageMemoryBarrierStoreLoadTestCode - : workgroupMemoryBarrierStoreLoadTestCode; - case 'ww': - return p.memType === MemoryType.NonAtomicStorageClass - ? storageMemoryBarrierStoreStoreTestCode - : workgroupMemoryBarrierStoreStoreTestCode; + case 'rw': { + switch (p.memType) { + case MemoryType.NonAtomicStorageClass: + return storageMemoryBarrierLoadStoreTestCode; + case MemoryType.NonAtomicTextureClass: + return textureMemoryBarrierLoadStoreTestCode; + default: + return p.normalBarrier + ? workgroupMemoryBarrierLoadStoreTestCode + : workgroupUniformLoadMemoryBarrierLoadStoreTestCode; + } + } + case 'wr': { + switch (p.memType) { + case MemoryType.NonAtomicStorageClass: + return storageMemoryBarrierStoreLoadTestCode; + case MemoryType.NonAtomicTextureClass: + return textureMemoryBarrierStoreLoadTestCode; + default: + return p.normalBarrier + ? workgroupMemoryBarrierStoreLoadTestCode + : workgroupUniformLoadMemoryBarrierStoreLoadTestCode; + } + } + case 'ww': { + switch (p.memType) { + case MemoryType.NonAtomicStorageClass: + return storageMemoryBarrierStoreStoreTestCode; + case MemoryType.NonAtomicTextureClass: + return textureMemoryBarrierStoreStoreTestCode; + default: + return p.normalBarrier + ? workgroupMemoryBarrierStoreStoreTestCode + : workgroupUniformLoadMemoryBarrierStoreStoreTestCode; + } + } } } @@ -123,13 +199,28 @@ g.test('workgroup_barrier_store_load') .combine('accessValueType', kAccessValueTypes) .combine('memType', kMemTypes) .combine('accessPair', ['wr'] as const) + .combine('normalBarrier', [true, false] as const) ) .beforeAllSubcases(t => { if (t.params.accessValueType === 'f16') { t.selectDeviceOrSkipTestCase('shader-f16'); } + t.skipIf( + !t.params.normalBarrier && t.params.memType !== MemoryType.NonAtomicWorkgroupClass, + 'workgroupUniformLoad does not have storage memory semantics' + ); + t.skipIf( + t.params.memType === MemoryType.NonAtomicTextureClass && t.params.accessValueType === 'f16', + 'textures do not support f16 access' + ); }) .fn(async t => { + t.skipIf( + t.params.memType === MemoryType.NonAtomicTextureClass && + !t.hasLanguageFeature('readonly_and_readwrite_storage_textures'), + 'requires RW storage textures feature' + ); + const resultCode = ` if (r0 == 1u) { atomicAdd(&test_results.seq, 1u); @@ -137,11 +228,14 @@ g.test('workgroup_barrier_store_load') atomicAdd(&test_results.weak, 1u); } `; - const testShader = buildTestShader( + let testShader = buildTestShader( getTestCode(t.params), t.params.memType, TestType.IntraWorkgroup ); + if (!t.params.normalBarrier) { + testShader += '\nvar<workgroup> placeholder_wg_var : u32;\n'; + } const resultShader = buildResultShader( resultCode, TestType.IntraWorkgroup, @@ -152,7 +246,8 @@ g.test('workgroup_barrier_store_load') memoryModelTestParams, testShader, resultShader, - t.params.accessValueType + t.params.accessValueType, + t.params.memType === MemoryType.NonAtomicTextureClass ); await memModelTester.run(15, 1); }); @@ -169,13 +264,28 @@ g.test('workgroup_barrier_load_store') .combine('accessValueType', kAccessValueTypes) .combine('memType', kMemTypes) .combine('accessPair', ['rw'] as const) + .combine('normalBarrier', [true, false] as const) ) .beforeAllSubcases(t => { if (t.params.accessValueType === 'f16') { t.selectDeviceOrSkipTestCase('shader-f16'); } + t.skipIf( + !t.params.normalBarrier && t.params.memType !== MemoryType.NonAtomicWorkgroupClass, + 'workgroupUniformLoad does not have storage memory semantics' + ); + t.skipIf( + t.params.memType === MemoryType.NonAtomicTextureClass && t.params.accessValueType === 'f16', + 'textures do not support f16 access' + ); }) .fn(async t => { + t.skipIf( + t.params.memType === MemoryType.NonAtomicTextureClass && + !t.hasLanguageFeature('readonly_and_readwrite_storage_textures'), + 'requires RW storage textures feature' + ); + const resultCode = ` if (r0 == 0u) { atomicAdd(&test_results.seq, 1u); @@ -183,11 +293,14 @@ g.test('workgroup_barrier_load_store') atomicAdd(&test_results.weak, 1u); } `; - const testShader = buildTestShader( + let testShader = buildTestShader( getTestCode(t.params), t.params.memType, TestType.IntraWorkgroup ); + if (!t.params.normalBarrier) { + testShader += '\nvar<workgroup> placeholder_wg_var : u32;\n'; + } const resultShader = buildResultShader( resultCode, TestType.IntraWorkgroup, @@ -198,7 +311,8 @@ g.test('workgroup_barrier_load_store') memoryModelTestParams, testShader, resultShader, - t.params.accessValueType + t.params.accessValueType, + t.params.memType === MemoryType.NonAtomicTextureClass ); await memModelTester.run(12, 1); }); @@ -215,13 +329,28 @@ g.test('workgroup_barrier_store_store') .combine('accessValueType', kAccessValueTypes) .combine('memType', kMemTypes) .combine('accessPair', ['ww'] as const) + .combine('normalBarrier', [true, false] as const) ) .beforeAllSubcases(t => { if (t.params.accessValueType === 'f16') { t.selectDeviceOrSkipTestCase('shader-f16'); } + t.skipIf( + !t.params.normalBarrier && t.params.memType !== MemoryType.NonAtomicWorkgroupClass, + 'workgroupUniformLoad does not have storage memory semantics' + ); + t.skipIf( + t.params.memType === MemoryType.NonAtomicTextureClass && t.params.accessValueType === 'f16', + 'textures do not support f16 access' + ); }) .fn(async t => { + t.skipIf( + t.params.memType === MemoryType.NonAtomicTextureClass && + !t.hasLanguageFeature('readonly_and_readwrite_storage_textures'), + 'requires RW storage textures feature' + ); + const resultCode = ` if (mem_x_0 == 2u) { atomicAdd(&test_results.seq, 1u); @@ -229,11 +358,14 @@ g.test('workgroup_barrier_store_store') atomicAdd(&test_results.weak, 1u); } `; - const testShader = buildTestShader( + let testShader = buildTestShader( getTestCode(t.params), t.params.memType, TestType.IntraWorkgroup ); + if (!t.params.normalBarrier) { + testShader += '\nvar<workgroup> placeholder_wg_var : u32;\n'; + } const resultShader = buildResultShader( resultCode, TestType.IntraWorkgroup, @@ -244,7 +376,8 @@ g.test('workgroup_barrier_store_store') memoryModelTestParams, testShader, resultShader, - t.params.accessValueType + t.params.accessValueType, + t.params.memType === MemoryType.NonAtomicTextureClass ); await memModelTester.run(10, 1); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts index f8e5b9034c..8dee32b72d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts @@ -1,5 +1,7 @@ -import { GPUTest } from '../../../gpu_test'; +import { GPUTest } from '../../../gpu_test.js'; import { checkElementsPassPredicate } from '../../../util/check_contents.js'; +import { align } from '../../../util/math.js'; +import { PRNG } from '../../../util/prng.js'; /* All buffer sizes are counted in units of 4-byte words. */ @@ -15,6 +17,9 @@ import { checkElementsPassPredicate } from '../../../util/check_contents.js'; export type AccessValueType = 'f16' | 'u32'; export const kAccessValueTypes = ['f16', 'u32'] as const; +/** The width used for textures (default compat limit in WebGPU). */ +const kWidth = 4096; + /* Parameter values are set heuristically, typically by a time-intensive search. */ export type MemoryModelTestParams = { /* Number of invocations per workgroup. The workgroups are 1-dimensional. */ @@ -76,12 +81,33 @@ const numReadOutputs = 2; type BufferWithSource = { /** Buffer used by shader code. */ deviceBuf: GPUBuffer; - /** Buffer populated from the host size, data is copied to device buffer for use by shader. */ + /** Buffer populated from the host side, data is copied to device buffer for use by shader. */ srcBuf: GPUBuffer; /** Size in bytes of the buffer. */ size: number; }; +/** Represents a device texture and a utility buffer for resetting memory and copying parameters. */ +type TextureWithSource = { + /** Texture used by shader code. */ + deviceTex: GPUTexture; + /** Buffer populated from the host side, data is copied to device buffer for use by shader. */ + srcBuf: GPUBuffer; + /** Size in bytes of the buffer. */ + size: number; +}; + +type SubBufferWithSource = { + /** Buffer used by shader code. This buffer is shared for multiple used */ + deviceBuf: GPUBuffer; + /** Buffer populated from the host side, data is copied to device buffer for use by shader. */ + srcBuf: GPUBuffer; + /** Size in bytes of this portion of the buffer. */ + size: number; + /** Offset in bytes of this portion of the buffer */ + offset: number; +}; + /** Specifies the buffers used during a memory model test. */ type MemoryModelBuffers = { /** This is the memory region that testing threads read from and write to. */ @@ -102,6 +128,10 @@ type MemoryModelBuffers = { stressParams: BufferWithSource; }; +type MemoryModelTextures = { + testLocations: TextureWithSource; +}; + /** The number of stress params to add to the stress params buffer. */ const numStressParams = 12; const barrierParamIndex = 0; @@ -128,11 +158,11 @@ const bytesPerWord = 4; * - enable directives, if necessary * - the type alias for AccessValueType */ -function shaderPreamble(accessValueType: AccessValueType): string { +function shaderPreamble(accessValueType: AccessValueType, constants: string): string { if (accessValueType === 'f16') { - return 'enable f16;\nalias AccessValueTy = f16;\n'; + return `enable f16;\nalias AccessValueTy = f16;\n${constants}\n`; } - return `alias AccessValueTy = ${accessValueType};\n`; + return `alias AccessValueTy = ${accessValueType};\n${constants}\n`; } /** @@ -175,10 +205,14 @@ export class MemoryModelTester { protected test: GPUTest; protected params: MemoryModelTestParams; protected buffers: MemoryModelBuffers; + protected textures: MemoryModelTextures | undefined; protected testPipeline: GPUComputePipeline; protected testBindGroup: GPUBindGroup; + protected textureBindGroup: GPUBindGroup | undefined; protected resultPipeline: GPUComputePipeline; protected resultBindGroup: GPUBindGroup; + protected prng: PRNG; + protected useTexture: boolean; /** Sets up a memory model test by initializing buffers and pipeline layouts. */ constructor( @@ -186,24 +220,36 @@ export class MemoryModelTester { params: MemoryModelTestParams, testShader: string, resultShader: string, - accessValueType: AccessValueType = 'u32' + accessValueType: AccessValueType = 'u32', + useTexture: boolean = false ) { + this.prng = new PRNG(1); this.test = t; this.params = params; - - testShader = shaderPreamble(accessValueType) + testShader; - resultShader = shaderPreamble(accessValueType) + resultShader; + this.useTexture = useTexture; + + const workgroupXSize = Math.min(params.workgroupSize, t.device.limits.maxComputeWorkgroupSizeX); + const constants = ` + const kNumBarriers = 1u; // MAINTENANCE_TODO: make barrier not an array + const kMaxWorkgroups = ${params.maxWorkgroups}u; + const kScratchMemorySize = ${params.scratchMemorySize}u; + const kWorkgroupXSize = ${workgroupXSize}u; + `; + testShader = shaderPreamble(accessValueType, constants) + testShader; + resultShader = shaderPreamble(accessValueType, constants) + resultShader; // set up buffers - const testingThreads = this.params.workgroupSize * this.params.testingWorkgroups; + const testingThreads = workgroupXSize * this.params.testingWorkgroups; const testLocationsSize = testingThreads * numMemLocations * this.params.memStride * bytesPerWord; const testLocationsBuffer: BufferWithSource = { deviceBuf: this.test.device.createBuffer({ + label: 'testLocationsBuffer', size: testLocationsSize, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, }), srcBuf: this.test.device.createBuffer({ + label: 'testLocationsSrcBuf', size: testLocationsSize, usage: GPUBufferUsage.COPY_SRC, }), @@ -213,10 +259,12 @@ export class MemoryModelTester { const readResultsSize = testingThreads * numReadOutputs * bytesPerWord; const readResultsBuffer: BufferWithSource = { deviceBuf: this.test.device.createBuffer({ + label: 'readResultsBuffer', size: readResultsSize, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, }), srcBuf: this.test.device.createBuffer({ + label: 'readResultsSrcBuf', size: readResultsSize, usage: GPUBufferUsage.COPY_SRC, }), @@ -226,10 +274,12 @@ export class MemoryModelTester { const testResultsSize = this.params.numBehaviors * bytesPerWord; const testResultsBuffer: BufferWithSource = { deviceBuf: this.test.device.createBuffer({ + label: 'testResultsBuffer', size: testResultsSize, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }), srcBuf: this.test.device.createBuffer({ + label: 'testResultsSrcBuffer', size: testResultsSize, usage: GPUBufferUsage.COPY_SRC, }), @@ -249,52 +299,87 @@ export class MemoryModelTester { size: shuffledWorkgroupsSize, }; - const barrierSize = bytesPerWord; - const barrierBuffer: BufferWithSource = { - deviceBuf: this.test.device.createBuffer({ - size: barrierSize, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, - }), + if (this.useTexture) { + const numTexels = testLocationsSize / bytesPerWord; + const width = kWidth; + const height = numTexels / width; + const textureSize: GPUExtent3D = { width, height }; + const textureLocations: TextureWithSource = { + deviceTex: this.test.device.createTexture({ + format: 'r32uint', + dimension: '2d', + size: textureSize, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING, + }), + srcBuf: testLocationsBuffer.srcBuf, + size: testLocationsSize, + }; + this.textures = { + testLocations: textureLocations, + }; + } + + // Combine 3 arrays into 1 buffer as we need to keep the number of storage buffers to 4 for compat. + const falseSharingAvoidanceQuantum = 4096; + const barrierSize = align(bytesPerWord, falseSharingAvoidanceQuantum); + const scratchpadSize = align( + this.params.scratchMemorySize * bytesPerWord, + falseSharingAvoidanceQuantum + ); + const scratchMemoryLocationsSize = align( + this.params.maxWorkgroups * bytesPerWord, + falseSharingAvoidanceQuantum + ); + const comboSize = barrierSize + scratchpadSize + scratchMemoryLocationsSize; + + const comboBuffer = this.test.device.createBuffer({ + label: 'comboBuffer', + size: comboSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + }); + + const barrierBuffer: SubBufferWithSource = { + deviceBuf: comboBuffer, srcBuf: this.test.device.createBuffer({ + label: 'barrierSrcBuf', size: barrierSize, usage: GPUBufferUsage.COPY_SRC, }), size: barrierSize, + offset: 0, }; - const scratchpadSize = this.params.scratchMemorySize * bytesPerWord; - const scratchpadBuffer: BufferWithSource = { - deviceBuf: this.test.device.createBuffer({ - size: scratchpadSize, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, - }), + const scratchpadBuffer: SubBufferWithSource = { + deviceBuf: comboBuffer, srcBuf: this.test.device.createBuffer({ + label: 'scratchpadSrcBuf', size: scratchpadSize, usage: GPUBufferUsage.COPY_SRC, }), size: scratchpadSize, + offset: barrierSize, }; - const scratchMemoryLocationsSize = this.params.maxWorkgroups * bytesPerWord; - const scratchMemoryLocationsBuffer: BufferWithSource = { - deviceBuf: this.test.device.createBuffer({ - size: scratchMemoryLocationsSize, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, - }), + const scratchMemoryLocationsBuffer: SubBufferWithSource = { + deviceBuf: comboBuffer, srcBuf: this.test.device.createBuffer({ + label: 'scratchMemoryLocationsSrcBuf', size: scratchMemoryLocationsSize, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, }), size: scratchMemoryLocationsSize, + offset: barrierSize + scratchpadSize, }; const stressParamsSize = numStressParams * bytesPerWord; const stressParamsBuffer: BufferWithSource = { deviceBuf: this.test.device.createBuffer({ + label: 'stressParamsBuffer', size: stressParamsSize, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, }), srcBuf: this.test.device.createBuffer({ + label: 'stressParamsSrcBuf', size: stressParamsSize, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, }), @@ -314,19 +399,50 @@ export class MemoryModelTester { // set up pipeline layouts const testLayout = this.test.device.createBindGroupLayout({ + label: 'testLayout', entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, - { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, - { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, - { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, + { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, ], }); + + let layouts: GPUBindGroupLayout[] = [testLayout]; + if (this.useTexture) { + const textureLayout = this.test.device.createBindGroupLayout({ + label: 'textureLayout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + storageTexture: { + access: 'read-write', + format: 'r32uint', + viewDimension: '2d', + }, + }, + ], + }); + layouts = [testLayout, textureLayout]; + + const texLocations = (this.textures as MemoryModelTextures).testLocations.deviceTex; + this.textureBindGroup = this.test.device.createBindGroup({ + label: 'textureBindGroup', + entries: [ + { + binding: 0, + resource: texLocations.createView(), + }, + ], + layout: textureLayout, + }); + } this.testPipeline = this.test.device.createComputePipeline({ + label: 'testPipeline', layout: this.test.device.createPipelineLayout({ - bindGroupLayouts: [testLayout], + bindGroupLayouts: layouts, }), compute: { module: this.test.device.createShaderModule({ @@ -336,19 +452,19 @@ export class MemoryModelTester { }, }); this.testBindGroup = this.test.device.createBindGroup({ + label: 'testBindGroup', entries: [ { binding: 0, resource: { buffer: this.buffers.testLocations.deviceBuf } }, { binding: 1, resource: { buffer: this.buffers.readResults.deviceBuf } }, { binding: 2, resource: { buffer: this.buffers.shuffledWorkgroups.deviceBuf } }, - { binding: 3, resource: { buffer: this.buffers.barrier.deviceBuf } }, - { binding: 4, resource: { buffer: this.buffers.scratchpad.deviceBuf } }, - { binding: 5, resource: { buffer: this.buffers.scratchMemoryLocations.deviceBuf } }, - { binding: 6, resource: { buffer: this.buffers.stressParams.deviceBuf } }, + { binding: 3, resource: { buffer: comboBuffer } }, + { binding: 4, resource: { buffer: this.buffers.stressParams.deviceBuf } }, ], layout: testLayout, }); const resultLayout = this.test.device.createBindGroupLayout({ + label: 'resultLayout', entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, @@ -357,6 +473,7 @@ export class MemoryModelTester { ], }); this.resultPipeline = this.test.device.createComputePipeline({ + label: 'resultPipeline', layout: this.test.device.createPipelineLayout({ bindGroupLayouts: [resultLayout], }), @@ -368,6 +485,7 @@ export class MemoryModelTester { }, }); this.resultBindGroup = this.test.device.createBindGroup({ + label: 'resultBindGroup', entries: [ { binding: 0, resource: { buffer: this.buffers.testLocations.deviceBuf } }, { binding: 1, resource: { buffer: this.buffers.readResults.deviceBuf } }, @@ -402,10 +520,16 @@ export class MemoryModelTester { this.copyBufferToBuffer(encoder, this.buffers.scratchpad); this.copyBufferToBuffer(encoder, this.buffers.scratchMemoryLocations); this.copyBufferToBuffer(encoder, this.buffers.stressParams); + if (this.useTexture) { + this.copyBufferToTexture(encoder, (this.textures as MemoryModelTextures).testLocations); + } const testPass = encoder.beginComputePass(); testPass.setPipeline(this.testPipeline); testPass.setBindGroup(0, this.testBindGroup); + if (this.useTexture) { + testPass.setBindGroup(1, this.textureBindGroup as GPUBindGroup); + } testPass.dispatchWorkgroups(numWorkgroups); testPass.end(); @@ -443,8 +567,8 @@ export class MemoryModelTester { * If the weak index's value is not 0, it means the test has observed a behavior disallowed by the memory model and * is considered a test failure. */ - protected checkResult(weakIndex: number): (i: number, v: number) => boolean { - return function (i: number, v: number): boolean { + protected checkResult(weakIndex: number): (i: number, v: number | bigint) => boolean { + return function (i: number, v: number | bigint): boolean { if (i === weakIndex && v > 0) { return false; } @@ -453,7 +577,7 @@ export class MemoryModelTester { } /** Returns a printer function that visualizes the results of checking the test results. */ - protected resultPrinter(weakIndex: number): (i: number) => string | number { + protected resultPrinter(weakIndex: number): (i: number) => string | number | bigint { return function (i: number): string | number { if (i === weakIndex) { return 0; @@ -464,16 +588,42 @@ export class MemoryModelTester { } /** Utility method that simplifies copying source buffers to device buffers. */ - protected copyBufferToBuffer(encoder: GPUCommandEncoder, buffer: BufferWithSource): void { - encoder.copyBufferToBuffer(buffer.srcBuf, 0, buffer.deviceBuf, 0, buffer.size); + protected copyBufferToBuffer( + encoder: GPUCommandEncoder, + buffer: BufferWithSource | SubBufferWithSource + ): void { + encoder.copyBufferToBuffer( + buffer.srcBuf, + 0, + buffer.deviceBuf, + (buffer as SubBufferWithSource).offset || 0, + buffer.size + ); } - /** Returns a random integer between 0 and the max. */ + /** Utility method that simplifies copying source buffers to device textures. */ + protected copyBufferToTexture(encoder: GPUCommandEncoder, texture: TextureWithSource): void { + const bytesPerWord = 4; // always uses r32uint format. + const numTexels = texture.size / bytesPerWord; + const size: GPUExtent3D = { width: kWidth, height: numTexels / kWidth }; + encoder.copyBufferToTexture( + { + buffer: texture.srcBuf, + offset: 0, + bytesPerRow: kWidth * bytesPerWord, + rowsPerImage: size.height, + }, + { texture: texture.deviceTex }, + size + ); + } + + /** Returns a random integer in the range [0, max). */ protected getRandomInt(max: number): number { - return Math.floor(Math.random() * max); + return this.prng.randomU32() % max; } - /** Returns a random number in between the min and max values. */ + /** Returns a random number in the range [min, max). */ protected getRandomInRange(min: number, max: number): number { if (min === max) { return min; @@ -626,7 +776,19 @@ const shaderMemStructures = ` }; struct IndexMemory { - value: array<u32> + value: array<u32>, + }; + + struct AtomicMemoryBarrier { + value: array<atomic<u32>, kNumBarriers> + }; + + struct IndexMemoryScratchpad { + value: array<u32, kMaxWorkgroups>, + }; + + struct IndexMemoryScratchLocations { + value: array<u32, kScratchMemorySize>, }; struct ReadResult { @@ -635,7 +797,14 @@ const shaderMemStructures = ` }; struct ReadResults { - value: array<ReadResult> + value: array<ReadResult>, + }; + + // These arrays are combine into 1 buffer because compat mode only supports 4 storage buffers by default. + struct CombinedData { + barrier: AtomicMemoryBarrier, + scratchpad: IndexMemoryScratchpad, + scratch_locations: IndexMemoryScratchLocations, }; struct StressParamsMemory { @@ -687,10 +856,8 @@ const twoBehaviorTestResultStructure = ` const commonTestShaderBindings = ` @group(0) @binding(1) var<storage, read_write> results : ReadResults; @group(0) @binding(2) var<storage, read> shuffled_workgroups : IndexMemory; - @group(0) @binding(3) var<storage, read_write> barrier : AtomicMemory; - @group(0) @binding(4) var<storage, read_write> scratchpad : IndexMemory; - @group(0) @binding(5) var<storage, read_write> scratch_locations : IndexMemory; - @group(0) @binding(6) var<uniform> stress_params : StressParamsMemory; + @group(0) @binding(3) var<storage, read_write> combo : CombinedData; + @group(0) @binding(4) var<uniform> stress_params : StressParamsMemory; `; /** The combined bindings for a test on atomic memory. */ @@ -709,6 +876,11 @@ const nonAtomicTestShaderBindings = [ commonTestShaderBindings, ].join('\n'); +/** The extra binding for texture non-atomic texture tests. */ +const textureBindings = ` +@group(1) @binding(0) var texture_locations : texture_storage_2d<r32uint, read_write>; +`; + /** Bindings used in the result aggregation phase of the test. */ const resultShaderBindings = ` @group(0) @binding(0) var<storage, read_write> test_locations : Memory; @@ -750,6 +922,16 @@ const memoryLocationFunctions = ` } `; +/** + * Function to convert an index into an equivalent 2D coordinate for the texture. + */ +const textureFunctions = ` + const kWidth = ${kWidth}; + fn indexToCoord(idx : u32) -> vec2u { + return vec2u(idx % kWidth, idx / kWidth); + } +`; + /** Functions that help add stress to the test. */ const testShaderFunctions = ` //Force the invocations in the workgroup to wait for each other, but without the general memory ordering @@ -758,12 +940,12 @@ const testShaderFunctions = ` // the barrier but does not overly reduce testing throughput. fn spin(limit: u32) { var i : u32 = 0u; - var bar_val : u32 = atomicAdd(&barrier.value[0], 1u); + var bar_val : u32 = atomicAdd(&combo.barrier.value[0], 1u); loop { if (i == 1024u || bar_val >= limit) { break; } - bar_val = atomicAdd(&barrier.value[0], 0u); + bar_val = atomicAdd(&combo.barrier.value[0], 0u); i = i + 1u; } } @@ -773,44 +955,44 @@ const testShaderFunctions = ` // the compiler optimizing out unused loads, where 100,000 is larger than the maximum number of stress iterations used // in any test. fn do_stress(iterations: u32, pattern: u32, workgroup_id: u32) { - let addr = scratch_locations.value[workgroup_id]; + let addr = combo.scratch_locations.value[workgroup_id]; switch(pattern) { case 0u: { for(var i: u32 = 0u; i < iterations; i = i + 1u) { - scratchpad.value[addr] = i; - scratchpad.value[addr] = i + 1u; + combo.scratchpad.value[addr] = i; + combo.scratchpad.value[addr] = i + 1u; } } case 1u: { for(var i: u32 = 0u; i < iterations; i = i + 1u) { - scratchpad.value[addr] = i; - let tmp1: u32 = scratchpad.value[addr]; + combo.scratchpad.value[addr] = i; + let tmp1: u32 = combo.scratchpad.value[addr]; if (tmp1 > 100000u) { - scratchpad.value[addr] = i; + combo.scratchpad.value[addr] = i; break; } } } case 2u: { for(var i: u32 = 0u; i < iterations; i = i + 1u) { - let tmp1: u32 = scratchpad.value[addr]; + let tmp1: u32 = combo.scratchpad.value[addr]; if (tmp1 > 100000u) { - scratchpad.value[addr] = i; + combo.scratchpad.value[addr] = i; break; } - scratchpad.value[addr] = i; + combo.scratchpad.value[addr] = i; } } case 3u: { for(var i: u32 = 0u; i < iterations; i = i + 1u) { - let tmp1: u32 = scratchpad.value[addr]; + let tmp1: u32 = combo.scratchpad.value[addr]; if (tmp1 > 100000u) { - scratchpad.value[addr] = i; + combo.scratchpad.value[addr] = i; break; } - let tmp2: u32 = scratchpad.value[addr]; + let tmp2: u32 = combo.scratchpad.value[addr]; if (tmp2 > 100000u) { - scratchpad.value[addr] = i; + combo.scratchpad.value[addr] = i; break; } } @@ -827,7 +1009,7 @@ const testShaderFunctions = ` */ const shaderEntryPoint = ` // Change to pipeline overridable constant when possible. - const workgroupXSize = 256u; + const workgroupXSize = kWorkgroupXSize; @compute @workgroup_size(workgroupXSize) fn main( @builtin(local_invocation_id) local_invocation_id : vec3<u32>, @builtin(workgroup_id) workgroup_id : vec3<u32>) { @@ -980,6 +1162,18 @@ const storageMemoryNonAtomicTestShaderCode = [ testShaderCommonHeader, ].join('\n'); +/** The common shader code for the test shaders that perform non-atomic texture memory litmus tests. */ +const textureMemoryNonAtomicTestShaderCode = [ + shaderMemStructures, + nonAtomicTestShaderBindings, + textureBindings, + memoryLocationFunctions, + textureFunctions, + testShaderFunctions, + shaderEntryPoint, + testShaderCommonHeader, +].join('\n'); + /** The common shader code for test shaders that perform atomic workgroup class memory litmus tests. */ const workgroupMemoryAtomicTestShaderCode = [ shaderMemStructures, @@ -1023,6 +1217,8 @@ export enum MemoryType { AtomicWorkgroupClass = 'atomic_workgroup', /** Non-atomic memory in the workgroup address space. */ NonAtomicWorkgroupClass = 'non_atomic_workgroup', + /** Non-atomic memory in a texture. */ + NonAtomicTextureClass = 'non_atomic_texture', } /** @@ -1052,21 +1248,26 @@ export function buildTestShader( testType: TestType ): string { let memoryTypeCode; - let isStorageAS = false; + let isGlobalSpace = false; switch (memoryType) { case MemoryType.AtomicStorageClass: memoryTypeCode = storageMemoryAtomicTestShaderCode; - isStorageAS = true; + isGlobalSpace = true; break; case MemoryType.NonAtomicStorageClass: memoryTypeCode = storageMemoryNonAtomicTestShaderCode; - isStorageAS = true; + isGlobalSpace = true; break; case MemoryType.AtomicWorkgroupClass: memoryTypeCode = workgroupMemoryAtomicTestShaderCode; break; case MemoryType.NonAtomicWorkgroupClass: memoryTypeCode = workgroupMemoryNonAtomicTestShaderCode; + break; + case MemoryType.NonAtomicTextureClass: + memoryTypeCode = textureMemoryNonAtomicTestShaderCode; + isGlobalSpace = true; + break; } let testTypeCode; switch (testType) { @@ -1074,7 +1275,7 @@ export function buildTestShader( testTypeCode = interWorkgroupTestShaderCode; break; case TestType.IntraWorkgroup: - if (isStorageAS) { + if (isGlobalSpace) { testTypeCode = storageIntraWorkgroupTestShaderCode; } else { testTypeCode = intraWorkgroupTestShaderCode; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/texture_intra_invocation_coherence.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/texture_intra_invocation_coherence.spec.ts new file mode 100644 index 0000000000..1ab8c3d5b4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/texture_intra_invocation_coherence.spec.ts @@ -0,0 +1,333 @@ +export const description = ` +Test that read/write storage textures are coherent within an invocation. + +Each invocation is assigned several random writing indices and a single +read index from among those. Writes are randomly predicated (except the +one corresponding to the read). Checks that an invocation can read data +it has written to the texture previously. +Does not test coherence between invocations + +Some platform (e.g. Metal) require a fence call to make writes visible +to reads performed by the same invocation. These tests attempt to ensure +WebGPU implementations emit correct fence calls.`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { unreachable, iterRange } from '../../../../common/util/util.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { PRNG } from '../../../util/prng.js'; + +const kRWStorageFormats: GPUTextureFormat[] = ['r32uint', 'r32sint', 'r32float']; +const kDimensions: GPUTextureViewDimension[] = ['1d', '2d', '2d-array', '3d']; + +export const g = makeTestGroup(GPUTest); + +function indexToCoord(dim: GPUTextureViewDimension): string { + switch (dim) { + case '1d': { + return ` +fn indexToCoord(idx : u32) -> u32 { + return idx; +}`; + } + case '2d': + case '2d-array': { + return ` +fn indexToCoord(idx : u32) -> vec2u { + return vec2u(idx % (wgx * num_wgs_x), idx / (wgx * num_wgs_x)); +}`; + } + case '3d': { + return ` +fn indexToCoord(idx : u32) -> vec3u { + return vec3u(idx % (wgx * num_wgs_x), idx / (wgx * num_wgs_x), 0); +}`; + } + default: { + unreachable(`unhandled dimension: ${dim}`); + } + } + return ``; +} + +function textureType(format: GPUTextureFormat, dim: GPUTextureViewDimension): string { + let t = `texture_storage_`; + switch (dim) { + case '1d': { + t += '1d'; + break; + } + case '2d': { + t += '2d'; + break; + } + case '2d-array': { + t += '2d_array'; + break; + } + case '3d': { + t += '3d'; + break; + } + default: { + unreachable(`unhandled dim: ${dim}`); + } + } + t += `<${format}, read_write>`; + return t; +} + +function textureStore(dim: GPUTextureViewDimension, index: string): string { + let code = `textureStore(t, indexToCoord(${index}), `; + if (dim === '2d-array') { + code += `0, `; + } + code += `texel)`; + return code; +} + +function textureLoad(dim: GPUTextureViewDimension, format: GPUTextureFormat): string { + let code = `textureLoad(t, indexToCoord(read_index[global_index])`; + if (dim === '2d-array') { + code += `, 0`; + } + code += `).x`; + if (format !== 'r32uint') { + code = `u32(${code})`; + } + return code; +} + +function texel(format: GPUTextureFormat): string { + switch (format) { + case 'r32uint': { + return 'vec4u(global_index,0,0,0)'; + } + case 'r32sint': { + return 'vec4i(i32(global_index),0,0,0)'; + } + case 'r32float': { + return 'vec4f(f32(global_index),0,0,0)'; + } + default: { + unreachable('unhandled format: ${format}'); + } + } + return ''; +} + +function getTextureSize(numTexels: number, dim: GPUTextureViewDimension): GPUExtent3D { + const size: GPUExtent3D = { width: 1, height: 1, depthOrArrayLayers: 1 }; + switch (dim) { + case '1d': { + size.width = numTexels; + break; + } + case '2d': + case '2d-array': + case '3d': { + size.width = numTexels / 2; + size.height = numTexels / 2; + // depthOrArrayLayers defaults to 1 + break; + } + default: { + unreachable(`unhandled dim: ${dim}`); + } + } + return size; +} + +g.test('texture_intra_invocation_coherence') + .desc(`Tests writes from an invocation are visible to reads from the same invocation`) + .params(u => u.combine('format', kRWStorageFormats).combine('dim', kDimensions)) + .beforeAllSubcases(t => { + t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format); + }) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + + const wgx = 16; + const wgy = t.device.limits.maxComputeInvocationsPerWorkgroup / wgx; + const num_wgs_x = 2; + const num_wgs_y = 2; + const invocations = wgx * wgy * num_wgs_x * num_wgs_y; + const num_writes_per_invocation = 4; + + const code = ` +requires readonly_and_readwrite_storage_textures; + +@group(0) @binding(0) +var t : ${textureType(t.params.format, t.params.dim)}; + +@group(1) @binding(0) +var<storage> write_indices : array<vec4u>; + +@group(1) @binding(1) +var<storage> read_index : array<u32>; + +@group(1) @binding(2) +var<storage> write_mask : array<vec4u>; + +@group(1) @binding(3) +var<storage, read_write> output : array<u32>; + +const wgx = ${wgx}u; +const wgy = ${wgy}u; +const num_wgs_x = ${num_wgs_x}u; +const num_wgs_y = ${num_wgs_y}u; + +${indexToCoord(t.params.dim)} + +@compute @workgroup_size(wgx, wgy, 1) +fn main(@builtin(global_invocation_id) gid : vec3u) { + let global_index = gid.x + gid.y * num_wgs_x * wgx; + + let write_index = write_indices[global_index]; + let mask = write_mask[global_index]; + let texel = ${texel(t.params.format)}; + + if mask.x != 0 { + ${textureStore(t.params.dim, 'write_index.x')}; + } + if mask.y != 0 { + ${textureStore(t.params.dim, 'write_index.y')}; + } + if mask.z != 0 { + ${textureStore(t.params.dim, 'write_index.z')}; + } + if mask.w != 0 { + ${textureStore(t.params.dim, 'write_index.w')}; + } + output[global_index] = ${textureLoad(t.params.dim, t.params.format)}; +}`; + + // To get a variety of testing, seed the random number generator based on which case this is. + // This means subcases will not execute the same code. + const seed = + kRWStorageFormats.indexOf(t.params.format) * kRWStorageFormats.length + + kDimensions.indexOf(t.params.dim); + const prng = new PRNG(seed); + + const num_write_indices = invocations * num_writes_per_invocation; + const write_indices = new Uint32Array([...iterRange(num_write_indices, x => x)]); + const write_masks = new Uint32Array([...iterRange(num_write_indices, x => 0)]); + // Shuffle the indices. + for (let i = 0; i < num_write_indices; i++) { + const remaining = num_write_indices - i; + const swapIdx = (prng.randomU32() % remaining) + i; + const tmp = write_indices[swapIdx]; + write_indices[swapIdx] = write_indices[i]; + write_indices[i] = tmp; + + // Assign random write masks + const mask = prng.randomU32() % 2; + write_masks[i] = mask; + } + const num_read_indices = invocations; + const read_indices = new Uint32Array(num_read_indices); + for (let i = 0; i < num_read_indices; i++) { + // Pick a random index from index from this invocation's writes to read from. + // Ensure that write is not masked out. + const readIdx = prng.randomU32() % num_writes_per_invocation; + read_indices[i] = write_indices[num_writes_per_invocation * i + readIdx]; + write_masks[num_writes_per_invocation * i + readIdx] = 1; + } + + // Buffers + const write_index_buffer = t.makeBufferWithContents( + write_indices, + GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE + ); + t.trackForCleanup(write_index_buffer); + const read_index_buffer = t.makeBufferWithContents( + read_indices, + GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE + ); + t.trackForCleanup(read_index_buffer); + const write_mask_buffer = t.makeBufferWithContents( + write_masks, + GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE + ); + t.trackForCleanup(write_mask_buffer); + const output_buffer = t.makeBufferWithContents( + new Uint32Array([...iterRange(invocations, x => 0)]), + GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE + ); + t.trackForCleanup(output_buffer); + + // Texture + const texture_size = getTextureSize(invocations * num_writes_per_invocation, t.params.dim); + const texture = t.device.createTexture({ + format: t.params.format, + dimension: t.params.dim === '2d-array' ? '2d' : (t.params.dim as GPUTextureDimension), + size: texture_size, + usage: GPUTextureUsage.STORAGE_BINDING, + }); + t.trackForCleanup(texture); + + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: 'main', + }, + }); + + const bg0 = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: texture.createView({ + format: t.params.format, + dimension: t.params.dim, + mipLevelCount: 1, + arrayLayerCount: 1, + }), + }, + ], + }); + const bg1 = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(1), + entries: [ + { + binding: 0, + resource: { + buffer: write_index_buffer, + }, + }, + { + binding: 1, + resource: { + buffer: read_index_buffer, + }, + }, + { + binding: 2, + resource: { + buffer: write_mask_buffer, + }, + }, + { + binding: 3, + resource: { + buffer: output_buffer, + }, + }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bg0); + pass.setBindGroup(1, bg1); + pass.dispatchWorkgroups(num_wgs_x, num_wgs_y, 1); + pass.end(); + t.queue.submit([encoder.finish()]); + + const expectedOutput = new Uint32Array([...iterRange(num_read_indices, x => x)]); + t.expectGPUBufferValuesEqual(output_buffer, expectedOutput); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts index 965dd283dd..70d3438fcf 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts @@ -7,6 +7,7 @@ TODO: add tests to check that textureLoad operations stay in-bounds. import { makeTestGroup } from '../../../common/framework/test_group.js'; import { assert } from '../../../common/util/util.js'; +import { Float16Array } from '../../../external/petamoriken/float16/float16.js'; import { GPUTest } from '../../gpu_test.js'; import { align } from '../../util/math.js'; import { generateTypes, supportedScalarTypes, supportsAtomics } from '../types.js'; @@ -25,6 +26,7 @@ const kMinI32 = -0x8000_0000; */ async function runShaderTest( t: GPUTest, + enables: string, stage: GPUShaderStageFlags, testSource: string, layout: GPUPipelineLayout, @@ -41,7 +43,7 @@ async function runShaderTest( usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE, }); - const source = ` + const source = `${enables} struct Constants { zero: u32 }; @@ -96,10 +98,12 @@ fn main() { /** Fill an ArrayBuffer with sentinel values, except clear a region to zero. */ function testFillArrayBuffer( array: ArrayBuffer, - type: 'u32' | 'i32' | 'f32', + type: 'u32' | 'i32' | 'f16' | 'f32', { zeroByteStart, zeroByteCount }: { zeroByteStart: number; zeroByteCount: number } ) { - const constructor = { u32: Uint32Array, i32: Int32Array, f32: Float32Array }[type]; + const constructor = { u32: Uint32Array, i32: Int32Array, f16: Float16Array, f32: Float32Array }[ + type + ]; assert(zeroByteCount % constructor.BYTES_PER_ELEMENT === 0); new constructor(array).fill(42); new constructor(array, zeroByteStart, zeroByteCount / constructor.BYTES_PER_ELEMENT).fill(0); @@ -122,6 +126,7 @@ g.test('linear_memory') TODO: Test types like vec2<atomic<i32>>, if that's allowed. TODO: Test exprIndexAddon as constexpr. TODO: Test exprIndexAddon as pipeline-overridable constant expression. + TODO: Adjust test logic to support array of f16 in the uniform address space ` ) .params(u => @@ -168,10 +173,15 @@ g.test('linear_memory') { shadowingMode: 'function-scope' }, ]) .expand('isAtomic', p => (supportsAtomics(p) ? [false, true] : [false])) - .beginSubcases() .expand('baseType', supportedScalarTypes) + .beginSubcases() .expandWithParams(generateTypes) ) + .beforeAllSubcases(t => { + if (t.params.baseType === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) .fn(async t => { const { addressSpace, @@ -189,6 +199,13 @@ g.test('linear_memory') assert(_kTypeInfo !== undefined, 'not an indexable type'); assert('arrayLength' in _kTypeInfo); + if (baseType === 'f16' && addressSpace === 'uniform' && containerType === 'array') { + // Array elements must be aligned to 16 bytes, but the logic in generateTypes + // creates an array of vec4 of the baseType. But for f16 that's only 8 bytes. + // We would need to write more complex logic for that. + t.skip('Test logic does not handle array of f16 in the uniform address space'); + } + let usesCanary = false; let globalSource = ''; let testFunctionSource = ''; @@ -429,6 +446,8 @@ fn runTest() -> u32 { ], }); + const enables = t.params.baseType === 'f16' ? 'enable f16;' : ''; + // Run it. if (bufferBindingSize !== undefined && baseType !== 'bool') { const expectedData = new ArrayBuffer(testBufferSize); @@ -450,6 +469,7 @@ fn runTest() -> u32 { // Run the shader, accessing the buffer. await runShaderTest( t, + enables, GPUShaderStage.COMPUTE, testSource, layout, @@ -475,6 +495,6 @@ fn runTest() -> u32 { bufferBindingEnd ); } else { - await runShaderTest(t, GPUShaderStage.COMPUTE, testSource, layout, []); + await runShaderTest(t, enables, GPUShaderStage.COMPUTE, testSource, layout, []); } }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts index de90301592..32a4c243df 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts @@ -545,6 +545,7 @@ g.test('vertex_buffer_access') .combine('additionalBuffers', [0, 4]) .combine('partialLastNumber', [false, true]) .combine('offsetVertexBuffer', [false, true]) + .beginSubcases() .combine('errorScale', [0, 1, 4, 10 ** 2, 10 ** 4, 10 ** 6]) .unless(p => p.drawCallTestParameter === 'instanceCount' && p.errorScale > 10 ** 4) // To avoid timeout ) diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts index fcf3159c64..a40b426332 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts @@ -1,7 +1,6 @@ export const description = `Test compute shader builtin variables`; import { makeTestGroup } from '../../../../common/framework/test_group.js'; -import { iterRange } from '../../../../common/util/util.js'; import { GPUTest } from '../../../gpu_test.js'; export const g = makeTestGroup(GPUTest); @@ -98,17 +97,14 @@ g.test('inputs') // WGSL shader that stores every builtin value to a buffer, for every invocation in the grid. const wgsl = ` - struct S { - data : array<u32> + struct Outputs { + local_id: vec3u, + local_index: u32, + global_id: vec3u, + group_id: vec3u, + num_groups: vec3u, }; - struct V { - data : array<vec3<u32>> - }; - @group(0) @binding(0) var<storage, read_write> local_id_out : V; - @group(0) @binding(1) var<storage, read_write> local_index_out : S; - @group(0) @binding(2) var<storage, read_write> global_id_out : V; - @group(0) @binding(3) var<storage, read_write> group_id_out : V; - @group(0) @binding(4) var<storage, read_write> num_groups_out : V; + @group(0) @binding(0) var<storage, read_write> outputs : array<Outputs>; ${structures} @@ -122,11 +118,13 @@ g.test('inputs') ) { let group_index = ((${group_id}.z * ${num_groups}.y) + ${group_id}.y) * ${num_groups}.x + ${group_id}.x; let global_index = group_index * ${invocationsPerGroup}u + ${local_index}; - local_id_out.data[global_index] = ${local_id}; - local_index_out.data[global_index] = ${local_index}; - global_id_out.data[global_index] = ${global_id}; - group_id_out.data[global_index] = ${group_id}; - num_groups_out.data[global_index] = ${num_groups}; + var o: Outputs; + o.local_id = ${local_id}; + o.local_index = ${local_index}; + o.global_id = ${global_id}; + o.group_id = ${group_id}; + o.num_groups = ${num_groups}; + outputs[global_index] = o; } `; @@ -140,35 +138,24 @@ g.test('inputs') }, }); - // Helper to create a `size`-byte buffer with binding number `binding`. - function createBuffer(size: number, binding: number) { - const buffer = t.device.createBuffer({ - size, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, - }); - t.trackForCleanup(buffer); - - bindGroupEntries.push({ - binding, - resource: { - buffer, - }, - }); - - return buffer; - } + // Offsets are in u32 size units + const kLocalIdOffset = 0; + const kLocalIndexOffset = 3; + const kGlobalIdOffset = 4; + const kGroupIdOffset = 8; + const kNumGroupsOffset = 12; + const kOutputElementSize = 16; // Create the output buffers. - const bindGroupEntries: GPUBindGroupEntry[] = []; - const localIdBuffer = createBuffer(totalInvocations * 16, 0); - const localIndexBuffer = createBuffer(totalInvocations * 4, 1); - const globalIdBuffer = createBuffer(totalInvocations * 16, 2); - const groupIdBuffer = createBuffer(totalInvocations * 16, 3); - const numGroupsBuffer = createBuffer(totalInvocations * 16, 4); + const outputBuffer = t.device.createBuffer({ + size: totalInvocations * kOutputElementSize * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + t.trackForCleanup(outputBuffer); const bindGroup = t.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), - entries: bindGroupEntries, + entries: [{ binding: 0, resource: { buffer: outputBuffer } }], }); // Run the shader. @@ -204,11 +191,7 @@ g.test('inputs') // Helper to check that the vec3<u32> value at each index of the provided `output` buffer // matches the expected value for that invocation, as generated by the `getBuiltinValue` // function. The `name` parameter is the builtin name, used for error messages. - const checkEachIndex = ( - output: Uint32Array, - name: string, - getBuiltinValue: (groupId: vec3, localId: vec3) => vec3 - ) => { + const checkEachIndex = (output: Uint32Array) => { // Loop over workgroups. for (let gz = 0; gz < t.params.numGroups.z; gz++) { for (let gy = 0; gy < t.params.numGroups.y; gy++) { @@ -220,30 +203,44 @@ g.test('inputs') const groupIndex = (gz * t.params.numGroups.y + gy) * t.params.numGroups.x + gx; const localIndex = (lz * t.params.groupSize.y + ly) * t.params.groupSize.x + lx; const globalIndex = groupIndex * invocationsPerGroup + localIndex; - const expected = getBuiltinValue( - { x: gx, y: gy, z: gz }, - { x: lx, y: ly, z: lz } - ); - if (output[globalIndex * 4 + 0] !== expected.x) { - return new Error( - `${name}.x failed at group(${gx},${gy},${gz}) local(${lx},${ly},${lz}))\n` + - ` expected: ${expected.x}\n` + - ` got: ${output[globalIndex * 4 + 0]}` - ); - } - if (output[globalIndex * 4 + 1] !== expected.y) { - return new Error( - `${name}.y failed at group(${gx},${gy},${gz}) local(${lx},${ly},${lz}))\n` + - ` expected: ${expected.y}\n` + - ` got: ${output[globalIndex * 4 + 1]}` + const globalOffset = globalIndex * kOutputElementSize; + + const expectEqual = (name: string, expected: number, actual: number) => { + if (actual !== expected) { + return new Error( + `${name} failed at group(${gx},${gy},${gz}) local(${lx},${ly},${lz}))\n` + + ` expected: ${expected}\n` + + ` got: ${actual}` + ); + } + return undefined; + }; + + const checkVec3Value = (name: string, fieldOffset: number, expected: vec3) => { + const offset = globalOffset + fieldOffset; + return ( + expectEqual(`${name}.x`, expected.x, output[offset + 0]) || + expectEqual(`${name}.y`, expected.y, output[offset + 1]) || + expectEqual(`${name}.z`, expected.z, output[offset + 2]) ); - } - if (output[globalIndex * 4 + 2] !== expected.z) { - return new Error( - `${name}.z failed at group(${gx},${gy},${gz}) local(${lx},${ly},${lz}))\n` + - ` expected: ${expected.z}\n` + - ` got: ${output[globalIndex * 4 + 2]}` + }; + + const error = + checkVec3Value('local_id', kLocalIdOffset, { x: lx, y: ly, z: lz }) || + checkVec3Value('global_id', kGlobalIdOffset, { + x: gx * t.params.groupSize.x + lx, + y: gy * t.params.groupSize.y + ly, + z: gz * t.params.groupSize.z + lz, + }) || + checkVec3Value('group_id', kGroupIdOffset, { x: gx, y: gy, z: gz }) || + checkVec3Value('num_groups', kNumGroupsOffset, t.params.numGroups) || + expectEqual( + 'local_index', + localIndex, + output[globalOffset + kLocalIndexOffset] ); + if (error) { + return error; } } } @@ -254,44 +251,8 @@ g.test('inputs') return undefined; }; - // Check @builtin(local_invocation_index) values. - t.expectGPUBufferValuesEqual( - localIndexBuffer, - new Uint32Array([...iterRange(totalInvocations, x => x % invocationsPerGroup)]) - ); - - // Check @builtin(local_invocation_id) values. - t.expectGPUBufferValuesPassCheck( - localIdBuffer, - outputData => checkEachIndex(outputData, 'local_invocation_id', (_, localId) => localId), - { type: Uint32Array, typedLength: totalInvocations * 4 } - ); - - // Check @builtin(global_invocation_id) values. - const getGlobalId = (groupId: vec3, localId: vec3) => { - return { - x: groupId.x * t.params.groupSize.x + localId.x, - y: groupId.y * t.params.groupSize.y + localId.y, - z: groupId.z * t.params.groupSize.z + localId.z, - }; - }; - t.expectGPUBufferValuesPassCheck( - globalIdBuffer, - outputData => checkEachIndex(outputData, 'global_invocation_id', getGlobalId), - { type: Uint32Array, typedLength: totalInvocations * 4 } - ); - - // Check @builtin(workgroup_id) values. - t.expectGPUBufferValuesPassCheck( - groupIdBuffer, - outputData => checkEachIndex(outputData, 'workgroup_id', (groupId, _) => groupId), - { type: Uint32Array, typedLength: totalInvocations * 4 } - ); - - // Check @builtin(num_workgroups) values. - t.expectGPUBufferValuesPassCheck( - numGroupsBuffer, - outputData => checkEachIndex(outputData, 'num_workgroups', () => t.params.numGroups), - { type: Uint32Array, typedLength: totalInvocations * 4 } - ); + t.expectGPUBufferValuesPassCheck(outputBuffer, outputData => checkEachIndex(outputData), { + type: Uint32Array, + typedLength: outputBuffer.size / 4, + }); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/fragment_builtins.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/fragment_builtins.spec.ts new file mode 100644 index 0000000000..2bb03dab8d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/fragment_builtins.spec.ts @@ -0,0 +1,1410 @@ +export const description = `Test fragment shader builtin variables and inter-stage variables + +* test builtin(position) +* test @interpolate +* test builtin(sample_index) +* test builtin(front_facing) +* test builtin(sample_mask) + +Note: @interpolate settings and sample_index affect whether or not the fragment shader +is evaluated per-fragment or per-sample. With @interpolate(, sample) or usage of +@builtin(sample_index) the fragment shader should be executed per-sample. + +* sample_mask output is tested in + src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts + +* frag_depth output is tested in + src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { ErrorWithExtra, assert, range, unreachable } from '../../../../common/util/util.js'; +import { InterpolationSampling, InterpolationType } from '../../../constants.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { getMultisampleFragmentOffsets } from '../../../multisample_info.js'; +import { dotProduct, subtractVectors } from '../../../util/math.js'; +import { TexelView } from '../../../util/texture/texel_view.js'; +import { findFailedPixels } from '../../../util/texture/texture_ok.js'; + +export const g = makeTestGroup(GPUTest); + +const s_deviceToPipelineMap = new WeakMap< + GPUDevice, + { + texture_2d?: GPUComputePipeline; + texture_multisampled_2d?: GPUComputePipeline; + } +>(); + +/** + * Returns an object of pipelines associated + * by weakmap to a device so we can cache pipelines. + */ +function getPipelinesForDevice(device: GPUDevice) { + let pipelines = s_deviceToPipelineMap.get(device); + if (!pipelines) { + pipelines = {}; + s_deviceToPipelineMap.set(device, pipelines); + } + return pipelines; +} + +/** + * Gets a compute pipeline that will copy the given texture if passed + * a dispatch size of texture.width, texture.height + * @param device a device + * @param texture texture the pipeline is needed for. + * @returns A GPUComputePipeline + */ +function getCopyMultisamplePipelineForDevice(device: GPUDevice, textures: GPUTexture[]) { + assert(textures.length === 4); + assert(textures[0].sampleCount === textures[1].sampleCount); + assert(textures[0].sampleCount === textures[2].sampleCount); + assert(textures[0].sampleCount === textures[3].sampleCount); + + const pipelineType = textures[0].sampleCount > 1 ? 'texture_multisampled_2d' : 'texture_2d'; + const pipelines = getPipelinesForDevice(device); + let pipeline = pipelines[pipelineType]; + if (!pipeline) { + const isMultisampled = pipelineType === 'texture_multisampled_2d'; + const numSamples = isMultisampled ? 'textureNumSamples(texture0)' : '1u'; + const sampleIndex = isMultisampled ? 'sampleIndex' : '0'; + const module = device.createShaderModule({ + code: ` + @group(0) @binding(0) var texture0: ${pipelineType}<f32>; + @group(0) @binding(1) var texture1: ${pipelineType}<f32>; + @group(0) @binding(2) var texture2: ${pipelineType}<f32>; + @group(0) @binding(3) var texture3: ${pipelineType}<f32>; + @group(0) @binding(4) var<storage, read_write> buffer: array<f32>; + + @compute @workgroup_size(1) fn cs(@builtin(global_invocation_id) id: vec3u) { + let numSamples = ${numSamples}; + let dimensions = textureDimensions(texture0); + let sampleIndex = id.x % numSamples; + let tx = id.x / numSamples; + let offset = ((id.y * dimensions.x + tx) * numSamples + sampleIndex) * 4; + let r = vec4u(textureLoad(texture0, vec2u(tx, id.y), ${sampleIndex}) * 255.0); + let g = vec4u(textureLoad(texture1, vec2u(tx, id.y), ${sampleIndex}) * 255.0); + let b = vec4u(textureLoad(texture2, vec2u(tx, id.y), ${sampleIndex}) * 255.0); + let a = vec4u(textureLoad(texture3, vec2u(tx, id.y), ${sampleIndex}) * 255.0); + + // expand rgba8unorm values back to their byte form, add them together + // and cast them to an f32 so we can recover the f32 values we encoded + // in the rgba8unorm texture. + buffer[offset + 0] = bitcast<f32>(dot(r, vec4u(0x1000000, 0x10000, 0x100, 0x1))); + buffer[offset + 1] = bitcast<f32>(dot(g, vec4u(0x1000000, 0x10000, 0x100, 0x1))); + buffer[offset + 2] = bitcast<f32>(dot(b, vec4u(0x1000000, 0x10000, 0x100, 0x1))); + buffer[offset + 3] = bitcast<f32>(dot(a, vec4u(0x1000000, 0x10000, 0x100, 0x1))); + } + `, + }); + + pipeline = device.createComputePipeline({ + label: 'copy multisampled texture pipeline', + layout: 'auto', + compute: { + module, + entryPoint: 'cs', + }, + }); + + pipelines[pipelineType] = pipeline; + } + return pipeline; +} + +function isTextureSameDimensions(a: GPUTexture, b: GPUTexture) { + return ( + a.sampleCount === b.sampleCount && + a.width === b.width && + a.height === b.height && + a.depthOrArrayLayers === b.depthOrArrayLayers + ); +} + +/** + * Copies a texture (even if multisampled) to a buffer + * @param t a gpu test + * @param texture texture to copy + * @returns buffer with copy of texture, mip level 0, array layer 0. + */ +function copyRGBA8EncodedFloatTexturesToBufferIncludingMultisampledTextures( + t: GPUTest, + textures: GPUTexture[] +) { + assert(textures.length === 4); + assert(isTextureSameDimensions(textures[0], textures[1])); + assert(isTextureSameDimensions(textures[0], textures[2])); + assert(isTextureSameDimensions(textures[0], textures[3])); + const { width, height, sampleCount } = textures[0]; + + const copyBuffer = t.device.createBuffer({ + size: width * height * sampleCount * 4 * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + t.trackForCleanup(copyBuffer); + + const buffer = t.device.createBuffer({ + size: copyBuffer.size, + usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, + }); + t.trackForCleanup(buffer); + + const pipeline = getCopyMultisamplePipelineForDevice(t.device, textures); + const encoder = t.device.createCommandEncoder(); + + const textureEntries = textures.map( + (texture, i) => ({ binding: i, resource: texture.createView() }) as GPUBindGroupEntry + ); + + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [...textureEntries, { binding: 4, resource: { buffer: copyBuffer } }], + }); + + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(width * sampleCount, height); + pass.end(); + + encoder.copyBufferToBuffer(copyBuffer, 0, buffer, 0, buffer.size); + + t.device.queue.submit([encoder.finish()]); + + return buffer; +} + +/* column constants */ +const kX = 0; +const kY = 1; +const kZ = 2; +const kW = 3; + +/** + * Gets a column of values from an array of arrays. + */ +function getColumn(values: readonly number[][], colNum: number) { + return values.map(v => v[colNum]); +} + +/** + * Computes the linear interpolation of 3 values from 3 vertices of a triangle + * based on barycentric coordinates + */ +function linearInterpolation(baryCoords: readonly number[], interCoords: readonly number[]) { + return dotProduct(baryCoords, interCoords); +} + +/** + * Computes the perspective interpolation of 3 values from 3 vertices of a + * triangle based on barycentric coordinates and their corresponding clip space + * W coordinates. + */ +function perspectiveInterpolation( + barycentricCoords: readonly number[], + clipSpaceTriangleCoords: readonly number[][], + interCoords: readonly number[] +) { + const [a, b, c] = barycentricCoords; + const [fa, fb, fc] = interCoords; + const wa = clipSpaceTriangleCoords[0][kW]; + const wb = clipSpaceTriangleCoords[1][kW]; + const wc = clipSpaceTriangleCoords[2][kW]; + + return ((a * fa) / wa + (b * fb) / wb + (c * fc) / wc) / (a / wa + b / wb + c / wc); +} + +/** + * Converts clip space coords to NDC coords + */ +function clipSpaceToNDC(point: readonly number[]) { + return point.map(v => v / point[kW]); +} + +/** + * Converts NDC coords to window coords. + */ +function ndcToWindow(ndcPoint: readonly number[], viewport: readonly number[]) { + const [xd, yd, zd] = ndcPoint; + const px = viewport[2]; + const py = viewport[3]; + const ox = viewport[0] + px / 2; + const oy = viewport[1] + py / 2; + const zNear = viewport[4]; + const zFar = viewport[5]; + // prettier-ignore + return [ + px / 2 * xd + ox, + -py / 2 * yd + oy, + zd * (zFar - zNear) + zNear, + ]; +} + +/** + * Computes barycentric coordinates of triangle for point p. + * @param trianglePoints points for triangle + * @param p point in triangle (or relative to it) + * @returns barycentric coords of p + */ +function calcBarycentricCoordinates(trianglePoints: number[][], p: number[]) { + const [a, b, c] = trianglePoints; + + const v0 = subtractVectors(b, a); + const v1 = subtractVectors(c, a); + const v2 = subtractVectors(p, a); + + const dot00 = dotProduct(v0, v0); + const dot01 = dotProduct(v0, v1); + const dot11 = dotProduct(v1, v1); + const dot20 = dotProduct(v2, v0); + const dot21 = dotProduct(v2, v1); + + const denom = 1 / (dot00 * dot11 - dot01 * dot01); + const v = (dot11 * dot20 - dot01 * dot21) * denom; + const w = (dot00 * dot21 - dot01 * dot20) * denom; + const u = 1 - v - w; + + return [u, v, w]; +} + +/** + * Returns true if point is inside triangle + */ +function isInsideTriangle(barycentricCoords: number[]) { + for (const v of barycentricCoords) { + if (v < 0 || v > 1) { + return false; + } + } + return true; +} + +/** + * Returns true if windowPoints define a clockwise triangle + */ +function isTriangleClockwise(windowPoints: readonly number[][]) { + let sum = 0; + for (let i = 0; i < 3; ++i) { + const p0 = windowPoints[i]; + const p1 = windowPoints[(i + 1) % 3]; + sum += p0[kX] * p1[kY] - p1[kX] * p0[kY]; + } + return sum >= 0; +} + +type FragData = { + baseVertexIndex: number; + fragmentPoint: readonly number[]; + fragmentBarycentricCoords: readonly number[]; + sampleBarycentricCoords: readonly number[]; + clipSpacePoints: readonly number[][]; + ndcPoints: readonly number[][]; + windowPoints: readonly number[][]; + sampleIndex: number; + sampleMask: number; + frontFacing: boolean; +}; + +/** + * For each sample in texture, computes the values that would be provided + * to the shader as `@builtin(position)` if the texture was a render target + * and every point in the texture was inside the triangle. + * @param texture The texture + * @param clipSpacePoints triangle points in clip space + * @returns the expected values for each sample + */ +function generateFragmentInputs({ + width, + height, + nearFar, + sampleCount, + frontFace, + clipSpacePoints, + interpolateFn, +}: { + width: number; + height: number; + nearFar: readonly number[]; + sampleCount: number; + frontFace?: GPUFrontFace; + clipSpacePoints: readonly number[][]; + interpolateFn: (fragData: FragData) => number[]; +}) { + const expected = new Float32Array(width * height * sampleCount * 4); + + const viewport = [0, 0, width, height, ...nearFar]; + + // For each triangle + for (let vertexIndex = 0; vertexIndex < clipSpacePoints.length; vertexIndex += 3) { + const ndcPoints = clipSpacePoints.slice(vertexIndex, vertexIndex + 3).map(clipSpaceToNDC); + const windowPoints = ndcPoints.map(p => ndcToWindow(p, viewport)); + const windowPoints2D = windowPoints.map(p => p.slice(0, 2)); + + const cw = isTriangleClockwise(windowPoints2D); + const frontFacing = frontFace === 'cw' ? cw : !cw; + const fragmentOffsets = getMultisampleFragmentOffsets(sampleCount)!; + + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + let sampleMask = 0; + for (let sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) { + const localSampleMask = 1 << sampleIndex; + const multisampleOffset = fragmentOffsets[sampleIndex]; + const sampleFragmentPoint = [x + multisampleOffset[0], y + multisampleOffset[1]]; + const sampleBarycentricCoords = calcBarycentricCoordinates( + windowPoints2D, + sampleFragmentPoint + ); + + const inside = isInsideTriangle(sampleBarycentricCoords); + if (inside) { + sampleMask |= localSampleMask; + } + } + + for (let sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) { + const fragmentPoint = [x + 0.5, y + 0.5]; + const multisampleOffset = fragmentOffsets[sampleIndex]; + const sampleFragmentPoint = [x + multisampleOffset[0], y + multisampleOffset[1]]; + const fragmentBarycentricCoords = calcBarycentricCoordinates( + windowPoints2D, + fragmentPoint + ); + const sampleBarycentricCoords = calcBarycentricCoordinates( + windowPoints2D, + sampleFragmentPoint + ); + + const inside = isInsideTriangle(sampleBarycentricCoords); + if (inside) { + const output = interpolateFn({ + baseVertexIndex: vertexIndex, + fragmentPoint, + fragmentBarycentricCoords, + sampleBarycentricCoords, + clipSpacePoints, + ndcPoints, + windowPoints, + sampleIndex, + sampleMask, + frontFacing, + }); + + const offset = ((y * width + x) * sampleCount + sampleIndex) * 4; + expected.set(output, offset); + } + } + } + } + } + return expected; +} + +/** + * Computes 'builtin(position)` + */ +function computeFragmentPosition({ + fragmentPoint, + fragmentBarycentricCoords, + clipSpacePoints, + windowPoints, +}: FragData) { + return [ + fragmentPoint[0], + fragmentPoint[1], + linearInterpolation(fragmentBarycentricCoords, getColumn(windowPoints, kZ)), + 1 / + perspectiveInterpolation( + fragmentBarycentricCoords, + clipSpacePoints, + getColumn(clipSpacePoints, kW) + ), + ]; +} + +/** + * Creates a function that will compute the interpolation of an inter-stage variable. + */ +function createInterStageInterpolationFn( + interStagePoints: number[][], + type: InterpolationType, + sampling: InterpolationSampling | undefined +) { + return function ({ + baseVertexIndex, + fragmentBarycentricCoords, + sampleBarycentricCoords, + clipSpacePoints, + }: FragData) { + const triangleInterStagePoints = interStagePoints.slice(baseVertexIndex, baseVertexIndex + 3); + const barycentricCoords = + sampling === 'center' ? fragmentBarycentricCoords : sampleBarycentricCoords; + switch (type) { + case 'perspective': + return triangleInterStagePoints[0].map((_, colNum: number) => + perspectiveInterpolation( + barycentricCoords, + clipSpacePoints, + getColumn(triangleInterStagePoints, colNum) + ) + ); + break; + case 'linear': + return triangleInterStagePoints[0].map((_, colNum: number) => + linearInterpolation(barycentricCoords, getColumn(triangleInterStagePoints, colNum)) + ); + break; + case 'flat': + return triangleInterStagePoints[0]; + break; + default: + unreachable(); + } + }; +} + +/** + * Creates a function that will compute the interpolation of an inter-stage variable + * and then return [1, 0, 0, 0] if all interpolated values are between 0.0 and 1.0 inclusive + * or [-1, 0, 0, 0] otherwise. + */ +function createInterStageInterpolationBetween0And1TestFn( + interStagePoints: number[][], + type: InterpolationType, + sampling: InterpolationSampling | undefined +) { + const interpolateFn = createInterStageInterpolationFn(interStagePoints, type, sampling); + return function (fragData: FragData) { + const interpolatedValues = interpolateFn(fragData); + const allTrue = interpolatedValues.reduce((all, v) => all && v >= 0 && v <= 1, true); + return [allTrue ? 1 : -1, 0, 0, 0]; + }; +} + +/** + * Computes 'builtin(sample_index)' + */ +function computeFragmentSampleIndex({ sampleIndex }: FragData) { + return [sampleIndex, 0, 0, 0]; +} + +/** + * Computes 'builtin(front_facing)' + */ +function computeFragmentFrontFacing({ frontFacing }: FragData) { + return [frontFacing ? 1 : 0, 0, 0, 0]; +} + +/** + * Computes 'builtin(sample_mask)' + */ +function computeSampleMask({ sampleMask }: FragData) { + return [sampleMask, 0, 0, 0]; +} + +/** + * Renders float32 fragment shader inputs values to 4 rgba8unorm textures that + * can be multisampled textures. It stores each of the channels, r, g, b, a of + * the shader input to a separate texture, doing the math required to store the + * float32 value into an rgba8unorm texel. + * + * Note: We could try to store the output to an vec4f storage buffer. + * Unfortunately, using a storage buffer has the issue that we need to compute + * an index with the very thing we're trying to test. Similarly, if we used a + * storage texture we would need to compute texture locations with the things + * we're trying to test. Also, using a storage buffer seems to affect certain + * backends like M1 Mac so it seems better to stick to rgba8unorm here and test + * using a storage buffer in a fragment shader separately. + * + * We can't use rgba32float because it's optional. We can't use rgba16float + * because it's optional in compat. We can't we use rgba32uint as that can't be + * multisampled. + */ +async function renderFragmentShaderInputsTo4TexturesAndReadbackValues( + t: GPUTest, + { + interpolationType, + interpolationSampling, + sampleCount, + width, + height, + nearFar, + frontFace, + clipSpacePoints, + interStagePoints, + fragInCode, + outputCode, + }: { + interpolationType: InterpolationType; + interpolationSampling?: InterpolationSampling; + width: number; + height: number; + sampleCount: number; + frontFace?: GPUFrontFace; + nearFar: readonly number[]; + clipSpacePoints: readonly number[][]; + interStagePoints: readonly number[][]; + fragInCode: string; + outputCode: string; + } +) { + const interpolate = `${interpolationType}${ + interpolationSampling ? `, ${interpolationSampling}` : '' + }`; + const module = t.device.createShaderModule({ + code: ` + struct Uniforms { + resolution: vec2f, + }; + + @group(0) @binding(0) var<uniform> uni: Uniforms; + + struct VertexOut { + @builtin(position) position: vec4f, + @location(0) @interpolate(${interpolate}) interpolatedValue: vec4f, + }; + + @vertex fn vs(@builtin(vertex_index) vNdx: u32) -> VertexOut { + let pos = array( + ${clipSpacePoints.map(p => `vec4f(${p.join(', ')})`).join(', ')} + ); + let interStage = array( + ${interStagePoints.map(p => `vec4f(${p.join(', ')})`).join(', ')} + ); + var v: VertexOut; + v.position = pos[vNdx]; + v.interpolatedValue = interStage[vNdx]; + _ = uni; + return v; + } + + struct FragmentIn { + @builtin(position) position: vec4f, + @location(0) @interpolate(${interpolate}) interpolatedValue: vec4f, + ${fragInCode} + }; + + struct FragOut { + @location(0) out0: vec4f, + @location(1) out1: vec4f, + @location(2) out2: vec4f, + @location(3) out3: vec4f, + }; + + fn u32ToRGBAUnorm(u: u32) -> vec4f { + return vec4f( + f32((u >> 24) & 0xFF) / 255.0, + f32((u >> 16) & 0xFF) / 255.0, + f32((u >> 8) & 0xFF) / 255.0, + f32((u >> 0) & 0xFF) / 255.0, + ); + } + + @fragment fn fs(fin: FragmentIn) -> FragOut { + var f: FragOut; + let v = ${outputCode}; + let u = bitcast<vec4u>(v); + f.out0 = u32ToRGBAUnorm(u[0]); + f.out1 = u32ToRGBAUnorm(u[1]); + f.out2 = u32ToRGBAUnorm(u[2]); + f.out3 = u32ToRGBAUnorm(u[3]); + _ = fin.interpolatedValue; + return f; + } + `, + }); + + const textures = range(4, () => { + const texture = t.device.createTexture({ + size: [width, height], + usage: + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_SRC, + format: 'rgba8unorm', + sampleCount, + }); + t.trackForCleanup(texture); + return texture; + }); + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + }, + fragment: { + module, + entryPoint: 'fs', + targets: textures.map(() => ({ format: 'rgba8unorm' })), + }, + ...(frontFace && { + primitive: { + frontFace, + }, + }), + multisample: { + count: sampleCount, + }, + }); + + const uniformBuffer = t.device.createBuffer({ + size: 8, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + t.trackForCleanup(uniformBuffer); + t.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([width, height])); + + const viewport = [0, 0, width, height, ...nearFar] as const; + + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [{ binding: 0, resource: { buffer: uniformBuffer } }], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: textures.map(texture => ({ + view: texture.createView(), + loadOp: 'clear', + storeOp: 'store', + })), + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.setViewport(viewport[0], viewport[1], viewport[2], viewport[3], viewport[4], viewport[5]); + pass.draw(clipSpacePoints.length); + pass.end(); + t.queue.submit([encoder.finish()]); + + const buffer = copyRGBA8EncodedFloatTexturesToBufferIncludingMultisampledTextures(t, textures); + await buffer.mapAsync(GPUMapMode.READ); + return new Float32Array(buffer.getMappedRange()); +} + +function checkSampleRectsApproximatelyEqual({ + width, + height, + sampleCount, + actual, + expected, + maxDiffULPsForFloatFormat, +}: { + width: number; + height: number; + sampleCount: number; + actual: Float32Array; + expected: Float32Array; + maxDiffULPsForFloatFormat: number; +}) { + const subrectOrigin = [0, 0, 0]; + const subrectSize = [width * sampleCount, height, 1]; + const areaDesc = { + bytesPerRow: width * sampleCount * 4 * 4, + rowsPerImage: height, + subrectOrigin, + subrectSize, + }; + + const format = 'rgba32float'; + const actTexelView = TexelView.fromTextureDataByReference( + format, + new Uint8Array(actual.buffer), + areaDesc + ); + const expTexelView = TexelView.fromTextureDataByReference( + format, + new Uint8Array(expected.buffer), + areaDesc + ); + + const failedPixelsMessage = findFailedPixels( + format, + { x: 0, y: 0, z: 0 }, + { width: width * sampleCount, height, depthOrArrayLayers: 1 }, + { actTexelView, expTexelView }, + { maxDiffULPsForFloatFormat } + ); + + if (failedPixelsMessage !== undefined) { + const msg = 'Texture level had unexpected contents:\n' + failedPixelsMessage; + return new ErrorWithExtra(msg, () => ({ + expTexelView, + actTexelView, + })); + } + + return undefined; +} + +g.test('inputs,position') + .desc( + ` + Test fragment shader builtin(position) values. + + Note: @builtin(position) is always a fragment position, never a sample position. + ` + ) + .params(u => + u // + .combine('nearFar', [[0, 1] as const, [0.25, 0.75] as const] as const) + .combine('sampleCount', [1, 4] as const) + .combine('interpolation', [ + { type: 'perspective', sampling: 'center' }, + { type: 'perspective', sampling: 'centroid' }, + { type: 'perspective', sampling: 'sample' }, + { type: 'linear', sampling: 'center' }, + { type: 'linear', sampling: 'centroid' }, + { type: 'linear', sampling: 'sample' }, + { type: 'flat' }, + ] as const) + ) + .beforeAllSubcases(t => { + const { + interpolation: { type, sampling }, + } = t.params; + t.skipIfInterpolationTypeOrSamplingNotSupported({ type, sampling }); + }) + .fn(async t => { + const { + nearFar, + sampleCount, + interpolation: { type, sampling }, + } = t.params; + // prettier-ignore + const clipSpacePoints = [ // ndc values + [0.333, 0.333, 0.333, 0.333], // 1, 1, 1 + [ 1.0, -3.0, 0.25, 1.0 ], // 1, -3, 0.25 + [-1.5, 0.5, 0.25, 0.5 ], // -3, 1, 0.5 + ]; + + const interStagePoints = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; + + const width = 4; + const height = 4; + const actual = await renderFragmentShaderInputsTo4TexturesAndReadbackValues(t, { + interpolationType: type, + interpolationSampling: sampling, + sampleCount, + width, + height, + nearFar, + clipSpacePoints, + interStagePoints, + fragInCode: '', + outputCode: 'fin.position', + }); + + const expected = generateFragmentInputs({ + width, + height, + nearFar, + sampleCount, + clipSpacePoints, + interpolateFn: computeFragmentPosition, + }); + + // Since @builtin(position) is always a fragment position, never a sample position, check + // the first coordinate. It should be 0.5, 0.5 always. This is just to double check + // that computeFragmentPosition is generating the correct values. + assert(expected[0] === 0.5); + assert(expected[1] === 0.5); + + t.expectOK( + checkSampleRectsApproximatelyEqual({ + width, + height, + sampleCount, + actual, + expected, + maxDiffULPsForFloatFormat: 4, + }) + ); + }); + +g.test('inputs,interStage') + .desc( + ` + Test fragment shader inter-stage variable values except for centroid interpolation. + ` + ) + .params(u => + u // + .combine('nearFar', [[0, 1] as const, [0.25, 0.75] as const] as const) + .combine('sampleCount', [1, 4] as const) + .combine('interpolation', [ + { type: 'perspective', sampling: 'center' }, + { type: 'perspective', sampling: 'sample' }, + { type: 'linear', sampling: 'center' }, + { type: 'linear', sampling: 'sample' }, + { type: 'flat' }, + ] as const) + ) + .beforeAllSubcases(t => { + const { + interpolation: { type, sampling }, + } = t.params; + t.skipIfInterpolationTypeOrSamplingNotSupported({ type, sampling }); + }) + .fn(async t => { + const { + nearFar, + sampleCount, + interpolation: { type, sampling }, + } = t.params; + // prettier-ignore + const clipSpacePoints = [ // ndc values + [0.333, 0.333, 0.333, 0.333], // 1, 1, 1 + [ 1.0, -3.0, 0.25, 1.0 ], // 1, -3, 0.25 + [-1.5, 0.5, 0.25, 0.5 ], // -3, 1, 0.5 + ]; + + const interStagePoints = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; + + const width = 4; + const height = 4; + const actual = await renderFragmentShaderInputsTo4TexturesAndReadbackValues(t, { + interpolationType: type, + interpolationSampling: sampling, + sampleCount, + width, + height, + nearFar, + clipSpacePoints, + interStagePoints, + fragInCode: '', + outputCode: 'fin.interpolatedValue', + }); + + const expected = generateFragmentInputs({ + width, + height, + nearFar, + sampleCount, + clipSpacePoints, + interpolateFn: createInterStageInterpolationFn(interStagePoints, type, sampling), + }); + + t.expectOK( + checkSampleRectsApproximatelyEqual({ + width, + height, + sampleCount, + actual, + expected, + maxDiffULPsForFloatFormat: 4, + }) + ); + }); + +g.test('inputs,interStage,centroid') + .desc( + ` + Test fragment shader inter-stage variable values in centroid sampling mode. + + Centroid sampling mode is trying to solve the following issue + + +-------------+ + |....s1|/ | + |......| | + |...../| s2 | + +------C------+ + |s3./ | | + |../ | | + |./ |s4 | + +-------------+ + + Above is a diagram of a texel where s1, s2, s3, s4 are sample points, + C is the center of the texel and the diagonal line is some edge of + a triangle. s1 and s3 are inside the triangle. In sampling = 'center' + modes, the interpolated value will be relative to C. The problem is, + C is outside of the triangle. In sample = 'centroid' mode, the + interpolated value will be computed relative to some point inside the + portion of the triangle inside the texel. While ideally it would be + the actual centroid, the specs from the various APIs suggest the only + guarantee is it's inside the triangle. + + So, we set the interStage values to barycentric coords. We expect + that when sampling mode is 'center', some interpolated values + will be outside of the triangle (ie, one or more of their values will + be outside the 0 to 1 range). In sampling mode = 'centroid' mode, none + of the values will be outside of the 0 to 1 range. + + Note: generateFragmentInputs below generates "expected". Values not + rendered to will be 0. Values rendered to outside the triangle will + be -1. Values rendered to inside the triangle will be 1. Manually + checking, "expected" for sampling = 'center' should have a couple of + -1 values where as "expected" for sampling = 'centroid' should not. + This was verified with manual testing. + + Since we only care about inside vs outside of the triangle, having + createInterStageInterpolationFn use the interpolated value relative + to the sample point when sampling = 'centroid' will give us a value + inside the triangle, which is good enough for our test. + ` + ) + .params(u => + u // + .combine('nearFar', [[0, 1] as const, [0.25, 0.75] as const] as const) + .combine('sampleCount', [1, 4] as const) + .combine('interpolation', [ + { type: 'perspective', sampling: 'center' }, + { type: 'perspective', sampling: 'centroid' }, + { type: 'linear', sampling: 'center' }, + { type: 'linear', sampling: 'centroid' }, + ] as const) + ) + .beforeAllSubcases(t => { + const { + interpolation: { type, sampling }, + } = t.params; + t.skipIfInterpolationTypeOrSamplingNotSupported({ type, sampling }); + }) + .fn(async t => { + const { + nearFar, + sampleCount, + interpolation: { type, sampling }, + } = t.params; + // + // We're drawing 1 triangle that cut the viewport + // + // -1 0 1 + // +===+===+ 2 + // |\..|...| + // +---+---+ 1 <--- + // | \|...| | + // +---+---+ 0 | viewport + // | |\..| | + // +---+---+ -1 <--- + // | | \| + // +===+===+ -2 + + // prettier-ignore + const clipSpacePoints = [ // ndc values + [ 1, -2, 0, 1], + [-1, 2, 0, 1], + [ 1, 2, 0, 1], + ]; + + // prettier-ignore + const interStagePoints = [ + [ 1, 0, 0, 0], + [ 0, 1, 0, 0], + [ 0, 0, 1, 0], + ]; + + const width = 4; + const height = 4; + const actual = await renderFragmentShaderInputsTo4TexturesAndReadbackValues(t, { + interpolationType: type, + interpolationSampling: sampling, + sampleCount, + width, + height, + nearFar, + clipSpacePoints, + interStagePoints, + fragInCode: '', + outputCode: + 'vec4f(select(-1.0, 1.0, all(fin.interpolatedValue >= vec4f(0)) && all(fin.interpolatedValue <= vec4f(1))), 0, 0, 0)', + }); + + const expected = generateFragmentInputs({ + width, + height, + nearFar, + sampleCount, + clipSpacePoints, + interpolateFn: createInterStageInterpolationBetween0And1TestFn( + interStagePoints, + type, + sampling + ), + }); + + t.expectOK( + checkSampleRectsApproximatelyEqual({ + width, + height, + sampleCount, + actual, + expected, + maxDiffULPsForFloatFormat: 3, + }) + ); + }); + +g.test('inputs,sample_index') + .desc( + ` + Test fragment shader builtin(sample_index) values. + ` + ) + .params(u => + u // + .combine('nearFar', [[0, 1] as const, [0.25, 0.75] as const] as const) + .combine('sampleCount', [1, 4] as const) + .combine('interpolation', [ + { type: 'perspective', sampling: 'center' }, + { type: 'perspective', sampling: 'centroid' }, + { type: 'perspective', sampling: 'sample' }, + { type: 'linear', sampling: 'center' }, + { type: 'linear', sampling: 'centroid' }, + { type: 'linear', sampling: 'sample' }, + { type: 'flat' }, + ] as const) + ) + .beforeAllSubcases(t => { + t.skipIf(t.isCompatibility, 'sample_index is not supported in compatibility mode'); + }) + .fn(async t => { + const { + nearFar, + sampleCount, + interpolation: { type, sampling }, + } = t.params; + // prettier-ignore + const clipSpacePoints = [ // ndc values + [0.333, 0.333, 0.333, 0.333], // 1, 1, 1 + [ 1.0, -3.0, 0.25, 1.0 ], // 1, -3, 0.25 + [-1.5, 0.5, 0.25, 0.5 ], // -3, 1, 0.5 + ]; + + const interStagePoints = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; + + const width = 4; + const height = 4; + const actual = await renderFragmentShaderInputsTo4TexturesAndReadbackValues(t, { + interpolationType: type, + interpolationSampling: sampling, + sampleCount, + width, + height, + nearFar, + clipSpacePoints, + interStagePoints, + fragInCode: `@builtin(sample_index) sampleIndex: u32,`, + outputCode: 'vec4f(f32(fin.sampleIndex), 0, 0, 0)', + }); + + const expected = generateFragmentInputs({ + width, + height, + nearFar, + sampleCount, + clipSpacePoints, + interpolateFn: computeFragmentSampleIndex, + }); + + t.expectOK( + checkSampleRectsApproximatelyEqual({ + width, + height, + sampleCount, + actual, + expected, + maxDiffULPsForFloatFormat: 1, + }) + ); + }); + +g.test('inputs,front_facing') + .desc( + ` + Test fragment shader builtin(front_facing) values. + + Draws a quad from 2 triangles that entirely cover clip space. (see diagram below in code) + One triangle is clockwise, the other is counter clockwise. The triangles + bisect pixels so that different samples are covered by each triangle so that some + samples should get different values for front_facing for the same fragment. + ` + ) + .params(u => + u // + .combine('nearFar', [[0, 1] as const, [0.25, 0.75] as const] as const) + .combine('sampleCount', [1, 4] as const) + .combine('frontFace', ['cw', 'ccw'] as const) + .combine('interpolation', [ + { type: 'perspective', sampling: 'center' }, + { type: 'perspective', sampling: 'centroid' }, + { type: 'perspective', sampling: 'sample' }, + { type: 'linear', sampling: 'center' }, + { type: 'linear', sampling: 'centroid' }, + { type: 'linear', sampling: 'sample' }, + { type: 'flat' }, + ] as const) + ) + .beforeAllSubcases(t => { + const { + interpolation: { type, sampling }, + } = t.params; + t.skipIfInterpolationTypeOrSamplingNotSupported({ type, sampling }); + }) + .fn(async t => { + const { + nearFar, + sampleCount, + frontFace, + interpolation: { type, sampling }, + } = t.params; + // + // We're drawing 2 triangles starting at y = -2 to y = +2 + // + // -1 0 1 + // +===+===+ 2 + // |\ | | + // +---+---+ 1 <--- + // | \| | | + // +---+---+ 0 | viewport + // | |\ | | + // +---+---+ -1 <--- + // | | \| + // +===+===+ -2 + + // prettier-ignore + const clipSpacePoints = [ + // ccw + [-1, -2, 0, 1], + [ 1, -2, 0, 1], + [-1, 2, 0, 1], + + // cw + [ 1, -2, 0, 1], + [-1, 2, 0, 1], + [ 1, 2, 0, 1], + ]; + + const interStagePoints = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + + [13, 14, 15, 16], + [17, 18, 19, 20], + [21, 22, 23, 24], + ]; + + const width = 4; + const height = 4; + const actual = await renderFragmentShaderInputsTo4TexturesAndReadbackValues(t, { + interpolationType: type, + interpolationSampling: sampling, + frontFace, + sampleCount, + width, + height, + nearFar, + clipSpacePoints, + interStagePoints, + fragInCode: '@builtin(front_facing) frontFacing: bool,', + outputCode: 'vec4f(select(0.0, 1.0, fin.frontFacing), 0, 0, 0)', + }); + + const expected = generateFragmentInputs({ + width, + height, + nearFar, + sampleCount, + clipSpacePoints, + frontFace, + interpolateFn: computeFragmentFrontFacing, + }); + + assert(expected.indexOf(0) >= 0, 'expect some values to be 0'); + assert(expected.findIndex(v => v !== 0) >= 0, 'expect some values to be non 0'); + + t.expectOK( + checkSampleRectsApproximatelyEqual({ + width, + height, + sampleCount, + actual, + expected, + maxDiffULPsForFloatFormat: 0, + }) + ); + }); + +g.test('inputs,sample_mask') + .desc( + ` + Test fragment shader builtin(sample_mask) values. + + Draws various triangles that should trigger different sample_mask values. + Checks that sample_mask matches what's expected. Note: the triangles + are selected so they do not intersect sample points as we don't want + to test precision issues on whether or not a sample point is inside + or outside the triangle when right on the edge. + + Example: x=-1, y=2, it draws the following triangle + + [ -0.8, -2 ] + [ 1.2, 2 ] + [ -0.8, 2 ] + + On to a 4x4 pixel texture + + -0.8, 2 + .----------------------. 1.2 2 + |...................../ + |..................../ + |.................../ + |................../ + |................./ + +-|---+-----+-----+/----+ --- + | |...|.....|...../ | ^ + | |...|.....|..../| | | + +-|---+-----+---/-+-----+ | + | |...|.....|../ | | | + | |...|.....|./ | | | + +-|---+-----+/----+-----+ texture / clip space + | |...|...../ | | | + | |...|..../| | | | + +-|---+---/-+-----+-----+ | + | |...|../ | | | | + | |...|./ | | | V + +-|---+/----+-----+-----+ --- + |.../ + |../ + |./ + |/ + / + . + -0.8, -2 + + Inside an individual pixel you might see this situation + + +-------------+ + |....s1|/ | + |......| | + |...../| s2 | + +------C------+ + |s3./ | | + |../ | | + |./ |s4 | + +-------------+ + + where s1, s2, s3, s4, are sample points and C is the center. For a sampleCount = 4 texture + the sample_mask is expected to emit sample_mask = 0b0101 + + ref: https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels + ` + ) + .params(u => + u // + .combine('nearFar', [[0, 1] as const, [0.25, 0.75] as const] as const) + .combine('sampleCount', [1, 4] as const) + .combine('interpolation', [ + // given that 'sample' effects whether things are run per-sample or per-fragment + // we test all of these to make sure they don't affect the result differently than expected. + { type: 'perspective', sampling: 'center' }, + { type: 'perspective', sampling: 'centroid' }, + { type: 'perspective', sampling: 'sample' }, + { type: 'linear', sampling: 'center' }, + { type: 'linear', sampling: 'centroid' }, + { type: 'linear', sampling: 'sample' }, + { type: 'flat' }, + ] as const) + .beginSubcases() + .combineWithParams([ + { x: -1, y: -1 }, + { x: -1, y: -2 }, + { x: -1, y: 1 }, + { x: -1, y: 3 }, + { x: -2, y: -1 }, + { x: -2, y: 3 }, + { x: -3, y: -1 }, + { x: -3, y: -2 }, + { x: -3, y: 1 }, + { x: 1, y: -1 }, + { x: 1, y: -3 }, + { x: 1, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: -2 }, + { x: 2, y: -3 }, + { x: 2, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: -1 }, + { x: 3, y: -3 }, + { x: 3, y: 1 }, + { x: 3, y: 2 }, + { x: 3, y: 3 }, + ]) + ) + .beforeAllSubcases(t => { + const { + interpolation: { type, sampling }, + } = t.params; + t.skipIfInterpolationTypeOrSamplingNotSupported({ type, sampling }); + }) + .fn(async t => { + const { + x, + y, + nearFar, + sampleCount, + interpolation: { type, sampling }, + } = t.params; + // prettier-ignore + const clipSpacePoints = [ + [ x + 0.2, -y, 0, 1], + [-x + 0.2, y, 0, 1], + [ x + 0.2, y, 0, 1], + ]; + + const interStagePoints = [ + [13, 14, 15, 16], + [17, 18, 19, 20], + [21, 22, 23, 24], + ]; + + const width = 4; + const height = 4; + const actual = await renderFragmentShaderInputsTo4TexturesAndReadbackValues(t, { + interpolationType: type, + interpolationSampling: sampling, + sampleCount, + width, + height, + nearFar, + clipSpacePoints, + interStagePoints, + fragInCode: '@builtin(sample_mask) sample_mask: u32,', + outputCode: 'vec4f(f32(fin.sample_mask), 0, 0, 0)', + }); + + const expected = generateFragmentInputs({ + width, + height, + nearFar, + sampleCount, + clipSpacePoints, + interpolateFn: computeSampleMask, + }); + + t.expectOK( + checkSampleRectsApproximatelyEqual({ + width, + height, + sampleCount, + actual, + expected, + maxDiffULPsForFloatFormat: 0, + }) + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/user_io.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/user_io.spec.ts new file mode 100644 index 0000000000..0c26f89872 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/user_io.spec.ts @@ -0,0 +1,213 @@ +export const description = ` +Test for user-defined shader I/O. + +passthrough: + * Data passed into vertex shader as uints and converted to test type + * Passed from vertex to fragment as test type + * Output from fragment shader as uint +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { range } from '../../../../common/util/util.js'; +import { GPUTest } from '../../../gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +function generateInterstagePassthroughCode(type: string): string { + return ` +${type === 'f16' ? 'enable f16;' : ''} +struct IOData { + @builtin(position) pos : vec4f, + @location(0) @interpolate(flat) user0 : ${type}, + @location(1) @interpolate(flat) user1 : vec2<${type}>, + @location(2) @interpolate(flat) user2 : vec4<${type}>, +} + +struct VertexInput { + @builtin(vertex_index) idx : u32, + @location(0) in0 : u32, + @location(1) in1 : vec2u, + @location(2) in2 : vec4u, +} + +@vertex +fn vsMain(input : VertexInput) -> IOData { + const vertices = array( + vec4f(-1, -1, 0, 1), + vec4f(-1, 1, 0, 1), + vec4f( 1, -1, 0, 1), + ); + var data : IOData; + data.pos = vertices[input.idx]; + data.user0 = ${type}(input.in0); + data.user1 = vec2<${type}>(input.in1); + data.user2 = vec4<${type}>(input.in2); + return data; +} + +struct FragOutput { + @location(0) out0 : u32, + @location(1) out1 : vec2u, + @location(2) out2 : vec4u, +} + +@fragment +fn fsMain(input : IOData) -> FragOutput { + var out : FragOutput; + out.out0 = u32(input.user0); + out.out1 = vec2u(input.user1); + out.out2 = vec4u(input.user2); + return out; +} +`; +} + +function drawPassthrough(t: GPUTest, code: string) { + // Default limit is 32 bytes of color attachments. + // These attachments use 28 bytes (which is why vec3 is skipped). + const formats: GPUTextureFormat[] = ['r32uint', 'rg32uint', 'rgba32uint']; + const components = [1, 2, 4]; + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ code }), + entryPoint: 'vsMain', + buffers: [ + { + arrayStride: 4, + attributes: [ + { + format: 'uint32', + offset: 0, + shaderLocation: 0, + }, + ], + }, + { + arrayStride: 8, + attributes: [ + { + format: 'uint32x2', + offset: 0, + shaderLocation: 1, + }, + ], + }, + { + arrayStride: 16, + attributes: [ + { + format: 'uint32x4', + offset: 0, + shaderLocation: 2, + }, + ], + }, + ], + }, + fragment: { + module: t.device.createShaderModule({ code }), + entryPoint: 'fsMain', + targets: formats.map(x => { + return { format: x }; + }), + }, + primitive: { + topology: 'triangle-list', + }, + }); + + const vertexBuffer = t.makeBufferWithContents( + new Uint32Array([ + // scalar: offset 0 + 1, 1, 1, 0, + // vec2: offset 16 + 2, 2, 2, 2, 2, 2, 0, 0, + // vec4: offset 48 + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + ]), + GPUBufferUsage.COPY_SRC | GPUBufferUsage.VERTEX + ); + + const bytesPerComponent = 4; + // 256 is the minimum bytes per row for texture to buffer copies. + const width = 256 / bytesPerComponent; + const height = 2; + const copyWidth = 4; + const outputTextures = range(3, i => { + const texture = t.device.createTexture({ + size: [width, height], + usage: + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.TEXTURE_BINDING, + format: formats[i], + }); + t.trackForCleanup(texture); + return texture; + }); + + let bufferSize = 1; + for (const comp of components) { + bufferSize *= comp; + } + bufferSize *= outputTextures.length * bytesPerComponent * copyWidth; + const outputBuffer = t.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + t.trackForCleanup(outputBuffer); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: outputTextures.map(t => ({ + view: t.createView(), + loadOp: 'clear', + storeOp: 'store', + })), + }); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer, 0, 12); + pass.setVertexBuffer(1, vertexBuffer, 16, 24); + pass.setVertexBuffer(2, vertexBuffer, 48, 48); + pass.draw(3); + pass.end(); + + // Copy 'copyWidth' samples from each attachment into a buffer to check the results. + let offset = 0; + let expectArray: number[] = []; + for (let i = 0; i < outputTextures.length; i++) { + encoder.copyTextureToBuffer( + { texture: outputTextures[i] }, + { + buffer: outputBuffer, + offset, + bytesPerRow: bytesPerComponent * components[i] * width, + rowsPerImage: height, + }, + { width: copyWidth, height: 1 } + ); + offset += components[i] * bytesPerComponent * copyWidth; + for (let j = 0; j < components[i]; j++) { + const value = i + 1; + expectArray = expectArray.concat([value, value, value, value]); + } + } + t.queue.submit([encoder.finish()]); + + const expect = new Uint32Array(expectArray); + t.expectGPUBufferValuesEqual(outputBuffer, expect); +} + +g.test('passthrough') + .desc('Tests passing user-defined data from vertex input through fragment output') + .params(u => u.combine('type', ['f32', 'f16', 'i32', 'u32'] as const)) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = generateInterstagePassthroughCode(t.params.type); + drawPassthrough(t, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/workgroup_size.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/workgroup_size.spec.ts new file mode 100644 index 0000000000..278265b7ad --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/workgroup_size.spec.ts @@ -0,0 +1,150 @@ +export const description = `Test that workgroup size is set correctly`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { iterRange } from '../../../../common/util/util.js'; +import { GPUTest } from '../../../gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +function checkResults( + sizeX: number, + sizeY: number, + sizeZ: number, + numWGs: number, + data: Uint32Array +): Error | undefined { + const totalInvocations = sizeX * sizeY * sizeZ; + for (let i = 0; i < numWGs; i++) { + const wgx_data = data[4 * i + 0]; + const wgy_data = data[4 * i + 1]; + const wgz_data = data[4 * i + 2]; + const total_data = data[4 * i + 3]; + if (wgx_data !== sizeX) { + let msg = `Incorrect workgroup size x dimension for wg ${i}:\n`; + msg += `- expected: ${wgx_data}\n`; + msg += `- got: ${sizeX}`; + return Error(msg); + } + if (wgy_data !== sizeY) { + let msg = `Incorrect workgroup size y dimension for wg ${i}:\n`; + msg += `- expected: ${wgy_data}\n`; + msg += `- got: ${sizeY}`; + return Error(msg); + } + if (wgz_data !== sizeZ) { + let msg = `Incorrect workgroup size y dimension for wg ${i}:\n`; + msg += `- expected: ${wgz_data}\n`; + msg += `- got: ${sizeZ}`; + return Error(msg); + } + if (total_data !== totalInvocations) { + let msg = `Incorrect workgroup total invocations for wg ${i}:\n`; + msg += `- expected: ${total_data}\n`; + msg += `- got: ${totalInvocations}`; + return Error(msg); + } + } + return undefined; +} + +g.test('workgroup_size') + .desc(`Test workgroup size is set correctly`) + .params(u => + u + .combine('wgx', [1, 3, 4, 8, 11, 16, 51, 64, 128, 256] as const) + .combine('wgy', [1, 3, 4, 8, 16, 51, 64, 256] as const) + .combine('wgz', [1, 3, 11, 16, 128, 256] as const) + .beginSubcases() + ) + .fn(async t => { + const { + maxComputeWorkgroupSizeX, + maxComputeWorkgroupSizeY, + maxComputeWorkgroupSizeZ, + maxComputeInvocationsPerWorkgroup, + } = t.device.limits; + t.skipIf( + t.params.wgx > maxComputeWorkgroupSizeX, + `workgroup size x: ${t.params.wgx} > limit: ${maxComputeWorkgroupSizeX}` + ); + t.skipIf( + t.params.wgy > maxComputeWorkgroupSizeY, + `workgroup size x: ${t.params.wgy} > limit: ${maxComputeWorkgroupSizeY}` + ); + t.skipIf( + t.params.wgz > maxComputeWorkgroupSizeZ, + `workgroup size x: ${t.params.wgz} > limit: ${maxComputeWorkgroupSizeZ}` + ); + const totalInvocations = t.params.wgx * t.params.wgy * t.params.wgz; + t.skipIf( + totalInvocations > maxComputeInvocationsPerWorkgroup, + `workgroup size: ${totalInvocations} > limit: ${maxComputeInvocationsPerWorkgroup}` + ); + + const code = ` +struct Values { + x : atomic<u32>, + y : atomic<u32>, + z : atomic<u32>, + total : atomic<u32>, +} + +@group(0) @binding(0) +var<storage, read_write> v : array<Values>; + +@compute @workgroup_size(${t.params.wgx}, ${t.params.wgy}, ${t.params.wgz}) +fn main(@builtin(local_invocation_id) lid : vec3u, + @builtin(workgroup_id) wgid : vec3u) { + atomicMax(&v[wgid.x].x, lid.x + 1); + atomicMax(&v[wgid.x].y, lid.y + 1); + atomicMax(&v[wgid.x].z, lid.z + 1); + atomicAdd(&v[wgid.x].total, 1); +}`; + + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: 'main', + }, + }); + + const numWorkgroups = totalInvocations < 256 ? 5 : 3; + const buffer = t.makeBufferWithContents( + new Uint32Array([...iterRange(numWorkgroups * 4, _i => 0)]), + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST + ); + t.trackForCleanup(buffer); + + const bg = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer, + }, + }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bg); + pass.dispatchWorkgroups(numWorkgroups, 1, 1); + pass.end(); + t.queue.submit([encoder.finish()]); + + const bufferReadback = await t.readGPUBufferRangeTyped(buffer, { + srcByteOffset: 0, + type: Uint32Array, + typedLength: 4 * numWorkgroups, + method: 'copy', + }); + const data: Uint32Array = bufferReadback.data; + + t.expectOK(checkResults(t.params.wgx, t.params.wgy, t.params.wgz, numWorkgroups, data)); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/stage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/stage.spec.ts new file mode 100644 index 0000000000..6e06e67e37 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/stage.spec.ts @@ -0,0 +1,133 @@ +export const description = `Test trivial shaders for each shader stage kind`; + +// There are many many more shaders executed in other tests. + +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { GPUTest } from '../../gpu_test.js'; +import { checkElementsEqual } from '../../util/check_contents.js'; + +export const g = makeTestGroup(GPUTest); + +g.test('basic_compute') + .desc(`Test a trivial compute shader`) + .fn(async t => { + const code = ` + +@group(0) @binding(0) +var<storage, read_write> v : vec4u; + +@compute @workgroup_size(1) +fn main() { + v = vec4u(1,2,3,42); +}`; + + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code, + }), + entryPoint: 'main', + }, + }); + + const buffer = t.makeBufferWithContents( + new Uint32Array([0, 0, 0, 0]), + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST + ); + t.trackForCleanup(buffer); + + const bg = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer, + }, + }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bg); + pass.dispatchWorkgroups(1, 1, 1); + pass.end(); + t.queue.submit([encoder.finish()]); + + const bufferReadback = await t.readGPUBufferRangeTyped(buffer, { + srcByteOffset: 0, + type: Uint32Array, + typedLength: 4, + method: 'copy', + }); + const got: Uint32Array = bufferReadback.data; + const expected = new Uint32Array([1, 2, 3, 42]); + + t.expectOK(checkElementsEqual(got, expected)); + }); + +g.test('basic_render') + .desc(`Test trivial vertex and fragment shaders`) + .fn(t => { + const code = ` +@vertex +fn vert_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4f { + // A right triangle covering the whole framebuffer. + const pos = array( + vec2f(-1,-3), + vec2f(3,1), + vec2f(-1,1)); + return vec4f(pos[idx], 0, 1); +} + +@fragment +fn frag_main() -> @location(0) vec4f { + return vec4(0, 1, 0, 1); // green +} +`; + const module = t.device.createShaderModule({ code }); + + const [width, height] = [8, 8] as const; + const format = 'rgba8unorm' as const; + const texture = t.device.createTexture({ + size: { width, height }, + usage: + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_SRC, + format, + }); + + // We'll copy one pixel only. + const dst = t.device.createBuffer({ + size: 4, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { module, entryPoint: 'vert_main' }, + fragment: { module, entryPoint: 'frag_main', targets: [{ format }] }, + }); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [{ view: texture.createView(), loadOp: 'clear', storeOp: 'store' }], + }); + pass.setPipeline(pipeline); + pass.draw(3); + pass.end(); + + encoder.copyTextureToBuffer( + { texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } }, + { buffer: dst, bytesPerRow: 256 }, + { width: 1, height: 1, depthOrArrayLayers: 1 } + ); + t.queue.submit([encoder.finish()]); + + // Expect one green pixel. + t.expectGPUBufferValuesEqual(dst, new Uint8Array([0x00, 0xff, 0x00, 0xff])); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/statement/compound.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/statement/compound.spec.ts new file mode 100644 index 0000000000..aed0cc2245 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/statement/compound.spec.ts @@ -0,0 +1,137 @@ +export const description = ` +Compound statement execution. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { TypedArrayBufferView } from '../../../../common/util/util.js'; +import { GPUTest } from '../../../gpu_test.js'; + +export const g = makeTestGroup(GPUTest); + +/** + * Builds, runs then checks the output of a statement shader test. + * + * @param t The test object + * @param ty The WGSL scalar type to be written + * @param values The expected output values of type ty + * @param wgsl_main The body of the WGSL entry point. + */ +export function runStatementTest( + t: GPUTest, + ty: string, + values: TypedArrayBufferView, + wgsl_main: string +) { + const wgsl = ` +struct Outputs { + data : array<${ty}>, +}; +var<private> count: u32 = 0; + +@group(0) @binding(1) var<storage, read_write> outputs : Outputs; + +fn put(value : ${ty}) { + outputs.data[count] = value; + count += 1; +} + +@compute @workgroup_size(1) +fn main() { + _ = &outputs; + ${wgsl_main} +} +`; + + const pipeline = t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ code: wgsl }), + entryPoint: 'main', + }, + }); + + const maxOutputValues = 1000; + const outputBuffer = t.device.createBuffer({ + size: 4 * (1 + maxOutputValues), + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + + const bindGroup = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [{ binding: 1, resource: { buffer: outputBuffer } }], + }); + + // Run the shader. + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginComputePass(); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(1); + pass.end(); + t.queue.submit([encoder.finish()]); + + t.expectGPUBufferValuesEqual(outputBuffer, values); +} + +// Consider a declaration X of identifier 'x' inside a compound statement. +// Check the value of 'x' at various places relative to X: +// a { b; X=c; d; { e; } } f; + +const kTests = { + uses: { + // Observe values without conflicting declarations. + src: `let x = 1; + put(x); + { + put(x); + let x = x+1; // The declaration in question + put(x); + { + put(x); + } + put(x); + } + put(x);`, + values: [1, 1, 2, 2, 2, 1], + }, + shadowed: { + // Observe values when shadowed + src: `let x = 1; + put(x); + { + put(x); + let x = x+1; // The declaration in question + put(x); + { + let x = x+1; // A shadow + put(x); + } + put(x); + } + put(x);`, + values: [1, 1, 2, 3, 2, 1], + }, + gone: { + // The declaration goes out of scope. + src: `{ + let x = 2; // The declaration in question + put(x); + } + let x = 1; + put(x);`, + values: [2, 1], + }, +} as const; + +g.test('decl') + .desc('Tests the value of a declared value in a compound statment.') + .params(u => u.combine('case', keysOf(kTests))) + .fn(t => { + runStatementTest( + t, + 'i32', + new Int32Array(kTests[t.params.case].values), + kTests[t.params.case].src + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/statement/discard.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/statement/discard.spec.ts new file mode 100644 index 0000000000..058ff50f17 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/statement/discard.spec.ts @@ -0,0 +1,645 @@ +export const description = ` +Execution tests for discard. + +The discard statement converts invocations into helpers. +This results in the following conditions: + * No outputs are written + * No resources are written + * Atomics are undefined + +Conditions that still occur: + * Derivative calculations are correct + * Reads + * Writes to non-external memory +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { iterRange } from '../../../../common/util/util.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { checkElementsPassPredicate } from '../../../util/check_contents.js'; + +export const g = makeTestGroup(GPUTest); + +// Framebuffer dimensions +const kWidth = 64; +const kHeight = 64; + +const kSharedCode = ` +@group(0) @binding(0) var<storage, read_write> output: array<vec2f>; +@group(0) @binding(1) var<storage, read_write> atomicIndex : atomic<u32>; +@group(0) @binding(2) var<storage> uniformValues : array<u32, 5>; + +@vertex +fn vsMain(@builtin(vertex_index) index : u32) -> @builtin(position) vec4f { + const vertices = array( + vec2(-1, -1), vec2(-1, 0), vec2( 0, -1), + vec2(-1, 0), vec2( 0, 0), vec2( 0, -1), + + vec2( 0, -1), vec2( 0, 0), vec2( 1, -1), + vec2( 0, 0), vec2( 1, 0), vec2( 1, -1), + + vec2(-1, 0), vec2(-1, 1), vec2( 0, 0), + vec2(-1, 1), vec2( 0, 1), vec2( 0, 0), + + vec2( 0, 0), vec2( 0, 1), vec2( 1, 0), + vec2( 0, 1), vec2( 1, 1), vec2( 1, 0), + ); + return vec4f(vec2f(vertices[index]), 0, 1); +} +`; + +function drawFullScreen( + t: GPUTest, + code: string, + dataChecker: (a: Float32Array) => Error | undefined, + framebufferChecker: (a: Uint32Array) => Error | undefined +) { + const pipeline = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ code }), + entryPoint: 'vsMain', + }, + fragment: { + module: t.device.createShaderModule({ code }), + entryPoint: 'fsMain', + targets: [{ format: 'r32uint' }], + }, + primitive: { + topology: 'triangle-list', + }, + }); + + const bytesPerWord = 4; + const framebuffer = t.device.createTexture({ + size: [kWidth, kHeight], + usage: + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.TEXTURE_BINDING, + format: 'r32uint', + }); + t.trackForCleanup(framebuffer); + + // Create a buffer to copy the framebuffer contents into. + // Initialize with a sentinel value and load this buffer to detect unintended writes. + const fbBuffer = t.makeBufferWithContents( + new Uint32Array([...iterRange(kWidth * kHeight, x => kWidth * kHeight)]), + GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST + ); + + // Create a buffer to hold the storage shader resources. + // (0,0) = vec2u width * height + // (0,1) = u32 + const dataSize = 2 * kWidth * kHeight * bytesPerWord; + const dataBufferSize = dataSize + bytesPerWord; + const dataBuffer = t.device.createBuffer({ + size: dataBufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + }); + + const uniformSize = bytesPerWord * 5; + const uniformBuffer = t.makeBufferWithContents( + // Loop bound, [derivative constants]. + new Uint32Array([4, 1, 4, 4, 7]), + GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE + ); + + // 'atomicIndex' packed at the end of the buffer. + const bg = t.device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: dataBuffer, + offset: 0, + size: dataSize, + }, + }, + { + binding: 1, + resource: { + buffer: dataBuffer, + offset: dataSize, + size: bytesPerWord, + }, + }, + { + binding: 2, + resource: { + buffer: uniformBuffer, + offset: 0, + size: uniformSize, + }, + }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + encoder.copyBufferToTexture( + { + buffer: fbBuffer, + offset: 0, + bytesPerRow: kWidth * bytesPerWord, + rowsPerImage: kHeight, + }, + { texture: framebuffer }, + { width: kWidth, height: kHeight } + ); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: framebuffer.createView(), + loadOp: 'load', + storeOp: 'store', + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bg); + pass.draw(24); + pass.end(); + encoder.copyTextureToBuffer( + { texture: framebuffer }, + { + buffer: fbBuffer, + offset: 0, + bytesPerRow: kWidth * bytesPerWord, + rowsPerImage: kHeight, + }, + { width: kWidth, height: kHeight } + ); + t.queue.submit([encoder.finish()]); + + t.expectGPUBufferValuesPassCheck(dataBuffer, dataChecker, { + type: Float32Array, + typedLength: dataSize / bytesPerWord, + }); + + t.expectGPUBufferValuesPassCheck(fbBuffer, framebufferChecker, { + type: Uint32Array, + typedLength: kWidth * kHeight, + }); +} + +g.test('all') + .desc('Test a shader that discards all fragments') + .fn(t => { + const code = ` +${kSharedCode} + +@fragment +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { + _ = uniformValues[0]; + discard; + let idx = atomicAdd(&atomicIndex, 1); + output[idx] = pos.xy; + return 1; +} +`; + + // No storage writes occur. + const dataChecker = (a: Float32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + return value === 0; + }, + { + predicatePrinter: [ + { + leftHeader: 'data exp ==', + getValueForCell: (idx: number) => { + return 0; + }, + }, + ], + } + ); + }; + + // No fragment outputs occur. + const fbChecker = (a: Uint32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + return value === kWidth * kHeight; + }, + { + predicatePrinter: [ + { + leftHeader: 'fb exp ==', + getValueForCell: (idx: number) => { + return 0; + }, + }, + ], + } + ); + }; + + drawFullScreen(t, code, dataChecker, fbChecker); + }); + +g.test('three_quarters') + .desc('Test a shader that discards all but the upper-left quadrant fragments') + .fn(t => { + const code = ` +${kSharedCode} + +@fragment +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { + _ = uniformValues[0]; + if (pos.x >= 0.5 * ${kWidth} || pos.y >= 0.5 * ${kHeight}) { + discard; + } + let idx = atomicAdd(&atomicIndex, 1); + output[idx] = pos.xy; + return idx; +} +`; + + // Only the the upper left quadrant is kept. + const dataChecker = (a: Float32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + const is_x = idx % 2 === 0; + if (is_x) { + return value < 0.5 * kWidth; + } else { + return value < 0.5 * kHeight; + } + }, + { + predicatePrinter: [ + { + leftHeader: 'data exp ==', + getValueForCell: (idx: number): number | string => { + const is_x = idx % 2 === 0; + if (is_x) { + const x = Math.floor(idx / 2) % kWidth; + if (x >= kWidth / 2) { + return 0; + } + } else { + const y = Math.floor((idx - 1) / kWidth); + if (y >= kHeight / 2) { + return 0; + } + } + if (is_x) { + return `< ${0.5 * kWidth}`; + } else { + return `< ${0.5 * kHeight}`; + } + }, + }, + ], + } + ); + }; + const fbChecker = (a: Uint32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + const x = idx % kWidth; + const y = Math.floor(idx / kWidth); + if (x < kWidth / 2 && y < kHeight / 2) { + return value < (kWidth * kHeight) / 4; + } else { + return value === kWidth * kHeight; + } + return value < (kWidth * kHeight) / 4; + }, + { + predicatePrinter: [ + { + leftHeader: 'fb exp ==', + getValueForCell: (idx: number) => { + const x = idx % kWidth; + const y = Math.floor(idx / kWidth); + if (x < kWidth / 2 && y < kHeight / 2) { + return 'any'; + } else { + return 0; + } + }, + }, + ], + } + ); + }; + + drawFullScreen(t, code, dataChecker, fbChecker); + }); + +g.test('function_call') + .desc('Test discards happening in a function call') + .fn(t => { + const code = ` +${kSharedCode} + +fn foo(pos : vec2f) { + let p = vec2i(pos); + if p.x <= ${kWidth} / 2 && p.y <= ${kHeight} / 2 { + discard; + } + if p.x >= ${kWidth} / 2 && p.y >= ${kHeight} / 2 { + discard; + } +} + +@fragment +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { + _ = uniformValues[0]; + foo(pos.xy); + let idx = atomicAdd(&atomicIndex, 1); + output[idx] = pos.xy; + return idx; +} +`; + + // Only the upper right and bottom left quadrants are kept. + const dataChecker = (a: Float32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + const is_x = idx % 2 === 0; + if (value === 0.0) { + return is_x ? a[idx + 1] === 0 : a[idx - 1] === 0; + } + + let expect = is_x ? kWidth : kHeight; + expect = 0.5 * expect; + if (value < expect) { + return is_x ? a[idx + 1] > 0.5 * kWidth : a[idx - 1] > 0.5 * kHeight; + } else { + return is_x ? a[idx + 1] < 0.5 * kWidth : a[idx - 1] < 0.5 * kHeight; + } + }, + { + predicatePrinter: [ + { + leftHeader: 'data exp ==', + getValueForCell: (idx: number): number | string => { + if (idx < (kWidth * kHeight) / 2) { + return 'any'; + } else { + return 0; + } + }, + }, + ], + } + ); + }; + const fbChecker = (a: Uint32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + const x = idx % kWidth; + const y = Math.floor(idx / kWidth); + if ((x >= kWidth / 2 && y >= kHeight / 2) || (x <= kWidth / 2 && y <= kHeight / 2)) { + return value === kWidth * kHeight; + } else { + return value < (kWidth * kHeight) / 2; + } + }, + { + predicatePrinter: [ + { + leftHeader: 'fb exp ==', + getValueForCell: (idx: number) => { + const x = idx % kWidth; + const y = Math.floor(idx / kWidth); + if ( + (x <= kWidth / 2 && y <= kHeight / 2) || + (x >= kWidth / 2 && y >= kHeight / 2) + ) { + return kWidth * kHeight; + } + return 'any'; + }, + }, + ], + } + ); + }; + + drawFullScreen(t, code, dataChecker, fbChecker); + }); + +g.test('loop') + .desc('Test discards in a loop') + .fn(t => { + const code = ` +${kSharedCode} + +@fragment +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { + _ = uniformValues[0]; + for (var i = 0; i < 2; i++) { + if i > 0 { + discard; + } + } + let idx = atomicAdd(&atomicIndex, 1); + output[idx] = pos.xy; + return 1; +} +`; + + // No storage writes occur. + const dataChecker = (a: Float32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + return value === 0; + }, + { + predicatePrinter: [ + { + leftHeader: 'data exp ==', + getValueForCell: (idx: number) => { + return 0; + }, + }, + ], + } + ); + }; + + // No fragment outputs occur. + const fbChecker = (a: Uint32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + return value === kWidth * kHeight; + }, + { + predicatePrinter: [ + { + leftHeader: 'fb exp ==', + getValueForCell: (idx: number) => { + return kWidth * kHeight; + }, + }, + ], + } + ); + }; + + drawFullScreen(t, code, dataChecker, fbChecker); + }); + +g.test('uniform_read_loop') + .desc('Test that helpers read a uniform value in a loop') + .fn(t => { + const code = ` +${kSharedCode} + +@fragment +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { + discard; + for (var i = 0u; i < uniformValues[0]; i++) { + } + let idx = atomicAdd(&atomicIndex, 1); + output[idx] = pos.xy; + return 1; +} +`; + + // No storage writes occur. + const dataChecker = (a: Float32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + return value === 0; + }, + { + predicatePrinter: [ + { + leftHeader: 'data exp ==', + getValueForCell: (idx: number) => { + return 0; + }, + }, + ], + } + ); + }; + + // No fragment outputs occur. + const fbChecker = (a: Uint32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + return value === kWidth * kHeight; + }, + { + predicatePrinter: [ + { + leftHeader: 'fb exp ==', + getValueForCell: (idx: number) => { + return kWidth * kHeight; + }, + }, + ], + } + ); + }; + + drawFullScreen(t, code, dataChecker, fbChecker); + }); + +g.test('derivatives') + .desc('Test that derivatives are correct in the presence of discard') + .fn(t => { + const code = ` +${kSharedCode} + +@fragment +fn fsMain(@builtin(position) pos : vec4f) -> @location(0) u32 { + let ipos = vec2i(pos.xy); + let lsb = ipos & vec2(0x1); + let left_sel = select(2, 4, lsb.y == 1); + let right_sel = select(1, 3, lsb.y == 1); + let uidx = select(left_sel, right_sel, lsb.x == 1); + if ((lsb.x | lsb.y) & 0x1) == 0 { + discard; + } + + let v = uniformValues[uidx]; + let idx = atomicAdd(&atomicIndex, 1); + let dx = dpdx(f32(v)); + let dy = dpdy(f32(v)); + output[idx] = vec2(dx, dy); + return idx; +} +`; + + // One pixel per quad is discarded. The derivatives values are always the same +/- 3. + const dataChecker = (a: Float32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + if (idx < (3 * (2 * kWidth * kHeight)) / 4) { + return value === -3 || value === 3; + } else { + return value === 0; + } + }, + { + predicatePrinter: [ + { + leftHeader: 'data exp ==', + getValueForCell: (idx: number) => { + if (idx < (3 * (2 * kWidth * kHeight)) / 4) { + return '+/- 3'; + } else { + return 0; + } + }, + }, + ], + } + ); + }; + + // 3/4 of the fragments are written. + const fbChecker = (a: Uint32Array) => { + return checkElementsPassPredicate( + a, + (idx: number, value: number | bigint) => { + const x = idx % kWidth; + const y = Math.floor(idx / kWidth); + if (((x | y) & 0x1) === 0) { + return value === kWidth * kHeight; + } else { + return value < (3 * (kWidth * kHeight)) / 4; + } + }, + { + predicatePrinter: [ + { + leftHeader: 'fb exp ==', + getValueForCell: (idx: number) => { + const x = idx % kWidth; + const y = Math.floor(idx / kWidth); + if (((x | y) & 0x1) === 0) { + return kWidth * kHeight; + } else { + return 'any'; + } + }, + }, + ], + } + ); + }; + + drawFullScreen(t, code, dataChecker, fbChecker); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts index e03a72f8df..1c6c9bc4a3 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts @@ -107,6 +107,10 @@ g.test('compute,zero_init') ? [true, false] : [false]) { for (const scalarType of supportedScalarTypes({ isAtomic, ...p })) { + // Fewer subcases: supportedScalarTypes was expanded to include f16 + // but that may take too much time. It would require more complex code. + if (scalarType === 'f16') continue; + // Fewer subcases: For nested types, skip atomic u32 and non-atomic i32. if (p._containerDepth > 0) { if (scalarType === 'u32' && isAtomic) continue; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts index 799ea3affb..76b094310d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts @@ -2,19 +2,32 @@ import { keysOf } from '../../common/util/data_tables.js'; import { assert } from '../../common/util/util.js'; import { align } from '../util/math.js'; -const kArrayLength = 3; +const kDefaultArrayLength = 3; export type Requirement = 'never' | 'may' | 'must'; // never is the same as "must not" -export type ContainerType = 'scalar' | 'vector' | 'matrix' | 'atomic' | 'array'; -export type ScalarType = 'i32' | 'u32' | 'f32' | 'bool'; +export type ContainerType = 'scalar' | 'vector' | 'matrix' | 'array'; +export type ScalarType = 'i32' | 'u32' | 'f16' | 'f32' | 'bool'; -export const HostSharableTypes = ['i32', 'u32', 'f32'] as const; +export const HostSharableTypes = ['i32', 'u32', 'f16', 'f32'] as const; + +// The alignment and size of a host shareable type. +// See "Alignment and Size" in the WGSL spec. https://w3.org/TR/WGSL/#alignment-and-size +// Note this is independent of the address space that values of this type might appear in. +// See RequiredAlignOf(...) for the 16-byte granularity requirement when +// values of a type are placed in the uniform address space. +type AlignmentAndSize = { + // AlignOf(T) for generated type T + alignment: number; + // SizeOf(T) for generated type T + size: number; +}; /** Info for each plain scalar type. */ export const kScalarTypeInfo = /* prettier-ignore */ { 'i32': { layout: { alignment: 4, size: 4 }, supportsAtomics: true, arrayLength: 1, innerLength: 0 }, 'u32': { layout: { alignment: 4, size: 4 }, supportsAtomics: true, arrayLength: 1, innerLength: 0 }, + 'f16': { layout: { alignment: 2, size: 2 }, supportsAtomics: false, arrayLength: 1, innerLength: 0, feature: 'shader-f16' }, 'f32': { layout: { alignment: 4, size: 4 }, supportsAtomics: false, arrayLength: 1, innerLength: 0 }, 'bool': { layout: undefined, supportsAtomics: false, arrayLength: 1, innerLength: 0 }, } as const; @@ -24,29 +37,71 @@ export const kScalarTypes = keysOf(kScalarTypeInfo); /** Info for each vecN<> container type. */ export const kVectorContainerTypeInfo = /* prettier-ignore */ { - 'vec2': { layout: { alignment: 8, size: 8 }, arrayLength: 2 , innerLength: 0 }, - 'vec3': { layout: { alignment: 16, size: 12 }, arrayLength: 3 , innerLength: 0 }, - 'vec4': { layout: { alignment: 16, size: 16 }, arrayLength: 4 , innerLength: 0 }, + 'vec2': { arrayLength: 2 , innerLength: 0 }, + 'vec3': { arrayLength: 3 , innerLength: 0 }, + 'vec4': { arrayLength: 4 , innerLength: 0 }, } as const; /** List of all vecN<> container types. */ export const kVectorContainerTypes = keysOf(kVectorContainerTypeInfo); +/** Returns the vector layout for a given vector container and base type, or undefined if that base type has no layout */ +function vectorLayout( + vectorContainer: 'vec2' | 'vec3' | 'vec4', + baseType: ScalarType +): undefined | AlignmentAndSize { + const n = kVectorContainerTypeInfo[vectorContainer].arrayLength; + const scalarLayout = kScalarTypeInfo[baseType].layout; + if (scalarLayout === undefined) { + return undefined; + } + if (n === 3) { + return { alignment: scalarLayout.alignment * 4, size: scalarLayout.size * 3 }; + } + return { alignment: scalarLayout.alignment * n, size: scalarLayout.size * n }; +} + /** Info for each matNxN<> container type. */ export const kMatrixContainerTypeInfo = /* prettier-ignore */ { - 'mat2x2': { layout: { alignment: 8, size: 16 }, arrayLength: 2, innerLength: 2 }, - 'mat3x2': { layout: { alignment: 8, size: 24 }, arrayLength: 3, innerLength: 2 }, - 'mat4x2': { layout: { alignment: 8, size: 32 }, arrayLength: 4, innerLength: 2 }, - 'mat2x3': { layout: { alignment: 16, size: 32 }, arrayLength: 2, innerLength: 3 }, - 'mat3x3': { layout: { alignment: 16, size: 48 }, arrayLength: 3, innerLength: 3 }, - 'mat4x3': { layout: { alignment: 16, size: 64 }, arrayLength: 4, innerLength: 3 }, - 'mat2x4': { layout: { alignment: 16, size: 32 }, arrayLength: 2, innerLength: 4 }, - 'mat3x4': { layout: { alignment: 16, size: 48 }, arrayLength: 3, innerLength: 4 }, - 'mat4x4': { layout: { alignment: 16, size: 64 }, arrayLength: 4, innerLength: 4 }, + 'mat2x2': { arrayLength: 2, innerLength: 2 }, + 'mat3x2': { arrayLength: 3, innerLength: 2 }, + 'mat4x2': { arrayLength: 4, innerLength: 2 }, + 'mat2x3': { arrayLength: 2, innerLength: 3 }, + 'mat3x3': { arrayLength: 3, innerLength: 3 }, + 'mat4x3': { arrayLength: 4, innerLength: 3 }, + 'mat2x4': { arrayLength: 2, innerLength: 4 }, + 'mat3x4': { arrayLength: 3, innerLength: 4 }, + 'mat4x4': { arrayLength: 4, innerLength: 4 }, } as const; /** List of all matNxN<> container types. */ export const kMatrixContainerTypes = keysOf(kMatrixContainerTypeInfo); +export const kMatrixContainerTypeLayoutInfo = + /* prettier-ignore */ { + 'f16': { + 'mat2x2': { layout: { alignment: 4, size: 8 } }, + 'mat3x2': { layout: { alignment: 4, size: 12 } }, + 'mat4x2': { layout: { alignment: 4, size: 16 } }, + 'mat2x3': { layout: { alignment: 8, size: 16 } }, + 'mat3x3': { layout: { alignment: 8, size: 24 } }, + 'mat4x3': { layout: { alignment: 8, size: 32 } }, + 'mat2x4': { layout: { alignment: 8, size: 16 } }, + 'mat3x4': { layout: { alignment: 8, size: 24 } }, + 'mat4x4': { layout: { alignment: 8, size: 32 } }, + }, + 'f32': { + 'mat2x2': { layout: { alignment: 8, size: 16 } }, + 'mat3x2': { layout: { alignment: 8, size: 24 } }, + 'mat4x2': { layout: { alignment: 8, size: 32 } }, + 'mat2x3': { layout: { alignment: 16, size: 32 } }, + 'mat3x3': { layout: { alignment: 16, size: 48 } }, + 'mat4x3': { layout: { alignment: 16, size: 64 } }, + 'mat2x4': { layout: { alignment: 16, size: 32 } }, + 'mat3x4': { layout: { alignment: 16, size: 48 } }, + 'mat4x4': { layout: { alignment: 16, size: 64 } }, + } +} as const; + export type AddressSpace = 'storage' | 'uniform' | 'private' | 'function' | 'workgroup' | 'handle'; export type AccessMode = 'read' | 'write' | 'read_write'; export type Scope = 'module' | 'function'; @@ -161,10 +216,39 @@ export function* generateTypes({ containerType: ContainerType; /** Whether to wrap the baseType in `atomic<>`. */ isAtomic?: boolean; -}) { +}): Generator< + { + /** WGSL name for the generated type */ + type: string; + _kTypeInfo: { + /** + * WGSL name for: + * - the generated type if it is scalar or atomic + * - the column vector type if the generated type is a matrix + * - the base type if the generated type is an array + */ + elementBaseType: string; + /** Layout details if host-shareable, and undefined otherwise. */ + layout: undefined | AlignmentAndSize; + supportsAtomics: boolean; + /** The number of elementBaseType items in the container. */ + arrayLength: number; + /** + * 0 for scalar and vector. + * For a matrix type, this is the number of rows in the matrix. + */ + innerLength?: number; + }; + }, + void +> { const scalarInfo = kScalarTypeInfo[baseType]; if (isAtomic) { assert(scalarInfo.supportsAtomics, 'type does not support atomics'); + assert( + containerType === 'scalar' || containerType === 'array', + "can only generate atomic inner types with containerType 'scalar' or 'array'" + ); } const scalarType = isAtomic ? `atomic<${baseType}>` : baseType; @@ -189,21 +273,29 @@ export function* generateTypes({ for (const vectorType of kVectorContainerTypes) { yield { type: `${vectorType}<${scalarType}>`, - _kTypeInfo: { elementBaseType: baseType, ...kVectorContainerTypeInfo[vectorType] }, + _kTypeInfo: { + elementBaseType: baseType, + ...kVectorContainerTypeInfo[vectorType], + layout: vectorLayout(vectorType, scalarType as ScalarType), + supportsAtomics: false, + }, }; } } if (containerType === 'matrix') { - // Matrices can only be f32. - if (baseType === 'f32') { + // Matrices can only be f16 or f32. + if (baseType === 'f16' || baseType === 'f32') { for (const matrixType of kMatrixContainerTypes) { - const matrixInfo = kMatrixContainerTypeInfo[matrixType]; + const matrixDimInfo = kMatrixContainerTypeInfo[matrixType]; + const matrixLayoutInfo = kMatrixContainerTypeLayoutInfo[baseType][matrixType]; yield { type: `${matrixType}<${scalarType}>`, _kTypeInfo: { - elementBaseType: `vec${matrixInfo.innerLength}<${scalarType}>`, - ...matrixInfo, + elementBaseType: `vec${matrixDimInfo.innerLength}<${scalarType}>`, + ...matrixDimInfo, + ...matrixLayoutInfo, + supportsAtomics: false, }, }; } @@ -212,41 +304,57 @@ export function* generateTypes({ // Array types if (containerType === 'array') { + let arrayElemType: string = scalarType; + let arrayElementCount: number = kDefaultArrayLength; + let supportsAtomics = scalarInfo.supportsAtomics; + let layout: undefined | AlignmentAndSize = undefined; + if (scalarInfo.layout) { + // Compute the layout of the array type. + // Adjust the array element count or element type as needed. + if (addressSpace === 'uniform') { + // Use a vec4 of the scalar type, to achieve a 16 byte alignment without internal padding. + // This works for 4-byte scalar types, and does not work for f16. + // It is the caller's responsibility to filter out the f16 case. + assert(!isAtomic, 'the uniform case is making vec4 of scalar, which cannot handle atomics'); + arrayElemType = `vec4<${baseType}>`; + supportsAtomics = false; + const arrayElemLayout = vectorLayout('vec4', baseType) as AlignmentAndSize; + // assert(arrayElemLayout.alignment % 16 === 0); // Callers responsibility to avoid + arrayElementCount = align(arrayElementCount, 4) / 4; + const arrayByteSize = arrayElementCount * arrayElemLayout.size; + layout = { alignment: arrayElemLayout.alignment, size: arrayByteSize }; + } else { + // The ordinary case. Use scalarType as the element type. + const stride = arrayStride(scalarInfo.layout); + let arrayByteSize = arrayElementCount * stride; + if (addressSpace === 'storage') { + // The buffer effective binding size must be a multiple of 4. + // Adjust the array element count as needed. + while (arrayByteSize % 4 > 0) { + arrayElementCount++; + arrayByteSize = arrayElementCount * stride; + } + } + layout = { alignment: scalarInfo.layout.alignment, size: arrayByteSize }; + } + } + const arrayTypeInfo = { elementBaseType: `${baseType}`, - arrayLength: kArrayLength, - layout: scalarInfo.layout - ? { - alignment: scalarInfo.layout.alignment, - size: - addressSpace === 'uniform' - ? // Uniform storage class must have array elements aligned to 16. - kArrayLength * - arrayStride({ - ...scalarInfo.layout, - alignment: 16, - }) - : kArrayLength * arrayStride(scalarInfo.layout), - } - : undefined, + arrayLength: arrayElementCount, + layout, + supportsAtomics, }; // Sized - if (addressSpace === 'uniform') { - yield { - type: `array<vec4<${scalarType}>,${kArrayLength}>`, - _kTypeInfo: arrayTypeInfo, - }; - } else { - yield { type: `array<${scalarType},${kArrayLength}>`, _kTypeInfo: arrayTypeInfo }; - } + yield { type: `array<${arrayElemType},${arrayElementCount}>`, _kTypeInfo: arrayTypeInfo }; // Unsized if (addressSpace === 'storage') { - yield { type: `array<${scalarType}>`, _kTypeInfo: arrayTypeInfo }; + yield { type: `array<${arrayElemType}>`, _kTypeInfo: arrayTypeInfo }; } } - function arrayStride(elementLayout: { size: number; alignment: number }) { + function arrayStride(elementLayout: AlignmentAndSize) { return align(elementLayout.size, elementLayout.alignment); } @@ -272,7 +380,7 @@ export function supportsAtomics(p: { ); } -/** Generates an iterator of supported base types (i32/u32/f32/bool) */ +/** Generates an iterator of supported base types (i32/u32/f16/f32/bool) */ export function* supportedScalarTypes(p: { isAtomic: boolean; addressSpace: string }) { for (const scalarType of kScalarTypes) { const info = kScalarTypeInfo[scalarType]; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/const_assert/const_assert.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/const_assert/const_assert.spec.ts index caaabc54d3..6c747f74a7 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/const_assert/const_assert.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/const_assert/const_assert.spec.ts @@ -59,9 +59,9 @@ g.test('constant_expression_no_assert') .desc(`Test that const_assert does not assert on a true conditional expression`) .params(u => u - .combine('case', keysOf(kConditionCases)) .combine('scope', ['module', 'function'] as const) .beginSubcases() + .combine('case', keysOf(kConditionCases)) ) .fn(t => { const expr = kConditionCases[t.params.case].expr; @@ -76,9 +76,9 @@ g.test('constant_expression_assert') .desc(`Test that const_assert does assert on a false conditional expression`) .params(u => u - .combine('case', keysOf(kConditionCases)) .combine('scope', ['module', 'function'] as const) .beginSubcases() + .combine('case', keysOf(kConditionCases)) ) .fn(t => { const expr = kConditionCases[t.params.case].expr; @@ -95,10 +95,10 @@ g.test('constant_expression_logical_or_no_assert') ) .params(u => u - .combine('lhs', keysOf(kConditionCases)) - .combine('rhs', keysOf(kConditionCases)) .combine('scope', ['module', 'function'] as const) .beginSubcases() + .combine('lhs', keysOf(kConditionCases)) + .combine('rhs', keysOf(kConditionCases)) ) .fn(t => { const expr = `(${kConditionCases[t.params.lhs].expr}) || (${ @@ -117,10 +117,10 @@ g.test('constant_expression_logical_or_assert') ) .params(u => u - .combine('lhs', keysOf(kConditionCases)) - .combine('rhs', keysOf(kConditionCases)) .combine('scope', ['module', 'function'] as const) .beginSubcases() + .combine('lhs', keysOf(kConditionCases)) + .combine('rhs', keysOf(kConditionCases)) ) .fn(t => { const expr = `(${kConditionCases[t.params.lhs].expr}) || (${ @@ -139,10 +139,10 @@ g.test('constant_expression_logical_and_no_assert') ) .params(u => u - .combine('lhs', keysOf(kConditionCases)) - .combine('rhs', keysOf(kConditionCases)) .combine('scope', ['module', 'function'] as const) .beginSubcases() + .combine('lhs', keysOf(kConditionCases)) + .combine('rhs', keysOf(kConditionCases)) ) .fn(t => { const expr = `(${kConditionCases[t.params.lhs].expr}) && (${ @@ -161,10 +161,10 @@ g.test('constant_expression_logical_and_assert') ) .params(u => u - .combine('lhs', keysOf(kConditionCases)) - .combine('rhs', keysOf(kConditionCases)) .combine('scope', ['module', 'function'] as const) .beginSubcases() + .combine('lhs', keysOf(kConditionCases)) + .combine('rhs', keysOf(kConditionCases)) ) .fn(t => { const expr = `(${kConditionCases[t.params.lhs].expr}) && (${ diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/compound_statement.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/compound_statement.spec.ts new file mode 100644 index 0000000000..8ad89f48a8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/compound_statement.spec.ts @@ -0,0 +1,98 @@ +export const description = ` +Validation tests for declarations in compound statements. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// 9.1 Compound Statements +// When a declaration is one of those statements, its identifier is in scope from +// the start of the next statement until the end of the compound statement. +// +// Enumerating cases: Consider a declaration X inside a compound statement. +// The X declaration should be tested with potential uses and potentially +// conflicting declarations in positions [a, b, c, d, e], in the following: +// a { b; X; c; { d; } } e; + +const kConflictTests = { + a: { + src: 'let x = 1; { let x = 1; }', + pass: true, + }, + bc: { + src: '{let x = 1; let x = 1; }', + pass: false, + }, + d: { + src: '{let x = 1; { let x = 1; }}', + pass: true, + }, + e: { + src: '{let x = 1; } let x = 1;', + pass: true, + }, +}; + +g.test('decl_conflict') + .desc( + 'Test a potentially conflicting declaration relative to a declaration in a compound statement' + ) + .params(u => u.combine('case', keysOf(kConflictTests))) + .fn(t => { + const wgsl = ` +@vertex fn vtx() -> @builtin(position) vec4f { + ${kConflictTests[t.params.case].src} + return vec4f(1); +}`; + t.expectCompileResult(kConflictTests[t.params.case].pass, wgsl); + }); + +const kUseTests = { + a: { + src: 'let y = x; { let x = 1; }', + pass: false, // not visible + }, + b: { + src: '{ let y = x; let x = 1; }', + pass: false, // not visible + }, + self: { + src: '{ let x = (x);}', + pass: false, // not visible + }, + c_yes: { + src: '{ const x = 1; const_assert x == 1; }', + pass: true, + }, + c_no: { + src: '{ const x = 1; const_assert x == 2; }', + pass: false, + }, + d_yes: { + src: '{ const x = 1; { const_assert x == 1; }}', + pass: true, + }, + d_no: { + src: '{ const x = 1; { const_assert x == 2; }}', + pass: false, + }, + e: { + src: '{ const x = 1; } let y = x;', + pass: false, // not visible + }, +}; + +g.test('decl_use') + .desc('Test a use of a declaration in a compound statement') + .params(u => u.combine('case', keysOf(kUseTests))) + .fn(t => { + const wgsl = ` +@vertex fn vtx() -> @builtin(position) vec4f { + ${kUseTests[t.params.case].src} + return vec4f(1); +}`; + t.expectCompileResult(kUseTests[t.params.case].pass, wgsl); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/const.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/const.spec.ts index 6ded2480c7..38ac76fa04 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/const.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/const.spec.ts @@ -3,6 +3,7 @@ Validation tests for const declarations `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; import { ShaderValidationTest } from '../shader_validation_test.js'; export const g = makeTestGroup(ShaderValidationTest); @@ -59,3 +60,160 @@ const b = S(4).a; `; t.expectCompileResult(t.params.target === 'a', wgsl); }); + +const kTypeCases = { + bool: { + code: `const x : bool = true;`, + valid: true, + }, + i32: { + code: `const x : i32 = 1i;`, + valid: true, + }, + u32: { + code: `const x : u32 = 1u;`, + valid: true, + }, + f32: { + code: `const x : f32 = 1f;`, + valid: true, + }, + f16: { + code: `enable f16;\nconst x : f16 = 1h;`, + valid: true, + }, + abstract_int: { + code: ` + const x = 0xffffffffff; + const_assert x == 0xffffffffff;`, + valid: true, + }, + abstract_float: { + code: ` + const x = 3937509.87755102; + const_assert x != 3937510.0; + const_assert x != 3937509.75;`, + valid: true, + }, + vec2i: { + code: `const x : vec2i = vec2i();`, + valid: true, + }, + vec3u: { + code: `const x : vec3u = vec3u();`, + valid: true, + }, + vec4f: { + code: `const x : vec4f = vec4f();`, + valid: true, + }, + mat2x2: { + code: `const x : mat2x2f = mat2x2f();`, + valid: true, + }, + mat4x3f: { + code: `const x : mat4x3<f32> = mat4x3<f32>();`, + valid: true, + }, + array_sized: { + code: `const x : array<u32, 4> = array(1,2,3,4);`, + valid: true, + }, + array_runtime: { + code: `const x : array<u32> = array(1,2,3);`, + valid: false, + }, + struct: { + code: `struct S { x : u32 }\nconst x : S = S(0);`, + valid: true, + }, + atomic: { + code: `const x : atomic<u32> = 0;`, + valid: false, + }, + vec_abstract_int: { + code: ` + const x = vec2(0xffffffffff,0xfffffffff0); + const_assert x.x == 0xffffffffff; + const_assert x.y == 0xfffffffff0;`, + valid: true, + }, + array_abstract_int: { + code: ` + const x = array(0xffffffffff,0xfffffffff0); + const_assert x[0] == 0xffffffffff; + const_assert x[1] == 0xfffffffff0;`, + valid: true, + }, +}; + +g.test('type') + .desc('Test const types') + .params(u => u.combine('case', keysOf(kTypeCases))) + .beforeAllSubcases(t => { + if (t.params.case === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const testcase = kTypeCases[t.params.case]; + const code = testcase.code; + const expect = testcase.valid; + t.expectCompileResult(expect, code); + }); + +const kInitCases = { + no_init: { + code: `const x : u32;`, + valid: false, + }, + no_type: { + code: `const x = 0;`, + valid: true, + }, + init_matching_type: { + code: `const x : i32 = 1i;`, + valid: true, + }, + init_mismatch_type: { + code: `const x : u32 = 1i;`, + valid: false, + }, + abs_int_init_convert: { + code: `const x : u32 = 1;`, + valid: true, + }, + abs_float_init_convert: { + code: `const x : f32 = 1.0;`, + valid: true, + }, + init_const_expr: { + code: `const x = 0;\nconst y = x + 2;`, + valid: true, + }, + init_override_expr: { + code: `override x : u32;\nconst y = x * 2;`, + valid: false, + }, + init_runtime_expr: { + code: `var<private> x = 1i;\nconst y = x - 1;`, + valid: false, + }, +}; + +g.test('initializer') + .desc('Test const initializers') + .params(u => u.combine('case', keysOf(kInitCases))) + .fn(t => { + const testcase = kInitCases[t.params.case]; + const code = testcase.code; + const expect = testcase.valid; + t.expectCompileResult(expect, code); + }); + +g.test('function_scope') + .desc('Test that const declarations are allowed in functions') + .fn(t => { + const code = `fn foo() { const x = 0; }`; + t.expectCompileResult(true, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/context_dependent_resolution.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/context_dependent_resolution.spec.ts new file mode 100644 index 0000000000..aedef043cd --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/context_dependent_resolution.spec.ts @@ -0,0 +1,338 @@ +export const description = ` +Tests that context dependent names do not participate in name resolution. +That is, a declaration named the same as a context dependent name will not interfere. + +Context-dependent names: + * Attribute names + * Built-in value names + * Diagnostic severity control + * Diagnostic triggering rules + * Enable extensions + * Language extensions + * Swizzles + * Interpolation type + * Interpolation sampling +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kAttributeCases = { + align: `struct S { @align(16) x : u32 }`, + binding: `@group(0) @binding(0) var s : sampler;`, + builtin: `@vertex fn main() -> @builtin(position) vec4f { return vec4f(); }`, + // const is not writable + // diagnostic is a keyword + group: `@group(0) @binding(0) var s : sampler;`, + id: `@id(1) override x : i32;`, + interpolate: `@fragment fn main(@location(0) @interpolate(flat) x : i32) { }`, + invariant: `@fragment fn main(@builtin(position) @invariant pos : vec4f) { }`, + location: `@fragment fn main(@location(0) x : f32) { }`, + must_use: `@must_use fn foo() -> u32 { return 0; }`, + size: `struct S { @size(4) x : u32 }`, + workgroup_size: `@compute @workgroup_size(1) fn main() { }`, + compute: `@compute @workgroup_size(1) fn main() { }`, + fragment: `@fragment fn main() { }`, + vertex: `@vertex fn main() -> @builtin(position) vec4f { return vec4f(); }`, +}; + +g.test('attribute_names') + .desc('Tests attribute names do not use name resolution') + .params(u => + u + .combine('case', keysOf(kAttributeCases)) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + const code = ` + ${t.params.decl} ${t.params.case} : u32 = 0; + ${kAttributeCases[t.params.case]} + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); + +const kBuiltinCases = { + vertex_index: ` + @vertex + fn main(@builtin(vertex_index) idx : u32) -> @builtin(position) vec4f + { return vec4f(); }`, + instance_index: ` + @vertex + fn main(@builtin(instance_index) idx : u32) -> @builtin(position) vec4f + { return vec4f(); }`, + position_vertex: ` + @vertex fn main() -> @builtin(position) vec4f + { return vec4f(); }`, + position_fragment: `@fragment fn main(@builtin(position) pos : vec4f) { }`, + front_facing: `@fragment fn main(@builtin(front_facing) x : bool) { }`, + frag_depth: `@fragment fn main() -> @builtin(frag_depth) f32 { return 0; }`, + sample_index: `@fragment fn main(@builtin(sample_index) x : u32) { }`, + sample_mask_input: `@fragment fn main(@builtin(sample_mask) x : u32) { }`, + sample_mask_output: `@fragment fn main() -> @builtin(sample_mask) u32 { return 0; }`, + local_invocation_id: ` + @compute @workgroup_size(1) + fn main(@builtin(local_invocation_id) id : vec3u) { }`, + local_invocation_index: ` + @compute @workgroup_size(1) + fn main(@builtin(local_invocation_index) id : u32) { }`, + global_invocation_id: ` + @compute @workgroup_size(1) + fn main(@builtin(global_invocation_id) id : vec3u) { }`, + workgroup_id: ` + @compute @workgroup_size(1) + fn main(@builtin(workgroup_id) id : vec3u) { }`, + num_workgroups: ` + @compute @workgroup_size(1) + fn main(@builtin(num_workgroups) id : vec3u) { }`, +}; + +g.test('builtin_value_names') + .desc('Tests builtin value names do not use name resolution') + .params(u => + u + .combine('case', keysOf(kBuiltinCases)) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + const code = ` + ${t.params.decl} ${t.params.case} : u32 = 0; + ${kBuiltinCases[t.params.case]} + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); + +const kDiagnosticSeverityCases = { + error: ` + diagnostic(error, derivative_uniformity); + @diagnostic(error, derivative_uniformity) fn foo() { } + `, + warning: ` + diagnostic(warning, derivative_uniformity); + @diagnostic(warning, derivative_uniformity) fn foo() { } + `, + off: ` + diagnostic(off, derivative_uniformity); + @diagnostic(off, derivative_uniformity) fn foo() { } + `, + info: ` + diagnostic(info, derivative_uniformity); + @diagnostic(info, derivative_uniformity) fn foo() { } + `, +}; + +g.test('diagnostic_severity_names') + .desc('Tests diagnostic severity names do not use name resolution') + .params(u => + u + .combine('case', keysOf(kDiagnosticSeverityCases)) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + const code = ` + ${kDiagnosticSeverityCases[t.params.case]} + ${t.params.decl} ${t.params.case} : u32 = 0; + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); + +const kDiagnosticRuleCases = { + derivative_uniformity: ` + diagnostic(off, derivative_uniformity); + @diagnostic(warning, derivative_uniformity) fn foo() { }`, + unknown_rule: ` + diagnostic(off, unknown_rule); + @diagnostic(warning, unknown_rule) fn foo() { }`, + unknown: ` + diagnostic(off, unknown.rule); + @diagnostic(warning, unknown.rule) fn foo() { }`, + rule: ` + diagnostic(off, unknown.rule); + @diagnostic(warning, unknown.rule) fn foo() { }`, +}; + +g.test('diagnostic_rule_names') + .desc('Tests diagnostic rule names do not use name resolution') + .params(u => + u + .combine('case', keysOf(kDiagnosticRuleCases)) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + const code = ` + ${kDiagnosticRuleCases[t.params.case]} + ${t.params.decl} ${t.params.case} : u32 = 0; + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); + +const kEnableCases = { + f16: `enable f16;`, +}; + +g.test('enable_names') + .desc('Tests enable extension names do not use name resolution') + .params(u => + u + .combine('case', keysOf(kEnableCases)) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase('shader-f16'); + }) + .fn(t => { + const code = ` + ${kEnableCases[t.params.case]} + ${t.params.decl} ${t.params.case} : u32 = 0; + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); + +const kLanguageCases = { + readonly_and_readwrite_storage_textures: `requires readonly_and_readwrite_storage_textures;`, + packed_4x8_integer_dot_product: `requires packed_4x8_integer_dot_product;`, + unrestricted_pointer_parameters: `requires unrestricted_pointer_parameters;`, + pointer_composite_access: `requires pointer_composite_access;`, +}; + +g.test('language_names') + .desc('Tests language extension names do not use name resolution') + .params(u => + u + .combine('case', keysOf(kLanguageCases)) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + t.skipIf(!t.hasLanguageFeature(t.params.case), 'Missing language feature'); + const code = ` + ${kLanguageCases[t.params.case]} + ${t.params.decl} ${t.params.case} : u32 = 0; + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); + +const kSwizzleCases = [ + 'x', + 'y', + 'z', + 'w', + 'xy', + 'yxz', + 'wxyz', + 'xyxy', + 'r', + 'g', + 'b', + 'a', + 'rgb', + 'arr', + 'bgra', + 'agra', +]; + +g.test('swizzle_names') + .desc('Tests swizzle names do not use name resolution') + .params(u => + u + .combine('case', kSwizzleCases) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + let code = `${t.params.decl} ${t.params.case} : u32 = 0;\n`; + if (t.params.case.length === 1) { + for (let i = 2; i <= 4; i++) { + code += `${t.params.decl} ${t.params.case.padEnd(i, t.params.case[0])} : u32 = 0;\n`; + } + } + code += `fn foo() { + var x : vec4f; + _ = x.${t.params.case}; + `; + if (t.params.case.length === 1) { + for (let i = 2; i <= 4; i++) { + code += `_ = x.${t.params.case.padEnd(i, t.params.case[0])};\n`; + } + } + code += `} + fn use_var() -> u32 { + return ${t.params.case}; + }`; + t.expectCompileResult(true, code); + }); + +const kInterpolationTypeCases = ['perspective', 'linear', 'flat']; + +g.test('interpolation_type_names') + .desc('Tests interpolation type names do not use name resolution') + .params(u => + u + .combine('case', kInterpolationTypeCases) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + const code = ` + ${t.params.decl} ${t.params.case} : u32 = 0; + @fragment fn main(@location(0) @interpolate(${t.params.case}) x : f32) { } + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); + +const kInterpolationSamplingCases = ['center', 'centroid', 'sample']; + +g.test('interpolation_sampling_names') + .desc('Tests interpolation type names do not use name resolution') + .params(u => + u + .combine('case', kInterpolationSamplingCases) + .beginSubcases() + .combine('decl', ['override', 'const', 'var<private>'] as const) + ) + .fn(t => { + const code = ` + ${t.params.decl} ${t.params.case} : u32 = 0; + @fragment fn main(@location(0) @interpolate(perspective, ${t.params.case}) x : f32) { } + fn use_var() -> u32 { + return ${t.params.case}; + } + `; + + t.expectCompileResult(true, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/let.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/let.spec.ts new file mode 100644 index 0000000000..0e116a0ef4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/let.spec.ts @@ -0,0 +1,180 @@ +export const description = ` +Validation tests for let declarations +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +interface Case { + code: string; + valid: boolean; + decls?: string; +} + +const kTypeCases: Record<string, Case> = { + bool: { + code: `let x : bool = true;`, + valid: true, + }, + i32: { + code: `let x : i32 = 1i;`, + valid: true, + }, + u32: { + code: `let x : u32 = 1u;`, + valid: true, + }, + f32: { + code: `let x : f32 = 1f;`, + valid: true, + }, + f16: { + code: `let x : f16 = 1h;`, + valid: true, + }, + vec2i: { + code: `let x : vec2i = vec2i();`, + valid: true, + }, + vec3u: { + code: `let x : vec3u = vec3u();`, + valid: true, + }, + vec4f: { + code: `let x : vec4f = vec4f();`, + valid: true, + }, + mat2x2: { + code: `let x : mat2x2f = mat2x2f();`, + valid: true, + }, + mat4x3f: { + code: `let x : mat4x3<f32> = mat4x3<f32>();`, + valid: true, + }, + array_sized: { + code: `let x : array<u32, 4> = array(1,2,3,4);`, + valid: true, + }, + array_runtime: { + code: `let x : array<u32> = array(1,2,3);`, + valid: false, + }, + struct: { + code: `let x : S = S(0);`, + valid: true, + decls: `struct S { x : u32 }`, + }, + atomic: { + code: `let x : atomic<u32> = 0;`, + valid: false, + }, + ptr_function: { + code: ` + var x : i32; + let y : ptr<function, i32> = &x;`, + valid: true, + }, + ptr_storage: { + code: `let y : ptr<storage, i32> = &x[0];`, + valid: true, + decls: `@group(0) @binding(0) var<storage> x : array<i32, 4>;`, + }, + load_rule: { + code: ` + var x : i32 = 1; + let y : i32 = x;`, + valid: true, + }, +}; + +g.test('type') + .desc('Test let types') + .params(u => u.combine('case', keysOf(kTypeCases))) + .beforeAllSubcases(t => { + if (t.params.case === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const testcase = kTypeCases[t.params.case]; + const code = ` +${t.params.case === 'f16' ? 'enable f16;' : ''} +${testcase.decls ?? ''} +fn foo() { + ${testcase.code} +}`; + const expect = testcase.valid; + t.expectCompileResult(expect, code); + }); + +const kInitCases: Record<string, Case> = { + no_init: { + code: `let x : u32;`, + valid: false, + }, + no_type: { + code: `let x = 1;`, + valid: true, + }, + init_matching_type: { + code: `let x : u32 = 1u;`, + valid: true, + }, + init_mismatch_type: { + code: `let x : u32 = 1i;`, + valid: false, + }, + ptr_type_mismatch: { + code: `var x : i32;\nlet y : ptr<function, u32> = &x;`, + valid: false, + }, + ptr_access_mismatch: { + code: `let y : ptr<storage, u32, read> = &x;`, + valid: false, + decls: `@group(0) @binding(0) var<storage, read_write> x : u32;`, + }, + ptr_addrspace_mismatch: { + code: `let y = ptr<storage, u32> = &x;`, + valid: false, + decls: `@group(0) @binding(0) var<uniform> x : u32;`, + }, + init_const_expr: { + code: `let y = x * 2;`, + valid: true, + decls: `const x = 1;`, + }, + init_override_expr: { + code: `let y = x + 1;`, + valid: true, + decls: `override x = 1;`, + }, + init_runtime_expr: { + code: `var x = 1;\nlet y = x << 1;`, + valid: true, + }, +}; + +g.test('initializer') + .desc('Test let initializers') + .params(u => u.combine('case', keysOf(kInitCases))) + .fn(t => { + const testcase = kInitCases[t.params.case]; + const code = ` +${testcase.decls ?? ''} +fn foo() { + ${testcase.code} +}`; + const expect = testcase.valid; + t.expectCompileResult(expect, code); + }); + +g.test('module_scope') + .desc('Test that let declarations are disallowed module scope') + .fn(t => { + const code = `let x = 0;`; + t.expectCompileResult(false, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/override.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/override.spec.ts index 82a35a2f59..41dd1391b2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/override.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/override.spec.ts @@ -3,6 +3,7 @@ Validation tests for override declarations `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; import { ShaderValidationTest } from '../shader_validation_test.js'; export const g = makeTestGroup(ShaderValidationTest); @@ -29,3 +30,180 @@ override c : i32 = ${t.params.target}; `; t.expectCompileResult(t.params.target === 'a', wgsl); }); + +const kIdCases = { + min: { + code: `@id(0) override x = 1;`, + valid: true, + }, + max: { + code: `@id(65535) override x = 1;`, + valid: true, + }, + neg: { + code: `@id(-1) override x = 1;`, + valid: false, + }, + too_large: { + code: `@id(65536) override x = 1;`, + valid: false, + }, + duplicate: { + code: ` + @id(1) override x = 1; + @id(1) override y = 1;`, + valid: false, + }, +}; + +g.test('id') + .desc('Test id attributes') + .params(u => u.combine('case', keysOf(kIdCases))) + .fn(t => { + const testcase = kIdCases[t.params.case]; + const code = testcase.code; + const expect = testcase.valid; + t.expectCompileResult(expect, code); + }); + +const kTypeCases = { + bool: { + code: `override x : bool;`, + valid: true, + }, + i32: { + code: `override x : i32;`, + valid: true, + }, + u32: { + code: `override x : u32;`, + valid: true, + }, + f32: { + code: `override x : f32;`, + valid: true, + }, + f16: { + code: `enable f16;\noverride x : f16;`, + valid: true, + }, + abs_int_conversion: { + code: `override x = 1;`, + valid: true, + }, + abs_float_conversion: { + code: `override x = 1.0;`, + valid: true, + }, + vec2_bool: { + code: `override x : vec2<bool>;`, + valid: false, + }, + vec2i: { + code: `override x : vec2i;`, + valid: false, + }, + vec3u: { + code: `override x : vec3u;`, + valid: false, + }, + vec4f: { + code: `override x : vec4f;`, + valid: false, + }, + mat2x2f: { + code: `override x : mat2x2f;`, + valid: false, + }, + matrix: { + code: `override x : mat4x3<f32>;`, + valid: false, + }, + array: { + code: `override x : array<u32, 4>;`, + valid: false, + }, + struct: { + code: `struct S { x : u32 }\noverride x : S;`, + valid: false, + }, + atomic: { + code: `override x : atomic<u32>;`, + valid: false, + }, +}; + +g.test('type') + .desc('Test override types') + .params(u => u.combine('case', keysOf(kTypeCases))) + .beforeAllSubcases(t => { + if (t.params.case === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const testcase = kTypeCases[t.params.case]; + const code = testcase.code; + const expect = testcase.valid; + t.expectCompileResult(expect, code); + }); + +const kInitCases = { + no_init_no_type: { + code: `override x;`, + valid: false, + }, + no_init: { + code: `override x : u32;`, + valid: true, + }, + no_type: { + code: `override x = 1;`, + valid: true, + }, + init_matching_type: { + code: `override x : u32 = 1u;`, + valid: true, + }, + init_mismatch_type: { + code: `override x : u32 = 1i;`, + valid: false, + }, + abs_int_init_convert: { + code: `override x : f32 = 1;`, + valid: true, + }, + abs_float_init_convert: { + code: `override x : f32 = 1.0;`, + valid: true, + }, + init_const_expr: { + code: `const x = 1;\noverride y = 2 * x;`, + valid: true, + }, + init_override_expr: { + code: `override x = 1;\noverride y = x + 2;`, + valid: true, + }, + init_runtime_expr: { + code: `var<private> x = 2;\noverride y = x;`, + valid: false, + }, +}; + +g.test('initializer') + .desc('Test override initializers') + .params(u => u.combine('case', keysOf(kInitCases))) + .fn(t => { + const testcase = kInitCases[t.params.case]; + const code = testcase.code; + const expect = testcase.valid; + t.expectCompileResult(expect, code); + }); + +g.test('function_scope') + .desc('Test that override declarations are disallowed in functions') + .fn(t => { + const code = `fn foo() { override x : u32; }`; + t.expectCompileResult(false, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/var.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/var.spec.ts new file mode 100644 index 0000000000..95e7417d23 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/decl/var.spec.ts @@ -0,0 +1,529 @@ +export const description = ` +Validation tests for host-shareable types. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// The set of types and their properties. +const kTypes = { + // Scalars. + bool: { + isHostShareable: false, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + i32: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + u32: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + f32: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + f16: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: true, + }, + + // Vectors. + 'vec2<bool>': { + isHostShareable: false, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + vec3i: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + vec4u: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + vec2f: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + vec3h: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: true, + }, + + // Matrices. + mat2x2f: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + mat3x4h: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: true, + }, + + // Atomics. + 'atomic<i32>': { + isHostShareable: true, + isConstructible: false, + isFixedFootprint: true, + requiresF16: false, + }, + 'atomic<u32>': { + isHostShareable: true, + isConstructible: false, + isFixedFootprint: true, + requiresF16: false, + }, + + // Arrays. + 'array<vec4<bool>>': { + isHostShareable: false, + isConstructible: false, + isFixedFootprint: false, + requiresF16: false, + }, + 'array<vec4<bool>, 4>': { + isHostShareable: false, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + 'array<vec4u>': { + isHostShareable: true, + isConstructible: false, + isFixedFootprint: false, + requiresF16: false, + }, + 'array<vec4u, 4>': { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + 'array<vec4u, array_size_const>': { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + 'array<vec4u, array_size_override>': { + isHostShareable: false, + isConstructible: false, + isFixedFootprint: true, + requiresF16: false, + }, + + // Structures. + S_u32: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + S_bool: { + isHostShareable: false, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + S_S_bool: { + isHostShareable: false, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + S_array_vec4u: { + isHostShareable: true, + isConstructible: false, + isFixedFootprint: false, + requiresF16: false, + }, + S_array_vec4u_4: { + isHostShareable: true, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + S_array_bool_4: { + isHostShareable: false, + isConstructible: true, + isFixedFootprint: true, + requiresF16: false, + }, + + // Misc. + 'ptr<function, u32>': { + isHostShareable: false, + isConstructible: false, + isFixedFootprint: false, + requiresF16: false, + }, + sampler: { + isHostShareable: false, + isConstructible: false, + isFixedFootprint: false, + requiresF16: false, + }, + 'texture_2d<f32>': { + isHostShareable: false, + isConstructible: false, + isFixedFootprint: false, + requiresF16: false, + }, +}; + +g.test('module_scope_types') + .desc('Test that only types that are allowed for a given address space are accepted.') + .params(u => + u + .combine('type', keysOf(kTypes)) + .combine('kind', [ + 'comment', + 'handle', + 'private', + 'storage_ro', + 'storage_rw', + 'uniform', + 'workgroup', + ]) + .combine('via_alias', [false, true]) + ) + .beforeAllSubcases(t => { + if (kTypes[t.params.type].requiresF16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kTypes[t.params.type]; + const isAtomic = t.params.type.indexOf('atomic') > -1; + + let decl = '<>'; + let shouldPass = false; + switch (t.params.kind) { + case 'comment': + // Control case to make sure all types are spelled correctly. + // We always emit an alias to the target type. + decl = '// '; + shouldPass = true; + break; + case 'handle': + decl = '@group(0) @binding(0) var foo : '; + shouldPass = t.params.type.indexOf('texture') > -1 || t.params.type.indexOf('sampler') > -1; + break; + case 'private': + decl = 'var<private> foo : '; + shouldPass = type.isConstructible; + break; + case 'storage_ro': + decl = '@group(0) @binding(0) var<storage, read> foo : '; + shouldPass = type.isHostShareable && !isAtomic; + break; + case 'storage_rw': + decl = '@group(0) @binding(0) var<storage, read_write> foo : '; + shouldPass = type.isHostShareable; + break; + case 'uniform': + decl = '@group(0) @binding(0) var<uniform> foo : '; + shouldPass = type.isHostShareable && type.isConstructible; + break; + case 'workgroup': + decl = 'var<workgroup> foo : '; + shouldPass = type.isFixedFootprint; + break; + } + + const wgsl = `${type.requiresF16 ? 'enable f16;' : ''} + const array_size_const = 4; + override array_size_override = 4; + + struct S_u32 { a : u32 } + struct S_bool { a : bool } + struct S_S_bool { a : S_bool } + struct S_array_vec4u { a : array<u32> } + struct S_array_vec4u_4 { a : array<vec4u, 4> } + struct S_array_bool_4 { a : array<bool, 4> } + + alias MyType = ${t.params.type}; + + ${decl} ${t.params.via_alias ? 'MyType' : t.params.type}; + `; + + t.expectCompileResult(shouldPass, wgsl); + }); + +g.test('function_scope_types') + .desc('Test that only types that are allowed for a given address space are accepted.') + .params(u => + u + .combine('type', keysOf(kTypes)) + .combine('kind', ['comment', 'var']) + .combine('via_alias', [false, true]) + ) + .beforeAllSubcases(t => { + if (kTypes[t.params.type].requiresF16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kTypes[t.params.type]; + + let decl = '<>'; + let shouldPass = false; + switch (t.params.kind) { + case 'comment': + // Control case to make sure all types are spelled correctly. + // We always emit an alias to the target type. + decl = '// '; + shouldPass = true; + break; + case 'var': + decl = 'var foo : '; + shouldPass = type.isConstructible; + break; + } + + const wgsl = `${type.requiresF16 ? 'enable f16;' : ''} + const array_size_const = 4; + override array_size_override = 4; + + struct S_u32 { a : u32 } + struct S_bool { a : bool } + struct S_S_bool { a : S_bool } + struct S_array_vec4u { a : array<u32> } + struct S_array_vec4u_4 { a : array<vec4u, 4> } + struct S_array_bool_4 { a : array<bool, 4> } + + alias MyType = ${t.params.type}; + + fn foo() { + ${decl} ${t.params.via_alias ? 'MyType' : t.params.type}; + }`; + + t.expectCompileResult(shouldPass, wgsl); + }); + +g.test('module_scope_initializers') + .desc('Test that initializers are only supported on address spaces that allow them.') + .params(u => + u + .combine('initializer', [false, true]) + .combine('kind', ['private', 'storage_ro', 'storage_rw', 'uniform', 'workgroup']) + ) + .fn(t => { + let decl = '<>'; + switch (t.params.kind) { + case 'private': + decl = 'var<private> foo : '; + break; + case 'storage_ro': + decl = '@group(0) @binding(0) var<storage, read> foo : '; + break; + case 'storage_rw': + decl = '@group(0) @binding(0) var<storage, read_write> foo : '; + break; + case 'uniform': + decl = '@group(0) @binding(0) var<uniform> foo : '; + break; + case 'workgroup': + decl = 'var<workgroup> foo : '; + break; + } + + const wgsl = `${decl} u32${t.params.initializer ? ' = 42u' : ''};`; + t.expectCompileResult(t.params.kind === 'private' || !t.params.initializer, wgsl); + }); + +g.test('handle_initializer') + .desc('Test that initializers are not allowed for handle types') + .params(u => + u.combine('initializer', [false, true]).combine('type', ['sampler', 'texture_2d<f32>']) + ) + .fn(t => { + const wgsl = ` + @group(0) @binding(0) var foo : ${t.params.type}; + @group(0) @binding(1) var bar : ${t.params.type}${t.params.initializer ? ' = foo' : ''};`; + t.expectCompileResult(!t.params.initializer, wgsl); + }); + +// A list of u32 initializers and their validity for the private address space. +const kInitializers = { + 'u32()': true, + '42u': true, + 'u32(sqrt(42.0))': true, + 'user_func()': false, + my_const_42u: true, + my_override_42u: true, + another_private_var: false, + 'vec4u(1, 2, 3, 4)[my_const_42u / 20]': true, + 'vec4u(1, 2, 3, 4)[my_override_42u / 20]': true, + 'vec4u(1, 2, 3, 4)[another_private_var / 20]': false, +}; + +g.test('initializer_kind') + .desc( + 'Test that initializers must be const-expression or override-expression for the private address space.' + ) + .params(u => + u.combine('initializer', keysOf(kInitializers)).combine('addrspace', ['private', 'function']) + ) + .fn(t => { + let wgsl = ` + const my_const_42u = 42u; + override my_override_42u : u32; + var<private> another_private_var = 42u; + + fn user_func() -> u32 { + return 42u; + } + `; + + if (t.params.addrspace === 'private') { + wgsl += ` + var<private> foo : u32 = ${t.params.initializer};`; + } else { + wgsl += ` + fn foo() { + var bar : u32 = ${t.params.initializer}; + }`; + } + t.expectCompileResult( + t.params.addrspace === 'function' || kInitializers[t.params.initializer], + wgsl + ); + }); + +g.test('function_addrspace_at_module_scope') + .desc('Test that the function address space is not allowed at module scope.') + .params(u => u.combine('addrspace', ['private', 'function'])) + .fn(t => { + t.expectCompileResult( + t.params.addrspace === 'private', + `var<${t.params.addrspace}> foo : i32;` + ); + }); + +// A list of resource variable declarations. +const kResourceDecls = { + uniform: 'var<uniform> buffer : vec4f;', + storage: 'var<storage> buffer : vec4f;', + texture: 'var t : texture_2d<f32>;', + sampler: 'var s : sampler;', +}; + +g.test('binding_point_on_resources') + .desc('Test that resource variables must have both @group and @binding attributes.') + .params(u => + u + .combine('decl', keysOf(kResourceDecls)) + .combine('group', ['', '@group(0)']) + .combine('binding', ['', '@binding(0)']) + ) + .fn(t => { + const shouldPass = t.params.group !== '' && t.params.binding !== ''; + const wgsl = `${t.params.group} ${t.params.binding} ${kResourceDecls[t.params.decl]}`; + t.expectCompileResult(shouldPass, wgsl); + }); + +g.test('binding_point_on_non_resources') + .desc('Test that non-resource variables cannot have either @group or @binding attributes.') + .params(u => + u + .combine('addrspace', ['private', 'workgroup']) + .combine('group', ['', '@group(0)']) + .combine('binding', ['', '@binding(0)']) + ) + .fn(t => { + const shouldPass = t.params.group === '' && t.params.binding === ''; + const wgsl = `${t.params.group} ${t.params.binding} var<${t.params.addrspace}> foo : i32;`; + t.expectCompileResult(shouldPass, wgsl); + }); + +g.test('binding_point_on_function_var') + .desc('Test that function variables cannot have either @group or @binding attributes.') + .params(u => u.combine('group', ['', '@group(0)']).combine('binding', ['', '@binding(0)'])) + .fn(t => { + const shouldPass = t.params.group === '' && t.params.binding === ''; + const wgsl = ` + fn foo() { + ${t.params.group} ${t.params.binding} var bar : i32; + }`; + t.expectCompileResult(shouldPass, wgsl); + }); + +g.test('binding_collisions') + .desc('Test that binding points can collide iff they are not used by the same entry point.') + .params(u => + u + .combine('a_group', [0, 1]) + .combine('b_group', [0, 1]) + .combine('a_binding', [0, 1]) + .combine('b_binding', [0, 1]) + .combine('b_use', ['same', 'different']) + ) + .fn(t => { + const wgsl = ` + @group(${t.params.a_group}) @binding(${t.params.a_binding}) var<uniform> a : vec4f; + @group(${t.params.b_group}) @binding(${t.params.b_binding}) var<uniform> b : vec4f; + + @fragment + fn main1() { + _ = a; + ${ + t.params.b_use === 'same' + ? '' + : ` + } + + @fragment + fn main2() {` + } + _ = b; + }`; + + const collision = + t.params.a_group === t.params.b_group && t.params.a_binding === t.params.b_binding; + const shouldFail = collision && t.params.b_use === 'same'; + t.expectCompileResult(!shouldFail, wgsl); + }); + +g.test('binding_collision_unused_helper') + .desc('Test that binding points can collide in an unused helper function.') + .fn(t => { + const wgsl = ` + @group(0) @binding(0) var<uniform> a : vec4f; + @group(0) @binding(0) var<uniform> b : vec4f; + + fn foo() { + _ = a; + _ = b; + }`; + + t.expectCompileResult(true, wgsl); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/access/vector.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/access/vector.spec.ts index 0294fc2d56..766adc0fb8 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/access/vector.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/access/vector.spec.ts @@ -178,10 +178,11 @@ g.test('vector') .desc('Tests validation of vector indexed and swizzles') .params(u => u - .combine('case', keysOf(kCases)) // .combine('vector_decl', ['const', 'let', 'var', 'param'] as const) .combine('vector_width', [2, 3, 4] as const) .combine('element_type', ['i32', 'u32', 'f32', 'f16', 'bool'] as const) + .beginSubcases() + .combine('case', keysOf(kCases)) ) .beforeAllSubcases(t => { if (t.params.element_type === 'f16') { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/add_sub_mul.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/add_sub_mul.spec.ts new file mode 100644 index 0000000000..3755eb7386 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/add_sub_mul.spec.ts @@ -0,0 +1,320 @@ +export const description = ` +Validation tests for add/sub/mul expressions. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { assert } from '../../../../../common/util/util.js'; +import { kBit } from '../../../../util/constants.js'; +import { + kAllScalarsAndVectors, + kConcreteNumericScalarsAndVectors, + kConvertableToFloatScalar, + ScalarType, + scalarTypeOf, + Type, + Value, + VectorType, +} from '../../../../util/conversion.js'; +import { nextAfterF16, nextAfterF32 } from '../../../../util/math.js'; +import { reinterpretU16AsF16, reinterpretU32AsF32 } from '../../../../util/reinterpret.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; +import { + kConstantAndOverrideStages, + validateConstOrOverrideBinaryOpEval, +} from '../call/builtin/const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// A list of operators tested in this file. +const kOperators = { + add: { op: '+' }, + sub: { op: '-' }, + mul: { op: '*' }, +}; + +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); +const kConcreteNumericScalarAndVectorTypes = objectsToRecord(kConcreteNumericScalarsAndVectors); + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector expressions are only accepted for compatible numeric types. + ` + ) + .params(u => + u + .combine('lhs', keysOf(kScalarAndVectorTypes)) + .combine( + 'rhs', + // Skip vec3 and vec4 on the RHS to keep the number of subcases down. + // vec3 + vec3 and vec4 + vec4 is tested in execution tests. + keysOf(kScalarAndVectorTypes).filter( + value => !(value.startsWith('vec3') || value.startsWith('vec4')) + ) + ) + .beginSubcases() + .combine('op', keysOf(kOperators)) + ) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kScalarAndVectorTypes[t.params.lhs]) === Type.f16 || + scalarTypeOf(kScalarAndVectorTypes[t.params.rhs]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const op = kOperators[t.params.op]; + const lhs = kScalarAndVectorTypes[t.params.lhs]; + const rhs = kScalarAndVectorTypes[t.params.rhs]; + const lhsElement = scalarTypeOf(lhs); + const rhsElement = scalarTypeOf(rhs); + const hasF16 = lhsElement === Type.f16 || rhsElement === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const lhs = ${lhs.create(0).wgsl()}; +const rhs = ${rhs.create(0).wgsl()}; +const foo = lhs ${op.op} rhs; +`; + + let elementsCompatible = lhsElement === rhsElement; + const elementTypes = [lhsElement, rhsElement]; + + // Booleans are not allowed for arithmetic expressions. + if (elementTypes.includes(Type.bool)) { + elementsCompatible = false; + + // AbstractInt is allowed with everything but booleans which are already checked above. + } else if (elementTypes.includes(Type.abstractInt)) { + elementsCompatible = true; + + // AbstractFloat is allowed with AbstractInt (checked above) or float types + } else if (elementTypes.includes(Type.abstractFloat)) { + elementsCompatible = elementTypes.every(e => kConvertableToFloatScalar.includes(e)); + } + + // Determine if the full type is compatible. The only invalid case is mixed vector sizes. + let valid = elementsCompatible; + if (lhs instanceof VectorType && rhs instanceof VectorType) { + valid = valid && lhs.width === rhs.width; + } + + t.expectCompileResult(valid, code); + }); + +g.test('scalar_vector_out_of_range') + .desc( + ` + Checks that constant or override evaluation of add/sub/mul operations on scalar/vectors that produce out of range values cause validation errors. + - Checks for all concrete numeric scalar and vector types, including scalar * vector and vector * scalar. + - Checks for all vector elements that could cause the out of range to happen. + - Checks for pairs of values at one ULP difference at the boundary of the out of range. + ` + ) + .params(u => + u + .combine('op', keysOf(kOperators)) + .combine('lhs', keysOf(kConcreteNumericScalarAndVectorTypes)) + .expand('rhs', p => { + if (kScalarAndVectorTypes[p.lhs] instanceof VectorType) { + return [p.lhs, scalarTypeOf(kScalarAndVectorTypes[p.lhs]).toString()]; + } + return [p.lhs]; + }) + .beginSubcases() + .expand('swap', p => { + if (p.lhs === p.rhs) { + return [false]; + } + return [false, true]; + }) + .combine('nonZeroIndex', [0, 1, 2, 3]) + .filter(p => { + const lType = kScalarAndVectorTypes[p.lhs]; + if (lType instanceof VectorType) { + return lType.width > p.nonZeroIndex; + } + return p.nonZeroIndex === 0; + }) + .combine('valueCase', ['halfmax', 'halfmax+ulp', 'sqrtmax', 'sqrtmax+ulp'] as const) + .combine('stage', kConstantAndOverrideStages) + ) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kScalarAndVectorTypes[t.params.lhs]) === Type.f16 || + scalarTypeOf(kScalarAndVectorTypes[t.params.rhs]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const { op, valueCase, nonZeroIndex, swap } = t.params; + let { lhs, rhs } = t.params; + + const elementType = scalarTypeOf(kScalarAndVectorTypes[lhs]); + + // Handle the swapping of LHS and RHS to test all cases of scalar * vector. + if (swap) { + [rhs, lhs] = [lhs, rhs]; + } + + // What is the maximum representable value for the type? Also how do we add a ULP? + let maxValue = 0; + let nextAfter: (v: number) => number = v => v + 1; + let outOfRangeIsError = false; + switch (elementType) { + case Type.f16: + maxValue = reinterpretU16AsF16(kBit.f16.positive.max); + nextAfter = v => nextAfterF16(v, 'positive', 'no-flush'); + outOfRangeIsError = true; + break; + case Type.f32: + maxValue = reinterpretU32AsF32(kBit.f32.positive.max); + nextAfter = v => nextAfterF32(v, 'positive', 'no-flush'); + outOfRangeIsError = true; + break; + case Type.u32: + maxValue = kBit.u32.max; + break; + case Type.i32: + maxValue = kBit.i32.positive.max; + break; + } + + // Decide on the test value that may or may not do an out of range computation. + let value; + switch (valueCase) { + case 'halfmax': + value = Math.floor(maxValue / 2); + break; + case 'halfmax+ulp': + value = nextAfter(Math.ceil(maxValue / 2)); + break; + case 'sqrtmax': + value = Math.floor(Math.sqrt(maxValue)); + break; + case 'sqrtmax+ulp': + value = nextAfter(Math.ceil(Math.sqrt(maxValue))); + break; + } + + // What value will be computed by the test? + let computedValue; + let leftValue = value; + const rightValue = value; + switch (op) { + case 'add': + computedValue = value + value; + break; + case 'sub': + computedValue = -value - value; + leftValue = -value; + break; + case 'mul': + computedValue = value * value; + break; + } + + // Creates either a scalar with the value, or a vector with the value only at a specific index. + const create = (type: ScalarType | VectorType, index: number, value: number): Value => { + if (type instanceof ScalarType) { + return type.create(value); + } else { + assert(type instanceof VectorType); + const values = new Array(type.width); + values.fill(0); + values[index] = value; + return type.create(values); + } + }; + + // Check if there is overflow + const success = Math.abs(computedValue) <= maxValue || !outOfRangeIsError; + validateConstOrOverrideBinaryOpEval( + t, + kOperators[op].op, + success, + t.params.stage, + create(kScalarAndVectorTypes[lhs], nonZeroIndex, leftValue), + t.params.stage, + create(kScalarAndVectorTypes[rhs], nonZeroIndex, rightValue) + ); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid integer operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&u)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `atomicLoad(&${e})`, + }, + + texture: { + expr: 't', + control: e => `i32(textureLoad(${e}, vec2(), 0).x)`, + }, + + sampler: { + expr: 's', + control: e => `i32(textureSampleLevel(t, ${e}, vec2(), 0).x)`, + }, + + struct: { + expr: 'str', + control: e => `${e}.u`, + }, +}; + +g.test('invalid_type_with_itself') + .desc( + ` + Validates that expressions are never accepted for non-scalar, non-vector, and non-matrix types. + ` + ) + .params(u => + u + .combine('op', keysOf(kOperators)) + .combine('type', keysOf(kInvalidTypes)) + .combine('control', [true, false]) + .beginSubcases() + ) + .fn(t => { + const op = kOperators[t.params.op]; + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<i32>; + +struct S { u : u32 } + +var<private> u : u32; +var<private> m : mat2x2f; +var<private> arr : array<i32, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = ${expr} ${op.op} ${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/and_or_xor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/and_or_xor.spec.ts new file mode 100644 index 0000000000..3dd3607365 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/and_or_xor.spec.ts @@ -0,0 +1,182 @@ +export const description = ` +Validation tests for logical and bitwise and/or/xor expressions. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { + kAllScalarsAndVectors, + ScalarType, + scalarTypeOf, + Type, + VectorType, +} from '../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// A list of operators and a flag for whether they support boolean values or not. +const kOperators = { + and: { op: '&', supportsBool: true }, + or: { op: '|', supportsBool: true }, + xor: { op: '^', supportsBool: false }, +}; + +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector expressions are only accepted for bool or compatible integer types. + ` + ) + .params(u => + u + .combine('lhs', keysOf(kScalarAndVectorTypes)) + .combine( + 'rhs', + // Skip vec3 and vec4 on the RHS to keep the number of subcases down. + // vec3 + vec3 and vec4 + vec4 is tested in execution tests. + keysOf(kScalarAndVectorTypes).filter( + value => !(value.startsWith('vec3') || value.startsWith('vec4')) + ) + ) + .beginSubcases() + .combine('op', keysOf(kOperators)) + ) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kScalarAndVectorTypes[t.params.lhs]) === Type.f16 || + scalarTypeOf(kScalarAndVectorTypes[t.params.rhs]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const op = kOperators[t.params.op]; + const lhs = kScalarAndVectorTypes[t.params.lhs]; + const rhs = kScalarAndVectorTypes[t.params.rhs]; + const lhsElement = scalarTypeOf(lhs); + const rhsElement = scalarTypeOf(rhs); + const hasF16 = lhsElement === Type.f16 || rhsElement === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const lhs = ${lhs.create(0).wgsl()}; +const rhs = ${rhs.create(0).wgsl()}; +const foo = lhs ${op.op} rhs; +`; + + // Determine if the element types are compatible. + const kIntegerTypes = [Type.abstractInt, Type.i32, Type.u32]; + let elementIsCompatible = false; + if (lhsElement === Type.abstractInt) { + // Abstract integers are compatible with any other integer type. + elementIsCompatible = kIntegerTypes.includes(rhsElement); + } else if (rhsElement === Type.abstractInt) { + // Abstract integers are compatible with any other numeric type. + elementIsCompatible = kIntegerTypes.includes(lhsElement); + } else if (kIntegerTypes.includes(lhsElement)) { + // Concrete integers are only compatible with values with the exact same type. + elementIsCompatible = lhsElement === rhsElement; + } else if (lhsElement === Type.bool) { + // Booleans are only compatible with other booleans. + elementIsCompatible = rhsElement === Type.bool; + } + + // Determine if the full type is compatible. + let valid = false; + if (lhs instanceof ScalarType && rhs instanceof ScalarType) { + valid = elementIsCompatible; + } else if (lhs instanceof VectorType && rhs instanceof VectorType) { + // Vectors are only compatible with if the vector widths match. + valid = lhs.width === rhs.width && elementIsCompatible; + } + + if (lhsElement === Type.bool) { + valid &&= op.supportsBool; + } + + t.expectCompileResult(valid, code); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid integer operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + mat2x2f: { + expr: 'm', + control: e => `i32(${e}[0][0])`, + }, + + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&u)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `atomicLoad(&${e})`, + }, + + texture: { + expr: 't', + control: e => `i32(textureLoad(${e}, vec2(), 0).x)`, + }, + + sampler: { + expr: 's', + control: e => `i32(textureSampleLevel(t, ${e}, vec2(), 0).x)`, + }, + + struct: { + expr: 'str', + control: e => `${e}.u`, + }, +}; + +g.test('invalid_types') + .desc( + ` + Validates that expressions are never accepted for non-scalar and non-vector types. + ` + ) + .params(u => + u + .combine('op', keysOf(kOperators)) + .combine('type', keysOf(kInvalidTypes)) + .combine('control', [true, false]) + .beginSubcases() + ) + .fn(t => { + const op = kOperators[t.params.op]; + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<i32>; + +struct S { u : u32 } + +var<private> u : u32; +var<private> m : mat2x2f; +var<private> arr : array<i32, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = ${expr} ${op.op} ${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/bitwise_shift.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/bitwise_shift.spec.ts index 5f7b995ded..e3f13d6813 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/bitwise_shift.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/bitwise_shift.spec.ts @@ -3,6 +3,13 @@ Validation tests for the bitwise shift binary expression operations `; import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + numElementsOf, + scalarTypeOf, +} from '../../../../util/conversion.js'; import { ShaderValidationTest } from '../../shader_validation_test.js'; export const g = makeTestGroup(ShaderValidationTest); @@ -21,6 +28,137 @@ function vectorize(v: string, size: number | undefined): string { return v; } +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector expressions are only accepted when the LHS is an integer and the RHS is abstract or unsigned. + ` + ) + .params(u => + u + .combine('lhs', keysOf(kScalarAndVectorTypes)) + .combine( + 'rhs', + // Skip vec3 and vec4 on the RHS to keep the number of subcases down. + keysOf(kScalarAndVectorTypes).filter( + value => !(value.startsWith('vec3') || value.startsWith('vec4')) + ) + ) + .beginSubcases() + .combine('op', ['<<', '>>']) + ) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kScalarAndVectorTypes[t.params.lhs]) === Type.f16 || + scalarTypeOf(kScalarAndVectorTypes[t.params.rhs]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const lhs = kScalarAndVectorTypes[t.params.lhs]; + const rhs = kScalarAndVectorTypes[t.params.rhs]; + const lhsElement = scalarTypeOf(lhs); + const rhsElement = scalarTypeOf(rhs); + const hasF16 = lhsElement === Type.f16 || rhsElement === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const lhs = ${lhs.create(0).wgsl()}; +const rhs = ${rhs.create(0).wgsl()}; +const foo = lhs ${t.params.op} rhs; +`; + + // The LHS must be an integer, and the RHS must be an abstract/unsigned integer. + // The vector widths must also match. + const lhs_valid = [Type.abstractInt, Type.i32, Type.u32].includes(lhsElement); + const rhs_valid = [Type.abstractInt, Type.u32].includes(rhsElement); + const valid = lhs_valid && rhs_valid && numElementsOf(lhs) === numElementsOf(rhs); + t.expectCompileResult(valid, code); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid u32 operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + mat2x2f: { + expr: 'm', + control: e => `u32(${e}[0][0])`, + }, + + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&u)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `atomicLoad(&${e})`, + }, + + texture: { + expr: 't', + control: e => `u32(textureLoad(${e}, vec2(), 0).x)`, + }, + + sampler: { + expr: 's', + control: e => `u32(textureSampleLevel(t, ${e}, vec2(), 0).x)`, + }, + + struct: { + expr: 'str', + control: e => `${e}.u`, + }, +}; + +g.test('invalid_types') + .desc( + ` + Validates that expressions are never accepted for non-scalar and non-vector types. + ` + ) + .params(u => + u + .combine('op', ['<<', '>>']) + .combine('type', keysOf(kInvalidTypes)) + .combine('control', [true, false]) + .beginSubcases() + ) + .fn(t => { + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<u32>; + +struct S { u : u32 } + +var<private> u : u32; +var<private> m : mat2x2f; +var<private> arr : array<u32, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = ${expr} ${t.params.op} ${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); + const kLeftShiftCases = [ // rhs >= bitwidth fails { lhs: `0u`, rhs: `31u`, pass: true }, @@ -80,28 +218,6 @@ fn main() { t.expectCompileResult(t.params.case.pass, code); }); -g.test('shift_left_vec_size_mismatch') - .desc('Tests validation of binary left shift of vectors with mismatched sizes') - .params(u => - u - .combine('vectorize_lhs', [2, 3, 4]) // - .combine('vectorize_rhs', [2, 3, 4]) - ) - .fn(t => { - const lhs = `1`; - const rhs = `1`; - const lhs_vec_size = t.params.vectorize_lhs; - const rhs_vec_size = t.params.vectorize_rhs; - const code = ` -@compute @workgroup_size(1) -fn main() { - const r = ${vectorize(lhs, lhs_vec_size)} << ${vectorize(rhs, rhs_vec_size)}; -} - `; - const pass = lhs_vec_size === rhs_vec_size; - t.expectCompileResult(pass, code); - }); - const kRightShiftCases = [ // rhs >= bitwidth fails { lhs: `0u`, rhs: `31u`, pass: true }, @@ -142,25 +258,3 @@ fn main() { `; t.expectCompileResult(t.params.case.pass, code); }); - -g.test('shift_right_vec_size_mismatch') - .desc('Tests validation of binary right shift of vectors with mismatched sizes') - .params(u => - u - .combine('vectorize_lhs', [2, 3, 4]) // - .combine('vectorize_rhs', [2, 3, 4]) - ) - .fn(t => { - const lhs = `1`; - const rhs = `1`; - const lhs_vec_size = t.params.vectorize_lhs; - const rhs_vec_size = t.params.vectorize_rhs; - const code = ` -@compute @workgroup_size(1) -fn main() { - const r = ${vectorize(lhs, lhs_vec_size)} >> ${vectorize(rhs, rhs_vec_size)}; -} - `; - const pass = lhs_vec_size === rhs_vec_size; - t.expectCompileResult(pass, code); - }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/comparison.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/comparison.spec.ts new file mode 100644 index 0000000000..6115874a10 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/comparison.spec.ts @@ -0,0 +1,186 @@ +export const description = ` +Validation tests for comparison expressions. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { + isFloatType, + kAllScalarsAndVectors, + ScalarType, + scalarTypeOf, + Type, + VectorType, +} from '../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); + +// A list of comparison operators and a flag for whether they support boolean values or not. +const kComparisonOperators = { + eq: { op: '==', supportsBool: true }, + ne: { op: '!=', supportsBool: true }, + gt: { op: '>', supportsBool: false }, + ge: { op: '>=', supportsBool: false }, + lt: { op: '<', supportsBool: false }, + le: { op: '<=', supportsBool: false }, +}; + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector comparison expressions are only accepted for compatible types. + ` + ) + .params(u => + u + .combine('lhs', keysOf(kScalarAndVectorTypes)) + .combine( + 'rhs', + // Skip vec3 and vec4 on the RHS to keep the number of subcases down. + keysOf(kScalarAndVectorTypes).filter( + value => !(value.startsWith('vec3') || value.startsWith('vec4')) + ) + ) + .beginSubcases() + .combine('op', keysOf(kComparisonOperators)) + ) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kScalarAndVectorTypes[t.params.lhs]) === Type.f16 || + scalarTypeOf(kScalarAndVectorTypes[t.params.rhs]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const lhs = kScalarAndVectorTypes[t.params.lhs]; + const rhs = kScalarAndVectorTypes[t.params.rhs]; + const lhsElement = scalarTypeOf(lhs); + const rhsElement = scalarTypeOf(rhs); + const hasF16 = lhsElement === Type.f16 || rhsElement === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const lhs = ${lhs.create(0).wgsl()}; +const rhs = ${rhs.create(0).wgsl()}; +const foo = lhs ${kComparisonOperators[t.params.op].op} rhs; +`; + + let valid = false; + + // Determine if the element types are comparable. + let elementIsCompatible = false; + if (lhsElement === Type.abstractInt) { + // Abstract integers are comparable to any other numeric type. + elementIsCompatible = rhsElement !== Type.bool; + } else if (rhsElement === Type.abstractInt) { + // Abstract integers are comparable to any other numeric type. + elementIsCompatible = lhsElement !== Type.bool; + } else if (lhsElement === Type.abstractFloat) { + // Abstract floats are comparable to any other float type. + elementIsCompatible = isFloatType(rhsElement); + } else if (rhsElement === Type.abstractFloat) { + // Abstract floats are comparable to any other float type. + elementIsCompatible = isFloatType(lhsElement); + } else { + // Non-abstract types are only comparable to values with the exact same type. + elementIsCompatible = lhsElement === rhsElement; + } + + // Determine if the full type is comparable. + if (lhs instanceof ScalarType && rhs instanceof ScalarType) { + valid = elementIsCompatible; + } else if (lhs instanceof VectorType && rhs instanceof VectorType) { + // Vectors are only comparable if the vector widths match. + valid = lhs.width === rhs.width && elementIsCompatible; + } + + if (lhsElement === Type.bool) { + valid &&= kComparisonOperators[t.params.op].supportsBool; + } + + t.expectCompileResult(valid, code); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid comparison operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + mat2x2f: { + expr: 'm', + control: e => `${e}[0]`, + }, + + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&u)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `atomicLoad(&${e})`, + }, + + texture: { + expr: 't', + control: e => `textureLoad(${e}, vec2(), 0)`, + }, + + sampler: { + expr: 's', + control: e => `textureSampleLevel(t, ${e}, vec2(), 0)`, + }, + + struct: { + expr: 'str', + control: e => `${e}.u`, + }, +}; + +g.test('invalid_types') + .desc( + ` + Validates that comparison expressions are never accepted for non-scalar and non-vector types. + ` + ) + .params(u => + u + .combine('op', keysOf(kComparisonOperators)) + .combine('type', keysOf(kInvalidTypes)) + .combine('control', [true, false]) + .beginSubcases() + ) + .fn(t => { + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<i32>; + +struct S { u : u32 } + +var<private> u : u32; +var<private> m : mat2x2f; +var<private> arr : array<i32, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = ${expr} ${kComparisonOperators[t.params.op].op} ${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/div_rem.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/div_rem.spec.ts new file mode 100644 index 0000000000..0f07abe3ae --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/binary/div_rem.spec.ts @@ -0,0 +1,279 @@ +export const description = ` +Validation tests for division and remainder expressions. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { assert } from '../../../../../common/util/util.js'; +import { kBit } from '../../../../util/constants.js'; +import { + ScalarType, + Type, + Value, + VectorType, + kAllScalarsAndVectors, + kConcreteNumericScalarsAndVectors, + kConvertableToFloatScalar, + scalarTypeOf, +} from '../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; +import { + kConstantAndOverrideStages, + validateConstOrOverrideBinaryOpEval, +} from '../call/builtin/const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// A list of operators tested in this file. +const kOperators = { + div: { op: '/' }, + rem: { op: '%' }, +} as const; + +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); +const kConcreteNumericScalarAndVectorTypes = objectsToRecord(kConcreteNumericScalarsAndVectors); + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector expressions are only accepted for compatible numeric types. + ` + ) + .params(u => + u + .combine('lhs', keysOf(kScalarAndVectorTypes)) + .combine( + 'rhs', + // Skip vec3 and vec4 on the RHS to keep the number of subcases down. + // vec3 + vec3 and vec4 + vec4 is tested in execution tests. + keysOf(kScalarAndVectorTypes).filter( + value => !(value.startsWith('vec3') || value.startsWith('vec4')) + ) + ) + .beginSubcases() + .combine('op', keysOf(kOperators)) + ) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kScalarAndVectorTypes[t.params.lhs]) === Type.f16 || + scalarTypeOf(kScalarAndVectorTypes[t.params.rhs]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const op = kOperators[t.params.op]; + const lhs = kScalarAndVectorTypes[t.params.lhs]; + const rhs = kScalarAndVectorTypes[t.params.rhs]; + const lhsElement = scalarTypeOf(lhs); + const rhsElement = scalarTypeOf(rhs); + const hasF16 = lhsElement === Type.f16 || rhsElement === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const lhs = ${lhs.create(1).wgsl()}; +const rhs = ${rhs.create(1).wgsl()}; +const foo = lhs ${op.op} rhs; +`; + + let elementsCompatible = lhsElement === rhsElement; + const elementTypes = [lhsElement, rhsElement]; + + // Booleans are not allowed for arithmetic expressions. + if (elementTypes.includes(Type.bool)) { + elementsCompatible = false; + + // AbstractInt is allowed with everything but booleans which are already checked above. + } else if (elementTypes.includes(Type.abstractInt)) { + elementsCompatible = true; + + // AbstractFloat is allowed with AbstractInt (checked above) or float types + } else if (elementTypes.includes(Type.abstractFloat)) { + elementsCompatible = elementTypes.every(e => kConvertableToFloatScalar.includes(e)); + } + + // Determine if the full type is compatible. The only invalid case is mixed vector sizes. + let valid = elementsCompatible; + if (lhs instanceof VectorType && rhs instanceof VectorType) { + valid = valid && lhs.width === rhs.width; + } + + t.expectCompileResult(valid, code); + }); + +g.test('scalar_vector_out_of_range') + .desc( + ` + Checks that constant or override evaluation of div/rem operations on scalar/vectors that produce out of division by 0 or out of range values cause validation errors. + - Checks for all concrete numeric scalar and vector types, including scalar * vector and vector * scalar. + - Checks for all vector elements that could cause the out of range to happen. + - Checks for valid small cases and 0, also the minimum i32. + ` + ) + .params(u => + u + .combine('op', keysOf(kOperators)) + .combine('lhs', keysOf(kConcreteNumericScalarAndVectorTypes)) + .expand('rhs', p => { + if (kScalarAndVectorTypes[p.lhs] instanceof VectorType) { + return [p.lhs, scalarTypeOf(kScalarAndVectorTypes[p.lhs]).toString()]; + } + return [p.lhs]; + }) + .beginSubcases() + .expand('swap', p => { + if (p.lhs === p.rhs) { + return [false]; + } + return [false, true]; + }) + .combine('nonOneIndex', [0, 1, 2, 3]) + .filter(p => { + const lType = kScalarAndVectorTypes[p.lhs]; + if (lType instanceof VectorType) { + return lType.width > p.nonOneIndex; + } + return p.nonOneIndex === 0; + }) + .expandWithParams(p => { + const cases = [ + { leftValue: 42, rightValue: 0, error: true, leftRuntime: false }, + { leftValue: 42, rightValue: 0, error: true, leftRuntime: true }, + { leftValue: 0, rightValue: 0, error: true, leftRuntime: true }, + { leftValue: 0, rightValue: 42, error: false, leftRuntime: false }, + ]; + if (p.lhs === 'i32') { + cases.push({ + leftValue: -kBit.i32.negative.min, + rightValue: -1, + error: true, + leftRuntime: false, + }); + cases.push({ + leftValue: -kBit.i32.negative.min + 1, + rightValue: -1, + error: false, + leftRuntime: false, + }); + } + return cases; + }) + .combine('stage', kConstantAndOverrideStages) + ) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kScalarAndVectorTypes[t.params.lhs]) === Type.f16 || + scalarTypeOf(kScalarAndVectorTypes[t.params.rhs]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const { op, leftValue, rightValue, error, leftRuntime, nonOneIndex, swap } = t.params; + let { lhs, rhs } = t.params; + + // Handle the swapping of LHS and RHS to test all cases of scalar * vector. + if (swap) { + [rhs, lhs] = [lhs, rhs]; + } + + // Creates either a scalar with the value, or a vector with the value only at a specific index. + const create = (type: ScalarType | VectorType, index: number, value: number): Value => { + if (type instanceof ScalarType) { + return type.create(value); + } else { + assert(type instanceof VectorType); + const values = new Array(type.width); + values.fill(1); + values[index] = value; + return type.create(values); + } + }; + + // Check if there is overflow + validateConstOrOverrideBinaryOpEval( + t, + kOperators[op].op, + !error, + leftRuntime ? 'runtime' : t.params.stage, + create(kScalarAndVectorTypes[lhs], nonOneIndex, leftValue), + t.params.stage, + create(kScalarAndVectorTypes[rhs], nonOneIndex, rightValue) + ); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid integer operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&u)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `atomicLoad(&${e})`, + }, + + texture: { + expr: 't', + control: e => `i32(textureLoad(${e}, vec2(), 0).x)`, + }, + + sampler: { + expr: 's', + control: e => `i32(textureSampleLevel(t, ${e}, vec2(), 0).x)`, + }, + + struct: { + expr: 'str', + control: e => `${e}.u`, + }, +}; + +g.test('invalid_type_with_itself') + .desc( + ` + Validates that expressions are never accepted for non-scalar, non-vector, and non-matrix types. + ` + ) + .params(u => + u + .combine('op', keysOf(kOperators)) + .combine('type', keysOf(kInvalidTypes)) + .combine('control', [true, false]) + .beginSubcases() + ) + .fn(t => { + const op = kOperators[t.params.op]; + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<i32>; + +struct S { u : u32 } + +var<private> u : u32; +var<private> m : mat2x2f; +var<private> arr : array<i32, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = ${expr} ${op.op} ${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/abs.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/abs.spec.ts index 32ecb0cbc8..2b0375d783 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/abs.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/abs.spec.ts @@ -6,9 +6,9 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - elementType, - kAllFloatAndIntegerScalarsAndVectors, + Type, + kAllNumericScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -21,7 +21,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatAndIntegerScalarsAndVectors); +const kValuesTypes = objectsToRecord(kAllNumericScalarsAndVectors); g.test('values') .desc( @@ -38,7 +38,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() never .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -52,3 +52,107 @@ Validates that constant evaluation and override evaluation of ${builtin}() never t.params.stage ); }); + +const kTests = { + valid: { + src: `_ = abs(1);`, + pass: true, + }, + alias: { + src: `_ = abs(i32_alias(1));`, + pass: true, + }, + + bool: { + src: `_ = abs(false);`, + pass: false, + }, + vec_bool: { + src: `_ = abs(vec2<bool>(false, true));`, + pass: false, + }, + matrix: { + src: `_ = abs(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = abs(a);`, + pass: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = abs(a);`, + pass: false, + }, + array_runtime: { + src: `_ = abs(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = abs(a);`, + pass: false, + }, + enumerant: { + src: `_ = abs(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 1u; + let p: ptr<function, u32> = &a; + _ = abs(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 1u; + let p: ptr<function, u32> = &a; + _ = abs(*p);`, + pass: true, + }, + sampler: { + src: `_ = abs(s);`, + pass: false, + }, + texture: { + src: `_ = abs(t);`, + pass: false, + }, + no_params: { + src: `_ = abs();`, + pass: false, + }, + too_many_params: { + src: `_ = abs(1, 2);`, + pass: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias i32_alias = i32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acos.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acos.spec.ts index 82171ed4b1..d76bb470b0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acos.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acos.spec.ts @@ -6,18 +6,18 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; +import { absBigInt } from '../../../../../util/math.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinusTwoToTwo, + minusTwoToTwoRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -25,7 +25,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -39,15 +39,23 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinusTwoToTwo, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusTwoToTwoRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { - const expectedResult = Math.abs(t.params.value) <= 1; + const expectedResult = + typeof t.params.value === 'bigint' + ? absBigInt(t.params.value) <= 1n + : Math.abs(t.params.value) <= 1; validateConstOrOverrideBuiltinEval( t, builtin, @@ -57,7 +65,8 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +// f32 is included here to confirm that validation is failing due to a type issue and not something else. +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -71,8 +80,137 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(0)], 'constant' ); }); + +const kTests = { + valid: { + src: `_ = acos(1);`, + pass: true, + }, + alias: { + src: `_ = acos(f32_alias(1));`, + pass: true, + }, + + bool: { + src: `_ = acos(false);`, + pass: false, + }, + i32: { + src: `_ = acos(1i);`, + pass: false, + }, + u32: { + src: `_ = acos(1u);`, + pass: false, + }, + vec_bool: { + src: `_ = acos(vec2<bool>(false, true));`, + pass: false, + }, + vec_i32: { + src: `_ = acos(vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = acos(vec2<u32>(1, 1));`, + pass: false, + }, + matrix: { + src: `_ = acos(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = acos(a);`, + pass: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = acos(a);`, + pass: false, + }, + array_runtime: { + src: `_ = acos(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = acos(a);`, + pass: false, + }, + enumerant: { + src: `_ = acos(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = acos(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = acos(*p);`, + pass: true, + }, + sampler: { + src: `_ = acos(s);`, + pass: false, + }, + texture: { + src: `_ = acos(t);`, + pass: false, + }, + no_params: { + src: `_ = acos();`, + pass: false, + }, + too_many_params: { + src: `_ = acos(1, 2);`, + pass: false, + }, + + greater_then_one: { + src: `_ = acos(1.1f);`, + pass: false, + }, + less_then_negative_one: { + src: `_ = acos(-1.1f);`, + pass: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias f32_alias = f32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acosh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acosh.spec.ts index a7ab8d83f9..82da85fdde 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acosh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/acosh.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -18,7 +17,7 @@ import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinusTwoToTwo, + minusTwoToTwoRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -26,7 +25,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,16 +39,25 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(fullRangeForType(kValuesTypes[u.type]), kMinusTwoToTwo)) + .expand('value', u => + unique( + minusTwoToTwoRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const expectedResult = isRepresentable(Math.acosh(t.params.value), elementType(type)); + const expectedResult = isRepresentable( + Math.acosh(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -59,7 +67,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); g.test('integer_argument') .desc( @@ -73,8 +81,132 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, - [type.create(1)], + /* expectedResult */ type === Type.f32, + [type.create(type === Type.abstractInt ? 1n : 1)], 'constant' ); }); + +const kTests = { + valid: { + src: `_ = acosh(1);`, + pass: true, + }, + alias: { + src: `_ = acosh(f32_alias(1));`, + pass: true, + }, + + bool: { + src: `_ = acosh(false);`, + pass: false, + }, + i32: { + src: `_ = acosh(1i);`, + pass: false, + }, + u32: { + src: `_ = acosh(1u);`, + pass: false, + }, + vec_bool: { + src: `_ = acosh(vec2<bool>(false, true));`, + pass: false, + }, + vec_i32: { + src: `_ = acosh(vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = acosh(vec2<u32>(1, 1));`, + pass: false, + }, + matrix: { + src: `_ = acosh(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = acosh(a);`, + pass: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = acosh(a);`, + pass: false, + }, + array_runtime: { + src: `_ = acosh(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = acosh(a);`, + pass: false, + }, + enumerant: { + src: `_ = acosh(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = acosh(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = acosh(*p);`, + pass: true, + }, + sampler: { + src: `_ = acosh(s);`, + pass: false, + }, + texture: { + src: `_ = acosh(t);`, + pass: false, + }, + no_params: { + src: `_ = acosh();`, + pass: false, + }, + too_many_params: { + src: `_ = acosh(1, 2);`, + pass: false, + }, + + less_then_one: { + src: `_ = acosh(.9f);`, + pass: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias f32_alias = f32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/all.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/all.spec.ts new file mode 100644 index 0000000000..f52d24e0cc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/all.spec.ts @@ -0,0 +1,191 @@ +const builtin = 'all'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { Type, elementTypeOf, kAllScalarsAndVectors } from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { validateConstOrOverrideBuiltinEval } from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kArgumentTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('argument_types') + .desc( + ` +Validates that scalar and vector arguments are rejected by ${builtin}() if not bool or vecN<bool> +` + ) + .params(u => u.combine('type', keysOf(kArgumentTypes))) + .fn(t => { + const type = kArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ elementTypeOf(type) === Type.bool, + [type.create(0)], + 'constant', + /* returnType */ Type.bool + ); + }); + +const kTests = { + valid: { + src: `_ = ${builtin}(true);`, + pass: true, + }, + alias: { + src: `_ = ${builtin}(bool_alias(true));`, + pass: true, + }, + bool: { + src: `_ = ${builtin}(false);`, + pass: true, + }, + i32: { + src: `_ = ${builtin}(1i);`, + pass: false, + }, + u32: { + src: `_ = ${builtin}(1u);`, + pass: false, + }, + f32: { + src: `_ = ${builtin}(1.0f);`, + pass: false, + }, + f16: { + src: `_ = ${builtin}(1.0h);`, + pass: false, + }, + vec_bool: { + src: `_ = ${builtin}(vec2<bool>(false, true));`, + pass: true, + }, + vec2_bool_implicit: { + src: `_ = ${builtin}(vec2(false, true));`, + pass: true, + }, + vec3_bool_implicit: { + src: `_ = ${builtin}(vec3(true));`, + pass: true, + }, + vec_i32: { + src: `_ = ${builtin}(vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = ${builtin}(vec2<u32>(1, 1));`, + pass: false, + }, + vec_f32: { + src: `_ = ${builtin}(vec2<f32>(1, 1));`, + pass: false, + }, + vec_f16: { + src: `_ = ${builtin}(vec2<f16>(1, 1));`, + pass: false, + }, + matrix: { + src: `_ = ${builtin}(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = ${builtin}(a);`, + pass: false, + }, + array: { + src: `var a: array<bool, 5>; + _ = ${builtin}(a);`, + pass: false, + }, + array_runtime: { + src: `_ = ${builtin}(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = ${builtin}(a);`, + pass: false, + }, + enumerant: { + src: `_ = ${builtin}(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = true; + let p: ptr<function, bool> = &a; + _ = ${builtin}(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = true; + let p: ptr<function, bool> = &a; + _ = ${builtin}(*p);`, + pass: true, + }, + sampler: { + src: `_ = ${builtin}(s);`, + pass: false, + }, + texture: { + src: `_ = ${builtin}(t);`, + pass: false, + }, + no_args: { + src: `_ = ${builtin}();`, + pass: false, + }, + too_many_args: { + src: `_ = ${builtin}(true, true);`, + pass: false, + }, +}; + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(true); }`); + }); + +g.test('arguments') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .beforeAllSubcases(t => { + if (t.params.test.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const src = kTests[t.params.test].src; + const enables = t.params.test.includes('f16') ? 'enable f16;' : ''; + const code = ` + ${enables} + alias bool_alias = bool; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: bool, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); + }`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/any.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/any.spec.ts new file mode 100644 index 0000000000..9f328bbbf3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/any.spec.ts @@ -0,0 +1,191 @@ +const builtin = 'any'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { Type, elementTypeOf, kAllScalarsAndVectors } from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { validateConstOrOverrideBuiltinEval } from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kArgumentTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('argument_types') + .desc( + ` +Validates that scalar and vector arguments are rejected by ${builtin}() if not bool or vecN<bool> +` + ) + .params(u => u.combine('type', keysOf(kArgumentTypes))) + .fn(t => { + const type = kArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ elementTypeOf(type) === Type.bool, + [type.create(0)], + 'constant', + /* returnType */ Type.bool + ); + }); + +const kTests = { + valid: { + src: `_ = ${builtin}(true);`, + pass: true, + }, + alias: { + src: `_ = ${builtin}(bool_alias(true));`, + pass: true, + }, + bool: { + src: `_ = ${builtin}(false);`, + pass: true, + }, + i32: { + src: `_ = ${builtin}(1i);`, + pass: false, + }, + u32: { + src: `_ = ${builtin}(1u);`, + pass: false, + }, + f32: { + src: `_ = ${builtin}(1.0f);`, + pass: false, + }, + f16: { + src: `_ = ${builtin}(1.0h);`, + pass: false, + }, + vec_bool: { + src: `_ = ${builtin}(vec2<bool>(false, true));`, + pass: true, + }, + vec2_bool_implicit: { + src: `_ = ${builtin}(vec2(false, true));`, + pass: true, + }, + vec3_bool_implicit: { + src: `_ = ${builtin}(vec3(true));`, + pass: true, + }, + vec_i32: { + src: `_ = ${builtin}(vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = ${builtin}(vec2<u32>(1, 1));`, + pass: false, + }, + vec_f32: { + src: `_ = ${builtin}(vec2<f32>(1, 1));`, + pass: false, + }, + vec_f16: { + src: `_ = ${builtin}(vec2<f16>(1, 1));`, + pass: false, + }, + matrix: { + src: `_ = ${builtin}(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = ${builtin}(a);`, + pass: false, + }, + array: { + src: `var a: array<bool, 5>; + _ = ${builtin}(a);`, + pass: false, + }, + array_runtime: { + src: `_ = ${builtin}(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = ${builtin}(a);`, + pass: false, + }, + enumerant: { + src: `_ = ${builtin}(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = true; + let p: ptr<function, bool> = &a; + _ = ${builtin}(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = true; + let p: ptr<function, bool> = &a; + _ = ${builtin}(*p);`, + pass: true, + }, + sampler: { + src: `_ = ${builtin}(s);`, + pass: false, + }, + texture: { + src: `_ = ${builtin}(t);`, + pass: false, + }, + no_args: { + src: `_ = ${builtin}();`, + pass: false, + }, + too_many_args: { + src: `_ = ${builtin}(true, true);`, + pass: false, + }, +}; + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(true); }`); + }); + +g.test('arguments') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .beforeAllSubcases(t => { + if (t.params.test.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const src = kTests[t.params.test].src; + const enables = t.params.test.includes('f16') ? 'enable f16;' : ''; + const code = ` + ${enables} + alias bool_alias = bool; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: bool, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); + }`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/arrayLength.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/arrayLength.spec.ts new file mode 100644 index 0000000000..27d8814b14 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/arrayLength.spec.ts @@ -0,0 +1,109 @@ +export const description = ` +Validation tests for arrayLength builtins. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('bool_type') + .specURL('https://www.w3.org/TR/WGSL/#arrayLength-builtin') + .desc( + ` +arrayLength accepts only runtime-sized arrays +` + ) + .fn(t => { + const code = ` +@compute @workgroup_size(1) +fn main() { + var b = true; + _ = arrayLength(&b); +}`; + + t.expectCompileResult(false, code); + }); + +const atomic_types = ['u32', 'i32'].map(j => `atomic<${j}>`); +const vec_types = [2, 3, 4] + .map(i => ['i32', 'u32', 'f32', 'f16'].map(j => `vec${i}<${j}>`)) + .reduce((a, c) => a.concat(c), []); +const f32_matrix_types = [2, 3, 4] + .map(i => [2, 3, 4].map(j => `mat${i}x${j}f`)) + .reduce((a, c) => a.concat(c), []); +const f16_matrix_types = [2, 3, 4] + .map(i => [2, 3, 4].map(j => `mat${i}x${j}<f16>`)) + .reduce((a, c) => a.concat(c), []); + +g.test('type') + .specURL('https://www.w3.org/TR/WGSL/#arrayLength-builtin') + .desc( + ` +arrayLength accepts only runtime-sized arrays +` + ) + .params(u => + u.combine('type', [ + 'i32', + 'u32', + 'f32', + 'f16', + ...f32_matrix_types, + ...f16_matrix_types, + ...vec_types, + ...atomic_types, + 'T', + 'array<i32, 2>', + 'array<i32>', + ]) + ) + .beforeAllSubcases(t => { + if (t.params.type.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = ` +struct T { + b: i32, +} +struct S { + ary: ${t.params.type} +} + +@group(0) @binding(0) var<storage, read_write> items: S; + +@compute @workgroup_size(1) +fn main() { + _ = arrayLength(&items.ary); +}`; + + t.expectCompileResult(t.params.type === 'array<i32>', code); + }); + +// Note, the `write` case actually fails because you can't declare a storage buffer of +// access_mode `write`. +g.test('access_mode') + .specURL('https://www.w3.org/TR/WGSL/#arrayLength-builtin') + .desc( + ` +arrayLength runtime-sized array must have an access_mode of read or read_write +` + ) + .params(u => u.combine('mode', ['read', 'read_write', 'write'])) + .fn(t => { + const code = ` +struct S { + ary: array<i32>, +} + +@group(0) @binding(0) var<storage, ${t.params.mode}> items: S; + +@compute @workgroup_size(1) +fn main() { + _ = arrayLength(&items.ary); +}`; + + t.expectCompileResult(t.params.mode !== 'write', code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asin.spec.ts index 8af7706169..16a193aec4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asin.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asin.spec.ts @@ -6,18 +6,18 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; +import { absBigInt } from '../../../../../util/math.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinusTwoToTwo, + minusTwoToTwoRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -25,7 +25,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -39,15 +39,23 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinusTwoToTwo, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusTwoToTwoRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { - const expectedResult = Math.abs(t.params.value) <= 1; + const expectedResult = + typeof t.params.value === 'bigint' + ? absBigInt(t.params.value) <= 1n + : Math.abs(t.params.value) <= 1; validateConstOrOverrideBuiltinEval( t, builtin, @@ -57,7 +65,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -71,8 +79,137 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(0)], 'constant' ); }); + +const kTests = { + valid: { + src: `_ = asin(1);`, + pass: true, + }, + alias: { + src: `_ = asin(f32_alias(1));`, + pass: true, + }, + + bool: { + src: `_ = asin(false);`, + pass: false, + }, + i32: { + src: `_ = asin(1i);`, + pass: false, + }, + u32: { + src: `_ = asin(1u);`, + pass: false, + }, + vec_bool: { + src: `_ = asin(vec2<bool>(false, true));`, + pass: false, + }, + vec_i32: { + src: `_ = asin(vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = asin(vec2<u32>(1, 1));`, + pass: false, + }, + matrix: { + src: `_ = asin(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = asin(a);`, + pass: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = asin(a);`, + pass: false, + }, + array_runtime: { + src: `_ = asin(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = asin(a);`, + pass: false, + }, + enumerant: { + src: `_ = asin(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = asin(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = asin(*p);`, + pass: true, + }, + sampler: { + src: `_ = asin(s);`, + pass: false, + }, + texture: { + src: `_ = asin(t);`, + pass: false, + }, + no_params: { + src: `_ = asin();`, + pass: false, + }, + too_many_params: { + src: `_ = asin(1, 2);`, + pass: false, + }, + + greater_then_one: { + src: `_ = asin(1.1f);`, + pass: false, + }, + less_then_negative_one: { + src: `_ = asin(-1.1f);`, + pass: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias f32_alias = f32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asinh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asinh.spec.ts index 4558d30966..285ff2dcd2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asinh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/asinh.spec.ts @@ -6,19 +6,19 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, + Type, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; -import { linearRange } from '../../../../../util/math.js'; +import { linearRange, linearRangeBigInt } from '../../../../../util/math.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, + rangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -26,7 +26,12 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +const additionalRangeForType = rangeForType( + linearRange(-2000, 2000, 10), + linearRangeBigInt(-2000n, 2000n, 10) +); g.test('values') .desc( @@ -41,17 +46,21 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() .expand('value', u => - unique(fullRangeForType(kValuesTypes[u.type]), linearRange(-2000, 2000, 10)) + unique(fullRangeForType(kValuesTypes[u.type]), additionalRangeForType(kValuesTypes[u.type])) ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const expectedResult = isRepresentable(Math.asinh(t.params.value), elementType(type)); + const expectedResult = isRepresentable( + Math.asinh(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -61,7 +70,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -75,8 +84,128 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); }); + +const kTests = { + valid: { + src: `_ = asinh(1);`, + pass: true, + }, + alias: { + src: `_ = asinh(f32_alias(1));`, + pass: true, + }, + + bool: { + src: `_ = asinh(false);`, + pass: false, + }, + i32: { + src: `_ = asinh(1i);`, + pass: false, + }, + u32: { + src: `_ = asinh(1u);`, + pass: false, + }, + vec_bool: { + src: `_ = asinh(vec2<bool>(false, true));`, + pass: false, + }, + vec_i32: { + src: `_ = asinh(vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = asinh(vec2<u32>(1, 1));`, + pass: false, + }, + matrix: { + src: `_ = asinh(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = asinh(a);`, + pass: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = asinh(a);`, + pass: false, + }, + array_runtime: { + src: `_ = asinh(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = asinh(a);`, + pass: false, + }, + enumerant: { + src: `_ = asinh(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = asinh(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = asinh(*p);`, + pass: true, + }, + sampler: { + src: `_ = asinh(s);`, + pass: false, + }, + texture: { + src: `_ = asinh(t);`, + pass: false, + }, + no_params: { + src: `_ = asinh();`, + pass: false, + }, + too_many_params: { + src: `_ = asinh(1, 2);`, + pass: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias f32_alias = f32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan.spec.ts index 3080f4e971..c5e964084d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan.spec.ts @@ -6,18 +6,17 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinus3PiTo3Pi, + minusThreePiToThreePiRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -25,7 +24,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -39,10 +38,15 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinus3PiTo3Pi, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusThreePiToThreePiRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -58,7 +62,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -72,8 +76,128 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(0)], 'constant' ); }); + +const kTests = { + valid: { + src: `_ = atan(1);`, + pass: true, + }, + alias: { + src: `_ = atan(f32_alias(1));`, + pass: true, + }, + + bool: { + src: `_ = atan(false);`, + pass: false, + }, + i32: { + src: `_ = atan(1i);`, + pass: false, + }, + u32: { + src: `_ = atan(1u);`, + pass: false, + }, + vec_bool: { + src: `_ = atan(vec2<bool>(false, true));`, + pass: false, + }, + vec_i32: { + src: `_ = atan(vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = atan(vec2<u32>(1, 1));`, + pass: false, + }, + matrix: { + src: `_ = atan(mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = atan(a);`, + pass: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = atan(a);`, + pass: false, + }, + array_runtime: { + src: `_ = atan(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = atan(a);`, + pass: false, + }, + enumerant: { + src: `_ = atan(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = atan(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = atan(*p);`, + pass: true, + }, + sampler: { + src: `_ = atan(s);`, + pass: false, + }, + texture: { + src: `_ = atan(t);`, + pass: false, + }, + no_params: { + src: `_ = atan();`, + pass: false, + }, + too_many_params: { + src: `_ = atan(1, 2);`, + pass: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias f32_alias = f32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan2.spec.ts index 33f1970697..20998bfe76 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan2.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atan2.spec.ts @@ -6,13 +6,13 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - Vector, - VectorType, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + VectorValue, + kFloatScalarsAndVectors, + kConcreteIntegerScalarsAndVectors, + kAllMatrices, + kAllBoolScalarsAndVectors, + scalarTypeOf, + Type, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -20,7 +20,7 @@ import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kSparseMinus3PiTo3Pi, + sparseMinusThreePiToThreePiRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -28,7 +28,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kFloatScalarsAndVectors); g.test('values') .desc( @@ -42,19 +42,29 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('y', u => unique(kSparseMinus3PiTo3Pi, fullRangeForType(kValuesTypes[u.type], 4))) - .expand('x', u => unique(kSparseMinus3PiTo3Pi, fullRangeForType(kValuesTypes[u.type], 4))) + .expand('y', u => + unique( + sparseMinusThreePiToThreePiRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type], 4) + ) + ) + .expand('x', u => + unique( + sparseMinusThreePiToThreePiRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type], 4) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; const expectedResult = isRepresentable( - Math.abs(Math.atan2(t.params.y, t.params.x)), - elementType(type) + Math.abs(Math.atan2(Number(t.params.x), Number(t.params.y))), + scalarTypeOf(type) ); validateConstOrOverrideBuiltinEval( t, @@ -65,42 +75,275 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kInvalidArgumentTypes = objectsToRecord([ + Type.f32, + ...kConcreteIntegerScalarsAndVectors, + ...kAllBoolScalarsAndVectors, + ...kAllMatrices, +]); -g.test('integer_argument_y') +g.test('invalid_argument_y') .desc( ` Validates that scalar and vector integer arguments are rejected by ${builtin}() ` ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) + .params(u => u.combine('type', keysOf(kInvalidArgumentTypes))) .fn(t => { - const yTy = kIntegerArgumentTypes[t.params.type]; - const xTy = yTy instanceof Vector ? new VectorType(yTy.size, TypeF32) : TypeF32; + const yTy = kInvalidArgumentTypes[t.params.type]; + const xTy = yTy instanceof VectorValue ? Type.vec(yTy.size, Type.f32) : Type.f32; validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ yTy === TypeF32, + /* expectedResult */ yTy === Type.f32, [yTy.create(1), xTy.create(1)], 'constant' ); }); -g.test('integer_argument_x') +g.test('invalid_argument_x') .desc( ` Validates that scalar and vector integer arguments are rejected by ${builtin}() ` ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) + .params(u => u.combine('type', keysOf(kInvalidArgumentTypes))) .fn(t => { - const xTy = kIntegerArgumentTypes[t.params.type]; - const yTy = xTy instanceof Vector ? new VectorType(xTy.size, TypeF32) : TypeF32; + const xTy = kInvalidArgumentTypes[t.params.type]; + const yTy = xTy instanceof VectorValue ? Type.vec(xTy.size, Type.f32) : Type.f32; validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ xTy === TypeF32, + /* expectedResult */ xTy === Type.f32, [yTy.create(1), xTy.create(1)], 'constant' ); }); + +const kTests = { + af: { + src: `_ = atan2(1.2, 2.2);`, + pass: true, + is_f16: false, + }, + ai: { + src: `_ = atan2(1, 2);`, + pass: true, + is_f16: false, + }, + ai_af: { + src: `_ = atan2(1, 2.1);`, + pass: true, + is_f16: false, + }, + af_ai: { + src: `_ = atan2(1.2, 2);`, + pass: true, + is_f16: false, + }, + ai_f32: { + src: `_ = atan2(1, 1.2f);`, + pass: true, + is_f16: false, + }, + f32_ai: { + src: `_ = atan2(1.2f, 1);`, + pass: true, + is_f16: false, + }, + af_f32: { + src: `_ = atan2(1.2, 2.2f);`, + pass: true, + is_f16: false, + }, + f32_af: { + src: `_ = atan2(2.2f, 1.2);`, + pass: true, + is_f16: false, + }, + f16_ai: { + src: `_ = atan2(1.2h, 1);`, + pass: true, + is_f16: true, + }, + ai_f16: { + src: `_ = atan2(1, 1.2h);`, + pass: true, + is_f16: true, + }, + af_f16: { + src: `_ = atan2(1.2, 1.2h);`, + pass: true, + is_f16: true, + }, + f16_af: { + src: `_ = atan2(1.2h, 1.2);`, + pass: true, + is_f16: true, + }, + + mixed_types: { + src: `_ = atan2(1.2f, vec2(1.2f));`, + pass: false, + is_f16: false, + }, + mixed_types_2: { + src: `_ = atan2(vec2(1.2f), 1.2f);`, + pass: false, + is_f16: false, + }, + f16_f32: { + src: `_ = atan2(1.2h, 1.2f);`, + pass: false, + is_f16: true, + }, + u32_f32: { + src: `_ = atan2(1u, 1.2f);`, + pass: false, + is_f16: false, + }, + f32_u32: { + src: `_ = atan2(1.2f, 1u);`, + pass: false, + is_f16: false, + }, + f32_i32: { + src: `_ = atan2(1.2f, 1i);`, + pass: false, + is_f16: false, + }, + i32_f32: { + src: `_ = atan2(1i, 1.2f);`, + pass: false, + is_f16: false, + }, + f32_bool: { + src: `_ = atan2(1.2f, true);`, + pass: false, + is_f16: false, + }, + bool_f32: { + src: `_ = atan2(false, 1.2f);`, + pass: false, + is_f16: false, + }, + vec_f32: { + src: `_ = atan2(vec2(1i), vec2(1.2f));`, + pass: false, + is_f16: false, + }, + f32_vec: { + src: `_ = atan2(vec2(1.2f), vec2(1i));`, + pass: false, + is_f16: false, + }, + matrix: { + src: `_ = atan2(mat2x2(1, 1, 1, 1), mat2x2(1, 1, 1, 1));`, + pass: false, + is_f16: false, + }, + atomic: { + src: ` _ = atan2(a, a);`, + pass: false, + is_f16: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = atan2(a, a);`, + pass: false, + is_f16: false, + }, + array_runtime: { + src: `_ = atan2(k.arry, k.arry);`, + pass: false, + is_f16: false, + }, + struct: { + src: `var a: A; + _ = atan2(a, a);`, + pass: false, + is_f16: false, + }, + enumerant: { + src: `_ = atan2(read_write, read_write);`, + pass: false, + is_f16: false, + }, + ptr: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = atan2(p, p);`, + pass: false, + is_f16: false, + }, + ptr_deref: { + src: `var<function> a = 1f; + let p: ptr<function, f32> = &a; + _ = atan2(*p, *p);`, + pass: true, + is_f16: false, + }, + sampler: { + src: `_ = atan2(s, s);`, + pass: false, + is_f16: false, + }, + texture: { + src: `_ = atan2(t, t);`, + pass: false, + is_f16: false, + }, + no_params: { + src: `_ = atan2();`, + pass: false, + is_f16: false, + }, + too_many_params: { + src: `_ = atan2(1, 2, 3);`, + pass: false, + is_f16: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .beforeAllSubcases(t => { + if (kTests[t.params.test].is_f16 === true) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias f32_alias = f32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1, 2); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atanh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atanh.spec.ts index 63a96f0f70..4eae8a71f0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atanh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atanh.spec.ts @@ -6,18 +6,18 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; +import { absBigInt } from '../../../../../util/math.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinusTwoToTwo, + minusTwoToTwoRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -25,7 +25,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -39,15 +39,23 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinusTwoToTwo, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusTwoToTwoRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { - const expectedResult = Math.abs(t.params.value) < 1; + const expectedResult = + typeof t.params.value === 'bigint' + ? absBigInt(t.params.value) < 1n + : Math.abs(t.params.value) < 1; validateConstOrOverrideBuiltinEval( t, builtin, @@ -57,7 +65,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -71,8 +79,145 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(0)], 'constant' ); }); + +const kTests = { + valid: { + src: `_ = atanh(.1);`, + pass: true, + }, + alias: { + src: `_ = atanh(f32_alias(.1));`, + pass: true, + }, + + bool: { + src: `_ = atanh(false);`, + pass: false, + }, + i32: { + src: `_ = atanh(0i);`, + pass: false, + }, + u32: { + src: `_ = atanh(0u);`, + pass: false, + }, + vec_bool: { + src: `_ = atanh(vec2<bool>(false, true));`, + pass: false, + }, + vec_i32: { + src: `_ = atanh(vec2<i32>(0, 0));`, + pass: false, + }, + vec_u32: { + src: `_ = atanh(vec2<u32>(0, 0));`, + pass: false, + }, + matrix: { + src: `_ = atanh(mat2x2(0, 0, 0, 0));`, + pass: false, + }, + atomic: { + src: ` _ = atanh(a);`, + pass: false, + }, + array: { + src: `var a: array<u32, 5>; + _ = atanh(a);`, + pass: false, + }, + array_runtime: { + src: `_ = atanh(k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = atanh(a);`, + pass: false, + }, + enumerant: { + src: `_ = atanh(read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 0f; + let p: ptr<function, f32> = &a; + _ = atanh(p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 0f; + let p: ptr<function, f32> = &a; + _ = atanh(*p);`, + pass: true, + }, + sampler: { + src: `_ = atanh(s);`, + pass: false, + }, + texture: { + src: `_ = atanh(t);`, + pass: false, + }, + no_params: { + src: `_ = atanh();`, + pass: false, + }, + too_many_params: { + src: `_ = atanh(0, .2);`, + pass: false, + }, + + one: { + src: `_ = atanh(1f);`, + pass: false, + }, + greater_then_one: { + src: `_ = atanh(1.1f);`, + pass: false, + }, + negative_one: { + src: `_ = atanh(-1f);`, + pass: false, + }, + less_then_negative_one: { + src: `_ = atanh(-1.1f);`, + pass: false, + }, +}; + +g.test('parameters') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const src = kTests[t.params.test].src; + const code = ` +alias f32_alias = f32; + +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: texture_2d<f32>; + +var<workgroup> a: atomic<u32>; + +struct A { + i: u32, +} +struct B { + arry: array<u32>, +} +@group(0) @binding(3) var<storage> k: B; + + +@vertex +fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); +}`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atomics.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atomics.spec.ts index 57c5aae613..fdb85664d2 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atomics.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/atomics.spec.ts @@ -8,18 +8,44 @@ import { ShaderValidationTest } from '../../../shader_validation_test.js'; export const g = makeTestGroup(ShaderValidationTest); -const kAtomicOps = { - add: { src: 'atomicAdd(&a,1)' }, - sub: { src: 'atomicSub(&a,1)' }, - max: { src: 'atomicMax(&a,1)' }, - min: { src: 'atomicMin(&a,1)' }, - and: { src: 'atomicAnd(&a,1)' }, - or: { src: 'atomicOr(&a,1)' }, - xor: { src: 'atomicXor(&a,1)' }, - load: { src: 'atomicLoad(&a)' }, - store: { src: 'atomicStore(&a,1)' }, - exchange: { src: 'atomicExchange(&a,1)' }, - compareexchangeweak: { src: 'atomicCompareExchangeWeak(&a,1,1)' }, +interface stringToString { + (a: string): string; +} + +const kAtomicOps: Record<string, stringToString> = { + add: (a: string): string => { + return `atomicAdd(${a},1)`; + }, + sub: (a: string): string => { + return `atomicSub(${a},1)`; + }, + max: (a: string): string => { + return `atomicMax(${a},1)`; + }, + min: (a: string): string => { + return `atomicMin(${a},1)`; + }, + and: (a: string): string => { + return `atomicAnd(${a},1)`; + }, + or: (a: string): string => { + return `atomicOr(${a},1)`; + }, + xor: (a: string): string => { + return `atomicXor(${a},1)`; + }, + load: (a: string): string => { + return `atomicLoad(${a})`; + }, + store: (a: string): string => { + return `atomicStore(${a},1)`; + }, + exchange: (a: string): string => { + return `atomicExchange(${a},1)`; + }, + compareexchangeweak: (a: string): string => { + return `atomicCompareExchangeWeak(${a},1,1)`; + }, }; g.test('stage') @@ -35,7 +61,7 @@ Atomic built-in functions must not be used in a vertex shader stage. .combine('atomicOp', keysOf(kAtomicOps)) ) .fn(t => { - const atomicOp = kAtomicOps[t.params.atomicOp].src; + const atomicOp = kAtomicOps[t.params.atomicOp](`&a`); let code = ` @group(0) @binding(0) var<storage, read_write> a: atomic<i32>; `; @@ -68,3 +94,186 @@ Atomic built-in functions must not be used in a vertex shader stage. const pass = t.params.stage !== 'vertex'; t.expectCompileResult(pass, code); }); + +function generateAtomicCode( + type: string, + access: string, + aspace: string, + style: string, + op: string +): string { + let moduleVar = ``; + let functionVar = ``; + let param = ``; + let aParam = ``; + if (style === 'var') { + aParam = `&a`; + switch (aspace) { + case 'storage': + moduleVar = `@group(0) @binding(0) var<storage, ${access}> a : atomic<${type}>;\n`; + break; + case 'workgroup': + moduleVar = `var<workgroup> a : atomic<${type}>;\n`; + break; + case 'uniform': + moduleVar = `@group(0) @binding(0) var<uniform> a : atomic<${type}>;\n`; + break; + case 'private': + moduleVar = `var<private> a : atomic<${type}>;\n`; + break; + case 'function': + functionVar = `var a : atomic<${type}>;\n`; + break; + default: + break; + } + } else { + const aspaceParam = aspace === 'storage' ? `, ${access}` : ``; + param = `p : ptr<${aspace}, atomic<${type}>${aspaceParam}>`; + aParam = `p`; + } + + return ` +${moduleVar} +fn foo(${param}) { + ${functionVar} + ${kAtomicOps[op](aParam)}; +} +`; +} + +g.test('atomic_parameterization') + .desc('Tests the valid atomic parameters') + .params(u => + u + .combine('op', keysOf(kAtomicOps)) + .beginSubcases() + .combine('aspace', ['storage', 'workgroup', 'private', 'uniform', 'function'] as const) + .combine('access', ['read', 'read_write'] as const) + .combine('type', ['i32', 'u32'] as const) + .combine('style', ['param', 'var'] as const) + .filter(t => { + switch (t.aspace) { + case 'uniform': + return t.style === 'param' && t.access === 'read'; + case 'workgroup': + return t.access === 'read_write'; + case 'function': + case 'private': + return t.style === 'param' && t.access === 'read_write'; + default: + return true; + } + }) + ) + .fn(t => { + if ( + t.params.style === 'param' && + !(t.params.aspace === 'function' || t.params.aspace === 'private') + ) { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + + const aspaceOK = t.params.aspace === 'storage' || t.params.aspace === 'workgroup'; + const accessOK = t.params.access === 'read_write'; + t.expectCompileResult( + aspaceOK && accessOK, + generateAtomicCode( + t.params.type, + t.params.access, + t.params.aspace, + t.params.style, + t.params.op + ) + ); + }); + +g.test('data_parameters') + .desc('Validates that data parameters must match atomic type (or be implicitly convertible)') + .params(u => + u + .combine('op', [ + 'atomicStore', + 'atomicAdd', + 'atomicSub', + 'atomicMax', + 'atomicMin', + 'atomicAnd', + 'atomicOr', + 'atomicXor', + 'atomicExchange', + 'atomicCompareExchangeWeak1', + 'atomicCompareExchangeWeak2', + ] as const) + .beginSubcases() + .combine('atomicType', ['i32', 'u32'] as const) + .combine('dataType', ['i32', 'u32', 'f32', 'AbstractInt'] as const) + ) + .fn(t => { + let dataValue = ''; + switch (t.params.dataType) { + case 'i32': + dataValue = '1i'; + break; + case 'u32': + dataValue = '1u'; + break; + case 'f32': + dataValue = '1f'; + break; + case 'AbstractInt': + dataValue = '1'; + break; + } + let op = ''; + switch (t.params.op) { + case 'atomicCompareExchangeWeak1': + op = `atomicCompareExchangeWeak(&a, ${dataValue}, 1)`; + break; + case 'atomicCompareExchangeWeak2': + op = `atomicCompareExchangeWeak(&a, 1, ${dataValue})`; + break; + default: + op = `${t.params.op}(&a, ${dataValue})`; + break; + } + const code = ` +var<workgroup> a : atomic<${t.params.atomicType}>; +fn foo() { + ${op}; +} +`; + + const expect = t.params.atomicType === t.params.dataType || t.params.dataType === 'AbstractInt'; + t.expectCompileResult(expect, code); + }); + +g.test('return_types') + .desc('Validates return types of atomics') + .params(u => + u + .combine('op', keysOf(kAtomicOps)) + .beginSubcases() + .combine('atomicType', ['i32', 'u32'] as const) + .combine('returnType', ['i32', 'u32', 'f32'] as const) + ) + .fn(t => { + let op = `${kAtomicOps[t.params.op]('&a')}`; + switch (t.params.op) { + case 'compareexchangeweak': + op = `let tmp : ${t.params.returnType} = ${op}.old_value`; + break; + default: + op = `let tmp : ${t.params.returnType} = ${op}`; + break; + } + const code = ` +var<workgroup> a : atomic<${t.params.atomicType}>; +fn foo() { + ${op}; +} +`; + + const expect = t.params.atomicType === t.params.returnType && t.params.op !== 'store'; + t.expectCompileResult(expect, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/barriers.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/barriers.spec.ts new file mode 100644 index 0000000000..5a4ef14657 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/barriers.spec.ts @@ -0,0 +1,109 @@ +export const description = ` +Validation tests for {storage,texture,workgroup}Barrier() builtins. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kEntryPoints = { + none: { supportsBarrier: true, code: `` }, + compute: { + supportsBarrier: true, + code: `@compute @workgroup_size(1) +fn main() { + foo(); +}`, + }, + vertex: { + supportsBarrier: false, + code: `@vertex +fn main() -> @builtin(position) vec4f { + foo(); + return vec4f(); +}`, + }, + fragment: { + supportsBarrier: false, + code: `@fragment +fn main() { + foo(); +}`, + }, + compute_and_fragment: { + supportsBarrier: false, + code: `@compute @workgroup_size(1) +fn main1() { + foo(); +} + +@fragment +fn main2() { + foo(); +} +`, + }, + fragment_without_call: { + supportsBarrier: true, + code: `@fragment +fn main() { +} +`, + }, +}; + +g.test('only_in_compute') + .specURL('https://www.w3.org/TR/WGSL/#sync-builtin-functions') + .desc( + ` +Synchronization functions must only be used in the compute shader stage. +` + ) + .params(u => + u + .combine('entry_point', keysOf(kEntryPoints)) + .combine('call', ['bar', 'storageBarrier', 'textureBarrier', 'workgroupBarrier']) + ) + .fn(t => { + if (t.params.call.startsWith('textureBarrier')) { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + } + + const config = kEntryPoints[t.params.entry_point]; + const code = ` +${config.code} +fn bar() {} + +fn foo() { + ${t.params.call}(); +}`; + t.expectCompileResult(t.params.call === 'bar' || config.supportsBarrier, code); + }); + +g.test('no_return_value') + .specURL('https://www.w3.org/TR/WGSL/#sync-builtin-functions') + .desc( + ` +Barrier functions do not return a value. +` + ) + .params(u => + u + .combine('assign', [false, true]) + .combine('rhs', ['bar', 'storageBarrier', 'textureBarrier', 'workgroupBarrier']) + ) + .fn(t => { + if (t.params.rhs.startsWith('textureBarrier')) { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + } + + const code = ` +fn bar() {} + +fn foo() { + ${t.params.assign ? '_ = ' : ''} ${t.params.rhs}(); +}`; + t.expectCompileResult(!t.params.assign || t.params.rhs === 'bar()', code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/ceil.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/ceil.spec.ts index 0f287907f8..951958d02c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/ceil.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/ceil.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -23,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -36,10 +35,11 @@ Validates that constant evaluation and override evaluation of ${builtin}() never .combine('stage', kConstantAndOverrideStages) .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -54,7 +54,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() never ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -68,7 +68,7 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(0)], 'constant' ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/clamp.spec.ts index 1cf28ffc2b..9a23328d65 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/clamp.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/clamp.spec.ts @@ -6,9 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - elementType, - kAllFloatAndIntegerScalarsAndVectors, + Type, + kFloatScalarsAndVectors, + kConcreteIntegerScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -21,7 +22,10 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatAndIntegerScalarsAndVectors); +const kValuesTypes = objectsToRecord([ + ...kFloatScalarsAndVectors, + ...kConcreteIntegerScalarsAndVectors, +]); g.test('values') .desc( @@ -40,7 +44,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .expand('high', u => fullRangeForType(kValuesTypes[u.type], 4)) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/const_override_validation.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/const_override_validation.ts index 86b88cb159..f3b964a6e3 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/const_override_validation.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/const_override_validation.ts @@ -2,100 +2,138 @@ import { assert, unreachable } from '../../../../../../common/util/util.js'; import { kValue } from '../../../../../util/constants.js'; import { Type, - TypeF16, Value, - elementType, - elementsOf, + elementTypeOf, isAbstractType, + scalarElementsOf, + scalarTypeOf, } from '../../../../../util/conversion.js'; -import { fullF16Range, fullF32Range, fullF64Range, linearRange } from '../../../../../util/math.js'; +import { + scalarF16Range, + scalarF32Range, + scalarF64Range, + linearRange, + linearRangeBigInt, +} from '../../../../../util/math.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; -/// A linear sweep between -2 to 2 -export const kMinusTwoToTwo = linearRange(-2, 2, 10); - -/// An array of values ranging from -3π to 3π, with a focus on multiples of π -export const kMinus3PiTo3Pi = [ - -3 * Math.PI, - -2.999 * Math.PI, - - -2.501 * Math.PI, - -2.5 * Math.PI, - -2.499 * Math.PI, - - -2.001 * Math.PI, - -2.0 * Math.PI, - -1.999 * Math.PI, - - -1.501 * Math.PI, - -1.5 * Math.PI, - -1.499 * Math.PI, - - -1.001 * Math.PI, - -1.0 * Math.PI, - -0.999 * Math.PI, - - -0.501 * Math.PI, - -0.5 * Math.PI, - -0.499 * Math.PI, - - -0.001, - 0, - 0.001, - - 0.499 * Math.PI, - 0.5 * Math.PI, - 0.501 * Math.PI, - - 0.999 * Math.PI, - 1.0 * Math.PI, - 1.001 * Math.PI, - - 1.499 * Math.PI, - 1.5 * Math.PI, - 1.501 * Math.PI, - - 1.999 * Math.PI, - 2.0 * Math.PI, - 2.001 * Math.PI, - - 2.499 * Math.PI, - 2.5 * Math.PI, - 2.501 * Math.PI, - - 2.999 * Math.PI, - 3 * Math.PI, -] as const; - -/// A minimal array of values ranging from -3π to 3π, with a focus on multiples -/// of π. Used when multiple parameters are being passed in, so the number of -/// cases becomes the square or more of this list. -export const kSparseMinus3PiTo3Pi = [ - -3 * Math.PI, - -2.5 * Math.PI, - -2.0 * Math.PI, - -1.5 * Math.PI, - -1.0 * Math.PI, - -0.5 * Math.PI, - 0, - 0.5 * Math.PI, - Math.PI, - 1.5 * Math.PI, - 2.0 * Math.PI, - 2.5 * Math.PI, - 3 * Math.PI, -] as const; +/** @returns a function that can select between ranges depending on type */ +export function rangeForType( + number_range: readonly number[], + bigint_range: readonly bigint[] +): (type: Type) => readonly (number | bigint)[] { + return (type: Type): readonly (number | bigint)[] => { + switch (scalarTypeOf(type).kind) { + case 'abstract-float': + case 'f32': + case 'f16': + return number_range; + case 'abstract-int': + return bigint_range; + } + unreachable(`Received unexpected type '${type}'`); + }; +} + +/* @returns a linear sweep between -2 to 2 for type */ +// prettier-ignore +export const minusTwoToTwoRangeForType = rangeForType( + linearRange(-2, 2, 10), + [ -2n, -1n, 0n, 1n, 2n ] +); + +/* @returns array of values ranging from -3π to 3π, with a focus on multiples of π */ +export const minusThreePiToThreePiRangeForType = rangeForType( + [ + -3 * Math.PI, + -2.999 * Math.PI, + + -2.501 * Math.PI, + -2.5 * Math.PI, + -2.499 * Math.PI, + + -2.001 * Math.PI, + -2.0 * Math.PI, + -1.999 * Math.PI, + + -1.501 * Math.PI, + -1.5 * Math.PI, + -1.499 * Math.PI, + + -1.001 * Math.PI, + -1.0 * Math.PI, + -0.999 * Math.PI, + + -0.501 * Math.PI, + -0.5 * Math.PI, + -0.499 * Math.PI, + + -0.001, + 0, + 0.001, + + 0.499 * Math.PI, + 0.5 * Math.PI, + 0.501 * Math.PI, + + 0.999 * Math.PI, + 1.0 * Math.PI, + 1.001 * Math.PI, + + 1.499 * Math.PI, + 1.5 * Math.PI, + 1.501 * Math.PI, + + 1.999 * Math.PI, + 2.0 * Math.PI, + 2.001 * Math.PI, + + 2.499 * Math.PI, + 2.5 * Math.PI, + 2.501 * Math.PI, + + 2.999 * Math.PI, + 3 * Math.PI, + ], + [-2n, -1n, 0n, 1n, 2n] +); + +/** + * @returns a minimal array of values ranging from -3π to 3π, with a focus on + * multiples of π. + * + * Used when multiple parameters are being passed in, so the number of cases + * becomes the square or more of this list. */ +export const sparseMinusThreePiToThreePiRangeForType = rangeForType( + [ + -3 * Math.PI, + -2.5 * Math.PI, + -2.0 * Math.PI, + -1.5 * Math.PI, + -1.0 * Math.PI, + -0.5 * Math.PI, + 0, + 0.5 * Math.PI, + Math.PI, + 1.5 * Math.PI, + 2.0 * Math.PI, + 2.5 * Math.PI, + 3 * Math.PI, + ], + [-2n, -1n, 0n, 1n, 2n] +); /// The evaluation stages to test export const kConstantAndOverrideStages = ['constant', 'override'] as const; export type ConstantOrOverrideStage = 'constant' | 'override'; +export type ExecutionStage = 'constant' | 'override' | 'runtime'; /** * @returns true if evaluation stage `stage` supports expressions of type @p. */ export function stageSupportsType(stage: ConstantOrOverrideStage, type: Type) { - if (stage === 'override' && isAbstractType(elementType(type)!)) { + if (stage === 'override' && isAbstractType(elementTypeOf(type)!)) { // Abstract numerics are concretized before being used in an override expression. return false; } @@ -110,23 +148,26 @@ export function stageSupportsType(stage: ConstantOrOverrideStage, type: Type) { * @param expectedResult false if an error is expected, true if no error is expected * @param args the arguments to pass to the builtin * @param stage the evaluation stage + * @param returnType the explicit return type of the result variable, if provided (implicit otherwise) */ export function validateConstOrOverrideBuiltinEval( t: ShaderValidationTest, builtin: string, expectedResult: boolean, args: Value[], - stage: ConstantOrOverrideStage + stage: ConstantOrOverrideStage, + returnType?: Type ) { - const elTys = args.map(arg => elementType(arg.type)!); - const enables = elTys.some(ty => ty === TypeF16) ? 'enable f16;' : ''; + const elTys = args.map(arg => elementTypeOf(arg.type)!); + const enables = elTys.some(ty => ty === Type.f16) ? 'enable f16;' : ''; + const optionalVarType = returnType ? `: ${returnType.toString()}` : ''; switch (stage) { case 'constant': { t.expectCompileResult( expectedResult, `${enables} -const v = ${builtin}(${args.map(arg => arg.wgsl()).join(', ')});` +const v ${optionalVarType} = ${builtin}(${args.map(arg => arg.wgsl()).join(', ')});` ); break; } @@ -138,7 +179,7 @@ const v = ${builtin}(${args.map(arg => arg.wgsl()).join(', ')});` let numOverrides = 0; for (const arg of args) { const argOverrides: string[] = []; - for (const el of elementsOf(arg)) { + for (const el of scalarElementsOf(arg)) { const name = `o${numOverrides++}`; overrideDecls.push(`override ${name} : ${el.type};`); argOverrides.push(name); @@ -150,7 +191,7 @@ const v = ${builtin}(${args.map(arg => arg.wgsl()).join(', ')});` expectedResult, code: `${enables} ${overrideDecls.join('\n')} -var<private> v = ${builtin}(${callArgs.join(', ')});`, +var<private> v ${optionalVarType} = ${builtin}(${callArgs.join(', ')});`, constants, reference: ['v'], }); @@ -159,24 +200,92 @@ var<private> v = ${builtin}(${callArgs.join(', ')});`, } } +/** + * Runs a validation test to check that evaluation of `binaryOp` either evaluates with or without + * error at shader creation time or pipeline creation time. + * @param t the ShaderValidationTest + * @param binaryOp the symbol of the binary operator + * @param expectedResult false if an error is expected, true if no error is expected + * @param leftStage the evaluation stage for the left argument + * @param left the left-hand side of the binary operation + * @param rightStage the evaluation stage for the right argument + * @param right the right-hand side of the binary operation + */ +export function validateConstOrOverrideBinaryOpEval( + t: ShaderValidationTest, + binaryOp: string, + expectedResult: boolean, + leftStage: ExecutionStage, + left: Value, + rightStage: ExecutionStage, + right: Value +) { + const allArgs = [left, right]; + const elTys = allArgs.map(arg => elementTypeOf(arg.type)!); + const enables = elTys.some(ty => ty === Type.f16) ? 'enable f16;' : ''; + + const codeLines = [enables]; + const constants: Record<string, number> = {}; + let numOverrides = 0; + + function addOperand(name: string, stage: ExecutionStage, value: Value) { + switch (stage) { + case 'runtime': + assert(!isAbstractType(value.type)); + codeLines.push(`var<private> ${name} = ${value.wgsl()};`); + return name; + + case 'constant': + codeLines.push(`const ${name} = ${value.wgsl()};`); + return name; + + case 'override': { + assert(!isAbstractType(value.type)); + const argOverrides: string[] = []; + for (const el of scalarElementsOf(value)) { + const elName = `o${numOverrides++}`; + codeLines.push(`override ${elName} : ${el.type};`); + constants[elName] = Number(el.value); + argOverrides.push(elName); + } + return `${value.type}(${argOverrides.join(', ')})`; + } + } + } + + const leftOperand = addOperand('left', leftStage, left); + const rightOperand = addOperand('right', rightStage, right); + + if (leftStage === 'override' || rightStage === 'override') { + t.expectPipelineResult({ + expectedResult, + code: codeLines.join('\n'), + constants, + reference: [`${leftOperand} ${binaryOp} ${rightOperand}`], + }); + } else { + codeLines.push(`fn f() { _ = ${leftOperand} ${binaryOp} ${rightOperand}; }`); + t.expectCompileResult(expectedResult, codeLines.join('\n')); + } +} /** @returns a sweep of the representable values for element type of `type` */ -export function fullRangeForType(type: Type, count?: number) { +export function fullRangeForType(type: Type, count?: number): readonly (number | bigint)[] { if (count === undefined) { count = 25; } - switch (elementType(type)?.kind) { + switch (scalarTypeOf(type)?.kind) { case 'abstract-float': - return fullF64Range({ + return scalarF64Range({ pos_sub: Math.ceil((count * 1) / 5), pos_norm: Math.ceil((count * 4) / 5), }); case 'f32': - return fullF32Range({ + return scalarF32Range({ pos_sub: Math.ceil((count * 1) / 5), pos_norm: Math.ceil((count * 4) / 5), }); case 'f16': - return fullF16Range({ + return scalarF16Range({ pos_sub: Math.ceil((count * 1) / 5), pos_norm: Math.ceil((count * 4) / 5), }); @@ -186,6 +295,9 @@ export function fullRangeForType(type: Type, count?: number) { ); case 'u32': return linearRange(0, kValue.u32.max, count).map(f => Math.floor(f)); + case 'abstract-int': + // Returned values are already ints, so don't need to be floored. + return linearRangeBigInt(kValue.i64.negative.min, kValue.i64.positive.max, count); } unreachable(); } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cos.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cos.spec.ts index b65593ccaa..361cb8ed99 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cos.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cos.spec.ts @@ -6,18 +6,17 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinus3PiTo3Pi, + minusThreePiToThreePiRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -25,7 +24,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -39,10 +38,15 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinus3PiTo3Pi, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusThreePiToThreePiRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -56,7 +60,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -70,8 +74,41 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(0)], 'constant' ); }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2args: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cosh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cosh.spec.ts index 126fc19e7e..aeb5457675 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cosh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cosh.spec.ts @@ -6,11 +6,9 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -24,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -41,13 +39,17 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const expectedResult = isRepresentable(Math.cosh(t.params.value), elementType(type)); + const expectedResult = isRepresentable( + Math.cosh(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -57,22 +59,40 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; -g.test('integer_argument') - .desc( - ` -Validates that scalar and vector integer arguments are rejected by ${builtin}() -` - ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) .fn(t => { - const type = kIntegerArgumentTypes[t.params.type]; - validateConstOrOverrideBuiltinEval( - t, - builtin, - /* expectedResult */ type === TypeF32, - [type.create(0)], - 'constant' + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` ); }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countLeadingZeros.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countLeadingZeros.spec.ts new file mode 100644 index 0000000000..15791edc80 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countLeadingZeros.spec.ts @@ -0,0 +1,198 @@ +const builtin = 'countLeadingZeros'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .fn(t => { + const expectedResult = true; // countLeadingZeros() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValuesTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kFloatTypes = objectsToRecord([Type.u32, ...kFloatScalarsAndVectors]); + +g.test('float_argument') + .desc( + ` +Validates that float arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kFloatTypes))) + .fn(t => { + const type = kFloatTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.u32, + [type.create(0)], + 'constant' + ); + }); + +const kTests: { + readonly [name: string]: { + /** Arguments to pass to the builtin with parentheses. */ + readonly args: string; + /** Should the test case pass. */ + readonly pass: boolean; + /** Additional setup code in the function scope. */ + readonly preamble?: string; + }; +} = { + valid: { + args: '(1u)', + pass: true, + }, + // Number of arguments. + no_parens: { + args: '', + pass: false, + }, + too_few_args: { + args: '()', + pass: false, + }, + too_many_args: { + args: '(1u,2u)', + pass: false, + }, + // Arguments types (only 1 argument for this builtin). + alias: { + args: '(u32_alias(1))', + pass: true, + }, + bool: { + args: '(false)', + pass: false, + }, + vec_bool: { + args: '(vec2<bool>(false,true))', + pass: false, + }, + matrix: { + args: '(mat2x2(1,1,1,1))', + pass: false, + }, + atomic: { + args: '(a)', + pass: false, + }, + array: { + preamble: 'var arry: array<u32, 5>;', + args: '(arry)', + pass: false, + }, + array_runtime: { + args: '(k.arry)', + pass: false, + }, + struct: { + preamble: 'var x: A;', + args: '(x)', + pass: false, + }, + enumerant: { + args: '(read_write)', + pass: false, + }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(p)', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(*p)', + pass: true, + }, + sampler: { + args: '(s)', + pass: false, + }, + texture: { + args: '(t)', + pass: false, + }, +}; + +g.test('arguments') + .desc(`Test compilation validation of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const test = kTests[t.params.test]; + t.expectCompileResult( + test.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${test.preamble ? test.preamble : ''} + _ = ${builtin}${test.args}; + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countOneBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countOneBits.spec.ts new file mode 100644 index 0000000000..b083c7ca4b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countOneBits.spec.ts @@ -0,0 +1,198 @@ +const builtin = 'countOneBits'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .fn(t => { + const expectedResult = true; // countOneBits() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValuesTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kFloatTypes = objectsToRecord([Type.u32, ...kFloatScalarsAndVectors]); + +g.test('float_argument') + .desc( + ` +Validates that float arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kFloatTypes))) + .fn(t => { + const type = kFloatTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.u32, + [type.create(0)], + 'constant' + ); + }); + +const kTests: { + readonly [name: string]: { + /** Arguments to pass to the builtin with parentheses. */ + readonly args: string; + /** Should the test case pass. */ + readonly pass: boolean; + /** Additional setup code in the function scope. */ + readonly preamble?: string; + }; +} = { + valid: { + args: '(1u)', + pass: true, + }, + // Number of arguments. + no_parens: { + args: '', + pass: false, + }, + too_few_args: { + args: '()', + pass: false, + }, + too_many_args: { + args: '(1u,2u)', + pass: false, + }, + // Arguments types (only 1 argument for this builtin). + alias: { + args: '(u32_alias(1))', + pass: true, + }, + bool: { + args: '(false)', + pass: false, + }, + vec_bool: { + args: '(vec2<bool>(false,true))', + pass: false, + }, + matrix: { + args: '(mat2x2(1,1,1,1))', + pass: false, + }, + atomic: { + args: '(a)', + pass: false, + }, + array: { + preamble: 'var arry: array<u32, 5>;', + args: '(arry)', + pass: false, + }, + array_runtime: { + args: '(k.arry)', + pass: false, + }, + struct: { + preamble: 'var x: A;', + args: '(x)', + pass: false, + }, + enumerant: { + args: '(read_write)', + pass: false, + }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(p)', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(*p)', + pass: true, + }, + sampler: { + args: '(s)', + pass: false, + }, + texture: { + args: '(t)', + pass: false, + }, +}; + +g.test('arguments') + .desc(`Test compilation validation of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const test = kTests[t.params.test]; + t.expectCompileResult( + test.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${test.preamble ? test.preamble : ''} + _ = ${builtin}${test.args}; + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countTrailingZeros.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countTrailingZeros.spec.ts new file mode 100644 index 0000000000..800537d348 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/countTrailingZeros.spec.ts @@ -0,0 +1,198 @@ +const builtin = 'countTrailingZeros'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .fn(t => { + const expectedResult = true; // countTrailingZeros() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValuesTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kFloatTypes = objectsToRecord([Type.u32, ...kFloatScalarsAndVectors]); + +g.test('float_argument') + .desc( + ` +Validates that float arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kFloatTypes))) + .fn(t => { + const type = kFloatTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.u32, + [type.create(0)], + 'constant' + ); + }); + +const kTests: { + readonly [name: string]: { + /** Arguments to pass to the builtin with parentheses. */ + readonly args: string; + /** Should the test case pass. */ + readonly pass: boolean; + /** Additional setup code in the function scope. */ + readonly preamble?: string; + }; +} = { + valid: { + args: '(1u)', + pass: true, + }, + // Number of arguments. + no_parens: { + args: '', + pass: false, + }, + too_few_args: { + args: '()', + pass: false, + }, + too_many_args: { + args: '(1u,2u)', + pass: false, + }, + // Arguments types (only 1 argument for this builtin). + alias: { + args: '(u32_alias(1))', + pass: true, + }, + bool: { + args: '(false)', + pass: false, + }, + vec_bool: { + args: '(vec2<bool>(false,true))', + pass: false, + }, + matrix: { + args: '(mat2x2(1,1,1,1))', + pass: false, + }, + atomic: { + args: '(a)', + pass: false, + }, + array: { + preamble: 'var arry: array<u32, 5>;', + args: '(arry)', + pass: false, + }, + array_runtime: { + args: '(k.arry)', + pass: false, + }, + struct: { + preamble: 'var x: A;', + args: '(x)', + pass: false, + }, + enumerant: { + args: '(read_write)', + pass: false, + }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(p)', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(*p)', + pass: true, + }, + sampler: { + args: '(s)', + pass: false, + }, + texture: { + args: '(t)', + pass: false, + }, +}; + +g.test('arguments') + .desc(`Test compilation validation of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const test = kTests[t.params.test]; + t.expectCompileResult( + test.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${test.preamble ? test.preamble : ''} + _ = ${builtin}${test.args}; + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cross.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cross.spec.ts new file mode 100644 index 0000000000..35dacb65d8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/cross.spec.ts @@ -0,0 +1,122 @@ +const builtin = 'cross'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatVec3, + scalarTypeOf, + ScalarType, +} from '../../../../../util/conversion.js'; +import { QuantizeFunc, quantizeToF16, quantizeToF32 } from '../../../../../util/math.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatVec3); + +function quantizeFunctionForScalarType(type: ScalarType): QuantizeFunc<number> { + switch (type) { + case Type.f32: + return quantizeToF32; + case Type.f16: + return quantizeToF16; + default: + return (v: number) => v; + } +} + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('a', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + .expand('b', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let expectedResult = true; + + const scalarType = scalarTypeOf(kValidArgumentTypes[t.params.type]); + const quantizeFn = quantizeFunctionForScalarType(scalarType); + + // Should be invalid if the cross product calculations result in intermediate + // values that exceed the maximum representable float value for the given type. + const a = Number(t.params.a); + const b = Number(t.params.b); + const ab = quantizeFn(a * b); + if (ab === Infinity || ab === -Infinity) { + expectedResult = false; + } + + const type = kValidArgumentTypes[t.params.type]; + + // Validates cross(vec3(a, a, a), vec3(b, b, b)); + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.a), type.create(t.params.b)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(vec3(0), vec3(1))', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_1arg: '(1.0)', + bad_3arg: '(1.0, 2.0, 3.0)', + // Wrong vector size + bad_vec2: '(vec2(0), vec2(1))', + bad_vec4: '(vec4(0), vec4(1))', + // Bad value for arg 0 + bad_0bool: '(false, vec3(1))', + bad_0array: '(array(1.1,2.2), vec3(1))', + bad_0struct: '(modf(2.2), vec3(1))', + // Bad value type for arg 1 + bad_1bool: '(vec3(0), true)', + bad_1array: '(vec3(0), array(1.1,2.2))', + bad_1struct: '(vec3(0), modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/degrees.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/degrees.spec.ts index 154455857d..058f6ffa6c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/degrees.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/degrees.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -24,7 +23,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -41,13 +40,17 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const expectedResult = isRepresentable((t.params.value * 180) / Math.PI, elementType(type)); + const expectedResult = isRepresentable( + (Number(t.params.value) * 180) / Math.PI, + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -57,7 +60,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -71,8 +74,41 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_too_few: '()', + bad_too_many: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/derivatives.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/derivatives.spec.ts new file mode 100644 index 0000000000..54620ce179 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/derivatives.spec.ts @@ -0,0 +1,129 @@ +export const description = ` +Validation tests for derivative builtins. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kConcreteF16ScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kDerivativeBuiltins = [ + 'dpdx', + 'dpdxCoarse', + 'dpdxFine', + 'dpdy', + 'dpdyCoarse', + 'dpdyFine', + 'fwidth', + 'fwidthCoarse', + 'fwidthFine', +]; + +const kEntryPoints = { + none: { supportsDerivative: true, code: `` }, + fragment: { + supportsDerivative: true, + code: `@fragment +fn main() { + foo(); +}`, + }, + vertex: { + supportsDerivative: false, + code: `@vertex +fn main() -> @builtin(position) vec4f { + foo(); + return vec4f(); +}`, + }, + compute: { + supportsDerivative: false, + code: `@compute @workgroup_size(1) +fn main() { + foo(); +}`, + }, + fragment_and_compute: { + supportsDerivative: false, + code: `@fragment +fn main1() { + foo(); +} + +@compute @workgroup_size(1) +fn main2() { + foo(); +} +`, + }, + compute_without_call: { + supportsDerivative: true, + code: `@compute @workgroup_size(1) +fn main() { +} +`, + }, +}; + +g.test('only_in_fragment') + .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') + .desc( + ` +Derivative functions must only be used in the fragment shader stage. +` + ) + .params(u => + u.combine('entry_point', keysOf(kEntryPoints)).combine('call', ['bar', ...kDerivativeBuiltins]) + ) + .fn(t => { + const config = kEntryPoints[t.params.entry_point]; + const code = ` +${config.code} +fn bar(f : f32) -> f32 { return f; } + +fn foo() { + _ = ${t.params.call}(1.0); +}`; + t.expectCompileResult(t.params.call === 'bar' || config.supportsDerivative, code); + }); + +// The list of invalid argument types to test, with an f32 control case. +const kArgumentTypes = objectsToRecord([ + Type.f32, + ...kConcreteIntegerScalarsAndVectors, + ...kConcreteF16ScalarsAndVectors, + Type.mat2x2f, +]); + +g.test('invalid_argument_types') + .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions') + .desc( + ` +Derivative builtins only accept f32 scalar and vector types. +` + ) + .params(u => + u.combine('type', keysOf(kArgumentTypes)).combine('call', ['', ...kDerivativeBuiltins]) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kArgumentTypes[t.params.type]; + const code = ` +${scalarTypeOf(kArgumentTypes[t.params.type]) === Type.f16 ? 'enable f16;' : ''} + +fn foo() { + let x: ${type.toString()} = ${t.params.call}(${type.create(1).wgsl()}); +}`; + t.expectCompileResult(kArgumentTypes[t.params.type] === Type.f32 || t.params.call === '', code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/determinant.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/determinant.spec.ts new file mode 100644 index 0000000000..1974e7ef99 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/determinant.spec.ts @@ -0,0 +1,95 @@ +const builtin = 'determinant'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// Generate a dictionary mapping each matrix type variation (columns,rows, +// floating point type) to a nontrivial matrix value of that type. +const kMatrixCases = ([2, 3, 4] as const) + .flatMap(cols => + ([2, 3, 4] as const).flatMap(rows => + (['abstract-int', 'abstract-float', 'f32', 'f16'] as const).map(type => ({ + [`mat${cols}x${rows}_${type}`]: (() => { + const suffix = (() => { + switch (type) { + case 'abstract-int': + return ''; + case 'abstract-float': + return '.0'; + case 'f32': + return 'f'; + case 'f16': + return 'h'; + } + })(); + return `(mat${cols}x${rows}(${[...Array(cols * rows).keys()] + .map(e => `${e}${suffix}`) + .join(', ')}))`; + })(), + })) + ) + ) + .reduce((a, b) => ({ ...a, ...b }), {}); + +g.test('matrix_args') + .desc(`Test compilation failure of ${builtin} with variously shaped matrices`) + .params(u => + u + .combine('cols', [2, 3, 4] as const) + .combine('rows', [2, 3, 4] as const) + .combine('type', ['abstract-int', 'abstract-float', 'f32', 'f16'] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const cols = t.params.cols; + const rows = t.params.rows; + const type = t.params.type; + const arg = kMatrixCases[`mat${cols}x${rows}_${type}`]; + t.expectCompileResult( + cols === rows, + t.wrapInEntryPoint(`const c = ${builtin}${arg};`, type === 'f16' ? ['f16'] : []) + ); + }); + +const kArgCases = { + good: '(mat2x2(0.0, 2.0, 3.0, 4.0))', // Included to check test implementation + bad_no_parens: '', + // Bad number of args + bad_too_few: '()', + bad_too_many: '(mat2x2(0.0, 2.0, 3.0, 4.0), mat2x2(0.0, 2.0, 3.0, 4.0))', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/distance.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/distance.spec.ts new file mode 100644 index 0000000000..e41a8dd31c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/distance.spec.ts @@ -0,0 +1,149 @@ +const builtin = 'distance'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, + ScalarType, +} from '../../../../../util/conversion.js'; +import { QuantizeFunc, quantizeToF16, quantizeToF32 } from '../../../../../util/math.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +function quantizeFunctionForScalarType(type: ScalarType): QuantizeFunc<number> { + switch (type) { + case Type.f32: + return quantizeToF32; + case Type.f16: + return quantizeToF16; + default: + return (v: number) => v; + } +} + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('a', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + .expand('b', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let expectedResult = true; + + const scalarType = scalarTypeOf(kValidArgumentTypes[t.params.type]); + const quantizeFn = quantizeFunctionForScalarType(scalarType); + + // Distance equation: length(a - b) + // Should be invalid if the calculations result in intermediate values that + // exceed the maximum representable float value for the given type. + const a = Number(t.params.a); + const b = Number(t.params.b); + const ab = quantizeFn(a - b); + + if (!Number.isFinite(ab)) { + expectedResult = false; + } + + // Only calculates the full length if the type is a vector. Otherwise abs(a-b) is used. + if (kValidArgumentTypes[t.params.type].width > 1) { + const ab2 = quantizeFn(ab * ab); + const sqrLen = quantizeFn(ab2 * kValidArgumentTypes[t.params.type].width); + // Square root does not need to be calculated because it can never fail if + // the previous results are finite. + + if (!Number.isFinite(ab2) || !Number.isFinite(sqrLen)) { + expectedResult = false; + } + } + + const type = kValidArgumentTypes[t.params.type]; + + // Validates distance(vecN(a), vecN(b)) or distance(a, b); + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.a), type.create(t.params.b)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(vec3(0), vec3(1))', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_1arg: '(vec3(0))', + bad_3arg: '(vec3(0), vec3(1), vec3(2))', + // Bad value for arg 0 + bad_0bool: '(false, vec3(1))', + bad_0array: '(array(1.1,2.2), vec3(1))', + bad_0struct: '(modf(2.2), vec3(1))', + bad_0int: '(0i, vec3(1))', + bad_0vec2i: '(vec2i(), vec3(1))', + bad_0vec3i: '(vec3i(), vec3(1))', + bad_0vec4i: '(vec4i(), vec3(1))', + bad_0uint: '(0u, vec3(1))', + bad_0vec2u: '(vec2u(), vec3(1))', + bad_0vec3u: '(vec3u(), vec3(1))', + bad_0vec4u: '(vec4u(), vec3(1))', + // Bad value type for arg 1 + bad_1bool: '(vec3(0), true)', + bad_1array: '(vec3(0), array(1.1,2.2))', + bad_1struct: '(vec3(0), modf(2.2))', + bad_1int: '(vec3(0), 0i)', + bad_1vec2i: '(vec3(0), vec2i())', + bad_1vec3i: '(vec3(0), vec3i())', + bad_1vec4i: '(vec3(0), vec4i())', + bad_1uint: '(vec3(0), 0u)', + bad_1vec2u: '(vec3(0), vec2u())', + bad_1vec3u: '(vec3(0), vec3u())', + bad_1vec4u: '(vec3(0), vec4u())', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/dot4I8Packed.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/dot4I8Packed.spec.ts new file mode 100644 index 0000000000..c079e08cb1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/dot4I8Packed.spec.ts @@ -0,0 +1,66 @@ +export const description = `Validate dot4I8Packed`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'dot4I8Packed'; +const kArgCases = { + good: '(1u,2u)', + bad_0args: '()', + bad_1args: '(1u)', + bad_3args: '(1u,2u,3u)', + bad_0i32: '(1i,2u)', + bad_0f32: '(1f,2u)', + bad_0bool: '(false,2u)', + bad_0vec2u: '(vec2u(),2u)', + bad_1i32: '(1u,2i)', + bad_1f32: '(1u,2f)', + bad_1bool: '(1u,true)', + bad_1vec2u: '(1u,vec2u())', + bad_bool_bool: '(false,true)', + bad_bool2_bool2: '(vec2<bool>(),vec2(false,true))', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/dot4U8Packed.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/dot4U8Packed.spec.ts new file mode 100644 index 0000000000..bd9356777e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/dot4U8Packed.spec.ts @@ -0,0 +1,66 @@ +export const description = `Validate dot4U8Packed`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'dot4U8Packed'; +const kArgCases = { + good: '(1u,2u)', + bad_0args: '()', + bad_1args: '(1u)', + bad_3args: '(1u,2u,3u)', + bad_0i32: '(1i,2u)', + bad_0f32: '(1f,2u)', + bad_0bool: '(false,2u)', + bad_0vec2u: '(vec2u(),2u)', + bad_1i32: '(1u,2i)', + bad_1f32: '(1u,2f)', + bad_1bool: '(1u,true)', + bad_1vec2u: '(1u,vec2u())', + bad_bool_bool: '(false,true)', + bad_bool2_bool2: '(vec2<bool>(),vec2(false,true))', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp.spec.ts index 244e91f2ae..f0a9b217b1 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp.spec.ts @@ -7,24 +7,52 @@ import { makeTestGroup } from '../../../../../../common/framework/test_group.js' import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { kValue } from '../../../../../util/constants.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { kConstantAndOverrideStages, + rangeForType, stageSupportsType, validateConstOrOverrideBuiltinEval, } from './const_override_validation.js'; export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +const valueForType = rangeForType( + [ + -1e2, + -1e3, + -4, + -3, + -2, + -1, + -1e-1, + -1e-2, + -1e-3, + 0, + 1e-3, + 1e-2, + 1e-1, + 1, + 2, + 3, + 4, + 1e2, + 1e3, + Math.log2(kValue.f16.positive.max) - 0.1, + Math.log2(kValue.f16.positive.max) + 0.1, + Math.log2(kValue.f32.positive.max) - 0.1, + Math.log2(kValue.f32.positive.max) + 0.1, + ], + [-100n, -1000n, -4n, -3n, -2n, -1n, 0n, 1n, 2n, 3n, 4n, 100n, 1000n] +); g.test('values') .desc( @@ -38,40 +66,20 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .combine('value', [ - -1e2, - -1e3, - -4, - -3, - -2, - -1, - -1e-1, - -1e-2, - -1e-3, - 0, - 1e-3, - 1e-2, - 1e-1, - 1, - 2, - 3, - 4, - 1e2, - 1e3, - Math.log2(kValue.f16.positive.max) - 0.1, - Math.log2(kValue.f16.positive.max) + 0.1, - Math.log2(kValue.f32.positive.max) - 0.1, - Math.log2(kValue.f32.positive.max) + 0.1, - ]) + .expand('value', u => valueForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const expectedResult = isRepresentable(Math.exp(t.params.value), elementType(type)); + const expectedResult = isRepresentable( + Math.exp(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -81,22 +89,40 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; -g.test('integer_argument') - .desc( - ` -Validates that scalar and vector integer arguments are rejected by ${builtin}() -` - ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) .fn(t => { - const type = kIntegerArgumentTypes[t.params.type]; - validateConstOrOverrideBuiltinEval( - t, - builtin, - /* expectedResult */ type === TypeF32, - [type.create(0)], - 'constant' + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` ); }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp2.spec.ts index 9addbc076b..0cf3d03542 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp2.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/exp2.spec.ts @@ -7,24 +7,52 @@ import { makeTestGroup } from '../../../../../../common/framework/test_group.js' import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { kValue } from '../../../../../util/constants.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { kConstantAndOverrideStages, + rangeForType, stageSupportsType, validateConstOrOverrideBuiltinEval, } from './const_override_validation.js'; export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +const valueForType = rangeForType( + [ + -1e2, + -1e3, + -4, + -3, + -2, + -1, + -1e-1, + -1e-2, + -1e-3, + 0, + 1e-3, + 1e-2, + 1e-1, + 1, + 2, + 3, + 4, + 1e2, + 1e3, + Math.log2(kValue.f16.positive.max) - 0.1, + Math.log2(kValue.f16.positive.max) + 0.1, + Math.log2(kValue.f32.positive.max) - 0.1, + Math.log2(kValue.f32.positive.max) + 0.1, + ], + [-100n, -1000n, -4n, -3n, -2n, -1n, 0n, 1n, 2n, 3n, 4n, 100n, 1000n] +); g.test('values') .desc( @@ -38,40 +66,20 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .combine('value', [ - -1e2, - -1e3, - -4, - -3, - -2, - -1, - -1e-1, - -1e-2, - -1e-3, - 0, - 1e-3, - 1e-2, - 1e-1, - 1, - 2, - 3, - 4, - 1e2, - 1e3, - Math.log2(kValue.f16.positive.max) - 0.1, - Math.log2(kValue.f16.positive.max) + 0.1, - Math.log2(kValue.f32.positive.max) - 0.1, - Math.log2(kValue.f32.positive.max) + 0.1, - ]) + .expand('value', u => valueForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const expectedResult = isRepresentable(Math.pow(2, t.params.value), elementType(type)); + const expectedResult = isRepresentable( + Math.pow(2, Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -81,22 +89,40 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; -g.test('integer_argument') - .desc( - ` -Validates that scalar and vector integer arguments are rejected by ${builtin}() -` - ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) .fn(t => { - const type = kIntegerArgumentTypes[t.params.type]; - validateConstOrOverrideBuiltinEval( - t, - builtin, - /* expectedResult */ type === TypeF32, - [type.create(0)], - 'constant' + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` ); }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/extractBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/extractBits.spec.ts new file mode 100644 index 0000000000..921e157408 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/extractBits.spec.ts @@ -0,0 +1,218 @@ +const builtin = 'extractBits'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, + u32, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors on valid inputs +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + .combineWithParams([ + { offset: 0, count: 0 }, + { offset: 0, count: 31 }, + { offset: 0, count: 32 }, + { offset: 4, count: 0 }, + { offset: 4, count: 27 }, + { offset: 4, count: 28 }, + { offset: 16, count: 0 }, + { offset: 16, count: 15 }, + { offset: 16, count: 16 }, + { offset: 32, count: 0 }, + ] as const) + ) + .fn(t => { + const expectedResult = true; // extractBits() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [ + kValuesTypes[t.params.type].create(t.params.value), + u32(t.params.offset), + u32(t.params.count), + ], + t.params.stage + ); + }); + +g.test('count_offset') + .desc( + ` +Validates that count and offset must be smaller than the size of the primitive. +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .beginSubcases() + .combineWithParams([ + // offset + count < 32 + { offset: 0, count: 31 }, + { offset: 1, count: 30 }, + { offset: 31, count: 0 }, + { offset: 30, count: 1 }, + // offset + count == 32 + { offset: 0, count: 32 }, + { offset: 1, count: 31 }, + { offset: 16, count: 16 }, + { offset: 31, count: 1 }, + { offset: 32, count: 0 }, + // offset + count > 32 + { offset: 2, count: 31 }, + { offset: 31, count: 2 }, + // offset > 32 + { offset: 33, count: 0 }, + { offset: 33, count: 1 }, + // count > 32 + { offset: 0, count: 33 }, + { offset: 1, count: 33 }, + ] as const) + ) + .fn(t => { + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ t.params.offset + t.params.count <= 32, + [u32(1), u32(t.params.offset), u32(t.params.count)], + t.params.stage + ); + }); + +interface Argument { + /** Argument as a string. */ + readonly arg: string; + /** Is this a valid argument type. Note that all args must be valid for the call to be valid. */ + readonly pass: boolean; + /** Additional setup code necessary for this arg in the function scope. */ + readonly preamble?: string; +} + +function typesToArguments(types: readonly Type[], pass: boolean): Record<string, Argument> { + return types.reduce( + (res, type) => ({ + ...res, + [type.toString()]: { arg: type.create(0).wgsl(), pass }, + }), + {} + ); +} + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kInputArgTypes: { readonly [name: string]: Argument } = { + ...typesToArguments([Type.u32], true), + ...typesToArguments([...kFloatScalarsAndVectors, Type.bool, Type.mat2x2f], false), + alias: { arg: 'u32_alias(1)', pass: true }, + vec_bool: { arg: 'vec2<bool>(false,true)', pass: false }, + atomic: { arg: 'a', pass: false }, + array: { + preamble: 'var arry: array<u32, 5>;', + arg: 'arry', + pass: false, + }, + array_runtime: { arg: 'k.arry', pass: false }, + struct: { + preamble: 'var x: A;', + arg: 'x', + pass: false, + }, + enumerant: { arg: 'read_write', pass: false }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + arg: 'p', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + arg: '*p', + pass: true, + }, + sampler: { arg: 's', pass: false }, + texture: { arg: 't', pass: false }, +}; + +g.test('typed_arguments') + .desc( + ` +Test compilation validation of ${builtin} with variously typed arguments + - For failing input types, just use the same type for offset and count to reduce combinations. +` + ) + .params(u => + u + .combine('input', keysOf(kInputArgTypes)) + .beginSubcases() + .combine('offset', keysOf(kInputArgTypes)) + .expand('count', u => (kInputArgTypes[u.input].pass ? keysOf(kInputArgTypes) : [u.offset])) + ) + .fn(t => { + const input = kInputArgTypes[t.params.input]; + const offset = kInputArgTypes[t.params.offset]; + const count = kInputArgTypes[t.params.count]; + t.expectCompileResult( + input.pass && offset.pass && count.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${input.preamble ? input.preamble : ''} + ${offset.preamble && offset !== input ? offset.preamble : ''} + ${count.preamble && count !== input && count !== offset ? count.preamble : ''} + _ = ${builtin}(${input.arg},${offset.arg},${count.arg}); + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1u,0u,1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/faceForward.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/faceForward.spec.ts new file mode 100644 index 0000000000..4a87b5cacc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/faceForward.spec.ts @@ -0,0 +1,152 @@ +const builtin = 'faceForward'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatVectors, + scalarTypeOf, + ScalarType, +} from '../../../../../util/conversion.js'; +import { QuantizeFunc, quantizeToF16, quantizeToF32 } from '../../../../../util/math.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatVectors); + +function quantizeFunctionForScalarType(type: ScalarType): QuantizeFunc<number> { + switch (type) { + case Type.f32: + return quantizeToF32; + case Type.f16: + return quantizeToF16; + default: + return (v: number) => v; + } +} + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('a', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + .expand('b', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + .expand('c', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let expectedResult = true; + + const scalarType = scalarTypeOf(kValidArgumentTypes[t.params.type]); + const quantizeFn = quantizeFunctionForScalarType(scalarType); + + // Face Forward equation: dot(b, c) < 0 ? -a : a + // Should be invalid if the calculations result in intermediate values that + // exceed the maximum representable float value for the given type. + const b = Number(t.params.b); + const c = Number(t.params.c); + const bc = quantizeFn(b * c); + const dp = quantizeFn(bc * kValidArgumentTypes[t.params.type].width); + + if (!Number.isFinite(dp)) { + expectedResult = false; + } + + const type = kValidArgumentTypes[t.params.type]; + + // Validates faceForward(vecN(a), vecN(b), vecN(c)); + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.a), type.create(t.params.b), type.create(t.params.c)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(vec3(0), vec3(1), vec3(0.5))', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_1arg: '(vec3(0))', + bad_2arg: '(vec3(0), vec3(1))', + bad_4arg: '(vec3(0), vec3(1), vec3(0.5), vec3(3))', + // Bad value for arg 0 + bad_0bool: '(false, vec3(1), vec3(0.5))', + bad_0array: '(array(1.1,2.2), vec3(1), vec3(0.5))', + bad_0struct: '(modf(2.2), vec3(1), vec3(0.5))', + bad_0int: '(1i, vec3(1), vec3(0.5))', + bad_0uint: '(1u, vec3(1), vec3(0.5))', + bad_0vec2i: '(vec2i(0), vec2(1), vec2(0.5))', + bad_0vec3i: '(vec3i(0), vec3(1), vec3(0.5))', + bad_0vec4i: '(vec4i(0), vec4(1), vec4(0.5))', + bad_0vec2u: '(vec2u(0), vec2(1), vec2(0.5))', + bad_0vec3u: '(vec3u(0), vec3(1), vec3(0.5))', + bad_0vec4u: '(vec4u(0), vec4(1), vec4(0.5))', + // Bad value type for arg 1 + bad_1bool: '(vec3(0), true, vec3(0.5))', + bad_1array: '(vec3(0), array(1.1,2.2), vec3(0.5))', + bad_1struct: '(vec3(0), modf(2.2), vec3(0.5))', + bad_1int: '(vec3(0), 1i, vec3(0.5))', + bad_1uint: '(vec3(0), 1u, vec3(0.5))', + bad_1vec2i: '(vec2(1), vec2i(1), vec2(0.5))', + bad_1vec3i: '(vec3(1), vec3i(1), vec3(0.5))', + bad_1vec4i: '(vec4(1), vec4i(1), vec4(0.5))', + bad_1vec2u: '(vec2(1), vec2u(1), vec2(0.5))', + bad_1vec3u: '(vec3(1), vec3u(1), vec3(0.5))', + bad_1vec4u: '(vec4(1), vec4u(1), vec4(0.5))', + // Bad value type for arg 2 + bad_2bool: '(vec3(0), vec3(1), true)', + bad_2array: '(vec3(0), vec3(1), array(1.1,2.2))', + bad_2struct: '(vec3(0), vec3(1), modf(2.2))', + bad_2int: '(vec3(0), vec3(1), 1i)', + bad_2uint: '(vec3(0), vec3(1), 1u)', + bad_2vec2i: '(vec2(1), vec2(1), vec2i(1))', + bad_2vec3i: '(vec3(1), vec3(1), vec3i(1))', + bad_2vec4i: '(vec4(1), vec4(1), vec4i(1))', + bad_2vec2u: '(vec2(1), vec2(1), vec2u(1))', + bad_2vec3u: '(vec3(1), vec3(1), vec3u(1))', + bad_2vec4u: '(vec4(1), vec4(1), vec4u(1))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/firstLeadingBit.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/firstLeadingBit.spec.ts new file mode 100644 index 0000000000..4aeb7e8bd2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/firstLeadingBit.spec.ts @@ -0,0 +1,198 @@ +const builtin = 'firstLeadingBit'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .fn(t => { + const expectedResult = true; // firstLeadingBit() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValuesTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kFloatTypes = objectsToRecord([Type.u32, ...kFloatScalarsAndVectors]); + +g.test('float_argument') + .desc( + ` +Validates that float arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kFloatTypes))) + .fn(t => { + const type = kFloatTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.u32, + [type.create(0)], + 'constant' + ); + }); + +const kTests: { + readonly [name: string]: { + /** Arguments to pass to the builtin with parentheses. */ + readonly args: string; + /** Should the test case pass. */ + readonly pass: boolean; + /** Additional setup code in the function scope. */ + readonly preamble?: string; + }; +} = { + valid: { + args: '(1u)', + pass: true, + }, + // Number of arguments. + no_parens: { + args: '', + pass: false, + }, + too_few_args: { + args: '()', + pass: false, + }, + too_many_args: { + args: '(1u,2u)', + pass: false, + }, + // Arguments types (only 1 argument for this builtin). + alias: { + args: '(u32_alias(1))', + pass: true, + }, + bool: { + args: '(false)', + pass: false, + }, + vec_bool: { + args: '(vec2<bool>(false,true))', + pass: false, + }, + matrix: { + args: '(mat2x2(1,1,1,1))', + pass: false, + }, + atomic: { + args: '(a)', + pass: false, + }, + array: { + preamble: 'var arry: array<u32, 5>;', + args: '(arry)', + pass: false, + }, + array_runtime: { + args: '(k.arry)', + pass: false, + }, + struct: { + preamble: 'var x: A;', + args: '(x)', + pass: false, + }, + enumerant: { + args: '(read_write)', + pass: false, + }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(p)', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(*p)', + pass: true, + }, + sampler: { + args: '(s)', + pass: false, + }, + texture: { + args: '(t)', + pass: false, + }, +}; + +g.test('arguments') + .desc(`Test compilation validation of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const test = kTests[t.params.test]; + t.expectCompileResult( + test.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${test.preamble ? test.preamble : ''} + _ = ${builtin}${test.args}; + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/firstTrailingBit.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/firstTrailingBit.spec.ts new file mode 100644 index 0000000000..897a213fb8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/firstTrailingBit.spec.ts @@ -0,0 +1,198 @@ +const builtin = 'firstTrailingBit'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .fn(t => { + const expectedResult = true; // firstTrailingBit() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValuesTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kFloatTypes = objectsToRecord([Type.u32, ...kFloatScalarsAndVectors]); + +g.test('float_argument') + .desc( + ` +Validates that float arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kFloatTypes))) + .fn(t => { + const type = kFloatTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.u32, + [type.create(0)], + 'constant' + ); + }); + +const kTests: { + readonly [name: string]: { + /** Arguments to pass to the builtin with parentheses. */ + readonly args: string; + /** Should the test case pass. */ + readonly pass: boolean; + /** Additional setup code in the function scope. */ + readonly preamble?: string; + }; +} = { + valid: { + args: '(1u)', + pass: true, + }, + // Number of arguments. + no_parens: { + args: '', + pass: false, + }, + too_few_args: { + args: '()', + pass: false, + }, + too_many_args: { + args: '(1u,2u)', + pass: false, + }, + // Arguments types (only 1 argument for this builtin). + alias: { + args: '(u32_alias(1))', + pass: true, + }, + bool: { + args: '(false)', + pass: false, + }, + vec_bool: { + args: '(vec2<bool>(false,true))', + pass: false, + }, + matrix: { + args: '(mat2x2(1,1,1,1))', + pass: false, + }, + atomic: { + args: '(a)', + pass: false, + }, + array: { + preamble: 'var arry: array<u32, 5>;', + args: '(arry)', + pass: false, + }, + array_runtime: { + args: '(k.arry)', + pass: false, + }, + struct: { + preamble: 'var x: A;', + args: '(x)', + pass: false, + }, + enumerant: { + args: '(read_write)', + pass: false, + }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(p)', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(*p)', + pass: true, + }, + sampler: { + args: '(s)', + pass: false, + }, + texture: { + args: '(t)', + pass: false, + }, +}; + +g.test('arguments') + .desc(`Test compilation validation of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const test = kTests[t.params.test]; + t.expectCompileResult( + test.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${test.preamble ? test.preamble : ''} + _ = ${builtin}${test.args}; + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/floor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/floor.spec.ts new file mode 100644 index 0000000000..ca699c6414 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/floor.spec.ts @@ -0,0 +1,108 @@ +const builtin = 'floor'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const expectedResult = true; // floor() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValuesTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); + +g.test('integer_argument') + .desc( + ` +Validates that scalar and vector integer arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) + .fn(t => { + const type = kIntegerArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.f32, + [type.create(0)], + 'constant' + ); + }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2args: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/fract.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/fract.spec.ts new file mode 100644 index 0000000000..c930c06ac4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/fract.spec.ts @@ -0,0 +1,94 @@ +const builtin = 'fract'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() error on invalid inputs. +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValidArgumentTypes[u.type])) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const expectedResult = true; + + const type = kValidArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.value)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/frexp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/frexp.spec.ts new file mode 100644 index 0000000000..16eb29f9ea --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/frexp.spec.ts @@ -0,0 +1,94 @@ +const builtin = 'frexp'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() error on invalid inputs. +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValidArgumentTypes[u.type])) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const expectedResult = true; + + const type = kValidArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.value)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/insertBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/insertBits.spec.ts new file mode 100644 index 0000000000..17065e33ae --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/insertBits.spec.ts @@ -0,0 +1,241 @@ +const builtin = 'insertBits'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, + u32, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors on valid inputs +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type], 5)) + .expand('newbits', u => fullRangeForType(kValuesTypes[u.type], 5)) + .combineWithParams([ + { offset: 0, count: 0 }, + { offset: 0, count: 31 }, + { offset: 0, count: 32 }, + { offset: 4, count: 0 }, + { offset: 4, count: 27 }, + { offset: 4, count: 28 }, + { offset: 16, count: 0 }, + { offset: 16, count: 15 }, + { offset: 16, count: 16 }, + { offset: 32, count: 0 }, + ] as const) + ) + .fn(t => { + const expectedResult = true; // insertBits() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [ + kValuesTypes[t.params.type].create(t.params.value), + kValuesTypes[t.params.type].create(t.params.newbits), + u32(t.params.offset), + u32(t.params.count), + ], + t.params.stage + ); + }); + +g.test('mismatched') + .desc( + ` +Validates that even with valid types, if arg0 and arg1 do not match types ${builtin}() errors +` + ) + .params(u => u.combine('arg0', keysOf(kValuesTypes)).combine('arg1', keysOf(kValuesTypes))) + .fn(t => { + const arg0 = kValuesTypes[t.params.arg0]; + const arg1 = kValuesTypes[t.params.arg1]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ t.params.arg0 === t.params.arg1, + [arg0.create(0), arg1.create(1), u32(0), u32(32)], + 'constant' + ); + }); + +g.test('count_offset') + .desc( + ` +Validates that count and offset must be smaller than the size of the primitive. +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .beginSubcases() + .combineWithParams([ + // offset + count < 32 + { offset: 0, count: 31 }, + { offset: 1, count: 30 }, + { offset: 31, count: 0 }, + { offset: 30, count: 1 }, + // offset + count == 32 + { offset: 0, count: 32 }, + { offset: 1, count: 31 }, + { offset: 16, count: 16 }, + { offset: 31, count: 1 }, + { offset: 32, count: 0 }, + // offset + count > 32 + { offset: 2, count: 31 }, + { offset: 31, count: 2 }, + // offset > 32 + { offset: 33, count: 0 }, + { offset: 33, count: 1 }, + // count > 32 + { offset: 0, count: 33 }, + { offset: 1, count: 33 }, + ] as const) + ) + .fn(t => { + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ t.params.offset + t.params.count <= 32, + [u32(0), u32(1), u32(t.params.offset), u32(t.params.count)], + t.params.stage + ); + }); + +interface Argument { + /** Argument as a string. */ + readonly arg: string; + /** Is this a valid argument type. Note that all args must be valid for the call to be valid. */ + readonly pass: boolean; + /** Additional setup code necessary for this arg in the function scope. */ + readonly preamble?: string; +} + +function typesToArguments(types: readonly Type[], pass: boolean): Record<string, Argument> { + return types.reduce( + (res, type) => ({ + ...res, + [type.toString()]: { arg: type.create(0).wgsl(), pass }, + }), + {} + ); +} + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kInputArgTypes: { readonly [name: string]: Argument } = { + ...typesToArguments([Type.u32], true), + ...typesToArguments([...kFloatScalarsAndVectors, Type.bool, Type.mat2x2f], false), + alias: { arg: 'u32_alias(1)', pass: true }, + vec_bool: { arg: 'vec2<bool>(false,true)', pass: false }, + atomic: { arg: 'a', pass: false }, + array: { + preamble: 'var arry: array<u32, 5>;', + arg: 'arry', + pass: false, + }, + array_runtime: { arg: 'k.arry', pass: false }, + struct: { + preamble: 'var x: A;', + arg: 'x', + pass: false, + }, + enumerant: { arg: 'read_write', pass: false }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + arg: 'p', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + arg: '*p', + pass: true, + }, + sampler: { arg: 's', pass: false }, + texture: { arg: 't', pass: false }, +}; + +g.test('typed_arguments') + .desc( + ` +Test compilation validation of ${builtin} with variously typed arguments + - The input types are matching to reduce testing permutations. Mismatching input types are + validated in 'mismatched' test above. + - For failing input types, just use the same type for offset and count to reduce combinations. +` + ) + .params(u => + u + .combine('input', keysOf(kInputArgTypes)) + .beginSubcases() + .combine('offset', keysOf(kInputArgTypes)) + .expand('count', u => (kInputArgTypes[u.input].pass ? keysOf(kInputArgTypes) : [u.offset])) + ) + .fn(t => { + const input = kInputArgTypes[t.params.input]; + const offset = kInputArgTypes[t.params.offset]; + const count = kInputArgTypes[t.params.count]; + t.expectCompileResult( + input.pass && offset.pass && count.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${input.preamble ? input.preamble : ''} + ${offset.preamble && offset !== input ? offset.preamble : ''} + ${count.preamble && count !== input && count !== offset ? count.preamble : ''} + _ = ${builtin}(${input.arg},${input.arg},${offset.arg},${count.arg}); + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(0u,1u,0u,1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/inverseSqrt.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/inverseSqrt.spec.ts index b2813cbe0a..806500d937 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/inverseSqrt.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/inverseSqrt.spec.ts @@ -6,11 +6,9 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -18,7 +16,7 @@ import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinusTwoToTwo, + minusTwoToTwoRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -26,7 +24,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,17 +38,27 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinusTwoToTwo, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusTwoToTwoRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; const expectedResult = - t.params.value > 0 && isRepresentable(1 / Math.sqrt(t.params.value), elementType(type)); + t.params.value > 0 && + isRepresentable( + 1 / Math.sqrt(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -60,22 +68,40 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; -g.test('integer_argument') - .desc( - ` -Validates that scalar and vector integer arguments are rejected by ${builtin}() -` - ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) .fn(t => { - const type = kIntegerArgumentTypes[t.params.type]; - validateConstOrOverrideBuiltinEval( - t, - builtin, - /* expectedResult */ type === TypeF32, - [type.create(1)], - 'constant' + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` ); }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/length.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/length.spec.ts index 60fbe6e285..003ad6811d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/length.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/length.spec.ts @@ -7,14 +7,13 @@ import { makeTestGroup } from '../../../../../../common/framework/test_group.js' import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { ScalarType, - TypeF16, - TypeF32, - elementType, - kAllFloatScalars, - kAllFloatVector2, - kAllFloatVector3, - kAllFloatVector4, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalar, + kConvertableToFloatVec2, + kConvertableToFloatVec3, + kConvertableToFloatVec4, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -33,7 +32,7 @@ export const g = makeTestGroup(ShaderValidationTest); * formed from `vec` of the element type `type`. */ function calculate( - vec: number[], + vec: (number | bigint)[], type: ScalarType ): { /** @@ -49,16 +48,25 @@ function calculate( /** The computed value of length(). */ result: number; } { - const squareSum = vec.reduce((prev, curr) => prev + curr * curr, 0); + const vec_number = vec.map(e => Number(e)); + const squareSum = vec_number.reduce((prev, curr) => prev + Number(curr) * Number(curr), 0); const result = Math.sqrt(squareSum); return { - isIntermediateRepresentable: isRepresentable(squareSum, type), - isResultRepresentable: isRepresentable(result, type), + isIntermediateRepresentable: isRepresentable( + squareSum, + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ), + isResultRepresentable: isRepresentable( + result, + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ), result, }; } -const kScalarTypes = objectsToRecord(kAllFloatScalars); +const kScalarTypes = objectsToRecord(kConvertableToFloatScalar); g.test('scalar') .desc( @@ -76,7 +84,7 @@ the input scalar value always compiles without error .expand('value', u => fullRangeForType(kScalarTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kScalarTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kScalarTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -92,7 +100,7 @@ the input scalar value always compiles without error ); }); -const kVec2Types = objectsToRecord(kAllFloatVector2); +const kVec2Types = objectsToRecord(kConvertableToFloatVec2); g.test('vec2') .desc( @@ -108,11 +116,11 @@ Validates that constant evaluation and override evaluation of ${builtin}() with .beginSubcases() .expand('x', u => fullRangeForType(kVec2Types[u.type], 5)) .expand('y', u => fullRangeForType(kVec2Types[u.type], 5)) - .expand('_result', u => [calculate([u.x, u.y], elementType(kVec2Types[u.type]))]) + .expand('_result', u => [calculate([u.x, u.y], scalarTypeOf(kVec2Types[u.type]))]) .filter(u => u._result.isResultRepresentable === u._result.isIntermediateRepresentable) ) .beforeAllSubcases(t => { - if (elementType(kVec2Types[t.params.type]) === TypeF16) { + if (scalarTypeOf(kVec2Types[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -127,7 +135,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() with ); }); -const kVec3Types = objectsToRecord(kAllFloatVector3); +const kVec3Types = objectsToRecord(kConvertableToFloatVec3); g.test('vec3') .desc( @@ -144,11 +152,11 @@ Validates that constant evaluation and override evaluation of ${builtin}() with .expand('x', u => fullRangeForType(kVec3Types[u.type], 4)) .expand('y', u => fullRangeForType(kVec3Types[u.type], 4)) .expand('z', u => fullRangeForType(kVec3Types[u.type], 4)) - .expand('_result', u => [calculate([u.x, u.y, u.z], elementType(kVec3Types[u.type]))]) + .expand('_result', u => [calculate([u.x, u.y, u.z], scalarTypeOf(kVec3Types[u.type]))]) .filter(u => u._result.isResultRepresentable === u._result.isIntermediateRepresentable) ) .beforeAllSubcases(t => { - if (elementType(kVec3Types[t.params.type]) === TypeF16) { + if (scalarTypeOf(kVec3Types[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -163,7 +171,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() with ); }); -const kVec4Types = objectsToRecord(kAllFloatVector4); +const kVec4Types = objectsToRecord(kConvertableToFloatVec4); g.test('vec4') .desc( @@ -181,11 +189,11 @@ Validates that constant evaluation and override evaluation of ${builtin}() with .expand('y', u => fullRangeForType(kVec4Types[u.type], 3)) .expand('z', u => fullRangeForType(kVec4Types[u.type], 3)) .expand('w', u => fullRangeForType(kVec4Types[u.type], 3)) - .expand('_result', u => [calculate([u.x, u.y, u.z, u.w], elementType(kVec4Types[u.type]))]) + .expand('_result', u => [calculate([u.x, u.y, u.z, u.w], scalarTypeOf(kVec4Types[u.type]))]) .filter(u => u._result.isResultRepresentable === u._result.isIntermediateRepresentable) ) .beforeAllSubcases(t => { - if (elementType(kVec4Types[t.params.type]) === TypeF16) { + if (scalarTypeOf(kVec4Types[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -200,7 +208,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() with ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -214,7 +222,7 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log.spec.ts index 5d84d0c0be..4ef0d553c9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -23,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,7 +39,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -55,7 +54,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -69,8 +68,41 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2args: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log2.spec.ts index 60f32d99c7..d242e1a410 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log2.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/log2.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -23,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,7 +39,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -55,7 +54,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -69,8 +68,41 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2args: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/max.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/max.spec.ts new file mode 100644 index 0000000000..f32589fcf6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/max.spec.ts @@ -0,0 +1,91 @@ +const builtin = 'max'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllNumericScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kAllNumericScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('a', u => fullRangeForType(kValuesTypes[u.type], 5)) + .expand('b', u => fullRangeForType(kValuesTypes[u.type], 5)) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kValuesTypes[t.params.type]; + const expectedResult = true; // should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.a), type.create(t.params.b)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(1.1, 2.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_1arg: '(1.0)', + bad_3arg: '(1.0, 2.0, 3.0)', + // Bad value for arg 0 + bad_0bool: '(false, 1.0)', + bad_0array: '(array(1.1,2.2), 1.0)', + bad_0struct: '(modf(2.2), 1.0)', + // Bad value type for arg 1 + bad_1bool: '(1.0, true)', + bad_1array: '(1.0, array(1.1,2.2))', + bad_1struct: '(1.0, modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/min.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/min.spec.ts new file mode 100644 index 0000000000..2222c44e92 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/min.spec.ts @@ -0,0 +1,91 @@ +const builtin = 'min'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllNumericScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kAllNumericScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('a', u => fullRangeForType(kValuesTypes[u.type], 5)) + .expand('b', u => fullRangeForType(kValuesTypes[u.type], 5)) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kValuesTypes[t.params.type]; + const expectedResult = true; // should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.a), type.create(t.params.b)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(1.1, 2.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_1arg: '(1.0)', + bad_3arg: '(1.0, 2.0, 3.0)', + // Bad value for arg 0 + bad_0bool: '(false, 1.0)', + bad_0array: '(array(1.1,2.2), 1.0)', + bad_0struct: '(modf(2.2), 1.0)', + // Bad value type for arg 1 + bad_1bool: '(1.0, true)', + bad_1array: '(1.0, array(1.1,2.2))', + bad_1struct: '(1.0, modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/modf.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/modf.spec.ts index b890f9026e..2a90fa878e 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/modf.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/modf.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -23,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,7 +39,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -55,7 +54,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -69,7 +68,7 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(0)], 'constant' ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/normalize.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/normalize.spec.ts new file mode 100644 index 0000000000..28e1d9cdc6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/normalize.spec.ts @@ -0,0 +1,146 @@ +const builtin = 'normalize'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatVectors, + scalarTypeOf, + ScalarType, +} from '../../../../../util/conversion.js'; +import { QuantizeFunc, quantizeToF16, quantizeToF32 } from '../../../../../util/math.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatVectors); + +function quantizeFunctionForScalarType(type: ScalarType): QuantizeFunc<number> { + switch (type) { + case Type.f32: + return quantizeToF32; + case Type.f16: + return quantizeToF16; + default: + return (v: number) => v; + } +} + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() rejects invalid values +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValidArgumentTypes[u.type])) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let expectedResult = true; + + const scalarType = scalarTypeOf(kValidArgumentTypes[t.params.type]); + const quantizeFn = quantizeFunctionForScalarType(scalarType); + + // Should be invalid if the normalization calculations result in intermediate + // values that exceed the maximum representable float value for the given type, + // or if the length is smaller than the smallest representable float value. + const v = Number(t.params.value); + const vv = quantizeFn(v * v); + const dp = quantizeFn(vv * kValidArgumentTypes[t.params.type].width); + const len = quantizeFn(Math.sqrt(dp)); + if (vv === Infinity || dp === Infinity || len === 0) { + expectedResult = false; + } + + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValidArgumentTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +const kInvalidArgumentTypes = objectsToRecord([ + Type.f32, + Type.f16, + Type.abstractInt, + Type.bool, + Type.vec(2, Type.bool), + Type.vec(3, Type.bool), + Type.vec(4, Type.bool), + ...kConcreteIntegerScalarsAndVectors, +]); + +g.test('invalid_argument') + .desc( + ` +Validates that all scalar arguments and vector integer or boolean arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kInvalidArgumentTypes))) + .beforeAllSubcases(t => { + if (kInvalidArgumentTypes[t.params.type] === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const expectedResult = false; // should always error with invalid argument types + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kInvalidArgumentTypes[t.params.type].create(0)], + 'constant' + ); + }); + +const kArgCases = { + good: '(vec3f(1, 0, 0))', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2args: '(vec3f(),vec3f())', + // Bad value for arg 0 + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack2x16snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack2x16snorm.spec.ts new file mode 100644 index 0000000000..f7b3718ca4 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack2x16snorm.spec.ts @@ -0,0 +1,58 @@ +const kFn = 'pack2x16snorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good: '(vec2f())', + good_vec2_abstract_float: '(vec2(0.1))', + bad_0args: '()', + bad_2args: '(vec2f(),vec2f())', + bad_abstract_int: '(1)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_u32: '(1u)', + bad_abstract_float: '(0.1)', + bad_bool: '(false)', + bad_vec4f: '(vec4f())', + bad_vec4u: '(vec4u())', + bad_vec4i: '(vec4i())', + bad_vec4b: '(vec4<bool>())', + bad_vec3f: '(vec3f())', + bad_array: '(array(1.0, 2.0, 3.0, 4.0))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; +const kReturnType = 'u32'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good' || t.params.arg === 'good_vec2_abstract_float', + `const c = ${kFn}${kArgCases[t.params.arg]};` + ); + }); + +g.test('return') + .desc(`Test ${kFn} return value type`) + .params(u => u.combine('type', ['u32', 'i32', 'f32', 'bool', 'vec2u'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack2x16unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack2x16unorm.spec.ts new file mode 100644 index 0000000000..4b6c22f02f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack2x16unorm.spec.ts @@ -0,0 +1,58 @@ +const kFn = 'pack2x16unorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good: '(vec2f())', + good_vec2_abstract_float: '(vec2(0.1))', + bad_0args: '()', + bad_2args: '(vec2f(),vec2f())', + bad_abstract_int: '(1)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_u32: '(1u)', + bad_abstract_float: '(0.1)', + bad_bool: '(false)', + bad_vec4f: '(vec4f())', + bad_vec4u: '(vec4u())', + bad_vec4i: '(vec4i())', + bad_vec4b: '(vec4<bool>())', + bad_vec3f: '(vec3f())', + bad_array: '(array(1.0, 2.0, 3.0, 4.0))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; +const kReturnType = 'u32'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good' || t.params.arg === 'good_vec2_abstract_float', + `const c = ${kFn}${kArgCases[t.params.arg]};` + ); + }); + +g.test('return') + .desc(`Test ${kFn} return value type`) + .params(u => u.combine('type', ['u32', 'i32', 'f32', 'bool', 'vec2u'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4x8snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4x8snorm.spec.ts new file mode 100644 index 0000000000..ea7e196769 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4x8snorm.spec.ts @@ -0,0 +1,58 @@ +const kFn = 'pack4x8snorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good: '(vec4f())', + good_vec4_abstract_float: '(vec4(0.1))', + bad_0args: '()', + bad_2args: '(vec4f(),vec4f())', + bad_abstract_int: '(1)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_u32: '(1u)', + bad_abstract_float: '(0.1)', + bad_bool: '(false)', + bad_vec4u: '(vec4u())', + bad_vec4i: '(vec4i())', + bad_vec4b: '(vec4<bool>())', + bad_vec2f: '(vec2f())', + bad_vec3f: '(vec3f())', + bad_array: '(array(1.0, 2.0, 3.0, 4.0))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; +const kReturnType = 'u32'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good' || t.params.arg === 'good_vec4_abstract_float', + `const c = ${kFn}${kArgCases[t.params.arg]};` + ); + }); + +g.test('return') + .desc(`Test ${kFn} return value type`) + .params(u => u.combine('type', ['u32', 'i32', 'f32', 'bool', 'vec2u'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4x8unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4x8unorm.spec.ts new file mode 100644 index 0000000000..46aafcec88 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4x8unorm.spec.ts @@ -0,0 +1,58 @@ +const kFn = 'pack4x8unorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good: '(vec4f())', + good_vec4_abstract_float: '(vec4(0.1))', + bad_0args: '()', + bad_2args: '(vec4f(),vec4f())', + bad_abstract_int: '(1)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_u32: '(1u)', + bad_abstract_float: '(0.1)', + bad_bool: '(false)', + bad_vec4u: '(vec4u())', + bad_vec4i: '(vec4i())', + bad_vec4b: '(vec4<bool>())', + bad_vec2f: '(vec2f())', + bad_vec3f: '(vec3f())', + bad_array: '(array(1.0, 2.0, 3.0, 4.0))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; +const kReturnType = 'u32'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good' || t.params.arg === 'good_vec4_abstract_float', + `const c = ${kFn}${kArgCases[t.params.arg]};` + ); + }); + +g.test('return') + .desc(`Test ${kFn} return value type`) + .params(u => u.combine('type', ['u32', 'i32', 'f32', 'bool', 'vec2u'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xI8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xI8.spec.ts new file mode 100644 index 0000000000..21b7b706cf --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xI8.spec.ts @@ -0,0 +1,62 @@ +export const description = `Validate pack4xI8`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'pack4xI8'; +const kArgCases = { + good: '(vec4i())', + bad_0args: '()', + bad_2args: '(vec4i(),vec4i())', + bad_0i32: '(1i)', + bad_0f32: '(1f)', + bad_0bool: '(false)', + bad_0vec4u: '(vec4u())', + bad_0vec4f: '(vec4f())', + bad_0vec4b: '(vec4<bool>())', + bad_0vec2i: '(vec2i())', + bad_0vec3i: '(vec3i())', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xI8Clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xI8Clamp.spec.ts new file mode 100644 index 0000000000..7af8958bb1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xI8Clamp.spec.ts @@ -0,0 +1,62 @@ +export const description = `Validate pack4xI8Clamp`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'pack4xI8Clamp'; +const kArgCases = { + good: '(vec4i())', + bad_0args: '()', + bad_2args: '(vec4i(),vec4i())', + bad_0i32: '(1i)', + bad_0f32: '(1f)', + bad_0bool: '(false)', + bad_0vec4u: '(vec4u())', + bad_0vec4f: '(vec4f())', + bad_0vec4b: '(vec4<bool>())', + bad_0vec2i: '(vec2i())', + bad_0vec3i: '(vec3i())', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xU8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xU8.spec.ts new file mode 100644 index 0000000000..89daf34f84 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xU8.spec.ts @@ -0,0 +1,62 @@ +export const description = `Validate pack4xU8`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'pack4xU8'; +const kArgCases = { + good: '(vec4u())', + bad_0args: '()', + bad_2args: '(vec4u(),vec4u())', + bad_0i32: '(1i)', + bad_0f32: '(1f)', + bad_0bool: '(false)', + bad_0vec4i: '(vec4i())', + bad_0vec4f: '(vec4f())', + bad_0vec4b: '(vec4<bool>())', + bad_0vec2u: '(vec2u())', + bad_0vec3u: '(vec3u())', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xU8Clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xU8Clamp.spec.ts new file mode 100644 index 0000000000..9d7bd0353b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/pack4xU8Clamp.spec.ts @@ -0,0 +1,62 @@ +export const description = `Validate pack4xU8Clamp`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'pack4xU8Clamp'; +const kArgCases = { + good: '(vec4u())', + bad_0args: '()', + bad_2args: '(vec4u(),vec4u())', + bad_0i32: '(1i)', + bad_0f32: '(1f)', + bad_0bool: '(false)', + bad_0vec4i: '(vec4i())', + bad_0vec4f: '(vec4f())', + bad_0vec4b: '(vec4<bool>())', + bad_0vec2u: '(vec2u())', + bad_0vec3u: '(vec3u())', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/quantizeToF16.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/quantizeToF16.spec.ts new file mode 100644 index 0000000000..4cad84e78c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/quantizeToF16.spec.ts @@ -0,0 +1,113 @@ +const builtin = 'quantizeToF16'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { Type, kConcreteF32ScalarsAndVectors } from '../../../../../util/conversion.js'; +import { quantizeToF16 } from '../../../../../util/math.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord([ + Type.abstractFloat, + Type.vec(2, Type.abstractFloat), + Type.vec(3, Type.abstractFloat), + Type.vec(4, Type.abstractFloat), + ...kConcreteF32ScalarsAndVectors, +]); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() error on invalid inputs. +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValidArgumentTypes[u.type])) + ) + .fn(t => { + let expectedResult = true; + + // Should be invalid if the quantized value exceeds the maximum representable + // 16-bit float value. + const f16Value = quantizeToF16(Number(t.params.value)); + if (f16Value === Infinity || f16Value === -Infinity) { + expectedResult = false; + } + + const type = kValidArgumentTypes[t.params.type]; + + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.value)], + t.params.stage + ); + }); + +const kArgCasesF16 = { + bad_0f16: '(1h)', + bad_0vec2h: '(vec2h())', + bad_0vec3h: '(vec3h())', + bad_0vec4h: '(vec4h())', +}; + +const kArgCases = { + good: '(vec3f())', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.0, 2.0)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', + ...kArgCasesF16, +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .beforeAllSubcases(t => { + if (t.params.arg in kArgCasesF16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/radians.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/radians.spec.ts index dd432ac194..9017231b69 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/radians.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/radians.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -23,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,7 +39,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -55,7 +54,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -69,8 +68,41 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_too_few: '()', + bad_too_many: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/reflect.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/reflect.spec.ts new file mode 100644 index 0000000000..c71f37f895 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/reflect.spec.ts @@ -0,0 +1,131 @@ +const builtin = 'reflect'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatVectors, + scalarTypeOf, + ScalarType, +} from '../../../../../util/conversion.js'; +import { QuantizeFunc, quantizeToF16, quantizeToF32 } from '../../../../../util/math.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatVectors); + +function quantizeFunctionForScalarType(type: ScalarType): QuantizeFunc<number> { + switch (type) { + case Type.f32: + return quantizeToF32; + case Type.f16: + return quantizeToF16; + default: + return (v: number) => v; + } +} + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('a', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + .expand('b', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let expectedResult = true; + + const scalarType = scalarTypeOf(kValidArgumentTypes[t.params.type]); + const quantizeFn = quantizeFunctionForScalarType(scalarType); + + // Reflect equation: a - 2 * dot(b, a) * b + // Should be invalid if the reflect calculations result in intermediate + // values that exceed the maximum representable float value for the given type. + const a = Number(t.params.a); + const b = Number(t.params.b); + const ab = quantizeFn(a * b); + const dp = quantizeFn(ab * kValidArgumentTypes[t.params.type].width); + const dp2 = quantizeFn(dp * 2); + const dp2b = quantizeFn(dp2 * b); + const a_dp2b = quantizeFn(a - dp2b); + + if ( + !Number.isFinite(ab) || + !Number.isFinite(dp) || + !Number.isFinite(dp2) || + !Number.isFinite(dp2b) || + !Number.isFinite(a_dp2b) + ) { + expectedResult = false; + } + + const type = kValidArgumentTypes[t.params.type]; + + // Validates reflect(vecN(a, a, a), vecN(b, b, b)); + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.a), type.create(t.params.b)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(vec3(0), vec3(1))', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_1arg: '(vec3(0))', + bad_3arg: '(vec3(0), vec3(1), vec3(2))', + // Bad value for arg 0 + bad_0bool: '(false, vec3(1))', + bad_0array: '(array(1.1,2.2), vec3(1))', + bad_0struct: '(modf(2.2), vec3(1))', + // Bad value type for arg 1 + bad_1bool: '(vec3(0), true)', + bad_1array: '(vec3(0), array(1.1,2.2))', + bad_1struct: '(vec3(0), modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/reverseBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/reverseBits.spec.ts new file mode 100644 index 0000000000..8b901bb595 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/reverseBits.spec.ts @@ -0,0 +1,198 @@ +const builtin = 'reverseBits'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConcreteIntegerScalarsAndVectors, + kFloatScalarsAndVectors, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConcreteIntegerScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() never errors +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .fn(t => { + const expectedResult = true; // reverseBits() should never error + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [kValuesTypes[t.params.type].create(t.params.value)], + t.params.stage + ); + }); + +// u32 is included here to confirm that validation is failing due to a type issue and not something else. +const kFloatTypes = objectsToRecord([Type.u32, ...kFloatScalarsAndVectors]); + +g.test('float_argument') + .desc( + ` +Validates that float arguments are rejected by ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kFloatTypes))) + .fn(t => { + const type = kFloatTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.u32, + [type.create(0)], + 'constant' + ); + }); + +const kTests: { + readonly [name: string]: { + /** Arguments to pass to the builtin with parentheses. */ + readonly args: string; + /** Should the test case pass. */ + readonly pass: boolean; + /** Additional setup code in the function scope. */ + readonly preamble?: string; + }; +} = { + valid: { + args: '(1u)', + pass: true, + }, + // Number of arguments. + no_parens: { + args: '', + pass: false, + }, + too_few_args: { + args: '()', + pass: false, + }, + too_many_args: { + args: '(1u,2u)', + pass: false, + }, + // Arguments types (only 1 argument for this builtin). + alias: { + args: '(u32_alias(1))', + pass: true, + }, + bool: { + args: '(false)', + pass: false, + }, + vec_bool: { + args: '(vec2<bool>(false,true))', + pass: false, + }, + matrix: { + args: '(mat2x2(1,1,1,1))', + pass: false, + }, + atomic: { + args: '(a)', + pass: false, + }, + array: { + preamble: 'var arry: array<u32, 5>;', + args: '(arry)', + pass: false, + }, + array_runtime: { + args: '(k.arry)', + pass: false, + }, + struct: { + preamble: 'var x: A;', + args: '(x)', + pass: false, + }, + enumerant: { + args: '(read_write)', + pass: false, + }, + ptr: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(p)', + pass: false, + }, + ptr_deref: { + preamble: `var<function> f = 1u; + let p: ptr<function, u32> = &f;`, + args: '(*p)', + pass: true, + }, + sampler: { + args: '(s)', + pass: false, + }, + texture: { + args: '(t)', + pass: false, + }, +}; + +g.test('arguments') + .desc(`Test compilation validation of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('test', keysOf(kTests))) + .fn(t => { + const test = kTests[t.params.test]; + t.expectCompileResult( + test.pass, + `alias u32_alias = u32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: u32, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${test.preamble ? test.preamble : ''} + _ = ${builtin}${test.args}; + return vec4<f32>(.4, .2, .3, .1); + }` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1u); }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/round.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/round.spec.ts index 3a4ea0408a..dae7482c18 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/round.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/round.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { fpTraitsFor } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -25,7 +24,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,15 +39,19 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() .expand('value', u => { - const constants = fpTraitsFor(elementType(kValuesTypes[u.type])).constants(); - return unique(fullRangeForType(kValuesTypes[u.type]), [ - constants.negative.min + 0.1, - constants.positive.max - 0.1, - ]); + if (scalarTypeOf(kValuesTypes[u.type]).kind === 'abstract-int') { + return fullRangeForType(kValuesTypes[u.type]); + } else { + const constants = fpTraitsFor(scalarTypeOf(kValuesTypes[u.type])).constants(); + return unique(fullRangeForType(kValuesTypes[u.type]), [ + constants.negative.min + 0.1, + constants.positive.max - 0.1, + ]); + } }) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -63,7 +66,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -77,7 +80,7 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/saturate.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/saturate.spec.ts index 1c7aa66a65..cbd1b3f369 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/saturate.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/saturate.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -23,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,7 +39,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -55,7 +54,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -69,7 +68,7 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/select.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/select.spec.ts new file mode 100644 index 0000000000..b03ec66b7d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/select.spec.ts @@ -0,0 +1,250 @@ +const builtin = 'select'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + concreteTypeOf, + isConvertible, + kAllScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { validateConstOrOverrideBuiltinEval } from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kArgumentTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('argument_types_1_and_2') + .desc( + ` +Validates that scalar and vector arguments are not rejected by ${builtin}() for args 1 and 2 +` + ) + .params(u => u.combine('type1', keysOf(kArgumentTypes)).combine('type2', keysOf(kArgumentTypes))) + .beforeAllSubcases(t => { + if ( + scalarTypeOf(kArgumentTypes[t.params.type1]) === Type.f16 || + scalarTypeOf(kArgumentTypes[t.params.type2]) === Type.f16 + ) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type1 = kArgumentTypes[t.params.type1]; + const type2 = kArgumentTypes[t.params.type2]; + // First and second arg must be the same or one convertible to the other. + // Note that we specify a concrete return type even if both args are abstract. + const returnType = isConvertible(type1, type2) + ? concreteTypeOf(type2) + : isConvertible(type2, type1) + ? concreteTypeOf(type1) + : undefined; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ returnType !== undefined, + [type1.create(0), type2.create(0), Type.bool.create(0)], + 'constant', + returnType + ); + }); + +g.test('argument_types_3') + .desc( + ` +Validates that third argument must be bool for ${builtin}() +` + ) + .params(u => u.combine('type', keysOf(kArgumentTypes))) + .beforeAllSubcases(t => { + if (scalarTypeOf(kArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ type === Type.bool, + [Type.i32.create(0), Type.i32.create(0), type.create(0)], + 'constant', + /*return_type*/ Type.i32 + ); + }); + +const kTests = { + valid: { + src: `_ = ${builtin}(1, 2, true);`, + pass: true, + }, + alias: { + src: `_ = ${builtin}(i32_alias(1), i32_alias(2), bool_alias(true));`, + pass: true, + }, + bool: { + src: `_ = ${builtin}(false, false, true);`, + pass: true, + }, + i32: { + src: `_ = ${builtin}(1i, 1i, true);`, + pass: true, + }, + u32: { + src: `_ = ${builtin}(1u, 1u, true);`, + pass: true, + }, + f32: { + src: `_ = ${builtin}(1.0f, 1.0f, true);`, + pass: true, + }, + f16: { + src: `_ = ${builtin}(1.0h, 1.0h, true);`, + pass: true, + }, + mixed_aint_afloat: { + src: `_ = ${builtin}(1, 1.0, true);`, + pass: true, + }, + mixed_i32_u32: { + src: `_ = ${builtin}(1i, 1u, true);`, + pass: false, + }, + vec_bool: { + src: `_ = ${builtin}(vec2<bool>(false, true), vec2<bool>(false, true), true);`, + pass: true, + }, + vec2_bool_implicit: { + src: `_ = ${builtin}(vec2(false, true), vec2(false, true), true);`, + pass: true, + }, + vec3_bool_implicit: { + src: `_ = ${builtin}(vec3(false), vec3(true), true);`, + pass: true, + }, + vec_i32: { + src: `_ = ${builtin}(vec2<i32>(1, 1), vec2<i32>(1, 1), true);`, + pass: true, + }, + vec_u32: { + src: `_ = ${builtin}(vec2<u32>(1, 1), vec2<u32>(1, 1), true);`, + pass: true, + }, + vec_f32: { + src: `_ = ${builtin}(vec2<f32>(1, 1), vec2<f32>(1, 1), true);`, + pass: true, + }, + vec_f16: { + src: `_ = ${builtin}(vec2<f16>(1, 1), vec2<f16>(1, 1), true);`, + pass: true, + }, + matrix: { + src: `_ = ${builtin}(mat2x2(1, 1, 1, 1), mat2x2(1, 1, 1, 1), true);`, + pass: false, + }, + atomic: { + src: ` _ = ${builtin}(a, a, true);`, + pass: false, + }, + array: { + src: `var a: array<bool, 5>; + _ = ${builtin}(a, a, true);`, + pass: false, + }, + array_runtime: { + src: `_ = ${builtin}(k.arry, k.arry, true);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = ${builtin}(a, a, true);`, + pass: false, + }, + enumerant: { + src: `_ = ${builtin}(read_write, read_write, true);`, + pass: false, + }, + ptr: { + src: `var<function> a = true; + let p: ptr<function, bool> = &a; + _ = ${builtin}(p, p, true);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = true; + let p: ptr<function, bool> = &a; + _ = ${builtin}(*p, *p, true);`, + pass: true, + }, + sampler: { + src: `_ = ${builtin}(s, s, true);`, + pass: false, + }, + texture: { + src: `_ = ${builtin}(t, t, true);`, + pass: false, + }, + no_args: { + src: `_ = ${builtin}();`, + pass: false, + }, + too_few_args: { + src: `_ = ${builtin}(1, true);`, + pass: false, + }, + too_many_args: { + src: `_ = ${builtin}(1, 1, 1, true);`, + pass: false, + }, +}; + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}(1, 2, true); }`); + }); + +g.test('arguments') + .desc(`Test that ${builtin} is validated correctly.`) + .params(u => u.combine('test', keysOf(kTests))) + .beforeAllSubcases(t => { + if (t.params.test.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const src = kTests[t.params.test].src; + const enables = t.params.test.includes('f16') ? 'enable f16;' : ''; + const code = ` + ${enables} + alias bool_alias = bool; + alias i32_alias = i32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: bool, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); + }`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/shader_stage_utils.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/shader_stage_utils.ts new file mode 100644 index 0000000000..34e5bc530d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/shader_stage_utils.ts @@ -0,0 +1,64 @@ +/** + * Use to test that certain WGSL builtins are only available in the fragment stage. + * Create WGSL that defines a function "foo" and its required variables that uses + * the builtin being tested. Append it to these code strings then compile. It should + * succeed or fail based on the value `expectSuccess`. + * + * See ./textureSample.spec.ts was one example + */ +export const kEntryPointsToValidateFragmentOnlyBuiltins = { + none: { + expectSuccess: true, + code: ``, + }, + fragment: { + expectSuccess: true, + code: ` + @fragment + fn main() { + foo(); + } + `, + }, + vertex: { + expectSuccess: false, + code: ` + @vertex + fn main() -> @builtin(position) vec4f { + foo(); + return vec4f(); + } + `, + }, + compute: { + expectSuccess: false, + code: ` + @compute @workgroup_size(1) + fn main() { + foo(); + } + `, + }, + fragment_and_compute: { + expectSuccess: false, + code: ` + @fragment + fn main1() { + foo(); + } + + @compute @workgroup_size(1) + fn main2() { + foo(); + } + `, + }, + compute_without_call: { + expectSuccess: true, + code: ` + @compute @workgroup_size(1) + fn main() { + } + `, + }, +}; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sign.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sign.spec.ts index f844961aee..2f1e33b101 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sign.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sign.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatAndSignedIntegerScalarsAndVectors, - kAllUnsignedIntegerScalarsAndVectors, + Type, + kFloatScalarsAndVectors, + kConcreteSignedIntegerScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -23,7 +22,10 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatAndSignedIntegerScalarsAndVectors); +const kValuesTypes = objectsToRecord([ + ...kFloatScalarsAndVectors, + ...kConcreteSignedIntegerScalarsAndVectors, +]); g.test('values') .desc( @@ -40,7 +42,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -55,25 +57,36 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kUnsignedIntegerArgumentTypes = objectsToRecord([ - TypeF32, - ...kAllUnsignedIntegerScalarsAndVectors, -]); +const kArgCases = { + good: '(1.0)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.0, 1.0)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0vec2u: '(vec2u(1))', + bad_0vec3u: '(vec3u(1))', + bad_0vec4u: '(vec4u(1))', +}; -g.test('unsigned_integer_argument') - .desc( - ` -Validates that scalar and vector integer arguments are rejected by ${builtin}() -` - ) - .params(u => u.combine('type', keysOf(kUnsignedIntegerArgumentTypes))) +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) .fn(t => { - const type = kUnsignedIntegerArgumentTypes[t.params.type]; - validateConstOrOverrideBuiltinEval( - t, - builtin, - /* expectedResult */ type === TypeF32, - [type.create(1)], - 'constant' + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` ); }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sin.spec.ts index 3822fccd3a..6be01123cc 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sin.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sin.spec.ts @@ -6,18 +6,17 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinus3PiTo3Pi, + minusThreePiToThreePiRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -25,7 +24,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -39,10 +38,15 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinus3PiTo3Pi, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusThreePiToThreePiRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) @@ -56,7 +60,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -70,8 +74,42 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, + [type.create(0)], 'constant' ); }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2args: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sinh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sinh.spec.ts index 09f48751fc..fc43d23cdd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sinh.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sinh.spec.ts @@ -6,11 +6,9 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -24,7 +22,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -41,13 +39,17 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .expand('value', u => fullRangeForType(kValuesTypes[u.type])) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const expectedResult = isRepresentable(Math.sinh(t.params.value), elementType(type)); + const expectedResult = isRepresentable( + Math.sinh(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -57,22 +59,40 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; -g.test('integer_argument') - .desc( - ` -Validates that scalar and vector integer arguments are rejected by ${builtin}() -` - ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) .fn(t => { - const type = kIntegerArgumentTypes[t.params.type]; - validateConstOrOverrideBuiltinEval( - t, - builtin, - /* expectedResult */ type === TypeF32, - [type.create(0)], - 'constant' + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` ); }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/smoothstep.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/smoothstep.spec.ts new file mode 100644 index 0000000000..643e5df09e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/smoothstep.spec.ts @@ -0,0 +1,241 @@ +const builtin = 'smoothstep'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + concreteTypeOf, + elementTypeOf, + isConvertibleToFloatType, + kAllScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); +const kArgumentTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() rejects invalid values +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value1', u => [-1000, -10, 0, 10, 1000]) + .expand('value2', u => [-1000, -10, 0, 10, 1000]) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kValuesTypes[t.params.type]; + + // We expect to fail if low == high as it results in a DBZ + const expectedResult = t.params.value1 !== t.params.value2; + + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.value1), type.create(t.params.value2), type.create(0)], + t.params.stage, + /* returnType */ concreteTypeOf(type, [Type.f32]) + ); + }); + +g.test('argument_types') + .desc( + ` +Validates that scalar and vector arguments are rejected by ${builtin}() if not float type or vecN<float type> +` + ) + .params(u => u.combine('type', keysOf(kArgumentTypes))) + .beforeAllSubcases(t => { + if (scalarTypeOf(kArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + /* expectedResult */ isConvertibleToFloatType(elementTypeOf(type)), + [type.create(0), type.create(1), type.create(2)], + 'constant', + /* returnType */ concreteTypeOf(type, [Type.f32]) + ); + }); + +const kTests = { + valid: { + src: `_ = ${builtin}(0.0, 42.0, 0.5);`, + pass: true, + }, + alias: { + src: `_ = ${builtin}(f32_alias(0), f32_alias(42), f32_alias(0.5));`, + pass: true, + }, + bool: { + src: `_ = ${builtin}(false, false, false);`, + pass: false, + }, + i32: { + src: `_ = ${builtin}(1i, 2i, 1i);`, + pass: false, + }, + u32: { + src: `_ = ${builtin}(1u, 2u, 1u);`, + pass: false, + }, + f32: { + src: `_ = ${builtin}(1.0f, 2.0f, 1.0f);`, + pass: true, + }, + f16: { + src: `_ = ${builtin}(1h, 2h, 1h);`, + pass: true, + }, + mixed_aint_afloat: { + src: `_ = ${builtin}(1.0, 2, 1);`, + pass: true, + }, + mixed_f32_afloat: { + src: `_ = ${builtin}(1.0f, 2.0, 1.0);`, + pass: true, + }, + mixed_f16_afloat: { + src: `_ = ${builtin}(1.0h, 2.0, 1.0);`, + pass: true, + }, + vec_bool: { + src: `_ = ${builtin}(vec2<bool>(false, true), vec2<bool>(false, true), vec2<bool>(false, true));`, + pass: false, + }, + vec_i32: { + src: `_ = ${builtin}(vec2<i32>(1, 1), vec2<i32>(1, 1), vec2<i32>(1, 1));`, + pass: false, + }, + vec_u32: { + src: `_ = ${builtin}(vec2<u32>(1, 1), vec2<u32>(1, 1), vec2<u32>(1, 1));`, + pass: false, + }, + vec_f32: { + src: `_ = ${builtin}(vec2<f32>(0, 0), vec2<f32>(1, 1), vec2<f32>(1, 1));`, + pass: true, + }, + matrix: { + src: `_ = ${builtin}(mat2x2(1, 1, 1, 1), mat2x2(1, 1, 1, 1), mat2x2(1, 1, 1, 1));`, + pass: false, + }, + atomic: { + src: ` _ = ${builtin}(a, a, a);`, + pass: false, + }, + array: { + src: `var a: array<bool, 5>; + _ = ${builtin}(a, a, a);`, + pass: false, + }, + array_runtime: { + src: `_ = ${builtin}(k.arry, k.arry, k.arry);`, + pass: false, + }, + struct: { + src: `var a: A; + _ = ${builtin}(a, a, a);`, + pass: false, + }, + enumerant: { + src: `_ = ${builtin}(read_write, read_write, read_write);`, + pass: false, + }, + ptr: { + src: `var<function> a = 1.0; + let p: ptr<function, f32> = &a; + _ = ${builtin}(p, p, p);`, + pass: false, + }, + ptr_deref: { + src: `var<function> a = 1.0; + let p: ptr<function, f32> = &a; + _ = ${builtin}(*p, *p, *p);`, + pass: true, + }, + sampler: { + src: `_ = ${builtin}(s, s, s);`, + pass: false, + }, + texture: { + src: `_ = ${builtin}(t, t, t);`, + pass: false, + }, + no_args: { + src: `_ = ${builtin}();`, + pass: false, + }, + too_few_args: { + src: `_ = ${builtin}(1.0, 2.0);`, + pass: false, + }, + too_many_args: { + src: `_ = ${builtin}(1.0, 2.0, 3.0, 4.0);`, + pass: false, + }, +}; + +g.test('arguments') + .desc(`Test that ${builtin} is validated correctly when called with different arguments.`) + .params(u => u.combine('test', keysOf(kTests))) + .beforeAllSubcases(t => { + if (t.params.test.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const src = kTests[t.params.test].src; + const enables = t.params.test.includes('f16') ? 'enable f16;' : ''; + const code = ` + ${enables} + alias f32_alias = f32; + + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: texture_2d<f32>; + + var<workgroup> a: atomic<u32>; + + struct A { + i: bool, + } + struct B { + arry: array<u32>, + } + @group(0) @binding(3) var<storage> k: B; + + @vertex + fn main() -> @builtin(position) vec4<f32> { + ${src} + return vec4<f32>(.4, .2, .3, .1); + }`; + t.expectCompileResult(kTests[t.params.test].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sqrt.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sqrt.spec.ts index a570ce4bc0..cabb0d59fb 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sqrt.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/sqrt.spec.ts @@ -6,11 +6,10 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { isRepresentable } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -18,7 +17,7 @@ import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinusTwoToTwo, + minusTwoToTwoRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -26,7 +25,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,17 +39,27 @@ Validates that constant evaluation and override evaluation of ${builtin}() input .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinusTwoToTwo, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusTwoToTwoRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; const expectedResult = - t.params.value >= 0 && isRepresentable(Math.sqrt(t.params.value), elementType(type)); + t.params.value >= 0 && + isRepresentable( + Math.sqrt(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); validateConstOrOverrideBuiltinEval( t, builtin, @@ -60,7 +69,7 @@ Validates that constant evaluation and override evaluation of ${builtin}() input ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kIntegerArgumentTypes = objectsToRecord([Type.f32, ...kConcreteIntegerScalarsAndVectors]); g.test('integer_argument') .desc( @@ -74,8 +83,41 @@ Validates that scalar and vector integer arguments are rejected by ${builtin}() validateConstOrOverrideBuiltinEval( t, builtin, - /* expectedResult */ type === TypeF32, + /* expectedResult */ type === Type.f32, [type.create(1)], 'constant' ); }); + +const kArgCases = { + good: '(1.1)', + bad_no_parens: '', + // Bad number of args + bad_too_few: '()', + bad_too_many: '(1.0,2.0)', + // Bad value type for arg 0 + bad_0i32: '(1i)', + bad_0u32: '(1u)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/step.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/step.spec.ts new file mode 100644 index 0000000000..a10ef20c7b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/step.spec.ts @@ -0,0 +1,108 @@ +const builtin = 'step'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() error on invalid inputs. +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('a', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + .expand('b', u => fullRangeForType(kValidArgumentTypes[u.type], 5)) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const expectedResult = true; + + const type = kValidArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.a), type.create(t.params.b)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(1.2, 2.3)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_1arg: '(1.2)', + bad_3arg: '(1.2, 2.3, 4.5)', + // Bad value for arg 0 + bad_0bool: '(false, 2.3)', + bad_0array: '(array(1.1,2.2), 2.3)', + bad_0struct: '(modf(2.2), 2.3)', + bad_0uint: '(1u, 2.3)', + bad_0int: '(1i, 2.3)', + bad_0vec2i: '(vec2i(), 2.3)', + bad_0vec2u: '(vec2u(), 2.3)', + bad_0vec3i: '(vec3i(), 2.3)', + bad_0vec3u: '(vec3u(), 2.3)', + bad_0vec4i: '(vec4i(), 2.3)', + bad_0vec4u: '(vec4u(), 2.3)', + // Bad value for arg 1 + bad_1bool: '(1.2, false)', + bad_1array: '(1.2, array(1.1,2.2))', + bad_1struct: '(1.2, modf(2.2))', + bad_1uint: '(1.2, 1u)', + bad_1int: '(1.2, 1i)', + bad_1vec2i: '(1.2, vec2i())', + bad_1vec2u: '(1.2, vec2u())', + bad_1vec3i: '(1.2, vec3i())', + bad_1vec3u: '(1.2, vec3u())', + bad_1vec4i: '(1.2, vec4i())', + bad_1vec4u: '(1.2, vec4u())', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/tan.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/tan.spec.ts index b9744643f6..9384585dd5 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/tan.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/tan.spec.ts @@ -6,11 +6,9 @@ Validation tests for the ${builtin}() builtin. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; import { - TypeF16, - TypeF32, - elementType, - kAllFloatScalarsAndVectors, - kAllIntegerScalarsAndVectors, + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, } from '../../../../../util/conversion.js'; import { fpTraitsFor } from '../../../../../util/floating_point.js'; import { ShaderValidationTest } from '../../../shader_validation_test.js'; @@ -18,7 +16,7 @@ import { ShaderValidationTest } from '../../../shader_validation_test.js'; import { fullRangeForType, kConstantAndOverrideStages, - kMinus3PiTo3Pi, + minusThreePiToThreePiRangeForType, stageSupportsType, unique, validateConstOrOverrideBuiltinEval, @@ -26,7 +24,7 @@ import { export const g = makeTestGroup(ShaderValidationTest); -const kValuesTypes = objectsToRecord(kAllFloatScalarsAndVectors); +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); g.test('values') .desc( @@ -40,18 +38,26 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec .combine('type', keysOf(kValuesTypes)) .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) .beginSubcases() - .expand('value', u => unique(kMinus3PiTo3Pi, fullRangeForType(kValuesTypes[u.type]))) + .expand('value', u => + unique( + minusThreePiToThreePiRangeForType(kValuesTypes[u.type]), + fullRangeForType(kValuesTypes[u.type]) + ) + ) ) .beforeAllSubcases(t => { - if (elementType(kValuesTypes[t.params.type]) === TypeF16) { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { t.selectDeviceOrSkipTestCase('shader-f16'); } }) .fn(t => { const type = kValuesTypes[t.params.type]; - const fp = fpTraitsFor(elementType(type)); + const fp = fpTraitsFor( + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); const smallestPositive = fp.constants().positive.min; - const v = fp.quantize(t.params.value); + const v = fp.quantize(Number(t.params.value)); const expectedResult = Math.abs(Math.cos(v)) > smallestPositive; validateConstOrOverrideBuiltinEval( t, @@ -62,22 +68,40 @@ Validates that constant evaluation and override evaluation of ${builtin}() rejec ); }); -const kIntegerArgumentTypes = objectsToRecord([TypeF32, ...kAllIntegerScalarsAndVectors]); +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; -g.test('integer_argument') - .desc( - ` -Validates that scalar and vector integer arguments are rejected by ${builtin}() -` - ) - .params(u => u.combine('type', keysOf(kIntegerArgumentTypes))) +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) .fn(t => { - const type = kIntegerArgumentTypes[t.params.type]; - validateConstOrOverrideBuiltinEval( - t, - builtin, - /* expectedResult */ type === TypeF32, - [type.create(0)], - 'constant' + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` ); }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/tanh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/tanh.spec.ts new file mode 100644 index 0000000000..965eb85111 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/tanh.spec.ts @@ -0,0 +1,98 @@ +const builtin = 'tanh'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { isRepresentable } from '../../../../../util/floating_point.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValuesTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() rejects invalid values +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValuesTypes)) + .filter(u => stageSupportsType(u.stage, kValuesTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValuesTypes[u.type])) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValuesTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kValuesTypes[t.params.type]; + const expectedResult = isRepresentable( + Math.tanh(Number(t.params.value)), + // AbstractInt is converted to AbstractFloat before calling into the builtin + scalarTypeOf(type).kind === 'abstract-int' ? Type.abstractFloat : scalarTypeOf(type) + ); + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.value)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureGather.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureGather.spec.ts new file mode 100644 index 0000000000..35a2be1d48 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureGather.spec.ts @@ -0,0 +1,335 @@ +const builtin = 'textureGather'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureGather component parameter must be correct type +* test textureGather component parameter must be between 0 and 3 inclusive +* test textureGather component parameter must be a const expression +* test textureGather coords parameter must be correct type +* test textureGather array_index parameter must be correct type +* test textureGather offset parameter must be correct type +* test textureGather offset parameter must be a const-expression +* test textureGather offset parameter must be between -8 and +7 inclusive +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +type TextureGatherArguments = { + hasComponentArg?: boolean; + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureGatherParameterTypes: { [n: string]: TextureGatherArguments } = { + 'texture_2d<f32>': { + hasComponentArg: true, + coordsArgType: Type.vec2f, + offsetArgType: Type.vec2i, + }, + 'texture_2d_array<f32>': { + hasComponentArg: true, + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + 'texture_cube<f32>': { hasComponentArg: true, coordsArgType: Type.vec3f }, + 'texture_cube_array<f32>': { + hasComponentArg: true, + coordsArgType: Type.vec3f, + hasArrayIndexArg: true, + }, + texture_depth_2d: { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + texture_depth_2d_array: { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + texture_depth_cube: { coordsArgType: Type.vec3f }, + texture_depth_cube_array: { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureGatherParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('component_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegather') + .desc( + ` +Validates that only incorrect components arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureGatherParameterTypes)) + // filter out types with no component argument + .filter(t => !!kValidTextureGatherParameterTypes[t.textureType].hasComponentArg) + .combine('componentType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1, 2, 3, 4] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.componentType]) || t.value >= 0) + .expand('offset', t => + kValidTextureGatherParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, componentType, offset, value } = t.params; + const componentArgType = kValuesTypes[componentType]; + const { offsetArgType, coordsArgType, hasArrayIndexArg } = + kValidTextureGatherParameterTypes[textureType]; + + const componentWGSL = componentArgType.create(value).wgsl(); + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGather(${componentWGSL}, t, s, ${coordWGSL}${arrayWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + (isConvertible(componentArgType, Type.i32) || isConvertible(componentArgType, Type.u32)) && + value >= 0 && + value <= 3; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('component_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegather') + .desc( + ` +Validates that only non-const components arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureGatherParameterTypes)) + // filter out types with no component argument + .filter(t => !!kValidTextureGatherParameterTypes[t.textureType].hasComponentArg) + .combine('varType', ['c', 'u', 'l']) + .beginSubcases() + .expand('offset', t => + kValidTextureGatherParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, varType, offset } = t.params; + const componentArgType = Type.u32; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureGatherParameterTypes[textureType]; + + const componentWGSL = `${componentArgType}(${varType})`; + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${componentArgType}; + +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = 1; + let v = textureGather(${componentWGSL}, t, s, ${coordWGSL}${arrayWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegather') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureGatherParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureGatherParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + hasComponentArg, + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureGatherParameterTypes[textureType]; + + const componentWGSL = hasComponentArg ? '0, ' : ''; + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGather(${componentWGSL}t, s, ${coordWGSL}${arrayWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegather') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureGatherParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureGatherParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { hasComponentArg, coordsArgType, offsetArgType } = + kValidTextureGatherParameterTypes[textureType]; + + const componentWGSL = hasComponentArg ? '0, ' : ''; + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGather(${componentWGSL}t, s, ${coordWGSL}, ${arrayWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegather') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureGatherParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + hasComponentArg, + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureGatherParameterTypes[textureType]; + + const componentWGSL = hasComponentArg ? '0, ' : ''; + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGather(${componentWGSL}t, s, ${coordWGSL}${arrayWGSL}, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegather') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureGatherParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { hasComponentArg, coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureGatherParameterTypes[textureType]; + + const componentWGSL = hasComponentArg ? '0, ' : ''; + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType!.create(0).wgsl()}; + let v = textureGather(${componentWGSL}t, s, ${coordWGSL}${arrayWGSL}, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureGatherCompare.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureGatherCompare.spec.ts new file mode 100644 index 0000000000..84ba2ad95d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureGatherCompare.spec.ts @@ -0,0 +1,264 @@ +const builtin = 'textureGatherCompare'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureGatherCompare coords parameter must be correct type +* test textureGatherCompare array_index parameter must be correct type +* test textureGatherCompare depth_ref parameter must be correct type +* test textureGatherCompare offset parameter must be correct type +* test textureGatherCompare offset parameter must be a const-expression +* test textureGatherCompare offset parameter must be between -8 and +7 inclusive +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +type TextureGatherCompareArguments = { + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureGatherCompareParameterTypes: { [n: string]: TextureGatherCompareArguments } = { + texture_depth_2d: { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + texture_depth_2d_array: { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + texture_depth_cube: { coordsArgType: Type.vec3f }, + texture_depth_cube_array: { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureGatherCompareParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegathercompare') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureGatherCompareParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureGatherCompareParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureGatherCompareParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGatherCompare(t, s, ${coordWGSL}${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegathercompare') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureGatherCompareParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureGatherCompareParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgType, offsetArgType } = kValidTextureGatherCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGatherCompare(t, s, ${coordWGSL}, ${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('depth_ref_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegathercompare') + .desc( + ` +Validates that only incorrect depth_ref arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureGatherCompareParameterTypes)) + .combine('depthRefType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.depthRefType]) || t.value >= 0) + .expand('offset', t => + kValidTextureGatherCompareParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, depthRefType, offset, value } = t.params; + const depthRefArgType = kValuesTypes[depthRefType]; + const { offsetArgType, coordsArgType, hasArrayIndexArg } = + kValidTextureGatherCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const depthRefWGSL = depthRefArgType.create(value).wgsl(); + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGatherCompare(t, s, ${coordWGSL}${arrayWGSL}, ${depthRefWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(depthRefArgType, Type.f32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegathercompare') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureGatherCompareParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureGatherCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureGatherCompare(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturegathercompare') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureGatherCompareParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureGatherCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType!.create(0).wgsl()}; + let v = textureGatherCompare(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureLoad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureLoad.spec.ts new file mode 100644 index 0000000000..9138b2ecc6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureLoad.spec.ts @@ -0,0 +1,370 @@ +const builtin = 'textureLoad'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureLoad coords parameter must be correct type +* test textureLoad array_index parameter must be correct type +* test textureLoad level parameter must be correct type +* test textureLoad sample_index parameter must be correct type +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { assert } from '../../../../../../common/util/util.js'; +import { kAllTextureFormats, kTextureFormatInfo } from '../../../../../format_info.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +type TextureLoadArguments = { + coordsArgTypes: readonly [ScalarType | VectorType, ScalarType | VectorType]; + usesMultipleTypes?: boolean; // texture can use f32, i32, u32 + hasArrayIndexArg?: boolean; + hasLevelArg?: boolean; + hasSampleIndexArg?: boolean; +}; + +const kCoords1DTypes = [Type.i32, Type.u32] as const; +const kCoords2DTypes = [Type.vec2i, Type.vec2u] as const; +const kCoords3DTypes = [Type.vec3i, Type.vec3u] as const; + +const kValidTextureLoadParameterTypesForNonStorageTextures: { [n: string]: TextureLoadArguments } = + { + texture_1d: { usesMultipleTypes: true, coordsArgTypes: kCoords1DTypes, hasLevelArg: true }, + texture_2d: { usesMultipleTypes: true, coordsArgTypes: kCoords2DTypes, hasLevelArg: true }, + texture_2d_array: { + usesMultipleTypes: true, + coordsArgTypes: kCoords2DTypes, + hasArrayIndexArg: true, + hasLevelArg: true, + }, + texture_3d: { usesMultipleTypes: true, coordsArgTypes: kCoords3DTypes, hasLevelArg: true }, + texture_multisampled_2d: { + usesMultipleTypes: true, + coordsArgTypes: kCoords2DTypes, + hasSampleIndexArg: true, + }, + texture_depth_2d: { coordsArgTypes: kCoords2DTypes, hasLevelArg: true }, + texture_depth_2d_array: { + coordsArgTypes: kCoords2DTypes, + hasArrayIndexArg: true, + hasLevelArg: true, + }, + texture_depth_multisampled_2d: { coordsArgTypes: kCoords2DTypes, hasSampleIndexArg: true }, + texture_external: { coordsArgTypes: kCoords2DTypes }, + }; + +const kValidTextureLoadParameterTypesForStorageTextures: { [n: string]: TextureLoadArguments } = { + texture_storage_1d: { coordsArgTypes: [Type.i32, Type.u32] }, + texture_storage_2d: { coordsArgTypes: [Type.vec2i, Type.vec2u] }, + texture_storage_2d_array: { + coordsArgTypes: [Type.vec2i, Type.vec2u], + hasArrayIndexArg: true, + }, + texture_storage_3d: { coordsArgTypes: [Type.vec3i, Type.vec3u] }, +} as const; + +const kNonStorageTextureTypes = keysOf(kValidTextureLoadParameterTypesForNonStorageTextures); +const kStorageTextureTypes = keysOf(kValidTextureLoadParameterTypesForStorageTextures); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +const kTexelType: { [n: string]: Type } = { + f32: Type.vec4f, + i32: Type.vec4i, + u32: Type.vec4u, +} as const; + +const kTexelTypes = keysOf(kTexelType); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument,non_storage') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#textureload') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kNonStorageTextureTypes) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .expand('texelType', t => + kValidTextureLoadParameterTypesForNonStorageTextures[t.textureType].usesMultipleTypes + ? kTexelTypes + : [''] + ) + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, coordType, texelType, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { coordsArgTypes, hasArrayIndexArg, hasLevelArg, hasSampleIndexArg } = + kValidTextureLoadParameterTypesForNonStorageTextures[textureType]; + + const texelTypeWGSL = texelType ? `<${texelType}>` : ''; + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const levelWGSL = hasLevelArg ? ', 0' : ''; + const sampleIndexWGSL = hasSampleIndexArg ? ', 0' : ''; + + const code = ` +@group(0) @binding(0) var t: ${textureType}${texelTypeWGSL}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureLoad(t, ${coordWGSL}${arrayWGSL}${levelWGSL}${sampleIndexWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(coordArgType, coordsArgTypes[0]) || + isConvertible(coordArgType, coordsArgTypes[1]); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('coords_argument,storage') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#textureload') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kStorageTextureTypes) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('format', kAllTextureFormats) + // filter to only storage texture formats. + .filter(t => !!kTextureFormatInfo[t.format].color?.storage) + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + ) + .beforeAllSubcases(t => + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures') + ) + .fn(t => { + const { textureType, coordType, format, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { coordsArgTypes, hasArrayIndexArg } = + kValidTextureLoadParameterTypesForStorageTextures[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + + const code = ` +@group(0) @binding(0) var t: ${textureType}<${format}, read>; +@fragment fn fs() -> @location(0) vec4f { + _ = textureLoad(t, ${coordWGSL}${arrayWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(coordArgType, coordsArgTypes[0]) || + isConvertible(coordArgType, coordsArgTypes[1]); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument,non_storage') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#textureload') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kNonStorageTextureTypes) + // filter out types with no array_index + .filter( + t => !!kValidTextureLoadParameterTypesForNonStorageTextures[t.textureType].hasArrayIndexArg + ) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .expand('texelType', t => + kValidTextureLoadParameterTypesForNonStorageTextures[t.textureType].usesMultipleTypes + ? kTexelTypes + : [''] + ) + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, arrayIndexType, texelType, value } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgTypes, hasLevelArg } = + kValidTextureLoadParameterTypesForNonStorageTextures[textureType]; + + const texelTypeWGSL = texelType ? `<${texelType}>` : ''; + const coordWGSL = coordsArgTypes[0].create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const levelWGSL = hasLevelArg ? ', 0' : ''; + + const code = ` +@group(0) @binding(0) var t: ${textureType}${texelTypeWGSL}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureLoad(t, ${coordWGSL}, ${arrayWGSL}${levelWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument,storage') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#textureload') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kStorageTextureTypes) + // filter out types with no array_index + .filter( + t => !!kValidTextureLoadParameterTypesForStorageTextures[t.textureType].hasArrayIndexArg + ) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('format', kAllTextureFormats) + // filter to only storage texture formats. + .filter(t => !!kTextureFormatInfo[t.format].color?.storage) + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + ) + .beforeAllSubcases(t => + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures') + ) + .fn(t => { + const { textureType, arrayIndexType, format, value } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgTypes, hasLevelArg } = + kValidTextureLoadParameterTypesForStorageTextures[textureType]; + + const coordWGSL = coordsArgTypes[0].create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const levelWGSL = hasLevelArg ? ', 0' : ''; + + const code = ` +@group(0) @binding(0) var t: ${textureType}<${format}, read>; +@fragment fn fs() -> @location(0) vec4f { + _ = textureLoad(t, ${coordWGSL}, ${arrayWGSL}${levelWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('level_argument,non_storage') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#textureload') + .desc( + ` +Validates that only incorrect level arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kNonStorageTextureTypes) + // filter out types with no level + .filter( + t => !!kValidTextureLoadParameterTypesForNonStorageTextures[t.textureType].hasLevelArg + ) + .combine('levelType', keysOf(kValuesTypes)) + .beginSubcases() + .expand('texelType', t => + kValidTextureLoadParameterTypesForNonStorageTextures[t.textureType].usesMultipleTypes + ? kTexelTypes + : [''] + ) + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.levelType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, levelType, texelType, value } = t.params; + const levelArgType = kValuesTypes[levelType]; + const { coordsArgTypes, hasArrayIndexArg } = + kValidTextureLoadParameterTypesForNonStorageTextures[textureType]; + + const texelTypeWGSL = texelType ? `<${texelType}>` : ''; + const coordWGSL = coordsArgTypes[0].create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const levelWGSL = levelArgType.create(value).wgsl(); + + const code = ` +@group(0) @binding(0) var t: ${textureType}${texelTypeWGSL}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureLoad(t, ${coordWGSL}${arrayWGSL}, ${levelWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(levelArgType, Type.i32) || isConvertible(levelArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('sample_index_argument,non_storage') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#textureload') + .desc( + ` +Validates that only incorrect sample_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kNonStorageTextureTypes) + // filter out types with no sample_index + .filter( + t => !!kValidTextureLoadParameterTypesForNonStorageTextures[t.textureType].hasSampleIndexArg + ) + .combine('sampleIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .expand('texelType', t => + kValidTextureLoadParameterTypesForNonStorageTextures[t.textureType].usesMultipleTypes + ? kTexelTypes + : [''] + ) + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.sampleIndexType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, sampleIndexType, texelType, value } = t.params; + const sampleIndexArgType = kValuesTypes[sampleIndexType]; + const { coordsArgTypes, hasArrayIndexArg, hasLevelArg } = + kValidTextureLoadParameterTypesForNonStorageTextures[textureType]; + assert(!hasLevelArg); + + const texelTypeWGSL = texelType ? `<${texelType}>` : ''; + const coordWGSL = coordsArgTypes[0].create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const sampleIndexWGSL = sampleIndexArgType.create(value).wgsl(); + + const code = ` +@group(0) @binding(0) var t: ${textureType}${texelTypeWGSL}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureLoad(t, ${coordWGSL}${arrayWGSL}, ${sampleIndexWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(sampleIndexArgType, Type.i32) || isConvertible(sampleIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSample.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSample.spec.ts new file mode 100644 index 0000000000..bdd3cbd8e8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSample.spec.ts @@ -0,0 +1,267 @@ +const builtin = 'textureSample'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSample coords parameter must be correct type +* test textureSample array_index parameter must be correct type +* test textureSample coords parameter must be correct type +* test textureSample offset parameter must be correct type +* test textureSample offset parameter must be a const-expression +* test textureSample offset parameter must be between -8 and +7 inclusive +* test textureSample not usable in a compute or vertex shader + +note: uniformity validation is covered in src/webgpu/shader/validation/uniformity/uniformity.spec.ts +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { kEntryPointsToValidateFragmentOnlyBuiltins } from './shader_stage_utils.js'; + +type TextureSampleArguments = { + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureSampleParameterTypes: { [n: string]: TextureSampleArguments } = { + 'texture_1d<f32>': { coordsArgType: Type.f32 }, + 'texture_2d<f32>': { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + 'texture_2d_array<f32>': { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + 'texture_3d<f32>': { coordsArgType: Type.vec3f, offsetArgType: Type.vec3i }, + 'texture_cube<f32>': { coordsArgType: Type.vec3f }, + 'texture_cube_array<f32>': { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, + texture_depth_2d: { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + texture_depth_2d_array: { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + texture_depth_cube: { coordsArgType: Type.vec3f }, + texture_depth_cube_array: { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureSampleParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesample') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureSampleParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSample(t, s, ${coordWGSL}${arrayWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesample') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureSampleParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgType, offsetArgType } = kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSample(t, s, ${coordWGSL}, ${arrayWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesample') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureSampleParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSample(t, s, ${coordWGSL}${arrayWGSL}, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesample') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureSampleParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType!.create(0).wgsl()}; + let v = textureSample(t, s, ${coordWGSL}${arrayWGSL}, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('only_in_fragment') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesample') + .desc( + ` +Validates that ${builtin} must not be used in a compute or vertex shader. +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('entryPoint', keysOf(kEntryPointsToValidateFragmentOnlyBuiltins)) + .expand('offset', t => + kValidTextureSampleParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, entryPoint, offset } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const config = kEntryPointsToValidateFragmentOnlyBuiltins[entryPoint]; + const code = ` +${config.code} +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; + +fn foo() { + _ = textureSample(t, s, ${coordWGSL}${arrayWGSL}${offsetWGSL}); +}`; + t.expectCompileResult(config.expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleBaseClampToEdge.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleBaseClampToEdge.spec.ts new file mode 100644 index 0000000000..4c3a7cb9b7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleBaseClampToEdge.spec.ts @@ -0,0 +1,54 @@ +const builtin = 'textureSampleBaseClampToEdge'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSampleBaseClampToEdge coords parameter must be correct type +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kTextureSampleBaseClampToEdgeTextureTypes = ['texture_2d<f32>', 'texture_external'] as const; +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplebaseclamptoedge') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureSampleBaseClampToEdgeTextureTypes) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, coordType, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const coordWGSL = coordArgType.create(value).wgsl(); + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleBaseClampToEdge(t, s, ${coordWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, Type.vec2f); + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleBias.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleBias.spec.ts new file mode 100644 index 0000000000..549add20e1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleBias.spec.ts @@ -0,0 +1,309 @@ +const builtin = 'textureSampleBias'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSampleBias coords parameter must be correct type +* test textureSampleBias array_index parameter must be correct type +* test textureSampleBias bias parameter must be correct type +* test textureSampleBias bias parameter must be between -16.0 and 15.99 inclusive if it's a constant +* test textureSampleBias offset parameter must be correct type +* test textureSampleBias offset parameter must be a const-expression +* test textureSampleBias offset parameter must be between -8 and +7 inclusive + +note: uniformity validation is covered in src/webgpu/shader/validation/uniformity/uniformity.spec.ts +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, + scalarTypeOf, + isFloatType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { kEntryPointsToValidateFragmentOnlyBuiltins } from './shader_stage_utils.js'; + +type TextureSampleBiasArguments = { + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureSampleBiasParameterTypes: { [n: string]: TextureSampleBiasArguments } = { + 'texture_2d<f32>': { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + 'texture_2d_array<f32>': { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + 'texture_3d<f32>': { coordsArgType: Type.vec3f, offsetArgType: Type.vec3i }, + 'texture_cube<f32>': { coordsArgType: Type.vec3f }, + 'texture_cube_array<f32>': { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureSampleBiasParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplebias') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureSampleBiasParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleBiasParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureSampleBiasParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleBias(t, s, ${coordWGSL}${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplebias') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureSampleBiasParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleBiasParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgType, offsetArgType } = kValidTextureSampleBiasParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleBias(t, s, ${coordWGSL}, ${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('bias_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplebias') + .desc( + ` +Validates that only incorrect bias arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureSampleBiasParameterTypes[t.textureType].offsetArgType) + .combine('biasType', keysOf(kValuesTypes)) + .beginSubcases() + // The spec mentions limits of > -16 and < 15.99 so pass some values around there + // No error is mentioned for out of range values so make sure no error is generated. + .combine('value', [-17, -16, -8, 0, 7, 15.99, 16]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.biasType]) || t.value >= 0) + // filter out non-integer values passed to integer types. + .filter(t => Number.isInteger(t.value) || isFloatType(scalarTypeOf(kValuesTypes[t.biasType]))) + .expand('offset', t => + kValidTextureSampleBiasParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, biasType, value, offset } = t.params; + const biasArgType = kValuesTypes[biasType]; + const args = [biasArgType.create(value)]; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleBiasParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const biasWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleBias(t, s, ${coordWGSL}${arrayWGSL}, ${biasWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(biasArgType, Type.f32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplebias') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureSampleBiasParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureSampleBiasParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleBias(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplebias') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureSampleBiasParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleBiasParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType!.create(0).wgsl()}; + let v = textureSampleBias(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('only_in_fragment') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesample') + .desc( + ` +Validates that ${builtin} must not be used in a compute or vertex shader. +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('entryPoint', keysOf(kEntryPointsToValidateFragmentOnlyBuiltins)) + .expand('offset', t => + kValidTextureSampleBiasParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, entryPoint, offset } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleBiasParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const config = kEntryPointsToValidateFragmentOnlyBuiltins[entryPoint]; + const code = ` +${config.code} +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; + +fn foo() { + _ = textureSampleBias(t, s, ${coordWGSL}${arrayWGSL}, 0${offsetWGSL}); +}`; + t.expectCompileResult(config.expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleCompare.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleCompare.spec.ts new file mode 100644 index 0000000000..9c07d0354a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleCompare.spec.ts @@ -0,0 +1,308 @@ +const builtin = 'textureSampleCompare'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSampleCompare coords parameter must be correct type +* test textureSampleCompare array_index parameter must be correct type +* test textureSampleCompare depth_ref parameter must be correct type +* test textureSampleCompare offset parameter must be correct type +* test textureSampleCompare offset parameter must be a const-expression +* test textureSampleCompare offset parameter must be between -8 and +7 inclusive +* test textureSample not usable in a compute or vertex shader + +note: uniformity validation is covered in src/webgpu/shader/validation/uniformity/uniformity.spec.ts +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { kEntryPointsToValidateFragmentOnlyBuiltins } from './shader_stage_utils.js'; + +type TextureSampleCompareArguments = { + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureSampleCompareParameterTypes: { [n: string]: TextureSampleCompareArguments } = { + texture_depth_2d: { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + texture_depth_2d_array: { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + texture_depth_cube: { coordsArgType: Type.vec3f }, + texture_depth_cube_array: { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureSampleCompareParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecompare') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureSampleCompareParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleCompareParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureSampleCompareParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureSampleCompare(t, s, ${coordWGSL}${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecompare') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureSampleCompareParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleCompareParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgType, offsetArgType } = kValidTextureSampleCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureSampleCompare(t, s, ${coordWGSL}, ${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('depth_ref_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecompare') + .desc( + ` +Validates that only incorrect depth_ref arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('depthRefType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.depthRefType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleCompareParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, depthRefType, value, offset } = t.params; + const depthRefArgType = kValuesTypes[depthRefType]; + const args = [depthRefArgType.create(value)]; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const depthRefWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureSampleCompare(t, s, ${coordWGSL}${arrayWGSL}, ${depthRefWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(depthRefArgType, Type.f32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecompare') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureSampleCompareParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureSampleCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + _ = textureSampleCompare(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecompare') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureSampleCompareParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType?.create(0).wgsl()}; + _ = textureSampleCompare(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('only_in_fragment') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesample') + .desc( + ` +Validates that ${builtin} must not be used in a compute or vertex shader. +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('entryPoint', keysOf(kEntryPointsToValidateFragmentOnlyBuiltins)) + .expand('offset', t => + kValidTextureSampleCompareParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, entryPoint, offset } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleCompareParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const config = kEntryPointsToValidateFragmentOnlyBuiltins[entryPoint]; + const code = ` +${config.code} +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; + +fn foo() { + _ = textureSampleCompare(t, s, ${coordWGSL}${arrayWGSL}, 0${offsetWGSL}); +}`; + t.expectCompileResult(config.expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleCompareLevel.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleCompareLevel.spec.ts new file mode 100644 index 0000000000..cfb2090dc7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleCompareLevel.spec.ts @@ -0,0 +1,268 @@ +const builtin = 'textureSampleCompareLevel'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSampleCompareLevel coords parameter must be correct type +* test textureSampleCompareLevel array_index parameter must be correct type +* test textureSampleCompareLevel depth_ref parameter must be correct type +* test textureSampleCompareLevel offset parameter must be correct type +* test textureSampleCompareLevel offset parameter must be a const-expression +* test textureSampleCompareLevel offset parameter must be between -8 and +7 inclusive +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +type TextureSampleCompareLevelArguments = { + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureSampleCompareLevelParameterTypes: { + [n: string]: TextureSampleCompareLevelArguments; +} = { + texture_depth_2d: { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + texture_depth_2d_array: { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + texture_depth_cube: { coordsArgType: Type.vec3f }, + texture_depth_cube_array: { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureSampleCompareLevelParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecomparelevel') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureSampleCompareLevelParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleCompareLevelParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureSampleCompareLevelParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleCompareLevel(t, s, ${coordWGSL}${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecomparelevel') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureSampleCompareLevelParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleCompareLevelParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgType, offsetArgType } = + kValidTextureSampleCompareLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleCompareLevel(t, s, ${coordWGSL}, ${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('depth_ref_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecomparelevel') + .desc( + ` +Validates that only incorrect depth_ref arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('depthRefType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.depthRefType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleCompareLevelParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, depthRefType, value, offset } = t.params; + const depthRefArgType = kValuesTypes[depthRefType]; + const args = [depthRefArgType.create(value)]; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleCompareLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const depthRefWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleCompareLevel(t, s, ${coordWGSL}${arrayWGSL}, ${depthRefWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(depthRefArgType, Type.f32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecomparelevel') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureSampleCompareLevelParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureSampleCompareLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleCompareLevel(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplecomparelevel') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureSampleCompareLevelParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleCompareLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler_comparison; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType?.create(0).wgsl()}; + let v = textureSampleCompareLevel(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleGrad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleGrad.spec.ts new file mode 100644 index 0000000000..3d1b522f86 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleGrad.spec.ts @@ -0,0 +1,317 @@ +const builtin = 'textureSampleGrad'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSampleGrad coords parameter must be correct type +* test textureSampleGrad array_index parameter must be correct type +* test textureSampleGrad ddX parameter must be correct type +* test textureSampleGrad ddY parameter must be correct type +* test textureSampleGrad coords parameter must be correct type +* test textureSampleGrad offset parameter must be correct type +* test textureSampleGrad offset parameter must be a const-expression +* test textureSampleGrad offset parameter must be between -8 and +7 inclusive +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +// Note: ddX and ddy parameter types match coords so we'll use coordsArgType for ddX and ddY. +type TextureSampleGradArguments = { + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureSampleGradParameterTypes: { [n: string]: TextureSampleGradArguments } = { + 'texture_2d<f32>': { + coordsArgType: Type.vec2f, + offsetArgType: Type.vec2i, + }, + 'texture_2d_array<f32>': { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + 'texture_3d<f32>': { coordsArgType: Type.vec3f, offsetArgType: Type.vec3i }, + 'texture_cube<f32>': { coordsArgType: Type.vec3f }, + 'texture_cube_array<f32>': { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureSampleGradParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplegrad') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureSampleGradParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleGradParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureSampleGradParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const ddWGSL = coordsRequiredType.create(0).wgsl(); + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleGrad(t, s, ${coordWGSL}${arrayWGSL}, ${ddWGSL}, ${ddWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplegrad') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureSampleGradParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleGradParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgType, offsetArgType } = kValidTextureSampleGradParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const ddWGSL = coordsArgType.create(0).wgsl(); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleGrad(t, s, ${coordWGSL}, ${arrayWGSL}, ${ddWGSL}, ${ddWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('ddX_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplegrad') + .desc( + ` +Validates that only incorrect ddX arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('ddxType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.ddxType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleGradParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, ddxType, value, offset } = t.params; + const ddxArgType = kValuesTypes[ddxType]; + const args = [ddxArgType.create(value)]; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleGradParameterTypes[textureType]; + + const ddxRequiredType = coordsArgType; + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const ddXWGSL = args.map(arg => arg.wgsl()).join(', '); + const ddYWGSL = coordsArgType.create(0).wgsl(); + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleGrad(t, s, ${coordWGSL}${arrayWGSL}, ${ddXWGSL}, ${ddYWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(ddxArgType, ddxRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('ddY_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplegrad') + .desc( + ` +Validates that only incorrect ddY arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('ddyType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.ddyType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleGradParameterTypes[t.textureType].offsetArgType ? [false, true] : [false] + ) + ) + .fn(t => { + const { textureType, ddyType, value, offset } = t.params; + const ddyArgType = kValuesTypes[ddyType]; + const args = [ddyArgType.create(value)]; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleGradParameterTypes[textureType]; + + const ddyRequiredType = coordsArgType; + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const ddXWGSL = coordsArgType.create(0).wgsl(); + const ddYWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleGrad(t, s, ${coordWGSL}${arrayWGSL}, ${ddXWGSL}, ${ddYWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(ddyArgType, ddyRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplegrad') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureSampleGradParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureSampleGradParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const ddWGSL = coordsArgType.create(0).wgsl(); + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleGrad(t, s, ${coordWGSL}${arrayWGSL}, ${ddWGSL}, ${ddWGSL}, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplegrad') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureSampleGradParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleGradParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const ddWGSL = coordsArgType.create(0).wgsl(); + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType!.create(0).wgsl()}; + let v = textureSampleGrad(t, s, ${coordWGSL}${arrayWGSL}, ${ddWGSL}, ${ddWGSL}, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleLevel.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleLevel.spec.ts new file mode 100644 index 0000000000..9a6701421c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureSampleLevel.spec.ts @@ -0,0 +1,282 @@ +const builtin = 'textureSampleLevel'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSampleLevel coords parameter must be correct type +* test textureSampleLevel array_index parameter must be correct type +* test textureSampleLevel level parameter must be correct type +* test textureSampleLevel offset parameter must be correct type +* test textureSampleLevel offset parameter must be a const-expression +* test textureSampleLevel offset parameter must be between -8 and +7 inclusive +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +type TextureSampleLevelArguments = { + coordsArgType: ScalarType | VectorType; + hasArrayIndexArg?: boolean; + levelIsF32?: boolean; + offsetArgType?: VectorType; +}; + +const kValidTextureSampleLevelParameterTypes: { [n: string]: TextureSampleLevelArguments } = { + 'texture_2d<f32>': { coordsArgType: Type.vec2f, levelIsF32: true, offsetArgType: Type.vec2i }, + 'texture_2d_array<f32>': { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + levelIsF32: true, + offsetArgType: Type.vec2i, + }, + 'texture_3d<f32>': { coordsArgType: Type.vec3f, levelIsF32: true, offsetArgType: Type.vec3i }, + 'texture_cube<f32>': { coordsArgType: Type.vec3f, levelIsF32: true }, + 'texture_cube_array<f32>': { + coordsArgType: Type.vec3f, + hasArrayIndexArg: true, + levelIsF32: true, + }, + texture_depth_2d: { coordsArgType: Type.vec2f, offsetArgType: Type.vec2i }, + texture_depth_2d_array: { + coordsArgType: Type.vec2f, + hasArrayIndexArg: true, + offsetArgType: Type.vec2i, + }, + texture_depth_cube: { coordsArgType: Type.vec3f }, + texture_depth_cube_array: { coordsArgType: Type.vec3f, hasArrayIndexArg: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureSampleLevelParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplelevel') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureSampleLevelParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleLevelParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, coordType, offset, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { + offsetArgType, + coordsArgType: coordsRequiredType, + hasArrayIndexArg, + } = kValidTextureSampleLevelParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offsetArgType?.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleLevel(t, s, ${coordWGSL}${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = isConvertible(coordArgType, coordsRequiredType); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplelevel') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureSampleLevelParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleLevelParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, arrayIndexType, value, offset } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgType, offsetArgType } = kValidTextureSampleLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleLevel(t, s, ${coordWGSL}, ${arrayWGSL}, 0${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('level_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplelevel') + .desc( + ` +Validates that only incorrect level arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('levelType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.levelType]) || t.value >= 0) + .expand('offset', t => + kValidTextureSampleLevelParameterTypes[t.textureType].offsetArgType + ? [false, true] + : [false] + ) + ) + .fn(t => { + const { textureType, levelType, value, offset } = t.params; + const levelArgType = kValuesTypes[levelType]; + const args = [levelArgType.create(value)]; + const { coordsArgType, hasArrayIndexArg, offsetArgType, levelIsF32 } = + kValidTextureSampleLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const levelWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offsetArgType!.create(0).wgsl()}` : ''; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleLevel(t, s, ${coordWGSL}${arrayWGSL}, ${levelWGSL}${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = levelIsF32 + ? isConvertible(levelArgType, Type.f32) + : isConvertible(levelArgType, Type.i32) || isConvertible(levelArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplelevel') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter(t => !!kValidTextureSampleLevelParameterTypes[t.textureType].offsetArgType) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.offsetType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const offsetArgType = kValuesTypes[offsetType]; + const args = [offsetArgType.create(value)]; + const { + coordsArgType, + hasArrayIndexArg, + offsetArgType: offsetRequiredType, + } = kValidTextureSampleLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@fragment fn fs() -> @location(0) vec4f { + let v = textureSampleLevel(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(offsetArgType, offsetRequiredType!) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturesamplelevel') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter(t => !!kValidTextureSampleLevelParameterTypes[t.textureType].offsetArgType) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coordsArgType, hasArrayIndexArg, offsetArgType } = + kValidTextureSampleLevelParameterTypes[textureType]; + + const coordWGSL = coordsArgType.create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const offsetWGSL = `${offsetArgType}(${varType})`; + + const code = ` +@group(0) @binding(0) var s: sampler; +@group(0) @binding(1) var t: ${textureType}; +@group(0) @binding(2) var<uniform> u: ${offsetArgType}; +@fragment fn fs() -> @location(0) vec4f { + const c = 1; + let l = ${offsetArgType!.create(0).wgsl()}; + let v = textureSampleLevel(t, s, ${coordWGSL}${arrayWGSL}, 0, ${offsetWGSL}); + return vec4f(0); +} +`; + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureStore.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureStore.spec.ts new file mode 100644 index 0000000000..6613377732 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/textureStore.spec.ts @@ -0,0 +1,168 @@ +const builtin = 'textureStore'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureStore coords parameter must be correct type +* test textureStore array_index parameter must be correct type +* test textureStore value parameter must be correct type +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { kAllTextureFormats, kTextureFormatInfo } from '../../../../../format_info.js'; +import { + Type, + kAllScalarsAndVectors, + isConvertible, + ScalarType, + VectorType, + isUnsignedType, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kTextureColorTypeToType = { + sint: Type.vec4i, + uint: Type.vec4u, + float: Type.vec4f, + 'unfilterable-float': Type.vec4f, +}; + +type TextureStoreArguments = { + coordsArgTypes: readonly [ScalarType | VectorType, ScalarType | VectorType]; + hasArrayIndexArg?: boolean; +}; + +const kValidTextureStoreParameterTypes: { [n: string]: TextureStoreArguments } = { + texture_storage_1d: { coordsArgTypes: [Type.i32, Type.u32] }, + texture_storage_2d: { coordsArgTypes: [Type.vec2i, Type.vec2u] }, + texture_storage_2d_array: { + coordsArgTypes: [Type.vec2i, Type.vec2u], + hasArrayIndexArg: true, + }, + texture_storage_3d: { coordsArgTypes: [Type.vec3i, Type.vec3u] }, +} as const; + +const kTextureTypes = keysOf(kValidTextureStoreParameterTypes); +const kValuesTypes = objectsToRecord(kAllScalarsAndVectors); + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturestore') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', keysOf(kValidTextureStoreParameterTypes)) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.coordType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, coordType, value } = t.params; + const coordArgType = kValuesTypes[coordType]; + const { coordsArgTypes, hasArrayIndexArg } = kValidTextureStoreParameterTypes[textureType]; + + const coordWGSL = coordArgType.create(value).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const format = 'rgba8unorm'; + const valueWGSL = 'vec4f(0)'; + + const code = ` +@group(0) @binding(0) var t: ${textureType}<${format},write>; +@fragment fn fs() -> @location(0) vec4f { + textureStore(t, ${coordWGSL}${arrayWGSL}, ${valueWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(coordArgType, coordsArgTypes[0]) || + isConvertible(coordArgType, coordsArgTypes[1]); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturestore') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(t => !!kValidTextureStoreParameterTypes[t.textureType].hasArrayIndexArg) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + // filter out unsigned types with negative values + .filter(t => !isUnsignedType(kValuesTypes[t.arrayIndexType]) || t.value >= 0) + ) + .fn(t => { + const { textureType, arrayIndexType, value } = t.params; + const arrayIndexArgType = kValuesTypes[arrayIndexType]; + const args = [arrayIndexArgType.create(value)]; + const { coordsArgTypes } = kValidTextureStoreParameterTypes[textureType]; + + const coordWGSL = coordsArgTypes[0].create(0).wgsl(); + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const format = 'rgba8unorm'; + const valueWGSL = 'vec4f(0)'; + + const code = ` +@group(0) @binding(0) var t: ${textureType}<${format}, write>; +@fragment fn fs() -> @location(0) vec4f { + textureStore(t, ${coordWGSL}, ${arrayWGSL}, ${valueWGSL}); + return vec4f(0); +} +`; + const expectSuccess = + isConvertible(arrayIndexArgType, Type.i32) || isConvertible(arrayIndexArgType, Type.u32); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('value_argument') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#texturestore') + .desc( + ` +Validates that only incorrect value arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('valueType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('format', kAllTextureFormats) + // filter to only storage texture formats. + .filter(t => !!kTextureFormatInfo[t.format].color?.storage) + .combine('value', [0, 1, 2]) + ) + .fn(t => { + const { textureType, valueType, format, value } = t.params; + const valueArgType = kValuesTypes[valueType]; + const args = [valueArgType.create(value)]; + const { coordsArgTypes, hasArrayIndexArg } = kValidTextureStoreParameterTypes[textureType]; + + const coordWGSL = coordsArgTypes[0].create(0).wgsl(); + const arrayWGSL = hasArrayIndexArg ? ', 0' : ''; + const valueWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` +@group(0) @binding(0) var t: ${textureType}<${format}, write>; +@fragment fn fs() -> @location(0) vec4f { + textureStore(t, ${coordWGSL}${arrayWGSL}, ${valueWGSL}); + return vec4f(0); +} +`; + const colorType = kTextureFormatInfo[format].color?.type; + const requiredValueType = kTextureColorTypeToType[colorType!]; + const expectSuccess = isConvertible(valueArgType, requiredValueType); + t.expectCompileResult(expectSuccess, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/trunc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/trunc.spec.ts new file mode 100644 index 0000000000..e0bed7dc5e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/trunc.spec.ts @@ -0,0 +1,94 @@ +const builtin = 'trunc'; +export const description = ` +Validation tests for the ${builtin}() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + Type, + kConvertableToFloatScalarsAndVectors, + scalarTypeOf, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +import { + fullRangeForType, + kConstantAndOverrideStages, + stageSupportsType, + validateConstOrOverrideBuiltinEval, +} from './const_override_validation.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidArgumentTypes = objectsToRecord(kConvertableToFloatScalarsAndVectors); + +g.test('values') + .desc( + ` +Validates that constant evaluation and override evaluation of ${builtin}() error on invalid inputs. +` + ) + .params(u => + u + .combine('stage', kConstantAndOverrideStages) + .combine('type', keysOf(kValidArgumentTypes)) + .filter(u => stageSupportsType(u.stage, kValidArgumentTypes[u.type])) + .beginSubcases() + .expand('value', u => fullRangeForType(kValidArgumentTypes[u.type])) + ) + .beforeAllSubcases(t => { + if (scalarTypeOf(kValidArgumentTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const expectedResult = true; + + const type = kValidArgumentTypes[t.params.type]; + validateConstOrOverrideBuiltinEval( + t, + builtin, + expectedResult, + [type.create(t.params.value)], + t.params.stage + ); + }); + +const kArgCases = { + good: '(1.2)', + bad_no_parens: '', + // Bad number of args + bad_0args: '()', + bad_2arg: '(1.2, 2.3)', + // Bad value for arg 0 + bad_0bool: '(false)', + bad_0array: '(array(1.1,2.2))', + bad_0struct: '(modf(2.2))', + bad_0uint: '(1u)', + bad_0int: '(1i)', + bad_0vec2i: '(vec2i())', + bad_0vec2u: '(vec2u())', + bad_0vec3i: '(vec3i())', + bad_0vec3u: '(vec3u())', + bad_0vec4i: '(vec4i())', + bad_0vec4u: '(vec4u())', +}; + +g.test('args') + .desc(`Test compilation failure of ${builtin} with variously shaped and typed arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.expectCompileResult( + t.params.arg === 'good', + `const c = ${builtin}${kArgCases[t.params.arg]};` + ); + }); + +g.test('must_use') + .desc(`Result of ${builtin} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${builtin}${kArgCases['good']}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16float.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16float.spec.ts new file mode 100644 index 0000000000..bac245f99a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16float.spec.ts @@ -0,0 +1,62 @@ +const kFn = 'unpack2x16float'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good_u32: '(1u)', + good_aint: '(1)', + bad_0args: '()', + bad_2args: '(1u,2u)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_f16: '(1h)', + bad_bool: '(false)', + bad_vec2u: '(vec2u())', + bad_vec3u: '(vec3u())', + bad_vec4u: '(vec4u())', + bad_array: '(array(1))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good_u32']; +const kReturnType = 'vec2f'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .beforeAllSubcases(t => { + if (t.params.arg === 'bad_f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let code = ''; + if (t.params.arg === 'bad_f16') { + code += 'enable f16;\n'; + } + code += `const c = ${kFn}${kArgCases[t.params.arg]};`; + + t.expectCompileResult(t.params.arg.startsWith('good'), code); + }); + +g.test('return') + .desc(`Test ${kFn} return value type ${kReturnType}`) + .params(u => u.combine('type', ['vec2u', 'vec2i', 'vec2f', 'vec2h', 'vec4f', 'vec3f', 'f32'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16snorm.spec.ts new file mode 100644 index 0000000000..d1cd6c5c4d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16snorm.spec.ts @@ -0,0 +1,62 @@ +const kFn = 'unpack2x16snorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good_u32: '(1u)', + good_aint: '(1)', + bad_0args: '()', + bad_2args: '(1u,2u)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_f16: '(1h)', + bad_bool: '(false)', + bad_vec2u: '(vec2u())', + bad_vec3u: '(vec3u())', + bad_vec4u: '(vec4u())', + bad_array: '(array(1))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good_u32']; +const kReturnType = 'vec2f'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .beforeAllSubcases(t => { + if (t.params.arg === 'bad_f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let code = ''; + if (t.params.arg === 'bad_f16') { + code += 'enable f16;\n'; + } + code += `const c = ${kFn}${kArgCases[t.params.arg]};`; + + t.expectCompileResult(t.params.arg.startsWith('good'), code); + }); + +g.test('return') + .desc(`Test ${kFn} return value type ${kReturnType}`) + .params(u => u.combine('type', ['vec2u', 'vec2i', 'vec2f', 'vec2h', 'vec4f', 'vec3f', 'f32'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16unorm.spec.ts new file mode 100644 index 0000000000..7fcc96f22f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack2x16unorm.spec.ts @@ -0,0 +1,62 @@ +const kFn = 'unpack2x16unorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good_u32: '(1u)', + good_aint: '(1)', + bad_0args: '()', + bad_2args: '(1u,2u)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_f16: '(1h)', + bad_bool: '(false)', + bad_vec2u: '(vec2u())', + bad_vec3u: '(vec3u())', + bad_vec4u: '(vec4u())', + bad_array: '(array(1))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good_u32']; +const kReturnType = 'vec2f'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .beforeAllSubcases(t => { + if (t.params.arg === 'bad_f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let code = ''; + if (t.params.arg === 'bad_f16') { + code += 'enable f16;\n'; + } + code += `const c = ${kFn}${kArgCases[t.params.arg]};`; + + t.expectCompileResult(t.params.arg.startsWith('good'), code); + }); + +g.test('return') + .desc(`Test ${kFn} return value type ${kReturnType}`) + .params(u => u.combine('type', ['vec2u', 'vec2i', 'vec2f', 'vec2h', 'vec4f', 'vec3f', 'f32'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4x8snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4x8snorm.spec.ts new file mode 100644 index 0000000000..98052f4494 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4x8snorm.spec.ts @@ -0,0 +1,62 @@ +const kFn = 'unpack4x8snorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good_u32: '(1u)', + good_aint: '(1)', + bad_0args: '()', + bad_2args: '(1u,2u)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_f16: '(1h)', + bad_bool: '(false)', + bad_vec2u: '(vec2u())', + bad_vec3u: '(vec3u())', + bad_vec4u: '(vec4u())', + bad_array: '(array(1))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good_u32']; +const kReturnType = 'vec4f'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .beforeAllSubcases(t => { + if (t.params.arg === 'bad_f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let code = ''; + if (t.params.arg === 'bad_f16') { + code += 'enable f16;\n'; + } + code += `const c = ${kFn}${kArgCases[t.params.arg]};`; + + t.expectCompileResult(t.params.arg.startsWith('good'), code); + }); + +g.test('return') + .desc(`Test ${kFn} return value type ${kReturnType}`) + .params(u => u.combine('type', ['vec4u', 'vec4i', 'vec4f', 'vec4h', 'vec3f', 'vec2f', 'f32'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4x8unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4x8unorm.spec.ts new file mode 100644 index 0000000000..746443641a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4x8unorm.spec.ts @@ -0,0 +1,62 @@ +const kFn = 'unpack4x8unorm'; +export const description = `Validate ${kFn}`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kArgCases = { + good_u32: '(1u)', + good_aint: '(1)', + bad_0args: '()', + bad_2args: '(1u,2u)', + bad_i32: '(1i)', + bad_f32: '(1f)', + bad_f16: '(1h)', + bad_bool: '(false)', + bad_vec2u: '(vec2u())', + bad_vec3u: '(vec3u())', + bad_vec4u: '(vec4u())', + bad_array: '(array(1))', + bad_struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good_u32']; +const kReturnType = 'vec4f'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .beforeAllSubcases(t => { + if (t.params.arg === 'bad_f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + let code = ''; + if (t.params.arg === 'bad_f16') { + code += 'enable f16;\n'; + } + code += `const c = ${kFn}${kArgCases[t.params.arg]};`; + + t.expectCompileResult(t.params.arg.startsWith('good'), code); + }); + +g.test('return') + .desc(`Test ${kFn} return value type ${kReturnType}`) + .params(u => u.combine('type', ['vec4u', 'vec4i', 'vec4f', 'vec4h', 'vec3f', 'vec2f', 'f32'])) + .fn(t => { + t.expectCompileResult( + t.params.type === kReturnType, + `const c: ${t.params.type} = ${kFn}${kGoodArgs};` + ); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4xI8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4xI8.spec.ts new file mode 100644 index 0000000000..9736818b71 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4xI8.spec.ts @@ -0,0 +1,61 @@ +export const description = `Validate unpack4xI8`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'unpack4xI8'; +const kArgCases = { + good: '(1u)', + bad_0args: '()', + bad_2args: '(1u,2u)', + bad_0i32: '(1i)', + bad_0f32: '(1f)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0vec3u: '(vec3u())', + bad_0vec4u: '(vec4u())', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4xU8.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4xU8.spec.ts new file mode 100644 index 0000000000..34b96a4615 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/unpack4xU8.spec.ts @@ -0,0 +1,61 @@ +export const description = `Validate unpack4xU8`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +const kFeature = 'packed_4x8_integer_dot_product'; +const kFn = 'unpack4xU8'; +const kArgCases = { + good: '(1u)', + bad_0args: '()', + bad_2args: '(1u,2u)', + bad_0i32: '(1i)', + bad_0f32: '(1f)', + bad_0bool: '(false)', + bad_0vec2u: '(vec2u())', + bad_0vec3u: '(vec3u())', + bad_0vec4u: '(vec4u())', + bad_0array: '(array(1))', + bad_0struct: '(modf(1.1))', +}; +const kGoodArgs = kArgCases['good']; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('unsupported') + .desc(`Test absence of ${kFn} when ${kFeature} is not supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(false, code); + }); + +g.test('supported') + .desc(`Test presence of ${kFn} when ${kFeature} is supported.`) + .params(u => u.combine('requires', [false, true])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const preamble = t.params.requires ? `requires ${kFeature}; ` : ''; + const code = `${preamble}const c = ${kFn}${kGoodArgs};`; + t.expectCompileResult(true, code); + }); + +g.test('args') + .desc(`Test compilation failure of ${kFn} with various numbers of and types of arguments`) + .params(u => u.combine('arg', keysOf(kArgCases))) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + t.expectCompileResult(t.params.arg === 'good', `const c = ${kFn}${kArgCases[t.params.arg]};`); + }); + +g.test('must_use') + .desc(`Result of ${kFn} must be used`) + .params(u => u.combine('use', [true, false])) + .fn(t => { + t.skipIfLanguageFeatureNotSupported(kFeature); + const use_it = t.params.use ? '_ = ' : ''; + t.expectCompileResult(t.params.use, `fn f() { ${use_it}${kFn}${kGoodArgs}; }`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/workgroupUniformLoad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/workgroupUniformLoad.spec.ts new file mode 100644 index 0000000000..ac7fd042eb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/call/builtin/workgroupUniformLoad.spec.ts @@ -0,0 +1,122 @@ +export const description = ` +Validation tests for the workgroupUniformLoad() builtin. +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kEntryPoints = { + none: { supportsBarrier: true, code: `` }, + compute: { + supportsBarrier: true, + code: `@compute @workgroup_size(1) +fn main() { + foo(); +}`, + }, + vertex: { + supportsBarrier: false, + code: `@vertex +fn main() -> @builtin(position) vec4f { + foo(); + return vec4f(); +}`, + }, + fragment: { + supportsBarrier: false, + code: `@fragment +fn main() { + foo(); +}`, + }, + compute_and_fragment: { + supportsBarrier: false, + code: `@compute @workgroup_size(1) +fn main1() { + foo(); +} + +@fragment +fn main2() { + foo(); +} +`, + }, + fragment_without_call: { + supportsBarrier: true, + code: `@fragment +fn main() { +} +`, + }, +}; + +g.test('only_in_compute') + .specURL('https://www.w3.org/TR/WGSL/#sync-builtin-functions') + .desc( + ` +Synchronization functions must only be used in the compute shader stage. +` + ) + .params(u => + u + .combine('entry_point', keysOf(kEntryPoints)) + .combine('call', ['bar()', 'workgroupUniformLoad(&wgvar)']) + ) + .fn(t => { + const config = kEntryPoints[t.params.entry_point]; + const code = ` +${config.code} + +var<workgroup> wgvar : u32; + +fn bar() -> u32 { + return 0; +} + +fn foo() { + _ = ${t.params.call}; +}`; + t.expectCompileResult(t.params.call === 'bar()' || config.supportsBarrier, code); + }); + +// A list of types that contains atomics, with a single control case. +const kAtomicTypes: string[] = [ + 'bool', // control case + 'atomic<i32>', + 'atomic<u32>', + 'array<atomic<i32>, 4>', + 'AtomicStruct', +]; + +g.test('no_atomics') + .desc( + ` +The argument passed to workgroupUniformLoad cannot contain any atomic types. + +NOTE: Various other valid types are tested via execution tests, so we only check for invalid types here. +` + ) + .params(u => + u.combine('type', kAtomicTypes).combine('call', ['bar()', 'workgroupUniformLoad(&wgvar)']) + ) + .fn(t => { + const code = ` +struct AtomicStruct { + a : atomic<u32> +} + +var<workgroup> wgvar : ${t.params.type}; + +fn bar() -> bool { + return true; +} + +fn foo() { + _ = ${t.params.call}; +}`; + t.expectCompileResult(t.params.type === 'bool' || t.params.call === 'bar()', code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/overload_resolution.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/overload_resolution.spec.ts new file mode 100644 index 0000000000..65ba9b0f35 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/overload_resolution.spec.ts @@ -0,0 +1,268 @@ +export const description = `Validation tests for implicit conversions and overload resolution`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../common/util/data_tables.js'; +import { + kAllNumericScalarsAndVectors, + isConvertible, + VectorType, +} from '../../../util/conversion.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +interface Case { + expr: string; + valid: boolean; + f16?: boolean; +} + +const kImplicitConversionCases: Record<string, Case> = { + absint_to_bool: { + expr: `any(1)`, + valid: false, + }, + absint_to_u32: { + expr: `1 == 1u`, + valid: true, + }, + absint_to_i32: { + expr: `1 == 1i`, + valid: true, + }, + absint_to_f32: { + expr: `1 == 1f`, + valid: true, + }, + absint_to_f16: { + expr: `1 == 1h`, + valid: true, + f16: true, + }, + absfloat_to_bool: { + expr: `any(1.0)`, + valid: false, + }, + absfloat_to_u32: { + expr: `1.0 == 1u`, + valid: false, + }, + absfloat_to_i32: { + expr: `1.0 == 1i`, + valid: false, + }, + absfloat_to_f32: { + expr: `1.0 == 1f`, + valid: true, + }, + absfloat_to_f16: { + expr: `1.0 == 1h`, + valid: true, + f16: true, + }, + vector_absint_to_bool: { + expr: `any(vec2(1))`, + valid: false, + }, + vector_absint_to_u32: { + expr: `all(vec2(1) == vec2u(1u))`, + valid: true, + }, + vector_absint_to_i32: { + expr: `all(vec3(1) == vec3i(1i))`, + valid: true, + }, + vector_absint_to_f32: { + expr: `all(vec4(1) == vec4f(1f))`, + valid: true, + }, + vector_absint_to_f16: { + expr: `all(vec2(1) == vec2h(1h))`, + valid: true, + f16: true, + }, + vector_absfloat_to_bool: { + expr: `any(vec2(1.0))`, + valid: false, + }, + vector_absfloat_to_u32: { + expr: `all(vec2(1.0) == vec2u(1u))`, + valid: false, + }, + vector_absfloat_to_i32: { + expr: `all(vec3(1.0) == vec2i(1i))`, + valid: false, + }, + vector_absfloat_to_f32: { + expr: `all(vec4(1.0) == vec4f(1f))`, + valid: true, + }, + vector_absfloat_to_f16: { + expr: `all(vec2(1.0) == vec2h(1h))`, + valid: true, + f16: true, + }, + vector_swizzle_integer: { + expr: `vec2(1).x == 1i`, + valid: true, + }, + vector_swizzle_float: { + expr: `vec2(1).y == 1f`, + valid: true, + }, + vector_default_ctor_integer: { + expr: `all(vec3().xy == vec2i())`, + valid: true, + }, + vector_default_ctor_abstract: { + expr: `all(vec3().xy == vec2())`, + valid: true, + }, + vector_swizzle_abstract: { + expr: `vec4(1f).x == 1`, + valid: true, + }, + vector_abstract_to_integer: { + expr: `all(vec4(1) == vec4i(1))`, + valid: true, + }, + vector_wrong_result_i32: { + expr: `vec2(1,2f).x == 1i`, + valid: false, + }, + vector_wrong_result_f32: { + expr: `vec2(1,2i).y == 2f`, + valid: false, + }, + vector_wrong_result_splat: { + expr: `vec2(1.0).x == 1i`, + valid: false, + }, + array_absint_to_bool: { + expr: `any(array(1)[0])`, + valid: false, + }, + array_absint_to_u32: { + expr: `array(1)[0] == array<u32,1>(1u)[0]`, + valid: true, + }, + array_absint_to_i32: { + expr: `array(1)[0] == array<i32,1>(1i)[0]`, + valid: true, + }, + array_absint_to_f32: { + expr: `array(1)[0] == array<f32,1>(1f)[0]`, + valid: true, + }, + array_absint_to_f16: { + expr: `array(1)[0] == array<f16,1>(1h)[0]`, + valid: true, + f16: true, + }, + array_absfloat_to_bool: { + expr: `any(array(1.0)[0])`, + valid: false, + }, + array_absfloat_to_u32: { + expr: `array(1.0)[0] == array<u32,1>(1u)[0]`, + valid: false, + }, + array_absfloat_to_i32: { + expr: `array(1.0)[0] == array<i32,1>(1i)[0]`, + valid: false, + }, + array_absfloat_to_f32: { + expr: `array(1.0)[0] == array<f32,1>(1f)[0]`, + valid: true, + }, + array_absfloat_to_f16: { + expr: `array(1.0)[0] == array<f16,1>(1h)[0]`, + valid: true, + f16: true, + }, + mat2x2_index_absint: { + expr: `all(mat2x2(1,2,3,4)[0] == vec2(1,2))`, + valid: true, + }, + mat2x2_index_absfloat: { + expr: `all(mat2x2(1,2,3,4)[1] == vec2(3.0,4.0))`, + valid: true, + }, + mat2x2_index_float: { + expr: `all(mat2x2(0,0,0,0)[1] == vec2f())`, + valid: true, + }, + mat2x2_wrong_result: { + expr: `all(mat2x2(0f,0,0,0)[0] == vec2h())`, + valid: false, + f16: true, + }, +}; + +g.test('implicit_conversions') + .desc('Test implicit conversions') + .params(u => u.combine('case', keysOf(kImplicitConversionCases))) + .beforeAllSubcases(t => { + if (kImplicitConversionCases[t.params.case].f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const testcase = kImplicitConversionCases[t.params.case]; + const code = `${testcase.f16 ? 'enable f16;' : ''} + const_assert ${testcase.expr};`; + t.expectCompileResult(testcase.valid, code); + }); + +const kTypes = objectsToRecord(kAllNumericScalarsAndVectors); +const kTypeKeys = keysOf(kTypes); + +g.test('overload_resolution') + .desc('Test overload resolution') + .params(u => + u + .combine('arg1', kTypeKeys) + .combine('arg2', kTypeKeys) + .beginSubcases() + .combine('op', ['min', 'max'] as const) + .filter(t => { + if (t.arg1 === t.arg2) { + return false; + } + const t1 = kTypes[t.arg1]; + const t2 = kTypes[t.arg2]; + const t1IsVector = t1 instanceof VectorType; + const t2IsVector = t2 instanceof VectorType; + if (t1IsVector !== t2IsVector) { + return false; + } + if (t1IsVector && t2IsVector && t1.size !== t2.size) { + return false; + } + return true; + }) + ) + .beforeAllSubcases(t => { + const t1 = kTypes[t.params.arg1]; + const t2 = kTypes[t.params.arg2]; + if (t1.requiresF16() || t2.requiresF16()) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const t1 = kTypes[t.params.arg1]; + const t2 = kTypes[t.params.arg2]; + const resTy = isConvertible(t1, t2) ? t2 : t1; + const enable = `${t1.requiresF16() || t2.requiresF16() ? 'enable f16;' : ''}`; + const min = 50; + const max = 100; + const res = t.params.op === 'min' ? min : max; + const v1 = t1.create(min).wgsl(); + const v2 = t2.create(max).wgsl(); + const resV = resTy.create(res).wgsl(); + const expr = `${t.params.op}(${v1}, ${v2}) == ${resV}`; + const assertExpr = t1 instanceof VectorType ? `all(${expr})` : expr; + const code = `${enable} + const_assert ${assertExpr};`; + t.expectCompileResult(isConvertible(t1, t2) || isConvertible(t2, t1), code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/precedence.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/precedence.spec.ts new file mode 100644 index 0000000000..15f7ad4a97 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/precedence.spec.ts @@ -0,0 +1,188 @@ +export const description = ` +Validation tests for operator precedence. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// Bit set for the binary operator groups. +const kMultiplicative = 1 << 0; +const kAdditive = 1 << 1; +const kShift = 1 << 2; +const kRelational = 1 << 3; +const kBinaryAnd = 1 << 4; +const kBinaryXor = 1 << 5; +const kBinaryOr = 1 << 6; +const kLogical = 1 << 7; + +// Set of other operators that each operator can precede without any parentheses. +const kCanPrecedeWithoutParens: Record<number, number> = {}; +kCanPrecedeWithoutParens[kMultiplicative] = kMultiplicative | kAdditive | kRelational; +kCanPrecedeWithoutParens[kAdditive] = kMultiplicative | kAdditive | kRelational; +kCanPrecedeWithoutParens[kShift] = kRelational | kLogical; +kCanPrecedeWithoutParens[kRelational] = kMultiplicative | kAdditive | kShift | kLogical; +kCanPrecedeWithoutParens[kBinaryAnd] = kBinaryAnd; +kCanPrecedeWithoutParens[kBinaryXor] = kBinaryXor; +kCanPrecedeWithoutParens[kBinaryOr] = kBinaryOr; +kCanPrecedeWithoutParens[kLogical] = kRelational; + +// The list of binary operators. +interface BinaryOperatorInfo { + op: string; + group: number; +} +const kBinaryOperators: Record<string, BinaryOperatorInfo> = { + mul: { op: '*', group: kMultiplicative }, + div: { op: '/', group: kMultiplicative }, + mod: { op: '%', group: kMultiplicative }, + + add: { op: '+', group: kAdditive }, + sub: { op: '-', group: kAdditive }, + + shl: { op: '<<', group: kShift }, + shr: { op: '>>', group: kShift }, + + lt: { op: '<', group: kRelational }, + gt: { op: '>', group: kRelational }, + le: { op: '<=', group: kRelational }, + ge: { op: '>=', group: kRelational }, + eq: { op: '==', group: kRelational }, + ne: { op: '!=', group: kRelational }, + + bin_and: { op: '&', group: kBinaryAnd }, + bin_xor: { op: '^', group: kBinaryXor }, + bin_or: { op: '|', group: kBinaryOr }, + + log_and: { op: '&&', group: kLogical }, + log_or: { op: '||', group: kLogical }, +}; + +g.test('binary_requires_parentheses') + .desc( + ` + Validates that certain binary operators require parentheses to bind correctly. + ` + ) + .params(u => + u + .combine('op1', keysOf(kBinaryOperators)) + .combine('op2', keysOf(kBinaryOperators)) + .filter(p => { + // Skip expressions that would parse as template lists. + if (p.op1 === 'lt' && ['gt', 'ge', 'shr'].includes(p.op2)) { + return false; + } + // Only combine logical operators with relational operators. + if (kBinaryOperators[p.op1].group === kLogical) { + return kBinaryOperators[p.op2].group === kRelational; + } + if (kBinaryOperators[p.op2].group === kLogical) { + return kBinaryOperators[p.op1].group === kRelational; + } + return true; + }) + ) + .fn(t => { + const op1 = kBinaryOperators[t.params.op1]; + const op2 = kBinaryOperators[t.params.op2]; + const code = ` +var<private> a : ${op1.group === kLogical ? 'bool' : 'u32'}; +var<private> b : u32; +var<private> c : ${op2.group === kLogical ? 'bool' : 'u32'}; +fn foo() { + let foo = a ${op1.op} b ${op2.op} c; +} +`; + + const valid = (kCanPrecedeWithoutParens[op1.group] & op2.group) !== 0; + t.expectCompileResult(valid, code); + }); + +g.test('mixed_logical_requires_parentheses') + .desc( + ` + Validates that mixed logical operators require parentheses to bind correctly. + ` + ) + .params(u => + u + .combine('op1', keysOf(kBinaryOperators)) + .combine('op2', keysOf(kBinaryOperators)) + .combine('parens', ['none', 'left', 'right']) + .filter(p => { + const group1 = kBinaryOperators[p.op1].group; + const group2 = kBinaryOperators[p.op2].group; + return group1 === kLogical && group2 === kLogical; + }) + ) + .fn(t => { + const op1 = kBinaryOperators[t.params.op1]; + const op2 = kBinaryOperators[t.params.op2]; + let expr = `a ${op1.op} b ${op2.op} c;`; + if (t.params.parens === 'left') { + expr = `(a ${op1.op} b) ${op2.op} c;`; + } else if (t.params.parens === 'right') { + expr = `a ${op1.op} (b ${op2.op} c);`; + } + const code = ` +var<private> a : bool; +var<private> b : bool; +var<private> c : bool; +fn foo() { + let bar = ${expr}; +} +`; + const valid = t.params.parens !== 'none' || t.params.op1 === t.params.op2; + t.expectCompileResult(valid, code); + }); + +// The list of miscellaneous other test cases. +interface Expression { + expr: string; + result: boolean; +} +const kExpressions: Record<string, Expression> = { + neg_member: { expr: '- str . a', result: true }, + comp_member: { expr: '~ str . a', result: true }, + addr_member: { expr: '& str . a', result: true }, + log_and_member: { expr: 'false && str . b', result: true }, + log_or_member: { expr: 'false || str . b', result: true }, + and_addr: { expr: ' v & &str .a', result: false }, + and_addr_paren: { expr: 'v & (&str).a', result: true }, + deref_member: { expr: ' * ptr_str . a', result: false }, + deref_member_paren: { expr: '(* ptr_str) . a', result: true }, + deref_idx: { expr: ' * ptr_vec [0]', result: false }, + deref_idx_paren: { expr: '(* ptr_vec) [1]', result: true }, +}; + +g.test('other') + .desc( + ` + Test that other operator precedence rules are correctly implemented. + ` + ) + .params(u => u.combine('expr', keysOf(kExpressions))) + .fn(t => { + const expr = kExpressions[t.params.expr]; + const wgsl = ` + struct S { + a: i32, + b: bool, + } + + fn main() { + var v = 42; + var vec = vec4(); + var str = S(42, false); + let ptr_vec = &vec; + let ptr_str = &str; + + let foo = ${expr.expr}; + } + `; + + t.expectCompileResult(expr.result, wgsl); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/address_of_and_indirection.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/address_of_and_indirection.spec.ts new file mode 100644 index 0000000000..7eca4286b9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/address_of_and_indirection.spec.ts @@ -0,0 +1,243 @@ +export const description = ` +Validation tests for unary address-of and indirection (dereference) +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kAddressSpaces = ['function', 'private', 'workgroup', 'uniform', 'storage']; +const kAccessModes = ['read', 'read_write']; +const kStorageTypes = ['bool', 'u32', 'i32', 'f32', 'f16']; +const kCompositeTypes = ['array', 'struct', 'vec', 'mat']; +const kDerefTypes = { + deref_address_of_identifier: { + wgsl: `(*(&a))`, + requires_pointer_composite_access: false, + }, + deref_pointer: { + wgsl: `(*p)`, + requires_pointer_composite_access: false, + }, + address_of_identifier: { + wgsl: `(&a)`, + requires_pointer_composite_access: true, + }, + pointer: { + wgsl: `p`, + requires_pointer_composite_access: true, + }, +}; + +g.test('basic') + .desc( + `Validates address-of (&) every supported variable type, ensuring the type is correct by + assigning to an explicitly typed pointer. Also validates dereferencing the reference, + ensuring the type is correct by assigning to an explicitly typed variable.` + ) + .params(u => + u + .combine('addressSpace', kAddressSpaces) + .combine('accessMode', kAccessModes) + .combine('storageType', kStorageTypes) + .combine('derefType', keysOf(kDerefTypes)) + .filter(t => { + if (t.storageType === 'bool') { + return t.addressSpace === 'function' || t.addressSpace === 'private'; + } + return true; + }) + .filter(t => { + // This test does not test composite access + return !kDerefTypes[t.derefType].requires_pointer_composite_access; + }) + ) + .beforeAllSubcases(t => { + if (t.params.storageType === 'f16') { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + } + }) + .fn(t => { + const isLocal = t.params.addressSpace === 'function'; + const deref = kDerefTypes[t.params.derefType]; + // Only specify access mode for storage buffers + const commaAccessMode = t.params.addressSpace === 'storage' ? `, ${t.params.accessMode}` : ''; + + let varDecl = ''; + if (t.params.addressSpace === 'uniform' || t.params.addressSpace === 'storage') { + varDecl += '@group(0) @binding(0) '; + } + varDecl += `var<${t.params.addressSpace}${commaAccessMode}> a : VarType;`; + + const wgsl = ` + ${t.params.storageType === 'f16' ? 'enable f16;' : ''} + + alias VarType = ${t.params.storageType}; + alias PtrType = ptr<${t.params.addressSpace}, VarType ${commaAccessMode}>; + + ${isLocal ? '' : varDecl} + + fn foo() { + ${isLocal ? varDecl : ''} + let p : PtrType = &a; + var deref : VarType = ${deref.wgsl}; + } + `; + + t.expectCompileResult(true, wgsl); + }); + +g.test('composite') + .desc( + `Validates address-of (&) every supported variable type for composite types, ensuring the type + is correct by assigning to an explicitly typed pointer. Also validates dereferencing the + reference followed by member/index access, ensuring the type is correct by assigning to an + explicitly typed variable.` + ) + .params(u => + u + .combine('addressSpace', kAddressSpaces) + .combine('compositeType', kCompositeTypes) + .combine('storageType', kStorageTypes) + .beginSubcases() + .combine('derefType', keysOf(kDerefTypes)) + .combine('accessMode', kAccessModes) + .filter(t => { + if (t.storageType === 'bool') { + return t.addressSpace === 'function' || t.addressSpace === 'private'; + } + return true; + }) + .filter(t => { + if (t.compositeType === 'mat') { + return t.storageType === 'f32' || t.storageType === 'f16'; + } + return true; + }) + ) + .beforeAllSubcases(t => { + if (t.params.storageType === 'f16') { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + } + }) + .fn(t => { + const isLocal = t.params.addressSpace === 'function'; + const deref = kDerefTypes[t.params.derefType]; + // Only specify access mode for storage buffers + const commaAccessMode = t.params.addressSpace === 'storage' ? `, ${t.params.accessMode}` : ''; + + let varDecl = ''; + if (t.params.addressSpace === 'uniform' || t.params.addressSpace === 'storage') { + varDecl += '@group(0) @binding(0) '; + } + varDecl += `var<${t.params.addressSpace}${commaAccessMode}> a : VarType;`; + + let wgsl = ` + ${t.params.storageType === 'f16' ? 'enable f16;' : ''}`; + + switch (t.params.compositeType) { + case 'array': + wgsl += ` + struct S { @align(16) member : ${t.params.storageType} } + alias VarType = array<S, 10>; + alias PtrType = ptr<${t.params.addressSpace}, VarType ${commaAccessMode}>; + ${isLocal ? '' : varDecl} + + fn foo() { + ${isLocal ? varDecl : ''} + let p : PtrType = &a; + var deref : ${t.params.storageType} = ${deref.wgsl}[0].member; + }`; + break; + case 'struct': + wgsl += ` + struct S { member : ${t.params.storageType} } + alias VarType = S; + alias PtrType = ptr<${t.params.addressSpace}, VarType ${commaAccessMode}>; + ${isLocal ? '' : varDecl} + + fn foo() { + ${isLocal ? varDecl : ''} + let p : PtrType = &a; + var deref : ${t.params.storageType} = ${deref.wgsl}.member; + }`; + break; + case 'vec': + wgsl += ` + alias VarType = vec3<${t.params.storageType}>; + alias PtrType = ptr<${t.params.addressSpace}, VarType ${commaAccessMode}>; + ${isLocal ? '' : varDecl} + + fn foo() { + ${isLocal ? varDecl : ''} + let p : PtrType = &a; + var deref_member : ${t.params.storageType} = ${deref.wgsl}.x; + var deref_index : ${t.params.storageType} = ${deref.wgsl}[0]; + }`; + break; + case 'mat': + wgsl += ` + alias VarType = mat2x3<${t.params.storageType}>; + alias PtrType = ptr<${t.params.addressSpace}, VarType ${commaAccessMode}>; + ${isLocal ? '' : varDecl} + + fn foo() { + ${isLocal ? varDecl : ''} + let p : PtrType = &a; + var deref_vec : vec3<${t.params.storageType}> = ${deref.wgsl}[0]; + var deref_elem : ${t.params.storageType} = ${deref.wgsl}[0][0]; + }`; + break; + } + + let shouldPass = true; + if ( + kDerefTypes[t.params.derefType].requires_pointer_composite_access && + !t.hasLanguageFeature('pointer_composite_access') + ) { + shouldPass = false; + } + + t.expectCompileResult(shouldPass, wgsl); + }); + +const kInvalidCases = { + address_of_let: ` + let a = 1; + let p = &a;`, + address_of_texture: ` + let p = &t;`, + address_of_sampler: ` + let p = &s;`, + address_of_function: ` + let p = &func;`, + address_of_vector_elem_via_member: ` + var a : vec3<f32>(); + let p = &a.x;`, + address_of_vector_elem_via_index: ` + var a : vec3<f32>(); + let p = &a[0];`, + address_of_matrix_elem: ` + var a : mat2x3<f32>(); + let p = &a[0][0];`, + deref_non_pointer: ` + var a = 1; + let p = *a; + `, +}; +g.test('invalid') + .desc('Test invalid cases of unary address-of and dereference') + .params(u => u.combine('case', keysOf(kInvalidCases))) + .fn(t => { + const wgsl = ` + @group(0) @binding(0) var s : sampler; + @group(0) @binding(1) var t : texture_2d<f32>; + fn func() {} + fn main() { + ${kInvalidCases[t.params.case]} + } + `; + t.expectCompileResult(false, wgsl); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/arithmetic_negation.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/arithmetic_negation.spec.ts new file mode 100644 index 0000000000..47a1a04990 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/arithmetic_negation.spec.ts @@ -0,0 +1,114 @@ +export const description = ` +Validation tests for arithmetic negation expressions. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { kAllScalarsAndVectors, scalarTypeOf, Type } from '../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector numeric negation expressions are accepted for numerical types that are signed. + ` + ) + .params(u => u.combine('type', keysOf(kScalarAndVectorTypes)).beginSubcases()) + .beforeAllSubcases(t => { + if (scalarTypeOf(kScalarAndVectorTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kScalarAndVectorTypes[t.params.type]; + const elementTy = scalarTypeOf(type); + const hasF16 = elementTy === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const rhs = ${type.create(0).wgsl()}; +const foo = -rhs; +`; + + t.expectCompileResult(elementTy.signed, code); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid negation operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + mat2x2f: { + expr: 'm', + control: e => `${e}[0][0]`, + }, + + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&b)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `atomicLoad(&${e})`, + }, + + texture: { + expr: 't', + control: e => `textureLoad(${e}, vec2(), 0).x`, + }, + + sampler: { + expr: 's', + control: e => `textureSampleLevel(t, ${e}, vec2(), 0).x`, + }, + + struct: { + expr: 'str', + control: e => `${e}.b`, + }, +}; + +g.test('invalid_types') + .desc( + ` + Validates that arithmetic negation expressions are never accepted for non-scalar and non-vector types. + ` + ) + .params(u => + u.combine('type', keysOf(kInvalidTypes)).combine('control', [true, false]).beginSubcases() + ) + .fn(t => { + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<i32>; + +struct S { b : i32 } + +var<private> b : i32; +var<private> m : mat2x2f; +var<private> arr : array<i32, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = -${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/bitwise_complement.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/bitwise_complement.spec.ts new file mode 100644 index 0000000000..b5b8556b9d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/bitwise_complement.spec.ts @@ -0,0 +1,114 @@ +export const description = ` +Validation tests for bitwise complement expressions. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { kAllScalarsAndVectors, scalarTypeOf, Type } from '../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector bitwise complement expressions are only accepted for integers. + ` + ) + .params(u => u.combine('type', keysOf(kScalarAndVectorTypes)).beginSubcases()) + .beforeAllSubcases(t => { + if (scalarTypeOf(kScalarAndVectorTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kScalarAndVectorTypes[t.params.type]; + const elementTy = scalarTypeOf(type); + const hasF16 = elementTy === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const rhs = ${type.create(0).wgsl()}; +const foo = ~rhs; +`; + + t.expectCompileResult([Type.abstractInt, Type.i32, Type.u32].includes(elementTy), code); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid complement operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + mat2x2f: { + expr: 'm', + control: e => `i32(${e}[0][0])`, + }, + + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&u)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `atomicLoad(&${e})`, + }, + + texture: { + expr: 't', + control: e => `i32(textureLoad(${e}, vec2(), 0).x)`, + }, + + sampler: { + expr: 's', + control: e => `i32(textureSampleLevel(t, ${e}, vec2(), 0).x)`, + }, + + struct: { + expr: 'str', + control: e => `${e}.u`, + }, +}; + +g.test('invalid_types') + .desc( + ` + Validates that bitwise complement expressions are never accepted for non-scalar and non-vector types. + ` + ) + .params(u => + u.combine('type', keysOf(kInvalidTypes)).combine('control', [true, false]).beginSubcases() + ) + .fn(t => { + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<i32>; + +struct S { u : u32 } + +var<private> u : u32; +var<private> m : mat2x2f; +var<private> arr : array<u32, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = ~${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/logical_negation.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/logical_negation.spec.ts new file mode 100644 index 0000000000..a85516ec3f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/expression/unary/logical_negation.spec.ts @@ -0,0 +1,114 @@ +export const description = ` +Validation tests for logical negation expressions. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../common/util/data_tables.js'; +import { kAllScalarsAndVectors, scalarTypeOf, Type } from '../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +// A list of scalar and vector types. +const kScalarAndVectorTypes = objectsToRecord(kAllScalarsAndVectors); + +g.test('scalar_vector') + .desc( + ` + Validates that scalar and vector logical negation expressions are only accepted for bool types. + ` + ) + .params(u => u.combine('type', keysOf(kScalarAndVectorTypes)).beginSubcases()) + .beforeAllSubcases(t => { + if (scalarTypeOf(kScalarAndVectorTypes[t.params.type]) === Type.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const type = kScalarAndVectorTypes[t.params.type]; + const elementTy = scalarTypeOf(type); + const hasF16 = elementTy === Type.f16; + const code = ` +${hasF16 ? 'enable f16;' : ''} +const rhs = ${type.create(0).wgsl()}; +const foo = !rhs; +`; + + t.expectCompileResult(elementTy === Type.bool, code); + }); + +interface InvalidTypeConfig { + // An expression that produces a value of the target type. + expr: string; + // A function that converts an expression of the target type into a valid negation operand. + control: (x: string) => string; +} +const kInvalidTypes: Record<string, InvalidTypeConfig> = { + mat2x2f: { + expr: 'm', + control: e => `bool(${e}[0][0])`, + }, + + array: { + expr: 'arr', + control: e => `${e}[0]`, + }, + + ptr: { + expr: '(&b)', + control: e => `*${e}`, + }, + + atomic: { + expr: 'a', + control: e => `bool(atomicLoad(&${e}))`, + }, + + texture: { + expr: 't', + control: e => `bool(textureLoad(${e}, vec2(), 0).x)`, + }, + + sampler: { + expr: 's', + control: e => `bool(textureSampleLevel(t, ${e}, vec2(), 0).x)`, + }, + + struct: { + expr: 'str', + control: e => `${e}.b`, + }, +}; + +g.test('invalid_types') + .desc( + ` + Validates that logical negation expressions are never accepted for non-scalar and non-vector types. + ` + ) + .params(u => + u.combine('type', keysOf(kInvalidTypes)).combine('control', [true, false]).beginSubcases() + ) + .fn(t => { + const type = kInvalidTypes[t.params.type]; + const expr = t.params.control ? type.control(type.expr) : type.expr; + const code = ` +@group(0) @binding(0) var t : texture_2d<f32>; +@group(0) @binding(1) var s : sampler; +@group(0) @binding(2) var<storage, read_write> a : atomic<i32>; + +struct S { b : bool } + +var<private> b : bool; +var<private> m : mat2x2f; +var<private> arr : array<bool, 4>; +var<private> str : S; + +@compute @workgroup_size(1) +fn main() { + let foo = !${expr}; +} +`; + + t.expectCompileResult(t.params.control, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/extension/pointer_composite_access.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/extension/pointer_composite_access.spec.ts new file mode 100644 index 0000000000..6940dc6469 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/extension/pointer_composite_access.spec.ts @@ -0,0 +1,130 @@ +export const description = ` +Validation tests for pointer_composite_access extension +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +function makeSource(module: string, init_expr: string, pointer_read_expr: string) { + return ` + ${module} + fn f() { + var a = ${init_expr}; + let p = &a; + let r = ${pointer_read_expr}; + }`; +} + +const kCases = { + // Via identifier 'a' + array_index_access_via_identifier: { + module: '', + init_expr: 'array<i32, 3>()', + via_deref: '(*(&a))[0]', + via_pointer: '(&a)[0]', + }, + vector_index_access_via_identifier: { + module: '', + init_expr: 'vec3<i32>()', + via_deref: '(*(&a))[0]', + via_pointer: '(&a)[0]', + }, + vector_member_access_via_identifier: { + module: '', + init_expr: 'vec3<i32>()', + via_deref: '(*(&a)).x', + via_pointer: '(&a).x', + }, + matrix_index_access_via_identifier: { + module: '', + init_expr: 'mat2x3<f32>()', + via_deref: '(*(&a))[0]', + via_pointer: '(&a)[0]', + }, + struct_member_access_via_identifier: { + module: 'struct S { a : i32, }', + init_expr: 'S()', + via_deref: '(*(&a)).a', + via_pointer: '(&a).a', + }, + builtin_struct_modf_via_identifier: { + module: '', + init_expr: 'modf(1.5)', + via_deref: 'vec2((*(&a)).fract, (*(&a)).whole)', + via_pointer: 'vec2((&a).fract, (&a).whole)', + }, + builtin_struct_frexp_via_identifier: { + module: '', + init_expr: 'frexp(1.5)', + via_deref: 'vec2((*(&a)).fract, f32((*(&a)).exp))', + via_pointer: 'vec2((&a).fract, f32((&a).exp))', + }, + + // Via pointer 'p' + array_index_access_via_pointer: { + module: '', + init_expr: 'array<i32, 3>()', + via_deref: '(*p)[0]', + via_pointer: 'p[0]', + }, + vector_index_access_via_pointer: { + module: '', + init_expr: 'vec3<i32>()', + via_deref: '(*p)[0]', + via_pointer: 'p[0]', + }, + vector_member_access_via_pointer: { + module: '', + init_expr: 'vec3<i32>()', + via_deref: '(*p).x', + via_pointer: 'p.x', + }, + matrix_index_access_via_pointer: { + module: '', + init_expr: 'mat2x3<f32>()', + via_deref: '(*p)[0]', + via_pointer: 'p[0]', + }, + struct_member_access_via_pointer: { + module: 'struct S { a : i32, }', + init_expr: 'S()', + via_deref: '(*p).a', + via_pointer: 'p.a', + }, + builtin_struct_modf_via_pointer: { + module: '', + init_expr: 'modf(1.5)', + via_deref: 'vec2((*p).fract, (*p).whole)', + via_pointer: 'vec2(p.fract, p.whole)', + }, + builtin_struct_frexp_via_pointer: { + module: '', + init_expr: 'frexp(1.5)', + via_deref: 'vec2((*p).fract, f32((*p).exp))', + via_pointer: 'vec2(p.fract, f32(p.exp))', + }, +}; + +g.test('deref') + .desc('Baseline test: pointer deref is always valid') + .params(u => u.combine('case', keysOf(kCases))) + .fn(t => { + const curr = kCases[t.params.case]; + const source = makeSource(curr.module, curr.init_expr, curr.via_deref); + t.expectCompileResult(true, source); + }); + +g.test('pointer') + .desc( + 'Tests that direct pointer access is valid if pointer_composite_access is supported, else it should fail' + ) + .params(u => u.combine('case', keysOf(kCases))) + .fn(t => { + const curr = kCases[t.params.case]; + const source = makeSource(curr.module, curr.init_expr, curr.via_pointer); + const should_pass = t.hasLanguageFeature('pointer_composite_access'); + t.expectCompileResult(should_pass, source); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/extension/readonly_and_readwrite_storage_textures.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/extension/readonly_and_readwrite_storage_textures.spec.ts new file mode 100644 index 0000000000..c74694d4b5 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/extension/readonly_and_readwrite_storage_textures.spec.ts @@ -0,0 +1,48 @@ +export const description = ` +Validation tests for the readonly_and_readwrite_storage_textures language feature +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { TexelFormats } from '../../types.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kFeatureName = 'readonly_and_readwrite_storage_textures'; + +g.test('var_decl') + .desc( + `Checks that the read and read_write access modes are only allowed with the language feature present` + ) + .paramsSubcasesOnly(u => + u + .combine('type', [ + 'texture_storage_1d', + 'texture_storage_2d', + 'texture_storage_2d_array', + 'texture_storage_3d', + ]) + .combine('format', TexelFormats) + .combine('access', ['read', 'write', 'read_write']) + ) + .fn(t => { + const { type, format, access } = t.params; + const source = `@group(0) @binding(0) var t : ${type}<${format.format}, ${access}>;`; + const requiresFeature = access !== 'write'; + t.expectCompileResult(t.hasLanguageFeature(kFeatureName) || !requiresFeature, source); + }); + +g.test('textureBarrier') + .desc( + `Checks that the textureBarrier() builtin is only allowed with the language feature present` + ) + .fn(t => { + t.expectCompileResult( + t.hasLanguageFeature(kFeatureName), + ` + @workgroup_size(1) @compute fn main() { + textureBarrier(); + } + ` + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/alias_analysis.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/alias_analysis.spec.ts index ba39485449..7efad7e798 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/alias_analysis.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/alias_analysis.spec.ts @@ -38,50 +38,284 @@ function shouldPass(aliased: boolean, ...uses: UseName[]): boolean { return !aliased || !uses.some(u => kUses[u].is_write) || uses.includes('no_access'); } +type AddressSpace = 'private' | 'function' | 'storage' | 'uniform' | 'workgroup'; + +const kWritableAddressSpaces = ['private', 'function', 'storage', 'workgroup'] as const; + +function ptr(addressSpace: AddressSpace, type: string) { + switch (addressSpace) { + case 'function': + return `ptr<function, ${type}>`; + case 'private': + return `ptr<private, ${type}>`; + case 'storage': + return `ptr<storage, ${type}, read_write>`; + case 'uniform': + return `ptr<uniform, ${type}>`; + case 'workgroup': + return `ptr<workgroup, ${type}>`; + } +} + +function declareModuleScopeVar( + name: string, + addressSpace: 'private' | 'storage' | 'uniform' | 'workgroup', + type: string +) { + const binding = name === 'x' ? 0 : 1; + switch (addressSpace) { + case 'private': + return `var<private> ${name} : ${type};`; + case 'storage': + return `@binding(${binding}) @group(0) var<storage, read_write> ${name} : ${type};`; + case 'uniform': + return `@binding(${binding}) @group(0) var<uniform> ${name} : ${type};`; + case 'workgroup': + return `var<workgroup> ${name} : ${type};`; + } +} + +function maybeDeclareModuleScopeVar(name: string, addressSpace: AddressSpace, type: string) { + if (addressSpace === 'function') { + return ''; + } + return declareModuleScopeVar(name, addressSpace, type); +} + +function maybeDeclareFunctionScopeVar(name: string, addressSpace: AddressSpace, type: string) { + switch (addressSpace) { + case 'function': + return `var ${name} : ${type};`; + default: + return ``; + } +} + +/** + * @returns true if a pointer of the given address space requires the + * 'unrestricted_pointer_parameters' language feature. + */ +function requiresUnrestrictedPointerParameters(addressSpace: AddressSpace) { + return addressSpace !== 'function' && addressSpace !== 'private'; +} + g.test('two_pointers') .desc(`Test aliasing of two pointers passed to a function.`) .params(u => u - .combine('address_space', ['private', 'function'] as const) + .combine('address_space', kWritableAddressSpaces) + .combine('aliased', [true, false]) + .beginSubcases() .combine('a_use', keysOf(kUses)) .combine('b_use', keysOf(kUses)) + ) + .fn(t => { + if (requiresUnrestrictedPointerParameters(t.params.address_space)) { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + + const code = ` +${maybeDeclareModuleScopeVar('x', t.params.address_space, 'i32')} +${maybeDeclareModuleScopeVar('y', t.params.address_space, 'i32')} + +fn callee(pa : ${ptr(t.params.address_space, 'i32')}, + pb : ${ptr(t.params.address_space, 'i32')}) -> i32 { + ${kUses[t.params.a_use].gen(`*pa`)} + ${kUses[t.params.b_use].gen(`*pb`)} + return 0; +} + +fn caller() { + ${maybeDeclareFunctionScopeVar('x', t.params.address_space, 'i32')} + ${maybeDeclareFunctionScopeVar('y', t.params.address_space, 'i32')} + callee(&x, ${t.params.aliased ? `&x` : `&y`}); +} +`; + t.expectCompileResult(shouldPass(t.params.aliased, t.params.a_use, t.params.b_use), code); + }); + +g.test('two_pointers_to_array_elements') + .desc(`Test aliasing of two array element pointers passed to a function.`) + .params(u => + u + .combine('address_space', kWritableAddressSpaces) + .combine('index', [0, 1]) .combine('aliased', [true, false]) .beginSubcases() + .combine('a_use', keysOf(kUses)) + .combine('b_use', keysOf(kUses)) ) .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + const code = ` -${t.params.address_space === 'private' ? `var<private> x : i32; var<private> y : i32;` : ``} +${maybeDeclareModuleScopeVar('x', t.params.address_space, 'array<i32, 4>')} +${maybeDeclareModuleScopeVar('y', t.params.address_space, 'array<i32, 4>')} -fn callee(pa : ptr<${t.params.address_space}, i32>, - pb : ptr<${t.params.address_space}, i32>) -> i32 { +fn callee(pa : ${ptr(t.params.address_space, 'i32')}, + pb : ${ptr(t.params.address_space, 'i32')}) -> i32 { ${kUses[t.params.a_use].gen(`*pa`)} ${kUses[t.params.b_use].gen(`*pb`)} return 0; } fn caller() { - ${t.params.address_space === 'function' ? `var x : i32; var y : i32;` : ``} - callee(&x, ${t.params.aliased ? `&x` : `&y`}); + ${maybeDeclareFunctionScopeVar('x', t.params.address_space, 'array<i32, 4>')} + ${maybeDeclareFunctionScopeVar('y', t.params.address_space, 'array<i32, 4>')} + callee(&x[${t.params.index}], ${t.params.aliased ? `&x[0]` : `&y[0]`}); } `; t.expectCompileResult(shouldPass(t.params.aliased, t.params.a_use, t.params.b_use), code); }); -g.test('one_pointer_one_module_scope') - .desc(`Test aliasing of a pointer with a direct access to a module-scope variable.`) +g.test('two_pointers_to_array_elements_indirect') + .desc( + `Test aliasing of two array pointers passed to a function, which indexes those arrays and then +passes the element pointers to another function.` + ) .params(u => u + .combine('address_space', kWritableAddressSpaces) + .combine('index', [0, 1]) + .combine('aliased', [true, false]) + .beginSubcases() .combine('a_use', keysOf(kUses)) .combine('b_use', keysOf(kUses)) + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const code = ` +${maybeDeclareModuleScopeVar('x', t.params.address_space, 'array<i32, 4>')} +${maybeDeclareModuleScopeVar('y', t.params.address_space, 'array<i32, 4>')} + +fn callee(pa : ${ptr(t.params.address_space, 'i32')}, + pb : ${ptr(t.params.address_space, 'i32')}) -> i32 { + ${kUses[t.params.a_use].gen(`*pa`)} + ${kUses[t.params.b_use].gen(`*pb`)} + return 0; +} + +fn index(pa : ${ptr(t.params.address_space, 'array<i32, 4>')}, + pb : ${ptr(t.params.address_space, 'array<i32, 4>')}) -> i32 { + return callee(&(*pa)[${t.params.index}], &(*pb)[0]); +} + +fn caller() { + ${maybeDeclareFunctionScopeVar('x', t.params.address_space, 'array<i32, 4>')} + ${maybeDeclareFunctionScopeVar('y', t.params.address_space, 'array<i32, 4>')} + index(&x, ${t.params.aliased ? `&x` : `&y`}); +} +`; + t.expectCompileResult(shouldPass(t.params.aliased, t.params.a_use, t.params.b_use), code); + }); + +g.test('two_pointers_to_struct_members') + .desc(`Test aliasing of two struct member pointers passed to a function.`) + .params(u => + u + .combine('address_space', kWritableAddressSpaces) + .combine('member', ['a', 'b']) + .combine('aliased', [true, false]) + .beginSubcases() + .combine('a_use', keysOf(kUses)) + .combine('b_use', keysOf(kUses)) + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const code = ` +struct S { + a : i32, + b : i32, +} + +${maybeDeclareModuleScopeVar('x', t.params.address_space, 'S')} +${maybeDeclareModuleScopeVar('y', t.params.address_space, 'S')} + +fn callee(pa : ${ptr(t.params.address_space, 'i32')}, + pb : ${ptr(t.params.address_space, 'i32')}) -> i32 { + ${kUses[t.params.a_use].gen(`*pa`)} + ${kUses[t.params.b_use].gen(`*pb`)} + return 0; +} + +fn caller() { + ${maybeDeclareFunctionScopeVar('x', t.params.address_space, 'S')} + ${maybeDeclareFunctionScopeVar('y', t.params.address_space, 'S')} + callee(&x.${t.params.member}, ${t.params.aliased ? `&x.a` : `&y.a`}); +} +`; + t.expectCompileResult(shouldPass(t.params.aliased, t.params.a_use, t.params.b_use), code); + }); + +g.test('two_pointers_to_struct_members_indirect') + .desc( + `Test aliasing of two structure pointers passed to a function, which accesses members of those +structures and then passes the member pointers to another function.` + ) + .params(u => + u + .combine('address_space', kWritableAddressSpaces) + .combine('member', ['a', 'b']) + .combine('aliased', [true, false]) + .beginSubcases() + .combine('a_use', keysOf(kUses)) + .combine('b_use', keysOf(kUses)) + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const code = ` +struct S { + a : i32, + b : i32, +} + +${maybeDeclareModuleScopeVar('x', t.params.address_space, 'S')} +${maybeDeclareModuleScopeVar('y', t.params.address_space, 'S')} + +fn callee(pa : ${ptr(t.params.address_space, 'i32')}, + pb : ${ptr(t.params.address_space, 'i32')}) -> i32 { + ${kUses[t.params.a_use].gen(`*pa`)} + ${kUses[t.params.b_use].gen(`*pb`)} + return 0; +} + +fn access(pa : ${ptr(t.params.address_space, 'S')}, + pb : ${ptr(t.params.address_space, 'S')}) -> i32 { + return callee(&(*pa).${t.params.member}, &(*pb).a); +} + +fn caller() { + ${maybeDeclareFunctionScopeVar('x', t.params.address_space, 'S')} + ${maybeDeclareFunctionScopeVar('y', t.params.address_space, 'S')} + access(&x, ${t.params.aliased ? `&x` : `&y`}); +} +`; + t.expectCompileResult(shouldPass(t.params.aliased, t.params.a_use, t.params.b_use), code); + }); + +g.test('one_pointer_one_module_scope') + .desc(`Test aliasing of a pointer with a direct access to a module-scope variable.`) + .params(u => + u + .combine('address_space', ['private', 'storage', 'workgroup'] as const) .combine('aliased', [true, false]) .beginSubcases() + .combine('a_use', keysOf(kUses)) + .combine('b_use', keysOf(kUses)) ) .fn(t => { + if (requiresUnrestrictedPointerParameters(t.params.address_space)) { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + const code = ` -var<private> x : i32; -var<private> y : i32; +${declareModuleScopeVar('x', t.params.address_space, 'i32')} +${declareModuleScopeVar('y', t.params.address_space, 'i32')} -fn callee(pb : ptr<private, i32>) -> i32 { +fn callee(pb : ${ptr(t.params.address_space, 'i32')}) -> i32 { ${kUses[t.params.a_use].gen(`x`)} ${kUses[t.params.b_use].gen(`*pb`)} return 0; @@ -98,29 +332,34 @@ g.test('subcalls') .desc(`Test aliasing of two pointers passed to a function, and then passed to other functions.`) .params(u => u - .combine('a_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) - .combine('b_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) + .combine('address_space', ['private', 'storage', 'workgroup'] as const) .combine('aliased', [true, false]) .beginSubcases() + .combine('a_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) + .combine('b_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) ) .fn(t => { + if (requiresUnrestrictedPointerParameters(t.params.address_space)) { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + const ptr_i32 = ptr(t.params.address_space, 'i32'); const code = ` -var<private> x : i32; -var<private> y : i32; +${declareModuleScopeVar('x', t.params.address_space, 'i32')} +${declareModuleScopeVar('y', t.params.address_space, 'i32')} -fn subcall_no_access(p : ptr<private, i32>) { +fn subcall_no_access(p : ${ptr_i32}) { let pp = &*p; } -fn subcall_binary_lhs(p : ptr<private, i32>) -> i32 { +fn subcall_binary_lhs(p : ${ptr_i32}) -> i32 { return *p + 1; } -fn subcall_assign(p : ptr<private, i32>) { +fn subcall_assign(p : ${ptr_i32}) { *p = 42; } -fn callee(pa : ptr<private, i32>, pb : ptr<private, i32>) -> i32 { +fn callee(pa : ${ptr_i32}, pb : ${ptr_i32}) -> i32 { let new_pa = &*pa; let new_pb = &*pb; subcall_${t.params.a_use}(new_pa); @@ -139,20 +378,25 @@ g.test('member_accessors') .desc(`Test aliasing of two pointers passed to a function and used with member accessors.`) .params(u => u - .combine('a_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) - .combine('b_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) + .combine('address_space', ['private', 'storage', 'workgroup'] as const) .combine('aliased', [true, false]) .beginSubcases() + .combine('a_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) + .combine('b_use', ['no_access', 'assign', 'binary_lhs'] as UseName[]) ) .fn(t => { + if (requiresUnrestrictedPointerParameters(t.params.address_space)) { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + + const ptr_S = ptr(t.params.address_space, 'S'); const code = ` struct S { a : i32 } -var<private> x : S; -var<private> y : S; +${declareModuleScopeVar('x', t.params.address_space, 'S')} +${declareModuleScopeVar('y', t.params.address_space, 'S')} -fn callee(pa : ptr<private, S>, - pb : ptr<private, S>) -> i32 { +fn callee(pa : ${ptr_S}, pb : ${ptr_S}) -> i32 { ${kUses[t.params.a_use].gen(`(*pa).a`)} ${kUses[t.params.b_use].gen(`(*pb).a`)} return 0; @@ -167,12 +411,18 @@ fn caller() { g.test('same_pointer_read_and_write') .desc(`Test that we can read from and write to the same pointer.`) - .params(u => u.beginSubcases()) + .params(u => + u.combine('address_space', ['private', 'storage', 'workgroup'] as const).beginSubcases() + ) .fn(t => { + if (requiresUnrestrictedPointerParameters(t.params.address_space)) { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + } + const code = ` -var<private> v : i32; +${declareModuleScopeVar('v', t.params.address_space, 'i32')} -fn callee(p : ptr<private, i32>) { +fn callee(p : ${ptr(t.params.address_space, 'i32')}) { *p = *p + 1; } @@ -185,10 +435,12 @@ fn caller() { g.test('aliasing_inside_function') .desc(`Test that we can alias pointers inside a function.`) - .params(u => u.beginSubcases()) + .params(u => + u.combine('address_space', ['private', 'storage', 'workgroup'] as const).beginSubcases() + ) .fn(t => { const code = ` -var<private> v : i32; +${declareModuleScopeVar('v', t.params.address_space, 'i32')} fn foo() { var v : i32; @@ -200,3 +452,236 @@ fn foo() { `; t.expectCompileResult(true, code); }); + +const kAtomicBuiltins = [ + 'atomicLoad', + 'atomicStore', + 'atomicAdd', + 'atomicSub', + 'atomicMax', + 'atomicMin', + 'atomicAnd', + 'atomicOr', + 'atomicXor', + 'atomicExchange', + 'atomicCompareExchangeWeak', +] as const; + +type AtomicBuiltins = (typeof kAtomicBuiltins)[number]; + +function isWrite(builtin: AtomicBuiltins) { + switch (builtin) { + case 'atomicLoad': + return false; + case 'atomicAdd': + case 'atomicSub': + case 'atomicMax': + case 'atomicMin': + case 'atomicAnd': + case 'atomicOr': + case 'atomicXor': + case 'atomicExchange': + case 'atomicCompareExchangeWeak': + case 'atomicStore': + return true; + } +} + +function callAtomicBuiltin(builtin: AtomicBuiltins, ptr: string) { + switch (builtin) { + case 'atomicLoad': + return `i += ${builtin}(${ptr})`; + case 'atomicStore': + return `${builtin}(${ptr}, 42)`; + case 'atomicAdd': + case 'atomicSub': + case 'atomicMax': + case 'atomicMin': + case 'atomicAnd': + case 'atomicOr': + case 'atomicXor': + case 'atomicExchange': + return `i += ${builtin}(${ptr}, 42)`; + case 'atomicCompareExchangeWeak': + return `${builtin}(${ptr}, 10, 42)`; + } +} + +g.test('two_atomic_pointers') + .desc(`Test aliasing of two atomic pointers passed to a function.`) + .params(u => + u + .combine('builtin_a', kAtomicBuiltins) + .combine('builtin_b', ['atomicLoad', 'atomicStore'] as const) + .combine('address_space', ['storage', 'workgroup'] as const) + .combine('aliased', [true, false]) + .beginSubcases() + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const ptr_atomic_i32 = ptr(t.params.address_space, 'atomic<i32>'); + const code = ` +${declareModuleScopeVar('x', t.params.address_space, 'atomic<i32>')} +${declareModuleScopeVar('y', t.params.address_space, 'atomic<i32>')} + +fn callee(pa : ${ptr_atomic_i32}, pb : ${ptr_atomic_i32}) { + var i : i32; + ${callAtomicBuiltin(t.params.builtin_a, 'pa')}; + ${callAtomicBuiltin(t.params.builtin_b, 'pb')}; +} + +fn caller() { + callee(&x, &${t.params.aliased ? 'x' : 'y'}); +} +`; + const shouldFail = + t.params.aliased && (isWrite(t.params.builtin_a) || isWrite(t.params.builtin_b)); + t.expectCompileResult(!shouldFail, code); + }); + +g.test('two_atomic_pointers_to_array_elements') + .desc(`Test aliasing of two atomic array element pointers passed to a function.`) + .params(u => + u + .combine('builtin_a', kAtomicBuiltins) + .combine('builtin_b', ['atomicLoad', 'atomicStore'] as const) + .combine('address_space', ['storage', 'workgroup'] as const) + .combine('index', [0, 1]) + .combine('aliased', [true, false]) + .beginSubcases() + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const ptr_atomic_i32 = ptr(t.params.address_space, 'atomic<i32>'); + const code = ` +${declareModuleScopeVar('x', t.params.address_space, 'array<atomic<i32>, 32>')} +${declareModuleScopeVar('y', t.params.address_space, 'array<atomic<i32>, 32>')} + +fn callee(pa : ${ptr_atomic_i32}, pb : ${ptr_atomic_i32}) { + var i : i32; + ${callAtomicBuiltin(t.params.builtin_a, 'pa')}; + ${callAtomicBuiltin(t.params.builtin_b, 'pb')}; +} + +fn caller() { + callee(&x[${t.params.index}], &${t.params.aliased ? 'x' : 'y'}[0]); +} +`; + const shouldFail = + t.params.aliased && (isWrite(t.params.builtin_a) || isWrite(t.params.builtin_b)); + t.expectCompileResult(!shouldFail, code); + }); + +g.test('two_atomic_pointers_to_struct_members') + .desc(`Test aliasing of two struct member atomic pointers passed to a function.`) + .params(u => + u + .combine('builtin_a', kAtomicBuiltins) + .combine('builtin_b', ['atomicLoad', 'atomicStore'] as const) + .combine('address_space', ['storage', 'workgroup'] as const) + .combine('member', ['a', 'b']) + .combine('aliased', [true, false]) + .beginSubcases() + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const ptr_atomic_i32 = ptr(t.params.address_space, 'atomic<i32>'); + const code = ` +struct S { + a : atomic<i32>, + b : atomic<i32>, +} + +${declareModuleScopeVar('x', t.params.address_space, 'S')} +${declareModuleScopeVar('y', t.params.address_space, 'S')} + +fn callee(pa : ${ptr_atomic_i32}, pb : ${ptr_atomic_i32}) { + var i : i32; + ${callAtomicBuiltin(t.params.builtin_a, 'pa')}; + ${callAtomicBuiltin(t.params.builtin_b, 'pb')}; +} + +fn caller() { + callee(&x.${t.params.member}, &${t.params.aliased ? 'x' : 'y'}.a); +} +`; + const shouldFail = + t.params.aliased && (isWrite(t.params.builtin_a) || isWrite(t.params.builtin_b)); + t.expectCompileResult(!shouldFail, code); + }); + +g.test('one_atomic_pointer_one_module_scope') + .desc(`Test aliasing of an atomic pointer with a direct access to a module-scope variable.`) + .params(u => + u + .combine('builtin_a', kAtomicBuiltins) + .combine('builtin_b', ['atomicLoad', 'atomicStore'] as const) + .combine('address_space', ['storage', 'workgroup'] as const) + .combine('aliased', [true, false]) + .beginSubcases() + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + const ptr_atomic_i32 = ptr(t.params.address_space, 'atomic<i32>'); + const code = ` +${declareModuleScopeVar('x', t.params.address_space, 'atomic<i32>')} +${declareModuleScopeVar('y', t.params.address_space, 'atomic<i32>')} + +fn callee(p : ${ptr_atomic_i32}) { + var i : i32; + ${callAtomicBuiltin(t.params.builtin_a, 'p')}; + ${callAtomicBuiltin(t.params.builtin_b, t.params.aliased ? '&x' : '&y')}; +} + +fn caller() { + callee(&x); +} +`; + const shouldFail = + t.params.aliased && (isWrite(t.params.builtin_a) || isWrite(t.params.builtin_b)); + t.expectCompileResult(!shouldFail, code); + }); + +g.test('workgroup_uniform_load') + .desc(`Test aliasing via workgroupUniformLoad.`) + .params(u => + u + .combine('use', ['load', 'store', 'workgroupUniformLoad'] as const) + .combine('aliased', [true, false]) + .beginSubcases() + ) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters'); + + function emitUse() { + switch (t.params.use) { + case 'load': + return `v = *pa`; + case 'store': + return `*pa = 1`; + case 'workgroupUniformLoad': + return `v = workgroupUniformLoad(pa)`; + } + } + + const code = ` +var<workgroup> x : i32; +var<workgroup> y : i32; + +fn callee(pa : ptr<workgroup, i32>, pb : ptr<workgroup, i32>) -> i32 { + var v : i32; + ${emitUse()}; + return v + workgroupUniformLoad(pb); +} + +fn caller() { + callee(&x, &${t.params.aliased ? 'x' : 'y'}); +} +`; + const shouldFail = t.params.aliased && t.params.use === 'store'; + t.expectCompileResult(!shouldFail, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/restrictions.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/restrictions.spec.ts index b6affd14d6..7c79e6c5ea 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/restrictions.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/functions/restrictions.spec.ts @@ -12,6 +12,30 @@ interface VertexPosCase { valid: boolean; } +const kCCommonTypeDecls = ` +struct runtime_array_struct { + arr : array<u32> +} + +struct constructible { + a : i32, + b : u32, + c : f32, + d : bool, +} + +struct host_shareable { + a : i32, + b : u32, + c : f32, +} + +struct struct_with_array { + a : array<constructible, 4> +} + +`; + const kVertexPosCases: Record<string, VertexPosCase> = { bare_position: { name: `@builtin(position) vec4f`, value: `vec4f()`, valid: true }, nested_position: { name: `pos_struct`, value: `pos_struct()`, valid: true }, @@ -145,20 +169,7 @@ g.test('function_return_types') const code = ` ${enable} -struct runtime_array_struct { - arr : array<u32> -} - -struct constructible { - a : i32, - b : u32, - c : f32, - d : bool, -} - -struct struct_with_array { - a : array<constructible, 4> -} +${kCCommonTypeDecls} struct atomic_struct { a : atomic<u32> @@ -192,7 +203,7 @@ fn foo() -> ${testcase.name} { interface ParamTypeCase { name: string; - valid: boolean; + valid: boolean | 'with_unrestricted_pointer_parameters'; } const kFunctionParamTypeCases: Record<string, ParamTypeCase> = { @@ -246,21 +257,42 @@ const kFunctionParamTypeCases: Record<string, ParamTypeCase> = { ptr3: { name: `ptr<private, u32>`, valid: true }, ptr4: { name: `ptr<private, constructible>`, valid: true }, + // Pointers only valid with unrestricted_pointer_parameters + ptr5: { name: `ptr<storage, u32>`, valid: 'with_unrestricted_pointer_parameters' }, + ptr6: { name: `ptr<storage, u32, read>`, valid: 'with_unrestricted_pointer_parameters' }, + ptr7: { name: `ptr<storage, u32, read_write>`, valid: 'with_unrestricted_pointer_parameters' }, + ptr8: { name: `ptr<uniform, u32>`, valid: 'with_unrestricted_pointer_parameters' }, + ptr9: { name: `ptr<workgroup, u32>`, valid: 'with_unrestricted_pointer_parameters' }, + ptr10: { + name: `ptr<storage, host_shareable, read_write>`, + valid: 'with_unrestricted_pointer_parameters', + }, + ptr11: { + name: `ptr<storage, host_shareable, read>`, + valid: 'with_unrestricted_pointer_parameters', + }, + ptr12: { + name: `ptr<uniform, host_shareable>`, + valid: 'with_unrestricted_pointer_parameters', + }, + ptrWorkgroupAtomic: { + name: `ptr<workgroup, atomic<u32>>`, + valid: 'with_unrestricted_pointer_parameters', + }, + ptrWorkgroupNestedAtomic: { + name: `ptr<workgroup, array<atomic<u32>,1>>`, + valid: 'with_unrestricted_pointer_parameters', + }, + // Invalid pointers. - ptr5: { name: `ptr<storage, u32>`, valid: false }, - ptr6: { name: `ptr<storage, u32, read>`, valid: false }, - ptr7: { name: `ptr<storage, u32, read_write>`, valid: false }, - ptr8: { name: `ptr<uniform, u32>`, valid: false }, - ptr9: { name: `ptr<workgroup, u32>`, valid: false }, - ptr10: { name: `ptr<handle, u32>`, valid: false }, // Can't spell handle address space - ptr12: { name: `ptr<not_an_address_space, u32>`, valid: false }, - ptr13: { name: `ptr<storage>`, valid: false }, // No store type - ptr14: { name: `ptr<private,clamp>`, valid: false }, // Invalid store type - ptr15: { name: `ptr<private,u32,read>`, valid: false }, // Can't specify access mode - ptr16: { name: `ptr<private,u32,write>`, valid: false }, // Can't specify access mode - ptr17: { name: `ptr<private,u32,read_write>`, valid: false }, // Can't specify access mode - ptrWorkgroupAtomic: { name: `ptr<workgroup, atomic<u32>>`, valid: false }, - ptrWorkgroupNestedAtomic: { name: `ptr<workgroup, array<atomic<u32>,1>>`, valid: false }, + invalid_ptr1: { name: `ptr<handle, u32>`, valid: false }, // Can't spell handle address space + invalid_ptr2: { name: `ptr<not_an_address_space, u32>`, valid: false }, + invalid_ptr3: { name: `ptr<storage>`, valid: false }, // No store type + invalid_ptr4: { name: `ptr<private,u32,read>`, valid: false }, // Can't specify access mode + invalid_ptr5: { name: `ptr<private,u32,write>`, valid: false }, // Can't specify access mode + invalid_ptr6: { name: `ptr<private,u32,read_write>`, valid: false }, // Can't specify access mode + invalid_ptr7: { name: `ptr<private,clamp>`, valid: false }, // Invalid store type + invalid_ptr8: { name: `ptr<function, texture_external>`, valid: false }, // non-constructable pointer type }; g.test('function_parameter_types') @@ -278,30 +310,23 @@ g.test('function_parameter_types') const code = ` ${enable} -struct runtime_array_struct { - arr : array<u32> -} - -struct constructible { - a : i32, - b : u32, - c : f32, - d : bool, -} - -struct struct_with_array { - a : array<constructible, 4> -} +${kCCommonTypeDecls} fn foo(param : ${testcase.name}) { }`; - t.expectCompileResult(testcase.valid, code); + let isValid = testcase.valid; + if (isValid === 'with_unrestricted_pointer_parameters') { + isValid = t.hasLanguageFeature('unrestricted_pointer_parameters'); + } + + t.expectCompileResult(isValid, code); }); interface ParamValueCase { value: string; matches: string[]; + needsUnrestrictedPointerParameters?: boolean; } const kFunctionParamValueCases: Record<string, ParamValueCase> = { @@ -426,15 +451,47 @@ const kFunctionParamValueCases: Record<string, ParamValueCase> = { ptr3: { value: `&g_u32`, matches: ['ptr3'] }, ptr4: { value: `&g_constructible`, matches: ['ptr4'] }, - // Invalid pointers - ptr5: { value: `&f_constructible.b`, matches: [] }, - ptr6: { value: `&g_constructible.b`, matches: [] }, - ptr7: { value: `&f_struct_with_array.a[1].b`, matches: [] }, - ptr8: { value: `&g_struct_with_array.a[2]`, matches: [] }, - ptr9: { value: `&ro_constructible.b`, matches: [] }, - ptr10: { value: `&rw_constructible`, matches: [] }, - ptr11: { value: `&uniform_constructible`, matches: [] }, - ptr12: { value: `&ro_constructible`, matches: [] }, + // Requires 'unrestricted_pointer_parameters' WGSL feature + ptr5: { + value: `&f_constructible.b`, + matches: ['ptr1'], + needsUnrestrictedPointerParameters: true, + }, + ptr6: { + value: `&g_constructible.b`, + matches: ['ptr3'], + needsUnrestrictedPointerParameters: true, + }, + ptr7: { + value: `&f_struct_with_array.a[1].b`, + matches: ['ptr1'], + needsUnrestrictedPointerParameters: true, + }, + ptr8: { + value: `&g_struct_with_array.a[2]`, + matches: ['ptr4'], + needsUnrestrictedPointerParameters: true, + }, + ptr9: { + value: `&ro_host_shareable.b`, + matches: ['ptr5', 'ptr6'], + needsUnrestrictedPointerParameters: true, + }, + ptr10: { + value: `&rw_host_shareable`, + matches: ['ptr10'], + needsUnrestrictedPointerParameters: true, + }, + ptr11: { + value: `&ro_host_shareable`, + matches: ['ptr11'], + needsUnrestrictedPointerParameters: true, + }, + ptr12: { + value: `&uniform_host_shareable`, + matches: ['ptr12'], + needsUnrestrictedPointerParameters: true, + }, }; function parameterMatches(decl: string, matches: string[]): boolean { @@ -454,10 +511,11 @@ g.test('function_parameter_matching') .params(u => u .combine('decl', keysOf(kFunctionParamTypeCases)) - .combine('arg', keysOf(kFunctionParamValueCases)) .filter(u => { - return kFunctionParamTypeCases[u.decl].valid; + return kFunctionParamTypeCases[u.decl].valid !== false; }) + .beginSubcases() + .combine('arg', keysOf(kFunctionParamValueCases)) ) .beforeAllSubcases(t => { if (kFunctionParamTypeCases[t.params.decl].name === 'f16') { @@ -471,26 +529,7 @@ g.test('function_parameter_matching') const code = ` ${enable} -struct runtime_array_struct { - arr : array<u32> -} - -struct constructible { - a : i32, - b : u32, - c : f32, - d : bool, -} - -struct host_shareable { - a : i32, - b : u32, - c : f32, -} - -struct struct_with_array { - a : array<constructible, 4> -} +${kCCommonTypeDecls} @group(0) @binding(0) var t : texture_2d<f32>; @group(0) @binding(1) @@ -507,11 +546,11 @@ var t_multisampled : texture_multisampled_2d<f32>; var t_external : texture_external; @group(1) @binding(0) -var<storage> ro_constructible : host_shareable; +var<storage> ro_host_shareable : host_shareable; @group(1) @binding(1) -var<storage, read_write> rw_constructible : host_shareable; +var<storage, read_write> rw_host_shareable : host_shareable; @group(1) @binding(2) -var<uniform> uniform_constructible : host_shareable; +var<uniform> uniform_host_shareable : host_shareable; fn bar(param : ${param.name}) { } @@ -568,7 +607,17 @@ fn foo() { } `; - t.expectCompileResult(parameterMatches(t.params.decl, arg.matches), code); + const needsUnrestrictedPointerParameters = + (kFunctionParamTypeCases[t.params.decl].valid === 'with_unrestricted_pointer_parameters' || + arg.needsUnrestrictedPointerParameters) ?? + false; + + let isValid = parameterMatches(t.params.decl, arg.matches); + if (isValid && needsUnrestrictedPointerParameters) { + isValid = t.hasLanguageFeature('unrestricted_pointer_parameters'); + } + + t.expectCompileResult(isValid, code); }); g.test('no_direct_recursion') @@ -691,67 +740,75 @@ function checkArgTypeMatch(param_type: string, arg_matches: string[]): boolean { return false; } -g.test('call_arg_types_match_params') +g.test('call_arg_types_match_1_param') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls') + .desc(`Test that the argument types match in order`) + .params(u => + u + .combine('p1_type', kParamsTypes) // + .beginSubcases() + .combine('arg1_value', keysOf(kArgValues)) + ) + .fn(t => { + const code = ` +fn bar(p1 : ${t.params.p1_type}) { } +fn foo() { + bar(${kArgValues[t.params.arg1_value].value}); +}`; + + const res = checkArgTypeMatch(t.params.p1_type, kArgValues[t.params.arg1_value].matches); + t.expectCompileResult(res, code); + }); + +g.test('call_arg_types_match_2_params') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls') + .desc(`Test that the argument types match in order`) + .params(u => + u + .combine('p1_type', kParamsTypes) + .combine('p2_type', kParamsTypes) + .beginSubcases() + .combine('arg1_value', keysOf(kArgValues)) + .combine('arg2_value', keysOf(kArgValues)) + ) + .fn(t => { + const code = ` +fn bar(p1 : ${t.params.p1_type}, p2 : ${t.params.p2_type}) { } +fn foo() { + bar(${kArgValues[t.params.arg1_value].value}, ${kArgValues[t.params.arg2_value].value}); +}`; + + const res = + checkArgTypeMatch(t.params.p1_type, kArgValues[t.params.arg1_value].matches) && + checkArgTypeMatch(t.params.p2_type, kArgValues[t.params.arg2_value].matches); + t.expectCompileResult(res, code); + }); + +g.test('call_arg_types_match_3_params') .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls') .desc(`Test that the argument types match in order`) .params(u => u - .combine('num_args', [1, 2, 3] as const) .combine('p1_type', kParamsTypes) .combine('p2_type', kParamsTypes) .combine('p3_type', kParamsTypes) + .beginSubcases() .combine('arg1_value', keysOf(kArgValues)) .combine('arg2_value', keysOf(kArgValues)) .combine('arg3_value', keysOf(kArgValues)) ) .fn(t => { - let code = ` - fn bar(`; - for (let i = 0; i < t.params.num_args; i++) { - switch (i) { - case 0: - default: { - code += `p${i} : ${t.params.p1_type},`; - break; - } - case 1: { - code += `p${i} : ${t.params.p2_type},`; - break; - } - case 2: { - code += `p${i} : ${t.params.p3_type},`; - break; - } - } - } - code += `) { } - fn foo() { - bar(`; - for (let i = 0; i < t.params.num_args; i++) { - switch (i) { - case 0: - default: { - code += `${kArgValues[t.params.arg1_value].value},`; - break; - } - case 1: { - code += `${kArgValues[t.params.arg2_value].value},`; - break; - } - case 2: { - code += `${kArgValues[t.params.arg3_value].value},`; - break; - } - } - } - code += `);\n}`; + const code = ` +fn bar(p1 : ${t.params.p1_type}, p2 : ${t.params.p2_type}, p3 : ${t.params.p3_type}) { } +fn foo() { + bar(${kArgValues[t.params.arg1_value].value}, + ${kArgValues[t.params.arg2_value].value}, + ${kArgValues[t.params.arg3_value].value}); +}`; - let res = checkArgTypeMatch(t.params.p1_type, kArgValues[t.params.arg1_value].matches); - if (res && t.params.num_args > 1) { - res = checkArgTypeMatch(t.params.p2_type, kArgValues[t.params.arg2_value].matches); - } - if (res && t.params.num_args > 2) { - res = checkArgTypeMatch(t.params.p3_type, kArgValues[t.params.arg3_value].matches); - } + const res = + checkArgTypeMatch(t.params.p1_type, kArgValues[t.params.arg1_value].matches) && + checkArgTypeMatch(t.params.p2_type, kArgValues[t.params.arg2_value].matches) && + checkArgTypeMatch(t.params.p3_type, kArgValues[t.params.arg3_value].matches); t.expectCompileResult(res, code); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/break.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/break.spec.ts index 7c0f067140..46074ba5d0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/break.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/break.spec.ts @@ -15,10 +15,6 @@ const kTests = { src: 'loop { if true { break; } }', pass: true, }, - continuing_break_if: { - src: 'loop { continuing { break if (true); } }', - pass: true, - }, while_break: { src: 'while true { break; }', pass: true, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/break_if.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/break_if.spec.ts new file mode 100644 index 0000000000..97a625f625 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/break_if.spec.ts @@ -0,0 +1,141 @@ +export const description = `Validation tests for break if`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kTests = { + compound_break: { + src: '{ break if true; }', + pass: false, + }, + loop_break: { + src: 'loop { break if true; }', + pass: false, + }, + loop_if_break: { + src: 'loop { if true { break if false; } }', + pass: false, + }, + continuing_break_if: { + src: 'loop { continuing { break if true; } }', + pass: true, + }, + continuing_break_if_parens: { + src: 'loop { continuing { break if (true); } }', + pass: true, + }, + continuing_break_if_not_last: { + src: 'loop { continuing { break if (true); let a = 4;} }', + pass: false, + }, + while_break: { + src: 'while true { break if true; }', + pass: false, + }, + while_if_break: { + src: 'while true { if true { break if true; } }', + pass: false, + }, + for_break: { + src: 'for (;;) { break if true; }', + pass: false, + }, + for_if_break: { + src: 'for (;;) { if true { break if true; } }', + pass: false, + }, + switch_case_break: { + src: 'switch(1) { default: { break if true; } }', + pass: false, + }, + switch_case_if_break: { + src: 'switch(1) { default: { if true { break if true; } } }', + pass: false, + }, + break: { + src: 'break if true;', + pass: false, + }, + return_break: { + src: 'return break if true;', + pass: false, + }, + if_break: { + src: 'if true { break if true; }', + pass: false, + }, + continuing_if_break: { + src: 'loop { continuing { if (true) { break if true; } } }', + pass: false, + }, + switch_break: { + src: 'switch(1) { break if true; }', + pass: false, + }, +}; + +g.test('placement') + .desc('Test that break if placement is validated correctly') + .params(u => u.combine('stmt', keysOf(kTests))) + .fn(t => { + const code = ` +@vertex +fn vtx() -> @builtin(position) vec4f { + ${kTests[t.params.stmt].src} + return vec4f(1); +} + `; + t.expectCompileResult(kTests[t.params.stmt].pass, code); + }); + +const vec_types = [2, 3, 4] + .map(i => ['i32', 'u32', 'f32', 'f16'].map(j => `vec${i}<${j}>`)) + .reduce((a, c) => a.concat(c), []); +const f32_matrix_types = [2, 3, 4] + .map(i => [2, 3, 4].map(j => `mat${i}x${j}f`)) + .reduce((a, c) => a.concat(c), []); +const f16_matrix_types = [2, 3, 4] + .map(i => [2, 3, 4].map(j => `mat${i}x${j}<f16>`)) + .reduce((a, c) => a.concat(c), []); + +g.test('non_bool_param') + .desc('Test that break if fails with a non-bool parameter') + .params(u => + u.combine('type', [ + 'f32', + 'f16', + 'i32', + 'u32', + 'S', + ...vec_types, + ...f32_matrix_types, + ...f16_matrix_types, + ]) + ) + .beforeAllSubcases(t => { + if (t.params.type.includes('f16')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = ` +struct S { + a: i32, +} + +@vertex +fn vtx() -> @builtin(position) vec4f { + var v: ${t.params.type}; + + loop { + continuing { + break if v; + } + } + return vec4f(1); +}`; + t.expectCompileResult(t.params.type === 'bool', code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/compound.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/compound.spec.ts new file mode 100644 index 0000000000..b3627c2e5b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/compound.spec.ts @@ -0,0 +1,52 @@ +export const description = `Validation tests for compound statements`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kTests = { + missing_start: { + src: '}', + pass: false, + }, + missing_end: { + src: '{', + pass: false, + }, + empty: { + src: '{}', + pass: true, + }, + semicolon: { + src: '{;}', + pass: true, + }, + semicolons: { + src: '{;;}', + pass: true, + }, + decl: { + src: '{const c = 1;}', + pass: true, + }, + nested: { + src: '{ {} }', + pass: true, + }, +}; + +g.test('parse') + .desc('Test that compound statments parse') + .params(u => u.combine('stmt', keysOf(kTests))) + .fn(t => { + const code = ` +@vertex +fn vtx() -> @builtin(position) vec4f { + ${kTests[t.params.stmt].src} + return vec4f(1); +} + `; + t.expectCompileResult(kTests[t.params.stmt].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/continuing.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/continuing.spec.ts new file mode 100644 index 0000000000..7b53e4eab8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/continuing.spec.ts @@ -0,0 +1,185 @@ +export const description = `Validation tests for continuing`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kTests = { + continuing_break_if: { + src: 'loop { continuing { break if true; } }', + pass: true, + }, + continuing_empty: { + src: 'loop { if a == 4 { break; } continuing { } }', + pass: true, + }, + continuing_break_if_parens: { + src: 'loop { continuing { break if (true); } }', + pass: true, + }, + continuing_discard: { + src: 'loop { if a == 4 { break; } continuing { discard; } }', + pass: true, + }, + continuing_continue_nested: { + src: 'loop { if a == 4 { break; } continuing { loop { if a == 4 { break; } continue; } } }', + pass: true, + }, + continuing_continue: { + src: 'loop { if a == 4 { break; } continuing { continue; } }', + pass: false, + }, + continuing_break: { + src: 'loop { continuing { break; } }', + pass: false, + }, + continuing_for: { + src: 'loop { if a == 4 { break; } continuing { for(;a < 4;) { } } }', + pass: true, + }, + continuing_for_break: { + src: 'loop { if a == 4 { break; } continuing { for(;;) { break; } } }', + pass: true, + }, + continuing_while: { + src: 'loop { if a == 4 { break; } continuing { while a < 4 { } } }', + pass: true, + }, + continuing_while_break: { + src: 'loop { if a == 4 { break; } continuing { while true { break; } } }', + pass: true, + }, + continuing_semicolon: { + src: 'loop { if a == 4 { break; } continuing { ; } }', + pass: true, + }, + continuing_functionn_call: { + src: 'loop { if a == 4 { break; } continuing { _ = b(); } }', + pass: true, + }, + continuing_let: { + src: 'loop { if a == 4 { break; } continuing { let c = b(); } }', + pass: true, + }, + continuing_var: { + src: 'loop { if a == 4 { break; } continuing { var a = b(); } }', + pass: true, + }, + continuing_const: { + src: 'loop { if a == 4 { break; } continuing { const a = 1; } }', + pass: true, + }, + continuing_block: { + src: 'loop { if a == 4 { break; } continuing { { } } }', + pass: true, + }, + continuing_const_assert: { + src: 'loop { if a == 4 { break; } continuing { const_assert(1 != 2); } }', + pass: true, + }, + continuing_loop: { + src: 'loop { if a == 4 { break; } continuing { loop { break; } } }', + pass: true, + }, + continuing_if: { + src: 'loop { if a == 4 { break; } continuing { if true { } else if false { } else { } } }', + pass: true, + }, + continuing_switch: { + src: 'loop { if a == 4 { break; } continuing { switch 2 { default: { } } } }', + pass: true, + }, + continuing_switch_break: { + src: 'loop { if a == 4 { break; } continuing { switch 2 { default: { break; } } } }', + pass: true, + }, + continuing_loop_nested_continuing: { + src: 'loop { if a == 4 { break; } continuing { loop { if a == 4 { break; } continuing { } } } }', + pass: true, + }, + continuing_inc: { + src: 'loop { if a == 4 { break; } continuing { a += 1; } }', + pass: true, + }, + continuing_dec: { + src: 'loop { if a == 4 { break; } continuing { a -= 1; } }', + pass: true, + }, + while: { + src: 'while a < 4 { continuing { break if true; } }', + pass: false, + }, + for: { + src: 'for (;a < 4;) { continuing { break if true; } }', + pass: false, + }, + switch_case: { + src: 'switch(1) { default: { continuing { break if true; } } }', + pass: false, + }, + switch: { + src: 'switch(1) { continuing { break if true; } }', + pass: false, + }, + continuing: { + src: 'continuing { break if true; }', + pass: false, + }, + return: { + src: 'return continuing { break if true; }', + pass: false, + }, + if_body: { + src: 'if true { continuing { break if true; } }', + pass: false, + }, + if: { + src: 'if true { } continuing { break if true; } }', + pass: false, + }, + if_else: { + src: 'if true { } else { } continuing { break if true; } }', + pass: false, + }, + continuing_continuing: { + src: 'loop { if a == 4 { break; } continuing { continuing { break if true; } } }', + pass: false, + }, + no_body: { + src: 'loop { if a == 4 { break; } continuing }', + pass: false, + }, + return_in_continue: { + src: 'loop { if a == 4 { break; } continuing { return vec4f(2); } }', + pass: false, + }, + return_if_nested_in_continue: { + src: 'loop { if a == 4 { break; } continuing { if true { return vec4f(2); } } }', + pass: false, + }, + return_for_nested_in_continue: { + src: 'loop { if a == 4 { break; } continuing { for(;a < 4;) { return vec4f(2); } } }', + pass: false, + }, +}; + +g.test('placement') + .desc('Test that continuing placement is validated correctly') + .params(u => u.combine('stmt', keysOf(kTests))) + .fn(t => { + const code = ` +fn b() -> i32 { + return 1; +} + +@fragment +fn frag() -> @location(0) vec4f { + var a = 0; + ${kTests[t.params.stmt].src} + return vec4f(1); +} + `; + t.expectCompileResult(kTests[t.params.stmt].pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/diagnostic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/diagnostic.spec.ts index 154a4253ea..701ae46f3a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/diagnostic.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/diagnostic.spec.ts @@ -199,3 +199,263 @@ g.test('conflicting_attribute_different_location') const code = `${kNestedLocations[t.params.loc](d1, d2)}`; t.expectCompileResult(true, code); }); + +g.test('after_other_directives') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#diagnostics') + .desc(`Tests other global directives before a diagnostic directive.`) + .params(u => + u.combine('directive', ['enable f16', 'requires readonly_and_readwrite_storage_textures']) + ) + .beforeAllSubcases(t => { + if (t.params.directive.startsWith('enable')) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + if (t.params.directive.startsWith('requires')) { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + } + + let code = `${t.params.directive};`; + code += generateDiagnostic('directive', 'info', 'derivative_uniformity') + ';'; + t.expectCompileResult(true, code); + }); + +interface ScopeCase { + code: string; + result: boolean | 'warn'; +} + +function scopeCode(body: string): string { + return ` +@group(0) @binding(0) var t : texture_1d<f32>; +@group(0) @binding(1) var s : sampler; +var<private> non_uniform_cond : bool; +var<private> non_uniform_coord : f32; +var<private> non_uniform_val : u32; +@fragment fn main() { + ${body} +} +`; +} + +const kScopeCases: Record<string, ScopeCase> = { + override_global_off: { + code: ` + ${generateDiagnostic('directive', 'error', 'derivative_uniformity')}; + ${scopeCode(` + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if non_uniform_cond { + _ = textureSample(t,s,0.0); + }`)}; + `, + result: true, + }, + override_global_on: { + code: ` + ${generateDiagnostic('directive', 'off', 'derivative_uniformity')}; + ${scopeCode(` + ${generateDiagnostic('', 'error', 'derivative_uniformity')} + if non_uniform_cond { + _ = textureSample(t,s,0.0); + }`)} + `, + result: false, + }, + override_global_warn: { + code: ` + ${generateDiagnostic('directive', 'error', 'derivative_uniformity')}; + ${scopeCode(` + ${generateDiagnostic('', 'warning', 'derivative_uniformity')} + if non_uniform_cond { + _ = textureSample(t,s,0.0); + }`)} + `, + result: 'warn', + }, + global_if_nothing_else_warn: { + code: ` + ${generateDiagnostic('directive', 'warning', 'derivative_uniformity')}; + ${scopeCode(` + if non_uniform_cond { + _ = textureSample(t,s,0.0); + }`)} + `, + result: 'warn', + }, + deepest_nesting_warn: { + code: scopeCode(` + ${generateDiagnostic('', 'error', 'derivative_uniformity')} + if non_uniform_cond { + ${generateDiagnostic('', 'warning', 'derivative_uniformity')} + if non_uniform_cond { + _ = textureSample(t,s,0.0); + } + }`), + result: 'warn', + }, + deepest_nesting_off: { + code: scopeCode(` + ${generateDiagnostic('', 'error', 'derivative_uniformity')} + if non_uniform_cond { + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if non_uniform_cond { + _ = textureSample(t,s,0.0); + } + }`), + result: true, + }, + deepest_nesting_error: { + code: scopeCode(` + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if non_uniform_cond { + ${generateDiagnostic('', 'error', 'derivative_uniformity')} + if non_uniform_cond { + _ = textureSample(t,s,0.0); + } + }`), + result: false, + }, + other_nest_unaffected: { + code: ` + ${generateDiagnostic('directive', 'warning', 'derivative_uniformity')}; + ${scopeCode(` + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if non_uniform_cond { + _ = textureSample(t,s,0.0); + } + if non_uniform_cond { + _ = textureSample(t,s,0.0); + }`)} + `, + result: 'warn', + }, + deeper_nest_no_effect: { + code: ` + ${generateDiagnostic('directive', 'error', 'derivative_uniformity')}; + ${scopeCode(` + if non_uniform_cond { + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if non_uniform_cond { + } + _ = textureSample(t,s,0.0); + }`)} + `, + result: false, + }, + call_unaffected_error: { + code: ` + ${generateDiagnostic('directive', 'error', 'derivative_uniformity')}; + fn foo() { _ = textureSample(t,s,0.0); } + ${scopeCode(` + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if non_uniform_cond { + foo(); + }`)} + `, + result: false, + }, + call_unaffected_warn: { + code: ` + ${generateDiagnostic('directive', 'warning', 'derivative_uniformity')}; + fn foo() { _ = textureSample(t,s,0.0); } + ${scopeCode(` + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if non_uniform_cond { + foo(); + }`)} + `, + result: 'warn', + }, + call_unaffected_off: { + code: ` + ${generateDiagnostic('directive', 'off', 'derivative_uniformity')}; + fn foo() { _ = textureSample(t,s,0.0); } + ${scopeCode(` + ${generateDiagnostic('', 'error', 'derivative_uniformity')} + if non_uniform_cond { + foo(); + }`)} + `, + result: true, + }, + if_condition_error: { + code: scopeCode(` + if (non_uniform_cond) { + ${generateDiagnostic('', 'error', 'derivative_uniformity')} + if textureSample(t,s,non_uniform_coord).x > 0.0 + ${generateDiagnostic('', 'off', 'derivative_uniformity')} { + } + }`), + result: false, + }, + if_condition_warn: { + code: scopeCode(` + if non_uniform_cond { + ${generateDiagnostic('', 'warning', 'derivative_uniformity')} + if textureSample(t,s,non_uniform_coord).x > 0.0 + ${generateDiagnostic('', 'error', 'derivative_uniformity')} { + } + }`), + result: 'warn', + }, + if_condition_off: { + code: scopeCode(` + if non_uniform_cond { + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + if textureSample(t,s,non_uniform_coord).x > 0.0 + ${generateDiagnostic('', 'error', 'derivative_uniformity')} { + } + }`), + result: true, + }, + switch_error: { + code: scopeCode(` + ${generateDiagnostic('', 'error', 'derivative_uniformity')} + switch non_uniform_val { + case 0 ${generateDiagnostic('', 'off', 'derivative_uniformity')} { + } + default { + _ = textureSample(t,s,0.0); + } + }`), + result: false, + }, + switch_warn: { + code: scopeCode(` + ${generateDiagnostic('', 'warning', 'derivative_uniformity')} + switch non_uniform_val { + case 0 ${generateDiagnostic('', 'off', 'derivative_uniformity')} { + } + default { + _ = textureSample(t,s,0.0); + } + }`), + result: 'warn', + }, + switch_off: { + code: scopeCode(` + ${generateDiagnostic('', 'off', 'derivative_uniformity')} + switch non_uniform_val { + case 0 ${generateDiagnostic('', 'error', 'derivative_uniformity')}{ + } + default { + _ = textureSample(t,s,0.0); + } + }`), + result: true, + }, +}; + +g.test('diagnostic_scoping') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#diagnostics') + .desc('Tests that innermost scope controls the diagnostic') + .params(u => u.combine('case', keysOf(kScopeCases))) + .fn(t => { + const testcase = kScopeCases[t.params.case]; + if (testcase.result === 'warn') { + t.expectCompileWarning(true, testcase.code); + } else { + t.expectCompileResult(testcase.result as boolean, testcase.code); + } + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/enable.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/enable.spec.ts index 230244c6b8..799053b547 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/enable.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/enable.spec.ts @@ -13,11 +13,21 @@ const kCases = { enable f16;`, pass: false, }, - after_decl: { + decl_after: { code: `enable f16; alias i = i32;`, pass: true, }, + requires_before: { + code: `requires readonly_and_readwrite_storage_textures; +enable f16;`, + pass: true, + }, + diagnostic_before: { + code: `diagnostic(info, derivative_uniformity); +enable f16;`, + pass: true, + }, const_assert_before: { code: `const_assert 1 == 1; enable f16;`, @@ -48,7 +58,7 @@ f16;`, enable f16;`, pass: true, }, - multipe_entries: { + multiple_entries: { code: `enable f16, f16, f16;`, pass: true, }, @@ -65,6 +75,10 @@ g.test('enable') }) .params(u => u.combine('case', keysOf(kCases))) .fn(t => { + if (t.params.case === 'requires_before') { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + } + const c = kCases[t.params.case]; t.expectCompileResult(c.pass, c.code); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/must_use.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/must_use.spec.ts index dd36fabcf6..058a5f8c9b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/must_use.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/must_use.spec.ts @@ -57,27 +57,74 @@ g.test('declaration') }); const kMustUseCalls = { + no_call: ``, // Never calling a @must_use function should pass phony: `_ = bar();`, let: `let tmp = bar();`, - var: `var tmp = bar();`, + local_var: `var tmp = bar();`, + private_var: `private_var = bar();`, + storage_var: `storage_var = bar();`, + pointer: ` + var a : f32; + let p = &a; + (*p) = bar();`, + vector_elem: ` + var a : vec3<f32>; + a.x = bar();`, + matrix_elem: ` + var a : mat3x2<f32>; + a[0][0] = bar();`, condition: `if bar() == 0 { }`, param: `baz(bar());`, - statement: `bar();`, + return: `return bar();`, + statement: `bar();`, // should fail if bar is @must_use }; g.test('call') .desc(`Validate that a call to must_use function cannot be the whole function call statement`) - .params(u => u.combine('use', ['@must_use', ''] as const).combine('call', keysOf(kMustUseCalls))) + .params(u => + u // + .combine('use', ['@must_use', ''] as const) + .combine('call', keysOf(kMustUseCalls)) + ) .fn(t => { const test = kMustUseCalls[t.params.call]; const code = ` - fn baz(param : u32) { } - ${t.params.use} fn bar() -> u32 { return 0; } - fn foo() { + @group(0) @binding(0) var<storage, read_write> storage_var : f32; + var<private> private_var : f32; + + fn baz(param : f32) { } + + ${t.params.use} fn bar() -> f32 { return 0; } + + fn foo() ${t.params.call === 'return' ? '-> f32' : ''} { ${test} }`; - const res = t.params.call !== 'statement' || t.params.use === ''; - t.expectCompileResult(res, code); + + const should_pass = t.params.call !== 'statement' || t.params.use === ''; + t.expectCompileResult(should_pass, code); + }); + +g.test('ignore_result_of_non_must_use_that_returns_call_of_must_use') + .desc( + `Test that ignoring the result of a non-@must_use function that returns the result of a @must_use function succeeds` + ) + .fn(t => { + const wgsl = ` + @must_use + fn f() -> f32 { + return 0; + } + + fn g() -> f32 { + return f(); + } + + fn main() { + g(); // Ignore result + } + `; + + t.expectCompileResult(true, wgsl); }); const kMustUseBuiltinCalls = { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/pipeline_stage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/pipeline_stage.spec.ts index 78dcb95782..f492121f25 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/pipeline_stage.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/pipeline_stage.spec.ts @@ -73,34 +73,46 @@ g.test('multiple_entry_points') t.expectCompileResult(true, code); }); -g.test('duplicate_compute_on_function') - .desc(`Test that duplcate @compute attributes are not allowed.`) - .params(u => u.combine('dupe', ['', '@compute'])) +g.test('extra_on_compute_function') + .desc(`Test that an extra stage attribute on @compute functions are not allowed.`) + .params(u => + u.combine('extra', ['', '@compute', '@fragment', '@vertex']).combine('before', [false, true]) + ) .fn(t => { + const before = t.params.before ? t.params.extra : ''; + const after = t.params.before ? '' : t.params.extra; const code = ` -@compute ${t.params.dupe} @workgroup_size(1) fn compute_1() {} +${before} @compute ${after} @workgroup_size(1) fn main() {} `; - t.expectCompileResult(t.params.dupe === '', code); + t.expectCompileResult(t.params.extra === '', code); }); -g.test('duplicate_fragment_on_function') - .desc(`Test that duplcate @fragment attributes are not allowed.`) - .params(u => u.combine('dupe', ['', '@fragment'])) +g.test('extra_on_fragment_function') + .desc(`Test that an extra stage attribute on @fragment functions are not allowed.`) + .params(u => + u.combine('extra', ['', '@compute', '@fragment', '@vertex']).combine('before', [false, true]) + ) .fn(t => { + const before = t.params.before ? t.params.extra : ''; + const after = t.params.before ? '' : t.params.extra; const code = ` -@fragment ${t.params.dupe} fn vtx() -> @location(0) vec4f { return vec4f(1); } +${before} @fragment ${after} fn main() -> @location(0) vec4f { return vec4f(1); } `; - t.expectCompileResult(t.params.dupe === '', code); + t.expectCompileResult(t.params.extra === '', code); }); -g.test('duplicate_vertex_on_function') - .desc(`Test that duplcate @vertex attributes are not allowed.`) - .params(u => u.combine('dupe', ['', '@vertex'])) +g.test('extra_on_vertex_function') + .desc(`Test that an extra stage attribute on @vertex functions are not allowed.`) + .params(u => + u.combine('extra', ['', '@compute', '@fragment', '@vertex']).combine('before', [false, true]) + ) .fn(t => { + const before = t.params.before ? t.params.extra : ''; + const after = t.params.before ? '' : t.params.extra; const code = ` -@vertex ${t.params.dupe} fn vtx() -> @builtin(position) vec4f { return vec4f(1); } +${before} @vertex ${after} fn main() -> @builtin(position) vec4f { return vec4f(1); } `; - t.expectCompileResult(t.params.dupe === '', code); + t.expectCompileResult(t.params.extra === '', code); }); g.test('placement') diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/requires.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/requires.spec.ts new file mode 100644 index 0000000000..2b365ae61e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/requires.spec.ts @@ -0,0 +1,103 @@ +export const description = `Parser validation tests for requires`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { kKnownWGSLLanguageFeatures } from '../../../capability_info.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kCases = { + valid: { code: `requires readonly_and_readwrite_storage_textures;`, pass: true }, + decl_before: { + code: `alias i = i32; +requires readonly_and_readwrite_storage_textures;`, + pass: false, + }, + decl_after: { + code: `requires readonly_and_readwrite_storage_textures; +alias i = i32;`, + pass: true, + }, + enable_before: { + code: `enable f16; +requires readonly_and_readwrite_storage_textures;`, + pass: true, + }, + diagnostic_before: { + code: `diagnostic(info, derivative_uniformity); +requires readonly_and_readwrite_storage_textures;`, + pass: true, + }, + const_assert_before: { + code: `const_assert 1 == 1; +requires readonly_and_readwrite_storage_textures;`, + pass: false, + }, + const_assert_after: { + code: `requires readonly_and_readwrite_storage_textures; +const_assert 1 == 1;`, + pass: true, + }, + embedded_comment: { + code: `/* comment + +*/requires readonly_and_readwrite_storage_textures;`, + pass: true, + }, + parens: { + code: `requires(readonly_and_readwrite_storage_textures);`, + pass: false, + }, + multi_line: { + code: `requires +readonly_and_readwrite_storage_textures;`, + pass: true, + }, + multiple_requires_duplicate: { + code: `requires readonly_and_readwrite_storage_textures; +requires readonly_and_readwrite_storage_textures;`, + pass: true, + }, + multiple_requires_different: { + code: `requires readonly_and_readwrite_storage_textures; +requires packed_4x8_integer_dot_product;`, + pass: true, + }, + multiple_entries_duplicate: { + code: `requires readonly_and_readwrite_storage_textures, readonly_and_readwrite_storage_textures, readonly_and_readwrite_storage_textures;`, + pass: true, + }, + multiple_entries_different: { + code: `requires readonly_and_readwrite_storage_textures, packed_4x8_integer_dot_product;`, + pass: true, + }, + unknown: { + code: `requires unknown;`, + pass: false, + }, +}; + +g.test('requires') + .desc(`Tests that requires are validated correctly.`) + .params(u => u.combine('case', keysOf(kCases))) + .beforeAllSubcases(t => { + if (t.params.case === 'enable_before') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + t.skipIfLanguageFeatureNotSupported('packed_4x8_integer_dot_product'); + + const c = kCases[t.params.case]; + t.expectCompileResult(c.pass, c.code); + }); + +g.test('wgsl_matches_api') + .desc(`Tests that language features are accepted iff the API reports support for them.`) + .params(u => u.combine('feature', kKnownWGSLLanguageFeatures)) + .fn(t => { + const code = `requires ${t.params.feature};`; + t.expectCompileResult(t.hasLanguageFeature(t.params.feature), code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts index 87cffcfafc..15a225e19f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts @@ -27,6 +27,21 @@ g.test('after_enable') t.expectCompileResult(/* pass */ false, `enable f16`); }); +g.test('after_requires') + .desc(`Test that a semicolon must be placed after a requires directive.`) + .fn(t => { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + t.expectCompileResult(/* pass */ true, `requires readonly_and_readwrite_storage_textures;`); + t.expectCompileResult(/* pass */ false, `requires readonly_and_readwrite_storage_textures`); + }); + +g.test('after_diagnostic') + .desc(`Test that a semicolon must be placed after a requires directive.`) + .fn(t => { + t.expectCompileResult(/* pass */ true, `diagnostic(info, derivative_uniformity);`); + t.expectCompileResult(/* pass */ false, `diagnostic(info, derivative_uniformity)`); + }); + g.test('after_struct_decl') .desc(`Test that a semicolon can be placed after an struct declaration.`) .fn(t => { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/shadow_builtins.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/shadow_builtins.spec.ts new file mode 100644 index 0000000000..3f72a0bf72 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/shadow_builtins.spec.ts @@ -0,0 +1,995 @@ +export const description = `Validation tests for identifiers`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('function_param') + .desc( + `Test that a function param can shadow a builtin, but the builtin is available for other params.` + ) + .fn(t => { + const code = ` +fn f(f: i32, i32: i32, t: i32) -> i32 { return i32; } + `; + t.expectCompileResult(true, code); + }); + +const kTests = { + abs: { + keyword: `abs`, + src: `_ = abs(1);`, + }, + acos: { + keyword: `acos`, + src: `_ = acos(.2);`, + }, + acosh: { + keyword: `acosh`, + src: `_ = acosh(1.2);`, + }, + all: { + keyword: `all`, + src: `_ = all(true);`, + }, + any: { + keyword: `any`, + src: `_ = any(true);`, + }, + array_templated: { + keyword: `array`, + src: `_ = array<i32, 2>(1, 2);`, + }, + array: { + keyword: `array`, + src: `_ = array(1, 2);`, + }, + array_length: { + keyword: `arrayLength`, + src: `_ = arrayLength(&placeholder.rt_arr);`, + }, + asin: { + keyword: `asin`, + src: `_ = asin(.2);`, + }, + asinh: { + keyword: `asinh`, + src: `_ = asinh(1.2);`, + }, + atan: { + keyword: `atan`, + src: `_ = atan(1.2);`, + }, + atanh: { + keyword: `atanh`, + src: `_ = atanh(.2);`, + }, + atan2: { + keyword: `atan2`, + src: `_ = atan2(1.2, 2.3);`, + }, + bool: { + keyword: `bool`, + src: `_ = bool(1);`, + }, + bitcast: { + keyword: `bitcast`, + src: `_ = bitcast<f32>(1i);`, + }, + ceil: { + keyword: `ceil`, + src: `_ = ceil(1.23);`, + }, + clamp: { + keyword: `clamp`, + src: `_ = clamp(1, 2, 3);`, + }, + cos: { + keyword: `cos`, + src: `_ = cos(2);`, + }, + cosh: { + keyword: `cosh`, + src: `_ = cosh(2.2);`, + }, + countLeadingZeros: { + keyword: `countLeadingZeros`, + src: `_ = countLeadingZeros(1);`, + }, + countOneBits: { + keyword: `countOneBits`, + src: `_ = countOneBits(1);`, + }, + countTrailingZeros: { + keyword: `countTrailingZeros`, + src: `_ = countTrailingZeros(1);`, + }, + cross: { + keyword: `cross`, + src: `_ = cross(vec3(1, 2, 3), vec3(4, 5, 6));`, + }, + degrees: { + keyword: `degrees`, + src: `_ = degrees(1);`, + }, + determinant: { + keyword: `determinant`, + src: `_ = determinant(mat2x2(1, 2, 3, 4));`, + }, + distance: { + keyword: `distance`, + src: `_ = distance(1, 2);`, + }, + dot: { + keyword: `dot`, + src: `_ = dot(vec2(1, 2,), vec2(2, 3));`, + }, + dot4U8Packed: { + keyword: `dot4U8Packed`, + src: `_ = dot4U8Packed(1, 2);`, + }, + dot4I8Packed: { + keyword: `dot4I8Packed`, + src: `_ = dot4I8Packed(1, 2);`, + }, + dpdx: { + keyword: `dpdx`, + src: `_ = dpdx(2);`, + }, + dpdxCoarse: { + keyword: `dpdxCoarse`, + src: `_ = dpdxCoarse(2);`, + }, + dpdxFine: { + keyword: `dpdxFine`, + src: `_ = dpdxFine(2);`, + }, + dpdy: { + keyword: `dpdy`, + src: `_ = dpdy(2);`, + }, + dpdyCoarse: { + keyword: `dpdyCoarse`, + src: `_ = dpdyCoarse(2);`, + }, + dpdyFine: { + keyword: `dpdyFine`, + src: `_ = dpdyFine(2);`, + }, + exp: { + keyword: `exp`, + src: `_ = exp(1);`, + }, + exp2: { + keyword: `exp2`, + src: `_ = exp2(2);`, + }, + extractBits: { + keyword: `extractBits`, + src: `_ = extractBits(1, 2, 3);`, + }, + f32: { + keyword: `f32`, + src: `_ = f32(1i);`, + }, + faceForward: { + keyword: `faceForward`, + src: `_ = faceForward(vec2(1, 2), vec2(3, 4), vec2(5, 6));`, + }, + firstLeadingBit: { + keyword: `firstLeadingBit`, + src: `_ = firstLeadingBit(1);`, + }, + firstTrailingBit: { + keyword: `firstTrailingBit`, + src: `_ = firstTrailingBit(1);`, + }, + floor: { + keyword: `floor`, + src: `_ = floor(1.2);`, + }, + fma: { + keyword: `fma`, + src: `_ = fma(1, 2, 3);`, + }, + fract: { + keyword: `fract`, + src: `_ = fract(1);`, + }, + frexp: { + keyword: `frexp`, + src: `_ = frexp(1);`, + }, + fwidth: { + keyword: `fwidth`, + src: `_ = fwidth(2);`, + }, + fwidthCoarse: { + keyword: `fwidthCoarse`, + src: `_ = fwidthCoarse(2);`, + }, + fwidthFine: { + keyword: `fwidthFine`, + src: `_ = fwidthFine(2);`, + }, + i32: { + keyword: `i32`, + src: `_ = i32(2u);`, + }, + insertBits: { + keyword: `insertBits`, + src: `_ = insertBits(1, 2, 3, 4);`, + }, + inverseSqrt: { + keyword: `inverseSqrt`, + src: `_ = inverseSqrt(1);`, + }, + ldexp: { + keyword: `ldexp`, + src: `_ = ldexp(1, 2);`, + }, + length: { + keyword: `length`, + src: `_ = length(1);`, + }, + log: { + keyword: `log`, + src: `_ = log(2);`, + }, + log2: { + keyword: `log2`, + src: `_ = log2(2);`, + }, + mat2x2_templated: { + keyword: `mat2x2`, + src: `_ = mat2x2<f32>(1, 2, 3, 4);`, + }, + mat2x2: { + keyword: `mat2x2`, + src: `_ = mat2x2(1, 2, 3, 4);`, + }, + mat2x3_templated: { + keyword: `mat2x3`, + src: `_ = mat2x3<f32>(1, 2, 3, 4, 5, 6);`, + }, + mat2x3: { + keyword: `mat2x3`, + src: `_ = mat2x3(1, 2, 3, 4, 5, 6);`, + }, + mat2x4_templated: { + keyword: `mat2x4`, + src: `_ = mat2x4<f32>(1, 2, 3, 4, 5, 6, 7, 8);`, + }, + mat2x4: { + keyword: `mat2x4`, + src: `_ = mat2x4(1, 2, 3, 4, 5, 6, 7, 8);`, + }, + mat3x2_templated: { + keyword: `mat3x2`, + src: `_ = mat3x2<f32>(1, 2, 3, 4, 5, 6);`, + }, + mat3x2: { + keyword: `mat3x2`, + src: `_ = mat3x2(1, 2, 3, 4, 5, 6);`, + }, + mat3x3_templated: { + keyword: `mat3x3`, + src: `_ = mat3x3<f32>(1, 2, 3, 4, 5, 6, 7, 8, 9);`, + }, + mat3x3: { + keyword: `mat3x3`, + src: `_ = mat3x3(1, 2, 3, 4, 5, 6, 7, 8, 9);`, + }, + mat3x4_templated: { + keyword: `mat3x4`, + src: `_ = mat3x4<f32>(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);`, + }, + mat3x4: { + keyword: `mat3x4`, + src: `_ = mat3x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);`, + }, + mat4x2_templated: { + keyword: `mat4x2`, + src: `_ = mat4x2<f32>(1, 2, 3, 4, 5, 6, 7, 8);`, + }, + mat4x2: { + keyword: `mat4x2`, + src: `_ = mat4x2(1, 2, 3, 4, 5, 6, 7, 8);`, + }, + mat4x3_templated: { + keyword: `mat4x3`, + src: `_ = mat4x3<f32>(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);`, + }, + mat4x3: { + keyword: `mat4x3`, + src: `_ = mat4x3(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);`, + }, + mat4x4_templated: { + keyword: `mat4x4`, + src: `_ = mat4x4<f32>(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);`, + }, + mat4x4: { + keyword: `mat4x4`, + src: `_ = mat4x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);`, + }, + max: { + keyword: `max`, + src: `_ = max(1, 2);`, + }, + min: { + keyword: `min`, + src: `_ = min(1, 2);`, + }, + mix: { + keyword: `mix`, + src: `_ = mix(1, 2, 3);`, + }, + modf: { + keyword: `modf`, + src: `_ = modf(1.2);`, + }, + normalize: { + keyword: `normalize`, + src: `_ = normalize(vec2(1, 2));`, + }, + pack2x16snorm: { + keyword: `pack2x16snorm`, + src: `_ = pack2x16snorm(vec2(1, 2));`, + }, + pack2x16unorm: { + keyword: `pack2x16unorm`, + src: `_ = pack2x16unorm(vec2(1, 2));`, + }, + pack2x16float: { + keyword: `pack2x16float`, + src: `_ = pack2x16float(vec2(1, 2));`, + }, + pack4x8snorm: { + keyword: `pack4x8snorm`, + src: `_ = pack4x8snorm(vec4(1, 2, 3, 4));`, + }, + pack4x8unorm: { + keyword: `pack4x8unorm`, + src: `_ = pack4x8unorm(vec4(1, 2, 3, 4));`, + }, + pack4xI8: { + keyword: `pack4xI8`, + src: `_ = pack4xI8(vec4(1, 2, 3, 4));`, + }, + pack4xU8: { + keyword: `pack4xU8`, + src: `_ = pack4xU8(vec4(1, 2, 3, 4));`, + }, + pack4xI8Clamp: { + keyword: `pack4xI8Clamp`, + src: `_ = pack4xI8Clamp(vec4(1, 2, 3, 4));`, + }, + pack4xU8Clamp: { + keyword: `pack4xU8Clamp`, + src: `_ = pack4xU8Clamp(vec4(1, 2, 3, 4));`, + }, + pow: { + keyword: `pow`, + src: `_ = pow(1, 2);`, + }, + quantizeToF16: { + keyword: `quantizeToF16`, + src: `_ = quantizeToF16(1.2);`, + }, + radians: { + keyword: `radians`, + src: `_ = radians(1.2);`, + }, + reflect: { + keyword: `reflect`, + src: `_ = reflect(vec2(1, 2), vec2(3, 4));`, + }, + refract: { + keyword: `refract`, + src: `_ = refract(vec2(1, 1), vec2(2, 2), 3);`, + }, + reverseBits: { + keyword: `reverseBits`, + src: `_ = reverseBits(1);`, + }, + round: { + keyword: `round`, + src: `_ = round(1.2);`, + }, + saturate: { + keyword: `saturate`, + src: `_ = saturate(1);`, + }, + select: { + keyword: `select`, + src: `_ = select(1, 2, false);`, + }, + sign: { + keyword: `sign`, + src: `_ = sign(1);`, + }, + sin: { + keyword: `sin`, + src: `_ = sin(2);`, + }, + sinh: { + keyword: `sinh`, + src: `_ = sinh(3);`, + }, + smoothstep: { + keyword: `smoothstep`, + src: `_ = smoothstep(1, 2, 3);`, + }, + sqrt: { + keyword: `sqrt`, + src: `_ = sqrt(24);`, + }, + step: { + keyword: `step`, + src: `_ = step(4, 5);`, + }, + tan: { + keyword: `tan`, + src: `_ = tan(2);`, + }, + tanh: { + keyword: `tanh`, + src: `_ = tanh(2);`, + }, + transpose: { + keyword: `transpose`, + src: `_ = transpose(mat2x2(1, 2, 3, 4));`, + }, + trunc: { + keyword: `trunc`, + src: `_ = trunc(2);`, + }, + u32: { + keyword: `u32`, + src: `_ = u32(1i);`, + }, + unpack2x16snorm: { + keyword: `unpack2x16snorm`, + src: `_ = unpack2x16snorm(2);`, + }, + unpack2x16unorm: { + keyword: `unpack2x16unorm`, + src: `_ = unpack2x16unorm(2);`, + }, + unpack2x16float: { + keyword: `unpack2x16float`, + src: `_ = unpack2x16float(2);`, + }, + unpack4x8snorm: { + keyword: `unpack4x8snorm`, + src: `_ = unpack4x8snorm(4);`, + }, + unpack4x8unorm: { + keyword: `unpack4x8unorm`, + src: `_ = unpack4x8unorm(4);`, + }, + unpack4xI8: { + keyword: `unpack4xI8`, + src: `_ = unpack4xI8(4);`, + }, + unpack4xU8: { + keyword: `unpack4xU8`, + src: `_ = unpack4xU8(4);`, + }, + vec2_templated: { + keyword: `vec2`, + src: `_ = vec2<f32>(1, 2);`, + }, + vec2: { + keyword: `vec2`, + src: `_ = vec2(1, 2);`, + }, + vec3_templated: { + keyword: `vec3`, + src: `_ = vec3<f32>(1, 2, 3);`, + }, + vec3: { + keyword: `vec3`, + src: `_ = vec3(1, 2, 3);`, + }, + vec4_templated: { + keyword: `vec4`, + src: `_ = vec4<f32>(1, 2, 3, 4);`, + }, + vec4: { + keyword: `vec4`, + src: `_ = vec4(1, 2, 3, 4);`, + }, +}; + +g.test('shadow_hides_builtin') + .desc(`Test that shadows hide builtins.`) + .params(u => + u + .combine('inject', ['none', 'function', 'sibling', 'module'] as const) + .beginSubcases() + .combine('builtin', keysOf(kTests)) + ) + .fn(t => { + const data = kTests[t.params.builtin]; + const local = `let ${data.keyword} = 4;`; + + const module_shadow = t.params.inject === 'module' ? `var<private> ${data.keyword} : i32;` : ``; + const sibling_func = t.params.inject === 'sibling' ? local : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +struct Data { + rt_arr: array<i32>, +} +@group(0) @binding(0) var<storage> placeholder: Data; + +${module_shadow} + +fn sibling() { + ${sibling_func} +} + +@fragment +fn main() -> @location(0) vec4f { + ${func} + ${data.src} + return vec4f(1); +} + `; + + const pass = t.params.inject === 'none' || t.params.inject === 'sibling'; + t.expectCompileResult(pass, code); + }); + +const kFloat16Tests = { + f16: { + keyword: `f16`, + src: `_ = f16(2);`, + }, +}; + +g.test('shadow_hides_builtin_f16') + .desc(`Test that shadows hide builtins when shader-f16 is enabled.`) + .params(u => + u + .combine('inject', ['none', 'function', 'sibling', 'module'] as const) + .beginSubcases() + .combine('builtin', keysOf(kFloat16Tests)) + ) + .beforeAllSubcases(t => { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + }) + .fn(t => { + const data = kFloat16Tests[t.params.builtin]; + const local = `let ${data.keyword} = 4;`; + + const module_shadow = t.params.inject === 'module' ? `var<private> ${data.keyword} : f16;` : ``; + const sibling_func = t.params.inject === 'sibling' ? local : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +enable f16; + +${module_shadow} + +fn sibling() { + ${sibling_func} +} + +@vertex +fn vtx() -> @builtin(position) vec4f { + ${func} + ${data.src} + return vec4f(1); +} + `; + const pass = t.params.inject === 'none' || t.params.inject === 'sibling'; + t.expectCompileResult(pass, code); + }); + +const kTextureTypeTests = { + texture_1d: { + keyword: `texture_1d`, + src: `var t: texture_1d<f32>;`, + }, + texture_2d: { + keyword: `texture_2d`, + src: `var t: texture_2d<f32>;`, + }, + texture_2d_array: { + keyword: `texture_2d_array`, + src: `var t: texture_2d_array<f32>;`, + }, + texture_3d: { + keyword: `texture_3d`, + src: `var t: texture_3d<f32>;`, + }, + texture_cube: { + keyword: `texture_cube`, + src: `var t: texture_cube<f32>;`, + }, + texture_cube_array: { + keyword: `texture_cube_array`, + src: `var t: texture_cube_array<f32>;`, + }, + texture_multisampled_2d: { + keyword: `texture_multisampled_2d`, + src: `var t: texture_multisampled_2d<f32>;`, + }, + texture_depth_multisampled_2d: { + keyword: `texture_depth_multisampled_2d`, + src: `var t: texture_depth_multisampled_2d;`, + }, + texture_external: { + keyword: `texture_external`, + src: `var t: texture_external;`, + }, + texture_storage_1d: { + keyword: `texture_storage_1d`, + src: `var t: texture_storage_1d<rgba8unorm, read_write>;`, + }, + texture_storage_2d: { + keyword: `texture_storage_2d`, + src: `var t: texture_storage_2d<rgba8unorm, read_write>;`, + }, + texture_storage_2d_array: { + keyword: `texture_storage_2d_array`, + src: `var t: texture_storage_2d_array<rgba8unorm, read_write>;`, + }, + texture_storage_3d: { + keyword: `texture_storage_3d`, + src: `var t: texture_storage_3d<rgba8unorm, read_write>;`, + }, + texture_depth_2d: { + keyword: `texture_depth_2d`, + src: `var t: texture_depth_2d;`, + }, + texture_depth_2d_array: { + keyword: `texture_depth_2d_array`, + src: `var t: texture_depth_2d_array;`, + }, + texture_depth_cube: { + keyword: `texture_depth_cube`, + src: `var t: texture_depth_cube;`, + }, + texture_depth_cube_array: { + keyword: `texture_depth_cube_array`, + src: `var t: texture_depth_cube_array;`, + }, + sampler: { + keyword: `sampler`, + src: `var s: sampler;`, + }, + sampler_comparison: { + keyword: `sampler_comparison`, + src: `var s: sampler_comparison;`, + }, +}; + +g.test('shadow_hides_builtin_handle_type') + .desc(`Test that shadows hide builtins when handle address space types are used.`) + .params(u => + u + .combine('inject', ['none', 'function', 'module'] as const) + .beginSubcases() + .combine('builtin', keysOf(kTextureTypeTests)) + ) + .fn(t => { + const data = kTextureTypeTests[t.params.builtin]; + const local = `let ${data.keyword} = 4;`; + + const module_shadow = t.params.inject === 'module' ? `var<private> ${data.keyword} : f32;` : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +${module_shadow} +@group(0) @binding(0) ${data.src} + +fn func() { + ${func} +} + `; + const pass = t.params.inject === 'none' || t.params.inject === 'function'; + t.expectCompileResult(pass, code); + }); + +const kTextureTests = { + textureDimensions: { + keyword: `textureDimensions`, + src: `_ = textureDimensions(t_2d);`, + }, + textureGather: { + keyword: `textureGather`, + src: `_ = textureGather(1, t_2d, s, vec2(1, 2));`, + }, + textureGatherCompare: { + keyword: `textureGatherCompare`, + src: `_ = textureGatherCompare(t_2d_depth, sc, vec2(1, 2), 3);`, + }, + textureLoad: { + keyword: `textureLoad`, + src: `_ = textureLoad(t_2d, vec2(1, 2), 1);`, + }, + textureNumLayers: { + keyword: `textureNumLayers`, + src: `_ = textureNumLayers(t_2d_array);`, + }, + textureNumLevels: { + keyword: `textureNumLevels`, + src: `_ = textureNumLevels(t_2d);`, + }, + textureNumSamples: { + keyword: `textureNumSamples`, + src: `_ = textureNumSamples(t_2d_ms);`, + }, + textureSample: { + keyword: `textureSample`, + src: `_ = textureSample(t_2d, s, vec2(1, 2));`, + }, + textureSampleBias: { + keyword: `textureSampleBias`, + src: `_ = textureSampleBias(t_2d, s, vec2(1, 2), 2);`, + }, + textureSampleCompare: { + keyword: `textureSampleCompare`, + src: `_ = textureSampleCompare(t_2d_depth, sc, vec2(1, 2), 2);`, + }, + textureSampleCompareLevel: { + keyword: `textureSampleCompareLevel`, + src: `_ = textureSampleCompareLevel(t_2d_depth, sc, vec2(1, 2), 3, vec2(1, 2));`, + }, + textureSampleGrad: { + keyword: `textureSampleGrad`, + src: `_ = textureSampleGrad(t_2d, s, vec2(1, 2), vec2(1, 2), vec2(1, 2));`, + }, + textureSampleLevel: { + keyword: `textureSampleLevel`, + src: `_ = textureSampleLevel(t_2d, s, vec2(1, 2), 3);`, + }, + textureSampleBaseClampToEdge: { + keyword: `textureSampleBaseClampToEdge`, + src: `_ = textureSampleBaseClampToEdge(t_2d, s, vec2(1, 2));`, + }, +}; + +g.test('shadow_hides_builtin_texture') + .desc(`Test that shadows hide texture builtins.`) + .params(u => + u + .combine('inject', ['none', 'function', 'sibling', 'module'] as const) + .beginSubcases() + .combine('builtin', keysOf(kTextureTests)) + ) + .fn(t => { + const data = kTextureTests[t.params.builtin]; + const local = `let ${data.keyword} = 4;`; + + const module_shadow = t.params.inject === 'module' ? `var<private> ${data.keyword} : i32;` : ``; + const sibling_func = t.params.inject === 'sibling' ? local : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +@group(0) @binding(0) var t_2d: texture_2d<f32>; +@group(0) @binding(1) var t_2d_depth: texture_depth_2d; +@group(0) @binding(2) var t_2d_array: texture_2d_array<f32>; +@group(0) @binding(3) var t_2d_ms: texture_multisampled_2d<f32>; + +@group(1) @binding(0) var s: sampler; +@group(1) @binding(1) var sc: sampler_comparison; + +${module_shadow} + +fn sibling() { + ${sibling_func} +} + +@fragment +fn main() -> @location(0) vec4f { + ${func} + ${data.src} + return vec4f(1); +} + `; + + const pass = t.params.inject === 'none' || t.params.inject === 'sibling'; + t.expectCompileResult(pass, code); + }); + +g.test('shadow_hides_builtin_atomic_type') + .desc(`Test that shadows hide builtins when atomic types are used.`) + .params(u => u.combine('inject', ['none', 'function', 'module'] as const).beginSubcases()) + .fn(t => { + const local = `let atomic = 4;`; + const module_shadow = t.params.inject === 'module' ? `var<private> atomic: i32;` : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +${module_shadow} + +var<workgroup> val: atomic<i32>; + +fn func() { + ${func} +} + `; + const pass = t.params.inject === 'none' || t.params.inject === 'function'; + t.expectCompileResult(pass, code); + }); + +const kAtomicTests = { + atomicLoad: { + keyword: `atomicLoad`, + src: `_ = atomicLoad(&a);`, + }, + atomicStore: { + keyword: `atomicStore`, + src: `atomicStore(&a, 1);`, + }, + atomicAdd: { + keyword: `atomicAdd`, + src: `_ = atomicAdd(&a, 1);`, + }, + atomicSub: { + keyword: `atomicSub`, + src: `_ = atomicSub(&a, 1);`, + }, + atomicMax: { + keyword: `atomicMax`, + src: `_ = atomicMax(&a, 1);`, + }, + atomicMin: { + keyword: `atomicMin`, + src: `_ = atomicMin(&a, 1);`, + }, + atomicAnd: { + keyword: `atomicAnd`, + src: `_ = atomicAnd(&a, 1);`, + }, + atomicOr: { + keyword: `atomicOr`, + src: `_ = atomicOr(&a, 1);`, + }, + atomicXor: { + keyword: `atomicXor`, + src: `_ = atomicXor(&a, 1);`, + }, +}; + +g.test('shadow_hides_builtin_atomic') + .desc(`Test that shadows hide builtin atomic methods.`) + .params(u => + u + .combine('inject', ['none', 'function', 'sibling', 'module'] as const) + .beginSubcases() + .combine('builtin', keysOf(kAtomicTests)) + ) + .fn(t => { + const data = kAtomicTests[t.params.builtin]; + const local = `let ${data.keyword} = 4;`; + + const module_shadow = t.params.inject === 'module' ? `var<private> ${data.keyword} : i32;` : ``; + const sibling_func = t.params.inject === 'sibling' ? local : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +var<workgroup> a: atomic<i32>; + +${module_shadow} + +fn sibling() { + ${sibling_func} +} + +@compute @workgroup_size(1) +fn main() { + ${func} + ${data.src} +} + `; + + const pass = t.params.inject === 'none' || t.params.inject === 'sibling'; + t.expectCompileResult(pass, code); + }); + +const kBarrierTests = { + storageBarrier: { + keyword: `storageBarrier`, + src: `storageBarrier();`, + }, + textureBarrier: { + keyword: `textureBarrier`, + src: `textureBarrier();`, + }, + workgroupBarrier: { + keyword: `workgroupBarrier`, + src: `workgroupBarrier();`, + }, + workgroupUniformLoad: { + keyword: `workgroupUniformLoad`, + src: `_ = workgroupUniformLoad(&u);`, + }, +}; + +g.test('shadow_hides_builtin_barriers') + .desc(`Test that shadows hide builtin barrier methods.`) + .params(u => + u + .combine('inject', ['none', 'function', 'sibling', 'module'] as const) + .beginSubcases() + .combine('builtin', keysOf(kBarrierTests)) + ) + .fn(t => { + const data = kBarrierTests[t.params.builtin]; + const local = `let ${data.keyword} = 4;`; + + const module_shadow = t.params.inject === 'module' ? `var<private> ${data.keyword} : i32;` : ``; + const sibling_func = t.params.inject === 'sibling' ? local : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +var<workgroup> u: u32; + +${module_shadow} + +fn sibling() { + ${sibling_func} +} + +@compute @workgroup_size(1) +fn main() { + ${func} + ${data.src} +} + `; + + const pass = t.params.inject === 'none' || t.params.inject === 'sibling'; + t.expectCompileResult(pass, code); + }); + +const kAccessModeTests = { + read: { + keyword: `read`, + src: `var<storage, read> a: i32;`, + }, + read_write: { + keyword: `read_write`, + src: `var<storage, read_write> a: i32;`, + }, + write: { + keyword: `write`, + src: `var t: texture_storage_1d<rgba8unorm, write>;`, + }, +}; + +g.test('shadow_hides_access_mode') + .desc(`Test that shadows hide access modes.`) + .params(u => + u + .combine('inject', ['none', 'function', 'module'] as const) + .beginSubcases() + .combine('builtin', keysOf(kAccessModeTests)) + ) + .fn(t => { + const data = kAccessModeTests[t.params.builtin]; + const local = `let ${data.keyword} = 4;`; + + const module_shadow = t.params.inject === 'module' ? `var<private> ${data.keyword} : i32;` : ``; + const func = t.params.inject === 'function' ? local : ``; + + const code = ` +${module_shadow} + +@group(0) @binding(0) ${data.src} + +@compute @workgroup_size(1) +fn main() { + ${func} +} + `; + + const pass = t.params.inject === 'none' || t.params.inject === 'function'; + t.expectCompileResult(pass, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/statement_behavior.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/statement_behavior.spec.ts new file mode 100644 index 0000000000..1acfd01d8b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/statement_behavior.spec.ts @@ -0,0 +1,143 @@ +export const description = ` +Test statement behavior analysis. + +Functions must have a behavior of {Return}, {Next}, or {Return, Next}. +Functions with a return type must have a behavior of {Return}. + +Each statement in the function must be valid according to the table. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kInvalidStatements = { + break: `break`, + break_if: `break if true`, + continue: `continue`, + loop1: `loop { }`, + loop2: `loop { continuing { } }`, + loop3: `loop { continue; continuing { } }`, + loop4: `loop { continuing { break; } }`, + loop5: `loop { continuing { continue; } }`, + loop6: `loop { continuing { return; } }`, + loop7: `loop { continue; break; }`, + loop8: `loop { continuing { break if true; return; } }`, + for1: `for (;;) { }`, + for2: `for (var i = 0; ; i++) { }`, + for3: `for (;; break) { }`, + for4: `for (;; continue ) { }`, + for5: `for (;; return ) { }`, + for6: `for (;;) { continue; break; }`, + // while loops always have break in their behaviors. + switch1: `switch (1) { case 1 { } }`, + sequence1: `return; loop { }`, + compound1: `{ loop { } }`, +}; + +g.test('invalid_statements') + .desc('Test statements with invalid behaviors') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#behaviors-rules') + .params(u => u.combine('body', keysOf(kInvalidStatements))) + .fn(t => { + const body = kInvalidStatements[t.params.body]; + const code = `fn foo() { + ${body}; + }`; + t.expectCompileResult(false, code); + }); + +const kValidStatements = { + empty: ``, + const_assert: `const_assert true`, + let: `let x = 1`, + var1: `var x = 1`, + var2: `var x : i32`, + assign: `v = 1`, + phony_assign: `_ = 1`, + compound_assign: `v += 1`, + return: `return`, + discard: `discard`, + function_call1: `bar()`, + function_call2: `workgroupBarrier()`, + + if1: `if true { } else { }`, + if2: `if true { }`, + + break1: `loop { break; }`, + break2: `loop { if false { break; } }`, + break_if: `loop { continuing { break if false; } }`, + + continue1: `loop { continue; continuing { break if true; } }`, + + loop1: `loop { break; }`, + loop2: `loop { break; continuing { } }`, + loop3: `loop { continue; continuing { break if true; } }`, + loop4: `loop { break; continue; }`, + + for1: `for (; true; ) { }`, + for2: `for (;;) { break; }`, + for3: `for (;true;) { continue; }`, + + while1: `while true { }`, + while2: `while true { continue; }`, + while3: `while true { continue; break; }`, + + switch1: `switch 1 { default { } }`, + swtich2: `switch 1 { case 1 { } default { } }`, + switch3: `switch 1 { default { break; } }`, + switch4: `switch 1 { default { } case 1 { break; } }`, + + sequence1: `return; let x = 1`, + sequence2: `if true { } let x = 1`, + sequence3: `switch 1 { default { break; return; } }`, + + compound1: `{ }`, + compound2: `{ loop { break; } if true { return; } }`, +}; + +g.test('valid_statements') + .desc('Test statements with valid behaviors') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#behaviors-rules') + .params(u => u.combine('body', keysOf(kValidStatements))) + .fn(t => { + const body = kValidStatements[t.params.body]; + const code = ` + var<private> v : i32; + fn bar() { } + fn foo() { + ${body}; + }`; + t.expectCompileResult(true, code); + }); + +const kInvalidFunctions = { + next_for_type: `fn foo() -> bool { }`, + next_return_for_type: `fn foo() -> bool { if true { return true; } }`, +}; + +g.test('invalid_functions') + .desc('Test functions with invalid behaviors') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#behaviors-rules') + .params(u => u.combine('function', keysOf(kInvalidFunctions))) + .fn(t => { + const func = kInvalidFunctions[t.params.function]; + t.expectCompileResult(false, func); + }); + +const kValidFunctions = { + empty: `fn foo() { }`, + next_return: `fn foo() { if true { return; } }`, + no_final_return: `fn foo() -> bool { if true { return true; } else { return false; } }`, +}; + +g.test('valid_functions') + .desc('Test functions with valid behaviors') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#behaviors-rules') + .params(u => u.combine('function', keysOf(kValidFunctions))) + .fn(t => { + const func = kValidFunctions[t.params.function]; + t.expectCompileResult(true, func); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/binding.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/binding.spec.ts index 2462025016..ae1b78d931 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/binding.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/binding.spec.ts @@ -124,17 +124,3 @@ var<storage> a: i32; }`; t.expectCompileResult(false, code); }); - -g.test('binding_without_group') - .desc(`Test validation of binding without group`) - .fn(t => { - const code = ` -@binding(1) -var<storage> a: i32; - -@workgroup_size(1, 1, 1) -@compute fn main() { - _ = a; -}`; - t.expectCompileResult(false, code); - }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts index 4b32d05539..d99eb82279 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts @@ -103,9 +103,9 @@ g.test('type') .params(u => u .combineWithParams(kBuiltins) - .combine('use_struct', [true, false] as const) - .combine('target_type', kTestTypes) .beginSubcases() + .combine('target_type', kTestTypes) + .combine('use_struct', [true, false] as const) ) .fn(t => { let code = ''; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group.spec.ts index 4d37c43a99..cdbf64201a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group.spec.ts @@ -124,17 +124,3 @@ var<storage> a: i32; }`; t.expectCompileResult(false, code); }); - -g.test('group_without_binding') - .desc(`Test validation of group without binding`) - .fn(t => { - const code = ` -@group(1) -var<storage> a: i32; - -@workgroup_size(1, 1, 1) -@compute fn main() { - _ = a; -}`; - t.expectCompileResult(false, code); - }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group_and_binding.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group_and_binding.spec.ts index 08b4b2738a..5a4168267b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group_and_binding.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/group_and_binding.spec.ts @@ -95,12 +95,12 @@ g.test('single_entry_point') .combine('stage', ['vertex', 'fragment', 'compute'] as const) .combine('a_kind', kResourceKindsA) .combine('b_kind', kResourceKindsB) + .combine('usage', ['direct', 'transitive'] as const) + .beginSubcases() .combine('a_group', [0, 3] as const) .combine('b_group', [0, 3] as const) .combine('a_binding', [0, 3] as const) .combine('b_binding', [0, 3] as const) - .combine('usage', ['direct', 'transitive'] as const) - .beginSubcases() ) .fn(t => { const resourceA = kResourceEmitters.get(t.params.a_kind) as ResourceDeclarationEmitter; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/layout_constraints.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/layout_constraints.spec.ts new file mode 100644 index 0000000000..7db1eefbe0 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/layout_constraints.spec.ts @@ -0,0 +1,543 @@ +export const description = `Validation of address space layout constraints`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +interface LayoutCase { + type: string; + decls?: string; + validity: boolean | 'non-uniform' | 'non-interface' | 'storage' | 'atomic'; + f16?: boolean; +} + +const kLayoutCases: Record<string, LayoutCase> = { + // Scalars + u32: { + type: 'u32', + validity: true, + }, + i32: { + type: 'i32', + validity: true, + }, + f32: { + type: 'f32', + validity: true, + }, + f16: { + type: 'f16', + validity: true, + f16: true, + }, + bool: { + type: 'bool', + validity: 'non-interface', + }, + + // Vectors + vec2u: { + type: 'vec2u', + validity: true, + }, + vec3u: { + type: 'vec3u', + validity: true, + }, + vec4u: { + type: 'vec4u', + validity: true, + }, + vec2i: { + type: 'vec2i', + validity: true, + }, + vec3i: { + type: 'vec3i', + validity: true, + }, + vec4i: { + type: 'vec4i', + validity: true, + }, + vec2f: { + type: 'vec2f', + validity: true, + }, + vec3f: { + type: 'vec3f', + validity: true, + }, + vec4f: { + type: 'vec4f', + validity: true, + }, + vec2h: { + type: 'vec2h', + validity: true, + f16: true, + }, + vec3h: { + type: 'vec3h', + validity: true, + f16: true, + }, + vec4h: { + type: 'vec4h', + validity: true, + f16: true, + }, + vec2b: { + type: 'vec2<bool>', + validity: 'non-interface', + }, + vec3b: { + type: 'vec3<bool>', + validity: 'non-interface', + }, + vec4b: { + type: 'vec4<bool>', + validity: 'non-interface', + }, + + // Matrices + mat2x2f: { + type: 'mat2x2f', + validity: true, + }, + mat2x3f: { + type: 'mat2x3f', + validity: true, + }, + mat2x4f: { + type: 'mat2x4f', + validity: true, + }, + mat3x2f: { + type: 'mat3x2f', + validity: true, + }, + mat3x3f: { + type: 'mat3x3f', + validity: true, + }, + mat3x4f: { + type: 'mat3x4f', + validity: true, + }, + mat4x2f: { + type: 'mat4x2f', + validity: true, + }, + mat4x3f: { + type: 'mat4x3f', + validity: true, + }, + mat4x4f: { + type: 'mat4x4f', + validity: true, + }, + mat2x2h: { + type: 'mat2x2h', + validity: true, + f16: true, + }, + mat2x3h: { + type: 'mat2x3h', + validity: true, + f16: true, + }, + mat2x4h: { + type: 'mat2x4h', + validity: true, + f16: true, + }, + mat3x2h: { + type: 'mat3x2h', + validity: true, + f16: true, + }, + mat3x3h: { + type: 'mat3x3h', + validity: true, + f16: true, + }, + mat3x4h: { + type: 'mat3x4h', + validity: true, + f16: true, + }, + mat4x2h: { + type: 'mat4x2h', + validity: true, + f16: true, + }, + mat4x3h: { + type: 'mat4x3h', + validity: true, + f16: true, + }, + mat4x4h: { + type: 'mat4x4h', + validity: true, + f16: true, + }, + + // Atomics + atomic_u32: { + type: 'atomic<u32>', + validity: 'atomic', + }, + atomic_i32: { + type: 'atomic<i32>', + validity: 'atomic', + }, + + // Sized arrays + array_u32: { + type: 'array<u32, 16>', + validity: 'non-uniform', + }, + array_i32: { + type: 'array<i32, 16>', + validity: 'non-uniform', + }, + array_f32: { + type: 'array<f32, 16>', + validity: 'non-uniform', + }, + array_f16: { + type: 'array<f16, 16>', + validity: 'non-uniform', + f16: true, + }, + array_bool: { + type: 'array<bool, 16>', + validity: 'non-interface', + }, + array_vec2f: { + type: 'array<vec2f, 16>', + validity: 'non-uniform', + }, + array_vec3f: { + type: 'array<vec3f, 16>', + validity: true, + }, + array_vec4f: { + type: 'array<vec4f, 16>', + validity: true, + }, + array_vec2h: { + type: 'array<vec2h, 16>', + validity: 'non-uniform', + f16: true, + }, + array_vec3h: { + type: 'array<vec3h, 16>', + validity: 'non-uniform', + f16: true, + }, + array_vec4h: { + type: 'array<vec4h, 16>', + validity: 'non-uniform', + f16: true, + }, + array_vec2b: { + type: 'array<vec2<bool>, 16>', + validity: 'non-interface', + }, + array_vec3b: { + type: 'array<vec3<bool>, 16>', + validity: 'non-interface', + }, + array_vec4b: { + type: 'array<vec4<bool>, 16>', + validity: 'non-interface', + }, + array_mat2x2f: { + type: 'array<mat2x2f, 16>', + validity: true, + }, + array_mat2x4f: { + type: 'array<mat2x4f, 16>', + validity: true, + }, + array_mat4x2f: { + type: 'array<mat4x2f, 16>', + validity: true, + }, + array_mat4x4f: { + type: 'array<mat4x4f, 16>', + validity: true, + }, + array_mat2x2h: { + type: 'array<mat2x2h, 16>', + validity: 'non-uniform', + f16: true, + }, + array_mat2x4h: { + type: 'array<mat2x4h, 16>', + validity: true, + f16: true, + }, + array_mat3x2h: { + type: 'array<mat3x2h, 16>', + validity: 'non-uniform', + f16: true, + }, + array_mat4x2h: { + type: 'array<mat4x2h, 16>', + validity: true, + f16: true, + }, + array_mat4x4h: { + type: 'array<mat4x4h, 16>', + validity: true, + f16: true, + }, + array_atomic: { + type: 'array<atomic<u32>, 16>', + validity: 'atomic', + }, + + // Runtime arrays + runtime_array_u32: { + type: 'array<u32>', + validity: 'storage', + }, + runtime_array_i32: { + type: 'array<i32>', + validity: 'storage', + }, + runtime_array_f32: { + type: 'array<f32>', + validity: 'storage', + }, + runtime_array_f16: { + type: 'array<f16>', + validity: 'storage', + f16: true, + }, + runtime_array_bool: { + type: 'array<bool>', + validity: false, + }, + runtime_array_vec2f: { + type: 'array<vec2f>', + validity: 'storage', + }, + runtime_array_vec3f: { + type: 'array<vec3f>', + validity: 'storage', + }, + runtime_array_vec4f: { + type: 'array<vec4f>', + validity: 'storage', + }, + runtime_array_vec2h: { + type: 'array<vec2h>', + validity: 'storage', + f16: true, + }, + runtime_array_vec3h: { + type: 'array<vec3h>', + validity: 'storage', + f16: true, + }, + runtime_array_vec4h: { + type: 'array<vec4h>', + validity: 'storage', + f16: true, + }, + runtime_array_vec2b: { + type: 'array<vec2<bool>>', + validity: false, + }, + runtime_array_vec3b: { + type: 'array<vec3<bool>>', + validity: false, + }, + runtime_array_vec4b: { + type: 'array<vec4<bool>>', + validity: false, + }, + runtime_array_mat2x2f: { + type: 'array<mat2x2f>', + validity: 'storage', + }, + runtime_array_mat2x4f: { + type: 'array<mat2x4f>', + validity: 'storage', + }, + runtime_array_mat4x2f: { + type: 'array<mat4x2f>', + validity: 'storage', + }, + runtime_array_mat4x4f: { + type: 'array<mat4x4f>', + validity: 'storage', + }, + runtime_array_mat2x2h: { + type: 'array<mat2x2h>', + validity: 'storage', + f16: true, + }, + runtime_array_mat2x4h: { + type: 'array<mat2x4h>', + validity: 'storage', + f16: true, + }, + runtime_array_mat3x2h: { + type: 'array<mat3x2h>', + validity: 'storage', + f16: true, + }, + runtime_array_mat4x2h: { + type: 'array<mat4x2h>', + validity: 'storage', + f16: true, + }, + runtime_array_mat4x4h: { + type: 'array<mat4x4h>', + validity: 'storage', + f16: true, + }, + runtime_array_atomic: { + type: 'array<atomic<u32>>', + validity: 'storage', + }, + + // Structs (and arrays of structs) + array_struct_u32: { + type: 'array<S, 16>', + decls: 'struct S { x : u32 }', + validity: 'non-uniform', + }, + array_struct_u32_size16: { + type: 'array<S, 16>', + decls: 'struct S { @size(16) x : u32 }', + validity: true, + }, + array_struct_vec2f: { + type: 'array<S, 16>', + decls: 'struct S { x : vec2f }', + validity: 'non-uniform', + }, + array_struct_vec2h: { + type: 'array<S, 16>', + decls: 'struct S { x : vec2h }', + validity: 'non-uniform', + f16: true, + }, + array_struct_vec2h_align16: { + type: 'array<S, 16>', + decls: 'struct S { @align(16) x : vec2h }', + validity: true, + f16: true, + }, + size_too_small: { + type: 'S', + decls: 'struct S { @size(2) x : u32 }', + validity: false, + }, + struct_padding: { + type: 'S', + decls: `struct T { x : u32 } + struct S { t : T, x : u32 }`, + validity: 'non-uniform', + }, + struct_array_u32: { + type: 'S', + decls: 'struct S { x : array<u32, 4> }', + validity: 'non-uniform', + }, + struct_runtime_array_u32: { + type: 'S', + decls: 'struct S { x : array<u32> }', + validity: 'storage', + }, + array_struct_size_5: { + type: 'array<S, 16>', + decls: 'struct S { @size(5) x : u32, y : u32 }', + validity: 'non-uniform', + }, + array_struct_size_5x2: { + type: 'array<S, 16>', + decls: 'struct S { @size(5) x : u32, @size(5) y : u32 }', + validity: true, + }, + struct_size_5: { + type: 'S', + decls: `struct T { @size(5) x : u32 } + struct S { x : u32, y : T }`, + validity: 'non-uniform', + }, + struct_size_5_align16: { + type: 'S', + decls: `struct T { @align(16) @size(5) x : u32 } + struct S { x : u32, y : T }`, + validity: true, + }, +}; + +g.test('layout_constraints') + .desc('Test address space layout constraints') + .params(u => + u + .combine('case', keysOf(kLayoutCases)) + .beginSubcases() + .combine('aspace', ['storage', 'uniform', 'function', 'private', 'workgroup'] as const) + ) + .beforeAllSubcases(t => { + const testcase = kLayoutCases[t.params.case]; + if (testcase.f16) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const testcase = kLayoutCases[t.params.case]; + const decls = testcase.decls !== undefined ? testcase.decls : ''; + let code = ` +${testcase.f16 ? 'enable f16;' : ''} +${decls} + +`; + + switch (t.params.aspace) { + case 'storage': + code += `@group(0) @binding(0) var<storage, read_write> v : ${testcase.type};\n`; + break; + case 'uniform': + code += `@group(0) @binding(0) var<uniform> v : ${testcase.type};\n`; + break; + case 'workgroup': + code += `var<workgroup> v : ${testcase.type};\n`; + break; + case 'private': + code += `var<private> v : ${testcase.type};\n`; + break; + default: + break; + } + + code += `@compute @workgroup_size(1,1,1) + fn main() { + `; + + if (t.params.aspace === 'function') { + code += `var v : ${testcase.type};\n`; + } + code += `}\n`; + + const is_interface = t.params.aspace === 'uniform' || t.params.aspace === 'storage'; + const supports_atomic = t.params.aspace === 'storage' || t.params.aspace === 'workgroup'; + const expect = + testcase.validity === true || + (testcase.validity === 'non-uniform' && t.params.aspace !== 'uniform') || + (testcase.validity === 'non-interface' && !is_interface) || + (testcase.validity === 'storage' && t.params.aspace === 'storage') || + (testcase.validity === 'atomic' && supports_atomic); + t.expectCompileResult(expect, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts index 8452679d71..3c41f1a8b5 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts @@ -380,3 +380,152 @@ g.test('location_fp16') }`; t.expectCompileResult(t.params.ext === '', code); }); + +interface OutOfOrderCase { + params?: string; + returnType?: string; + decls?: string; + returnValue?: string; + valid: boolean; +} + +const kOutOfOrderCases: Record<string, OutOfOrderCase> = { + reverse_params: { + params: `@location(2) p1 : f32, @location(1) p2 : f32, @location(0) p3 : f32`, + valid: true, + }, + no_zero_params: { + params: `@location(2) p1 : f32, @location(1) p2 : f32`, + valid: true, + }, + reverse_overlap: { + params: `@location(2) p1 : f32, @location(1) p2 : f32, @location(1) p3 : f32`, + valid: false, + }, + struct: { + params: `p1 : S`, + decls: `struct S { + @location(1) x : f32, + @location(0) y : f32, + }`, + valid: true, + }, + struct_override: { + params: `@location(0) p1 : S`, + decls: `struct S { + @location(1) x : f32, + @location(0) y : f32, + }`, + valid: false, + }, + struct_random: { + params: `p1 : S, p2 : T`, + decls: `struct S { + @location(16) x : f32, + @location(4) y : f32, + } + struct T { + @location(13) x : f32, + @location(7) y : f32, + }`, + valid: true, + }, + struct_random_overlap: { + params: `p1 : S, p2 : T`, + decls: `struct S { + @location(16) x : f32, + @location(4) y : f32, + } + struct T { + @location(13) x : f32, + @location(4) y : f32, + }`, + valid: false, + }, + mixed_locations1: { + params: `@location(12) p1 : f32, p2 : S`, + decls: `struct S { + @location(2) x : f32, + }`, + valid: true, + }, + mixed_locations2: { + params: `p1 : S, @location(2) p2 : f32`, + decls: `struct S { + @location(12) x : f32, + }`, + valid: true, + }, + mixed_overlap: { + params: `p1 : S, @location(12) p2 : f32`, + decls: `struct S { + @location(12) x : f32, + }`, + valid: false, + }, + with_param_builtin: { + params: `p : S`, + decls: `struct S { + @location(12) x : f32, + @builtin(position) pos : vec4f, + @location(0) y : f32, + }`, + valid: true, + }, + non_zero_return: { + returnType: `@location(1) vec4f`, + returnValue: `vec4f()`, + valid: true, + }, + reverse_return: { + returnType: `S`, + returnValue: `S()`, + decls: `struct S { + @location(2) x : f32, + @location(1) y : f32, + @location(0) z : f32, + }`, + valid: true, + }, + gap_return: { + returnType: `S`, + returnValue: `S()`, + decls: `struct S { + @location(13) x : f32, + @location(7) y : f32, + @location(2) z : f32, + }`, + valid: true, + }, + with_return_builtin: { + returnType: `S`, + returnValue: `S()`, + decls: `struct S { + @location(11) x : f32, + @builtin(frag_depth) d : f32, + @location(10) y : f32, + }`, + valid: true, + }, +}; + +g.test('out_of_order') + .desc(`Test validation of out of order locations`) + .params(u => u.combine('case', keysOf(kOutOfOrderCases))) + .fn(t => { + const testcase = kOutOfOrderCases[t.params.case]; + const decls = testcase.decls !== undefined ? testcase.decls : ``; + const params = testcase.params !== undefined ? testcase.params : ``; + const returnType = testcase.returnType !== undefined ? `-> ${testcase.returnType}` : ``; + const returnValue = testcase.returnValue !== undefined ? `return ${testcase.returnValue};` : ``; + const code = ` +${decls} + +@fragment +fn main(${params}) ${returnType} { + ${returnValue} +} +`; + + t.expectCompileResult(testcase.valid, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/size.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/size.spec.ts index f81dde4a1d..564c3f2b7c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/size.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/size.spec.ts @@ -103,7 +103,7 @@ const kSizeTests = { }; g.test('size') - .desc(`Test validation of ize`) + .desc(`Test validation of size`) .params(u => u.combine('attr', keysOf(kSizeTests))) .fn(t => { const code = ` @@ -210,3 +210,21 @@ g.test('size_non_struct') t.expectCompileResult(data.pass, code); }); + +g.test('size_creation_fixed_footprint') + .desc(`Test that @size is only valid on types that have creation-fixed footprint.`) + .params(u => u.combine('array_size', [', 4', ''])) + .fn(t => { + const code = ` +struct S { + @size(64) a: array<f32${t.params.array_size}>, +}; +@group(0) @binding(0) +var<storage> a: S; + +@workgroup_size(1) +@compute fn main() { + _ = a.a[0]; +}`; + t.expectCompileResult(t.params.array_size !== '', code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/alias.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/alias.spec.ts index 266b4f9a12..59e8607a5f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/alias.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/alias.spec.ts @@ -3,6 +3,7 @@ Validation tests for type aliases `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; import { ShaderValidationTest } from '../shader_validation_test.js'; export const g = makeTestGroup(ShaderValidationTest); @@ -121,3 +122,124 @@ alias T = ${t.params.target}; `; t.expectCompileResult(t.params.target === 'i32', wgsl); }); + +const kTypes = [ + 'bool', + 'i32', + 'u32', + 'f32', + 'f16', + 'vec2<i32>', + 'vec3<u32>', + 'vec4<f32>', + 'mat2x2<f32>', + 'mat2x3<f32>', + 'mat2x4<f32>', + 'mat3x2<f32>', + 'mat3x3<f32>', + 'mat3x4<f32>', + 'mat4x2<f32>', + 'mat4x3<f32>', + 'mat4x4<f32>', + 'array<u32>', + 'array<i32, 4>', + 'array<vec2<u32>, 8>', + 'S', + 'T', + 'atomic<u32>', + 'atomic<i32>', + 'ptr<function, u32>', + 'ptr<private, i32>', + 'ptr<workgroup, f32>', + 'ptr<uniform, vec2f>', + 'ptr<storage, vec2u>', + 'ptr<storage, vec3i, read>', + 'ptr<storage, vec4f, read_write>', + 'sampler', + 'sampler_comparison', + 'texture_1d<f32>', + 'texture_2d<u32>', + 'texture_2d_array<i32>', + 'texture_3d<f32>', + 'texture_cube<i32>', + 'texture_cube_array<u32>', + 'texture_multisampled_2d<f32>', + 'texture_depth_multisampled_2d', + 'texture_external', + 'texture_storage_1d<rgba8snorm, write>', + 'texture_storage_1d<r32uint, write>', + 'texture_storage_1d<r32sint, read_write>', + 'texture_storage_1d<r32float, read>', + 'texture_storage_2d<rgba16uint, write>', + 'texture_storage_2d_array<rg32float, write>', + 'texture_storage_3d<bgra8unorm, write>', + 'texture_depth_2d', + 'texture_depth_2d_array', + 'texture_depth_cube', + 'texture_depth_cube_array', + + // Pre-declared aliases (spot check) + 'vec2f', + 'vec3u', + 'vec4i', + 'mat2x2f', + + // User-defined aliases + 'anotherAlias', + 'random_alias', +]; + +g.test('any_type') + .desc('Test that any type can be aliased') + .params(u => u.combine('type', kTypes)) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const ty = t.params.type; + t.skipIf( + ty.includes('texture_storage') && + ty.includes('read') && + !t.hasLanguageFeature('readonly_and_readwrite_storage_textures'), + 'Missing language feature' + ); + const enable = ty === 'f16' ? 'enable f16;' : ''; + const code = ` + ${enable} + struct S { x : u32 } + struct T { y : S } + alias anotherAlias = u32; + alias random_alias = i32; + alias myType = ${ty};`; + t.expectCompileResult(true, code); + }); + +const kMatchCases = { + function_param: ` + fn foo(x : u32) { } + fn bar() { + var x : alias_alias_u32; + foo(x); + }`, + constructor: `var<private> v : u32 = alias_u32(1);`, + template_param: `var<private> v : vec2<alias_u32> = vec2<u32>();`, + predeclared_alias: `var<private> v : vec2<alias_alias_u32> = vec2u();`, + struct_element: ` + struct S { x : alias_u32 } + const c_u32 = 0u; + const c = S(c_u32);`, +}; + +g.test('match_non_alias') + .desc('Test that type checking succeeds using aliased and unaliased type') + .params(u => u.combine('case', keysOf(kMatchCases))) + .fn(t => { + const testcase = kMatchCases[t.params.case]; + const code = ` + alias alias_u32 = u32; + alias alias_alias_u32 = alias_u32; + ${testcase}`; + t.expectCompileResult(true, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/array.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/array.spec.ts new file mode 100644 index 0000000000..636906f52e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/array.spec.ts @@ -0,0 +1,122 @@ +export const description = ` +Validation tests for array types +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidCases = { + // Basic element types. + i32: `alias T = array<i32>;`, + u32: `alias T = array<u32>;`, + f32: `alias T = array<f32>;`, + f16: `enable f16;\nalias T = array<f16>;`, + bool: `alias T = array<bool>;`, + + // Composite elements + vec2u: `alias T = array<vec2u>;`, + vec3i: `alias T = array<vec3i>;`, + vec4f: `alias T = array<vec4f>;`, + array: `alias T = array<array<u32, 4>>;`, + struct: `struct S { x : u32 }\nalias T = array<S>;`, + mat2x2f: `alias T = array<mat2x2f>;`, + mat4x4h: `enable f16;\nalias T = array<mat4x4h>;`, + + // Atomic elements + atomicu: `alias T = array<atomic<u32>>;`, + atomici: `alias T = array<atomic<i32>>;`, + + // Count expressions + literal_count: `alias T = array<u32, 4>;`, + literali_count: `alias T = array<u32, 4i>;`, + literalu_count: `alias T = array<u32, 4u>;`, + const_count: `const x = 8;\nalias T = array<u32, x>;`, + const_expr_count1: `alias T = array<u32, 1 + 3>;`, + const_expr_count2: `const x = 4;\nalias T = array<u32, x * 2>;`, + override_count: `override x : u32;\nalias T = array<u32, x>;`, + override_expr1: `override x = 2;\nalias T = array<u32, vec2(x,x).x>;`, + override_expr2: `override x = 1;\nalias T = array<u32, x + 1>;`, + override_zero: `override x = 0;\nalias T = array<u32, x>;`, + override_neg: `override x = -1;\nalias T = array<u32, x>;`, + + // Same array types + same_const_value1: ` + const x = 8; + const y = 8; + var<private> v : array<u32, x> = array<u32, y>();`, + same_const_value2: ` + const x = 8; + var<private> v : array<u32, x> = array<u32, 8>();`, + same_const_value3: ` + var<private> v : array<u32, 8i> = array<u32, 8u>();`, + same_override: ` + requires unrestricted_pointer_parameters; + override x : u32; + var<workgroup> v : array<u32, x>; + fn bar(p : ptr<workgroup, array<u32, x>>) { } + fn foo() { bar(&v); }`, + + // Shadow + shadow: `alias array = vec2f;`, +}; + +g.test('valid') + .desc('Valid array type tests') + .params(u => u.combine('case', keysOf(kValidCases))) + .beforeAllSubcases(t => { + const code = kValidCases[t.params.case]; + if (code.indexOf('f16') >= 0) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = kValidCases[t.params.case]; + t.skipIf( + code.indexOf('unrestricted') >= 0 && !t.hasLanguageFeature('unrestricted_pointer_parameters'), + 'Test requires unrestricted_pointer_parameters' + ); + t.expectCompileResult(true, code); + }); + +const kInvalidCases = { + f16_without_enable: `alias T = array<f16>;`, + runtime_nested: `alias T = array<array<u32>, 4>;`, + override_nested: ` + override x : u32; + alias T = array<array<u32, x>, 4>;`, + override_nested_struct: ` + override x : u32; + struct T { x : array<u32, x> }`, + zero_size: `alias T = array<u32, 0>;`, + negative_size: `alias T = array<u32, 1 - 2>;`, + const_zero: `const x = 0;\nalias T = array<u32, x>;`, + const_neg: `const x = 1;\nconst y = 2;\nalias T = array<u32, x - y>;`, + incompatible_overrides: ` + requires unrestricted_pointer_parameters; + override x = 8; + override y = 8; + var<workgroup> v : array<u32, x> + fn bar(p : ptr<workgroup, array<u32 y>>) { } + fn foo() { bar(&v); }`, +}; + +g.test('invalid') + .desc('Invalid array type tests') + .params(u => u.combine('case', keysOf(kInvalidCases))) + .beforeAllSubcases(t => { + const code = kInvalidCases[t.params.case]; + if (code.indexOf('f16') >= 0) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = kInvalidCases[t.params.case]; + t.skipIf( + code.indexOf('unrestricted') >= 0 && !t.hasLanguageFeature('unrestricted_pointer_parameters'), + 'Test requires unrestricted_pointer_parameters' + ); + t.expectCompileResult(false, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/atomics.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/atomics.spec.ts new file mode 100644 index 0000000000..36c37176e8 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/atomics.spec.ts @@ -0,0 +1,145 @@ +export const description = ` +Validation tests for atomic types + +Tests covered: +* Base type +* Address spaces +* Invalid operations (non-exhaustive) + +Note: valid operations (e.g. atomic built-in functions) are tested in the builtin tests. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('type') + .desc('Test of the underlying atomic data type') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#atomic-types') + .params(u => + u.combine('type', [ + 'u32', + 'i32', + 'f32', + 'f16', + 'bool', + 'vec2u', + 'vec3i', + 'vec4f', + 'mat2x2f', + 'R', + 'S', + 'array<u32, 1>', + 'array<i32, 4>', + 'array<u32>', + 'array<i32>', + 'atomic<u32>', + 'atomic<i32>', + ] as const) + ) + .beforeAllSubcases(t => { + if (t.params.type === 'f16') { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = ` +struct S { + x : u32 +} +struct T { + x : i32 +} +struct R { + x : f32 +} + +struct Test { + x : atomic<${t.params.type}> +} +`; + + const expect = t.params.type === 'u32' || t.params.type === 'i32'; + t.expectCompileResult(expect, code); + }); + +g.test('address_space') + .desc('Test allowed address spaces for atomics') + .specURL('https://gpuweb.github.io/gpuweb/wgsl/#atomic-types') + .params(u => + u + .combine('aspace', [ + 'storage', + 'workgroup', + 'storage-ro', + 'uniform', + 'private', + 'function', + 'function-let', + ] as const) + .beginSubcases() + .combine('type', ['i32', 'u32'] as const) + ) + .fn(t => { + let moduleVar = ``; + let functionVar = ''; + switch (t.params.aspace) { + case 'storage-ro': + moduleVar = `@group(0) @binding(0) var<storage> x : atomic<${t.params.type}>;\n`; + break; + case 'storage': + moduleVar = `@group(0) @binding(0) var<storage, read_write> x : atomic<${t.params.type}>;\n`; + break; + case 'uniform': + moduleVar = `@group(0) @binding(0) var<uniform> x : atomic<${t.params.type}>;\n`; + break; + case 'workgroup': + case 'private': + moduleVar = `var<${t.params.aspace}> x : atomic<${t.params.type}>;\n`; + break; + case 'function': + functionVar = `var x : atomic<${t.params.type}>;\n`; + break; + case 'function-let': + functionVar = `let x : atomic<${t.params.type}>;\n`; + break; + } + const code = ` +${moduleVar} + +fn foo() { + ${functionVar} +} +`; + + const expect = t.params.aspace === 'storage' || t.params.aspace === 'workgroup'; + t.expectCompileResult(expect, code); + }); + +const kInvalidOperations = { + add: `a1 + a2`, + load: `a1`, + store: `a1 = 1u`, + deref: `*a1 = 1u`, + equality: `a1 == a2`, + abs: `abs(a1)`, + address_abs: `abs(&a1)`, +}; + +g.test('invalid_operations') + .desc('Tests that a selection of invalid operations are invalid') + .params(u => u.combine('op', keysOf(kInvalidOperations))) + .fn(t => { + const code = ` +var<workgroup> a1 : atomic<u32>; +var<workgroup> a2 : atomic<u32>; + +fn foo() { + let x : u32 = ${kInvalidOperations[t.params.op]}; +} +`; + + t.expectCompileResult(false, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/matrix.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/matrix.spec.ts new file mode 100644 index 0000000000..02458c6c88 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/matrix.spec.ts @@ -0,0 +1,152 @@ +export const description = ` +Validation tests for matrix types +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { keysOf } from '../../../../common/util/data_tables.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +const kValidCases = { + // Basic matrices + mat2x2_f32: `alias T = mat2x2<f32>;`, + mat2x3_f32: `alias T = mat2x3<f32>;`, + mat2x4_f32: `alias T = mat2x4<f32>;`, + mat3x2_f32: `alias T = mat3x2<f32>;`, + mat3x3_f32: `alias T = mat3x3<f32>;`, + mat3x4_f32: `alias T = mat3x4<f32>;`, + mat4x2_f32: `alias T = mat4x2<f32>;`, + mat4x3_f32: `alias T = mat4x3<f32>;`, + mat4x4_f32: `alias T = mat4x4<f32>;`, + mat2x2_f16: `enable f16;\nalias T = mat2x2<f16>;`, + mat2x3_f16: `enable f16;\nalias T = mat2x3<f16>;`, + mat2x4_f16: `enable f16;\nalias T = mat2x4<f16>;`, + mat3x2_f16: `enable f16;\nalias T = mat3x2<f16>;`, + mat3x3_f16: `enable f16;\nalias T = mat3x3<f16>;`, + mat3x4_f16: `enable f16;\nalias T = mat3x4<f16>;`, + mat4x2_f16: `enable f16;\nalias T = mat4x2<f16>;`, + mat4x3_f16: `enable f16;\nalias T = mat4x3<f16>;`, + mat4x4_f16: `enable f16;\nalias T = mat4x4<f16>;`, + + // Pre-declared aliases + mat2x2f: `alias T = mat2x2f;`, + mat2x3f: `alias T = mat2x3f;`, + mat2x4f: `alias T = mat2x4f;`, + mat3x2f: `alias T = mat3x2f;`, + mat3x3f: `alias T = mat3x3f;`, + mat3x4f: `alias T = mat3x4f;`, + mat4x2f: `alias T = mat4x2f;`, + mat4x3f: `alias T = mat4x3f;`, + mat4x4f: `alias T = mat4x4f;`, + mat2x2h: `enable f16;\nalias T = mat2x2h;`, + mat2x3h: `enable f16;\nalias T = mat2x3h;`, + mat2x4h: `enable f16;\nalias T = mat2x4h;`, + mat3x2h: `enable f16;\nalias T = mat3x2h;`, + mat3x3h: `enable f16;\nalias T = mat3x3h;`, + mat3x4h: `enable f16;\nalias T = mat3x4h;`, + mat4x2h: `enable f16;\nalias T = mat4x2h;`, + mat4x3h: `enable f16;\nalias T = mat4x3h;`, + mat4x4h: `enable f16;\nalias T = mat4x4h;`, + + trailing_comman: `alias T = mat2x2<f32,>;`, + + // Abstract matrices + abstract_2x2: `const m = mat2x2(1,1,1,1);`, + abstract_2x3: `const m = mat2x3(1,1,1,1,1,1);`, + abstract_2x4: `const m = mat2x4(1,1,1,1,1,1,1,1);`, + + // Base roots shadowable + shadow_mat2x2: `alias mat2x2 = array<vec2f, 2>;`, + shadow_mat2x3: `alias mat2x3 = array<vec2f, 3>;`, + shadow_mat2x4: `alias mat2x4 = array<vec2f, 4>;`, + shadow_mat3x2: `alias mat3x2 = array<vec3f, 2>;`, + shadow_mat3x3: `alias mat3x3 = array<vec3f, 3>;`, + shadow_mat3x4: `alias mat3x4 = array<vec3f, 4>;`, + shadow_mat4x2: `alias mat4x2 = array<vec4f, 2>;`, + shadow_mat4x3: `alias mat4x3 = array<vec4f, 3>;`, + shadow_mat4x4: `alias mat4x4 = array<vec4f, 4>;`, + + // Pre-declared aliases shadowable + shadow_mat2x2f: `alias mat2x2f = mat2x2<f32>;`, + shadow_mat2x3f: `alias mat2x3f = mat2x3<f32>;`, + shadow_mat2x4f: `alias mat2x4f = mat2x4<f32>;`, + shadow_mat3x2f: `alias mat3x2f = mat3x2<f32>;`, + shadow_mat3x3f: `alias mat3x3f = mat3x3<f32>;`, + shadow_mat3x4f: `alias mat3x4f = mat3x4<f32>;`, + shadow_mat4x2f: `alias mat4x2f = mat4x2<f32>;`, + shadow_mat4x3f: `alias mat4x3f = mat4x3<f32>;`, + shadow_mat4x4f: `alias mat4x4f = mat4x4<f32>;`, + shadow_mat2x2h: `enable f16;\nalias mat2x2h = mat2x2<f16>;`, + shadow_mat2x3h: `enable f16;\nalias mat2x3h = mat2x3<f16>;`, + shadow_mat2x4h: `enable f16;\nalias mat2x4h = mat2x4<f16>;`, + shadow_mat3x2h: `enable f16;\nalias mat3x2h = mat3x2<f16>;`, + shadow_mat3x3h: `enable f16;\nalias mat3x3h = mat3x3<f16>;`, + shadow_mat3x4h: `enable f16;\nalias mat3x4h = mat3x4<f16>;`, + shadow_mat4x2h: `enable f16;\nalias mat4x2h = mat4x2<f16>;`, + shadow_mat4x3h: `enable f16;\nalias mat4x3h = mat4x3<f16>;`, + shadow_mat4x4h: `enable f16;\nalias mat4x4h = mat4x4<f16>;`, +}; + +g.test('valid') + .desc('Valid matrix type tests') + .params(u => u.combine('case', keysOf(kValidCases))) + .beforeAllSubcases(t => { + const code = kValidCases[t.params.case]; + if (code.indexOf('f16') >= 0) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = kValidCases[t.params.case]; + t.expectCompileResult(true, code); + }); + +const kInvalidCases = { + // Invalid component types + mat2x2_i32: `alias T = mat2x2<i32>;`, + mat3x3_u32: `alias T = mat3x3<u32>;`, + mat4x4_bool: `alias T = mat4x4<bool>;`, + mat2x2_vec4f: `alias T = mat2x2<vec2f>;`, + mat2x2_array: `alias T = mat2x2<array<f32, 2>>;`, + mat2x2_struct: `struct S { x : f32 }\nalias T = mat2x2<S>;`, + + // Invalid dimensions + mat1x1: `alias T = mat1x1<f32>;`, + mat2x1: `alias T = mat2x1<f32>;`, + mat2x5: `alias T = mat2x5<f32>;`, + mat5x5: `alias T = mat5x5<f32>;`, + + // Half-precision aliases require enable + no_enable_mat2x2h: `alias T = mat2x2h;`, + no_enable_mat2x3h: `alias T = mat2x3h;`, + no_enable_mat2x4h: `alias T = mat2x4h;`, + no_enable_mat3x2h: `alias T = mat3x2h;`, + no_enable_mat3x3h: `alias T = mat3x3h;`, + no_enable_mat3x4h: `alias T = mat3x4h;`, + no_enable_mat4x2h: `alias T = mat4x2h;`, + no_enable_mat4x3h: `alias T = mat4x3h;`, + no_enable_mat4x4h: `alias T = mat4x4h;`, + + missing_template: `alias T = mat2x2;`, + missing_left_template: `alias T = mat2x2f32>;`, + missing_right_template: `alias T = mat2x2<f32;`, + missing_comp: `alias T = mat2x2<>;`, + mat2x2i: `alias T = mat2x2i;`, + mat2x2u: `alias T = mat2x2u;`, + mat2x2b: `alias T = mat2x2b;`, +}; + +g.test('invalid') + .desc('Invalid matrix type tests') + .params(u => u.combine('case', keysOf(kInvalidCases))) + .beforeAllSubcases(t => { + const code = kInvalidCases[t.params.case]; + if (code.indexOf('f16') >= 0) { + t.selectDeviceOrSkipTestCase('shader-f16'); + } + }) + .fn(t => { + const code = kInvalidCases[t.params.case]; + t.expectCompileResult(false, code); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/textures.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/textures.spec.ts new file mode 100644 index 0000000000..59e5b26db6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/types/textures.spec.ts @@ -0,0 +1,170 @@ +export const description = ` +Validation tests for various texture types in shaders. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { + isTextureFormatUsableAsStorageFormat, + kAllTextureFormats, + kColorTextureFormats, + kTextureFormatInfo, +} from '../../../format_info.js'; +import { getPlainTypeInfo } from '../../../util/shader.js'; +import { ShaderValidationTest } from '../shader_validation_test.js'; + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('texel_formats') + .desc( + 'Test channels and channel format of various texel formats when using as the storage texture format' + ) + .params(u => + u + .combine('format', kColorTextureFormats) + .filter(p => kTextureFormatInfo[p.format].color.storage) + .beginSubcases() + .combine('shaderScalarType', ['f32', 'u32', 'i32', 'bool', 'f16'] as const) + ) + .beforeAllSubcases(t => { + if (t.params.shaderScalarType === 'f16') { + t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] }); + } + + if (!isTextureFormatUsableAsStorageFormat(t.params.format, t.isCompatibility)) { + t.skip('storage usage is unsupported'); + } + }) + .fn(t => { + const { format, shaderScalarType } = t.params; + const info = kTextureFormatInfo[format]; + const validShaderScalarType = getPlainTypeInfo(info.color.type); + const shaderValueType = `vec4<${shaderScalarType}>`; + const wgsl = ` + @group(0) @binding(0) var tex: texture_storage_2d<${format}, read>; + @compute @workgroup_size(1) fn main() { + let v : ${shaderValueType} = textureLoad(tex, vec2u(0)); + _ = v; + } +`; + t.expectCompileResult(validShaderScalarType === shaderScalarType, wgsl); + }); + +g.test('texel_formats,as_value') + .desc('Test that texel format cannot be used as value') + .fn(t => { + const wgsl = ` + @compute @workgroup_size(1) fn main() { + var i = rgba8unorm; + } +`; + t.expectCompileResult(false, wgsl); + }); + +const kValidTextureSampledTypes = ['f32', 'i32', 'u32']; + +g.test('sampled_texture_types') + .desc( + `Test that for texture_xx<T> +- The sampled type T must be f32, i32, or u32 +` + ) + .params(u => + u + .combine('textureType', ['texture_2d', 'texture_multisampled_2d']) + .beginSubcases() + .combine('sampledType', [ + ...kValidTextureSampledTypes, + 'bool', + 'vec2', + 'mat2x2', + '1.0', + '1', + '1u', + ] as const) + ) + .fn(t => { + const { textureType, sampledType } = t.params; + const wgsl = `@group(0) @binding(0) var tex: ${textureType}<${sampledType}>;`; + t.expectCompileResult(kValidTextureSampledTypes.includes(sampledType), wgsl); + }); + +g.test('external_sampled_texture_types') + .desc( + `Test that texture_extenal compiles and cannot specify address space +` + ) + .fn(t => { + t.expectCompileResult(true, `@group(0) @binding(0) var tex: texture_external;`); + t.expectCompileResult(false, `@group(0) @binding(0) var<private> tex: texture_external;`); + }); + +const kAccessModes = ['read', 'write', 'read_write']; + +g.test('storage_texture_types') + .desc( + `Test that for texture_storage_xx<format, access> +- format must be an enumerant for one of the texel formats for storage textures +- access must be an enumerant for one of the access modes + +Besides, the shader compilation should always pass regardless of whether the format supports the usage indicated by the access or not. +` + ) + .params(u => + u.combine('access', [...kAccessModes, 'storage'] as const).combine('format', kAllTextureFormats) + ) + .fn(t => { + const { format, access } = t.params; + const info = kTextureFormatInfo[format]; + // bgra8unorm is considered a valid storage format at shader compilation stage + const isFormatValid = + info.color?.storage || + info.depth?.storage || + info.stencil?.storage || + format === 'bgra8unorm'; + const isAccessValid = kAccessModes.includes(access); + const wgsl = `@group(0) @binding(0) var tex: texture_storage_2d<${format}, ${access}>;`; + t.expectCompileResult(isFormatValid && isAccessValid, wgsl); + }); + +g.test('depth_texture_types') + .desc( + `Test that for texture_depth_xx +- must not specify an address space +` + ) + .params(u => + u.combine('textureType', [ + 'texture_depth_2d', + 'texture_depth_2d_array', + 'texture_depth_cube', + 'texture_depth_cube_array', + ]) + ) + .fn(t => { + const { textureType } = t.params; + t.expectCompileResult(true, `@group(0) @binding(0) var t: ${textureType};`); + t.expectCompileResult(false, `@group(0) @binding(0) var<private> t: ${textureType};`); + t.expectCompileResult(false, `@group(0) @binding(0) var<storage, read> t: ${textureType};`); + }); + +g.test('sampler_types') + .desc( + `Test that for sampler and sampler_comparison +- cannot specify address space +- cannot be declared in WGSL function scope +` + ) + .params(u => u.combine('samplerType', ['sampler', 'sampler_comparison'])) + .fn(t => { + const { samplerType } = t.params; + t.expectCompileResult(true, `@group(0) @binding(0) var s: ${samplerType};`); + t.expectCompileResult(false, `@group(0) @binding(0) var<private> s: ${samplerType};`); + t.expectCompileResult( + false, + ` + @compute @workgroup_size(1) fn main() { + var s: ${samplerType}; + } + ` + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/uniformity/uniformity.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/uniformity/uniformity.spec.ts index 41249e445d..c794bded28 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/uniformity/uniformity.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/uniformity/uniformity.spec.ts @@ -21,6 +21,7 @@ const kCollectiveOps = [ { op: 'fwidthCoarse', stage: 'fragment' }, { op: 'fwidthFine', stage: 'fragment' }, { op: 'storageBarrier', stage: 'compute' }, + { op: 'textureBarrier', stage: 'compute' }, { op: 'workgroupBarrier', stage: 'compute' }, { op: 'workgroupUniformLoad', stage: 'compute' }, ]; @@ -43,6 +44,8 @@ const kConditions = [ { cond: 'nonuniform_and2', expectation: false }, { cond: 'uniform_func_var', expectation: true }, { cond: 'nonuniform_func_var', expectation: false }, + { cond: 'storage_texture_ro', expectation: true }, + { cond: 'storage_texture_rw', expectation: false }, ]; function generateCondition(condition: string): string { @@ -98,6 +101,12 @@ function generateCondition(condition: string): string { case 'nonuniform_func_var': { return `n_f == 0`; } + case 'storage_texture_ro': { + return `textureLoad(ro_storage_texture, vec2()).x == 0`; + } + case 'storage_texture_rw': { + return `textureLoad(rw_storage_texture, vec2()).x == 0`; + } default: { unreachable(`Unhandled condition`); } @@ -116,6 +125,7 @@ function generateOp(op: string): string { return `let x = ${op}(tex_depth, s_comp, vec2(0,0), 0);\n`; } case 'storageBarrier': + case 'textureBarrier': case 'workgroupBarrier': { return `${op}();\n`; } @@ -181,12 +191,16 @@ g.test('basics') .desc(`Test collective operations in simple uniform or non-uniform control flow.`) .params(u => u - .combineWithParams(kCollectiveOps) - .combineWithParams(kConditions) .combine('statement', ['if', 'for', 'while', 'switch'] as const) .beginSubcases() + .combineWithParams(kConditions) + .combineWithParams(kCollectiveOps) ) .fn(t => { + if (t.params.op === 'textureBarrier' || t.params.cond.startsWith('storage_texture')) { + t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures'); + } + let code = ` @group(0) @binding(0) var s : sampler; @group(0) @binding(1) var s_comp : sampler_comparison; @@ -197,6 +211,9 @@ g.test('basics') @group(1) @binding(1) var<storage, read_write> rw_buffer : array<f32, 4>; @group(1) @binding(2) var<uniform> uniform_buffer : vec4<f32>; + @group(2) @binding(0) var ro_storage_texture : texture_storage_2d<rgba8unorm, read>; + @group(2) @binding(1) var rw_storage_texture : texture_storage_2d<rgba8unorm, read_write>; + var<private> priv_var : array<f32, 4> = array(0,0,0,0); const c = false; @@ -367,7 +384,14 @@ function generatePointerCheck(check: string): string { } } -const kPointerCases = { +interface PointerCase { + code: string; + check: 'address' | 'contents'; + uniform: boolean | 'never'; + needs_deref_sugar?: boolean; +} + +const kPointerCases: Record<string, PointerCase> = { address_uniform_literal: { code: `let ptr = &wg_array[0];`, check: `address`, @@ -585,6 +609,168 @@ const kPointerCases = { check: `contents`, uniform: false, }, + contents_lhs_ref_pointer_deref1: { + code: `*&func_scalar = uniform_value; + let test_val = func_scalar;`, + check: `contents`, + uniform: true, + }, + contents_lhs_ref_pointer_deref1a: { + code: `*&func_scalar = nonuniform_value; + let test_val = func_scalar;`, + check: `contents`, + uniform: false, + }, + contents_lhs_ref_pointer_deref2: { + code: `*&(func_array[nonuniform_value]) = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: false, + }, + contents_lhs_ref_pointer_deref2a: { + code: `(func_array[nonuniform_value]) = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: false, + }, + contents_lhs_ref_pointer_deref3: { + code: `*&(func_array[needs_uniform(uniform_value)]) = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: true, + }, + contents_lhs_ref_pointer_deref3a: { + code: `*&(func_array[needs_uniform(nonuniform_value)]) = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: 'never', + }, + contents_lhs_ref_pointer_deref4: { + code: `*&((*&(func_struct.x[uniform_value])).x[uniform_value].x[uniform_value]) = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: true, + }, + contents_lhs_ref_pointer_deref4a: { + code: `*&((*&(func_struct.x[uniform_value])).x[uniform_value].x[uniform_value]) = nonuniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + }, + contents_lhs_ref_pointer_deref4b: { + code: `*&((*&(func_struct.x[uniform_value])).x[uniform_value].x[nonuniform_value]) = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + }, + contents_lhs_ref_pointer_deref4c: { + code: `*&((*&(func_struct.x[uniform_value])).x[nonuniform_value]).x[uniform_value] = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + }, + contents_lhs_ref_pointer_deref4d: { + code: `*&((*&(func_struct.x[nonuniform_value])).x[uniform_value].x)[uniform_value] = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + }, + contents_lhs_ref_pointer_deref4e: { + code: `*&((*&(func_struct.x[uniform_value])).x[needs_uniform(nonuniform_value)].x[uniform_value]) = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: 'never', + }, + + // The following cases require the 'pointer_composite_access' language feature. + contents_lhs_pointer_deref2: { + code: `(&func_array)[uniform_value] = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: true, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref2a: { + code: `(&func_array)[nonuniform_value] = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: false, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref3: { + code: `(&func_array)[needs_uniform(uniform_value)] = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: true, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref3a: { + code: `(&func_array)[needs_uniform(nonuniform_value)] = uniform_value; + let test_val = func_array[0];`, + check: `contents`, + uniform: 'never', + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref4: { + code: `(&((&(func_struct.x[uniform_value])).x[uniform_value]).x)[uniform_value] = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: true, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref4a: { + code: `(&((&(func_struct.x[uniform_value])).x[uniform_value]).x)[uniform_value] = nonuniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref4b: { + code: `(&((&(func_struct.x[uniform_value])).x)[uniform_value]).x[nonuniform_value] = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref4c: { + code: `(&((&(func_struct.x[uniform_value])).x[nonuniform_value]).x)[uniform_value] = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref4d: { + code: `(&((&(func_struct.x[nonuniform_value])).x[uniform_value]).x)[uniform_value] = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: false, + needs_deref_sugar: true, + }, + contents_lhs_pointer_deref4e: { + code: `(&((&(func_struct.x[uniform_value])).x)[needs_uniform(nonuniform_value)].x[uniform_value]) = uniform_value; + let test_val = func_struct.x[0].x[0].x[0];`, + check: `contents`, + uniform: 'never', + needs_deref_sugar: true, + }, + contents_rhs_pointer_deref1: { + code: `let test_val = (&func_array)[uniform_value];`, + check: `contents`, + uniform: true, + needs_deref_sugar: true, + }, + contents_rhs_pointer_deref1a: { + code: `let test_val = (&func_array)[nonuniform_value];`, + check: `contents`, + uniform: false, + needs_deref_sugar: true, + }, + contents_rhs_pointer_deref2: { + code: `let test_val = (&func_array)[needs_uniform(nonuniform_value)];`, + check: `contents`, + uniform: `never`, + needs_deref_sugar: true, + }, }; g.test('pointers') @@ -612,6 +798,13 @@ var<storage> uniform_value : u32; @group(0) @binding(1) var<storage, read_write> nonuniform_value : u32; +fn needs_uniform(val : u32) -> u32{ + if val == 0 { + workgroupBarrier(); + } + return val; +} + @compute @workgroup_size(16, 1, 1) fn main(@builtin(local_invocation_id) lid : vec3<u32>, @builtin(global_invocation_id) gid : vec3<u32>) { @@ -627,11 +820,16 @@ fn main(@builtin(local_invocation_id) lid : vec3<u32>, ` ${generatePointerCheck(testcase.check)} }`; - if (!testcase.uniform) { + + if (testcase.needs_deref_sugar === true) { + t.skipIfLanguageFeatureNotSupported('pointer_composite_access'); + } + // Explicitly check false to distinguish from never. + if (testcase.uniform === false) { const without_check = code + `}\n`; t.expectCompileResult(true, without_check); } - t.expectCompileResult(testcase.uniform, with_check); + t.expectCompileResult(testcase.uniform === true, with_check); }); function expectedUniformity(uniform: string, init: string): boolean { @@ -2019,6 +2217,7 @@ g.test('binary_expressions') u .combine('e1', keysOf(kExpressionCases)) .combine('e2', keysOf(kExpressionCases)) + .beginSubcases() .combine('op', keysOf(kBinOps)) ) .fn(t => { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/binary_stream.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/binary_stream.ts index a6512020e6..2531521dc4 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/binary_stream.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/binary_stream.ts @@ -85,6 +85,16 @@ export default class BinaryStream { return this.view.getInt16(this.alignedOffset(2), /* littleEndian */ true); } + /** writeI64() writes a bitint to the buffer at the next 64-bit aligned offset */ + writeI64(value: bigint) { + this.view.setBigInt64(this.alignedOffset(8), value, /* littleEndian */ true); + } + + /** readI64() reads a bigint from the buffer at the next 64-bit aligned offset */ + readI64(): bigint { + return this.view.getBigInt64(this.alignedOffset(8), /* littleEndian */ true); + } + /** writeI32() writes a int32 to the buffer at the next 32-bit aligned offset */ writeI32(value: number) { this.view.setInt32(this.alignedOffset(4), value, /* littleEndian */ true); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts index 298e7ae4a9..ed71d85d35 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts @@ -19,7 +19,7 @@ import { generatePrettyTable } from './pretty_diff_tables.js'; /** Generate an expected value at `index`, to test for equality with the actual value. */ export type CheckElementsGenerator = (index: number) => number; /** Check whether the actual `value` at `index` is as expected. */ -export type CheckElementsPredicate = (index: number, value: number) => boolean; +export type CheckElementsPredicate = (index: number, value: number | bigint) => boolean; /** * Provides a pretty-printing implementation for a particular CheckElementsPredicate. * This is an array; each element provides info to print an additional row in the error message. @@ -29,9 +29,9 @@ export type CheckElementsSupplementalTableRows = Array<{ leftHeader: string; /** * Get the value for a cell in the table with element index `index`. - * May be a string or a number; a number will be formatted according to the TypedArray type used. + * May be a string or numeric (number | bigint); numerics will be formatted according to the TypedArray type used. */ - getValueForCell: (index: number) => number | string; + getValueForCell: (index: number) => string | number | bigint; }>; /** @@ -43,7 +43,10 @@ export function checkElementsEqual( expected: TypedArrayBufferView ): ErrorWithExtra | undefined { assert(actual.constructor === expected.constructor, 'TypedArray type mismatch'); - assert(actual.length === expected.length, 'size mismatch'); + assert( + actual.length === expected.length, + `length mismatch: expected ${expected.length} got ${actual.length}` + ); let failedElementsFirstMaybe: number | undefined = undefined; /** Sparse array with `true` for elements that failed. */ @@ -221,13 +224,17 @@ function failCheckElements({ const printElementsEnd = Math.min(size, failedElementsLast + 2); const printElementsCount = printElementsEnd - printElementsStart; - const numberToString = printAsFloat - ? (n: number) => n.toPrecision(4) - : (n: number) => intToPaddedHex(n, { byteLength: ctor.BYTES_PER_ELEMENT }); + const numericToString = (val: number | bigint): string => { + if (typeof val === 'number' && printAsFloat) { + return val.toPrecision(4); + } + return intToPaddedHex(val, { byteLength: ctor.BYTES_PER_ELEMENT }); + }; + const numberPrefix = printAsFloat ? '' : '0x:'; const printActual = actual.subarray(printElementsStart, printElementsEnd); - const printExpected: Array<Iterable<string | number>> = []; + const printExpected: Array<Iterable<string | number | bigint>> = []; if (predicatePrinter) { for (const { leftHeader, getValueForCell: cell } of predicatePrinter) { printExpected.push( @@ -246,7 +253,7 @@ function failCheckElements({ const opts = { fillToWidth: 120, - numberToString, + numericToString, }; const msg = `Array had unexpected contents at indices ${failedElementsFirst} through ${failedElementsLast}. Starting at index ${printElementsStart}: @@ -263,10 +270,11 @@ ${generatePrettyTable(opts, [ // Helper helpers /** Convert an integral `number` into a hex string, padded to the specified `byteLength`. */ -function intToPaddedHex(number: number, { byteLength }: { byteLength: number }) { - assert(Number.isInteger(number), 'number must be integer'); - let s = Math.abs(number).toString(16); - if (byteLength) s = s.padStart(byteLength * 2, '0'); - if (number < 0) s = '-' + s; - return s; +function intToPaddedHex(val: number | bigint, { byteLength }: { byteLength: number }) { + assert(Number.isInteger(val), 'number must be integer'); + const is_negative = typeof val === 'number' ? val < 0 : val < 0n; + let str = (is_negative ? -val : val).toString(16); + if (byteLength) str = str.padStart(byteLength * 2, '0'); + if (is_negative) str = '-' + str; + return str; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts index a1de0e48ba..4ab3679b23 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts @@ -143,12 +143,7 @@ function XYZ_to_lin_P3(XYZ: Array<Array<number>>) { * https://drafts.csswg.org/css-color/#predefined-to-predefined * display-p3 and sRGB share the same white points. */ -export function displayP3ToSrgb(pixel: { R: number; G: number; B: number; A: number }): { - R: number; - G: number; - B: number; - A: number; -} { +export function displayP3ToSrgb(pixel: Readonly<RGBA>): RGBA { assert( pixel.R !== undefined && pixel.G !== undefined && pixel.B !== undefined, 'color space conversion requires all of R, G and B components' @@ -161,11 +156,7 @@ export function displayP3ToSrgb(pixel: { R: number; G: number; B: number; A: num rgbVec = [rgbMatrix[0][0], rgbMatrix[1][0], rgbMatrix[2][0]]; rgbVec = gam_sRGB(rgbVec); - pixel.R = rgbVec[0]; - pixel.G = rgbVec[1]; - pixel.B = rgbVec[2]; - - return pixel; + return { R: rgbVec[0], G: rgbVec[1], B: rgbVec[2], A: pixel.A }; } /** * @returns the converted pixels in `{R: number, G: number, B: number, A: number}`. @@ -174,12 +165,7 @@ export function displayP3ToSrgb(pixel: { R: number; G: number; B: number; A: num * https://drafts.csswg.org/css-color/#predefined-to-predefined * display-p3 and sRGB share the same white points. */ -export function srgbToDisplayP3(pixel: { R: number; G: number; B: number; A: number }): { - R: number; - G: number; - B: number; - A: number; -} { +export function srgbToDisplayP3(pixel: Readonly<RGBA>): RGBA { assert( pixel.R !== undefined && pixel.G !== undefined && pixel.B !== undefined, 'color space conversion requires all of R, G and B components' @@ -192,13 +178,10 @@ export function srgbToDisplayP3(pixel: { R: number; G: number; B: number; A: num rgbVec = [rgbMatrix[0][0], rgbMatrix[1][0], rgbMatrix[2][0]]; rgbVec = gam_P3(rgbVec); - pixel.R = rgbVec[0]; - pixel.G = rgbVec[1]; - pixel.B = rgbVec[2]; - - return pixel; + return { R: rgbVec[0], G: rgbVec[1], B: rgbVec[2], A: pixel.A }; } +export type RGBA = { R: number; G: number; B: number; A: number }; type InPlaceColorConversion = (rgba: { R: number; G: number; @@ -247,9 +230,10 @@ export function makeInPlaceColorConversion({ // This technically represents colors outside the src gamut, so no clamping yet. if (requireColorSpaceConversion) { - // WebGPU currently only supports dstColorSpace = 'srgb'. if (srcColorSpace === 'display-p3' && dstColorSpace === 'srgb') { - rgba = displayP3ToSrgb(rgba); + Object.assign(rgba, displayP3ToSrgb(rgba)); + } else if (srcColorSpace === 'srgb' && dstColorSpace === 'display-p3') { + Object.assign(rgba, srgbToDisplayP3(rgba)); } else { unreachable(); } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts index 45599d25f6..3aa62d6781 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts @@ -5,10 +5,18 @@ import { deserializeExpectation, serializeExpectation, } from '../shader/execution/expression/case_cache.js'; -import { Expectation, toComparator } from '../shader/execution/expression/expression.js'; +import { Expectation, toComparator } from '../shader/execution/expression/expectation.js'; import BinaryStream from './binary_stream.js'; -import { isFloatValue, Matrix, Scalar, Value, Vector } from './conversion.js'; +import { + ArrayValue, + isFloatValue, + isScalarValue, + MatrixValue, + ScalarValue, + Value, + VectorValue, +} from './conversion.js'; import { FPInterval } from './floating_point.js'; /** Comparison describes the result of a Comparator function. */ @@ -98,9 +106,9 @@ function compareValue(got: Value, expected: Value): Comparison { } } - if (got instanceof Scalar) { + if (isScalarValue(got)) { const g = got; - const e = expected as Scalar; + const e = expected as ScalarValue; const isFloat = g.type.kind === 'f64' || g.type.kind === 'f32' || g.type.kind === 'f16'; const matched = (isFloat && (g.value as number) === (e.value as number)) || (!isFloat && g.value === e.value); @@ -111,8 +119,8 @@ function compareValue(got: Value, expected: Value): Comparison { }; } - if (got instanceof Vector) { - const e = expected as Vector; + if (got instanceof VectorValue || got instanceof ArrayValue) { + const e = expected as VectorValue | ArrayValue; const gLen = got.elements.length; const eLen = e.elements.length; let matched = gLen === eLen; @@ -130,8 +138,8 @@ function compareValue(got: Value, expected: Value): Comparison { }; } - if (got instanceof Matrix) { - const e = expected as Matrix; + if (got instanceof MatrixValue) { + const e = expected as MatrixValue; const gCols = got.type.cols; const eCols = e.type.cols; const gRows = got.type.rows; @@ -153,7 +161,7 @@ function compareValue(got: Value, expected: Value): Comparison { }; } - throw new Error(`unhandled type '${typeof got}`); + throw new Error(`unhandled type '${typeof got}'`); } /** @@ -175,7 +183,7 @@ function compareInterval(got: Value, expected: FPInterval): Comparison { } } - if (got instanceof Scalar) { + if (isScalarValue(got)) { const g = got.value as number; const matched = expected.contains(g); return { @@ -197,7 +205,7 @@ function compareInterval(got: Value, expected: FPInterval): Comparison { */ function compareVector(got: Value, expected: FPInterval[]): Comparison { // Check got type - if (!(got instanceof Vector)) { + if (!(got instanceof VectorValue)) { return { matched: false, got: `${Colors.red((typeof got).toString())}(${got})`, @@ -262,7 +270,7 @@ function convertArrayToString<T>(m: T[]): string { */ function compareMatrix(got: Value, expected: FPInterval[][]): Comparison { // Check got type - if (!(got instanceof Matrix)) { + if (!(got instanceof MatrixValue)) { return { matched: false, got: `${Colors.red((typeof got).toString())}(${got})`, diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts index 5ee819c64e..11f0677316 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts @@ -242,6 +242,21 @@ export const kBit = { } as const; export const kValue = { + // Limits of i64 + i64: { + positive: { + min: BigInt(0n), + max: BigInt(9223372036854775807n), + }, + negative: { + min: BigInt(-9223372036854775808n), + max: BigInt(0n), + }, + isOOB: (val: bigint): boolean => { + return val > kValue.i64.positive.max || val < kValue.i64.negative.min; + }, + }, + // Limits of i32 i32: { positive: { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts index d98367447d..29d892d14b 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts @@ -6,6 +6,7 @@ import { Float16Array } from '../../external/petamoriken/float16/float16.js'; import BinaryStream from './binary_stream.js'; import { kBit } from './constants.js'; import { + align, cartesianProduct, clamp, correctlyRoundedF16, @@ -84,6 +85,8 @@ const workingDataI16 = new Int16Array(workingData); const workingDataI32 = new Int32Array(workingData); const workingDataI8 = new Int8Array(workingData); const workingDataF64 = new Float64Array(workingData); +const workingDataI64 = new BigInt64Array(workingData); +const workingDataU64 = new BigUint64Array(workingData); const workingDataView = new DataView(workingData); /** @@ -107,7 +110,7 @@ export function float32ToFloatBits( assert(mantissaBits <= 23); if (Number.isNaN(n)) { - // NaN = all exponent bits true, 1 or more mantissia bits true + // NaN = all exponent bits true, 1 or more mantissa bits true return (((1 << exponentBits) - 1) << mantissaBits) | ((1 << mantissaBits) - 1); } @@ -584,6 +587,7 @@ export type ScalarKind = | 'u32' | 'u16' | 'u8' + | 'abstract-int' | 'i32' | 'i16' | 'i8' @@ -593,11 +597,18 @@ export type ScalarKind = export class ScalarType { readonly kind: ScalarKind; // The named type readonly _size: number; // In bytes - readonly read: (buf: Uint8Array, offset: number) => Scalar; // reads a scalar from a buffer - - constructor(kind: ScalarKind, size: number, read: (buf: Uint8Array, offset: number) => Scalar) { + readonly _signed: boolean; + readonly read: (buf: Uint8Array, offset: number) => ScalarValue; // reads a scalar from a buffer + + constructor( + kind: ScalarKind, + size: number, + signed: boolean, + read: (buf: Uint8Array, offset: number) => ScalarValue + ) { this.kind = kind; this._size = size; + this._signed = signed; this.read = read; } @@ -609,32 +620,64 @@ export class ScalarType { return this._size; } - /** Constructs a Scalar of this type with `value` */ - public create(value: number): Scalar { - switch (this.kind) { - case 'abstract-float': - return abstractFloat(value); - case 'f64': - return f64(value); - case 'f32': - return f32(value); - case 'f16': - return f16(value); - case 'u32': - return u32(value); - case 'u16': - return u16(value); - case 'u8': - return u8(value); - case 'i32': - return i32(value); - case 'i16': - return i16(value); - case 'i8': - return i8(value); - case 'bool': - return bool(value !== 0); + public get alignment(): number { + return this._size; + } + + public get signed(): boolean { + return this._signed; + } + + // This allows width to be checked in cases where scalar and vector types are mixed. + public get width(): number { + return 1; + } + + public requiresF16(): boolean { + return this.kind === 'f16'; + } + + /** Constructs a ScalarValue of this type with `value` */ + public create(value: number | bigint): ScalarValue { + switch (typeof value) { + case 'number': + switch (this.kind) { + case 'abstract-float': + return abstractFloat(value); + case 'abstract-int': + return abstractInt(BigInt(value)); + case 'f64': + return f64(value); + case 'f32': + return f32(value); + case 'f16': + return f16(value); + case 'u32': + return u32(value); + case 'u16': + return u16(value); + case 'u8': + return u8(value); + case 'i32': + return i32(value); + case 'i16': + return i16(value); + case 'i8': + return i8(value); + case 'bool': + return bool(value !== 0); + } + break; + case 'bigint': + switch (this.kind) { + case 'abstract-int': + return abstractInt(value); + case 'bool': + return bool(value !== 0n); + } + break; } + unreachable(`Scalar<${this.kind}>.create() does not support ${typeof value}`); } } @@ -643,6 +686,20 @@ export class VectorType { readonly width: number; // Number of elements in the vector readonly elementType: ScalarType; // Element type + // Maps a string representation of a vector type to vector type. + private static instances = new Map<string, VectorType>(); + + static create(width: number, elementType: ScalarType): VectorType { + const key = `${elementType.toString()} ${width}}`; + let ty = this.instances.get(key); + if (ty !== undefined) { + return ty; + } + ty = new VectorType(width, elementType); + this.instances.set(key, ty); + return ty; + } + constructor(width: number, elementType: ScalarType) { this.width = width; this.elementType = elementType; @@ -652,13 +709,13 @@ export class VectorType { * @returns a vector constructed from the values read from the buffer at the * given byte offset */ - public read(buf: Uint8Array, offset: number): Vector { - const elements: Array<Scalar> = []; + public read(buf: Uint8Array, offset: number): VectorValue { + const elements: Array<ScalarValue> = []; for (let i = 0; i < this.width; i++) { elements[i] = this.elementType.read(buf, offset); offset += this.elementType.size; } - return new Vector(elements); + return new VectorValue(elements); } public toString(): string { @@ -669,29 +726,27 @@ export class VectorType { return this.elementType.size * this.width; } + public get alignment(): number { + return VectorType.alignmentOf(this.width, this.elementType); + } + + public static alignmentOf(width: number, elementType: ScalarType) { + return elementType.size * (width === 3 ? 4 : width); + } + /** Constructs a Vector of this type with the given values */ - public create(value: number | readonly number[]): Vector { + public create(value: (number | bigint) | readonly (number | bigint)[]): VectorValue { if (value instanceof Array) { assert(value.length === this.width); } else { value = Array(this.width).fill(value); } - return new Vector(value.map(v => this.elementType.create(v))); + return new VectorValue(value.map(v => this.elementType.create(v))); } -} -// Maps a string representation of a vector type to vector type. -const vectorTypes = new Map<string, VectorType>(); - -export function TypeVec(width: number, elementType: ScalarType): VectorType { - const key = `${elementType.toString()} ${width}}`; - let ty = vectorTypes.get(key); - if (ty !== undefined) { - return ty; + public requiresF16(): boolean { + return this.elementType.requiresF16(); } - ty = new VectorType(width, elementType); - vectorTypes.set(key, ty); - return ty; } /** MatrixType describes the type of WGSL Matrix. */ @@ -700,6 +755,20 @@ export class MatrixType { readonly rows: number; // Number of elements per column in the Matrix readonly elementType: ScalarType; // Element type + // Maps a string representation of a Matrix type to Matrix type. + private static instances = new Map<string, MatrixType>(); + + static create(cols: number, rows: number, elementType: ScalarType): MatrixType { + const key = `${elementType.toString()} ${cols} ${rows}`; + let ty = this.instances.get(key); + if (ty !== undefined) { + return ty; + } + ty = new MatrixType(cols, rows, elementType); + this.instances.set(key, ty); + return ty; + } + constructor(cols: number, rows: number, elementType: ScalarType) { this.cols = cols; this.rows = rows; @@ -716,8 +785,8 @@ export class MatrixType { * @returns a Matrix constructed from the values read from the buffer at the * given byte offset */ - public read(buf: Uint8Array, offset: number): Matrix { - const elements: Scalar[][] = [...Array(this.cols)].map(_ => [...Array(this.rows)]); + public read(buf: Uint8Array, offset: number): MatrixValue { + const elements: ScalarValue[][] = [...Array(this.cols)].map(_ => [...Array(this.rows)]); for (let c = 0; c < this.cols; c++) { for (let r = 0; r < this.rows; r++) { elements[c][r] = this.elementType.read(buf, offset); @@ -729,100 +798,265 @@ export class MatrixType { offset += this.elementType.size; } } - return new Matrix(elements); + return new MatrixValue(elements); } public toString(): string { return `mat${this.cols}x${this.rows}<${this.elementType}>`; } + + public get size(): number { + return VectorType.alignmentOf(this.rows, this.elementType) * this.cols; + } + + public get alignment(): number { + return VectorType.alignmentOf(this.rows, this.elementType); + } + + public requiresF16(): boolean { + return this.elementType.requiresF16(); + } + + /** Constructs a Matrix of this type with the given values */ + public create(value: (number | bigint) | readonly (number | bigint)[]): MatrixValue { + if (value instanceof Array) { + assert(value.length === this.cols * this.rows); + } else { + value = Array(this.cols * this.rows).fill(value); + } + const columns: (number | bigint)[][] = []; + for (let i = 0; i < this.cols; i++) { + const start = i * this.rows; + columns.push(value.slice(start, start + this.rows)); + } + return new MatrixValue(columns.map(c => c.map(v => this.elementType.create(v)))); + } } -// Maps a string representation of a Matrix type to Matrix type. -const matrixTypes = new Map<string, MatrixType>(); +/** ArrayType describes the type of WGSL Array. */ +export class ArrayType { + readonly count: number; // Number of elements in the array. Zero represents a runtime-sized array. + readonly elementType: Type; // Element type -export function TypeMat(cols: number, rows: number, elementType: ScalarType): MatrixType { - const key = `${elementType.toString()} ${cols} ${rows}`; - let ty = matrixTypes.get(key); - if (ty !== undefined) { + // Maps a string representation of a array type to array type. + private static instances = new Map<string, ArrayType>(); + + static create(count: number, elementType: Type): ArrayType { + const key = `${elementType.toString()} ${count}`; + let ty = this.instances.get(key); + if (ty !== undefined) { + return ty; + } + ty = new ArrayType(count, elementType); + this.instances.set(key, ty); return ty; } - ty = new MatrixType(cols, rows, elementType); - matrixTypes.set(key, ty); - return ty; + + constructor(count: number, elementType: Type) { + this.count = count; + this.elementType = elementType; + } + + /** + * @returns a array constructed from the values read from the buffer at the + * given byte offset + */ + public read(buf: Uint8Array, offset: number): ArrayValue { + const elements: Array<Value> = []; + + for (let i = 0; i < this.count; i++) { + elements[i] = this.elementType.read(buf, offset); + offset += this.stride; + } + return new ArrayValue(elements); + } + + public toString(): string { + return this.count !== 0 + ? `array<${this.elementType}, ${this.count}>` + : `array<${this.elementType}>`; + } + + public get stride(): number { + return align(this.elementType.size, this.elementType.alignment); + } + + public get size(): number { + return this.stride * this.count; + } + + public get alignment(): number { + return this.elementType.alignment; + } + + public requiresF16(): boolean { + return this.elementType.requiresF16(); + } + + /** Constructs an Array of this type with the given values */ + public create(value: (number | bigint) | readonly (number | bigint)[]): ArrayValue { + if (value instanceof Array) { + assert(value.length === this.count); + } else { + value = Array(this.count).fill(value); + } + return new ArrayValue(value.map(v => this.elementType.create(v))); + } } -/** Type is a ScalarType, VectorType, or MatrixType. */ -export type Type = ScalarType | VectorType | MatrixType; +/** ArrayElementType infers the element type of the indexable type A */ +type ArrayElementType<A> = A extends { [index: number]: infer T } ? T : never; /** Copy bytes from `buf` at `offset` into the working data, then read it out using `workingDataOut` */ -function valueFromBytes(workingDataOut: TypedArrayBufferView, buf: Uint8Array, offset: number) { +function valueFromBytes<A extends TypedArrayBufferView>( + workingDataOut: A, + buf: Uint8Array, + offset: number +): ArrayElementType<A> { for (let i = 0; i < workingDataOut.BYTES_PER_ELEMENT; ++i) { workingDataU8[i] = buf[offset + i]; } - return workingDataOut[0]; + return workingDataOut[0] as ArrayElementType<A>; } -export const TypeI32 = new ScalarType('i32', 4, (buf: Uint8Array, offset: number) => +const abstractIntType = new ScalarType('abstract-int', 8, true, (buf: Uint8Array, offset: number) => + abstractInt(valueFromBytes(workingDataI64, buf, offset)) +); +const i32Type = new ScalarType('i32', 4, true, (buf: Uint8Array, offset: number) => i32(valueFromBytes(workingDataI32, buf, offset)) ); -export const TypeU32 = new ScalarType('u32', 4, (buf: Uint8Array, offset: number) => +const u32Type = new ScalarType('u32', 4, false, (buf: Uint8Array, offset: number) => u32(valueFromBytes(workingDataU32, buf, offset)) ); -export const TypeAbstractFloat = new ScalarType( +const i16Type = new ScalarType('i16', 2, true, (buf: Uint8Array, offset: number) => + i16(valueFromBytes(workingDataI16, buf, offset)) +); +const u16Type = new ScalarType('u16', 2, false, (buf: Uint8Array, offset: number) => + u16(valueFromBytes(workingDataU16, buf, offset)) +); +const i8Type = new ScalarType('i8', 1, true, (buf: Uint8Array, offset: number) => + i8(valueFromBytes(workingDataI8, buf, offset)) +); +const u8Type = new ScalarType('u8', 1, false, (buf: Uint8Array, offset: number) => + u8(valueFromBytes(workingDataU8, buf, offset)) +); +const abstractFloatType = new ScalarType( 'abstract-float', 8, + true, (buf: Uint8Array, offset: number) => abstractFloat(valueFromBytes(workingDataF64, buf, offset)) ); -export const TypeF64 = new ScalarType('f64', 8, (buf: Uint8Array, offset: number) => +const f64Type = new ScalarType('f64', 8, true, (buf: Uint8Array, offset: number) => f64(valueFromBytes(workingDataF64, buf, offset)) ); -export const TypeF32 = new ScalarType('f32', 4, (buf: Uint8Array, offset: number) => +const f32Type = new ScalarType('f32', 4, true, (buf: Uint8Array, offset: number) => f32(valueFromBytes(workingDataF32, buf, offset)) ); -export const TypeI16 = new ScalarType('i16', 2, (buf: Uint8Array, offset: number) => - i16(valueFromBytes(workingDataI16, buf, offset)) -); -export const TypeU16 = new ScalarType('u16', 2, (buf: Uint8Array, offset: number) => - u16(valueFromBytes(workingDataU16, buf, offset)) -); -export const TypeF16 = new ScalarType('f16', 2, (buf: Uint8Array, offset: number) => +const f16Type = new ScalarType('f16', 2, true, (buf: Uint8Array, offset: number) => f16Bits(valueFromBytes(workingDataU16, buf, offset)) ); -export const TypeI8 = new ScalarType('i8', 1, (buf: Uint8Array, offset: number) => - i8(valueFromBytes(workingDataI8, buf, offset)) -); -export const TypeU8 = new ScalarType('u8', 1, (buf: Uint8Array, offset: number) => - u8(valueFromBytes(workingDataU8, buf, offset)) -); -export const TypeBool = new ScalarType('bool', 4, (buf: Uint8Array, offset: number) => +const boolType = new ScalarType('bool', 4, false, (buf: Uint8Array, offset: number) => bool(valueFromBytes(workingDataU32, buf, offset) !== 0) ); +/** Type is a ScalarType, VectorType, MatrixType or ArrayType. */ +export type Type = ScalarType | VectorType | MatrixType | ArrayType; + +/** Type holds pre-declared Types along with helper constructor functions. */ +export const Type = { + abstractInt: abstractIntType, + 'abstract-int': abstractIntType, + i32: i32Type, + u32: u32Type, + i16: i16Type, + u16: u16Type, + i8: i8Type, + u8: u8Type, + + abstractFloat: abstractFloatType, + 'abstract-float': abstractFloatType, + f64: f64Type, + f32: f32Type, + f16: f16Type, + + bool: boolType, + + vec: (width: number, elementType: ScalarType) => VectorType.create(width, elementType), + + vec2ai: VectorType.create(2, abstractIntType), + vec2i: VectorType.create(2, i32Type), + vec2u: VectorType.create(2, u32Type), + vec2af: VectorType.create(2, abstractFloatType), + vec2f: VectorType.create(2, f32Type), + vec2h: VectorType.create(2, f16Type), + vec2b: VectorType.create(2, boolType), + vec3ai: VectorType.create(3, abstractIntType), + vec3i: VectorType.create(3, i32Type), + vec3u: VectorType.create(3, u32Type), + vec3af: VectorType.create(3, abstractFloatType), + vec3f: VectorType.create(3, f32Type), + vec3h: VectorType.create(3, f16Type), + vec3b: VectorType.create(3, boolType), + vec4ai: VectorType.create(4, abstractIntType), + vec4i: VectorType.create(4, i32Type), + vec4u: VectorType.create(4, u32Type), + vec4af: VectorType.create(4, abstractFloatType), + vec4f: VectorType.create(4, f32Type), + vec4h: VectorType.create(4, f16Type), + vec4b: VectorType.create(4, boolType), + + mat: (cols: number, rows: number, elementType: ScalarType) => + MatrixType.create(cols, rows, elementType), + + mat2x2f: MatrixType.create(2, 2, f32Type), + mat2x2h: MatrixType.create(2, 2, f16Type), + mat3x2f: MatrixType.create(3, 2, f32Type), + mat3x2h: MatrixType.create(3, 2, f16Type), + mat4x2f: MatrixType.create(4, 2, f32Type), + mat4x2h: MatrixType.create(4, 2, f16Type), + mat2x3f: MatrixType.create(2, 3, f32Type), + mat2x3h: MatrixType.create(2, 3, f16Type), + mat3x3f: MatrixType.create(3, 3, f32Type), + mat3x3h: MatrixType.create(3, 3, f16Type), + mat4x3f: MatrixType.create(4, 3, f32Type), + mat4x3h: MatrixType.create(4, 3, f16Type), + mat2x4f: MatrixType.create(2, 4, f32Type), + mat2x4h: MatrixType.create(2, 4, f16Type), + mat3x4f: MatrixType.create(3, 4, f32Type), + mat3x4h: MatrixType.create(3, 4, f16Type), + mat4x4f: MatrixType.create(4, 4, f32Type), + mat4x4h: MatrixType.create(4, 4, f16Type), + + array: (count: number, elementType: Type) => ArrayType.create(count, elementType), +}; + /** @returns the ScalarType from the ScalarKind */ export function scalarType(kind: ScalarKind): ScalarType { switch (kind) { case 'abstract-float': - return TypeAbstractFloat; + return Type.abstractFloat; case 'f64': - return TypeF64; + return Type.f64; case 'f32': - return TypeF32; + return Type.f32; case 'f16': - return TypeF16; + return Type.f16; case 'u32': - return TypeU32; + return Type.u32; case 'u16': - return TypeU16; + return Type.u16; case 'u8': - return TypeU8; + return Type.u8; + case 'abstract-int': + return Type.abstractInt; case 'i32': - return TypeI32; + return Type.i32; case 'i16': - return TypeI16; + return Type.i16; case 'i8': - return TypeI8; + return Type.i8; case 'bool': - return TypeBool; + return Type.bool; } } @@ -837,23 +1071,54 @@ export function numElementsOf(ty: Type): number { if (ty instanceof MatrixType) { return ty.cols * ty.rows; } + if (ty instanceof ArrayType) { + return ty.count; + } throw new Error(`unhandled type ${ty}`); } /** @returns the scalar elements of the given Value */ -export function elementsOf(value: Value): Scalar[] { - if (value instanceof Scalar) { +export function elementsOf(value: Value): Value[] { + if (isScalarValue(value)) { + return [value]; + } + if (value instanceof VectorValue) { + return value.elements; + } + if (value instanceof MatrixValue) { + return value.elements.flat(); + } + if (value instanceof ArrayValue) { + return value.elements; + } + throw new Error(`unhandled value ${value}`); +} + +/** @returns the scalar elements of the given Value */ +export function scalarElementsOf(value: Value): ScalarValue[] { + if (isScalarValue(value)) { return [value]; } - if (value instanceof Vector) { + if (value instanceof VectorValue) { return value.elements; } - if (value instanceof Matrix) { + if (value instanceof MatrixValue) { return value.elements.flat(); } + if (value instanceof ArrayValue) { + return value.elements.map(els => scalarElementsOf(els)).flat(); + } throw new Error(`unhandled value ${value}`); } +/** @returns the inner element type of the given type */ +export function elementTypeOf(t: Type) { + if (t instanceof ScalarType) { + return t; + } + return t.elementType; +} + /** @returns the scalar (element) type of the given Type */ export function scalarTypeOf(ty: Type): ScalarType { if (ty instanceof ScalarType) { @@ -865,27 +1130,93 @@ export function scalarTypeOf(ty: Type): ScalarType { if (ty instanceof MatrixType) { return ty.elementType; } + if (ty instanceof ArrayType) { + return scalarTypeOf(ty.elementType); + } throw new Error(`unhandled type ${ty}`); } -/** ScalarValue is the JS type that can be held by a Scalar */ -type ScalarValue = boolean | number; +/** + * @returns the implicit concretized type of the given Type. + * @param abstractIntToF32 if true, returns f32 for abstractInt else i32 + * Example: vec3<abstract-float> -> vec3<float> + */ +export function concreteTypeOf(ty: Type, allowedScalarTypes?: Type[]): Type { + if (allowedScalarTypes && allowedScalarTypes.length > 0) { + // https://www.w3.org/TR/WGSL/#conversion-rank + switch (ty) { + case Type.abstractInt: + if (allowedScalarTypes.includes(Type.i32)) { + return Type.i32; + } + if (allowedScalarTypes.includes(Type.u32)) { + return Type.u32; + } + // fallthrough. + case Type.abstractFloat: + if (allowedScalarTypes.includes(Type.f32)) { + return Type.f32; + } + if (allowedScalarTypes.includes(Type.f16)) { + return Type.f32; + } + throw new Error(`no ${ty}`); + } + } else { + switch (ty) { + case Type.abstractInt: + return Type.i32; + case Type.abstractFloat: + return Type.f32; + } + } + if (ty instanceof ScalarType) { + return ty; + } + if (ty instanceof VectorType) { + return Type.vec(ty.width, concreteTypeOf(ty.elementType, allowedScalarTypes) as ScalarType); + } + if (ty instanceof MatrixType) { + return Type.mat( + ty.cols, + ty.rows, + concreteTypeOf(ty.elementType, allowedScalarTypes) as ScalarType + ); + } + if (ty instanceof ArrayType) { + return Type.array(ty.count, concreteTypeOf(ty.elementType, allowedScalarTypes)); + } + throw new Error(`unhandled type ${ty}`); +} -/** Class that encapsulates a single scalar value of various types. */ -export class Scalar { - readonly value: ScalarValue; // The scalar value - readonly type: ScalarType; // The type of the scalar +function hex(sizeInBytes: number, bitsLow: number, bitsHigh?: number) { + let hex = ''; + workingDataU32[0] = bitsLow; + if (bitsHigh !== undefined) { + workingDataU32[1] = bitsHigh; + } + for (let i = 0; i < sizeInBytes; ++i) { + hex = workingDataU8[i].toString(16).padStart(2, '0') + hex; + } + return `0x${hex}`; +} + +function withPoint(x: number) { + const str = `${x}`; + return str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; +} - // The scalar value, packed in one or two 32-bit unsigned integers. - // Whether or not the bits1 is used depends on `this.type.size`. - readonly bits1: number; - readonly bits0: number; +/** Class that encapsulates a single abstract-int value. */ +export class AbstractIntValue { + readonly value: bigint; // The abstract-integer value + readonly bitsLow: number; // The low 32 bits of the abstract-integer value. + readonly bitsHigh: number; // The high 32 bits of the abstract-integer value. + readonly type = Type.abstractInt; // The type of the value. - public constructor(type: ScalarType, value: ScalarValue, bits1: number, bits0: number) { + public constructor(value: bigint, bitsLow: number, bitsHigh: number) { this.value = value; - this.type = type; - this.bits1 = bits1; - this.bits0 = bits0; + this.bitsLow = bitsLow; + this.bitsHigh = bitsHigh; } /** @@ -894,200 +1225,583 @@ export class Scalar { * @param offset the offset in buffer, in units of `buffer` */ public copyTo(buffer: TypedArrayBufferView, offset: number) { - assert(this.type.kind !== 'f64', `Copying f64 values to/from buffers is not defined`); - workingDataU32[1] = this.bits1; - workingDataU32[0] = this.bits0; - for (let i = 0; i < this.type.size; i++) { + workingDataU32[0] = this.bitsLow; + workingDataU32[1] = this.bitsHigh; + for (let i = 0; i < 8; i++) { buffer[offset + i] = workingDataU8[i]; } } + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + // WGSL parses negative numbers as a negated positive. + // This means '-9223372036854775808' parses as `-' & '9223372036854775808', so must be written as + // '(-9223372036854775807 - 1)' in WGSL, because '9223372036854775808' is not a valid AbstractInt. + if (this.value === -9223372036854775808n) { + return `(-9223372036854775807 - 1)`; + } + return `${this.value}`; + } + + public toString(): string { + return `${Colors.bold(this.value.toString())} (${hex(8, this.bitsLow, this.bitsHigh)})`; + } +} + +/** Class that encapsulates a single abstract-float value. */ +export class AbstractFloatValue { + readonly value: number; // The f32 value + readonly bitsLow: number; // The low 32 bits of the abstract-float value. + readonly bitsHigh: number; // The high 32 bits of the abstract-float value. + readonly type = Type.abstractFloat; // The type of the value. + + public constructor(value: number, bitsLow: number, bitsHigh: number) { + this.value = value; + this.bitsLow = bitsLow; + this.bitsHigh = bitsHigh; + } + /** - * @returns the WGSL representation of this scalar value + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU32[0] = this.bitsLow; + workingDataU32[1] = this.bitsHigh; + for (let i = 0; i < 8; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ public wgsl(): string { - const withPoint = (x: number) => { - const str = `${x}`; - return str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; - }; - if (isFinite(this.value as number)) { - switch (this.type.kind) { - case 'abstract-float': - return `${withPoint(this.value as number)}`; - case 'f64': - return `${withPoint(this.value as number)}`; - case 'f32': - return `${withPoint(this.value as number)}f`; - case 'f16': - return `${withPoint(this.value as number)}h`; - case 'u32': - return `${this.value}u`; - case 'i32': - return `i32(${this.value})`; - case 'bool': - return `${this.value}`; + return `${withPoint(this.value)}`; + } + + public toString(): string { + switch (this.value) { + case Infinity: + case -Infinity: + return Colors.bold(this.value.toString()); + default: { + let str = this.value.toString(); + str = str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; + return isSubnormalNumberF64(this.value.valueOf()) + ? `${Colors.bold(str)} (${hex(8, this.bitsLow, this.bitsHigh)} subnormal)` + : `${Colors.bold(str)} (${hex(8, this.bitsLow, this.bitsHigh)})`; } } - throw new Error( - `scalar of value ${this.value} and type ${this.type} has no WGSL representation` - ); + } +} + +/** Class that encapsulates a single i32 value. */ +export class I32Value { + readonly value: number; // The i32 value + readonly bits: number; // The i32 value, bitcast to a 32-bit integer. + readonly type = Type.i32; // The type of the value. + + public constructor(value: number, bits: number) { + this.value = value; + this.bits = bits; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU32[0] = this.bits; + for (let i = 0; i < 4; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + return `i32(${this.value})`; } public toString(): string { - if (this.type.kind === 'bool') { - return Colors.bold(this.value.toString()); + return `${Colors.bold(this.value.toString())} (${hex(4, this.bits)})`; + } +} + +/** Class that encapsulates a single u32 value. */ +export class U32Value { + readonly value: number; // The u32 value + readonly type = Type.u32; // The type of the value. + + public constructor(value: number) { + this.value = value; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU32[0] = this.value; + for (let i = 0; i < 4; i++) { + buffer[offset + i] = workingDataU8[i]; } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + return `${this.value}u`; + } + + public toString(): string { + return `${Colors.bold(this.value.toString())} (${hex(4, this.value)})`; + } +} + +/** + * Class that encapsulates a single i16 value. + * @note type does not exist in WGSL yet + */ +export class I16Value { + readonly value: number; // The i16 value + readonly bits: number; // The i16 value, bitcast to a 16-bit integer. + readonly type = Type.i16; // The type of the value. + + public constructor(value: number, bits: number) { + this.value = value; + this.bits = bits; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU16[0] = this.bits; + for (let i = 0; i < 4; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + return `i16(${this.value})`; + } + + public toString(): string { + return `${Colors.bold(this.value.toString())} (${hex(2, this.bits)})`; + } +} + +/** + * Class that encapsulates a single u16 value. + * @note type does not exist in WGSL yet + */ +export class U16Value { + readonly value: number; // The u16 value + readonly type = Type.u16; // The type of the value. + + public constructor(value: number) { + this.value = value; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU16[0] = this.value; + for (let i = 0; i < 2; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + assert(false, 'u16 is not a WGSL type'); + return `u16(${this.value})`; + } + + public toString(): string { + return `${Colors.bold(this.value.toString())} (${hex(2, this.value)})`; + } +} + +/** + * Class that encapsulates a single i8 value. + * @note type does not exist in WGSL yet + */ +export class I8Value { + readonly value: number; // The i8 value + readonly bits: number; // The i8 value, bitcast to a 8-bit integer. + readonly type = Type.i8; // The type of the value. + + public constructor(value: number, bits: number) { + this.value = value; + this.bits = bits; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU8[0] = this.bits; + for (let i = 0; i < 4; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + return `i8(${this.value})`; + } + + public toString(): string { + return `${Colors.bold(this.value.toString())} (${hex(2, this.bits)})`; + } +} + +/** + * Class that encapsulates a single u8 value. + * @note type does not exist in WGSL yet + */ +export class U8Value { + readonly value: number; // The u8 value + readonly type = Type.u8; // The type of the value. + + public constructor(value: number) { + this.value = value; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU8[0] = this.value; + for (let i = 0; i < 2; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + assert(false, 'u8 is not a WGSL type'); + return `u8(${this.value})`; + } + + public toString(): string { + return `${Colors.bold(this.value.toString())} (${hex(2, this.value)})`; + } +} + +/** + * Class that encapsulates a single f64 value + * @note type does not exist in WGSL yet + */ +export class F64Value { + readonly value: number; // The f32 value + readonly bitsLow: number; // The low 32 bits of the abstract-float value. + readonly bitsHigh: number; // The high 32 bits of the abstract-float value. + readonly type = Type.f64; // The type of the value. + + public constructor(value: number, bitsLow: number, bitsHigh: number) { + this.value = value; + this.bitsLow = bitsLow; + this.bitsHigh = bitsHigh; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU32[0] = this.bitsLow; + workingDataU32[1] = this.bitsHigh; + for (let i = 0; i < 8; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + assert(false, 'f64 is not a WGSL type'); + return `${withPoint(this.value)}`; + } + + public toString(): string { switch (this.value) { case Infinity: case -Infinity: return Colors.bold(this.value.toString()); default: { - workingDataU32[1] = this.bits1; - workingDataU32[0] = this.bits0; - let hex = ''; - for (let i = 0; i < this.type.size; ++i) { - hex = workingDataU8[i].toString(16).padStart(2, '0') + hex; - } - const n = this.value as Number; - if (n !== null && isFloatValue(this)) { - let str = this.value.toString(); - str = str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; - switch (this.type.kind) { - case 'abstract-float': - return isSubnormalNumberF64(n.valueOf()) - ? `${Colors.bold(str)} (0x${hex} subnormal)` - : `${Colors.bold(str)} (0x${hex})`; - case 'f64': - return isSubnormalNumberF64(n.valueOf()) - ? `${Colors.bold(str)} (0x${hex} subnormal)` - : `${Colors.bold(str)} (0x${hex})`; - case 'f32': - return isSubnormalNumberF32(n.valueOf()) - ? `${Colors.bold(str)} (0x${hex} subnormal)` - : `${Colors.bold(str)} (0x${hex})`; - case 'f16': - return isSubnormalNumberF16(n.valueOf()) - ? `${Colors.bold(str)} (0x${hex} subnormal)` - : `${Colors.bold(str)} (0x${hex})`; - default: - unreachable( - `Printing of floating point kind ${this.type.kind} is not implemented...` - ); - } - } - return `${Colors.bold(this.value.toString())} (0x${hex})`; + let str = this.value.toString(); + str = str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; + return isSubnormalNumberF64(this.value.valueOf()) + ? `${Colors.bold(str)} (${hex(8, this.bitsLow, this.bitsHigh)} subnormal)` + : `${Colors.bold(str)} (${hex(8, this.bitsLow, this.bitsHigh)})`; } } } } -export interface ScalarBuilder { - (value: number): Scalar; +/** Class that encapsulates a single f32 value. */ +export class F32Value { + readonly value: number; // The f32 value + readonly bits: number; // The f32 value, bitcast to a 32-bit integer. + readonly type = Type.f32; // The type of the value. + + public constructor(value: number, bits: number) { + this.value = value; + this.bits = bits; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU32[0] = this.bits; + for (let i = 0; i < 4; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + return `${withPoint(this.value)}f`; + } + + public toString(): string { + switch (this.value) { + case Infinity: + case -Infinity: + return Colors.bold(this.value.toString()); + default: { + let str = this.value.toString(); + str = str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; + return isSubnormalNumberF32(this.value.valueOf()) + ? `${Colors.bold(str)} (${hex(4, this.bits)} subnormal)` + : `${Colors.bold(str)} (${hex(4, this.bits)})`; + } + } + } } -/** Create a Scalar of `type` by storing `value` as an element of `workingDataArray` and retrieving it. - * The working data array *must* be an alias of `workingData`. - */ -function scalarFromValue( - type: ScalarType, - workingDataArray: TypedArrayBufferView, - value: number -): Scalar { - // Clear all bits of the working data since `value` may be smaller; the upper bits should be 0. - workingDataU32[1] = 0; - workingDataU32[0] = 0; - workingDataArray[0] = value; - return new Scalar(type, workingDataArray[0], workingDataU32[1], workingDataU32[0]); -} - -/** Create a Scalar of `type` by storing `value` as an element of `workingDataStoreArray` and - * reinterpreting it as an element of `workingDataLoadArray`. - * Both working data arrays *must* be aliases of `workingData`. - */ -function scalarFromBits( - type: ScalarType, - workingDataStoreArray: TypedArrayBufferView, - workingDataLoadArray: TypedArrayBufferView, - bits: number -): Scalar { - // Clear all bits of the working data since `value` may be smaller; the upper bits should be 0. - workingDataU32[1] = 0; - workingDataU32[0] = 0; - workingDataStoreArray[0] = bits; - return new Scalar(type, workingDataLoadArray[0], workingDataU32[1], workingDataU32[0]); +/** Class that encapsulates a single f16 value. */ +export class F16Value { + readonly value: number; // The f16 value + readonly bits: number; // The f16 value, bitcast to a 16-bit integer. + readonly type = Type.f16; // The type of the value. + + public constructor(value: number, bits: number) { + this.value = value; + this.bits = bits; + } + + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + workingDataU16[0] = this.bits; + for (let i = 0; i < 2; i++) { + buffer[offset + i] = workingDataU8[i]; + } + } + + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + return `${withPoint(this.value)}h`; + } + + public toString(): string { + switch (this.value) { + case Infinity: + case -Infinity: + return Colors.bold(this.value.toString()); + default: { + let str = this.value.toString(); + str = str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; + return isSubnormalNumberF16(this.value.valueOf()) + ? `${Colors.bold(str)} (${hex(2, this.bits)} subnormal)` + : `${Colors.bold(str)} (${hex(2, this.bits)})`; + } + } + } } +/** Class that encapsulates a single bool value. */ +export class BoolValue { + readonly value: boolean; // The bool value + readonly type = Type.bool; // The type of the value. -/** Create an AbstractFloat from a numeric value, a JS `number`. */ -export const abstractFloat = (value: number): Scalar => - scalarFromValue(TypeAbstractFloat, workingDataF64, value); + public constructor(value: boolean) { + this.value = value; + } -/** Create an f64 from a numeric value, a JS `number`. */ -export const f64 = (value: number): Scalar => scalarFromValue(TypeF64, workingDataF64, value); + /** + * Copies the scalar value to the buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the offset in buffer, in units of `buffer` + */ + public copyTo(buffer: TypedArrayBufferView, offset: number) { + buffer[offset] = this.value ? 1 : 0; + } -/** Create an f32 from a numeric value, a JS `number`. */ -export const f32 = (value: number): Scalar => scalarFromValue(TypeF32, workingDataF32, value); + /** @returns the WGSL representation of this scalar value */ + public wgsl(): string { + return this.value.toString(); + } -/** Create an f16 from a numeric value, a JS `number`. */ -export const f16 = (value: number): Scalar => scalarFromValue(TypeF16, workingDataF16, value); + public toString(): string { + return Colors.bold(this.value.toString()); + } +} -/** Create an f32 from a bit representation, a uint32 represented as a JS `number`. */ -export const f32Bits = (bits: number): Scalar => - scalarFromBits(TypeF32, workingDataU32, workingDataF32, bits); +/** Scalar represents all the scalar value types */ +export type ScalarValue = + | AbstractIntValue + | AbstractFloatValue + | I32Value + | U32Value + | I16Value + | U16Value + | I8Value + | U8Value + | F64Value + | F32Value + | F16Value + | BoolValue; + +export interface ScalarBuilder<T> { + (value: T): ScalarValue; +} -/** Create an f16 from a bit representation, a uint16 represented as a JS `number`. */ -export const f16Bits = (bits: number): Scalar => - scalarFromBits(TypeF16, workingDataU16, workingDataF16, bits); +export function isScalarValue(value: object): value is ScalarValue { + return ( + value instanceof AbstractIntValue || + value instanceof AbstractFloatValue || + value instanceof I32Value || + value instanceof U32Value || + value instanceof I16Value || + value instanceof U16Value || + value instanceof I8Value || + value instanceof U8Value || + value instanceof F64Value || + value instanceof F32Value || + value instanceof F16Value || + value instanceof BoolValue + ); +} -/** Create an i32 from a numeric value, a JS `number`. */ -export const i32 = (value: number): Scalar => scalarFromValue(TypeI32, workingDataI32, value); +/** Create an AbstractInt from a numeric value, a JS `bigint`. */ +export function abstractInt(value: bigint) { + workingDataI64[0] = value; + return new AbstractIntValue(workingDataI64[0], workingDataU32[0], workingDataU32[1]); +} -/** Create an i16 from a numeric value, a JS `number`. */ -export const i16 = (value: number): Scalar => scalarFromValue(TypeI16, workingDataI16, value); +/** Create an AbstractInt from a bit representation, a uint64 represented as a JS `bigint`. */ +export function abstractIntBits(value: bigint) { + workingDataU64[0] = value; + return new AbstractIntValue(workingDataI64[0], workingDataU32[0], workingDataU32[1]); +} -/** Create an i8 from a numeric value, a JS `number`. */ -export const i8 = (value: number): Scalar => scalarFromValue(TypeI8, workingDataI8, value); +/** Create an AbstractFloat from a numeric value, a JS `number`. */ +export function abstractFloat(value: number) { + workingDataF64[0] = value; + return new AbstractFloatValue(workingDataF64[0], workingDataU32[0], workingDataU32[1]); +} + +/** Create an i32 from a numeric value, a JS `number`. */ +export function i32(value: number) { + workingDataI32[0] = value; + return new I32Value(workingDataI32[0], workingDataU32[0]); +} /** Create an i32 from a bit representation, a uint32 represented as a JS `number`. */ -export const i32Bits = (bits: number): Scalar => - scalarFromBits(TypeI32, workingDataU32, workingDataI32, bits); +export function i32Bits(bits: number) { + workingDataU32[0] = bits; + return new I32Value(workingDataI32[0], workingDataU32[0]); +} -/** Create an i16 from a bit representation, a uint16 represented as a JS `number`. */ -export const i16Bits = (bits: number): Scalar => - scalarFromBits(TypeI16, workingDataU16, workingDataI16, bits); +/** Create a u32 from a numeric value, a JS `number`. */ +export function u32(value: number) { + workingDataU32[0] = value; + return new U32Value(workingDataU32[0]); +} -/** Create an i8 from a bit representation, a uint8 represented as a JS `number`. */ -export const i8Bits = (bits: number): Scalar => - scalarFromBits(TypeI8, workingDataU8, workingDataI8, bits); +/** Create a u32 from a bit representation, a uint32 represented as a JS `number`. */ +export function u32Bits(bits: number) { + workingDataU32[0] = bits; + return new U32Value(workingDataU32[0]); +} -/** Create a u32 from a numeric value, a JS `number`. */ -export const u32 = (value: number): Scalar => scalarFromValue(TypeU32, workingDataU32, value); +/** Create an i16 from a numeric value, a JS `number`. */ +export function i16(value: number) { + workingDataI16[0] = value; + return new I16Value(workingDataI16[0], workingDataU16[0]); +} /** Create a u16 from a numeric value, a JS `number`. */ -export const u16 = (value: number): Scalar => scalarFromValue(TypeU16, workingDataU16, value); +export function u16(value: number) { + workingDataU16[0] = value; + return new U16Value(workingDataU16[0]); +} + +/** Create an i8 from a numeric value, a JS `number`. */ +export function i8(value: number) { + workingDataI8[0] = value; + return new I8Value(workingDataI8[0], workingDataU8[0]); +} /** Create a u8 from a numeric value, a JS `number`. */ -export const u8 = (value: number): Scalar => scalarFromValue(TypeU8, workingDataU8, value); +export function u8(value: number) { + workingDataU8[0] = value; + return new U8Value(workingDataU8[0]); +} + +/** Create an f64 from a numeric value, a JS `number`. */ +export function f64(value: number) { + workingDataF64[0] = value; + return new F64Value(workingDataF64[0], workingDataU32[0], workingDataU32[1]); +} -/** Create an u32 from a bit representation, a uint32 represented as a JS `number`. */ -export const u32Bits = (bits: number): Scalar => - scalarFromBits(TypeU32, workingDataU32, workingDataU32, bits); +/** Create an f32 from a numeric value, a JS `number`. */ +export function f32(value: number) { + workingDataF32[0] = value; + return new F32Value(workingDataF32[0], workingDataU32[0]); +} + +/** Create an f32 from a bit representation, a uint32 represented as a JS `number`. */ +export function f32Bits(bits: number) { + workingDataU32[0] = bits; + return new F32Value(workingDataF32[0], workingDataU32[0]); +} -/** Create an u16 from a bit representation, a uint16 represented as a JS `number`. */ -export const u16Bits = (bits: number): Scalar => - scalarFromBits(TypeU16, workingDataU16, workingDataU16, bits); +/** Create an f16 from a numeric value, a JS `number`. */ +export function f16(value: number) { + workingDataF16[0] = value; + return new F16Value(workingDataF16[0], workingDataU16[0]); +} -/** Create an u8 from a bit representation, a uint8 represented as a JS `number`. */ -export const u8Bits = (bits: number): Scalar => - scalarFromBits(TypeU8, workingDataU8, workingDataU8, bits); +/** Create an f16 from a bit representation, a uint16 represented as a JS `number`. */ +export function f16Bits(bits: number) { + workingDataU16[0] = bits; + return new F16Value(workingDataF16[0], workingDataU16[0]); +} /** Create a boolean value. */ -export function bool(value: boolean): Scalar { - // WGSL does not support using 'bool' types directly in storage / uniform - // buffers, so instead we pack booleans in a u32, where 'false' is zero and - // 'true' is any non-zero value. - workingDataU32[0] = value ? 1 : 0; - workingDataU32[1] = 0; - return new Scalar(TypeBool, value, workingDataU32[1], workingDataU32[0]); +export function bool(value: boolean): ScalarValue { + return new BoolValue(value); } /** A 'true' literal value */ @@ -1099,11 +1813,11 @@ export const False = bool(false); /** * Class that encapsulates a vector value. */ -export class Vector { - readonly elements: Array<Scalar>; +export class VectorValue { + readonly elements: Array<ScalarValue>; readonly type: VectorType; - public constructor(elements: Array<Scalar>) { + public constructor(elements: Array<ScalarValue>) { if (elements.length < 2 || elements.length > 4) { throw new Error(`vector element count must be between 2 and 4, got ${elements.length}`); } @@ -1117,7 +1831,7 @@ export class Vector { } } this.elements = elements; - this.type = TypeVec(elements.length, elements[0].type); + this.type = VectorType.create(elements.length, elements[0].type); } /** @@ -1165,19 +1879,24 @@ export class Vector { } } +/** Helper for constructing a new vector with the provided values */ +export function vec(...elements: ScalarValue[]) { + return new VectorValue(elements); +} + /** Helper for constructing a new two-element vector with the provided values */ -export function vec2(x: Scalar, y: Scalar) { - return new Vector([x, y]); +export function vec2(x: ScalarValue, y: ScalarValue) { + return new VectorValue([x, y]); } /** Helper for constructing a new three-element vector with the provided values */ -export function vec3(x: Scalar, y: Scalar, z: Scalar) { - return new Vector([x, y, z]); +export function vec3(x: ScalarValue, y: ScalarValue, z: ScalarValue) { + return new VectorValue([x, y, z]); } /** Helper for constructing a new four-element vector with the provided values */ -export function vec4(x: Scalar, y: Scalar, z: Scalar, w: Scalar) { - return new Vector([x, y, z, w]); +export function vec4(x: ScalarValue, y: ScalarValue, z: ScalarValue, w: ScalarValue) { + return new VectorValue([x, y, z, w]); } /** @@ -1186,7 +1905,7 @@ export function vec4(x: Scalar, y: Scalar, z: Scalar, w: Scalar) { * @param v array of numbers to be converted, must contain 2, 3 or 4 elements * @param op function to convert from number to Scalar, e.g. 'f32` */ -export function toVector(v: readonly number[], op: (n: number) => Scalar): Vector { +export function toVector(v: readonly number[], op: (n: number) => ScalarValue): VectorValue { switch (v.length) { case 2: return vec2(op(v[0]), op(v[1])); @@ -1201,11 +1920,11 @@ export function toVector(v: readonly number[], op: (n: number) => Scalar): Vecto /** * Class that encapsulates a Matrix value. */ -export class Matrix { - readonly elements: Scalar[][]; +export class MatrixValue { + readonly elements: ScalarValue[][]; readonly type: MatrixType; - public constructor(elements: Array<Array<Scalar>>) { + public constructor(elements: Array<Array<ScalarValue>>) { const num_cols = elements.length; if (num_cols < 2 || num_cols > 4) { throw new Error(`matrix cols count must be between 2 and 4, got ${num_cols}`); @@ -1226,7 +1945,7 @@ export class Matrix { } this.elements = elements; - this.type = TypeMat(num_cols, num_rows, elem_type); + this.type = MatrixType.create(num_cols, num_rows, elem_type); } /** @@ -1262,41 +1981,90 @@ export class Matrix { } /** + * Class that encapsulates an Array value. + */ +export class ArrayValue { + readonly elements: Value[]; + readonly type: ArrayType; + + public constructor(elements: Array<Value>) { + const elem_type = elements[0].type; + if (!elements.every(c => elements.every(r => objectEquals(r.type, elem_type)))) { + throw new Error(`cannot mix array element types`); + } + + this.elements = elements; + this.type = ArrayType.create(elements.length, elem_type); + } + + /** + * Copies the array value to the Uint8Array buffer at the provided byte offset. + * @param buffer the destination buffer + * @param offset the byte offset within buffer + */ + public copyTo(buffer: Uint8Array, offset: number) { + for (const element of this.elements) { + element.copyTo(buffer, offset); + offset += this.type.elementType.size; + } + } + + /** + * @returns the WGSL representation of this array value + */ + public wgsl(): string { + const els = this.elements.map(r => r.wgsl()).join(', '); + return isAbstractType(this.type.elementType) ? `array(${els})` : `${this.type}(${els})`; + } + + public toString(): string { + return this.wgsl(); + } +} + +/** Helper for constructing an ArrayValue with the provided values */ +export function array(...elements: Value[]) { + return new ArrayValue(elements); +} + +/** * Helper for constructing Matrices from arrays of numbers * * @param m array of array of numbers to be converted, all Array of number must * be of the same length. All Arrays must have 2, 3, or 4 elements. * @param op function to convert from number to Scalar, e.g. 'f32` */ -export function toMatrix(m: ROArrayArray<number>, op: (n: number) => Scalar): Matrix { +export function toMatrix(m: ROArrayArray<number>, op: (n: number) => ScalarValue): MatrixValue { const cols = m.length; const rows = m[0].length; - const elements: Scalar[][] = [...Array<Scalar[]>(cols)].map(_ => [...Array<Scalar>(rows)]); + const elements: ScalarValue[][] = [...Array<ScalarValue[]>(cols)].map(_ => [ + ...Array<ScalarValue>(rows), + ]); for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { elements[i][j] = op(m[i][j]); } } - return new Matrix(elements); + return new MatrixValue(elements); } -/** Value is a Scalar or Vector value. */ -export type Value = Scalar | Vector | Matrix; +/** Value is a Scalar, Vector, Matrix or Array value. */ +export type Value = ScalarValue | VectorValue | MatrixValue | ArrayValue; -export type SerializedValueScalar = { +export type SerializedScalarValue = { kind: 'scalar'; type: ScalarKind; value: boolean | number; }; -export type SerializedValueVector = { +export type SerializedVectorValue = { kind: 'vector'; type: ScalarKind; value: boolean[] | readonly number[]; }; -export type SerializedValueMatrix = { +export type SerializedMatrixValue = { kind: 'matrix'; type: ScalarKind; value: ROArrayArray<number>; @@ -1314,6 +2082,7 @@ enum SerializedScalarKind { I16, I8, Bool, + AbstractInt, } /** serializeScalarKind() serializes a ScalarKind to a BinaryStream */ @@ -1340,6 +2109,9 @@ function serializeScalarKind(s: BinaryStream, v: ScalarKind) { case 'u8': s.writeU8(SerializedScalarKind.U8); return; + case 'abstract-int': + s.writeU8(SerializedScalarKind.AbstractInt); + return; case 'i32': s.writeU8(SerializedScalarKind.I32); return; @@ -1353,6 +2125,7 @@ function serializeScalarKind(s: BinaryStream, v: ScalarKind) { s.writeU8(SerializedScalarKind.Bool); return; } + unreachable(`Do not know what to write scalar kind = ${v}`); } /** deserializeScalarKind() deserializes a ScalarKind from a BinaryStream */ @@ -1373,6 +2146,8 @@ function deserializeScalarKind(s: BinaryStream): ScalarKind { return 'u16'; case SerializedScalarKind.U8: return 'u8'; + case SerializedScalarKind.AbstractInt: + return 'abstract-int'; case SerializedScalarKind.I32: return 'i32'; case SerializedScalarKind.I16: @@ -1394,51 +2169,66 @@ enum SerializedValueKind { /** serializeValue() serializes a Value to a BinaryStream */ export function serializeValue(s: BinaryStream, v: Value) { - const serializeScalar = (scalar: Scalar, kind: ScalarKind) => { - switch (kind) { - case 'abstract-float': - s.writeF64(scalar.value as number); - return; - case 'f64': - s.writeF64(scalar.value as number); - return; - case 'f32': - s.writeF32(scalar.value as number); - return; - case 'f16': - s.writeF16(scalar.value as number); - return; - case 'u32': - s.writeU32(scalar.value as number); - return; - case 'u16': - s.writeU16(scalar.value as number); - return; - case 'u8': - s.writeU8(scalar.value as number); - return; - case 'i32': - s.writeI32(scalar.value as number); - return; - case 'i16': - s.writeI16(scalar.value as number); - return; - case 'i8': - s.writeI8(scalar.value as number); - return; - case 'bool': - s.writeBool(scalar.value as boolean); - return; + const serializeScalar = (scalar: ScalarValue, kind: ScalarKind) => { + switch (typeof scalar.value) { + case 'number': + switch (kind) { + case 'abstract-float': + s.writeF64(scalar.value); + return; + case 'f64': + s.writeF64(scalar.value); + return; + case 'f32': + s.writeF32(scalar.value); + return; + case 'f16': + s.writeF16(scalar.value); + return; + case 'u32': + s.writeU32(scalar.value); + return; + case 'u16': + s.writeU16(scalar.value); + return; + case 'u8': + s.writeU8(scalar.value); + return; + case 'i32': + s.writeI32(scalar.value); + return; + case 'i16': + s.writeI16(scalar.value); + return; + case 'i8': + s.writeI8(scalar.value); + return; + } + break; + case 'bigint': + switch (kind) { + case 'abstract-int': + s.writeI64(scalar.value); + return; + } + break; + case 'boolean': + switch (kind) { + case 'bool': + s.writeBool(scalar.value); + return; + } + break; } }; - if (v instanceof Scalar) { + if (isScalarValue(v)) { s.writeU8(SerializedValueKind.Scalar); serializeScalarKind(s, v.type.kind); serializeScalar(v, v.type.kind); return; } - if (v instanceof Vector) { + if (v instanceof VectorValue) { s.writeU8(SerializedValueKind.Vector); serializeScalarKind(s, v.type.elementType.kind); s.writeU8(v.type.width); @@ -1447,7 +2237,7 @@ export function serializeValue(s: BinaryStream, v: Value) { } return; } - if (v instanceof Matrix) { + if (v instanceof MatrixValue) { s.writeU8(SerializedValueKind.Matrix); serializeScalarKind(s, v.type.elementType.kind); s.writeU8(v.type.cols); @@ -1481,6 +2271,8 @@ export function deserializeValue(s: BinaryStream): Value { return u16(s.readU16()); case 'u8': return u8(s.readU8()); + case 'abstract-int': + return abstractInt(s.readI64()); case 'i32': return i32(s.readI32()); case 'i16': @@ -1498,23 +2290,23 @@ export function deserializeValue(s: BinaryStream): Value { return deserializeScalar(scalarKind); case SerializedValueKind.Vector: { const width = s.readU8(); - const scalars = new Array<Scalar>(width); + const scalars = new Array<ScalarValue>(width); for (let i = 0; i < width; i++) { scalars[i] = deserializeScalar(scalarKind); } - return new Vector(scalars); + return new VectorValue(scalars); } case SerializedValueKind.Matrix: { const numCols = s.readU8(); const numRows = s.readU8(); - const columns = new Array<Scalar[]>(numCols); + const columns = new Array<ScalarValue[]>(numCols); for (let c = 0; c < numCols; c++) { - columns[c] = new Array<Scalar>(numRows); + columns[c] = new Array<ScalarValue>(numRows); for (let i = 0; i < numRows; i++) { columns[c][i] = deserializeScalar(scalarKind); } } - return new Matrix(columns); + return new MatrixValue(columns); } default: unreachable(`invalid serialized value kind: ${valueKind}`); @@ -1533,7 +2325,7 @@ export function isFloatValue(v: Value): boolean { */ export function isAbstractType(ty: Type): boolean { if (ty instanceof ScalarType) { - return ty.kind === 'abstract-float'; + return ty.kind === 'abstract-float' || ty.kind === 'abstract-int'; } return false; } @@ -1552,84 +2344,222 @@ export function isFloatType(ty: Type): boolean { return false; } +/** + * @returns if `ty` is a type convertible to floating point type. + * @note this does not consider composite types. + * Use elementType() if you want to test the element type. + */ +export function isConvertibleToFloatType(ty: Type): boolean { + if (ty instanceof ScalarType) { + return ( + ty.kind === 'abstract-int' || + ty.kind === 'abstract-float' || + ty.kind === 'f64' || + ty.kind === 'f32' || + ty.kind === 'f16' + ); + } + return false; +} + +/** + * @returns if `ty` is an unsigned type. + */ +export function isUnsignedType(ty: Type): boolean { + if (ty instanceof ScalarType) { + return ty.kind === 'u8' || ty.kind === 'u16' || ty.kind === 'u32'; + } else { + return isUnsignedType(ty.elementType); + } +} + +/** @returns true if an argument of type 'src' can be used for a parameter of type 'dst' */ +export function isConvertible(src: Type, dst: Type) { + if (src === dst) { + return true; + } + + const widthOf = (ty: Type) => { + return ty instanceof VectorType ? ty.width : 1; + }; + + if (widthOf(src) !== widthOf(dst)) { + return false; + } + + const elSrc = scalarTypeOf(src); + const elDst = scalarTypeOf(dst); + + switch (elSrc.kind) { + case 'abstract-float': + switch (elDst.kind) { + case 'abstract-float': + case 'f16': + case 'f32': + case 'f64': + return true; + default: + return false; + } + case 'abstract-int': + switch (elDst.kind) { + case 'abstract-int': + case 'abstract-float': + case 'f16': + case 'f32': + case 'f64': + case 'u16': + case 'u32': + case 'u8': + case 'i16': + case 'i32': + case 'i8': + return true; + default: + return false; + } + default: + return false; + } +} + /// All floating-point scalar types -export const kAllFloatScalars = [TypeAbstractFloat, TypeF32, TypeF16] as const; +const kFloatScalars = [Type.abstractFloat, Type.f32, Type.f16] as const; /// All floating-point vec2 types -export const kAllFloatVector2 = [ - TypeVec(2, TypeAbstractFloat), - TypeVec(2, TypeF32), - TypeVec(2, TypeF16), -] as const; +const kFloatVec2 = [Type.vec2af, Type.vec2f, Type.vec2h] as const; /// All floating-point vec3 types -export const kAllFloatVector3 = [ - TypeVec(3, TypeAbstractFloat), - TypeVec(3, TypeF32), - TypeVec(3, TypeF16), -] as const; +const kFloatVec3 = [Type.vec3af, Type.vec3f, Type.vec3h] as const; /// All floating-point vec4 types -export const kAllFloatVector4 = [ - TypeVec(4, TypeAbstractFloat), - TypeVec(4, TypeF32), - TypeVec(4, TypeF16), +const kFloatVec4 = [Type.vec4af, Type.vec4f, Type.vec4h] as const; + +export const kConcreteF32ScalarsAndVectors = [ + Type.f32, + Type.vec2f, + Type.vec3f, + Type.vec4f, ] as const; -/// All floating-point vector types -export const kAllFloatVectors = [ - ...kAllFloatVector2, - ...kAllFloatVector3, - ...kAllFloatVector4, +/// All f16 floating-point scalar and vector types +export const kConcreteF16ScalarsAndVectors = [ + Type.f16, + Type.vec2h, + Type.vec3h, + Type.vec4h, ] as const; +/// All floating-point vector types +export const kFloatVectors = [...kFloatVec2, ...kFloatVec3, ...kFloatVec4] as const; + /// All floating-point scalar and vector types -export const kAllFloatScalarsAndVectors = [...kAllFloatScalars, ...kAllFloatVectors] as const; - -/// All integer scalar and vector types -export const kAllIntegerScalarsAndVectors = [ - TypeI32, - TypeVec(2, TypeI32), - TypeVec(3, TypeI32), - TypeVec(4, TypeI32), - TypeU32, - TypeVec(2, TypeU32), - TypeVec(3, TypeU32), - TypeVec(4, TypeU32), +export const kFloatScalarsAndVectors = [...kFloatScalars, ...kFloatVectors] as const; + +// Abstract and concrete integer types are not grouped into an 'all' type, +// because for many validation tests there is a valid conversion of +// AbstractInt -> AbstractFloat, but not one for the concrete integers. Thus, an +// AbstractInt literal will be a potentially valid input, whereas the concrete +// integers will not be. For many tests the pattern is to have separate fixtures +// for the things that might be valid and those that are never valid. + +/// All signed integer vector types +export const kConcreteSignedIntegerVectors = [Type.vec2i, Type.vec3i, Type.vec4i] as const; + +/// All unsigned integer vector types +export const kConcreteUnsignedIntegerVectors = [Type.vec2u, Type.vec3u, Type.vec4u] as const; + +/// All concrete integer vector types +export const kConcreteIntegerVectors = [ + ...kConcreteSignedIntegerVectors, + ...kConcreteUnsignedIntegerVectors, ] as const; /// All signed integer scalar and vector types -export const kAllSignedIntegerScalarsAndVectors = [ - TypeI32, - TypeVec(2, TypeI32), - TypeVec(3, TypeI32), - TypeVec(4, TypeI32), +export const kConcreteSignedIntegerScalarsAndVectors = [ + Type.i32, + ...kConcreteSignedIntegerVectors, ] as const; /// All unsigned integer scalar and vector types -export const kAllUnsignedIntegerScalarsAndVectors = [ - TypeU32, - TypeVec(2, TypeU32), - TypeVec(3, TypeU32), - TypeVec(4, TypeU32), +export const kConcreteUnsignedIntegerScalarsAndVectors = [ + Type.u32, + ...kConcreteUnsignedIntegerVectors, ] as const; -/// All floating-point and integer scalar and vector types -export const kAllFloatAndIntegerScalarsAndVectors = [ - ...kAllFloatScalarsAndVectors, - ...kAllIntegerScalarsAndVectors, +/// All concrete integer scalar and vector types +export const kConcreteIntegerScalarsAndVectors = [ + ...kConcreteSignedIntegerScalarsAndVectors, + ...kConcreteUnsignedIntegerScalarsAndVectors, ] as const; -/// All floating-point and signed integer scalar and vector types -export const kAllFloatAndSignedIntegerScalarsAndVectors = [ - ...kAllFloatScalarsAndVectors, - ...kAllSignedIntegerScalarsAndVectors, +/// All types which are convertable to floating-point scalar types. +export const kConvertableToFloatScalar = [Type.abstractInt, ...kFloatScalars] as const; + +/// All types which are convertable to floating-point vector 2 types. +export const kConvertableToFloatVec2 = [Type.vec2ai, ...kFloatVec2] as const; + +/// All types which are convertable to floating-point vector 3 types. +export const kConvertableToFloatVec3 = [Type.vec3ai, ...kFloatVec3] as const; + +/// All types which are convertable to floating-point vector 4 types. +export const kConvertableToFloatVec4 = [Type.vec4ai, ...kFloatVec4] as const; + +/// All the types which are convertable to floating-point vector types. +export const kConvertableToFloatVectors = [ + Type.vec2ai, + Type.vec3ai, + Type.vec4ai, + ...kFloatVectors, ] as const; -/** @returns the inner element type of the given type */ -export function elementType(t: ScalarType | VectorType | MatrixType) { - if (t instanceof ScalarType) { - return t; - } - return t.elementType; -} +/// All types which are convertable to floating-point scalar or vector types. +export const kConvertableToFloatScalarsAndVectors = [ + Type.abstractInt, + ...kFloatScalars, + ...kConvertableToFloatVectors, +] as const; + +/// All the numeric scalar and vector types. +export const kAllNumericScalarsAndVectors = [ + ...kConvertableToFloatScalarsAndVectors, + ...kConcreteIntegerScalarsAndVectors, +] as const; + +/// All the concrete integer and floating point scalars and vectors. +export const kConcreteNumericScalarsAndVectors = [ + ...kConcreteIntegerScalarsAndVectors, + ...kConcreteF16ScalarsAndVectors, + ...kConcreteF32ScalarsAndVectors, +] as const; + +/// All boolean types. +export const kAllBoolScalarsAndVectors = [Type.bool, Type.vec2b, Type.vec3b, Type.vec4b] as const; + +/// All the scalar and vector types. +export const kAllScalarsAndVectors = [ + ...kAllBoolScalarsAndVectors, + ...kAllNumericScalarsAndVectors, +] as const; + +/// All the matrix types +export const kAllMatrices = [ + Type.mat2x2f, + Type.mat2x2h, + Type.mat2x3f, + Type.mat2x3h, + Type.mat2x4f, + Type.mat2x4h, + Type.mat3x2f, + Type.mat3x2h, + Type.mat3x3f, + Type.mat3x3h, + Type.mat3x4f, + Type.mat3x4h, + Type.mat4x2f, + Type.mat4x2h, + Type.mat4x3f, + Type.mat4x3h, + Type.mat4x4f, + Type.mat4x4h, +] as const; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts index 1e6c0402cb..8a90f43248 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts @@ -9,7 +9,12 @@ import { } from '../../common/util/util.js'; import { getDefaultLimits, kLimits } from '../capability_info.js'; +// MUST_NOT_BE_IMPORTED_BY_DATA_CACHE +// This file should not be transitively imported by .cache.ts files + export interface DeviceProvider { + /** Adapter the device was created from. Cannot be reused; just for adapter info. */ + readonly adapter: GPUAdapter; readonly device: GPUDevice; expectDeviceLost(reason: GPUDeviceLostReason): void; } @@ -283,6 +288,8 @@ type DeviceHolderState = 'free' | 'acquired'; * Holds a GPUDevice and tracks its state (free/acquired) and handles device loss. */ class DeviceHolder implements DeviceProvider { + /** Adapter the device was created from. Cannot be reused; just for adapter info. */ + readonly adapter: GPUAdapter; /** The device. Will be cleared during cleanup if there were unexpected errors. */ private _device: GPUDevice | undefined; /** Whether the device is in use by a test or not. */ @@ -307,10 +314,11 @@ class DeviceHolder implements DeviceProvider { const device = await adapter.requestDevice(descriptor); assert(device !== null, 'requestDevice returned null'); - return new DeviceHolder(device); + return new DeviceHolder(adapter, device); } - private constructor(device: GPUDevice) { + private constructor(adapter: GPUAdapter, device: GPUDevice) { + this.adapter = adapter; this._device = device; void this._device.lost.then(ev => { this.lostInfo = ev; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/floating_point.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/floating_point.ts index e271e7db7a..b644052ebf 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/floating_point.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/floating_point.ts @@ -1,7 +1,8 @@ import { ROArrayArray, ROArrayArrayArray } from '../../common/util/types.js'; import { assert, unreachable } from '../../common/util/util.js'; import { Float16Array } from '../../external/petamoriken/float16/float16.js'; -import { Case, IntervalFilter } from '../shader/execution/expression/expression.js'; +import { Case } from '../shader/execution/expression/case.js'; +import { IntervalFilter } from '../shader/execution/expression/interval_filter.js'; import BinaryStream from './binary_stream.js'; import { anyOf } from './compare.js'; @@ -11,7 +12,7 @@ import { f16, f32, isFloatType, - Scalar, + ScalarValue, ScalarType, toMatrix, toVector, @@ -23,6 +24,7 @@ import { correctlyRoundedF16, correctlyRoundedF32, correctlyRoundedF64, + every2DArray, flatten2DArray, FlushMode, flushSubnormalNumberF16, @@ -36,10 +38,24 @@ import { map2DArray, oneULPF16, oneULPF32, - quantizeToF32, quantizeToF16, + quantizeToF32, + scalarF16Range, + scalarF32Range, + scalarF64Range, + sparseMatrixF16Range, + sparseMatrixF32Range, + sparseMatrixF64Range, + sparseScalarF16Range, + sparseScalarF32Range, + sparseScalarF64Range, + sparseVectorF16Range, + sparseVectorF32Range, + sparseVectorF64Range, unflatten2DArray, - every2DArray, + vectorF16Range, + vectorF32Range, + vectorF64Range, } from './math.js'; /** Indicate the kind of WGSL floating point numbers being operated on */ @@ -83,12 +99,12 @@ export function deserializeFPKind(s: BinaryStream): FPKind { // Containers /** - * Representation of bounds for an interval as an array with either one or two - * elements. Single element indicates that the interval is a single point. For - * two elements, the first is the lower bound of the interval and the second is - * the upper bound. + * Representation of endpoints for an interval as an array with either one or + * two elements. Single element indicates that the interval is a single point. + * For two elements, the first is the lower edges of the interval and the + * second is the upper edge, i.e. e[0] <= e[1], where e is an IntervalEndpoints */ -export type IntervalBounds = readonly [number] | readonly [number, number]; +export type IntervalEndpoints = readonly [number] | readonly [number, number]; /** Represents a closed interval of floating point numbers */ export class FPInterval { @@ -102,15 +118,18 @@ export class FPInterval { * `FPTraits.toInterval` is the preferred way to create FPIntervals * * @param kind the floating point number type this is an interval for - * @param bounds beginning and end of the interval + * @param endpoints beginning and end of the interval */ - public constructor(kind: FPKind, ...bounds: IntervalBounds) { + public constructor(kind: FPKind, ...endpoints: IntervalEndpoints) { this.kind = kind; - const begin = bounds[0]; - const end = bounds.length === 2 ? bounds[1] : bounds[0]; - assert(!Number.isNaN(begin) && !Number.isNaN(end), `bounds need to be non-NaN`); - assert(begin <= end, `bounds[0] (${begin}) must be less than or equal to bounds[1] (${end})`); + const begin = endpoints[0]; + const end = endpoints.length === 2 ? endpoints[1] : endpoints[0]; + assert(!Number.isNaN(begin) && !Number.isNaN(end), `endpoints need to be non-NaN`); + assert( + begin <= end, + `endpoints[0] (${begin}) must be less than or equal to endpoints[1] (${end})` + ); this.begin = begin; this.end = end; @@ -122,7 +141,7 @@ export class FPInterval { } /** @returns begin and end if non-point interval, otherwise just begin */ - public bounds(): IntervalBounds { + public endpoints(): IntervalEndpoints { return this.isPoint() ? [this.begin] : [this.begin, this.end]; } @@ -163,7 +182,7 @@ export class FPInterval { /** @returns a string representation for logging purposes */ public toString(): string { - return `{ '${this.kind}', [${this.bounds().map(this.traits().scalarBuilder)}] }`; + return `{ '${this.kind}', [${this.endpoints().map(this.traits().scalarBuilder)}] }`; } } @@ -312,7 +331,7 @@ interface ScalarToIntervalOp { * occur and returns a span of those points to be used as the domain instead. * * Used by this.runScalarToIntervalOp before invoking impl. - * If not defined, the bounds of the existing domain are assumed to be the + * If not defined, the endpoints of the existing domain are assumed to be the * extrema. * * This is only implemented for operations that meet all the following @@ -323,6 +342,14 @@ interface ScalarToIntervalOp { * i.e. fooInterval takes in x: number | FPInterval, not x: number */ extrema?: (x: FPInterval) => FPInterval; + + /** + * Restricts the inputs to operation to the given domain. + * + * Only defined for operations that have tighter domain requirements than 'must + * be finite'. + */ + domain?: () => FPInterval; } /** @@ -334,6 +361,13 @@ export interface ScalarPairToInterval { (x: number, y: number): FPInterval; } +/** Domain for a ScalarPairToInterval implementation */ +interface ScalarPairToIntervalDomain { + // Arrays to support discrete valid domain intervals + x: readonly FPInterval[]; + y: readonly FPInterval[]; +} + /** Operation used to implement a ScalarPairToInterval */ interface ScalarPairToIntervalOp { /** @returns acceptance interval for a function at point (x, y) */ @@ -343,23 +377,24 @@ interface ScalarPairToIntervalOp { * occur and returns spans of those points to be used as the domain instead. * * Used by runScalarPairToIntervalOp before invoking impl. - * If not defined, the bounds of the existing domain are assumed to be the + * If not defined, the endpoints of the existing domain are assumed to be the * extrema. * - * This is only implemented for functions that meet all of the following + * This is only implemented for functions that meet all the following * criteria: * a) non-monotonic * b) used in inherited accuracy calculations * c) need to take in an interval for b) */ extrema?: (x: FPInterval, y: FPInterval) => [FPInterval, FPInterval]; -} -/** Domain for a ScalarPairToInterval implementation */ -interface ScalarPairToIntervalDomain { - // Arrays to support discrete valid domain intervals - x: readonly FPInterval[]; - y: readonly FPInterval[]; + /** + * Restricts the inputs to operation to the given domain. + * + * Only defined for operations that have tighter domain requirements than 'must + * be finite'. + */ + domain?: () => ScalarPairToIntervalDomain; } /** @@ -556,7 +591,7 @@ export interface VectorMatrixToVector { // Traits /** - * Typed structure containing all the limits/constants defined for each + * Typed structure containing all the constants defined for each * WGSL floating point kind */ interface FPConstants { @@ -599,10 +634,12 @@ interface FPConstants { sixth: number; }; }; + bias: number; unboundedInterval: FPInterval; zeroInterval: FPInterval; negPiToPiInterval: FPInterval; greaterThanZeroInterval: FPInterval; + negOneToOneInterval: FPInterval; zeroVector: { 2: FPVector; 3: FPVector; @@ -635,7 +672,7 @@ interface FPConstants { /** A representation of an FPInterval for a case param */ export type FPIntervalParam = { kind: FPKind; - interval: number | IntervalBounds; + interval: number | IntervalEndpoints; }; /** Abstract base class for all floating-point traits */ @@ -650,7 +687,7 @@ export abstract class FPTraits { // Utilities - Implemented /** @returns an interval containing the point or the original interval */ - public toInterval(n: number | IntervalBounds | FPInterval): FPInterval { + public toInterval(n: number | IntervalEndpoints | FPInterval): FPInterval { if (n instanceof FPInterval) { if (n.kind === this.kind) { return n; @@ -661,7 +698,7 @@ export abstract class FPTraits { return this.constants().unboundedInterval; } - return new FPInterval(this.kind, ...n.bounds()); + return new FPInterval(this.kind, ...n.endpoints()); } if (n instanceof Array) { @@ -674,7 +711,7 @@ export abstract class FPTraits { /** * Makes a param that can be turned into an interval */ - public toParam(n: number | IntervalBounds): FPIntervalParam { + public toParam(n: number | IntervalEndpoints): FPIntervalParam { return { kind: this.kind, interval: n, @@ -685,18 +722,18 @@ export abstract class FPTraits { * Converts p into an FPInterval if it is an FPIntervalPAram */ public fromParam( - p: number | IntervalBounds | FPIntervalParam - ): number | IntervalBounds | FPInterval { + p: number | IntervalEndpoints | FPIntervalParam + ): number | IntervalEndpoints | FPInterval { const param = p as FPIntervalParam; if (param.interval && param.kind) { assert(param.kind === this.kind); return this.toInterval(param.interval); } - return p as number | IntervalBounds; + return p as number | IntervalEndpoints; } /** - * @returns an interval with the tightest bounds that includes all provided + * @returns an interval with the tightest endpoints that includes all provided * intervals */ public spanIntervals(...intervals: readonly FPInterval[]): FPInterval { @@ -715,7 +752,7 @@ export abstract class FPTraits { } /** Narrow an array of values to FPVector if possible */ - public isVector(v: ReadonlyArray<number | IntervalBounds | FPInterval>): v is FPVector { + public isVector(v: ReadonlyArray<number | IntervalEndpoints | FPInterval>): v is FPVector { if (v.every(e => e instanceof FPInterval && e.kind === this.kind)) { return v.length === 2 || v.length === 3 || v.length === 4; } @@ -723,7 +760,7 @@ export abstract class FPTraits { } /** @returns an FPVector representation of an array of values if possible */ - public toVector(v: ReadonlyArray<number | IntervalBounds | FPInterval>): FPVector { + public toVector(v: ReadonlyArray<number | IntervalEndpoints | FPInterval>): FPVector { if (this.isVector(v) && v.every(e => e.kind === this.kind)) { return v; } @@ -762,7 +799,7 @@ export abstract class FPTraits { } /** Narrow an array of an array of values to FPMatrix if possible */ - public isMatrix(m: Array2D<number | IntervalBounds | FPInterval> | FPVector[]): m is FPMatrix { + public isMatrix(m: Array2D<number | IntervalEndpoints | FPInterval> | FPVector[]): m is FPMatrix { if (!m.every(c => c.every(e => e instanceof FPInterval && e.kind === this.kind))) { return false; } @@ -787,7 +824,7 @@ export abstract class FPTraits { } /** @returns an FPMatrix representation of an array of an array of values if possible */ - public toMatrix(m: Array2D<number | IntervalBounds | FPInterval> | FPVector[]): FPMatrix { + public toMatrix(m: Array2D<number | IntervalEndpoints | FPInterval> | FPVector[]): FPMatrix { if ( this.isMatrix(m) && every2DArray(m, (e: FPInterval) => { @@ -840,48 +877,6 @@ export abstract class FPTraits { return needs_zero ? values.concat(0) : values; } - /** - * Restrict the inputs to an ScalarToInterval operation - * - * Only used for operations that have tighter domain requirements than 'must - * be finite'. - * - * @param domain interval to restrict inputs to - * @param impl operation implementation to run if input is within the required domain - * @returns a ScalarToInterval that calls impl if domain contains the input, - * otherwise it returns an unbounded interval */ - protected limitScalarToIntervalDomain( - domain: FPInterval, - impl: ScalarToInterval - ): ScalarToInterval { - return (n: number): FPInterval => { - return domain.contains(n) ? impl(n) : this.constants().unboundedInterval; - }; - } - - /** - * Restrict the inputs to a ScalarPairToInterval - * - * Only used for operations that have tighter domain requirements than 'must be - * finite'. - * - * @param domain set of intervals to restrict inputs to - * @param impl operation implementation to run if input is within the required domain - * @returns a ScalarPairToInterval that calls impl if domain contains the input, - * otherwise it returns an unbounded interval */ - protected limitScalarPairToIntervalDomain( - domain: ScalarPairToIntervalDomain, - impl: ScalarPairToInterval - ): ScalarPairToInterval { - return (x: number, y: number): FPInterval => { - if (!domain.x.some(d => d.contains(x)) || !domain.y.some(d => d.contains(y))) { - return this.constants().unboundedInterval; - } - - return impl(x, y); - }; - } - /** Stub for scalar to interval generator */ protected unimplementedScalarToInterval(name: string, _x: number | FPInterval): FPInterval { unreachable(`'${name}' is not yet implemented for '${this.kind}'`); @@ -1053,14 +1048,14 @@ export abstract class FPTraits { unreachable(`'refract' is not yet implemented for '${this.kind}'`); } - /** Version of absoluteErrorInterval that always returns the unboundedInterval */ - protected unboundedAbsoluteErrorInterval(_n: number, _error_range: number): FPInterval { - return this.constants().unboundedInterval; + /** Stub for absolute errors */ + protected unimplementedAbsoluteErrorInterval(_n: number, _error_range: number): FPInterval { + unreachable(`Absolute Error is not implement for '${this.kind}'`); } - /** Version of ulpInterval that always returns the unboundedInterval */ - protected unboundedUlpInterval(_n: number, _numULP: number): FPInterval { - return this.constants().unboundedInterval; + /** Stub for ULP errors */ + protected unimplementedUlpInterval(_n: number, _numULP: number): FPInterval { + unreachable(`ULP Error is not implement for '${this.kind}'`); } // Utilities - Defined by subclass @@ -1079,8 +1074,22 @@ export abstract class FPTraits { public abstract readonly flushSubnormal: (n: number) => number; /** @returns 1 * ULP: (number) */ public abstract readonly oneULP: (target: number, mode?: FlushMode) => number; - /** @returns a builder for converting numbers to Scalars */ - public abstract readonly scalarBuilder: (n: number) => Scalar; + /** @returns a builder for converting numbers to ScalarsValues */ + public abstract readonly scalarBuilder: (n: number) => ScalarValue; + /** @returns a range of scalars for testing */ + public abstract scalarRange(): readonly number[]; + /** @returns a reduced range of scalars for testing */ + public abstract sparseScalarRange(): readonly number[]; + /** @returns a range of dim element vectors for testing */ + public abstract vectorRange(dim: number): ROArrayArray<number>; + /** @returns a reduced range of dim element vectors for testing */ + public abstract sparseVectorRange(dim: number): ROArrayArray<number>; + /** @returns a reduced range of cols x rows matrices for testing + * + * A non-sparse version of this generator is intentionally not provided due to + * runtime issues with more dense ranges. + */ + public abstract sparseMatrixRange(cols: number, rows: number): ROArrayArrayArray<number>; // Framework - Cases @@ -1705,7 +1714,6 @@ export abstract class FPTraits { ): Case | undefined { param0 = map2DArray(param0, this.quantize); param1 = map2DArray(param1, this.quantize); - const results = ops.map(o => o(param0, param1)); if (filter === 'finite' && results.some(m => m.some(c => c.some(r => !r.isFinite())))) { return undefined; @@ -1973,6 +1981,15 @@ export abstract class FPTraits { assert(!Number.isNaN(n), `flush not defined for NaN`); const values = this.correctlyRounded(n); const inputs = this.addFlushedIfNeeded(values); + + if (op.domain !== undefined) { + // Cannot invoke op.domain() directly in the .some, because the narrowing doesn't propegate. + const domain = op.domain(); + if (inputs.some(i => !domain.contains(i))) { + return this.constants().unboundedInterval; + } + } + const results = new Set<FPInterval>(inputs.map(op.impl)); return this.spanIntervals(...results); } @@ -1998,10 +2015,25 @@ export abstract class FPTraits { ): FPInterval { assert(!Number.isNaN(x), `flush not defined for NaN`); assert(!Number.isNaN(y), `flush not defined for NaN`); + const x_values = this.correctlyRounded(x); const y_values = this.correctlyRounded(y); const x_inputs = this.addFlushedIfNeeded(x_values); const y_inputs = this.addFlushedIfNeeded(y_values); + + if (op.domain !== undefined) { + // Cannot invoke op.domain() directly in the .some, because the narrowing doesn't propegate. + const domain = op.domain(); + + if (x_inputs.some(i => !domain.x.some(e => e.contains(i)))) { + return this.constants().unboundedInterval; + } + + if (y_inputs.some(j => !domain.y.some(e => e.contains(j)))) { + return this.constants().unboundedInterval; + } + } + const intervals = new Set<FPInterval>(); x_inputs.forEach(inner_x => { y_inputs.forEach(inner_y => { @@ -2252,7 +2284,7 @@ export abstract class FPTraits { } const result = this.spanIntervals( - ...x.bounds().map(b => this.roundAndFlushScalarToInterval(b, op)) + ...x.endpoints().map(b => this.roundAndFlushScalarToInterval(b, op)) ); return result.isFinite() ? result : this.constants().unboundedInterval; } @@ -2282,8 +2314,8 @@ export abstract class FPTraits { } const outputs = new Set<FPInterval>(); - x.bounds().forEach(inner_x => { - y.bounds().forEach(inner_y => { + x.endpoints().forEach(inner_x => { + y.endpoints().forEach(inner_y => { outputs.add(this.roundAndFlushScalarPairToInterval(inner_x, inner_y, op)); }); }); @@ -2312,9 +2344,9 @@ export abstract class FPTraits { } const outputs = new Set<FPInterval>(); - x.bounds().forEach(inner_x => { - y.bounds().forEach(inner_y => { - z.bounds().forEach(inner_z => { + x.endpoints().forEach(inner_x => { + y.endpoints().forEach(inner_y => { + z.endpoints().forEach(inner_z => { outputs.add(this.roundAndFlushScalarTripleToInterval(inner_x, inner_y, inner_z, op)); }); }); @@ -2337,7 +2369,7 @@ export abstract class FPTraits { return this.constants().unboundedInterval; } - const x_values = cartesianProduct<number>(...x.map(e => e.bounds())); + const x_values = cartesianProduct<number>(...x.map(e => e.endpoints())); const outputs = new Set<FPInterval>(); x_values.forEach(inner_x => { @@ -2366,8 +2398,8 @@ export abstract class FPTraits { return this.constants().unboundedInterval; } - const x_values = cartesianProduct<number>(...x.map(e => e.bounds())); - const y_values = cartesianProduct<number>(...y.map(e => e.bounds())); + const x_values = cartesianProduct<number>(...x.map(e => e.endpoints())); + const y_values = cartesianProduct<number>(...y.map(e => e.endpoints())); const outputs = new Set<FPInterval>(); x_values.forEach(inner_x => { @@ -2393,7 +2425,7 @@ export abstract class FPTraits { return this.constants().unboundedVector[x.length]; } - const x_values = cartesianProduct<number>(...x.map(e => e.bounds())); + const x_values = cartesianProduct<number>(...x.map(e => e.endpoints())); const outputs = new Set<FPVector>(); x_values.forEach(inner_x => { @@ -2437,8 +2469,8 @@ export abstract class FPTraits { return this.constants().unboundedVector[x.length]; } - const x_values = cartesianProduct<number>(...x.map(e => e.bounds())); - const y_values = cartesianProduct<number>(...y.map(e => e.bounds())); + const x_values = cartesianProduct<number>(...x.map(e => e.endpoints())); + const y_values = cartesianProduct<number>(...y.map(e => e.endpoints())); const outputs = new Set<FPVector>(); x_values.forEach(inner_x => { @@ -2495,12 +2527,15 @@ export abstract class FPTraits { protected runMatrixToMatrixOp(m: FPMatrix, op: MatrixToMatrixOp): FPMatrix { const num_cols = m.length; const num_rows = m[0].length; - if (m.some(c => c.some(r => !r.isFinite()))) { - return this.constants().unboundedMatrix[num_cols][num_rows]; - } + + // Do not check for OOB inputs and exit early here, because the shape of + // the output matrix may be determined by the operation being run, + // i.e. transpose. const m_flat: readonly FPInterval[] = flatten2DArray(m); - const m_values: ROArrayArray<number> = cartesianProduct<number>(...m_flat.map(e => e.bounds())); + const m_values: ROArrayArray<number> = cartesianProduct<number>( + ...m_flat.map(e => e.endpoints()) + ); const outputs = new Set<FPMatrix>(); m_values.forEach(inner_m => { @@ -2522,6 +2557,33 @@ export abstract class FPTraits { /** * Calculate the Matrix of acceptance intervals by running a scalar operation + * component-wise over a scalar and a matrix. + * + * An example of this is performing constant scaling. + * + * @param i scalar input + * @param m matrix input + * @param op scalar operation to be run component-wise + * @returns a matrix of intervals with the outputs of op.impl + */ + protected runScalarPairToIntervalOpScalarMatrixComponentWise( + i: FPInterval, + m: FPMatrix, + op: ScalarPairToIntervalOp + ): FPMatrix { + const cols = m.length; + const rows = m[0].length; + return this.toMatrix( + unflatten2DArray( + flatten2DArray(m).map(e => this.runScalarPairToIntervalOp(i, e, op)), + cols, + rows + ) + ); + } + + /** + * Calculate the Matrix of acceptance intervals by running a scalar operation * component-wise over a pair of matrices. * * An example of this is performing matrix addition. @@ -2531,14 +2593,14 @@ export abstract class FPTraits { * @param op scalar operation to be run component-wise * @returns a matrix of intervals with the outputs of op.impl */ - protected runScalarPairToIntervalOpMatrixComponentWise( + protected runScalarPairToIntervalOpMatrixMatrixComponentWise( x: FPMatrix, y: FPMatrix, op: ScalarPairToIntervalOp ): FPMatrix { assert( x.length === y.length && x[0].length === y[0].length, - `runScalarPairToIntervalOpMatrixComponentWise requires matrices of the same dimensions` + `runScalarPairToIntervalOpMatrixMatrixComponentWise requires matrices of the same dimensions` ); const cols = x.length; @@ -2673,7 +2735,7 @@ export abstract class FPTraits { // This op is implemented differently for f32 and f16. private readonly AcosIntervalOp: ScalarToIntervalOp = { - impl: this.limitScalarToIntervalDomain(this.toInterval([-1.0, 1.0]), (n: number) => { + impl: (n: number) => { assert(this.kind === 'f32' || this.kind === 'f16'); // acos(n) = atan2(sqrt(1.0 - n * n), n) or a polynomial approximation with absolute error const y = this.sqrtInterval(this.subtractionInterval(1, this.multiplicationInterval(n, n))); @@ -2682,7 +2744,10 @@ export abstract class FPTraits { this.atan2Interval(y, n), this.absoluteErrorInterval(Math.acos(n), approx_abs_error) ); - }), + }, + domain: () => { + return this.constants().negOneToOneInterval; + }, }; protected acosIntervalImpl(n: number): FPInterval { @@ -2751,7 +2816,7 @@ export abstract class FPTraits { ) => FPInterval; protected additionMatrixMatrixIntervalImpl(x: Array2D<number>, y: Array2D<number>): FPMatrix { - return this.runScalarPairToIntervalOpMatrixComponentWise( + return this.runScalarPairToIntervalOpMatrixMatrixComponentWise( this.toMatrix(x), this.toMatrix(y), this.AdditionIntervalOp @@ -2766,16 +2831,19 @@ export abstract class FPTraits { // This op is implemented differently for f32 and f16. private readonly AsinIntervalOp: ScalarToIntervalOp = { - impl: this.limitScalarToIntervalDomain(this.toInterval([-1.0, 1.0]), (n: number) => { + impl: (n: number) => { assert(this.kind === 'f32' || this.kind === 'f16'); // asin(n) = atan2(n, sqrt(1.0 - n * n)) or a polynomial approximation with absolute error const x = this.sqrtInterval(this.subtractionInterval(1, this.multiplicationInterval(n, n))); - const approx_abs_error = this.kind === 'f32' ? 6.77e-5 : 3.91e-3; + const approx_abs_error = this.kind === 'f32' ? 6.81e-5 : 3.91e-3; return this.spanIntervals( this.atan2Interval(n, x), this.absoluteErrorInterval(Math.asin(n), approx_abs_error) ); - }), + }, + domain: () => { + return this.constants().negOneToOneInterval; + }, }; /** Calculate an acceptance interval for asin(n) */ @@ -2836,29 +2904,23 @@ export abstract class FPTraits { : [this.toInterval([-(2 ** 14), -(2 ** -14)]), this.toInterval([2 ** -14, 2 ** 14])]; const ulp_error = this.kind === 'f32' ? 4096 : 5; return { - impl: this.limitScalarPairToIntervalDomain( - { - x: domain_x, - y: domain_y, - }, - (y: number, x: number): FPInterval => { - // Accurate result in f64 - let atan_yx = Math.atan(y / x); - // Offset by +/-pi according to the definition. Use pi value in f64 because we are - // handling accurate result. - if (x < 0) { - // x < 0, y > 0, result is atan(y/x) + π - if (y > 0) { - atan_yx = atan_yx + kValue.f64.positive.pi.whole; - } else { - // x < 0, y < 0, result is atan(y/x) - π - atan_yx = atan_yx - kValue.f64.positive.pi.whole; - } + impl: (y: number, x: number): FPInterval => { + // Accurate result in f64 + let atan_yx = Math.atan(y / x); + // Offset by +/-pi according to the definition. Use pi value in f64 because we are + // handling accurate result. + if (x < 0) { + // x < 0, y > 0, result is atan(y/x) + π + if (y > 0) { + atan_yx = atan_yx + kValue.f64.positive.pi.whole; + } else { + // x < 0, y < 0, result is atan(y/x) - π + atan_yx = atan_yx - kValue.f64.positive.pi.whole; } - - return this.ulpInterval(atan_yx, ulp_error); } - ), + + return this.ulpInterval(atan_yx, ulp_error); + }, extrema: (y: FPInterval, x: FPInterval): [FPInterval, FPInterval] => { // There is discontinuity, which generates an unbounded result, at y/x = 0 that will dominate the accuracy if (y.contains(0)) { @@ -2869,6 +2931,9 @@ export abstract class FPTraits { } return [y, x]; }, + domain: () => { + return { x: domain_x, y: domain_y }; + }, }; } @@ -2984,14 +3049,14 @@ export abstract class FPTraits { public abstract readonly clampIntervals: ScalarTripleToInterval[]; private readonly CosIntervalOp: ScalarToIntervalOp = { - impl: this.limitScalarToIntervalDomain( - this.constants().negPiToPiInterval, - (n: number): FPInterval => { - assert(this.kind === 'f32' || this.kind === 'f16'); - const abs_error = this.kind === 'f32' ? 2 ** -11 : 2 ** -7; - return this.absoluteErrorInterval(Math.cos(n), abs_error); - } - ), + impl: (n: number): FPInterval => { + assert(this.kind === 'f32' || this.kind === 'f16'); + const abs_error = this.kind === 'f32' ? 2 ** -11 : 2 ** -7; + return this.absoluteErrorInterval(Math.cos(n), abs_error); + }, + domain: () => { + return this.constants().negPiToPiInterval; + }, }; protected cosIntervalImpl(n: number): FPInterval { @@ -3041,7 +3106,11 @@ export abstract class FPTraits { this.multiplicationInterval(x[0], y[1]), this.multiplicationInterval(x[1], y[0]) ); - return [r0, r1, r2]; + + if (r0.isFinite() && r1.isFinite() && r2.isFinite()) { + return [r0, r1, r2]; + } + return this.constants().unboundedVector[3]; }, }; @@ -3281,18 +3350,12 @@ export abstract class FPTraits { ? [this.toInterval([-(2 ** 126), -(2 ** -126)]), this.toInterval([2 ** -126, 2 ** 126])] : [this.toInterval([-(2 ** 14), -(2 ** -14)]), this.toInterval([2 ** -14, 2 ** 14])]; return { - impl: this.limitScalarPairToIntervalDomain( - { - x: domain_x, - y: domain_y, - }, - (x: number, y: number): FPInterval => { - if (y === 0) { - return constants.unboundedInterval; - } - return this.ulpInterval(x / y, 2.5); + impl: (x: number, y: number): FPInterval => { + if (y === 0) { + return constants.unboundedInterval; } - ), + return this.ulpInterval(x / y, 2.5); + }, extrema: (x: FPInterval, y: FPInterval): [FPInterval, FPInterval] => { // division has a discontinuity at y = 0. if (y.contains(0)) { @@ -3300,6 +3363,9 @@ export abstract class FPTraits { } return [x, y]; }, + domain: () => { + return { x: domain_x, y: domain_y }; + }, }; } @@ -3346,7 +3412,10 @@ export abstract class FPTraits { x: readonly number[] | readonly FPInterval[], y: readonly number[] | readonly FPInterval[] ): FPInterval { - assert(x.length === y.length, `dot not defined for vectors with different lengths`); + assert( + x.length === y.length, + `dot not defined for vectors with different lengths, x = ${x}, y = ${y}` + ); return this.runVectorPairToIntervalOp(this.toVector(x), this.toVector(y), this.DotIntervalOp); } @@ -3517,12 +3586,12 @@ export abstract class FPTraits { public abstract readonly fractInterval: (n: number) => FPInterval; private readonly InverseSqrtIntervalOp: ScalarToIntervalOp = { - impl: this.limitScalarToIntervalDomain( - this.constants().greaterThanZeroInterval, - (n: number): FPInterval => { - return this.ulpInterval(1 / Math.sqrt(n), 2); - } - ), + impl: (n: number): FPInterval => { + return this.ulpInterval(1 / Math.sqrt(n), 2); + }, + domain: () => { + return this.constants().greaterThanZeroInterval; + }, }; protected inverseSqrtIntervalImpl(n: number | FPInterval): FPInterval { @@ -3534,21 +3603,19 @@ export abstract class FPTraits { private readonly LdexpIntervalOp: ScalarPairToIntervalOp = { impl: (e1: number, e2: number) => { - assert(this.kind === 'f32' || this.kind === 'f16'); assert(Number.isInteger(e2), 'the second param of ldexp must be an integer'); - const bias = this.kind === 'f32' ? 127 : 15; // Spec explicitly calls indeterminate value if e2 > bias + 1 - if (e2 > bias + 1) { + if (e2 > this.constants().bias + 1) { return this.constants().unboundedInterval; } - // The spec says the result of ldexp(e1, e2) = e1 * 2 ^ e2, and the accuracy is correctly - // rounded to the true value, so the inheritance framework does not need to be invoked to - // determine bounds. + // The spec says the result of ldexp(e1, e2) = e1 * 2 ^ e2, and the + // accuracy is correctly rounded to the true value, so the inheritance + // framework does not need to be invoked to determine endpoints. // Instead, the value at a higher precision is calculated and passed to // correctlyRoundedInterval. const result = e1 * 2 ** e2; if (!Number.isFinite(result)) { - // Overflowed TS's number type, so definitely out of bounds for f32/f16 + // Overflowed TS's number type, so definitely out of bounds return this.constants().unboundedInterval; } // The result may be zero if e2 + bias <= 0, but we can't simply span the interval to 0.0. @@ -3606,17 +3673,17 @@ export abstract class FPTraits { ) => FPInterval; private readonly LogIntervalOp: ScalarToIntervalOp = { - impl: this.limitScalarToIntervalDomain( - this.constants().greaterThanZeroInterval, - (n: number): FPInterval => { - assert(this.kind === 'f32' || this.kind === 'f16'); - const abs_error = this.kind === 'f32' ? 2 ** -21 : 2 ** -7; - if (n >= 0.5 && n <= 2.0) { - return this.absoluteErrorInterval(Math.log(n), abs_error); - } - return this.ulpInterval(Math.log(n), 3); + impl: (n: number): FPInterval => { + assert(this.kind === 'f32' || this.kind === 'f16'); + const abs_error = this.kind === 'f32' ? 2 ** -21 : 2 ** -7; + if (n >= 0.5 && n <= 2.0) { + return this.absoluteErrorInterval(Math.log(n), abs_error); } - ), + return this.ulpInterval(Math.log(n), 3); + }, + domain: () => { + return this.constants().greaterThanZeroInterval; + }, }; protected logIntervalImpl(x: number | FPInterval): FPInterval { @@ -3627,17 +3694,17 @@ export abstract class FPTraits { public abstract readonly logInterval: (x: number | FPInterval) => FPInterval; private readonly Log2IntervalOp: ScalarToIntervalOp = { - impl: this.limitScalarToIntervalDomain( - this.constants().greaterThanZeroInterval, - (n: number): FPInterval => { - assert(this.kind === 'f32' || this.kind === 'f16'); - const abs_error = this.kind === 'f32' ? 2 ** -21 : 2 ** -7; - if (n >= 0.5 && n <= 2.0) { - return this.absoluteErrorInterval(Math.log2(n), abs_error); - } - return this.ulpInterval(Math.log2(n), 3); + impl: (n: number): FPInterval => { + assert(this.kind === 'f32' || this.kind === 'f16'); + const abs_error = this.kind === 'f32' ? 2 ** -21 : 2 ** -7; + if (n >= 0.5 && n <= 2.0) { + return this.absoluteErrorInterval(Math.log2(n), abs_error); } - ), + return this.ulpInterval(Math.log2(n), 3); + }, + domain: () => { + return this.constants().greaterThanZeroInterval; + }, }; protected log2IntervalImpl(x: number | FPInterval): FPInterval { @@ -3791,14 +3858,10 @@ export abstract class FPTraits { } protected multiplicationMatrixScalarIntervalImpl(mat: Array2D<number>, scalar: number): FPMatrix { - const cols = mat.length; - const rows = mat[0].length; - return this.toMatrix( - unflatten2DArray( - flatten2DArray(mat).map(e => this.multiplicationInterval(e, scalar)), - cols, - rows - ) + return this.runScalarPairToIntervalOpScalarMatrixComponentWise( + this.toInterval(scalar), + this.toMatrix(mat), + this.MultiplicationIntervalOp ); } @@ -3809,7 +3872,7 @@ export abstract class FPTraits { ) => FPMatrix; protected multiplicationScalarMatrixIntervalImpl(scalar: number, mat: Array2D<number>): FPMatrix { - return this.multiplicationMatrixScalarIntervalImpl(mat, scalar); + return this.multiplicationMatrixScalarInterval(mat, scalar); } /** Calculate an acceptance interval of x * y, when x is a scalar and y is a matrix */ @@ -3830,13 +3893,22 @@ export abstract class FPTraits { const x_transposed = this.transposeInterval(mat_x); + let oob_result: boolean = false; const result: FPInterval[][] = [...Array(y_cols)].map(_ => [...Array(x_rows)]); mat_y.forEach((y, i) => { x_transposed.forEach((x, j) => { result[i][j] = this.dotInterval(x, y); + if (!oob_result && !result[i][j].isFinite()) { + oob_result = true; + } }); }); + if (oob_result) { + return this.constants().unboundedMatrix[result.length as 2 | 3 | 4][ + result[0].length as 2 | 3 | 4 + ]; + } return result as ROArrayArray<FPInterval> as FPMatrix; } @@ -3896,7 +3968,11 @@ export abstract class FPTraits { private readonly NormalizeIntervalOp: VectorToVectorOp = { impl: (n: readonly number[]): FPVector => { const length = this.lengthInterval(n); - return this.toVector(n.map(e => this.divisionInterval(e, length))); + const result = this.toVector(n.map(e => this.divisionInterval(e, length))); + if (result.some(r => !r.isFinite())) { + return this.constants().unboundedVector[result.length]; + } + return result; }, }; @@ -3955,11 +4031,16 @@ export abstract class FPTraits { // y = normal of reflecting surface const t = this.multiplicationInterval(2.0, this.dotInterval(x, y)); const rhs = this.multiplyVectorByScalar(y, t); - return this.runScalarPairToIntervalOpVectorComponentWise( + const result = this.runScalarPairToIntervalOpVectorComponentWise( this.toVector(x), rhs, this.SubtractionIntervalOp ); + + if (result.some(r => !r.isFinite())) { + return this.constants().unboundedVector[result.length]; + } + return result; }, }; @@ -4015,11 +4096,16 @@ export abstract class FPTraits { const k_sqrt = this.sqrtInterval(k); const t = this.additionInterval(dot_times_r, k_sqrt); // t = r * dot(i, s) + sqrt(k) - return this.runScalarPairToIntervalOpVectorComponentWise( + const result = this.runScalarPairToIntervalOpVectorComponentWise( this.multiplyVectorByScalar(i, r), this.multiplyVectorByScalar(s, t), this.SubtractionIntervalOp ); // (i * r) - (s * t) + + if (result.some(r => !r.isFinite())) { + return this.constants().unboundedVector[result.length]; + } + return result; } /** Calculate acceptance interval vectors of reflect(i, s, r) */ @@ -4116,14 +4202,14 @@ export abstract class FPTraits { public abstract readonly signInterval: (n: number) => FPInterval; private readonly SinIntervalOp: ScalarToIntervalOp = { - impl: this.limitScalarToIntervalDomain( - this.constants().negPiToPiInterval, - (n: number): FPInterval => { - assert(this.kind === 'f32' || this.kind === 'f16'); - const abs_error = this.kind === 'f32' ? 2 ** -11 : 2 ** -7; - return this.absoluteErrorInterval(Math.sin(n), abs_error); - } - ), + impl: (n: number): FPInterval => { + assert(this.kind === 'f32' || this.kind === 'f16'); + const abs_error = this.kind === 'f32' ? 2 ** -11 : 2 ** -7; + return this.absoluteErrorInterval(Math.sin(n), abs_error); + }, + domain: () => { + return this.constants().negPiToPiInterval; + }, }; protected sinIntervalImpl(n: number): FPInterval { @@ -4250,7 +4336,7 @@ export abstract class FPTraits { ) => FPInterval; protected subtractionMatrixMatrixIntervalImpl(x: Array2D<number>, y: Array2D<number>): FPMatrix { - return this.runScalarPairToIntervalOpMatrixComponentWise( + return this.runScalarPairToIntervalOpMatrixMatrixComponentWise( this.toMatrix(x), this.toMatrix(y), this.SubtractionIntervalOp @@ -4375,6 +4461,7 @@ class F32Traits extends FPTraits { sixth: kValue.f32.negative.pi.sixth, }, }, + bias: 127, unboundedInterval: kF32UnboundedInterval, zeroInterval: kF32ZeroInterval, // Have to use the constants.ts values here, because values defined in the @@ -4389,6 +4476,7 @@ class F32Traits extends FPTraits { kValue.f32.positive.subnormal.min, kValue.f32.positive.max ), + negOneToOneInterval: new FPInterval('f32', -1, 1), zeroVector: { 2: [kF32ZeroInterval, kF32ZeroInterval], 3: [kF32ZeroInterval, kF32ZeroInterval, kF32ZeroInterval], @@ -4520,6 +4608,11 @@ class F32Traits extends FPTraits { public readonly flushSubnormal = flushSubnormalNumberF32; public readonly oneULP = oneULPF32; public readonly scalarBuilder = f32; + public readonly scalarRange = scalarF32Range; + public readonly sparseScalarRange = sparseScalarF32Range; + public readonly vectorRange = vectorF32Range; + public readonly sparseVectorRange = sparseVectorF32Range; + public readonly sparseMatrixRange = sparseMatrixF32Range; // Framework - Fundamental Error Intervals - Overrides public readonly absoluteErrorInterval = this.absoluteErrorIntervalImpl.bind(this); @@ -4686,7 +4779,7 @@ class F32Traits extends FPTraits { private unpack2x16floatIntervalImpl(n: number): FPVector { assert( n >= kValue.u32.min && n <= kValue.u32.max, - 'unpack2x16floatInterval only accepts values on the bounds of u32' + 'unpack2x16floatInterval only accepts valid u32 values' ); this.unpackDataU32[0] = n; if (this.unpackDataF16.some(f => !isFiniteF16(f))) { @@ -4710,7 +4803,7 @@ class F32Traits extends FPTraits { private unpack2x16snormIntervalImpl(n: number): FPVector { assert( n >= kValue.u32.min && n <= kValue.u32.max, - 'unpack2x16snormInterval only accepts values on the bounds of u32' + 'unpack2x16snormInterval only accepts valid u32 values' ); const op = (n: number): FPInterval => { return this.ulpInterval(Math.max(n / 32767, -1), 3); @@ -4726,7 +4819,7 @@ class F32Traits extends FPTraits { private unpack2x16unormIntervalImpl(n: number): FPVector { assert( n >= kValue.u32.min && n <= kValue.u32.max, - 'unpack2x16unormInterval only accepts values on the bounds of u32' + 'unpack2x16unormInterval only accepts valid u32 values' ); const op = (n: number): FPInterval => { return this.ulpInterval(n / 65535, 3); @@ -4742,7 +4835,7 @@ class F32Traits extends FPTraits { private unpack4x8snormIntervalImpl(n: number): FPVector { assert( n >= kValue.u32.min && n <= kValue.u32.max, - 'unpack4x8snormInterval only accepts values on the bounds of u32' + 'unpack4x8snormInterval only accepts valid u32 values' ); const op = (n: number): FPInterval => { return this.ulpInterval(Math.max(n / 127, -1), 3); @@ -4762,7 +4855,7 @@ class F32Traits extends FPTraits { private unpack4x8unormIntervalImpl(n: number): FPVector { assert( n >= kValue.u32.min && n <= kValue.u32.max, - 'unpack4x8unormInterval only accepts values on the bounds of u32' + 'unpack4x8unormInterval only accepts valid u32 values' ); const op = (n: number): FPInterval => { return this.ulpInterval(n / 255, 3); @@ -4836,6 +4929,7 @@ class FPAbstractTraits extends FPTraits { sixth: kValue.f64.negative.pi.sixth, }, }, + bias: 1023, unboundedInterval: kAbstractUnboundedInterval, zeroInterval: kAbstractZeroInterval, // Have to use the constants.ts values here, because values defined in the @@ -4850,6 +4944,8 @@ class FPAbstractTraits extends FPTraits { kValue.f64.positive.subnormal.min, kValue.f64.positive.max ), + negOneToOneInterval: new FPInterval('abstract', -1, 1), + zeroVector: { 2: [kAbstractZeroInterval, kAbstractZeroInterval], 3: [kAbstractZeroInterval, kAbstractZeroInterval, kAbstractZeroInterval], @@ -4992,14 +5088,17 @@ class FPAbstractTraits extends FPTraits { unreachable(`'FPAbstractTraits.oneULP should never be called`); }; public readonly scalarBuilder = abstractFloat; + public readonly scalarRange = scalarF64Range; + public readonly sparseScalarRange = sparseScalarF64Range; + public readonly vectorRange = vectorF64Range; + public readonly sparseVectorRange = sparseVectorF64Range; + public readonly sparseMatrixRange = sparseMatrixF64Range; // Framework - Fundamental Error Intervals - Overrides - public readonly absoluteErrorInterval = this.unboundedAbsoluteErrorInterval.bind(this); + public readonly absoluteErrorInterval = this.unimplementedAbsoluteErrorInterval.bind(this); // Should use FP.f32 instead public readonly correctlyRoundedInterval = this.correctlyRoundedIntervalImpl.bind(this); public readonly correctlyRoundedMatrix = this.correctlyRoundedMatrixImpl.bind(this); - public readonly ulpInterval = (n: number, numULP: number): FPInterval => { - return this.toInterval(kF32Traits.ulpInterval(n, numULP)); - }; + public readonly ulpInterval = this.unimplementedUlpInterval.bind(this); // Should use FP.f32 instead // Framework - API - Overrides public readonly absInterval = this.absIntervalImpl.bind(this); @@ -5023,40 +5122,38 @@ class FPAbstractTraits extends FPTraits { 'atan2Interval' ); public readonly atanhInterval = this.unimplementedScalarToInterval.bind(this, 'atanhInterval'); - public readonly ceilInterval = this.unimplementedScalarToInterval.bind(this, 'ceilInterval'); + public readonly ceilInterval = this.ceilIntervalImpl.bind(this); public readonly clampMedianInterval = this.clampMedianIntervalImpl.bind(this); public readonly clampMinMaxInterval = this.clampMinMaxIntervalImpl.bind(this); public readonly clampIntervals = [this.clampMedianInterval, this.clampMinMaxInterval]; public readonly cosInterval = this.unimplementedScalarToInterval.bind(this, 'cosInterval'); public readonly coshInterval = this.unimplementedScalarToInterval.bind(this, 'coshInterval'); - public readonly crossInterval = this.crossIntervalImpl.bind(this); - public readonly degreesInterval = this.degreesIntervalImpl.bind(this); + public readonly crossInterval = this.unimplementedVectorPairToVector.bind(this, 'crossInterval'); + public readonly degreesInterval = this.unimplementedScalarToInterval.bind( + this, + 'degreesInterval' + ); public readonly determinantInterval = this.unimplementedMatrixToInterval.bind( this, - 'determinantInterval' + 'determinant' ); public readonly distanceInterval = this.unimplementedDistance.bind(this); - public readonly divisionInterval = ( - x: number | FPInterval, - y: number | FPInterval - ): FPInterval => { - return this.toInterval(kF32Traits.divisionInterval(x, y)); - }; + public readonly divisionInterval = this.unimplementedScalarPairToInterval.bind( + this, + 'divisionInterval' + ); public readonly dotInterval = this.unimplementedVectorPairToInterval.bind(this, 'dotInterval'); public readonly expInterval = this.unimplementedScalarToInterval.bind(this, 'expInterval'); public readonly exp2Interval = this.unimplementedScalarToInterval.bind(this, 'exp2Interval'); public readonly faceForwardIntervals = this.unimplementedFaceForward.bind(this); public readonly floorInterval = this.floorIntervalImpl.bind(this); - public readonly fmaInterval = this.fmaIntervalImpl.bind(this); - public readonly fractInterval = this.unimplementedScalarToInterval.bind(this, 'fractInterval'); + public readonly fmaInterval = this.unimplementedScalarTripleToInterval.bind(this, 'fmaInterval'); + public readonly fractInterval = this.fractIntervalImpl.bind(this); public readonly inverseSqrtInterval = this.unimplementedScalarToInterval.bind( this, 'inverseSqrtInterval' ); - public readonly ldexpInterval = this.unimplementedScalarPairToInterval.bind( - this, - 'ldexpInterval' - ); + public readonly ldexpInterval = this.ldexpIntervalImpl.bind(this); public readonly lengthInterval = this.unimplementedLength.bind(this); public readonly logInterval = this.unimplementedScalarToInterval.bind(this, 'logInterval'); public readonly log2Interval = this.unimplementedScalarToInterval.bind(this, 'log2Interval'); @@ -5077,14 +5174,10 @@ class FPAbstractTraits extends FPTraits { this, 'multiplicationMatrixMatrixInterval' ); - public readonly multiplicationMatrixScalarInterval = this.unimplementedMatrixScalarToMatrix.bind( - this, - 'multiplicationMatrixScalarInterval' - ); - public readonly multiplicationScalarMatrixInterval = this.unimplementedScalarMatrixToMatrix.bind( - this, - 'multiplicationScalarMatrixInterval' - ); + public readonly multiplicationMatrixScalarInterval = + this.multiplicationMatrixScalarIntervalImpl.bind(this); + public readonly multiplicationScalarMatrixInterval = + this.multiplicationScalarMatrixIntervalImpl.bind(this); public readonly multiplicationMatrixVectorInterval = this.unimplementedMatrixVectorToVector.bind( this, 'multiplicationMatrixVectorInterval' @@ -5099,16 +5192,17 @@ class FPAbstractTraits extends FPTraits { 'normalizeInterval' ); public readonly powInterval = this.unimplementedScalarPairToInterval.bind(this, 'powInterval'); - public readonly radiansInterval = this.radiansIntervalImpl.bind(this); + public readonly radiansInterval = this.unimplementedScalarToInterval.bind(this, 'radiansImpl'); public readonly reflectInterval = this.unimplementedVectorPairToVector.bind( this, 'reflectInterval' ); public readonly refractInterval = this.unimplementedRefract.bind(this); - public readonly remainderInterval = (x: number, y: number): FPInterval => { - return this.toInterval(kF32Traits.remainderInterval(x, y)); - }; - public readonly roundInterval = this.unimplementedScalarToInterval.bind(this, 'roundInterval'); + public readonly remainderInterval = this.unimplementedScalarPairToInterval.bind( + this, + 'remainderInterval' + ); + public readonly roundInterval = this.roundIntervalImpl.bind(this); public readonly saturateInterval = this.saturateIntervalImpl.bind(this); public readonly signInterval = this.signIntervalImpl.bind(this); public readonly sinInterval = this.unimplementedScalarToInterval.bind(this, 'sinInterval'); @@ -5118,7 +5212,7 @@ class FPAbstractTraits extends FPTraits { 'smoothStepInterval' ); public readonly sqrtInterval = this.unimplementedScalarToInterval.bind(this, 'sqrtInterval'); - public readonly stepInterval = this.unimplementedScalarPairToInterval.bind(this, 'stepInterval'); + public readonly stepInterval = this.stepIntervalImpl.bind(this); public readonly subtractionInterval = this.subtractionIntervalImpl.bind(this); public readonly subtractionMatrixMatrixInterval = this.subtractionMatrixMatrixIntervalImpl.bind(this); @@ -5179,6 +5273,7 @@ class F16Traits extends FPTraits { sixth: kValue.f16.negative.pi.sixth, }, }, + bias: 15, unboundedInterval: kF16UnboundedInterval, zeroInterval: kF16ZeroInterval, // Have to use the constants.ts values here, because values defined in the @@ -5193,6 +5288,8 @@ class F16Traits extends FPTraits { kValue.f16.positive.subnormal.min, kValue.f16.positive.max ), + negOneToOneInterval: new FPInterval('f16', -1, 1), + zeroVector: { 2: [kF16ZeroInterval, kF16ZeroInterval], 3: [kF16ZeroInterval, kF16ZeroInterval, kF16ZeroInterval], @@ -5324,6 +5421,11 @@ class F16Traits extends FPTraits { public readonly flushSubnormal = flushSubnormalNumberF16; public readonly oneULP = oneULPF16; public readonly scalarBuilder = f16; + public readonly scalarRange = scalarF16Range; + public readonly sparseScalarRange = sparseScalarF16Range; + public readonly vectorRange = vectorF16Range; + public readonly sparseVectorRange = sparseVectorF16Range; + public readonly sparseMatrixRange = sparseMatrixF16Range; // Framework - Fundamental Error Intervals - Overrides public readonly absoluteErrorInterval = this.absoluteErrorIntervalImpl.bind(this); @@ -5437,5 +5539,6 @@ export function isRepresentable(value: number, type: ScalarType) { const constants = fpTraitsFor(type).constants(); return value >= constants.negative.min && value <= constants.positive.max; } + assert(false, `isRepresentable() is not yet implemented for type ${type}`); } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts index 851db40c71..20d7818df6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts @@ -414,6 +414,25 @@ export function oneULPF32(target: number, mode: FlushMode = 'flush'): number { } /** + * @returns an integer value between 0..0xffffffff using a simple non-cryptographic hash function + * @param values integers to generate hash from. + */ +export function hashU32(...values: number[]) { + let n = 0x3504_f333; + for (const v of values) { + n = v + (n << 7) + (n >>> 1); + n = (n * 0x29493) & 0xffff_ffff; + } + n ^= n >>> 8; + n += n << 15; + n = n & 0xffff_ffff; + if (n < 0) { + n = ~n * 2 + 1; + } + return n; +} + +/** * @returns ulp(x), the unit of least precision for a specific number as a 32-bit float * * ulp(x) is the distance between the two floating point numbers nearest x. @@ -881,6 +900,41 @@ export function biasedRange(a: number, b: number, num_steps: number): readonly n } /** + * Version of biasedRange that operates on bigint values + * + * biasedRange was not made into a generic or to take in (number|bigint), + * because that introduces a bunch of complexity overhead related to type + * differentiation. + * + * Scaling is used internally so that the number of possible indices is + * significantly larger than num_steps. This is done to avoid duplicate entries + * in the resulting range due to quantizing to integers during the calculation. + * + * If a and b are close together, such that the number of integers between them + * is close to num_steps, then duplicates will occur regardless of scaling. + */ +export function biasedRangeBigInt(a: bigint, b: bigint, num_steps: number): readonly bigint[] { + if (num_steps <= 0) { + return []; + } + + // Avoid division by 0 + if (num_steps === 1) { + return [a]; + } + + const c = 2; + const scaling = 1000; + const scaled_num_steps = num_steps * scaling; + + return Array.from(Array(num_steps).keys()).map(i => { + const biased_i = Math.pow(i / (num_steps - 1), c); // Floating Point on [0, 1] + const scaled_i = Math.trunc((scaled_num_steps - 1) * biased_i); // Integer on [0, scaled_num_steps - 1] + return lerpBigInt(a, b, scaled_i, scaled_num_steps); + }); +} + +/** * @returns an ascending sorted array of numbers spread over the entire range of 32-bit floats * * Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals. @@ -891,12 +945,12 @@ export function biasedRange(a: number, b: number, num_steps: number): readonly n * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f32 range. * * This function is intended to provide dense coverage of the f32 range, for a minimal list of values to use to cover - * f32 behaviour, use sparseF32Range instead. + * f32 behaviour, use sparseScalarF32Range instead. * * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries * must be 0 or greater. */ -export function fullF32Range( +export function scalarF32Range( counts: { neg_norm?: number; neg_sub?: number; @@ -943,8 +997,8 @@ export function fullF32Range( * @param low the lowest f32 value to permit when filtered * @param high the highest f32 value to permit when filtered */ -export function sourceFilteredF32Range(source: String, low: number, high: number): Array<number> { - return fullF32Range().filter(x => source !== 'const' || (x >= low && x <= high)); +export function filteredScalarF32Range(source: String, low: number, high: number): Array<number> { + return scalarF32Range().filter(x => source !== 'const' || (x >= low && x <= high)); } /** @@ -958,12 +1012,12 @@ export function sourceFilteredF32Range(source: String, low: number, high: number * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f16 range. * * This function is intended to provide dense coverage of the f16 range, for a minimal list of values to use to cover - * f16 behaviour, use sparseF16Range instead. + * f16 behaviour, use sparseScalarF16Range instead. * * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries * must be 0 or greater. */ -export function fullF16Range( +export function scalarF16Range( counts: { neg_norm?: number; neg_sub?: number; @@ -1009,12 +1063,12 @@ export function fullF16Range( * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f64 range. * * This function is intended to provide dense coverage of the f64 range, for a minimal list of values to use to cover - * f64 behaviour, use sparseF64Range instead. + * f64 behaviour, use sparseScalarF64Range instead. * * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries * must be 0 or greater. */ -export function fullF64Range( +export function scalarF64Range( counts: { neg_norm?: number; neg_sub?: number; @@ -1065,7 +1119,7 @@ export function fullF64Range( * @param counts structure param with 4 entries indicating the number of entries * to be generated each region, entries must be 0 or greater. */ -export function filteredF64Range( +export function limitedScalarF64Range( begin: number, end: number, counts: { neg_norm?: number; neg_sub?: number; pos_sub: number; pos_norm: number } = { @@ -1136,27 +1190,18 @@ export function sparseI32Range(): readonly number[] { const kVectorI32Values = { 2: kInterestingI32Values.flatMap(f => [ [f, 1], - [1, f], - [f, -1], [-1, f], ]), 3: kInterestingI32Values.flatMap(f => [ - [f, 1, 2], - [1, f, 2], - [1, 2, f], - [f, -1, -2], - [-1, f, -2], - [-1, -2, f], + [f, 1, -2], + [-1, f, 2], + [1, -2, f], ]), 4: kInterestingI32Values.flatMap(f => [ - [f, 1, 2, 3], - [1, f, 2, 3], - [1, 2, f, 3], - [1, 2, 3, f], - [f, -1, -2, -3], - [-1, f, -2, -3], - [-1, -2, f, -3], - [-1, -2, -3, f], + [f, -1, 2, 3], + [1, f, -2, 3], + [1, 2, f, -3], + [-1, 2, -3, f], ]), }; @@ -1178,6 +1223,38 @@ export function vectorI32Range(dim: number): ROArrayArray<number> { return kVectorI32Values[dim]; } +const kSparseVectorI32Values = { + 2: sparseI32Range().map((i, idx) => [idx % 2 === 0 ? i : idx, idx % 2 === 1 ? i : -idx]), + 3: sparseI32Range().map((i, idx) => [ + idx % 3 === 0 ? i : idx, + idx % 3 === 1 ? i : -idx, + idx % 3 === 2 ? i : idx, + ]), + 4: sparseI32Range().map((i, idx) => [ + idx % 4 === 0 ? i : idx, + idx % 4 === 1 ? i : -idx, + idx % 4 === 2 ? i : idx, + idx % 4 === 3 ? i : -idx, + ]), +}; + +/** + * Minimal set of vectors, indexed by dimension, that contain interesting + * abstract integer values. + * + * This is an even more stripped down version of `vectorI32Range` for when + * pairs of vectors are being tested. + * All interesting integers from sparseI32Range are guaranteed to be + * tested, but not in every position. + */ +export function sparseVectorI32Range(dim: number): ROArrayArray<number> { + assert( + dim === 2 || dim === 3 || dim === 4, + 'sparseVectorI32Range only accepts dimensions 2, 3, and 4' + ); + return kSparseVectorI32Values[dim]; +} + /** * @returns an ascending sorted array of numbers spread over the entire range of 32-bit signed ints * @@ -1256,6 +1333,38 @@ export function vectorU32Range(dim: number): ROArrayArray<number> { return kVectorU32Values[dim]; } +const kSparseVectorU32Values = { + 2: sparseU32Range().map((i, idx) => [idx % 2 === 0 ? i : idx, idx % 2 === 1 ? i : -idx]), + 3: sparseU32Range().map((i, idx) => [ + idx % 3 === 0 ? i : idx, + idx % 3 === 1 ? i : -idx, + idx % 3 === 2 ? i : idx, + ]), + 4: sparseU32Range().map((i, idx) => [ + idx % 4 === 0 ? i : idx, + idx % 4 === 1 ? i : -idx, + idx % 4 === 2 ? i : idx, + idx % 4 === 3 ? i : -idx, + ]), +}; + +/** + * Minimal set of vectors, indexed by dimension, that contain interesting + * abstract integer values. + * + * This is an even more stripped down version of `vectorU32Range` for when + * pairs of vectors are being tested. + * All interesting integers from sparseU32Range are guaranteed to be + * tested, but not in every position. + */ +export function sparseVectorU32Range(dim: number): ROArrayArray<number> { + assert( + dim === 2 || dim === 3 || dim === 4, + 'sparseVectorU32Range only accepts dimensions 2, 3, and 4' + ); + return kSparseVectorU32Values[dim]; +} + /** * @returns an ascending sorted array of numbers spread over the entire range of 32-bit unsigned ints * @@ -1267,6 +1376,124 @@ export function fullU32Range(count: number = 50): Array<number> { return [0, ...biasedRange(1, kValue.u32.max, count)].map(Math.trunc); } +/** Short list of i64 values of interest to test against */ +const kInterestingI64Values: readonly bigint[] = [ + kValue.i64.negative.max, + kValue.i64.negative.max / 2n, + -256n, + -10n, + -1n, + 0n, + 1n, + 10n, + 256n, + kValue.i64.positive.max / 2n, + kValue.i64.positive.max, +]; + +/** @returns minimal i64 values that cover the entire range of i64 behaviours + * + * This is used instead of fullI64Range when the number of test cases being + * generated is a super linear function of the length of i64 values which is + * leading to time outs. + */ +export function sparseI64Range(): readonly bigint[] { + return kInterestingI64Values; +} + +const kVectorI64Values = { + 2: kInterestingI64Values.flatMap(f => [ + [f, 1n], + [-1n, f], + ]), + 3: kInterestingI64Values.flatMap(f => [ + [f, 1n, -2n], + [-1n, f, 2n], + [1n, -2n, f], + ]), + 4: kInterestingI64Values.flatMap(f => [ + [f, -1n, 2n, 3n], + [1n, f, -2n, 3n], + [1n, 2n, f, -3n], + [-1n, 2n, -3n, f], + ]), +}; + +/** + * Returns set of vectors, indexed by dimension containing interesting i64 + * values. + * + * The tests do not do the simple option for coverage of computing the cartesian + * product of all of the interesting i64 values N times for vecN tests, + * because that creates a huge number of tests for vec3 and vec4, leading to + * time outs. + * + * Instead they insert the interesting i64 values into each location of the + * vector to get a spread of testing over the entire range. This reduces the + * number of cases being run substantially, but maintains coverage. + */ +export function vectorI64Range(dim: number): ROArrayArray<bigint> { + assert(dim === 2 || dim === 3 || dim === 4, 'vectorI64Range only accepts dimensions 2, 3, and 4'); + return kVectorI64Values[dim]; +} + +const kSparseVectorI64Values = { + 2: sparseI64Range().map((i, idx) => [ + idx % 2 === 0 ? i : BigInt(idx), + idx % 2 === 1 ? i : -BigInt(idx), + ]), + 3: sparseI64Range().map((i, idx) => [ + idx % 3 === 0 ? i : BigInt(idx), + idx % 3 === 1 ? i : -BigInt(idx), + idx % 3 === 2 ? i : BigInt(idx), + ]), + 4: sparseI64Range().map((i, idx) => [ + idx % 4 === 0 ? i : BigInt(idx), + idx % 4 === 1 ? i : -BigInt(idx), + idx % 4 === 2 ? i : BigInt(idx), + idx % 4 === 3 ? i : -BigInt(idx), + ]), +}; + +/** + * Minimal set of vectors, indexed by dimension, that contain interesting + * abstract integer values. + * + * This is an even more stripped down version of `vectorI64Range` for when + * pairs of vectors are being tested. + * All interesting integers from sparseI64Range are guaranteed to be + * tested, but not in every position. + */ +export function sparseVectorI64Range(dim: number): ROArrayArray<bigint> { + assert( + dim === 2 || dim === 3 || dim === 4, + 'sparseVectorI64Range only accepts dimensions 2, 3, and 4' + ); + return kSparseVectorI64Values[dim]; +} + +/** + * @returns an ascending sorted array of numbers spread over the entire range of 64-bit signed ints + * + * Numbers are divided into 2 regions: negatives, and positives, with their spreads biased towards 0 + * Zero is included in range. + * + * @param counts structure param with 2 entries indicating the number of entries to be generated each region, values must be 0 or greater. + */ +export function fullI64Range( + counts: { + negative?: number; + positive: number; + } = { positive: 50 } +): Array<bigint> { + counts.negative = counts.negative === undefined ? counts.positive : counts.negative; + return [ + ...biasedRangeBigInt(kValue.i64.negative.min, -1n, counts.negative), + 0n, + ...biasedRangeBigInt(1n, kValue.i64.positive.max, counts.positive), + ]; +} + /** Short list of f32 values of interest to test against */ const kInterestingF32Values: readonly number[] = [ kValue.f32.negative.min, @@ -1299,34 +1526,25 @@ const kInterestingF32Values: readonly number[] = [ * specific values of interest. If there are known values of interest they * should be appended to this list in the test generation code. */ -export function sparseF32Range(): readonly number[] { +export function sparseScalarF32Range(): readonly number[] { return kInterestingF32Values; } const kVectorF32Values = { - 2: sparseF32Range().flatMap(f => [ + 2: kInterestingF32Values.flatMap(f => [ [f, 1.0], - [1.0, f], - [f, -1.0], [-1.0, f], ]), - 3: sparseF32Range().flatMap(f => [ - [f, 1.0, 2.0], - [1.0, f, 2.0], - [1.0, 2.0, f], - [f, -1.0, -2.0], - [-1.0, f, -2.0], - [-1.0, -2.0, f], + 3: kInterestingF32Values.flatMap(f => [ + [f, 1.0, -2.0], + [-1.0, f, 2.0], + [1.0, -2.0, f], ]), - 4: sparseF32Range().flatMap(f => [ - [f, 1.0, 2.0, 3.0], - [1.0, f, 2.0, 3.0], - [1.0, 2.0, f, 3.0], - [1.0, 2.0, 3.0, f], - [f, -1.0, -2.0, -3.0], - [-1.0, f, -2.0, -3.0], - [-1.0, -2.0, f, -3.0], - [-1.0, -2.0, -3.0, f], + 4: kInterestingF32Values.flatMap(f => [ + [f, -1.0, 2.0, 3.0], + [1.0, f, -2.0, 3.0], + [1.0, 2.0, f, -3.0], + [-1.0, 2.0, -3.0, f], ]), }; @@ -1349,13 +1567,13 @@ export function vectorF32Range(dim: number): ROArrayArray<number> { } const kSparseVectorF32Values = { - 2: sparseF32Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]), - 3: sparseF32Range().map((f, idx) => [ + 2: sparseScalarF32Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]), + 3: sparseScalarF32Range().map((f, idx) => [ idx % 3 === 0 ? f : idx, idx % 3 === 1 ? f : -idx, idx % 3 === 2 ? f : idx, ]), - 4: sparseF32Range().map((f, idx) => [ + 4: sparseScalarF32Range().map((f, idx) => [ idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx, idx % 4 === 2 ? f : idx, @@ -1369,8 +1587,8 @@ const kSparseVectorF32Values = { * * This is an even more stripped down version of `vectorF32Range` for when * pairs of vectors are being tested. - * All of the interesting floats from sparseF32 are guaranteed to be tested, but - * not in every position. + * All of the interesting floats from sparseScalarF32 are guaranteed to be + * tested, but not in every position. */ export function sparseVectorF32Range(dim: number): ROArrayArray<number> { assert( @@ -1480,16 +1698,16 @@ const kSparseMatrixF32Values = { }; /** - * Returns a minimal set of matrices, indexed by dimension containing interesting - * float values. + * Returns a minimal set of matrices, indexed by dimension containing + * interesting float values. * * This is the matrix analogue of `sparseVectorF32Range`, so it is producing a * minimal coverage set of matrices that test all of the interesting f32 values. * There is not a more expansive set of matrices, since matrices are even more * expensive than vectors for increasing runtime with coverage. * - * All of the interesting floats from sparseF32 are guaranteed to be tested, but - * not in every position. + * All of the interesting floats from sparseScalarF32 are guaranteed to be + * tested, but not in every position. */ export function sparseMatrixF32Range(c: number, r: number): ROArrayArrayArray<number> { assert( @@ -1535,34 +1753,25 @@ const kInterestingF16Values: readonly number[] = [ * specific values of interest. If there are known values of interest they * should be appended to this list in the test generation code. */ -export function sparseF16Range(): readonly number[] { +export function sparseScalarF16Range(): readonly number[] { return kInterestingF16Values; } const kVectorF16Values = { - 2: sparseF16Range().flatMap(f => [ + 2: kInterestingF16Values.flatMap(f => [ [f, 1.0], - [1.0, f], - [f, -1.0], [-1.0, f], ]), - 3: sparseF16Range().flatMap(f => [ - [f, 1.0, 2.0], - [1.0, f, 2.0], - [1.0, 2.0, f], - [f, -1.0, -2.0], - [-1.0, f, -2.0], - [-1.0, -2.0, f], + 3: kInterestingF16Values.flatMap(f => [ + [f, 1.0, -2.0], + [-1.0, f, 2.0], + [1.0, -2.0, f], ]), - 4: sparseF16Range().flatMap(f => [ - [f, 1.0, 2.0, 3.0], - [1.0, f, 2.0, 3.0], - [1.0, 2.0, f, 3.0], - [1.0, 2.0, 3.0, f], - [f, -1.0, -2.0, -3.0], - [-1.0, f, -2.0, -3.0], - [-1.0, -2.0, f, -3.0], - [-1.0, -2.0, -3.0, f], + 4: kInterestingF16Values.flatMap(f => [ + [f, -1.0, 2.0, 3.0], + [1.0, f, -2.0, 3.0], + [1.0, 2.0, f, -3.0], + [-1.0, 2.0, -3.0, f], ]), }; @@ -1585,13 +1794,13 @@ export function vectorF16Range(dim: number): ROArrayArray<number> { } const kSparseVectorF16Values = { - 2: sparseF16Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]), - 3: sparseF16Range().map((f, idx) => [ + 2: sparseScalarF16Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]), + 3: sparseScalarF16Range().map((f, idx) => [ idx % 3 === 0 ? f : idx, idx % 3 === 1 ? f : -idx, idx % 3 === 2 ? f : idx, ]), - 4: sparseF16Range().map((f, idx) => [ + 4: sparseScalarF16Range().map((f, idx) => [ idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx, idx % 4 === 2 ? f : idx, @@ -1605,8 +1814,8 @@ const kSparseVectorF16Values = { * * This is an even more stripped down version of `vectorF16Range` for when * pairs of vectors are being tested. - * All of the interesting floats from sparseF16 are guaranteed to be tested, but - * not in every position. + * All of the interesting floats from sparseScalarF16 are guaranteed to be + * tested, but not in every position. */ export function sparseVectorF16Range(dim: number): ROArrayArray<number> { assert( @@ -1724,7 +1933,7 @@ const kSparseMatrixF16Values = { * There is not a more expansive set of matrices, since matrices are even more * expensive than vectors for increasing runtime with coverage. * - * All of the interesting floats from sparseF16 are guaranteed to be tested, but + * All of the interesting floats from sparseScalarF16 are guaranteed to be tested, but * not in every position. */ export function sparseMatrixF16Range(c: number, r: number): ROArrayArray<number>[] { @@ -1771,34 +1980,25 @@ const kInterestingF64Values: readonly number[] = [ * specific values of interest. If there are known values of interest they * should be appended to this list in the test generation code. */ -export function sparseF64Range(): readonly number[] { +export function sparseScalarF64Range(): readonly number[] { return kInterestingF64Values; } const kVectorF64Values = { - 2: sparseF64Range().flatMap(f => [ + 2: kInterestingF64Values.flatMap(f => [ [f, 1.0], - [1.0, f], - [f, -1.0], [-1.0, f], ]), - 3: sparseF64Range().flatMap(f => [ - [f, 1.0, 2.0], - [1.0, f, 2.0], - [1.0, 2.0, f], - [f, -1.0, -2.0], - [-1.0, f, -2.0], - [-1.0, -2.0, f], + 3: kInterestingF64Values.flatMap(f => [ + [f, 1.0, -2.0], + [-1.0, f, 2.0], + [1.0, -2.0, f], ]), - 4: sparseF64Range().flatMap(f => [ - [f, 1.0, 2.0, 3.0], - [1.0, f, 2.0, 3.0], - [1.0, 2.0, f, 3.0], - [1.0, 2.0, 3.0, f], - [f, -1.0, -2.0, -3.0], - [-1.0, f, -2.0, -3.0], - [-1.0, -2.0, f, -3.0], - [-1.0, -2.0, -3.0, f], + 4: kInterestingF64Values.flatMap(f => [ + [f, -1.0, 2.0, 3.0], + [1.0, f, -2.0, 3.0], + [1.0, 2.0, f, -3.0], + [-1.0, 2.0, -3.0, f], ]), }; @@ -1821,13 +2021,13 @@ export function vectorF64Range(dim: number): ROArrayArray<number> { } const kSparseVectorF64Values = { - 2: sparseF64Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]), - 3: sparseF64Range().map((f, idx) => [ + 2: sparseScalarF64Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]), + 3: sparseScalarF64Range().map((f, idx) => [ idx % 3 === 0 ? f : idx, idx % 3 === 1 ? f : -idx, idx % 3 === 2 ? f : idx, ]), - 4: sparseF64Range().map((f, idx) => [ + 4: sparseScalarF64Range().map((f, idx) => [ idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx, idx % 4 === 2 ? f : idx, @@ -1841,7 +2041,7 @@ const kSparseVectorF64Values = { * * This is an even more stripped down version of `vectorF64Range` for when * pairs of vectors are being tested. - * All the interesting floats from sparseF64 are guaranteed to be tested, but + * All the interesting floats from sparseScalarF64 are guaranteed to be tested, but * not in every position. */ export function sparseVectorF64Range(dim: number): ROArrayArray<number> { @@ -1960,19 +2160,19 @@ const kSparseMatrixF64Values = { * There is not a more expansive set of matrices, since matrices are even more * expensive than vectors for increasing runtime with coverage. * - * All the interesting floats from sparseF64 are guaranteed to be tested, but + * All the interesting floats from sparseScalarF64 are guaranteed to be tested, but * not in every position. */ -export function sparseMatrixF64Range(c: number, r: number): ROArrayArray<number>[] { +export function sparseMatrixF64Range(cols: number, rows: number): ROArrayArrayArray<number> { assert( - c === 2 || c === 3 || c === 4, + cols === 2 || cols === 3 || cols === 4, 'sparseMatrixF64Range only accepts column counts of 2, 3, and 4' ); assert( - r === 2 || r === 3 || r === 4, + rows === 2 || rows === 3 || rows === 4, 'sparseMatrixF64Range only accepts row counts of 2, 3, and 4' ); - return kSparseMatrixF64Values[c][r]; + return kSparseMatrixF64Values[cols][rows]; } /** @@ -2009,8 +2209,8 @@ export function signExtend(n: number, bits: number): number { return (n << shift) >> shift; } -export interface QuantizeFunc { - (num: number): number; +export interface QuantizeFunc<T> { + (num: T): T; } /** @returns the closest 32-bit floating point value to the input */ @@ -2051,11 +2251,25 @@ export function quantizeToU32(num: number): number { return Math.trunc(num); } +/** + * @returns the closest 64-bit signed integer value to the input. + */ +export function quantizeToI64(num: bigint): bigint { + if (num >= kValue.i64.positive.max) { + return kValue.i64.positive.max; + } + if (num <= kValue.i64.negative.min) { + return kValue.i64.negative.min; + } + return num; +} + /** @returns whether the number is an integer and a power of two */ export function isPowerOfTwo(n: number): boolean { if (!Number.isInteger(n)) { return false; } + assert((n | 0) === n, 'isPowerOfTwo only supports 32-bit numbers'); return n !== 0 && (n & (n - 1)) === 0; } @@ -2245,3 +2459,32 @@ export function every2DArray<T>(m: ROArrayArray<T>, op: (input: T) => boolean): ); return m.every(col => col.every(el => op(el))); } + +/** + * Subtracts 2 vectors + */ +export function subtractVectors(v1: readonly number[], v2: readonly number[]) { + return v1.map((v, i) => v - v2[i]); +} + +/** + * Computes the dot product of 2 vectors + */ +export function dotProduct(v1: readonly number[], v2: readonly number[]) { + return v1.reduce((a, v, i) => a + v * v2[i], 0); +} + +/** @returns the absolute value of a bigint */ +export function absBigInt(v: bigint): bigint { + return v < 0n ? -v : v; +} + +/** @returns the maximum from a list of bigints */ +export function maxBigInt(...vals: bigint[]): bigint { + return vals.reduce((prev, cur) => (cur > prev ? cur : prev)); +} + +/** @returns the minimum from a list of bigints */ +export function minBigInt(...vals: bigint[]): bigint { + return vals.reduce((prev, cur) => (cur < prev ? cur : prev)); +} diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts index af98ab7ecf..8dab839c1f 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts @@ -1,6 +1,28 @@ import { range } from '../../common/util/util.js'; /** + * @returns a function that converts numerics to strings, depending on if they + * should be treated as integers or not. + */ +export function numericToStringBuilder(is_integer: boolean): (n: number | bigint) => string { + if (is_integer) { + return (val: number | bigint): string => { + if (typeof val === 'number') { + return val.toFixed(); + } + return val.toString(); + }; + } + + return (val: number | bigint): string => { + if (typeof val === 'number') { + return val.toPrecision(6); + } + return val.toString(); + }; +} + +/** * Pretty-prints a "table" of cell values (each being `number | string`), right-aligned. * Each row may be any iterator, including lazily-generated (potentially infinite) rows. * @@ -12,8 +34,11 @@ import { range } from '../../common/util/util.js'; * Each remaining argument provides one row for the table. */ export function generatePrettyTable( - { fillToWidth, numberToString }: { fillToWidth: number; numberToString: (n: number) => string }, - rows: ReadonlyArray<Iterable<string | number>> + { + fillToWidth, + numericToString, + }: { fillToWidth: number; numericToString: (n: number | bigint) => string }, + rows: ReadonlyArray<Iterable<string | number | bigint>> ): string { const rowStrings = range(rows.length, () => ''); let totalTableWidth = 0; @@ -23,7 +48,13 @@ export function generatePrettyTable( for (;;) { const cellsForColumn = iters.map(iter => { const r = iter.next(); // Advance the iterator for each row, in lock-step. - return r.done ? undefined : typeof r.value === 'number' ? numberToString(r.value) : r.value; + if (r.done) { + return undefined; + } + if (typeof r.value === 'number' || typeof r.value === 'bigint') { + return numericToString(r.value); + } + return r.value; }); if (cellsForColumn.every(cell => cell === undefined)) break; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts index 2a09061527..721d121aba 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts @@ -11,6 +11,27 @@ export const kDefaultFragmentShaderCode = ` return vec4<f32>(1.0, 1.0, 1.0, 1.0); }`; +// MAINTENANCE_TODO(#3344): deduplicate fullscreen quad shader code. +export const kFullscreenQuadVertexShaderCode = ` + struct VertexOutput { + @builtin(position) Position : vec4<f32> + }; + + @vertex fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + var pos = array<vec2<f32>, 6>( + vec2<f32>( 1.0, 1.0), + vec2<f32>( 1.0, -1.0), + vec2<f32>(-1.0, -1.0), + vec2<f32>( 1.0, 1.0), + vec2<f32>(-1.0, -1.0), + vec2<f32>(-1.0, 1.0)); + + var output : VertexOutput; + output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0); + return output; + } +`; + const kPlainTypeInfo = { i32: { suffix: '', @@ -157,7 +178,9 @@ export function getFragmentShaderCodeWithOutput( }`; } -export type TShaderStage = 'compute' | 'vertex' | 'fragment' | 'empty'; +export const kValidShaderStages = ['compute', 'vertex', 'fragment'] as const; +export type TValidShaderStage = (typeof kValidShaderStages)[number]; +export type TShaderStage = TValidShaderStage | 'empty'; /** * Return a foo shader of the given stage with the given entry point diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts index 67b4fc7156..8da318aae6 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts @@ -157,15 +157,23 @@ export function viewDimensionsForTextureDimension(textureDimension: GPUTextureDi } } -/** Returns the default view dimension for a given texture descriptor. */ -export function defaultViewDimensionsForTexture(textureDescriptor: Readonly<GPUTextureDescriptor>) { - switch (textureDescriptor.dimension) { +/** Returns the effective view dimension for a given texture dimension and depthOrArrayLayers */ +export function effectiveViewDimensionForDimension( + viewDimension: GPUTextureViewDimension | undefined, + dimension: GPUTextureDimension | undefined, + depthOrArrayLayers: number +) { + if (viewDimension) { + return viewDimension; + } + + switch (dimension || '2d') { case '1d': return '1d'; - case '2d': { - const sizeDict = reifyExtent3D(textureDescriptor.size); - return sizeDict.depthOrArrayLayers > 1 ? '2d-array' : '2d'; - } + case '2d': + case undefined: + return depthOrArrayLayers > 1 ? '2d-array' : '2d'; + break; case '3d': return '3d'; default: @@ -173,6 +181,28 @@ export function defaultViewDimensionsForTexture(textureDescriptor: Readonly<GPUT } } +/** Returns the effective view dimension for a given texture */ +export function effectiveViewDimensionForTexture( + texture: GPUTexture, + viewDimension: GPUTextureViewDimension | undefined +) { + return effectiveViewDimensionForDimension( + viewDimension, + texture.dimension, + texture.depthOrArrayLayers + ); +} + +/** Returns the default view dimension for a given texture descriptor. */ +export function defaultViewDimensionsForTexture(textureDescriptor: Readonly<GPUTextureDescriptor>) { + const sizeDict = reifyExtent3D(textureDescriptor.size); + return effectiveViewDimensionForDimension( + undefined, + textureDescriptor.dimension, + sizeDict.depthOrArrayLayers + ); +} + /** Reifies the optional fields of `GPUTextureDescriptor`. * MAINTENANCE_TODO: viewFormats should not be omitted here, but it seems likely that the * @webgpu/types definition will have to change before we can include it again. diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/color_space_conversions.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/color_space_conversions.spec.ts new file mode 100644 index 0000000000..e6b281d57a --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/color_space_conversions.spec.ts @@ -0,0 +1,108 @@ +export const description = 'Color space conversion helpers'; + +import { Fixture } from '../../../common/framework/fixture.js'; +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { ErrorWithExtra } from '../../../common/util/util.js'; +import { makeInPlaceColorConversion } from '../color_space_conversion.js'; +import { clamp } from '../math.js'; + +import { TexelView } from './texel_view.js'; +import { findFailedPixels } from './texture_ok.js'; + +const kTestColors = [ + [0xff, 0, 0], + [0, 0xff, 0], + [0, 0, 0xff], + [0x80, 0x80, 0], + [0, 0x80, 0x80], + [0x80, 0, 0x80], +] as const; + +function floatToU8(v: number) { + return clamp(Math.round(v * 255), { min: 0, max: 255 }); +} + +export const g = makeTestGroup(Fixture); + +g.test('util_matches_2d_canvas') + .desc(`Test color space conversion helpers matches canvas 2d's color space conversion`) + .params(u => + u.combineWithParams([ + { srcColorSpace: 'srgb', dstColorSpace: 'display-p3' }, + { srcColorSpace: 'display-p3', dstColorSpace: 'srgb' }, + ] as { srcColorSpace: PredefinedColorSpace; dstColorSpace: PredefinedColorSpace }[]) + ) + .fn(t => { + const { srcColorSpace, dstColorSpace } = t.params; + + // putImageData an ImageData(srcColorSpace) in to a canvas2D(dstColorSpace) + // then call getImageData. This will convert the colors via the canvas 2D API + const width = kTestColors.length; + const height = 1; + const imgData = new ImageData( + new Uint8ClampedArray(kTestColors.map(v => [...v, 255]).flat()), + width, + height, + { colorSpace: srcColorSpace } + ); + const ctx = new OffscreenCanvas(width, height).getContext('2d', { + colorSpace: dstColorSpace, + })!; + ctx.putImageData(imgData, 0, 0); + const expectedData = ctx.getImageData(0, 0, width, height).data; + + const conversionFn = makeInPlaceColorConversion({ + srcPremultiplied: false, + dstPremultiplied: false, + srcColorSpace, + dstColorSpace, + }); + + // Convert the data via our conversion functions + const convertedData = new Uint8ClampedArray( + kTestColors + .map(color => { + const [R, G, B] = color.map(v => v / 255); + const floatColor = { R, G, B, A: 1 }; + conversionFn(floatColor); + return [ + floatToU8(floatColor.R), + floatToU8(floatColor.G), + floatToU8(floatColor.B), + floatToU8(floatColor.A), + ]; + }) + .flat() + ); + + const subrectOrigin = [0, 0, 0]; + const subrectSize = [width, height, 1]; + const areaDesc = { + bytesPerRow: width * 4, + rowsPerImage: height, + subrectOrigin, + subrectSize, + }; + + const format = 'rgba8unorm'; + const actTexelView = TexelView.fromTextureDataByReference(format, convertedData, areaDesc); + const expTexelView = TexelView.fromTextureDataByReference(format, expectedData, areaDesc); + + const failedPixelsMessage = findFailedPixels( + format, + { x: 0, y: 0, z: 0 }, + { width, height, depthOrArrayLayers: 1 }, + { actTexelView, expTexelView }, + { maxDiffULPsForNormFormat: 0 } + ); + + if (failedPixelsMessage !== undefined) { + const msg = 'Color space conversion had unexpected results:\n' + failedPixelsMessage; + t.expectOK( + new ErrorWithExtra(msg, () => ({ + expTexelView, + actTexelView, + })) + ); + } + }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts index 20f075e6f2..b063c939a9 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts @@ -297,7 +297,9 @@ TODO: Test NaN, Infinity, -Infinity [1]` g.test('ufloat_texel_data_in_shader') .desc( ` -TODO: Test NaN, Infinity [1]` +Note: this uses values that are representable by both rg11b10ufloat and rgb9e5ufloat. + +TODO: Test NaN, Infinity` ) .params(u => u @@ -312,21 +314,19 @@ TODO: Test NaN, Infinity [1]` // Test extrema makeParam(format, () => 0), - // [2]: Test NaN, Infinity - // Test some values - makeParam(format, () => 0.119140625), - makeParam(format, () => 1.40625), - makeParam(format, () => 24896), + makeParam(format, () => 128), + makeParam(format, () => 1984), + makeParam(format, () => 3968), // Test scattered mixed values makeParam(format, (bitLength, i) => { - return [24896, 1.40625, 0.119140625, 0.23095703125][i]; + return [128, 1984, 3968][i]; }), // Test mixed values that are close in magnitude. makeParam(format, (bitLength, i) => { - return [0.1337890625, 0.17919921875, 0.119140625, 0.125][i]; + return [0.05859375, 0.03125, 0.03515625][i]; }), ]; }) diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts index 42490d800b..0555ac5920 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts @@ -1,5 +1,6 @@ import { assert, unreachable } from '../../../common/util/util.js'; import { UncompressedTextureFormat, EncodableTextureFormat } from '../../format_info.js'; +import { kValue } from '../constants.js'; import { assertInIntegerRange, float32ToFloatBits, @@ -424,6 +425,8 @@ function makeNormalizedInfo( } const dataType: ComponentDataType = opt.signed ? 'snorm' : 'unorm'; + const min = opt.signed ? -1 : 0; + const max = 1; return { componentOrder, componentInfo: makePerTexelComponent(componentOrder, { @@ -438,7 +441,7 @@ function makeNormalizedInfo( numberToBits, bitsToNumber, bitsToULPFromZero, - numericRange: { min: opt.signed ? -1 : 0, max: 1 }, + numericRange: { min, max, finiteMin: min, finiteMax: max }, }; } @@ -454,9 +457,9 @@ function makeIntegerInfo( opt: { signed: boolean } ): TexelRepresentationInfo { assert(bitLength <= 32); - const numericRange = opt.signed - ? { min: -(2 ** (bitLength - 1)), max: 2 ** (bitLength - 1) - 1 } - : { min: 0, max: 2 ** bitLength - 1 }; + const min = opt.signed ? -(2 ** (bitLength - 1)) : 0; + const max = opt.signed ? 2 ** (bitLength - 1) - 1 : 2 ** bitLength - 1; + const numericRange = { min, max, finiteMin: min, finiteMax: max }; const maxUnsignedValue = 2 ** bitLength; const encode = applyEach( (n: number) => (assertInIntegerRange(n, bitLength, opt.signed), n), @@ -576,8 +579,13 @@ function makeFloatInfo( bitsToNumber, bitsToULPFromZero, numericRange: restrictedDepth - ? { min: 0, max: 1 } - : { min: Number.NEGATIVE_INFINITY, max: Number.POSITIVE_INFINITY }, + ? { min: 0, max: 1, finiteMin: 0, finiteMax: 1 } + : { + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + finiteMin: bitLength === 32 ? kValue.f32.negative.min : kValue.f16.negative.min, + finiteMax: bitLength === 32 ? kValue.f32.positive.max : kValue.f16.positive.max, + }, }; } @@ -592,6 +600,7 @@ const identity = (n: number) => n; const kFloat11Format = { signed: 0, exponentBits: 5, mantissaBits: 6, bias: 15 } as const; const kFloat10Format = { signed: 0, exponentBits: 5, mantissaBits: 5, bias: 15 } as const; +export type PerComponentFiniteMax = Record<TexelComponent, number>; export type TexelRepresentationInfo = { /** Order of components in the packed representation. */ readonly componentOrder: TexelComponent[]; @@ -619,7 +628,12 @@ export type TexelRepresentationInfo = { /** Convert integer bit representations into ULPs-from-zero, e.g. unorm8 255 -> 255 ULPs */ readonly bitsToULPFromZero: ComponentMapFn; /** The valid range of numeric "color" values, e.g. [0, Infinity] for ufloat. */ - readonly numericRange: null | { min: number; max: number }; + readonly numericRange: null | { + min: number; + max: number; + finiteMin: number; + finiteMax: number | PerComponentFiniteMax; + }; // Add fields as needed }; @@ -765,7 +779,7 @@ export const kTexelRepresentationInfo: { A: normalizedIntegerAsFloat(components.A!, 2, false), }), bitsToULPFromZero: components => components, - numericRange: { min: 0, max: 1 }, + numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 }, }, rg11b10ufloat: { componentOrder: kRGB, @@ -809,7 +823,16 @@ export const kTexelRepresentationInfo: { G: floatBitsToNormalULPFromZero(components.G!, kFloat11Format), B: floatBitsToNormalULPFromZero(components.B!, kFloat10Format), }), - numericRange: { min: 0, max: Number.POSITIVE_INFINITY }, + numericRange: { + min: 0, + max: Number.POSITIVE_INFINITY, + finiteMin: 0, + finiteMax: { + R: floatBitsToNumber(0b111_1011_1111, kFloat11Format), + G: floatBitsToNumber(0b111_1011_1111, kFloat11Format), + B: floatBitsToNumber(0b11_1101_1111, kFloat10Format), + } as PerComponentFiniteMax, + }, }, rgb9e5ufloat: { componentOrder: kRGB, @@ -854,7 +877,12 @@ export const kTexelRepresentationInfo: { G: floatBitsToNormalULPFromZero(components.G!, kUFloat9e5Format), B: floatBitsToNormalULPFromZero(components.B!, kUFloat9e5Format), }), - numericRange: { min: 0, max: Number.POSITIVE_INFINITY }, + numericRange: { + min: 0, + max: Number.POSITIVE_INFINITY, + finiteMin: 0, + finiteMax: ufloatM9E5BitsToNumber(0b11_1111_1111_1111, kUFloat9e5Format), + }, }, depth32float: makeFloatInfo([TexelComponent.Depth], 32, { restrictedDepth: true }), depth16unorm: makeNormalizedInfo([TexelComponent.Depth], 16, { signed: false, sRGB: false }), @@ -868,7 +896,7 @@ export const kTexelRepresentationInfo: { numberToBits: () => unreachable('depth24plus has no representation'), bitsToNumber: () => unreachable('depth24plus has no representation'), bitsToULPFromZero: () => unreachable('depth24plus has no representation'), - numericRange: { min: 0, max: 1 }, + numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 }, }, stencil8: makeIntegerInfo([TexelComponent.Stencil], 8, { signed: false }), 'depth32float-stencil8': { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts index fea23b674e..0b920ef699 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts @@ -1,6 +1,6 @@ import { assert, memcpy } from '../../../common/util/util.js'; import { kTextureFormatInfo, EncodableTextureFormat } from '../../format_info.js'; -import { generatePrettyTable } from '../pretty_diff_tables.js'; +import { generatePrettyTable, numericToStringBuilder } from '../pretty_diff_tables.js'; import { reifyExtent3D, reifyOrigin3D } from '../unions.js'; import { fullSubrectCoordinates } from './base.js'; @@ -166,10 +166,13 @@ export class TexelView { const info = kTextureFormatInfo[this.format]; const repr = kTexelRepresentationInfo[this.format]; - const integerSampleType = info.sampleType === 'uint' || info.sampleType === 'sint'; - const numberToString = integerSampleType - ? (n: number) => n.toFixed() - : (n: number) => n.toPrecision(6); + // MAINTENANCE_TODO: Print depth-stencil formats as float+int instead of float+float. + const printAsInteger = info.color + ? // For color, pick the type based on the format type + ['uint', 'sint'].includes(info.color.type) + : // Print depth as "float", depth-stencil as "float,float", stencil as "int". + !info.depth; + const numericToString = numericToStringBuilder(printAsInteger); const componentOrderStr = repr.componentOrder.join(',') + ':'; const subrectCoords = [...fullSubrectCoordinates(subrectOrigin, subrectSize)]; @@ -188,13 +191,13 @@ export class TexelView { yield* [' act. colors', '==', componentOrderStr]; for (const coords of subrectCoords) { const pixel = t.color(coords); - yield `${repr.componentOrder.map(ch => numberToString(pixel[ch]!)).join(',')}`; + yield `${repr.componentOrder.map(ch => numericToString(pixel[ch]!)).join(',')}`; } })(this); const opts = { fillToWidth: 120, - numberToString, + numericToString, }; return `${generatePrettyTable(opts, [printCoords, printActualBytes, printActualColors])}`; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts index 7b85489246..d0267f0627 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts @@ -2,7 +2,7 @@ import { assert, ErrorWithExtra, unreachable } from '../../../common/util/util.j import { kTextureFormatInfo, EncodableTextureFormat } from '../../format_info.js'; import { GPUTest } from '../../gpu_test.js'; import { numbersApproximatelyEqual } from '../conversion.js'; -import { generatePrettyTable } from '../pretty_diff_tables.js'; +import { generatePrettyTable, numericToStringBuilder } from '../pretty_diff_tables.js'; import { reifyExtent3D, reifyOrigin3D } from '../unions.js'; import { fullSubrectCoordinates } from './base.js'; @@ -223,11 +223,13 @@ export function findFailedPixels( const info = kTextureFormatInfo[format]; const repr = kTexelRepresentationInfo[format]; - - const integerSampleType = info.sampleType === 'uint' || info.sampleType === 'sint'; - const numberToString = integerSampleType - ? (n: number) => n.toFixed() - : (n: number) => n.toPrecision(6); + // MAINTENANCE_TODO: Print depth-stencil formats as float+int instead of float+float. + const printAsInteger = info.color + ? // For color, pick the type based on the format type + ['uint', 'sint'].includes(info.color.type) + : // Print depth as "float", depth-stencil as "float,float", stencil as "int". + !info.depth; + const numericToString = numericToStringBuilder(printAsInteger); const componentOrderStr = repr.componentOrder.join(',') + ':'; @@ -245,14 +247,14 @@ export function findFailedPixels( yield* [' act. colors', '==', componentOrderStr]; for (const coords of failedPixels) { const pixel = actTexelView.color(coords); - yield `${repr.componentOrder.map(ch => numberToString(pixel[ch]!)).join(',')}`; + yield `${repr.componentOrder.map(ch => numericToString(pixel[ch]!)).join(',')}`; } })(); const printExpectedColors = (function* () { yield* [' exp. colors', '==', componentOrderStr]; for (const coords of failedPixels) { const pixel = expTexelView.color(coords); - yield `${repr.componentOrder.map(ch => numberToString(pixel[ch]!)).join(',')}`; + yield `${repr.componentOrder.map(ch => numericToString(pixel[ch]!)).join(',')}`; } })(); const printActualULPs = (function* () { @@ -272,7 +274,7 @@ export function findFailedPixels( const opts = { fillToWidth: 120, - numberToString, + numericToString, }; return `\ between ${lowerCorner} and ${upperCorner} inclusive: diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts index 163930e20e..b4da68ba36 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts @@ -13,7 +13,6 @@ import { GPUConst } from '../../constants.js'; import { kAllTextureFormats, kFeaturesForFormats, - kTextureFormats, filterFormatsByFeature, viewCompatible, } from '../../format_info.js'; @@ -387,7 +386,7 @@ g.test('viewFormats') .combine('viewFormatFeature', kFeaturesForFormats) .beginSubcases() .expand('viewFormat', ({ viewFormatFeature }) => - filterFormatsByFeature(viewFormatFeature, kTextureFormats) + filterFormatsByFeature(viewFormatFeature, kAllTextureFormats) ) ) .beforeAllSubcases(t => { @@ -402,7 +401,7 @@ g.test('viewFormats') const ctx = canvas.getContext('webgpu'); assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas'); - const compatible = viewCompatible(format, viewFormat); + const compatible = viewCompatible(t.isCompatibility, format, viewFormat); // Test configure() produces an error if the formats aren't compatible. t.expectValidationError(() => { diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts index 609dacb907..633d88d2b8 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts @@ -40,6 +40,27 @@ class GPUContextTest extends GPUTest { return ctx; } + + expectTextureDestroyed(texture: GPUTexture, expectDestroyed = true) { + this.expectValidationError(() => { + // Try using the texture in a render pass. Because it's a canvas texture + // it should have RENDER_ATTACHMENT usage. + assert((texture.usage & GPUTextureUsage.RENDER_ATTACHMENT) !== 0); + const encoder = this.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(), + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.end(); + // Submitting should generate a validation error if the texture is destroyed. + this.queue.submit([encoder.finish()]); + }, expectDestroyed); + } } export const g = makeTestGroup(GPUContextTest); @@ -170,7 +191,7 @@ g.test('multiple_frames') // Ensure that each frame a new texture object is returned. t.expect(currentTexture !== prevTexture); - // Ensure that texture contents are transparent black. + // Ensure that the texture's initial contents are transparent black. t.expectSingleColor(currentTexture, currentTexture.format, { size: [currentTexture.width, currentTexture.height, 1], exp: { R: 0, G: 0, B: 0, A: 0 }, @@ -178,7 +199,8 @@ g.test('multiple_frames') } if (clearTexture) { - // Clear the texture to test that texture contents don't carry over from frame to frame. + // Fill the texture with a non-zero color, to test that texture + // contents don't carry over from frame to frame. const encoder = t.device.createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: [ @@ -215,10 +237,8 @@ g.test('multiple_frames') } } - // Call frameCheck for the first time from requestAnimationFrame - // To make sure two frameChecks are run in different frames for onscreen canvas. - // offscreen canvas doesn't care. - requestAnimationFrame(frameCheck); + // Render the first frame immediately. The rest will be triggered recursively. + frameCheck(); }); }); @@ -235,6 +255,8 @@ g.test('resize') // Trigger a resize by changing the width. ctx.canvas.width = 4; + t.expectTextureDestroyed(prevTexture); + // When the canvas resizes the texture returned by getCurrentTexture should immediately begin // returning a new texture matching the update dimensions. let currentTexture = ctx.getCurrentTexture(); @@ -263,7 +285,6 @@ g.test('resize') t.expect(currentTexture.height === ctx.canvas.height); t.expect(prevTexture.width === 4); t.expect(prevTexture.height === 2); - prevTexture = currentTexture; // Ensure that texture contents are transparent black. t.expectSingleColor(currentTexture, currentTexture.format, { @@ -271,13 +292,31 @@ g.test('resize') exp: { R: 0, G: 0, B: 0, A: 0 }, }); - // Simply setting the canvas width and height values to their current values should not trigger - // a change in the texture. - ctx.canvas.width = 4; - ctx.canvas.height = 4; - - currentTexture = ctx.getCurrentTexture(); - t.expect(prevTexture === currentTexture); + // HTMLCanvasElement behaves differently than OffscreenCanvas + if (t.params.canvasType === 'onscreen') { + // Ensure canvas goes back to defaults when set to negative numbers. + ctx.canvas.width = -1; + currentTexture = ctx.getCurrentTexture(); + t.expect(currentTexture.width === 300); + t.expect(currentTexture.height === 4); + + ctx.canvas.height = -1; + currentTexture = ctx.getCurrentTexture(); + t.expect(currentTexture.width === 300); + t.expect(currentTexture.height === 150); + + // Setting the canvas width and height values to their current values should + // still trigger a change in the texture. + prevTexture = ctx.getCurrentTexture(); + const { width, height } = ctx.canvas; + ctx.canvas.width = width; + ctx.canvas.height = height; + + t.expectTextureDestroyed(prevTexture); + + currentTexture = ctx.getCurrentTexture(); + t.expect(prevTexture !== currentTexture); + } }); g.test('expiry') @@ -307,6 +346,14 @@ TODO: test more canvas types, and ways to update the rendering .combine('prevFrameCallsite', ['runInNewCanvasFrame', 'requestAnimationFrame'] as const) .combine('getCurrentTextureAgain', [true, false] as const) ) + .beforeAllSubcases(t => { + if ( + t.params.prevFrameCallsite === 'requestAnimationFrame' && + typeof requestAnimationFrame === 'undefined' + ) { + throw new SkipTestCase('requestAnimationFrame not available'); + } + }) .fn(t => { const { canvasType, prevFrameCallsite, getCurrentTextureAgain } = t.params; const ctx = t.initCanvasContext(t.params.canvasType); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts index 7fd7142f00..84d940ed04 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts @@ -14,7 +14,12 @@ TODO: implement all canvas types, see TODO on kCanvasTypes. `; import { makeTestGroup } from '../../../common/framework/test_group.js'; -import { assert, raceWithRejectOnTimeout, unreachable } from '../../../common/util/util.js'; +import { + ErrorWithExtra, + assert, + raceWithRejectOnTimeout, + unreachable, +} from '../../../common/util/util.js'; import { kCanvasAlphaModes, kCanvasColorSpaces, @@ -27,7 +32,10 @@ import { CanvasType, createCanvas, createOnscreenCanvas, + createOffscreenCanvas, } from '../../util/create_elements.js'; +import { TexelView } from '../../util/texture/texel_view.js'; +import { findFailedPixels } from '../../util/texture/texture_ok.js'; export const g = makeTestGroup(GPUTest); @@ -61,6 +69,26 @@ const expect = { ]), }; +/** + * Given 4 pixels in rgba8unorm format, puts them into an ImageData + * of the specified color space and then puts them into an srgb color space + * canvas (the default). If the color space is different there will be a + * conversion. Returns the resulting 4 pixels in rgba8unorm format. + */ +function convertRGBA8UnormBytesToColorSpace( + expected: Uint8ClampedArray, + srcColorSpace: PredefinedColorSpace, + dstColorSpace: PredefinedColorSpace +) { + const srcImgData = new ImageData(2, 2, { colorSpace: srcColorSpace }); + srcImgData.data.set(expected); + const dstCanvas = new OffscreenCanvas(2, 2); + const dstCtx = dstCanvas.getContext('2d', { colorSpace: dstColorSpace }); + assert(dstCtx !== null); + dstCtx.putImageData(srcImgData, 0, 0); + return dstCtx.getImageData(0, 0, 2, 2).data; +} + function initWebGPUCanvasContent<T extends CanvasType>( t: GPUTest, format: GPUTextureFormat, @@ -119,7 +147,7 @@ function drawImageSourceIntoCanvas( image: CanvasImageSource, colorSpace: PredefinedColorSpace ) { - const canvas: HTMLCanvasElement = createOnscreenCanvas(t, 2, 2); + const canvas = createOffscreenCanvas(t, 2, 2); const ctx = canvas.getContext('2d', { colorSpace }); assert(ctx !== null); ctx.drawImage(image, 0, 0); @@ -147,22 +175,13 @@ function checkImageResultWithDifferentColorSpaceCanvas( // draw the WebGPU derived data into a canvas const fromWebGPUCtx = drawImageSourceIntoCanvas(t, image, destinationColorSpace); - // create a 2D canvas with the same source data in the same color space as the WebGPU - // canvas - const source2DCanvas: HTMLCanvasElement = createOnscreenCanvas(t, 2, 2); - const source2DCtx = source2DCanvas.getContext('2d', { colorSpace: sourceColorSpace }); - assert(source2DCtx !== null); - const imgData = source2DCtx.getImageData(0, 0, 2, 2); - imgData.data.set(sourceData); - source2DCtx.putImageData(imgData, 0, 0); - - // draw the source 2D canvas into another 2D canvas with the destination color space and - // then pull out the data. This result should be the same as the WebGPU derived data - // written to a 2D canvas of the same destination color space. - const from2DCtx = drawImageSourceIntoCanvas(t, source2DCanvas, destinationColorSpace); - const expect = from2DCtx.getImageData(0, 0, 2, 2).data; - - readPixelsFrom2DCanvasAndCompare(t, fromWebGPUCtx, expect); + const expect = convertRGBA8UnormBytesToColorSpace( + sourceData, + sourceColorSpace, + destinationColorSpace + ); + + readPixelsFrom2DCanvasAndCompare(t, fromWebGPUCtx, expect, 2); } function checkImageResult( @@ -171,18 +190,55 @@ function checkImageResult( sourceColorSpace: PredefinedColorSpace, expect: Uint8ClampedArray ) { + // canvas(colorSpace)->img(colorSpace)->canvas(colorSpace).drawImage->canvas(colorSpace).getImageData->actual + // hard coded data->expected checkImageResultWithSameColorSpaceCanvas(t, image, sourceColorSpace, expect); + + // canvas(colorSpace)->img(colorSpace)->canvas(diffColorSpace).drawImage->canvas(diffColorSpace).getImageData->actual + // hard coded data->ImageData(colorSpace)->canvas(diffColorSpace).putImageData->canvas(diffColorSpace).getImageData->expected checkImageResultWithDifferentColorSpaceCanvas(t, image, sourceColorSpace, expect); } function readPixelsFrom2DCanvasAndCompare( t: GPUTest, ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, - expect: Uint8ClampedArray + expect: Uint8ClampedArray, + maxDiffULPsForNormFormat = 0 ) { - const actual = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data; + const { width, height } = ctx.canvas; + const actual = ctx.getImageData(0, 0, width, height).data; - t.expectOK(checkElementsEqual(actual, expect)); + const subrectOrigin = [0, 0, 0]; + const subrectSize = [width, height, 1]; + + const areaDesc = { + bytesPerRow: width * 4, + rowsPerImage: height, + subrectOrigin, + subrectSize, + }; + + const format = 'rgba8unorm'; + const actTexelView = TexelView.fromTextureDataByReference(format, actual, areaDesc); + const expTexelView = TexelView.fromTextureDataByReference(format, expect, areaDesc); + + const failedPixelsMessage = findFailedPixels( + format, + { x: 0, y: 0, z: 0 }, + { width, height, depthOrArrayLayers: 1 }, + { actTexelView, expTexelView }, + { maxDiffULPsForNormFormat } + ); + + if (failedPixelsMessage !== undefined) { + const msg = 'Canvas had unexpected contents:\n' + failedPixelsMessage; + t.expectOK( + new ErrorWithExtra(msg, () => ({ + expTexelView, + actTexelView, + })) + ); + } } g.test('onscreenCanvas,snapshot') @@ -265,7 +321,7 @@ g.test('offscreenCanvas,snapshot') .combine('format', kCanvasTextureFormats) .combine('alphaMode', kCanvasAlphaModes) .combine('colorSpace', kCanvasColorSpaces) - .combine('snapshotType', ['convertToBlob', 'transferToImageBitmap', 'imageBitmap']) + .combine('snapshotType', ['convertToBlob', 'transferToImageBitmap', 'imageBitmap'] as const) ) .fn(async t => { const offscreenCanvas = initWebGPUCanvasContent( @@ -284,11 +340,7 @@ g.test('offscreenCanvas,snapshot') return; } const blob = await offscreenCanvas.convertToBlob(); - const url = URL.createObjectURL(blob); - const img = new Image(offscreenCanvas.width, offscreenCanvas.height); - img.src = url; - await raceWithRejectOnTimeout(img.decode(), 5000, 'load image timeout'); - snapshot = img; + snapshot = await createImageBitmap(blob); break; } case 'transferToImageBitmap': { @@ -383,6 +435,19 @@ g.test('drawTo2DCanvas') - colorSpace = {"srgb", "display-p3"} - WebGPU canvas type = {"onscreen", "offscreen"} - 2d canvas type = {"onscreen", "offscreen"} + + + * makes a webgpu canvas with the given colorSpace and puts data in via copy convoluted + copy process + * makes a 2d canvas with 'srgb' colorSpace (the default) + * draws the webgpu canvas into the 2d canvas so if the color spaces do not match + there will be a conversion. + * gets the pixels from the 2d canvas via getImageData + * compares them to hard coded values that are converted to expected values by copying + to an ImageData of the given color space, and then using putImageData into an srgb canvas. + + canvas(colorSpace) -> canvas(srgb).drawImage -> canvas(srgb).getImageData -> actual + ImageData(colorSpace) -> canvas(srgb).putImageData -> canvas(srgb).getImageData -> expected ` ) .params(u => @@ -396,35 +461,47 @@ g.test('drawTo2DCanvas') .fn(t => { const { format, webgpuCanvasType, alphaMode, colorSpace, canvas2DType } = t.params; - const canvas = initWebGPUCanvasContent(t, format, alphaMode, colorSpace, webgpuCanvasType); + const webgpuCanvas = initWebGPUCanvasContent( + t, + format, + alphaMode, + colorSpace, + webgpuCanvasType + ); - const expectCanvas = createCanvas(t, canvas2DType, canvas.width, canvas.height); - const ctx = expectCanvas.getContext('2d') as CanvasRenderingContext2D; + const actualCanvas = createCanvas(t, canvas2DType, webgpuCanvas.width, webgpuCanvas.height); + const ctx = actualCanvas.getContext('2d') as CanvasRenderingContext2D; if (ctx === null) { t.skip(canvas2DType + ' canvas cannot get 2d context'); return; } - ctx.drawImage(canvas, 0, 0); - readPixelsFrom2DCanvasAndCompare(t, ctx, expect[t.params.alphaMode]); + ctx.drawImage(webgpuCanvas, 0, 0); + + readPixelsFrom2DCanvasAndCompare( + t, + ctx, + convertRGBA8UnormBytesToColorSpace(expect[t.params.alphaMode], colorSpace, 'srgb') + ); }); g.test('transferToImageBitmap_unconfigured_nonzero_size') .desc( `Regression test for a crash when calling transferImageBitmap on an unconfigured. Case where the canvas is not empty` ) + .params(u => u.combine('readbackCanvasType', ['onscreen', 'offscreen'] as const)) .fn(t => { - const canvas = createCanvas(t, 'offscreen', 2, 3); + const kWidth = 2; + const kHeight = 3; + const canvas = createCanvas(t, 'offscreen', kWidth, kHeight); canvas.getContext('webgpu'); // Transferring gives an ImageBitmap of the correct size filled with transparent black. const ib = canvas.transferToImageBitmap(); - t.expect(ib.width === canvas.width); - t.expect(ib.height === canvas.height); + t.expect(ib.width === kWidth); + t.expect(ib.height === kHeight); - const readbackCanvas = document.createElement('canvas'); - readbackCanvas.width = canvas.width; - readbackCanvas.height = canvas.height; + const readbackCanvas = createCanvas(t, t.params.readbackCanvasType, kWidth, kHeight); const readbackContext = readbackCanvas.getContext('2d', { alpha: true, }); @@ -434,7 +511,7 @@ g.test('transferToImageBitmap_unconfigured_nonzero_size') } // Since there isn't a configuration we expect the ImageBitmap to have the default alphaMode of "opaque". - const expected = new Uint8ClampedArray(canvas.width * canvas.height * 4); + const expected = new Uint8ClampedArray(kWidth * kHeight * 4); for (let i = 0; i < expected.byteLength; i += 4) { expected[i + 0] = 0; expected[i + 1] = 0; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts index 06c3cd30b2..dc80eff9de 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts @@ -97,9 +97,7 @@ class F extends CopyToTextureUtils { } const imageData = new ImageData(imagePixels, width, height, { colorSpace }); - // MAINTENANCE_TODO: Remove as any when tsc support imageData.colorSpace - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - if (typeof (imageData as any).colorSpace === 'undefined') { + if (typeof imageData.colorSpace === 'undefined') { this.skip('color space attr is not supported for ImageData'); } @@ -762,7 +760,7 @@ g.test('color_space_conversion') .params(u => u .combine('srcColorSpace', ['srgb', 'display-p3'] as const) - .combine('dstColorSpace', ['srgb'] as const) + .combine('dstColorSpace', ['srgb', 'display-p3'] as const) .combine('dstColorFormat', kValidTextureFormatsForCopyE2T) .combine('dstPremultiplied', [true, false]) .combine('srcDoFlipYDuringCopy', [true, false]) diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts index 1888eb7e58..3f73a41c84 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts @@ -1,8 +1,10 @@ export const description = ` copyToTexture with HTMLVideoElement and VideoFrame. -- videos with various encodings/formats (webm vp8, webm vp9, ogg theora, mp4), color spaces - (bt.601, bt.709, bt.2020) +- videos with various encodings/formats (webm vp8, webm vp9, ogg theora, mp4), video color spaces + (bt.601, bt.709, bt.2020) and dst color spaces(display-p3, srgb). + + TODO: Test video in BT.2020 color space `; import { makeTestGroup } from '../../../common/framework/test_group.js'; @@ -11,7 +13,11 @@ import { startPlayingAndWaitForVideo, getVideoElement, getVideoFrameFromVideoElement, - kVideoExpectations, + convertToUnorm8, + kPredefinedColorSpace, + kVideoNames, + kVideoInfo, + kVideoExpectedColors, } from '../../web_platform/util.js'; const kFormat = 'rgba8unorm'; @@ -36,17 +42,17 @@ It creates HTMLVideoElement with videos under Resource folder. - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases) - TODO: partial copy tests should be added - TODO: all valid dstColorFormat tests should be added. - - TODO: dst color space tests need to be added ` ) .params(u => u // - .combineWithParams(kVideoExpectations) + .combine('videoName', kVideoNames) .combine('sourceType', ['VideoElement', 'VideoFrame'] as const) .combine('srcDoFlipYDuringCopy', [true, false]) + .combine('dstColorSpace', kPredefinedColorSpace) ) .fn(async t => { - const { videoName, sourceType, srcDoFlipYDuringCopy } = t.params; + const { videoName, sourceType, srcDoFlipYDuringCopy, dstColorSpace } = t.params; if (sourceType === 'VideoFrame' && typeof VideoFrame === 'undefined') { t.skip('WebCodec is not supported'); @@ -58,8 +64,8 @@ It creates HTMLVideoElement with videos under Resource folder. let source, width, height; if (sourceType === 'VideoFrame') { source = await getVideoFrameFromVideoElement(t, videoElement); - width = source.codedWidth; - height = source.codedHeight; + width = source.displayWidth; + height = source.displayHeight; } else { source = videoElement; width = source.videoWidth; @@ -82,33 +88,63 @@ It creates HTMLVideoElement with videos under Resource folder. { texture: dstTexture, origin: { x: 0, y: 0 }, - colorSpace: 'srgb', + colorSpace: dstColorSpace, premultipliedAlpha: true, }, { width, height, depthOrArrayLayers: 1 } ); + const srcColorSpace = kVideoInfo[videoName].colorSpace; + const presentColors = kVideoExpectedColors[srcColorSpace][dstColorSpace]; + + // visible rect is whole frame, no clipping. + const expect = kVideoInfo[videoName].display; + if (srcDoFlipYDuringCopy) { t.expectSinglePixelComparisonsAreOkInTexture({ texture: dstTexture }, [ - // Top-left should be blue. - { coord: { x: width * 0.25, y: height * 0.25 }, exp: t.params._blueExpectation }, - // Top-right should be green. - { coord: { x: width * 0.75, y: height * 0.25 }, exp: t.params._greenExpectation }, - // Bottom-left should be yellow. - { coord: { x: width * 0.25, y: height * 0.75 }, exp: t.params._yellowExpectation }, - // Bottom-right should be red. - { coord: { x: width * 0.75, y: height * 0.75 }, exp: t.params._redExpectation }, + // Flipped top-left. + { + coord: { x: width * 0.25, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.bottomLeftColor]), + }, + // Flipped top-right. + { + coord: { x: width * 0.75, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.bottomRightColor]), + }, + // Flipped bottom-left. + { + coord: { x: width * 0.25, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.topLeftColor]), + }, + // Flipped bottom-right. + { + coord: { x: width * 0.75, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.topRightColor]), + }, ]); } else { t.expectSinglePixelComparisonsAreOkInTexture({ texture: dstTexture }, [ - // Top-left should be yellow. - { coord: { x: width * 0.25, y: height * 0.25 }, exp: t.params._yellowExpectation }, - // Top-right should be red. - { coord: { x: width * 0.75, y: height * 0.25 }, exp: t.params._redExpectation }, - // Bottom-left should be blue. - { coord: { x: width * 0.25, y: height * 0.75 }, exp: t.params._blueExpectation }, - // Bottom-right should be green. - { coord: { x: width * 0.75, y: height * 0.75 }, exp: t.params._greenExpectation }, + // Top-left. + { + coord: { x: width * 0.25, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topLeftColor]), + }, + // Top-right. + { + coord: { x: width * 0.75, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topRightColor]), + }, + // Bottom-left. + { + coord: { x: width * 0.25, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomLeftColor]), + }, + // Bottom-right. + { + coord: { x: width * 0.75, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomRightColor]), + }, ]); } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts index baa2a985d2..8e812ccd2a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts @@ -1,20 +1,25 @@ export const description = ` Tests for external textures from HTMLVideoElement (and other video-type sources?). -- videos with various encodings/formats (webm vp8, webm vp9, ogg theora, mp4), color spaces - (bt.601, bt.709, bt.2020) +- videos with various encodings/formats (webm vp8, webm vp9, ogg theora, mp4), video color spaces + (bt.601, bt.709, bt.2020) and dst color spaces(display-p3, srgb) TODO: consider whether external_texture and copyToTexture video tests should be in the same file +TODO(#3193): Test video in BT.2020 color space `; import { makeTestGroup } from '../../../common/framework/test_group.js'; import { GPUTest, TextureTestMixin } from '../../gpu_test.js'; +import { createCanvas } from '../../util/create_elements.js'; import { startPlayingAndWaitForVideo, getVideoFrameFromVideoElement, getVideoElement, - kVideoExpectations, - kVideoRotationExpectations, + convertToUnorm8, + kPredefinedColorSpace, + kVideoNames, + kVideoInfo, + kVideoExpectedColors, } from '../../web_platform/util.js'; const kHeight = 16; @@ -23,7 +28,10 @@ const kFormat = 'rgba8unorm'; export const g = makeTestGroup(TextureTestMixin(GPUTest)); -function createExternalTextureSamplingTestPipeline(t: GPUTest): GPURenderPipeline { +function createExternalTextureSamplingTestPipeline( + t: GPUTest, + colorAttachmentFormat: GPUTextureFormat = kFormat +): GPURenderPipeline { const pipeline = t.device.createRenderPipeline({ layout: 'auto', vertex: { @@ -59,7 +67,7 @@ function createExternalTextureSamplingTestPipeline(t: GPUTest): GPURenderPipelin entryPoint: 'main', targets: [ { - format: kFormat, + format: colorAttachmentFormat, }, ], }, @@ -73,13 +81,14 @@ function createExternalTextureSamplingTestBindGroup( t: GPUTest, checkNonStandardIsZeroCopy: true | undefined, source: HTMLVideoElement | VideoFrame, - pipeline: GPURenderPipeline + pipeline: GPURenderPipeline, + dstColorSpace: PredefinedColorSpace ): GPUBindGroup { const linearSampler = t.device.createSampler(); const externalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, + source, + colorSpace: dstColorSpace, }); if (checkNonStandardIsZeroCopy) { @@ -133,22 +142,24 @@ g.test('importExternalTexture,sample') .desc( ` Tests that we can import an HTMLVideoElement/VideoFrame into a GPUExternalTexture, sample from it -for several combinations of video format and color space. +for several combinations of video format, video color spaces and dst color spaces. ` ) .params(u => u // .combineWithParams(checkNonStandardIsZeroCopyIfAvailable()) + .combine('videoName', kVideoNames) .combine('sourceType', ['VideoElement', 'VideoFrame'] as const) - .combineWithParams(kVideoExpectations) + .combine('dstColorSpace', kPredefinedColorSpace) ) .fn(async t => { - const sourceType = t.params.sourceType; + const { videoName, sourceType, dstColorSpace } = t.params; + if (sourceType === 'VideoFrame' && typeof VideoFrame === 'undefined') { t.skip('WebCodec is not supported'); } - const videoElement = getVideoElement(t, t.params.videoName); + const videoElement = getVideoElement(t, videoName); await startPlayingAndWaitForVideo(videoElement, async () => { const source = @@ -167,7 +178,8 @@ for several combinations of video format and color space. t, t.params.checkNonStandardIsZeroCopy, source, - pipeline + pipeline, + dstColorSpace ); const commandEncoder = t.device.createCommandEncoder(); @@ -187,88 +199,162 @@ for several combinations of video format and color space. passEncoder.end(); t.device.queue.submit([commandEncoder.finish()]); + const srcColorSpace = kVideoInfo[videoName].colorSpace; + const presentColors = kVideoExpectedColors[srcColorSpace][dstColorSpace]; + + // visible rect is whole frame, no clipping. + const expect = kVideoInfo[videoName].display; + // For validation, we sample a few pixels away from the edges to avoid compression // artifacts. t.expectSinglePixelComparisonsAreOkInTexture({ texture: colorAttachment }, [ - // Top-left should be yellow. - { coord: { x: kWidth * 0.25, y: kHeight * 0.25 }, exp: t.params._yellowExpectation }, - // Top-right should be red. - { coord: { x: kWidth * 0.75, y: kHeight * 0.25 }, exp: t.params._redExpectation }, - // Bottom-left should be blue. - { coord: { x: kWidth * 0.25, y: kHeight * 0.75 }, exp: t.params._blueExpectation }, - // Bottom-right should be green. - { coord: { x: kWidth * 0.75, y: kHeight * 0.75 }, exp: t.params._greenExpectation }, + // Top-left. + { + coord: { x: kWidth * 0.25, y: kHeight * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topLeftColor]), + }, + // Top-right. + { + coord: { x: kWidth * 0.75, y: kHeight * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topRightColor]), + }, + // Bottom-left. + { + coord: { x: kWidth * 0.25, y: kHeight * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomLeftColor]), + }, + // Bottom-right. + { + coord: { x: kWidth * 0.75, y: kHeight * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomRightColor]), + }, ]); - - if (sourceType === 'VideoFrame') (source as VideoFrame).close(); }); }); -g.test('importExternalTexture,sampleWithRotationMetadata') +g.test('importExternalTexture,sample_non_YUV_video_frame') .desc( ` -Tests that when importing an HTMLVideoElement/VideoFrame into a GPUExternalTexture, sampling from -it will honor rotation metadata. +Tests that we can import an VideoFrame with non-YUV pixel format into a GPUExternalTexture and sample it. ` ) .params(u => u // - .combineWithParams(checkNonStandardIsZeroCopyIfAvailable()) - .combine('sourceType', ['VideoElement', 'VideoFrame'] as const) - .combineWithParams(kVideoRotationExpectations) + .combine('videoFrameFormat', ['RGBA', 'RGBX', 'BGRA', 'BGRX'] as const) ) - .fn(async t => { - const sourceType = t.params.sourceType; - const videoElement = getVideoElement(t, t.params.videoName); + .fn(t => { + const { videoFrameFormat } = t.params; - await startPlayingAndWaitForVideo(videoElement, async () => { - const source = - sourceType === 'VideoFrame' - ? await getVideoFrameFromVideoElement(t, videoElement) - : videoElement; + if (typeof VideoFrame === 'undefined') { + t.skip('WebCodec is not supported'); + } - const colorAttachment = t.device.createTexture({ - format: kFormat, - size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 }, - usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, - }); + const canvas = createCanvas(t, 'onscreen', kWidth, kHeight); - const pipeline = createExternalTextureSamplingTestPipeline(t); - const bindGroup = createExternalTextureSamplingTestBindGroup( - t, - t.params.checkNonStandardIsZeroCopy, - source, - pipeline - ); + const canvasContext = canvas.getContext('2d'); - const commandEncoder = t.device.createCommandEncoder(); - const passEncoder = commandEncoder.beginRenderPass({ - colorAttachments: [ - { - view: colorAttachment.createView(), - clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - loadOp: 'clear', - storeOp: 'store', - }, - ], - }); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, bindGroup); - passEncoder.draw(6); - passEncoder.end(); - t.device.queue.submit([commandEncoder.finish()]); + if (canvasContext === null) { + t.skip(' onscreen canvas 2d context not available'); + } - // For validation, we sample a few pixels away from the edges to avoid compression - // artifacts. - t.expectSinglePixelComparisonsAreOkInTexture({ texture: colorAttachment }, [ - { coord: { x: kWidth * 0.25, y: kHeight * 0.25 }, exp: t.params._topLeftExpectation }, - { coord: { x: kWidth * 0.75, y: kHeight * 0.25 }, exp: t.params._topRightExpectation }, - { coord: { x: kWidth * 0.25, y: kHeight * 0.75 }, exp: t.params._bottomLeftExpectation }, - { coord: { x: kWidth * 0.75, y: kHeight * 0.75 }, exp: t.params._bottomRightExpectation }, - ]); + const ctx = canvasContext as CanvasRenderingContext2D; + + const rectWidth = Math.floor(kWidth / 2); + const rectHeight = Math.floor(kHeight / 2); + + // Red + ctx.fillStyle = `rgba(255, 0, 0, 1.0)`; + ctx.fillRect(0, 0, rectWidth, rectHeight); + // Lime + ctx.fillStyle = `rgba(0, 255, 0, 1.0)`; + ctx.fillRect(rectWidth, 0, kWidth - rectWidth, rectHeight); + // Blue + ctx.fillStyle = `rgba(0, 0, 255, 1.0)`; + ctx.fillRect(0, rectHeight, rectWidth, kHeight - rectHeight); + // Fuchsia + ctx.fillStyle = `rgba(255, 0, 255, 1.0)`; + ctx.fillRect(rectWidth, rectHeight, kWidth - rectWidth, kHeight - rectHeight); + + const imageData = ctx.getImageData(0, 0, kWidth, kHeight); + + // Create video frame with default color space 'srgb' + const frameInit: VideoFrameBufferInit = { + format: videoFrameFormat, + codedWidth: kWidth, + codedHeight: kHeight, + timestamp: 0, + }; + + const frame = new VideoFrame(imageData.data.buffer, frameInit); + let textureFormat: GPUTextureFormat = 'rgba8unorm'; + + if (videoFrameFormat === 'BGRA' || videoFrameFormat === 'BGRX') { + textureFormat = 'bgra8unorm'; + } - if (sourceType === 'VideoFrame') (source as VideoFrame).close(); + const colorAttachment = t.device.createTexture({ + format: textureFormat, + size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 }, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const pipeline = createExternalTextureSamplingTestPipeline(t, textureFormat); + const bindGroup = createExternalTextureSamplingTestBindGroup( + t, + undefined /* checkNonStandardIsZeroCopy */, + frame, + pipeline, + 'srgb' + ); + + const commandEncoder = t.device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: colorAttachment.createView(), + clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], }); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, bindGroup); + passEncoder.draw(6); + passEncoder.end(); + t.device.queue.submit([commandEncoder.finish()]); + + const expected = { + topLeft: new Uint8Array([255, 0, 0, 255]), + topRight: new Uint8Array([0, 255, 0, 255]), + bottomLeft: new Uint8Array([0, 0, 255, 255]), + bottomRight: new Uint8Array([255, 0, 255, 255]), + }; + + // For validation, we sample a few pixels away from the edges to avoid compression + // artifacts. + t.expectSinglePixelComparisonsAreOkInTexture({ texture: colorAttachment }, [ + // Top-left. + { + coord: { x: kWidth * 0.25, y: kHeight * 0.25 }, + exp: expected.topLeft, + }, + // Top-right. + { + coord: { x: kWidth * 0.75, y: kHeight * 0.25 }, + exp: expected.topRight, + }, + // Bottom-left. + { + coord: { x: kWidth * 0.25, y: kHeight * 0.75 }, + exp: expected.bottomLeft, + }, + // Bottom-right. + { + coord: { x: kWidth * 0.75, y: kHeight * 0.75 }, + exp: expected.bottomRight, + }, + ]); }); g.test('importExternalTexture,sampleWithVideoFrameWithVisibleRectParam') @@ -281,10 +367,13 @@ parameters are present. .params(u => u // .combineWithParams(checkNonStandardIsZeroCopyIfAvailable()) - .combineWithParams(kVideoExpectations) + .combine('videoName', kVideoNames) + .combine('dstColorSpace', kPredefinedColorSpace) ) .fn(async t => { - const videoElement = getVideoElement(t, t.params.videoName); + const { videoName, dstColorSpace } = t.params; + + const videoElement = getVideoElement(t, videoName); await startPlayingAndWaitForVideo(videoElement, async () => { const source = await getVideoFrameFromVideoElement(t, videoElement); @@ -292,15 +381,24 @@ parameters are present. // All tested videos are derived from an image showing yellow, red, blue or green in each // quadrant. In this test we crop the video to each quadrant and check that desired color // is sampled from each corner of the cropped image. - const srcVideoHeight = 240; - const srcVideoWidth = 320; + // visible rect clip applies on raw decoded frame, which defines based on video frame coded size. + const srcVideoHeight = source.codedHeight; + const srcVideoWidth = source.codedWidth; + + const srcColorSpace = kVideoInfo[videoName].colorSpace; + const presentColors = kVideoExpectedColors[srcColorSpace][dstColorSpace]; + + // The test crops raw decoded videos first and then apply transform. Expectation should + // use coded colors as reference. + const expect = kVideoInfo[videoName].coded; + const cropParams = [ - // Top left (yellow) + // Top left { subRect: { x: 0, y: 0, width: srcVideoWidth / 2, height: srcVideoHeight / 2 }, - color: t.params._yellowExpectation, + color: convertToUnorm8(presentColors[expect.topLeftColor]), }, - // Top right (red) + // Top right { subRect: { x: srcVideoWidth / 2, @@ -308,9 +406,9 @@ parameters are present. width: srcVideoWidth / 2, height: srcVideoHeight / 2, }, - color: t.params._redExpectation, + color: convertToUnorm8(presentColors[expect.topRightColor]), }, - // Bottom left (blue) + // Bottom left { subRect: { x: 0, @@ -318,9 +416,9 @@ parameters are present. width: srcVideoWidth / 2, height: srcVideoHeight / 2, }, - color: t.params._blueExpectation, + color: convertToUnorm8(presentColors[expect.bottomLeftColor]), }, - // Bottom right (green) + // Bottom right { subRect: { x: srcVideoWidth / 2, @@ -328,14 +426,12 @@ parameters are present. width: srcVideoWidth / 2, height: srcVideoHeight / 2, }, - color: t.params._greenExpectation, + color: convertToUnorm8(presentColors[expect.bottomRightColor]), }, ]; for (const cropParam of cropParams) { - // MAINTENANCE_TODO: remove cast with TypeScript 4.9.6+. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const subRect = new VideoFrame(source as any, { visibleRect: cropParam.subRect }); + const subRect = new VideoFrame(source, { visibleRect: cropParam.subRect }); const colorAttachment = t.device.createTexture({ format: kFormat, @@ -348,7 +444,8 @@ parameters are present. t, t.params.checkNonStandardIsZeroCopy, subRect, - pipeline + pipeline, + dstColorSpace ); const commandEncoder = t.device.createCommandEncoder(); @@ -387,22 +484,24 @@ g.test('importExternalTexture,compute') .desc( ` Tests that we can import an HTMLVideoElement/VideoFrame into a GPUExternalTexture and use it in a -compute shader, for several combinations of video format and color space. +compute shader, for several combinations of video format, video color spaces and dst color spaces. ` ) .params(u => u // .combineWithParams(checkNonStandardIsZeroCopyIfAvailable()) + .combine('videoName', kVideoNames) .combine('sourceType', ['VideoElement', 'VideoFrame'] as const) - .combineWithParams(kVideoExpectations) + .combine('dstColorSpace', kPredefinedColorSpace) ) .fn(async t => { - const sourceType = t.params.sourceType; + const { videoName, sourceType, dstColorSpace } = t.params; + if (sourceType === 'VideoFrame' && typeof VideoFrame === 'undefined') { t.skip('WebCodec is not supported'); } - const videoElement = getVideoElement(t, t.params.videoName); + const videoElement = getVideoElement(t, videoName); await startPlayingAndWaitForVideo(videoElement, async () => { const source = @@ -410,8 +509,8 @@ compute shader, for several combinations of video format and color space. ? await getVideoFrameFromVideoElement(t, videoElement) : videoElement; const externalTexture = t.device.importExternalTexture({ - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - source: source as any, + source, + colorSpace: dstColorSpace, }); if (t.params.checkNonStandardIsZeroCopy) { expectZeroCopyNonStandard(t, externalTexture); @@ -422,29 +521,51 @@ compute shader, for several combinations of video format and color space. usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING, }); + // Use display size of VideoFrame and video size of HTMLVideoElement as frame size. These sizes are presenting size which + // apply transformation in video metadata if any. + const pipeline = t.device.createComputePipeline({ layout: 'auto', compute: { - // Shader loads 4 pixels near each corner, and then store them in a storage texture. + // Shader loads 4 pixels, and then store them in a storage texture. module: t.device.createShaderModule({ code: ` + override frameWidth : i32 = 0; + override frameHeight : i32 = 0; @group(0) @binding(0) var t : texture_external; @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>; @compute @workgroup_size(1) fn main() { - var yellow : vec4<f32> = textureLoad(t, vec2<i32>(80, 60)); + let coordTopLeft = vec2<i32>(frameWidth / 4, frameHeight / 4); + let coordTopRight = vec2<i32>(frameWidth / 4 * 3, frameHeight / 4); + let coordBottomLeft = vec2<i32>(frameWidth / 4, frameHeight / 4 * 3); + let coordBottomRight = vec2<i32>(frameWidth / 4 * 3, frameHeight / 4 * 3); + var yellow : vec4<f32> = textureLoad(t, coordTopLeft); textureStore(outImage, vec2<i32>(0, 0), yellow); - var red : vec4<f32> = textureLoad(t, vec2<i32>(240, 60)); + var red : vec4<f32> = textureLoad(t, coordTopRight); textureStore(outImage, vec2<i32>(0, 1), red); - var blue : vec4<f32> = textureLoad(t, vec2<i32>(80, 180)); + var blue : vec4<f32> = textureLoad(t, coordBottomLeft); textureStore(outImage, vec2<i32>(1, 0), blue); - var green : vec4<f32> = textureLoad(t, vec2<i32>(240, 180)); + var green : vec4<f32> = textureLoad(t, coordBottomRight); textureStore(outImage, vec2<i32>(1, 1), green); return; } `, }), entryPoint: 'main', + + // Use display size of VideoFrame and video size of HTMLVideoElement as frame size. These sizes are presenting size which + // apply transformation in video metadata if any. + constants: { + frameWidth: + sourceType === 'VideoFrame' + ? (source as VideoFrame).displayWidth + : (source as HTMLVideoElement).videoWidth, + frameHeight: + sourceType === 'VideoFrame' + ? (source as VideoFrame).displayHeight + : (source as HTMLVideoElement).videoHeight, + }, }, }); @@ -464,17 +585,21 @@ compute shader, for several combinations of video format and color space. pass.end(); t.device.queue.submit([encoder.finish()]); + const srcColorSpace = kVideoInfo[videoName].colorSpace; + const presentColors = kVideoExpectedColors[srcColorSpace][dstColorSpace]; + + // visible rect is whole frame, no clipping. + const expect = kVideoInfo[videoName].display; + t.expectSinglePixelComparisonsAreOkInTexture({ texture: outputTexture }, [ - // Top-left should be yellow. - { coord: { x: 0, y: 0 }, exp: t.params._yellowExpectation }, - // Top-right should be red. - { coord: { x: 0, y: 1 }, exp: t.params._redExpectation }, - // Bottom-left should be blue. - { coord: { x: 1, y: 0 }, exp: t.params._blueExpectation }, - // Bottom-right should be green. - { coord: { x: 1, y: 1 }, exp: t.params._greenExpectation }, + // Top-left. + { coord: { x: 0, y: 0 }, exp: convertToUnorm8(presentColors[expect.topLeftColor]) }, + // Top-right. + { coord: { x: 0, y: 1 }, exp: convertToUnorm8(presentColors[expect.topRightColor]) }, + // Bottom-left. + { coord: { x: 1, y: 0 }, exp: convertToUnorm8(presentColors[expect.bottomLeftColor]) }, + // Bottom-right. + { coord: { x: 1, y: 1 }, exp: convertToUnorm8(presentColors[expect.bottomRightColor]) }, ]); - - if (sourceType === 'VideoFrame') (source as VideoFrame).close(); }); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html index c910c97b1d..a0068dbf7a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html @@ -14,6 +14,7 @@ <link rel="help" href="https://gpuweb.github.io/gpuweb/" /> <meta name="assert" content="WebGPU bgra8norm canvas with colorSpace set should be rendered correctly" /> <link rel="match" href="./ref/canvas_colorspace-ref.html" /> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <script type="module"> import { runColorSpaceTest } from './canvas_colorspace.html.js'; runColorSpaceTest('bgra8unorm'); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html index 7f57858e49..b38fef9591 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html @@ -14,7 +14,7 @@ <link rel="help" href="https://gpuweb.github.io/gpuweb/" /> <meta name="assert" content="WebGPU rgba16float canvas with colorSpace set should be rendered correctly" /> <link rel="match" href="./ref/canvas_colorspace-ref.html" /> - <meta name=fuzzy content="maxDifference=1;totalPixels=8192"> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <script type="module"> import { runColorSpaceTest } from './canvas_colorspace.html.js'; runColorSpaceTest('rgba16float'); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html index e57e04ef5c..404aed360c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html @@ -14,6 +14,7 @@ <link rel="help" href="https://gpuweb.github.io/gpuweb/" /> <meta name="assert" content="WebGPU rgba8unorm canvas with colorSpace set should be rendered correctly" /> <link rel="match" href="./ref/canvas_colorspace-ref.html" /> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <script type="module"> import { runColorSpaceTest } from './canvas_colorspace.html.js'; runColorSpaceTest('rgba8unorm'); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html index 70920dc0e6..5c3b888fee 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html @@ -8,7 +8,7 @@ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space" /> <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" /> - <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400"> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <style> body { background-color: #F0E68C; } #c-canvas { background-color: #8CF0E6; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html index d12751fac2..81335296c0 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html @@ -8,7 +8,7 @@ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space" /> <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" /> - <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400"> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <style> body { background-color: #F0E68C; } #c-canvas { background-color: #8CF0E6; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html index ed722013c1..28e2553fed 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html @@ -8,7 +8,7 @@ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space" /> <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" /> - <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400"> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <style> body { background-color: #F0E68C; } #c-canvas { background-color: #8CF0E6; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html index 8a028b168e..ca76fac7cd 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html @@ -8,7 +8,7 @@ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space" /> <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" /> - <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400"> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <style> body { background-color: #F0E68C; } #c-canvas { background-color: #8CF0E6; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html index fa938aba41..7936e0b81c 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html @@ -8,7 +8,7 @@ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space" /> <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" /> - <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400"> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <style> body { background-color: #F0E68C; } #c-canvas { background-color: #8CF0E6; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html index b62e71054c..da48abd2be 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html @@ -8,7 +8,7 @@ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space" /> <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" /> - <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400"> + <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999"> <style> body { background-color: #F0E68C; } #c-canvas { background-color: #8CF0E6; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html index f51145645b..6a64b3da5d 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html @@ -5,11 +5,11 @@ <link rel="help" href="https://gpuweb.github.io/gpuweb/" /> <meta name="assert" content="WebGPU canvas with image-rendering set should be rendered correctly" /> <link rel="match" href="./ref/canvas_image_rendering-ref.html" /> - <canvas id="elem1" width="64" height="64" style="width: 99px; height: 99px;"></canvas> - <canvas id="elem2" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;"></canvas> - <canvas id="elem3" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges"></canvas> - <canvas id="elem4" width="64" height="64" style="width: 99px; height: 99px;"></canvas> - <canvas id="elem5" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;"></canvas> - <canvas id="elem6" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges"></canvas> + <canvas id="elem1" width="64" height="64" style="width: 128px; height: 128px;"></canvas> + <canvas id="elem2" width="64" height="64" style="width: 128px; height: 128px; image-rendering: pixelated;"></canvas> + <canvas id="elem3" width="64" height="64" style="width: 128px; height: 128px; image-rendering: crisp-edges"></canvas> + <canvas id="elem4" width="64" height="64" style="width: 128px; height: 128px;"></canvas> + <canvas id="elem5" width="64" height="64" style="width: 128px; height: 128px; image-rendering: pixelated;"></canvas> + <canvas id="elem6" width="64" height="64" style="width: 128px; height: 128px; image-rendering: crisp-edges"></canvas> <script type="module" src="canvas_image_rendering.html.js"></script> </html> diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/delay_get_texture.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/delay_get_texture.html.ts new file mode 100644 index 0000000000..a73216a34e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/delay_get_texture.html.ts @@ -0,0 +1,46 @@ +import { timeout } from '../../../common/util/timeout.js'; +import { takeScreenshotDelayed } from '../../../common/util/wpt_reftest_wait.js'; + +function assert(condition: boolean, msg?: string | (() => string)): asserts condition { + if (!condition) { + throw new Error(msg && (typeof msg === 'string' ? msg : msg())); + } +} + +void (async () => { + assert( + typeof navigator !== 'undefined' && navigator.gpu !== undefined, + 'No WebGPU implementation found' + ); + + const adapter = await navigator.gpu.requestAdapter(); + assert(adapter !== null); + const device = await adapter.requestDevice(); + assert(device !== null); + + const canvas = document.getElementById('cvs0') as HTMLCanvasElement; + const ctx = canvas.getContext('webgpu') as unknown as GPUCanvasContext; + ctx.configure({ + device, + format: navigator.gpu.getPreferredCanvasFormat(), + alphaMode: 'premultiplied', + }); + + timeout(() => { + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: ctx.getCurrentTexture().createView(), + clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.end(); + device.queue.submit([encoder.finish()]); + + takeScreenshotDelayed(50); + }, 100); +})(); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/delay_get_texture.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/delay_get_texture.https.html new file mode 100644 index 0000000000..054c352ac2 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/delay_get_texture.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <title>WebGPU delay getCurrentTexture</title> + <meta charset="utf-8" /> + <link rel="help" href="https://gpuweb.github.io/gpuweb/" /> + <meta name="assert" content="WebGPU delay calling getCurrentTexture should be presented correctly" /> + <link rel="match" href="./ref/delay_get_texture-ref.html" /> + <canvas id="cvs0" width="20" height="20" style="width: 20px; height: 20px;"></canvas> + <script type="module" src="delay_get_texture.html.js"></script> +</html> diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html index f9eca704e8..56e3453c56 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html @@ -3,12 +3,12 @@ <title>WebGPU canvas_image_rendering (ref)</title> <meta charset="utf-8" /> <link rel="help" href="https://gpuweb.github.io/gpuweb/" /> - <img id="elem1" width="64" height="64" style="width: 99px; height: 99px;"> - <img id="elem2" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;"> - <img id="elem3" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges"> - <img id="elem4" width="64" height="64" style="width: 99px; height: 99px;"> - <img id="elem5" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;"> - <img id="elem6" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges"> + <img id="elem1" width="64" height="64" style="width: 128px; height: 128px;"> + <img id="elem2" width="64" height="64" style="width: 128px; height: 128px; image-rendering: pixelated;"> + <img id="elem3" width="64" height="64" style="width: 128px; height: 128px; image-rendering: crisp-edges"> + <img id="elem4" width="64" height="64" style="width: 128px; height: 128px;"> + <img id="elem5" width="64" height="64" style="width: 128px; height: 128px; image-rendering: pixelated;"> + <img id="elem6" width="64" height="64" style="width: 128px; height: 128px; image-rendering: crisp-edges"> <script type="module"> import { takeScreenshotDelayed } from '../../../../common/util/wpt_reftest_wait.js'; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/delay_get_texture-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/delay_get_texture-ref.html new file mode 100644 index 0000000000..fcf485dbe1 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/delay_get_texture-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <title>WebGPU delay getCurrentTexture (ref)</title> + <meta charset="utf-8" /> + <link rel="help" href="https://gpuweb.github.io/gpuweb/" /> + <canvas id="cvs0" width="20" height="20" style="width: 20px; height: 20px;"></canvas> + <script> + function draw(canvas) { + var c = document.getElementById(canvas); + var ctx = c.getContext('2d'); + ctx.fillStyle = '#00FF00'; + ctx.fillRect(0, 0, c.width, c.height); + } + + draw('cvs0'); + </script> +</html> diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts index 84ac6b31d1..56514b2203 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts @@ -1,9 +1,10 @@ import { Fixture, SkipTestCase } from '../../common/framework/fixture.js'; import { getResourcePath } from '../../common/framework/resources.js'; -import { makeTable } from '../../common/util/data_tables.js'; +import { keysOf } from '../../common/util/data_tables.js'; import { timeout } from '../../common/util/timeout.js'; import { ErrorWithExtra, raceWithRejectOnTimeout } from '../../common/util/util.js'; import { GPUTest } from '../gpu_test.js'; +import { RGBA, srgbToDisplayP3 } from '../util/color_space_conversion.js'; declare global { interface HTMLMediaElement { @@ -13,113 +14,342 @@ declare global { } } -export const kVideoInfo = - /* prettier-ignore */ makeTable( - ['mimeType' ] as const, - [undefined ] as const, { - // All video names - 'four-colors-vp8-bt601.webm': ['video/webm; codecs=vp8' ], - 'four-colors-theora-bt601.ogv': ['video/ogg; codecs=theora' ], - 'four-colors-h264-bt601.mp4': ['video/mp4; codecs=avc1.4d400c'], - 'four-colors-vp9-bt601.webm': ['video/webm; codecs=vp9' ], - 'four-colors-vp9-bt709.webm': ['video/webm; codecs=vp9' ], - 'four-colors-vp9-bt2020.webm': ['video/webm; codecs=vp9' ], - 'four-colors-h264-bt601-rotate-90.mp4': ['video/mp4; codecs=avc1.4d400c'], - 'four-colors-h264-bt601-rotate-180.mp4': ['video/mp4; codecs=avc1.4d400c'], - 'four-colors-h264-bt601-rotate-270.mp4': ['video/mp4; codecs=avc1.4d400c'], - } as const); -export type VideoName = keyof typeof kVideoInfo; +// MAINTENANCE_TODO: Uses raw floats as expectation in external_texture related cases has some diffs. +// Remove this conversion utils and uses raw float data as expectation in external_textrue +// related cases when resolve this. +export function convertToUnorm8(expectation: Readonly<RGBA>): Uint8Array { + const rgba8Unorm = new Uint8ClampedArray(4); + rgba8Unorm[0] = Math.round(expectation.R * 255.0); + rgba8Unorm[1] = Math.round(expectation.G * 255.0); + rgba8Unorm[2] = Math.round(expectation.B * 255.0); + rgba8Unorm[3] = Math.round(expectation.A * 255.0); + return new Uint8Array(rgba8Unorm.buffer); +} + +// MAINTENANCE_TODO: Add helper function for BT.601 and BT.709 to remove all magic numbers. // Expectation values about converting video contents to sRGB color space. // Source video color space affects expected values. // The process to calculate these expected pixel values can be found: // https://github.com/gpuweb/cts/pull/2242#issuecomment-1430382811 // and https://github.com/gpuweb/cts/pull/2242#issuecomment-1463273434 const kBt601PixelValue = { - red: new Float32Array([0.972945567233341, 0.141794376683341, -0.0209589916711088, 1.0]), - green: new Float32Array([0.248234279433399, 0.984810378661784, -0.0564701319494314, 1.0]), - blue: new Float32Array([0.10159735826538, 0.135451122863674, 1.00262982899724, 1.0]), - yellow: new Float32Array([0.995470750775951, 0.992742114518355, -0.0774291236205402, 1.0]), -}; - -function convertToUnorm8(expectation: Float32Array): Uint8Array { - const unorm8 = new Uint8ClampedArray(expectation.length); + srgb: { + red: { R: 0.972945567233341, G: 0.141794376683341, B: -0.0209589916711088, A: 1.0 }, + green: { R: 0.248234279433399, G: 0.984810378661784, B: -0.0564701319494314, A: 1.0 }, + blue: { R: 0.10159735826538, G: 0.135451122863674, B: 1.00262982899724, A: 1.0 }, + yellow: { R: 0.995470750775951, G: 0.992742114518355, B: -0.0701036235167653, A: 1.0 }, + }, +} as const; - for (let i = 0; i < expectation.length; ++i) { - unorm8[i] = Math.round(expectation[i] * 255.0); - } +const kBt709PixelValue = { + srgb: { + red: { R: 1.0, G: 0.0, B: 0.0, A: 1.0 }, + green: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, + blue: { R: 0.0, G: 0.0, B: 1.0, A: 1.0 }, + yellow: { R: 1.0, G: 1.0, B: 0.0, A: 1.0 }, + }, +} as const; - return new Uint8Array(unorm8.buffer); +function makeTable<Table extends { readonly [K: string]: {} }>({ + table, +}: { + table: Table; +}): { + readonly [F in keyof Table]: { + readonly [K in keyof Table[F]]: Table[F][K]; + }; +} { + return Object.fromEntries( + Object.entries(table).map(([k, row]) => [k, { ...row }]) + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + ) as any; } -// kVideoExpectations uses unorm8 results -const kBt601Red = convertToUnorm8(kBt601PixelValue.red); -const kBt601Green = convertToUnorm8(kBt601PixelValue.green); -const kBt601Blue = convertToUnorm8(kBt601PixelValue.blue); -const kBt601Yellow = convertToUnorm8(kBt601PixelValue.yellow); - -export const kVideoExpectations = [ - { - videoName: 'four-colors-vp8-bt601.webm', - _redExpectation: kBt601Red, - _greenExpectation: kBt601Green, - _blueExpectation: kBt601Blue, - _yellowExpectation: kBt601Yellow, - }, - { - videoName: 'four-colors-theora-bt601.ogv', - _redExpectation: kBt601Red, - _greenExpectation: kBt601Green, - _blueExpectation: kBt601Blue, - _yellowExpectation: kBt601Yellow, - }, - { - videoName: 'four-colors-h264-bt601.mp4', - _redExpectation: kBt601Red, - _greenExpectation: kBt601Green, - _blueExpectation: kBt601Blue, - _yellowExpectation: kBt601Yellow, +// Video expected pixel value table. Finding expected pixel value +// with video color space and dst color space. +export const kVideoExpectedColors = makeTable({ + table: { + bt601: { + 'display-p3': { + yellow: srgbToDisplayP3(kBt601PixelValue.srgb.yellow), + red: srgbToDisplayP3(kBt601PixelValue.srgb.red), + blue: srgbToDisplayP3(kBt601PixelValue.srgb.blue), + green: srgbToDisplayP3(kBt601PixelValue.srgb.green), + }, + srgb: { + yellow: kBt601PixelValue.srgb.yellow, + red: kBt601PixelValue.srgb.red, + blue: kBt601PixelValue.srgb.blue, + green: kBt601PixelValue.srgb.green, + }, + }, + bt709: { + 'display-p3': { + yellow: srgbToDisplayP3(kBt709PixelValue.srgb.yellow), + red: srgbToDisplayP3(kBt709PixelValue.srgb.red), + blue: srgbToDisplayP3(kBt709PixelValue.srgb.blue), + green: srgbToDisplayP3(kBt709PixelValue.srgb.green), + }, + srgb: { + yellow: kBt709PixelValue.srgb.yellow, + red: kBt709PixelValue.srgb.red, + blue: kBt709PixelValue.srgb.blue, + green: kBt709PixelValue.srgb.green, + }, + }, }, - { - videoName: 'four-colors-vp9-bt601.webm', - _redExpectation: kBt601Red, - _greenExpectation: kBt601Green, - _blueExpectation: kBt601Blue, - _yellowExpectation: kBt601Yellow, - }, - { - videoName: 'four-colors-vp9-bt709.webm', - _redExpectation: new Uint8Array([255, 0, 0, 255]), - _greenExpectation: new Uint8Array([0, 255, 0, 255]), - _blueExpectation: new Uint8Array([0, 0, 255, 255]), - _yellowExpectation: new Uint8Array([255, 255, 0, 255]), - }, -] as const; +} as const); -export const kVideoRotationExpectations = [ - { - videoName: 'four-colors-h264-bt601-rotate-90.mp4', - _topLeftExpectation: kBt601Red, - _topRightExpectation: kBt601Green, - _bottomLeftExpectation: kBt601Yellow, - _bottomRightExpectation: kBt601Blue, - }, - { - videoName: 'four-colors-h264-bt601-rotate-180.mp4', - _topLeftExpectation: kBt601Green, - _topRightExpectation: kBt601Blue, - _bottomLeftExpectation: kBt601Red, - _bottomRightExpectation: kBt601Yellow, - }, - { - videoName: 'four-colors-h264-bt601-rotate-270.mp4', - _topLeftExpectation: kBt601Blue, - _topRightExpectation: kBt601Yellow, - _bottomLeftExpectation: kBt601Green, - _bottomRightExpectation: kBt601Red, +// MAINTENANCE_TODO: Add BT.2020 video in table. +// Video container and codec defines several transform ops to apply to raw decoded frame to display. +// Our test cases covers 'visible rect' and 'rotation'. +// 'visible rect' is associated with the +// video bitstream and should apply to the raw decoded frames before any transformation. +// 'rotation' is associated with the track or presentation and should transform +// the whole visible rect (e.g. 90-degree rotate makes visible rect of vertical video to horizontal) +// The order to apply these transformations is below: + +// [raw decoded frame] ----visible rect clipping ---->[visible frame] ---rotation ---> present +// ^ ^ +// | | +// coded size display size +// The table holds test videos meta infos, including mimeType to check browser compatibility +// video color space, raw frame content layout and the frame displayed layout. +export const kVideoInfo = makeTable({ + table: { + 'four-colors-vp8-bt601.webm': { + mimeType: 'video/webm; codecs=vp8', + colorSpace: 'bt601', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-h264-bt601.mp4': { + mimeType: 'video/mp4; codecs=avc1.4d400c', + colorSpace: 'bt601', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-vp9-bt601.webm': { + mimeType: 'video/webm; codecs=vp9', + colorSpace: 'bt601', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-vp9-bt709.webm': { + mimeType: 'video/webm; codecs=vp9', + colorSpace: 'bt709', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + // video coded content has been rotate + 'four-colors-h264-bt601-rotate-90.mp4': { + mimeType: 'video/mp4; codecs=avc1.4d400c', + colorSpace: 'bt601', + coded: { + topLeftColor: 'red', + topRightColor: 'green', + bottomLeftColor: 'yellow', + bottomRightColor: 'blue', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-h264-bt601-rotate-180.mp4': { + mimeType: 'video/mp4; codecs=avc1.4d400c', + colorSpace: 'bt601', + coded: { + topLeftColor: 'green', + topRightColor: 'blue', + bottomLeftColor: 'red', + bottomRightColor: 'yellow', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-h264-bt601-rotate-270.mp4': { + mimeType: 'video/mp4; codecs=avc1.4d400c', + colorSpace: 'bt601', + coded: { + topLeftColor: 'blue', + topRightColor: 'yellow', + bottomLeftColor: 'green', + bottomRightColor: 'red', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-vp9-bt601-rotate-90.mp4': { + mimeType: 'video/mp4; codecs=vp09.00.10.08', + colorSpace: 'bt601', + coded: { + topLeftColor: 'red', + topRightColor: 'green', + bottomLeftColor: 'yellow', + bottomRightColor: 'blue', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-vp9-bt601-rotate-180.mp4': { + mimeType: 'video/mp4; codecs=vp09.00.10.08', + colorSpace: 'bt601', + coded: { + topLeftColor: 'green', + topRightColor: 'blue', + bottomLeftColor: 'red', + bottomRightColor: 'yellow', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-vp9-bt601-rotate-270.mp4': { + mimeType: 'video/mp4; codecs=vp09.00.10.08', + colorSpace: 'bt601', + coded: { + topLeftColor: 'blue', + topRightColor: 'yellow', + bottomLeftColor: 'green', + bottomRightColor: 'red', + }, + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + }, + 'four-colors-h264-bt601-hflip.mp4': { + mimeType: 'video/mp4; codecs=avc1.4d400c', + colorSpace: 'bt601', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'red', + topRightColor: 'yellow', + bottomLeftColor: 'green', + bottomRightColor: 'blue', + }, + }, + 'four-colors-h264-bt601-vflip.mp4': { + mimeType: 'video/mp4; codecs=avc1.4d400c', + colorSpace: 'bt601', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'blue', + topRightColor: 'green', + bottomLeftColor: 'yellow', + bottomRightColor: 'red', + }, + }, + 'four-colors-vp9-bt601-hflip.mp4': { + mimeType: 'video/mp4; codecs=vp09.00.10.08', + colorSpace: 'bt601', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'red', + topRightColor: 'yellow', + bottomLeftColor: 'green', + bottomRightColor: 'blue', + }, + }, + 'four-colors-vp9-bt601-vflip.mp4': { + mimeType: 'video/mp4; codecs=vp09.00.10.08', + colorSpace: 'bt601', + coded: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, + display: { + topLeftColor: 'blue', + topRightColor: 'green', + bottomLeftColor: 'yellow', + bottomRightColor: 'red', + }, + }, }, -] as const; +} as const); +type VideoName = keyof typeof kVideoInfo; +export const kVideoNames: readonly VideoName[] = keysOf(kVideoInfo); + +export const kPredefinedColorSpace = ['display-p3', 'srgb'] as const; /** * Starts playing a video and waits for it to be consumable. * Returns a promise which resolves after `callback` (which may be async) completes. @@ -153,7 +383,12 @@ export function startPlayingAndWaitForVideo( video.addEventListener( 'error', - event => reject(new ErrorWithExtra('Video received "error" event', () => ({ event }))), + event => + reject( + new ErrorWithExtra('Video received "error" event, message: ' + event.message, () => ({ + event, + })) + ), true ); @@ -238,6 +473,7 @@ export async function getVideoFrameFromVideoElement( const transformer: TransformStream = new TransformStream({ transform(videoFrame, _controller) { videoTrack.stop(); + test.trackForCleanup(videoFrame); resolve(videoFrame); }, flush(controller) { @@ -267,6 +503,10 @@ export async function getVideoFrameFromVideoElement( * */ export function getVideoElement(t: GPUTest, videoName: VideoName): HTMLVideoElement { + if (typeof HTMLVideoElement === 'undefined') { + t.skip('HTMLVideoElement not available'); + } + const videoElement = document.createElement('video'); const videoInfo = kVideoInfo[videoName]; @@ -277,6 +517,8 @@ export function getVideoElement(t: GPUTest, videoName: VideoName): HTMLVideoElem const videoUrl = getResourcePath(videoName); videoElement.src = videoUrl; + t.trackForCleanup(videoElement); + return videoElement; } diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts index 67f9f693be..14fe135633 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts @@ -1,9 +1,13 @@ export const description = ` -Tests WebGPU is available in a worker. +Tests WebGPU is available in a dedicated worker and a shared worker. -Note: The CTS test can be run in a worker by passing in worker=1 as -a query parameter. This test is specifically to check that WebGPU -is available in a worker. +Note: Any CTS test can be run in a worker by passing ?worker=dedicated, ?worker=shared, +?worker=service as a query parameter. The tests in this file are specifically to check +that WebGPU is available in each worker type. When run in combination with a ?worker flag, +they will test workers created from other workers (where APIs exist to do so). + +TODO[2]: Figure out how to make these tests run in service workers (not actually +important unless service workers gain the ability to launch other workers). `; import { Fixture } from '../../../common/framework/fixture.js'; @@ -12,24 +16,52 @@ import { assert } from '../../../common/util/util.js'; export const g = makeTestGroup(Fixture); -function isNode(): boolean { - return typeof process !== 'undefined' && process?.versions?.node !== undefined; -} +const isNode = typeof process !== 'undefined' && process?.versions?.node !== undefined; + +// [1]: we load worker_launcher dynamically because ts-node support +// is using commonjs which doesn't support import.meta. Further, +// we need to put the url in a string and pass the string to import +// otherwise typescript tries to parse the file which again, fails. +// worker_launcher.js is excluded in node.tsconfig.json. + +// [2]: That hack does not work in Service Workers. +const isServiceWorker = globalThis.constructor.name === 'ServiceWorkerGlobalScope'; -g.test('worker') - .desc(`test WebGPU is available in DedicatedWorkers and check for basic functionality`) +g.test('dedicated_worker') + .desc(`test WebGPU is available in dedicated workers and check for basic functionality`) .fn(async t => { - if (isNode()) { - t.skip('node does not support 100% compatible workers'); - return; - } - // Note: we load worker_launcher dynamically because ts-node support - // is using commonjs which doesn't support import.meta. Further, - // we need to put the url in a string add pass the string to import - // otherwise typescript tries to parse the file which again, fails. - // worker_launcher.js is excluded in node.tsconfig.json. + t.skipIf(isNode, 'node does not support 100% compatible workers'); + + t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2] const url = './worker_launcher.js'; - const { launchWorker } = await import(url); - const result = await launchWorker(); + const { launchDedicatedWorker } = await import(url); // [1] + + const result = await launchDedicatedWorker(); + assert(result.error === undefined, `should be no error from worker but was: ${result.error}`); + }); + +g.test('shared_worker') + .desc(`test WebGPU is available in shared workers and check for basic functionality`) + .fn(async t => { + t.skipIf(isNode, 'node does not support 100% compatible workers'); + + t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2] + const url = './worker_launcher.js'; + const { launchSharedWorker } = await import(url); // [1] + + const result = await launchSharedWorker(); + assert(result.error === undefined, `should be no error from worker but was: ${result.error}`); + }); + +g.test('service_worker') + .desc(`test WebGPU is available in service workers and check for basic functionality`) + .fn(async t => { + t.skipIf(isNode, 'node does not support 100% compatible workers'); + + t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2] + const url = './worker_launcher.js'; + const { launchServiceWorker } = await import(url); // [1] + + const result = await launchServiceWorker(); assert(result.error === undefined, `should be no error from worker but was: ${result.error}`); }); diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts index a3cf8064e2..033473d63a 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts @@ -1,6 +1,10 @@ import { getGPU, setDefaultRequestAdapterOptions } from '../../../common/util/navigator_gpu.js'; import { assert, objectEquals, iterRange } from '../../../common/util/util.js'; +// Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom". +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +declare const self: any; + async function basicTest() { const adapter = await getGPU(null).requestAdapter(); assert(adapter !== null, 'Failed to get adapter.'); @@ -68,7 +72,7 @@ async function basicTest() { device.destroy(); } -self.onmessage = async (ev: MessageEvent) => { +async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) { const defaultRequestAdapterOptions: GPURequestAdapterOptions = ev.data.defaultRequestAdapterOptions; setDefaultRequestAdapterOptions(defaultRequestAdapterOptions); @@ -79,5 +83,17 @@ self.onmessage = async (ev: MessageEvent) => { } catch (err: unknown) { error = (err as Error).toString(); } - self.postMessage({ error }); + this.postMessage({ error }); +} + +self.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(ev.source || self, ev); +}; + +self.onconnect = (event: MessageEvent) => { + const port = event.ports[0]; + + port.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(port, ev); + }; }; diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts index 72059eb99f..4b1d31ae49 100644 --- a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts @@ -1,10 +1,15 @@ +import { SkipTestCase } from '../../../common/framework/fixture.js'; import { getDefaultRequestAdapterOptions } from '../../../common/util/navigator_gpu.js'; export type TestResult = { error: String | undefined; }; -export async function launchWorker() { +export async function launchDedicatedWorker() { + if (typeof Worker === 'undefined') { + throw new SkipTestCase(`Worker undefined in context ${globalThis.constructor.name}`); + } + const selfPath = import.meta.url; const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); const workerPath = selfPathDir + '/worker.js'; @@ -16,3 +21,55 @@ export async function launchWorker() { worker.postMessage({ defaultRequestAdapterOptions: getDefaultRequestAdapterOptions() }); return await promise; } + +export async function launchSharedWorker() { + if (typeof SharedWorker === 'undefined') { + throw new SkipTestCase(`SharedWorker undefined in context ${globalThis.constructor.name}`); + } + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + const workerPath = selfPathDir + '/worker.js'; + const worker = new SharedWorker(workerPath, { type: 'module' }); + + const port = worker.port; + const promise = new Promise<TestResult>(resolve => { + port.addEventListener('message', ev => resolve(ev.data as TestResult), { once: true }); + }); + port.start(); + port.postMessage({ + defaultRequestAdapterOptions: getDefaultRequestAdapterOptions(), + }); + return await promise; +} + +export async function launchServiceWorker() { + if (typeof navigator === 'undefined' || typeof navigator.serviceWorker === 'undefined') { + throw new SkipTestCase( + `navigator.serviceWorker undefined in context ${globalThis.constructor.name}` + ); + } + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + const serviceWorkerPath = selfPathDir + '/worker.js'; + const registration = await navigator.serviceWorker.register(serviceWorkerPath, { + type: 'module', + }); + await registration.update(); + + const promise = new Promise<TestResult>(resolve => { + navigator.serviceWorker.addEventListener( + 'message', + ev => { + resolve(ev.data as TestResult); + void registration.unregister(); + }, + { once: true } + ); + }); + registration.active?.postMessage({ + defaultRequestAdapterOptions: getDefaultRequestAdapterOptions(), + }); + return await promise; +} |