Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ const gpuTier = await getGPUTier();

Based on the reported `fps` the GPU is then classified into either `tier: 1 (>= 15 fps)`, `tier: 2 (>= 30 fps)` or `tier: 3 (>= 60 fps)`. The higher the tier the more graphically intensive workload you can offer to the user.

## Result types

`getGPUTier()` returns a `type` field indicating how the result was produced:

| `type` | Meaning |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `BENCHMARK` | Matched a benchmark entry; `fps` reflects the measured framerate for that GPU. |
| `FALLBACK` | Renderer recognised but no benchmark match found. `tier` is a conservative default. |
| `BENCHMARK_FETCH_FAILED` | Benchmark fetch failed (CDN outage, strict CSP, offline, etc.). Safe to retry. |
| `BLOCKLISTED` | Renderer is on a known-bad list (drivers with severe issues). `tier` is always 0. |
| `WEBGL_UNSUPPORTED` | No WebGL context could be created. `tier` is always 0. |
| `SSR` | Running server-side — no `window`, detection skipped. |

## API

```ts
Expand Down
21 changes: 20 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export type TierType =
| 'WEBGL_UNSUPPORTED'
| 'BLOCKLISTED'
| 'FALLBACK'
| 'BENCHMARK';
| 'BENCHMARK'
| 'BENCHMARK_FETCH_FAILED';

export type TierResult = {
tier: number;
Expand All @@ -103,6 +104,11 @@ export const getGPUTier = async ({
benchmarksURL = `https://unpkg.com/@pmndrs/detect-gpu@${version}/dist/benchmarks`,
}: GetGPUTier = {}): Promise<TierResult> => {
const queryCache: { [k: string]: Promise<ModelEntry[]> } = {};
// Set when any loadBenchmarks() call rejects with a non-OutdatedBenchmarksError
// (e.g. network failure, CORS, CSP blocking unpkg). Consulted only when the
// benchmark result list is empty, so a successful renderer match trumps a
// failed sibling fetch on a different benchmark file.
let benchmarkFetchFailed = false;
if (isSSR) {
return {
tier: 0,
Expand Down Expand Up @@ -178,6 +184,7 @@ export const getGPUTier = async ({
if (error instanceof OutdatedBenchmarksError) {
throw error;
}
benchmarkFetchFailed = true;
debug?.("queryBenchmarks - couldn't load benchmark:", { error });
return;
}
Expand Down Expand Up @@ -318,6 +325,18 @@ export const getGPUTier = async ({
);
if (blocklistedModel) return toResult(0, 'BLOCKLISTED', blocklistedModel);

// Distinguish "couldn't reach the benchmark CDN" from "GPU is genuinely
// unknown". Silent tier-1 FALLBACK misrepresents fast hardware as slow
// whenever a network/CSP/CORS issue blocks the benchmarks fetch; this
// branch lets consumers detect the condition and retry.
if (benchmarkFetchFailed) {
return toResult(
1,
'BENCHMARK_FETCH_FAILED',
`${renderer} (${rawRenderer})`
);
}

// Apple Silicon on desktop Safari: the renderer string is the generic
// "Apple GPU" and Safari reports identical WebGL capabilities across
// M1–M5 (verified empirically on M1 Max / M2 / M4), so no web-facing
Expand Down
21 changes: 19 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ for (const renderers of [RENDERER_MOBILE, RENDERER_TABLET, RENDERER_DESKTOP]) {
'BLOCKLISTED',
'FALLBACK',
'BENCHMARK',
'BENCHMARK_FETCH_FAILED',
]).toContain(type);
});
}
Expand Down Expand Up @@ -339,6 +340,22 @@ test('Apple Silicon desktop Safari — conservative tier-3 FALLBACK', async () =
expect(result.fps).toBe(60);
});

test('benchmark fetch failure surfaces as BENCHMARK_FETCH_FAILED, not silent FALLBACK', async () => {
// Silent degradation to tier-1 FALLBACK misrepresents fast hardware as
// slow whenever the benchmark CDN is blocked (CSP, CORS, unpkg outage).
// Consumers need a way to detect the condition so they can retry.
const result = await getTier({
isMobile: false,
renderer:
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Laptop GPU (0x00002520) Direct3D11 vs_5_0 ps_5_0, D3D11)',
loadBenchmarks: async () => {
throw new Error('simulated network failure');
},
});
expect(result.type).toBe('BENCHMARK_FETCH_FAILED');
expect(result.tier).toBe(1);
});

test('Apple GPU on mobile does NOT take the desktop tier-3 path', async () => {
// iPhone/iPad route through deobfuscateAppleGPU and resolve to specific
// chip benchmarks. The desktop tier-3 fallback must not fire on mobile,
Expand Down Expand Up @@ -417,11 +434,11 @@ test('Apple GPU on mobile does NOT take the desktop tier-3 path', async () => {
});
});

test(`When queryBenchmarks throws, FALLBACK is returned`, async () => {
test(`When queryBenchmarks throws, BENCHMARK_FETCH_FAILED is returned`, async () => {
expectGPUResults(
{
tier: 1,
type: 'FALLBACK',
type: 'BENCHMARK_FETCH_FAILED',
},
await getTier({
loadBenchmarks: async (): Promise<ModelEntry[]> => {
Expand Down