1- // Vendor
2- import {
3- GL_ARRAY_BUFFER ,
4- GL_COLOR_BUFFER_BIT ,
5- GL_FLOAT ,
6- GL_FRAGMENT_SHADER ,
7- GL_RGBA ,
8- GL_STATIC_DRAW ,
9- GL_TRIANGLES ,
10- GL_UNSIGNED_BYTE ,
11- GL_VERTEX_SHADER ,
12- } from 'webgl-constants' ;
13-
14- // Internal
151import { deviceInfo } from './deviceInfo' ;
162
3+ // WebGL enum values, inlined to avoid a dep on `webgl-constants`. Same
4+ // numbers as `gl.ARRAY_BUFFER` etc., which minifiers can't fold because
5+ // they're property accesses. SEE:
6+ // https://registry.khronos.org/webgl/specs/latest/1.0/#5.14
7+ const GL_ARRAY_BUFFER = 0x8892 ;
8+ const GL_COLOR_BUFFER_BIT = 0x4000 ;
9+ const GL_FLOAT = 0x1406 ;
10+ const GL_FRAGMENT_SHADER = 0x8b30 ;
11+ const GL_RGBA = 0x1908 ;
12+ const GL_STATIC_DRAW = 0x88e4 ;
13+ const GL_TRIANGLES = 0x0004 ;
14+ const GL_UNSIGNED_BYTE = 0x1401 ;
15+ const GL_VERTEX_SHADER = 0x8b31 ;
16+
1717const debug = false ? console . warn : undefined ;
1818
1919export function deobfuscateAppleGPU (
@@ -25,67 +25,78 @@ export function deobfuscateAppleGPU(
2525 debug ?.( 'Safari 14+ obfuscates its GPU type and version, using fallback' ) ;
2626 return [ renderer ] ;
2727 }
28- const pixelId = calculateMagicPixelId ( gl ) ;
2928 const codeA = '801621810' as const ;
3029 const codeB = '8016218135' as const ;
3130 const codeC = '80162181161' as const ;
3231 const codeFB = '80162181255' ;
3332
3433 // All chipsets that support at least iOS 12:
35- const possibleChipsets : [
36- string ,
37- typeof codeA | typeof codeB | typeof codeC ,
38- number ,
39- ] [ ] = deviceInfo ?. isIpad
40- ? [
41- // ['a4', 5], // ipad 1st gen
42- // ['a5', 9], // ipad 2 / ipad mini 1st gen
43- // ['a5x', 9], // ipad 3rd gen
44- // ['a6x', 10], // ipad 4th gen
45- [ 'a7' , codeC , 12 ] , // ipad air / ipad mini 2 / ipad mini 3
46- [ 'a8' , codeB , 15 ] , // pad mini 4
47- [ 'a8x' , codeB , 15 ] , // ipad air 2
48- [ 'a9' , codeB , 15 ] , // ipad 5th gen
49- [ 'a9x' , codeB , 15 ] , // pro 9.7 2016 / pro 12.9 2015
50- [ 'a10' , codeB , 15 ] , // ipad 7th gen / ipad 6th gen
51- [ 'a10x' , codeB , 15 ] , // pro 10.5 2017 / pro 12.9 2nd gen, 2017
52- [ 'a12' , codeA , 15 ] , // ipad 8th gen / ipad air 3rd gen / ipad mini 5th gen
53- [ 'a12x' , codeA , 15 ] , // ipad pro 11 3st gen / ipad pro 12.9 3rd gen
54- [ 'a12z' , codeA , 15 ] , // ipad pro 11 4nd gen / ipad pro 12.9 4th gen
55- [ 'a14' , codeA , 15 ] , // ipad air 4th gen
56- [ 'a15' , codeA , 15 ] , // ipad mini 6th gen / ipad 10th gen
57- [ 'm1' , codeA , 15 ] , // ipad pro 11 5nd gen / ipad pro 12.9 5th gen / ipad air 5th gen
58- [ 'm2' , codeA , 15 ] , // ipad pro 11 6nd gen / ipad pro 12.9 6th gen
59- ]
60- : [
61- // ['a4', 7], // 4 / ipod touch 4th gen
62- // ['a5', 9], // 4S / ipod touch 5th gen
63- // ['a6', 10], // 5 / 5C
64- [ 'a7' , codeC , 12 ] , // 5S
65- [ 'a8' , codeB , 12 ] , // 6 / 6 plus / ipod touch 6th gen
66- [ 'a9' , codeB , 15 ] , // 6s / 6s plus/ se 1st gen
67- [ 'a10' , codeB , 15 ] , // 7 / 7 plus / iPod Touch 7th gen
68- [ 'a11' , codeA , 15 ] , // 8 / 8 plus / X
69- [ 'a12' , codeA , 15 ] , // XS / XS Max / XR
70- [ 'a13' , codeA , 15 ] , // 11 / 11 pro / 11 pro max / se 2nd gen
71- [ 'a14' , codeA , 15 ] , // 12 / 12 mini / 12 pro / 12 pro max
72- [ 'a15' , codeA , 15 ] , // 13 / 13 mini / 13 pro / 13 pro max / se 3rd gen / 14 / 14 plus
73- [ 'a16' , codeA , 15 ] , // 14 pro / 14 pro max / 15 / 15 plus
74- [ 'a17' , codeA , 15 ] , // 15 pro / 15 pro max
75- ] ;
76- let chipsets : typeof possibleChipsets ;
77-
78- // In iOS 14.x Apple started normalizing the outcome of this hack,
79- // we use this fact to limit the list to devices that support ios 14+
80- if ( pixelId === codeFB ) {
81- chipsets = possibleChipsets . filter ( ( [ , , iosVersion ] ) => iosVersion >= 14 ) ;
82- } else {
83- chipsets = possibleChipsets . filter ( ( [ , id ] ) => id === pixelId ) ;
84- // If nothing was found to match the pixel id, include all chipsets:
85- if ( ! chipsets . length ) {
86- chipsets = possibleChipsets ;
34+ let chipsets : [ string , typeof codeA | typeof codeB | typeof codeC , number ] [ ] =
35+ deviceInfo ?. isIpad
36+ ? [
37+ // ['a4', 5], // ipad 1st gen
38+ // ['a5', 9], // ipad 2 / ipad mini 1st gen
39+ // ['a5x', 9], // ipad 3rd gen
40+ // ['a6x', 10], // ipad 4th gen
41+ [ 'a7' , codeC , 12 ] , // ipad air / ipad mini 2 / ipad mini 3
42+ [ 'a8' , codeB , 15 ] , // pad mini 4
43+ [ 'a8x' , codeB , 15 ] , // ipad air 2
44+ [ 'a9' , codeB , 15 ] , // ipad 5th gen
45+ [ 'a9x' , codeB , 15 ] , // pro 9.7 2016 / pro 12.9 2015
46+ [ 'a10' , codeB , 15 ] , // ipad 7th gen / ipad 6th gen
47+ [ 'a10x' , codeB , 15 ] , // pro 10.5 2017 / pro 12.9 2nd gen, 2017
48+ [ 'a12' , codeA , 15 ] , // ipad 8th gen / ipad air 3rd gen / ipad mini 5th gen
49+ [ 'a12x' , codeA , 15 ] , // ipad pro 11 3st gen / ipad pro 12.9 3rd gen
50+ [ 'a12z' , codeA , 15 ] , // ipad pro 11 4nd gen / ipad pro 12.9 4th gen
51+ [ 'a14' , codeA , 15 ] , // ipad air 4th gen
52+ [ 'a15' , codeA , 15 ] , // ipad mini 6th gen / ipad 10th gen
53+ [ 'a16' , codeA , 15 ] , // ipad air 11 / ipad air 13 (a16, 2025) / ipad 11th gen
54+ [ 'a17 pro' , codeA , 15 ] , // ipad mini 7th gen
55+ [ 'm1' , codeA , 15 ] , // ipad pro 11 5nd gen / ipad pro 12.9 5th gen / ipad air 5th gen
56+ [ 'm2' , codeA , 15 ] , // ipad pro 11 6nd gen / ipad pro 12.9 6th gen
57+ [ 'm3' , codeA , 15 ] , // ipad air 11-inch (m3, 2025) / ipad air 13-inch (m3, 2025)
58+ [ 'm4' , codeA , 15 ] , // ipad pro 11-inch (m4, 2024) / ipad pro 13-inch (m4, 2024)
59+ [ 'm5' , codeA , 15 ] , // ipad pro 11-inch (m5, 2025) / ipad pro 13-inch (m5, 2025)
60+ ]
61+ : [
62+ // ['a4', 7], // 4 / ipod touch 4th gen
63+ // ['a5', 9], // 4S / ipod touch 5th gen
64+ // ['a6', 10], // 5 / 5C
65+ [ 'a7' , codeC , 12 ] , // 5S
66+ [ 'a8' , codeB , 12 ] , // 6 / 6 plus / ipod touch 6th gen
67+ [ 'a9' , codeB , 15 ] , // 6s / 6s plus/ se 1st gen
68+ [ 'a10' , codeB , 15 ] , // 7 / 7 plus / iPod Touch 7th gen
69+ [ 'a11' , codeA , 15 ] , // 8 / 8 plus / X
70+ [ 'a12' , codeA , 15 ] , // XS / XS Max / XR
71+ [ 'a13' , codeA , 15 ] , // 11 / 11 pro / 11 pro max / se 2nd gen
72+ [ 'a14' , codeA , 15 ] , // 12 / 12 mini / 12 pro / 12 pro max
73+ [ 'a15' , codeA , 15 ] , // 13 / 13 mini / 13 pro / 13 pro max / se 3rd gen / 14 / 14 plus
74+ [ 'a16' , codeA , 15 ] , // 14 pro / 14 pro max / 15 / 15 plus
75+ [ 'a17 pro' , codeA , 15 ] , // 15 pro / 15 pro max
76+ [ 'a18' , codeA , 15 ] , // iphone 16 / iphone 16 plus / iphone 16e
77+ [ 'a18 pro' , codeA , 15 ] , // iphone 16 pro / iphone 16 pro max
78+ [ 'a19' , codeA , 15 ] , // iphone 17
79+ [ 'a19 pro' , codeA , 15 ] , // iphone 17 air / iphone 17 pro / iphone 17 pro max
80+ ] ;
81+ // On iOS 14+ the pixel ID was normalized by Apple and only tells us
82+ // "iOS 14+" — which the UA-based iOSVersion already does, without a
83+ // WebGL draw call. Skip the shader when we can.
84+ const skipShader =
85+ deviceInfo ?. iOSVersion !== undefined && deviceInfo . iOSVersion >= 14 ;
86+ if ( ! skipShader ) {
87+ const pixelId = calculateMagicPixelId ( gl ) ;
88+ if ( pixelId === codeFB ) {
89+ chipsets = chipsets . filter ( ( [ , , iosVersion ] ) => iosVersion >= 14 ) ;
90+ } else {
91+ const matched = chipsets . filter ( ( [ , id ] ) => id === pixelId ) ;
92+ if ( matched . length ) chipsets = matched ;
8793 }
8894 }
95+ chipsets = filterByIOSVersion (
96+ chipsets ,
97+ deviceInfo ?. iOSVersion ,
98+ ! ! deviceInfo ?. isIpad
99+ ) ;
89100 const renderers = chipsets . map ( ( [ gpu ] ) => `apple ${ gpu } gpu` ) ;
90101 debug ?.(
91102 `iOS 12.2+ obfuscates its GPU type and version, using closest matches: ${ JSON . stringify (
@@ -95,6 +106,36 @@ export function deobfuscateAppleGPU(
95106 return renderers ;
96107}
97108
109+ const IOS_CHIP_CUTOFFS = [
110+ { minIOS : 13 , iphone : 9 , ipad : 8 } ,
111+ { minIOS : 16 , iphone : 10 , ipad : 9 } ,
112+ { minIOS : 17 , iphone : 12 , ipad : 10 } ,
113+ { minIOS : 26 , iphone : 13 , ipad : 12 } ,
114+ ] ;
115+
116+ function minimumAChip ( iOSVersion : number , isIpad : boolean ) : number {
117+ const cutoff = IOS_CHIP_CUTOFFS . findLast ( ( c ) => iOSVersion >= c . minIOS ) ;
118+ return cutoff ? ( isIpad ? cutoff . ipad : cutoff . iphone ) : 0 ;
119+ }
120+
121+ // Drop A-series chipsets that cannot run the detected iOS major. M-series
122+ // chips (2021+) and any unrecognized prefix are preserved.
123+ /** @internal — exported for test access only. */
124+ export function filterByIOSVersion < T extends readonly [ string , ...unknown [ ] ] > (
125+ chipsets : T [ ] ,
126+ iOSVersion : number | undefined ,
127+ isIpad : boolean
128+ ) : T [ ] {
129+ if ( ! iOSVersion ) return chipsets ;
130+ const minAChip = minimumAChip ( iOSVersion , isIpad ) ;
131+ if ( ! minAChip ) return chipsets ;
132+ return chipsets . filter ( ( [ gpu ] ) => {
133+ if ( gpu . startsWith ( 'm' ) ) return true ;
134+ const match = / ^ a ( \d + ) / . exec ( gpu ) ;
135+ return match ? parseInt ( match [ 1 ] , 10 ) >= minAChip : true ;
136+ } ) ;
137+ }
138+
98139// Apple GPU (iOS 12.2+, Safari 14+)
99140// SEE: https://github.com/pmndrs/detect-gpu/issues/7
100141// CREDIT: https://medium.com/@Samsy /detecting-apple-a10-iphone-7-to-a11-iphone-8-and-b019b8f0eb87
0 commit comments