gpu-atlas 0.1.0
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/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/gpu-atlas.js +1938 -0
- package/dist/gpu-atlas.js.map +1 -0
- package/dist/index.d.ts +398 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gpu-atlas contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
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 explicit — tick 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
|