Skip to content

Commit

Permalink
Use a temp device to get the default limits.
Browse files Browse the repository at this point in the history
Originally I was using the limits in capability_info.ts
but passing unknown limits, like maxStorageBuffersInFragmentStage,
to an adapter that doesn't know that limit ends up causing an error.

So, instead, create temp adapter and from that a temp device
with no limits requested to query the limits. We can't query
until the first request since getGPU is not async.
  • Loading branch information
greggman committed Jan 7, 2025
1 parent b7299d5 commit 6d4583b
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 129 deletions.
5 changes: 4 additions & 1 deletion src/common/runtime/helper/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +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' },
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
63 changes: 45 additions & 18 deletions src/common/util/navigator_gpu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// eslint-disable-next-line import/no-restricted-paths
import { getDefaultLimitsForAdapter } from '../../webgpu/capability_info.js';
import { TestCaseRecorder } from '../framework/fixture.js';
import { globalTestConfig } from '../framework/test_config.js';

Expand Down Expand Up @@ -34,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 @@ -52,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 @@ -65,15 +73,28 @@ export function getGPU(recorder: TestCaseRecorder | null): GPU {

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

impl.requestAdapter = async function (options?: GPURequestAdapterOptions) {
const adapter = await oldFn.call(this, { ...defaultRequestAdapterOptions, ...options });
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(getDefaultLimitsForAdapter(adapter)).map(([key, { default: v }]) => [
key,
v,
])
Object.entries(s_defaultLimits).map(([key, v]) => [key, v])
);

Object.defineProperty(adapter, 'limits', {
Expand All @@ -87,17 +108,15 @@ export function getGPU(recorder: TestCaseRecorder | null): GPU {

const enforceDefaultLimits = (adapter: GPUAdapter, desc: GPUDeviceDescriptor | undefined) => {
if (desc?.requiredLimits) {
const limits = getDefaultLimitsForAdapter(adapter);
for (const [key, value] of Object.entries(desc.requiredLimits)) {
const info = limits[key as keyof ReturnType<typeof getDefaultLimitsForAdapter>];
if (info && value !== undefined) {
const [beyondLimit, condition] =
info.class === 'maximum'
? [value > info.default, 'greater']
: [value < info.default, 'less'];
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 ${info.default}`,
`requestedLimit ${value} for ${key} is ${condition} than adapter limit ${limit}`,
'OperationError'
);
}
Expand All @@ -106,14 +125,22 @@ export function getGPU(recorder: TestCaseRecorder | null): GPU {
}
};

// eslint-disable-next-line @typescript-eslint/unbound-method
const origFn = GPUAdapter.prototype.requestDevice;
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 origFn.call(this, desc);
return await origRequestDeviceFn.call(this, desc);
};
}

Expand Down
220 changes: 110 additions & 110 deletions src/resources/cache/hashes.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6d4583b

Please sign in to comment.