gputex 0.6.0 → 0.8.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/README.md CHANGED
@@ -83,13 +83,19 @@ a bbox diagonal it follows anti-correlated channels, worth **+2–4 dB on
83
83
  normal-map-like content**) plus projection-based index assignment (each
84
84
  pixel is projected onto the colinear endpoint line in O(1) instead of
85
85
  searching every palette entry), and the block bits packed with
86
- straight-line constant shifts. The formats with coarse 4-level palettes
87
- (BC1, ASTC) add up to two least-squares endpoint refit rounds accepted per
88
- block only when they lower the error, and BC5 one; BC7's 16-level mode-6
89
- palette makes the refit redundant on a principal-axis seed (≤0.05 dB), so
90
- it skips it and stays the cheapest per pixel. On GPUs that report the
91
- `shader-f16` feature everything runs in f16 — the f32 shaders are the
92
- automatic fallback.
86
+ straight-line constant shifts. BC1's coarse 4-level palette adds up to two
87
+ least-squares endpoint refit rounds, solved from per-pass projection
88
+ moments and accepted per block only when they lower the error; its
89
+ near-flat blocks skip the line fit and take the endpoint pair whose ⅔/⅓
90
+ interpolant lands nearest the block colour (direct 565 rounding is up to 4
91
+ levels off — worth up to +3.9 dB on maps with flat regions). BC5 refits
92
+ once; BC7's 16-level mode-6 palette makes the refit redundant on a
93
+ principal-axis seed (≤0.05 dB). ASTC spends every one of its 128 bits: a
94
+ wide-span opaque block gets 16 weight levels with 192-level (trit-coded)
95
+ endpoints, a small-span one exact 8-bit endpoints with 8 levels,
96
+ exactly-grayscale blocks a luminance-only mode with 32 levels. On GPUs that
97
+ report the `shader-f16` feature everything runs in f16 — the f32 shaders
98
+ are the automatic fallback.
93
99
 
94
100
  ETC2 is the exception to the endpoint-line story: its blocks are per-subblock
95
101
  base colours shifted by scalar modifier tables. The encoder exploits the
@@ -97,24 +103,26 @@ algebra of that scalar shift — table and index selection depend only on each
97
103
  texel's luma-sum difference from the base, exactly (modulo decode clamping) —
98
104
  so the whole 8-table × 4-modifier search collapses to a handful of scalar
99
105
  threshold tests against a two-candidate table shortlist, with subblock error
100
- constants and the flip preselect computed O(1) from quadrant sums. A gated
101
- base-colour refit and a closed-form least-squares fit of ETC2's planar mode
102
- (which rescues the smooth gradients ETC1-style blocks band on) complete the
103
- block, all driven by the same estimates. The rewrite took the GPU pass
104
- from 6.0 ms to ~0.2 ms at 2048² (30×, within ~0.2 dB of the exhaustive
105
- search on photographic content — only the base refit was traded for
106
- speed). Its f16 module is EXACT-VALUE: lumas, D values and thresholds
107
- are integers f16 represents exactly, while the sums-of-squares estimates
108
- stay f32 (they overflow f16), so the two modules produce byte-identical
109
- output — f16 buys register pressure on mobile GPUs, not different
110
- results.
111
-
112
- On the repo's test cards this lands within **≤0.1 dB** of the exhaustive
113
- per-block reference encoders (BC5 matches the reference exactly; ASTC and
114
- BC1-on-normal-maps measure slightly above it), trailing only on adversarial
115
- high-frequency noise, where any single-line seed loses to an exhaustive
116
- search — while encoding an order of magnitude faster. See the benchmark
117
- table below.
106
+ constants and the flip preselect computed O(1) from quadrant sums (exactly
107
+ gray blocks, which give the preselect nothing to go on, score both flips on
108
+ a one-channel path). A closed-form least-squares fit of ETC2's planar mode
109
+ (which rescues the smooth gradients ETC1-style blocks band on) completes
110
+ the block, driven by the same estimates. There is no base-colour refit
111
+ (~0.2 dB on photographic content for ≥13% GPU). The kernel reads the source
112
+ through `textureGather` and keeps every per-texel quantity in registers
113
+ with constant indexing (numbers below). Its f16 module is EXACT-VALUE: lumas,
114
+ D values and thresholds are integers (or halves) f16 represents exactly,
115
+ while the sums and estimates stay f32 (they overflow f16), so the two
116
+ modules produce byte-identical output wherever the sampler's unorm
117
+ conversion is exact (verified on Apple) — f16 buys register space, not
118
+ different results.
119
+
120
+ On the repo's test textures this lands within a few tenths of a dB of the
121
+ per-block CPU reference encoders (`gputex/testing`) and above them on
122
+ several (BC5 matches exactly; BC1 on flat content, BC7 and ASTC on some
123
+ maps measure above), trailing only on adversarial high-frequency noise,
124
+ where any single-line seed loses to an exhaustive search — while encoding
125
+ an order of magnitude faster. See the benchmark table below.
118
126
 
119
127
  #### SVG sources
120
128
 
@@ -331,39 +339,49 @@ readback) rather than a round trip per level.
331
339
 
332
340
  ## Benchmarks
333
341
 
334
- Measured with the repo's GPU test suite (see below) on an Apple Silicon GPU
335
- (`metal-3`) in Chrome, encoding a 2048×2048 image. **GPU pass** is the compute
336
- shader alone (WebGPU timestamp queries, median of 20 runs); end-to-end wall
337
- time adds ~2–4 ms of image upload + result readback regardless of format.
338
- Each encoder caches its GPU resources (source texture, output/staging
339
- buffers, bind group) across encodes, so repeated encodes — including mip
340
- chains — skip per-call allocation: in an interleaved A/B this cuts BC7
341
- end-to-end wall time by ~10% at 512², ~20% at 1024–2048² and ~35% at 4096².
342
+ Measured on an Apple Silicon GPU (`metal-3`, M3) in Chrome with the `/eval`
343
+ dev page: per-dispatch compute time from timestamp queries over batches of
344
+ back-to-back dispatches, all variants interleaved in one session, encoding
345
+ the procedural 2048×2048 benchmark image. **GPU pass** is the compute
346
+ shader alone.
342
347
 
343
348
  | Format | Shader | GPU pass |
344
349
  | -------- | ------------- | ----------- |
345
- | BC1 | f16 (default) | **0.26 ms** |
346
- | BC1 | f32 | 0.46 ms |
347
- | BC5 | f16 (default) | **0.26 ms** |
348
- | BC5 | f32 | 0.26 ms |
349
- | BC7 | f16 (default) | **0.26 ms** |
350
- | BC7 | f32 | 0.59 ms |
351
- | ASTC 4×4 | f16 (default) | **0.26 ms** |
352
- | ASTC 4×4 | f32 | 0.56 ms |
353
- | ETC2 | f16 + f32 | 0.20 ms |
354
-
355
- The ETC2 figure is the interleaved `/ab` harness measurement (batched
356
- dispatches, clock-stable). On a 100 GB/s part just reading the 2048² RGBA8
357
- source costs ~0.15 ms, so the entire selection algorithm adds ~30% on top
358
- of touching the bytes. Two faster variants live in git history and were
359
- deliberately not shipped: a two-pass 2 B/px prepared source (encode pass
360
- 0.115 ms, but the prep pass is also bandwidth-bound and cannot overlap, so
361
- the per-texture total regressed) and an O(1) hedged table pick (−3% for
362
- −0.5 dB — a poor trade against the scored search).
363
-
364
- Timestamps are quantised to 100 µs by Chrome and Apple GPU clock states swing
365
- timings by ~2×, so sub-millisecond figures are indicative (±0.1 ms); compare
366
- variants only within a single session.
350
+ | BC1 | f16 (default) | **0.33 ms** |
351
+ | BC1 | f32 | 0.50 ms |
352
+ | BC5 | f16 (default) | **0.14 ms** |
353
+ | BC5 | f32 | 0.15 ms |
354
+ | BC7 | f16 (default) | **0.19 ms** |
355
+ | BC7 | f32 | 0.52 ms |
356
+ | ASTC 4×4 | f16 (default) | **0.17 ms** |
357
+ | ASTC 4×4 | f32 | 0.28 ms |
358
+ | ETC2 | f16 (default) | **0.14 ms** |
359
+ | ETC2 | f32 | 0.15 ms |
360
+
361
+ End-to-end `encodeToBytes()` wall time adds the upload and the readback.
362
+ Each encoder caches its GPU resources (source texture, output/staging
363
+ buffers, bind groups) across encodes, and outputs above ~3 MB are encoded
364
+ in row bands — one submission and staging buffer per ~2 MB of output, so
365
+ the readback of one band (a GPU-process copy that dominated large encodes)
366
+ overlaps the compute of the next — with the result array pre-faulted while
367
+ the GPU works. In an interleaved A/B against the single-submission
368
+ readback this cuts wall time by 23–33% at 4096² and 5–25% at 2048²
369
+ (bytes identical); a fresh 4096² encode is then dominated by the ~8 ms
370
+ `copyExternalImageToTexture` upload.
371
+
372
+ On a 100 GB/s part just reading the 2048² RGBA8 source costs ~0.14 ms, so
373
+ BC5/BC7/ASTC sit within ~1.3× of simply touching the bytes and ETC2 at it;
374
+ BC1's refit rounds keep it ALU-bound. On real textures (which Apple's
375
+ lossless framebuffer compression makes cheaper to read) and at 1024², where
376
+ the source stays cached, the ETC2 kernel is ALU-exposed again: 0.035–0.04 ms
377
+ at 1024², 0.13–0.15 ms at 2048² and 0.49–0.57 ms at 4096² across the corpus.
378
+ A two-pass 2 B/px prepared-source ETC2 variant lives in git history and was
379
+ not shipped: its prep pass is also bandwidth-bound and cannot overlap, so
380
+ the per-texture total regressed.
381
+
382
+ Single-dispatch timestamps are coarse and Apple GPU clock states swing
383
+ timings by up to ~2× across page loads, so compare variants only within a
384
+ single session, interleaved (as `/eval` and `/ab` do).
367
385
 
368
386
  ## Testing
369
387
 
@@ -382,6 +400,19 @@ time per format across image sizes (256²–4096²) — the numbers that matter
382
400
  for runtime streaming, where host overhead dominates small textures
383
401
  (results on `window.__GPUTEX_BENCH__`).
384
402
 
403
+ For shader work, `/eval` compares WGSL variants on speed AND quality in one
404
+ session: `example/scripts/ab-sync.sh` snapshots the working-tree shaders
405
+ (`<fmt>_work`) and git HEAD's (`<fmt>_head`) into `example/public/ab/`, and
406
+ `/eval?shaders=bc7_f16_head,bc7_f16_work` times them interleaved and scores
407
+ each image through the GPU's own hardware decoder (the encoded blocks are
408
+ copied into a real compressed texture and sampled), reporting PSNR, blocks
409
+ that got worse/better, the worst regression on blocks the first variant
410
+ encodes near-losslessly, and byte-identical coverage — over the full
411
+ 1K/2K/4K texture corpus in seconds (`window.__GPUTEX_EVAL__`). For host-side
412
+ changes, `example/scripts/ab-lib.sh` builds git HEAD's library and the
413
+ working tree side by side (`/ab/gputex_head.js`, `/ab/gputex_work.js`) so
414
+ both can be driven in one page with alternating calls.
415
+
385
416
  The page runs three groups against the live WebGPU device and renders
386
417
  PASS/FAIL tables (machine-readable copy on `window.__GPUTEX_TESTS__`):
387
418
 
package/dist/index.d.ts CHANGED
@@ -165,10 +165,10 @@ declare abstract class Encoder {
165
165
  private _cachedSrcSource;
166
166
  private _cachedSrcFlipY;
167
167
  private _cachedDst;
168
- private _cachedStaging;
168
+ private _cachedStagings;
169
169
  private _cachedParams;
170
- private _lastParams;
171
- private _cachedBindGroup;
170
+ private _lastParamsKey;
171
+ private _cachedBindGroups;
172
172
  private _cachedPrepPlanes;
173
173
  private _cachedPrepBindGroup;
174
174
  private _resourcesBusy;
@@ -182,8 +182,9 @@ declare abstract class Encoder {
182
182
  private _chainPrepBindGroups;
183
183
  private _chainParams;
184
184
  private _chainBindGroups;
185
+ private _chainBandBindGroups;
185
186
  private _chainDst;
186
- private _chainStaging;
187
+ private _chainStagings;
187
188
  private _chainBusy;
188
189
  constructor({ device, adapter, ownsDevice, disableF16 }: EncoderOptions);
189
190
  protected _buildPipeline(): void;
@@ -295,10 +296,17 @@ declare abstract class Encoder {
295
296
  /** Block-grid geometry + packed output offsets for a chain of levels.
296
297
  * `byteSpan` is both the dst buffer size and the readback copy size (a
297
298
  * multiple of 4: byteLen is a multiple of bytesPerBlock ≥ 8, offsets are
298
- * CHAIN_ALIGN-ed). */
299
+ * CHAIN_ALIGN-ed). `bands` splits the base level into row bands. */
299
300
  private _chainGeometry;
300
- /** Shared chain-encode tail: one compute pass with a dispatch per level,
301
- * one submit, one staging readback sliced into per-level byte arrays. */
301
+ /** One uniform buffer, one write: level i's { blocksX, blocksY, width,
302
+ * height, y0 = 0 } at slot i, then base-level band b at slot
303
+ * levels + b (y0 = the band's first block row). Slots are CHAIN_ALIGN
304
+ * apart. */
305
+ private _createChainParams;
306
+ /** Shared chain-encode tail: one submission per base-level row band —
307
+ * band b dispatches its rows of level 0, the last band also every tail
308
+ * level — each read back through its own staging buffer as it lands,
309
+ * then sliced into per-level byte arrays. */
302
310
  private _submitChainAndRead;
303
311
  /** Create the query set + resolve/staging buffers for one timed
304
312
  * submission, or null when the device lacks 'timestamp-query'. */