From e5928582ebbb84ffa6bf2f255a59c3dcb68cec6c Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Thu, 31 Jul 2025 08:13:23 -0700 Subject: [PATCH] Add support for apple silicon gpus Current version fallsback for all apple silicon gpus. { "gpu": "apple gpu (Apple GPU)", "isMobile": false, "tier": 1, "type": "FALLBACK" } This change will support up to m4 chips { "fps": 120, "gpu": "apple m4 max", "isMobile": false, "tier": 3, "type": "BENCHMARK" } Also includes a new html page that outputs all the available WebGL info WebGL Information: { "vendor": "WebKit", "renderer": "WebKit WebGL", "webglVersion": "WebGL 1.0", "unmaskedVendor": "Apple Inc.", "unmaskedRenderer": "Apple GPU", "capabilities": { "maxTextureSize": 16384, "maxVertexUniformVectors": 1024, "maxFragmentUniformVectors": 1024, "maxVaryingVectors": 30, "maxRenderbufferSize": 16384 } } --- src/internal/deobfuscateAppleGPU.ts | 48 +++++- src/internal/getAppleGPUFromCapabilities.ts | 61 ++++++++ test-browser.html | 164 ++++++++++++++++++++ test/desktop-safari.test.ts | 67 ++++++++ 4 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 src/internal/getAppleGPUFromCapabilities.ts create mode 100644 test-browser.html create mode 100644 test/desktop-safari.test.ts diff --git a/src/internal/deobfuscateAppleGPU.ts b/src/internal/deobfuscateAppleGPU.ts index 9c06c3f..c3e3442 100644 --- a/src/internal/deobfuscateAppleGPU.ts +++ b/src/internal/deobfuscateAppleGPU.ts @@ -13,6 +13,7 @@ import { // Internal import { deviceInfo } from './deviceInfo'; +import { getAppleGPUFromCapabilities } from './getAppleGPUFromCapabilities'; const debug = false ? console.warn : undefined; @@ -21,22 +22,41 @@ export function deobfuscateAppleGPU( renderer: string, isMobileTier: boolean ) { - if (!isMobileTier) { - debug?.('Safari 14+ obfuscates its GPU type and version, using fallback'); - return [renderer]; - } const pixelId = calculateMagicPixelId(gl); const codeA = '801621810' as const; const codeB = '8016218135' as const; const codeC = '80162181161' as const; const codeFB = '80162181255'; + const codeM = '80162181255' as const; // Observed on Apple Silicon Macs + + // Desktop Apple Silicon chipsets + const desktopChipsets: [ + string, + typeof codeA | typeof codeB | typeof codeC | typeof codeM, + number, + ][] = [ + ['m1', codeM, 11], // Released with macOS 11 Big Sur + ['m1 pro', codeM, 12], // Released with macOS 12 Monterey + ['m1 max', codeM, 12], // Released with macOS 12 Monterey + ['m1 ultra', codeM, 12], // Released with macOS 12 Monterey + ['m2', codeM, 12], // Released with macOS 12 Monterey + ['m2 pro', codeM, 13], // Released with macOS 13 Ventura + ['m2 max', codeM, 13], // Released with macOS 13 Ventura + ['m2 ultra', codeM, 13], // Released with macOS 13 Ventura + ['m3', codeM, 14], // Released with macOS 14 Sonoma + ['m3 pro', codeM, 14], // Released with macOS 14 Sonoma + ['m3 max', codeM, 14], // Released with macOS 14 Sonoma + ['m4', codeM, 14], // First on iPad, then Mac with macOS 14 + ['m4 pro', codeM, 15], // Released with macOS 15 Sequoia + ['m4 max', codeM, 15], // Released with macOS 15 Sequoia + ]; // All chipsets that support at least iOS 12: const possibleChipsets: [ string, - typeof codeA | typeof codeB | typeof codeC, + typeof codeA | typeof codeB | typeof codeC | typeof codeM, number, - ][] = deviceInfo?.isIpad + ][] = !isMobileTier ? desktopChipsets : deviceInfo?.isIpad ? [ // ['a4', 5], // ipad 1st gen // ['a5', 9], // ipad 2 / ipad mini 1st gen @@ -86,9 +106,21 @@ export function deobfuscateAppleGPU( chipsets = possibleChipsets; } } - const renderers = chipsets.map(([gpu]) => `apple ${gpu} gpu`); + + // For desktop, if we only have generic matches, use capability-based detection + if (!isMobileTier && chipsets.length === desktopChipsets.length) { + const capabilityBasedGPUs = getAppleGPUFromCapabilities(gl); + debug?.( + `Using capability-based detection for desktop Safari, possible GPUs: ${JSON.stringify( + capabilityBasedGPUs + )}` + ); + return capabilityBasedGPUs; + } + + const renderers = chipsets.map(([gpu]) => !isMobileTier ? `apple ${gpu}` : `apple ${gpu} gpu`); debug?.( - `iOS 12.2+ obfuscates its GPU type and version, using closest matches: ${JSON.stringify( + `${isMobileTier ? 'iOS 12.2+' : 'Safari 14+'} obfuscates its GPU type and version, using closest matches: ${JSON.stringify( renderers )}` ); diff --git a/src/internal/getAppleGPUFromCapabilities.ts b/src/internal/getAppleGPUFromCapabilities.ts new file mode 100644 index 0000000..bc5da2e --- /dev/null +++ b/src/internal/getAppleGPUFromCapabilities.ts @@ -0,0 +1,61 @@ +export function getAppleGPUFromCapabilities( + gl: WebGLRenderingContext | WebGL2RenderingContext +): string[] { + // Get various WebGL capabilities that differ between Apple Silicon generations + const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); + const maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS); + const maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS); + const maxVaryingVectors = gl.getParameter(gl.MAX_VARYING_VECTORS); + const maxVertexTextureImageUnits = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); + const maxCombinedTextureImageUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); + const maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + + // WebGL2 specific capabilities + let maxDrawBuffers = 4; + let maxColorAttachments = 4; + let max3DTextureSize = 0; + let maxArrayTextureLayers = 0; + + if ((gl as WebGL2RenderingContext).MAX_DRAW_BUFFERS) { + const gl2 = gl as WebGL2RenderingContext; + maxDrawBuffers = gl2.getParameter(gl2.MAX_DRAW_BUFFERS); + maxColorAttachments = gl2.getParameter(gl2.MAX_COLOR_ATTACHMENTS); + max3DTextureSize = gl2.getParameter(gl2.MAX_3D_TEXTURE_SIZE); + maxArrayTextureLayers = gl2.getParameter(gl2.MAX_ARRAY_TEXTURE_LAYERS); + } + + // Calculate a capability score + const score = + (maxTextureSize / 4096) + + (maxVertexUniformVectors / 256) + + (maxFragmentUniformVectors / 256) + + (maxVaryingVectors / 16) + + (maxVertexTextureImageUnits / 16) + + (maxCombinedTextureImageUnits / 32) + + (maxRenderbufferSize / 8192) + + (maxDrawBuffers / 4) + + (maxColorAttachments / 4) + + (max3DTextureSize / 2048) + + (maxArrayTextureLayers / 2048); + + // Estimate GPU based on capability score + // These thresholds are approximate and may need tuning + if (score >= 15) { + // High-end M-series (M4 Pro/Max, M3 Pro/Max, M2 Ultra, M1 Ultra) + return ['apple m4 pro', 'apple m4 max', 'apple m3 pro', 'apple m3 max', 'apple m2 ultra', 'apple m1 ultra']; + } else if (score >= 12) { + // Mid-range M-series (M4, M3, M2 Pro, M1 Pro/Max) + return ['apple m4', 'apple m3', 'apple m2 pro', 'apple m1 pro', 'apple m1 max']; + } else if (score >= 10) { + // Base M-series (M2, M1) + return ['apple m2', 'apple m1']; + } else { + // Fallback to all desktop Apple GPUs + return [ + 'apple m4 pro', 'apple m4', + 'apple m3 pro', 'apple m3', + 'apple m2 pro', 'apple m2', + 'apple m1 pro', 'apple m1' + ]; + } +} \ No newline at end of file diff --git a/test-browser.html b/test-browser.html new file mode 100644 index 0000000..7946e88 --- /dev/null +++ b/test-browser.html @@ -0,0 +1,164 @@ + + + + + + GPU Detection Test + + + +
+

GPU Detection Test

+ +
+ Browser:
+ Platform: +
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/test/desktop-safari.test.ts b/test/desktop-safari.test.ts new file mode 100644 index 0000000..b211610 --- /dev/null +++ b/test/desktop-safari.test.ts @@ -0,0 +1,67 @@ +/** + * @jest-environment jsdom + */ + +import { getTier } from './utils'; + +describe('Desktop Safari GPU Detection', () => { + it('should handle obfuscated Apple GPU gracefully', async () => { + const result = await getTier({ + renderer: 'Apple GPU', + isMobile: false, + isIpad: false, + screenSize: { width: 3456, height: 2234 } + }); + + console.log('Test result:', JSON.stringify(result, null, 2)); + + // Currently returns FALLBACK since we can't deobfuscate without WebGL context + expect(result.type).toBe('FALLBACK'); + expect(result.isMobile).toBe(false); + expect(result.gpu).toContain('apple gpu'); + expect(result.tier).toBe(1); + }); + + it('should detect specific Apple Silicon GPU models', async () => { + const result = await getTier({ + renderer: 'Apple M4 Pro', + isMobile: false, + isIpad: false, + screenSize: { width: 4112, height: 2658 } + }); + + console.log('M4 Pro test result:', JSON.stringify(result, null, 2)); + + // Should return BENCHMARK type + expect(result.type).toBe('BENCHMARK'); + expect(result.isMobile).toBe(false); + + // Should detect Apple M4 Pro GPU + expect(result.gpu).toBe('apple m4 pro'); + + // Should have tier 3 (120 fps) + expect(result.tier).toBe(3); + expect(result.fps).toBe(120); + }); + + it('should handle various screen sizes for M4 Pro', async () => { + const screenSizes = [ + { width: 4112, height: 2658 }, // One of M4 Pro's resolutions + { width: 5120, height: 2880 }, // Another M4 Pro resolution + ]; + + for (const screenSize of screenSizes) { + const result = await getTier({ + renderer: 'Apple M4 Pro', + isMobile: false, + isIpad: false, + screenSize + }); + + expect(result.type).toBe('BENCHMARK'); + expect(result.gpu).toBe('apple m4 pro'); + expect(result.tier).toBe(3); + expect(result.fps).toBe(120); + } + }); +}); \ No newline at end of file