Skip to content

Add support for Apple silicon gpus on desktop Safari #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
48 changes: 40 additions & 8 deletions src/internal/deobfuscateAppleGPU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {

// Internal
import { deviceInfo } from './deviceInfo';
import { getAppleGPUFromCapabilities } from './getAppleGPUFromCapabilities';

const debug = false ? console.warn : undefined;

Expand All @@ -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
Expand Down Expand Up @@ -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
)}`
);
Expand Down
61 changes: 61 additions & 0 deletions src/internal/getAppleGPUFromCapabilities.ts
Original file line number Diff line number Diff line change
@@ -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'
];
}
}
164 changes: 164 additions & 0 deletions test-browser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPU Detection Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.result {
background: #f8f9fa;
padding: 20px;
border-radius: 5px;
border: 1px solid #dee2e6;
margin-top: 20px;
}
.result h2 {
margin-top: 0;
color: #495057;
font-size: 18px;
}
pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.loading {
color: #6c757d;
font-style: italic;
}
.error {
color: #dc3545;
background: #f8d7da;
border-color: #f5c6cb;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-top: 20px;
}
button:hover {
background: #0056b3;
}
.info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>GPU Detection Test</h1>

<div class="info">
<strong>Browser:</strong> <span id="browser-info"></span><br>
<strong>Platform:</strong> <span id="platform-info"></span>
</div>

<button onclick="detectGPU()">Run GPU Detection</button>

<div id="result" class="result" style="display: none;">
<h2>Detection Result:</h2>
<pre id="result-json"></pre>
</div>

<div id="webgl-info" class="result" style="display: none; margin-top: 20px;">
<h2>WebGL Information:</h2>
<pre id="webgl-json"></pre>
</div>
</div>

<script type="module">
import { getGPUTier } from './dist/detect-gpu.esm.js';

// Display browser info
document.getElementById('browser-info').textContent = navigator.userAgent;
document.getElementById('platform-info').textContent = navigator.platform;

window.detectGPU = async function() {
const resultDiv = document.getElementById('result');
const resultJson = document.getElementById('result-json');
const webglDiv = document.getElementById('webgl-info');
const webglJson = document.getElementById('webgl-json');

resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultJson.textContent = 'Detecting GPU...';

try {
// Run GPU detection
const result = await getGPUTier();

// Display result
resultDiv.className = 'result';
resultJson.textContent = JSON.stringify(result, null, 2);

// Get WebGL debug info
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

if (gl) {
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
const webglInfo = {
vendor: gl.getParameter(gl.VENDOR),
renderer: gl.getParameter(gl.RENDERER),
webglVersion: gl.getParameter(gl.VERSION)
};

if (debugInfo) {
webglInfo.unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
webglInfo.unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
}

// Add some capability info
webglInfo.capabilities = {
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
maxVertexUniformVectors: gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
maxFragmentUniformVectors: gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS),
maxVaryingVectors: gl.getParameter(gl.MAX_VARYING_VECTORS),
maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE)
};

webglDiv.style.display = 'block';
webglJson.textContent = JSON.stringify(webglInfo, null, 2);
}

} catch (error) {
resultDiv.className = 'result error';
resultJson.textContent = 'Error: ' + error.message;
console.error(error);
}
}

// Auto-run detection on load
window.addEventListener('load', () => {
detectGPU();
});
</script>
</body>
</html>
67 changes: 67 additions & 0 deletions test/desktop-safari.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});