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 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