Skip to content

Commit

Permalink
Add "enforceDefaultLimits" flag (#4124)
Browse files Browse the repository at this point in the history
I'm not sure what to do about this but dealing with dependent
limits is kind of a PITA.

Tests that deal with storage buffers and storage textures need to
take into account that they might have 0. But, they also need to
be tested with the maximum number of storage buffers/textures.

So, if you set the test to use max storage buffers/textures then,
unless you have a device that supports 0 you have no easy way to
test that the test functions when the limit is 0.

While refactoring the tests I start without requesting the limit
so on compat I get zero. Once that works I add `MaxLimitsTestMixin`
or similar to request the maximum limits, otherwise a compat device
would get no coverage.

But, now I have the issue that if I'm modifying the test I need to
remember to test with 0 so I have to go manually comment out the code
that's requesting max limits.

So, I thought I'd add this option.
  • Loading branch information
greggman authored Jan 7, 2025
1 parent 10b66cd commit 63658e1
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 110 deletions.
6 changes: 6 additions & 0 deletions src/common/framework/test_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export type TestConfig = {
*/
forceFallbackAdapter: boolean;

/**
* Enforce the default limits on the adapter
*/
enforceDefaultLimits: boolean;

/**
* Whether to enable the `logToWebSocket` function used for out-of-band test logging.
*/
Expand All @@ -59,5 +64,6 @@ export const globalTestConfig: TestConfig = {
unrollConstEvalLoops: false,
compatibility: false,
forceFallbackAdapter: false,
enforceDefaultLimits: false,
logToWebSocket: false,
};
2 changes: 2 additions & 0 deletions src/common/runtime/cmdline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ for (let i = 0; i < sys.args.length; ++i) {
globalTestConfig.compatibility = true;
} else if (a === '--force-fallback-adapter') {
globalTestConfig.forceFallbackAdapter = true;
} else if (a === '--enforce-default-limits') {
globalTestConfig.enforceDefaultLimits = true;
} else if (a === '--log-to-websocket') {
globalTestConfig.logToWebSocket = true;
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/common/runtime/helper/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface CTSOptions {
debug: boolean;
compatibility: boolean;
forceFallbackAdapter: boolean;
enforceDefaultLimits: boolean;
unrollConstEvalLoops: boolean;
powerPreference: GPUPowerPreference | null;
logToWebSocket: boolean;
Expand All @@ -63,6 +64,7 @@ export const kDefaultCTSOptions: CTSOptions = {
debug: true,
compatibility: false,
forceFallbackAdapter: false,
enforceDefaultLimits: false,
unrollConstEvalLoops: false,
powerPreference: null,
logToWebSocket: false,
Expand Down Expand Up @@ -100,6 +102,10 @@ export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = {
debug: { description: 'show more info' },
compatibility: { description: 'run in compatibility mode' },
forceFallbackAdapter: { description: 'pass forceFallbackAdapter: true to requestAdapter' },
enforceDefaultLimits: {
description: `force the adapter limits to the default limits.
Note: May fail on tests for low-power/high-performance`,
},
unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' },
powerPreference: {
description: 'set default powerPreference for some tests',
Expand Down
1 change: 1 addition & 0 deletions src/common/runtime/helper/utils_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function setupWorkerEnvironment(ctsOptions: CTSOptions): Logger {
globalTestConfig.enableDebugLogs = ctsOptions.debug;
globalTestConfig.unrollConstEvalLoops = ctsOptions.unrollConstEvalLoops;
globalTestConfig.compatibility = compatibility;
globalTestConfig.enforceDefaultLimits = ctsOptions.enforceDefaultLimits;
globalTestConfig.logToWebSocket = ctsOptions.logToWebSocket;

const log = new Logger();
Expand Down
2 changes: 2 additions & 0 deletions src/common/runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ for (let i = 0; i < sys.args.length; ++i) {
emitCoverage = true;
} else if (a === '--force-fallback-adapter') {
globalTestConfig.forceFallbackAdapter = true;
} else if (a === '--enforce-default-limits') {
globalTestConfig.enforceDefaultLimits = true;
} else if (a === '--log-to-websocket') {
globalTestConfig.logToWebSocket = true;
} else if (a === '--gpu-provider') {
Expand Down
1 change: 1 addition & 0 deletions src/common/runtime/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const { runnow, powerPreference, compatibility, forceFallbackAdapter } = options
globalTestConfig.enableDebugLogs = options.debug;
globalTestConfig.unrollConstEvalLoops = options.unrollConstEvalLoops;
globalTestConfig.compatibility = compatibility;
globalTestConfig.enforceDefaultLimits = options.enforceDefaultLimits;
globalTestConfig.logToWebSocket = options.logToWebSocket;

const logger = new Logger();
Expand Down
84 changes: 84 additions & 0 deletions src/common/util/navigator_gpu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// eslint-disable-next-line import/no-restricted-paths
import { TestCaseRecorder } from '../framework/fixture.js';
import { globalTestConfig } from '../framework/test_config.js';

import { ErrorWithExtra, assert, objectEquals } from './util.js';

Expand Down Expand Up @@ -31,6 +33,7 @@ export function setGPUProvider(provider: GPUProvider) {
}

let impl: GPU | undefined = undefined;
let s_defaultLimits: Record<string, GPUSize64> | undefined = undefined;

let defaultRequestAdapterOptions: GPURequestAdapterOptions | undefined;

Expand All @@ -49,6 +52,14 @@ export function getDefaultRequestAdapterOptions() {
return defaultRequestAdapterOptions;
}

function copyLimits(objLike: GPUSupportedLimits) {
const obj: Record<string, number> = {};
for (const key in objLike) {
obj[key] = (objLike as unknown as Record<string, number>)[key];
}
return obj;
}

/**
* Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
* Throws an exception if not found.
Expand All @@ -60,6 +71,79 @@ export function getGPU(recorder: TestCaseRecorder | null): GPU {

impl = gpuProvider();

if (globalTestConfig.enforceDefaultLimits) {
// eslint-disable-next-line @typescript-eslint/unbound-method
const origRequestAdapterFn = impl.requestAdapter;
// eslint-disable-next-line @typescript-eslint/unbound-method
const origRequestDeviceFn = GPUAdapter.prototype.requestDevice;

impl.requestAdapter = async function (options?: GPURequestAdapterOptions) {
if (!s_defaultLimits) {
const tempAdapter = await origRequestAdapterFn.call(this, {
...defaultRequestAdapterOptions,
...options,
});
// eslint-disable-next-line no-restricted-syntax
const tempDevice = await tempAdapter?.requestDevice();
s_defaultLimits = copyLimits(tempDevice!.limits);
tempDevice?.destroy();
}
const adapter = await origRequestAdapterFn.call(this, {
...defaultRequestAdapterOptions,
...options,
});
if (adapter) {
const limits = Object.fromEntries(
Object.entries(s_defaultLimits).map(([key, v]) => [key, v])
);

Object.defineProperty(adapter, 'limits', {
get() {
return limits;
},
});
}
return adapter;
};

const enforceDefaultLimits = (adapter: GPUAdapter, desc: GPUDeviceDescriptor | undefined) => {
if (desc?.requiredLimits) {
for (const [key, value] of Object.entries(desc.requiredLimits)) {
const limit = s_defaultLimits![key];
if (limit !== undefined && value !== undefined) {
const [beyondLimit, condition] = key.startsWith('max')
? [value > limit, 'greater']
: [value < limit, 'less'];
if (beyondLimit) {
throw new DOMException(
`requestedLimit ${value} for ${key} is ${condition} than adapter limit ${limit}`,
'OperationError'
);
}
}
}
}
};

GPUAdapter.prototype.requestDevice = async function (
this: GPUAdapter,
desc?: GPUDeviceDescriptor | undefined
) {
// We need to enforce the default limits because even though we patched the adapter to
// show defaults for adapter.limits, there are tests that test we throw when we request more than the max.
// In other words.
//
// adapter.requestDevice({ requiredLimits: {
// maxXXX: addapter.limits.maxXXX + 1, // should throw
// });
//
// But unless we enforce this manually, it won't actual through if the adapter's
// true limits are higher than we patched above.
enforceDefaultLimits(this, desc);
return await origRequestDeviceFn.call(this, desc);
};
}

if (defaultRequestAdapterOptions) {
// eslint-disable-next-line @typescript-eslint/unbound-method
const oldFn = impl.requestAdapter;
Expand Down
Loading

0 comments on commit 63658e1

Please sign in to comment.