gpu-atlas 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,248 +1,287 @@
1
- # gpu-atlas
2
-
3
- **What WebGPU actually does on a device — measured, not declared.**
4
-
5
- **[Run it on your device →](https://ahnminjae08043-glitch.github.io/gpu-atlas/)**
6
- Takes a few seconds. Nothing is uploaded; the profile stays in your browser
7
- unless you save it yourself. It records the GPU and browser, not your user agent
8
- string — a profile is meant to be shareable without handing over more than the
9
- device description it is about.
10
-
11
- ---
12
-
13
- ## What measuring three devices turned up
14
-
15
- | | desktop | laptop | phone |
16
- |---|---|---|---|
17
- | GPU | NVIDIA Lovelace | Apple silicon | Adreno 7xx |
18
- | browser | Chrome 151 | Safari 26 | Samsung Internet 30 |
19
-
20
- ### Performance does not scale by a single factor
21
-
22
- | benchmark | desktop | Apple | Adreno | spread |
23
- |---|---:|---:|---:|---:|
24
- | triangle throughput | 3,906 MTri/s | 354 | 65 | **60x** |
25
- | texture sampling | 238 GSample/s | 18.4 | 11.0 | 22x |
26
- | fill rate | 124,464 MPixel/s | 35,079 | 6,522 | 19x |
27
- | bind group switching | 3,913,043 /s | 909,091 | 217,391 | 18x |
28
- | fragment ALU | 2,994 MPixel/s | 267 | 179 | 17x |
29
- | draw call overhead | 9,896,907 /s | 1,818,182 | 952,381 | 10x |
30
- | pipeline switching | 1,698,113 /s | 1,363,636 | 203,046 | 8x |
31
-
32
- Geometry spreads three times wider than anything else, and the ordering is not
33
- uniform either. Against Apple silicon the desktop's geometry lead (11x) is
34
- ordinary and its texture sampling lead (13x) is the largest; against Adreno,
35
- geometry is the worst axis by a distance. Tile-based mobile GPUs pay binning
36
- cost on vertices, so **on phones, cutting triangles buys far more than cutting
37
- pixels** the reverse of the desktop instinct.
38
-
39
- Pipeline switching is the flattest axis of all: Apple silicon is within 1.2x of
40
- a desktop discrete GPU there while being 11x behind on geometry. Any single
41
- "this device is N times slower" number would be wrong in both directions at
42
- once.
43
-
44
- ### Capability differences that break code
45
-
46
- **`maxUniformBufferBindingSize` differs by 10,922x.** Safari allows 683MB;
47
- Chrome and Samsung Internet cap at 64KB. Code developed on a Mac against large
48
- uniform buffers does not merely run slower elsewhere it fails outright.
49
-
50
- **No compressed texture format works everywhere.** Desktop Chrome has BC only.
51
- Safari has ETC2 and ASTC but no BC. Adreno has all three. Desktop Chrome and
52
- Safari both desktops share *zero* compressed formats.
53
-
54
- **`bgra8unorm` is storage-writable on desktop and on Apple, but not on Adreno.**
55
- It is also the preferred canvas format on two of the three (the phone reports
56
- `rgba8unorm`), which makes it an easy thing to build on and have fail on phones.
57
-
58
- `maxStorageBufferBindingSize` spans 16x (2GB / 683MB / 128MB).
59
-
60
- ### Browsers quantize GPU timestamps, and by different amounts
61
-
62
- `timestamp-query` results are rounded into buckets as a Spectre mitigation.
63
- Measured rather than assumed:
64
-
65
- | | GPU timer | `performance.now()` |
66
- |---|---|---|
67
- | Chrome 151 | 65,536 ns (2^16) | 0.1 ms |
68
- | Samsung Internet 30 | 65,536 ns (2^16) | 0.1 ms |
69
- | Safari 26 | no quantization detected | 1 ms |
70
-
71
- Both Chromium browsers return exactly 2^16 despite different GPU vendors and
72
- operating systems, while WebKit does not quantize the GPU timer at all — this is
73
- browser policy, not hardware.
74
-
75
- The practical consequence: **on Chromium, GPU work shorter than ~65 microseconds
76
- cannot be measured.** It reports as zero or as a value indistinguishable from
77
- unrelated work. Before accounting for this, two unrelated benchmarks here
78
- reported byte-identical timings.
79
-
80
- ### A benchmark that measured nothing
81
-
82
- Fragment work was originally created by stacking identical opaque fullscreen
83
- draws. On Apple silicon that reported 112,524 MPixel/s a 37x *advantage* over
84
- an RTX 4060, which is not plausible. A tile-based deferred renderer discards
85
- occluded opaque fragments before shading them, so every draw but the last was
86
- being thrown away. Adreno, tile-based but not deferred to the same degree, did
87
- not do this, so the same benchmark id was measuring different work per
88
- architecture.
89
-
90
- Additive blending fixes it, since each draw must contribute to the accumulated
91
- result. The corrected figure is 267 MPixel/s **530x lower**, and consistent
92
- with a laptop GPU. Geometry throughput was unaffected, as its triangles occupy
93
- distinct screen positions and never occluded one another.
94
-
95
- ---
96
-
97
- ## Usage
98
-
99
- ```bash
100
- npm install gpu-atlas
101
- ```
102
-
103
- ```js
104
- import { probe, breakingIssues, pickFormat } from 'gpu-atlas';
105
-
106
- const profile = await probe();
107
-
108
- // Anything that will break on this device
109
- for (const issue of breakingIssues(profile)) {
110
- console.warn(issue.subject, issue.detail);
111
- }
112
-
113
- // Pick a format verified to work here, rather than one merely declared
114
- const hdr = pickFormat(profile, ['rgba16float', 'rgb10a2unorm', 'rgba8unorm'], 'render');
115
- ```
116
-
117
- Comparing devices — this needs no GPU, so it also works in Node:
118
-
119
- ```js
120
- import { compareProfiles, formatComparison } from 'gpu-atlas';
121
-
122
- const comparison = compareProfiles([desktop, laptop, phone]);
123
- console.log(formatComparison(comparison));
124
-
125
- // Sorted by spread, so the worst portability risk is first
126
- const worst = comparison.benchmarks[0];
127
- console.log(worst.id, worst.ratio); // "triangle-throughput", 60.0
128
- ```
129
-
130
- Measurements flagged `unreliable` quantized or unstable — are marked rather
131
- than folded silently into a ratio.
132
-
133
- ## What it measures
134
-
135
- **Texture formats.** 53 formats, each checked for six separate capabilities:
136
- creation, shader sampling, render target, blending, storage binding, and 4x
137
- MSAA. A format that creates fine but fails to bind is a real failure mode, and
138
- it is invisible in the feature list.
139
-
140
- **WGSL compilation.** Cases where implementations diverge: function pointers,
141
- dynamic uniform indexing, struct alignment, override constants, workgroup
142
- atomics, uniformity analysis. Chrome uses Dawn/Tint, Firefox uses wgpu/naga,
143
- Safari has its own compiler, and each targets a different backend language.
144
- Compile time is recorded too, since it drives first-frame stalls.
145
-
146
- **Limits.** Declared values are requested for real, then bisected to find the
147
- actual ceiling when a device refuses.
148
-
149
- **Benchmarks.** Separated by axis rather than collapsed into a score, for the
150
- reason the table above demonstrates.
151
-
152
- ## Measurement notes
153
-
154
- Getting numbers is easy; getting numbers that mean anything was most of the work.
155
-
156
- **Quantization is measured, not assumed** and validated before it is believed.
157
- Work below one bucket reports as zero, so a trivial workload is grown until
158
- readings become non-zero. The smallest positive reading bounds the bucket, and
159
- the smallest gap between distinct readings lands on it. That candidate then has
160
- to *behave* like a bucket: under real quantization every reading is a multiple
161
- of it. Without that check, a fine-grained timer looks identical to a quantized
162
- one, and the same Safari machine reported a different timer on consecutive runs.
163
-
164
- **Both clocks are measured.** `performance.now()` is quantized too, to a full
165
- millisecond in Safari, and the draw-call benchmarks are wall-clock by necessity
166
- — their cost lives in browser validation and driver calls, which barely register
167
- on GPU timestamps. Each benchmark scales its repetitions until it spans enough
168
- ticks of whichever clock timed it, and every result carries that tick count.
169
-
170
- **This is the only way to read a variation of zero correctly.** Perfect
171
- consistency and a timer that cannot resolve the work look identical otherwise.
172
- Mobile turned out to be genuinely more reproducible than desktop — geometry
173
- throughput repeated at exactly 65.1 MTri/s across runs weeks apart — but that
174
- only became a claim worth making once ticks confirmed the measurement was not
175
- sitting on the floor.
176
-
177
- ## Feature-aware baselines
178
-
179
- WebGPU widens core capabilities through features `texture-formats-tier1` adds
180
- storage binding to a set of formats, `float32-blendable` adds blending to 32-bit
181
- float targets. Comparing against a fixed core baseline produces a flood of false
182
- "more permissive than spec" reports, so the baseline is raised to match what a
183
- device declares before comparing. What survives is genuine divergence.
184
-
185
- ## The profile
186
-
187
- `probe()` returns a JSON-serializable `AtlasProfile` keeping `declared` and
188
- `verified` strictly separate, plus a `discrepancies` list where they disagree:
189
-
190
- - `breaking` — code relying on the declared value will fail here
191
- - `degraded` it works, but slower or with reduced capability
192
- - `note` worth recording, not worth acting on
193
-
194
- ## Profile schema
195
-
196
- Profiles are the point of this project, so a profile states which schema it was
197
- captured under and `SCHEMA_VERSION` is bumped whenever a field is added,
198
- removed, or changes meaning. The history is kept in `src/types.ts`.
199
-
200
- Version 2 made measurement trustworthiness explicittick counts, quantization
201
- flags, measured timer resolutions and changed the overdraw benchmarks to blend
202
- additively. Version 3 made errors structured rather than preformatted strings,
203
- and widened the fingerprint from 32 bits to 128, since the narrow version
204
- collided at a rate that mattered once profiles were being collected in bulk.
205
- Version 4 dropped the raw user agent: browser, version, platform and mobile are
206
- already parsed into their own fields, so keeping the original string added
207
- identifying detail without adding information.
208
-
209
- `compareProfiles` accepts older profiles and still compares their capability
210
- data, but marks pre-v2 benchmark numbers `staleBenchmarks` and treats them as
211
- unreliable, because a version 1 profile's silence about quantization means
212
- "not recorded" rather than "fine".
213
-
214
- ## Contributing
215
-
216
- ```bash
217
- npm install
218
- npm test # comparison and quantization detection, no GPU needed
219
- npm run dev # demo at /demo/
220
- ```
221
-
222
- The probe needs a real GPU, so it is verified by running the demo on actual
223
- devices. Everything that does not profile comparison, quantization detection,
224
- discrepancy analysis, fingerprinting is unit tested and runs in CI.
225
-
226
- Automating the probe itself was attempted and does not currently work:
227
- Playwright's bundled Chromium ships without WebGPU, and driving a system Chrome
228
- through it leaves `navigator.gpu` undefined regardless of `--enable-unsafe-swiftshader`,
229
- `--use-angle=swiftshader`, or headed mode. Deno's built-in WebGPU looks like the
230
- more promising route for anyone who wants to try again.
231
-
232
- ## Status
233
-
234
- Early, and honest about it. **Three devices is not a dataset.** The differences
235
- above are facts about these three machines; whether they generalize needs many
236
- more profiles. Firefox and iOS are entirely unmeasured.
237
-
238
- Worth stating plainly: the original premise — that browsers misreport their own
239
- capabilities has not held up. All three devices did exactly what they
240
- declared, zero discrepancies each. The value turned out to be in the gaps
241
- *between* devices, which is why comparison exists at all.
242
-
243
- If you run the probe, saving the JSON and opening an issue with it genuinely
244
- helps.
245
-
246
- ## License
247
-
248
- MIT
1
+ # gpu-atlas
2
+
3
+ **What WebGPU actually does on a device — measured, not declared.**
4
+
5
+ **[Run it on your device →](https://ahnminjae08043-glitch.github.io/gpu-atlas/)**
6
+ Takes a few seconds. Nothing is uploaded; the profile stays in your browser
7
+ unless you save it yourself. It records the GPU and browser, not your user agent
8
+ string — a profile is meant to be shareable without handing over more than the
9
+ device description it is about.
10
+
11
+ ---
12
+
13
+ ## What measuring three devices turned up
14
+
15
+ | | desktop | tablet | phone |
16
+ |---|---|---|---|
17
+ | GPU | NVIDIA Lovelace | Apple | Adreno 7xx |
18
+ | browser | Chrome 151 | Safari 26.6 | Samsung Internet 30 |
19
+ | OS | Windows | iPadOS | Android 16 |
20
+
21
+ The tablet is an iPad, and finding that out took longer than it should have.
22
+ Since iPadOS 13 Safari sends a Macintosh user agent containing neither `iPad`
23
+ nor `Mobi`, so the probe filed it as a desktop and left `platform` empty — a
24
+ profile that could not say whether it came from a Mac or an iPad. Schema 5 uses
25
+ `maxTouchPoints` to tell them apart. Everything below was re-measured after the
26
+ fix; profiles at schema 4 and earlier cannot make the distinction at all.
27
+
28
+ ### Performance does not scale by a single factor
29
+
30
+ | benchmark | desktop | iPad | Adreno | spread |
31
+ |---|---:|---:|---:|---:|
32
+ | triangle throughput | 4,246 MTri/s | 356 | 65 | **65x** |
33
+ | texture sampling | 254 GSample/s | 17.4 | 11.0 | 23x |
34
+ | fill rate | 127,117 MPixel/s | 36,263 | 6,522 | 20x |
35
+ | fragment ALU | 3,122 MPixel/s | 266 | 179 | 18x |
36
+ | bind group switching | 4,067,797 /s | 888,889 | 279,720 | 14x |
37
+ | pipeline switching | 1,904,762 /s | 1,200,000 | 181,818 | 11x |
38
+ | draw call overhead | 6,185,567 /s | 1,828,571 | 764,331 | 8x |
39
+
40
+ Geometry spreads nearly three times wider than the next-widest axis, and the
41
+ ordering is not uniform either. Against the iPad the desktop's geometry lead
42
+ (12x) is ordinary and its texture sampling lead (15x) is the largest; against
43
+ Adreno, geometry is the worst axis by a distance. Tile-based mobile GPUs pay
44
+ binning cost on vertices, so **on phones, cutting triangles buys far more than
45
+ cutting pixels** — the reverse of the desktop instinct.
46
+
47
+ Pipeline switching is the flattest axis of all: the iPad is within 1.6x of a
48
+ desktop discrete GPU there while being 12x behind on geometry. Any single "this
49
+ device is N times slower" number would be wrong in both directions at once.
50
+
51
+ Draw call overhead came back flagged `unreliable` on the desktop the run
52
+ varied more than 15% between samples. The number is printed rather than hidden,
53
+ with the flag attached.
54
+
55
+ ### Capability differences that break code
56
+
57
+ **`maxUniformBufferBindingSize` differs by 10,923x.** The iPad allows
58
+ 715,827,880 bytes; Chrome and Samsung Internet cap at 65,536. Code developed
59
+ against large uniform buffers does not merely run slower elsewhere — it fails
60
+ outright. Treat the upper figure as one device's, not WebKit's: it is large
61
+ enough to look memory-derived, and no second Apple device has been measured.
62
+
63
+ **No compressed texture format works on all three.** Desktop Chrome has BC only
64
+ (6 formats). The iPad has ETC2 and ASTC but no BC (5). Adreno has all three
65
+ (11). Desktop Chrome and the iPad share *zero* — and across all three devices
66
+ the intersection is also empty, so there is no single compressed format to ship.
67
+
68
+ **`bgra8unorm` is storage-writable on desktop and on the iPad, but not on
69
+ Adreno.** It is also the preferred canvas format on those same two, while the
70
+ phone reports `rgba8unorm` — an easy thing to build on and have fail on phones.
71
+
72
+ `maxStorageBufferBindingSize` spans 16x (2.1GB / 716MB / 134MB) and
73
+ `maxBufferSize` 3x.
74
+
75
+ ### Browsers quantize GPU timestamps, and by different amounts
76
+
77
+ `timestamp-query` results are rounded into buckets as a Spectre mitigation.
78
+ Measured rather than assumed:
79
+
80
+ | | GPU timer | `performance.now()` |
81
+ |---|---|---|
82
+ | Chrome 151 | 65,536 ns (2^16) | 0.1 ms |
83
+ | Samsung Internet 30 | 65,536 ns (2^16) | 0.1 ms |
84
+ | Safari 26.6 | no quantization detected | 1 ms |
85
+
86
+ Both Chromium browsers return exactly 2^16 despite different GPU vendors and
87
+ operating systems, while WebKit does not quantize the GPU timer at all this is
88
+ browser policy, not hardware.
89
+
90
+ The practical consequence: **on Chromium, GPU work shorter than ~65 microseconds
91
+ cannot be measured.** It reports as zero or as a value indistinguishable from
92
+ unrelated work. Before accounting for this, two unrelated benchmarks here
93
+ reported byte-identical timings.
94
+
95
+ ### A benchmark that measured nothing
96
+
97
+ Fragment work was originally created by stacking identical opaque fullscreen
98
+ draws. On the iPad that reported 112,524 MPixel/s — a 36x *advantage* over an
99
+ RTX 4060, which is not plausible. A tile-based deferred renderer discards
100
+ occluded opaque fragments before shading them, so every draw but the last was
101
+ being thrown away. Adreno, tile-based but not deferred to the same degree, did
102
+ not do this, so the same benchmark id was measuring different work per
103
+ architecture.
104
+
105
+ Additive blending fixes it, since each draw must contribute to the accumulated
106
+ result. The corrected figure is 266 MPixel/s — **423x lower**, and in line with
107
+ the rest of that device's numbers. Geometry throughput was unaffected, as its
108
+ triangles occupy distinct screen positions and never occluded one another, which
109
+ is the signature a real fix should have.
110
+ ---
111
+
112
+ ## Usage
113
+
114
+ ```bash
115
+ npm install gpu-atlas
116
+ ```
117
+
118
+ Or without installing anything, straight from a CDN:
119
+
120
+ ```js
121
+ import { probe } from 'https://esm.sh/gpu-atlas';
122
+ ```
123
+
124
+ ```js
125
+ import { probe, breakingIssues, pickFormat } from 'gpu-atlas';
126
+
127
+ const profile = await probe();
128
+
129
+ // Anything that will break on this device
130
+ for (const issue of breakingIssues(profile)) {
131
+ console.warn(issue.subject, issue.detail);
132
+ }
133
+
134
+ // Pick a format verified to work here, rather than one merely declared
135
+ const hdr = pickFormat(profile, ['rgba16float', 'rgb10a2unorm', 'rgba8unorm'], 'render');
136
+ ```
137
+
138
+ Comparing devices — this needs no GPU, so it also works in Node:
139
+
140
+ ```js
141
+ import { compareProfiles, formatComparison } from 'gpu-atlas';
142
+
143
+ const comparison = compareProfiles([desktop, tablet, phone]);
144
+ console.log(formatComparison(comparison));
145
+
146
+ // Sorted by spread, so the worst portability risk is first
147
+ const worst = comparison.benchmarks[0];
148
+ console.log(worst.id, worst.ratio); // "triangle-throughput", 65.2
149
+ ```
150
+
151
+ Measurements flagged `unreliable` — quantized or unstable — are marked rather
152
+ than folded silently into a ratio.
153
+
154
+ ## What it measures
155
+
156
+ **Texture formats.** 53 formats, each checked for six separate capabilities:
157
+ creation, shader sampling, render target, blending, storage binding, and 4x
158
+ MSAA. A format that creates fine but fails to bind is a real failure mode, and
159
+ it is invisible in the feature list.
160
+
161
+ **WGSL compilation.** Cases where implementations diverge: function pointers,
162
+ dynamic uniform indexing, struct alignment, override constants, workgroup
163
+ atomics, uniformity analysis. Chrome uses Dawn/Tint, Firefox uses wgpu/naga,
164
+ Safari has its own compiler, and each targets a different backend language.
165
+ Compile time is recorded too, since it drives first-frame stalls.
166
+
167
+ **Limits.** Declared values are requested for real, then bisected to find the
168
+ actual ceiling when a device refuses.
169
+
170
+ **Benchmarks.** Separated by axis rather than collapsed into a score, for the
171
+ reason the table above demonstrates.
172
+
173
+ ## Measurement notes
174
+
175
+ Getting numbers is easy; getting numbers that mean anything was most of the work.
176
+
177
+ **Quantization is measured, not assumed** — and validated before it is believed.
178
+ Work below one bucket reports as zero, so a trivial workload is grown until
179
+ readings become non-zero. The smallest positive reading bounds the bucket, and
180
+ the smallest gap between distinct readings lands on it. That candidate then has
181
+ to *behave* like a bucket: under real quantization every reading is a multiple
182
+ of it. Without that check, a fine-grained timer looks identical to a quantized
183
+ one, and the same Safari machine reported a different timer on consecutive runs.
184
+
185
+ **Both clocks are measured.** `performance.now()` is quantized too, to a full
186
+ millisecond in Safari, and the draw-call benchmarks are wall-clock by necessity
187
+ their cost lives in browser validation and driver calls, which barely register
188
+ on GPU timestamps. Each benchmark scales its repetitions until it spans enough
189
+ ticks of whichever clock timed it, and every result carries that tick count.
190
+
191
+ **This is the only way to read a variation of zero correctly.** Perfect
192
+ consistency and a timer that cannot resolve the work look identical otherwise.
193
+ Mobile turned out to be genuinely more reproducible than desktop — geometry
194
+ throughput repeated at exactly 65.1 MTri/s across runs weeks apart — but that
195
+ only became a claim worth making once ticks confirmed the measurement was not
196
+ sitting on the floor.
197
+
198
+ ## Feature-aware baselines
199
+
200
+ WebGPU widens core capabilities through features`texture-formats-tier1` adds
201
+ storage binding to a set of formats, `float32-blendable` adds blending to 32-bit
202
+ float targets. Comparing against a fixed core baseline produces a flood of false
203
+ "more permissive than spec" reports, so the baseline is raised to match what a
204
+ device declares before comparing. What survives is genuine divergence.
205
+
206
+ ## The profile
207
+
208
+ `probe()` returns a JSON-serializable `AtlasProfile` keeping `declared` and
209
+ `verified` strictly separate, plus a `discrepancies` list where they disagree:
210
+
211
+ - `breaking` code relying on the declared value will fail here
212
+ - `degraded` it works, but slower or with reduced capability
213
+ - `note` — worth recording, not worth acting on
214
+
215
+ ## Profile schema
216
+
217
+ Profiles are the point of this project, so a profile states which schema it was
218
+ captured under and `SCHEMA_VERSION` is bumped whenever a field is added,
219
+ removed, or changes meaning. The history is kept in `src/types.ts`.
220
+
221
+ Version 2 made measurement trustworthiness explicit — tick counts, quantization
222
+ flags, measured timer resolutions and changed the overdraw benchmarks to blend
223
+ additively. Version 3 made errors structured rather than preformatted strings,
224
+ and widened the fingerprint from 32 bits to 128, since the narrow version
225
+ collided at a rate that mattered once profiles were being collected in bulk.
226
+ Version 4 dropped the raw user agent: browser, version, platform and mobile are
227
+ already parsed into their own fields, so keeping the original string added
228
+ identifying detail without adding information.
229
+
230
+ `compareProfiles` accepts older profiles and still compares their capability
231
+ data, but marks pre-v2 benchmark numbers `staleBenchmarks` and treats them as
232
+ unreliable, because a version 1 profile's silence about quantization means
233
+ "not recorded" rather than "fine".
234
+
235
+ ## Contributing
236
+
237
+ ```bash
238
+ npm install
239
+ npm test # comparison and quantization detection, no GPU needed
240
+ npm run dev # demo at /demo/
241
+ ```
242
+
243
+ The probe needs a real GPU, so it is verified by running the demo on actual
244
+ devices. Everything that does not — profile comparison, quantization detection,
245
+ discrepancy analysis, fingerprinting — is unit tested and runs in CI.
246
+
247
+ Automating the probe itself was attempted and does not currently work:
248
+ Playwright's bundled Chromium ships without WebGPU, and driving a system Chrome
249
+ through it leaves `navigator.gpu` undefined regardless of `--enable-unsafe-swiftshader`,
250
+ `--use-angle=swiftshader`, or headed mode. Deno's built-in WebGPU looks like the
251
+ more promising route for anyone who wants to try again.
252
+
253
+ ## Status
254
+
255
+ Early, and honest about it. **Three devices is not a dataset.** The differences
256
+ above are facts about these three machines; whether they generalize needs many
257
+ more profiles. Firefox is entirely unmeasured, and so is any iPhone — the one
258
+ Apple device here is an iPad, which is not the same GPU class and not the same
259
+ browser build.
260
+
261
+ Worth stating plainly: the original premise — that browsers misreport their own
262
+ capabilities — has not held up. All three devices did exactly what they
263
+ declared, zero discrepancies each. The value turned out to be in the gaps
264
+ *between* devices, which is why comparison exists at all.
265
+
266
+ ### Contributing a profile
267
+
268
+ The dataset is the bottleneck, and it takes about a minute to widen it.
269
+
270
+ 1. Open the [demo](https://ahnminjae08043-glitch.github.io/gpu-atlas/) on any
271
+ device with WebGPU — a phone counts, and phones are underrepresented.
272
+ 2. Run the probe, then press **Share profile**. That copies the JSON and opens a
273
+ prefilled issue.
274
+ 3. Paste, and submit.
275
+
276
+ A profile records the GPU vendor and architecture the browser reports, which
277
+ formats and limits actually worked, timing numbers, and coarse environment facts
278
+ — browser and version, platform, core count, memory bucket, pixel ratio. The raw
279
+ user-agent string is deliberately not collected. Press **Copy profile** first if
280
+ you would rather read the whole thing before posting it.
281
+
282
+ Crashes are worth reporting too. If the probe fell over, it met something it was
283
+ not built for, and that is a finding.
284
+
285
+ ## License
286
+
287
+ MIT