gputex 0.5.0 → 0.7.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
@@ -109,12 +115,12 @@ stay f32 (they overflow f16), so the two modules produce byte-identical
109
115
  output — f16 buys register pressure on mobile GPUs, not different
110
116
  results.
111
117
 
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.
118
+ On the repo's test textures this lands within a few tenths of a dB of the
119
+ per-block CPU reference encoders (`gputex/testing`) and above them on
120
+ several (BC5 matches exactly; BC1 on flat content, BC7 and ASTC on some
121
+ maps measure above), trailing only on adversarial high-frequency noise,
122
+ where any single-line seed loses to an exhaustive search — while encoding
123
+ an order of magnitude faster. See the benchmark table below.
118
124
 
119
125
  #### SVG sources
120
126
 
@@ -331,39 +337,46 @@ readback) rather than a round trip per level.
331
337
 
332
338
  ## Benchmarks
333
339
 
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².
340
+ Measured on an Apple Silicon GPU (`metal-3`, M3) in Chrome with the `/eval`
341
+ dev page: per-dispatch compute time from timestamp queries over batches of
342
+ back-to-back dispatches, all variants interleaved in one session, encoding
343
+ the procedural 2048×2048 benchmark image. **GPU pass** is the compute
344
+ shader alone.
342
345
 
343
346
  | Format | Shader | GPU pass |
344
347
  | -------- | ------------- | ----------- |
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 |
348
+ | BC1 | f16 (default) | **0.33 ms** |
349
+ | BC1 | f32 | 0.50 ms |
350
+ | BC5 | f16 (default) | **0.14 ms** |
351
+ | BC5 | f32 | 0.15 ms |
352
+ | BC7 | f16 (default) | **0.19 ms** |
353
+ | BC7 | f32 | 0.52 ms |
354
+ | ASTC 4×4 | f16 (default) | **0.17 ms** |
355
+ | ASTC 4×4 | f32 | 0.28 ms |
353
356
  | ETC2 | f16 + f32 | 0.20 ms |
354
357
 
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
358
+ End-to-end `encodeToBytes()` wall time adds the upload and the readback.
359
+ Each encoder caches its GPU resources (source texture, output/staging
360
+ buffers, bind groups) across encodes, and outputs above ~3 MB are encoded
361
+ in row bands — one submission and staging buffer per ~2 MB of output, so
362
+ the readback of one band (a GPU-process copy that dominated large encodes)
363
+ overlaps the compute of the next — with the result array pre-faulted while
364
+ the GPU works. In an interleaved A/B against the single-submission
365
+ readback this cuts wall time by 23–33% at 4096² and 5–25% at 2048²
366
+ (bytes identical); a fresh 4096² encode is then dominated by the ~8 ms
367
+ `copyExternalImageToTexture` upload.
368
+
369
+ On a 100 GB/s part just reading the 2048² RGBA8 source costs ~0.15 ms, so
370
+ BC5/BC7/ASTC/ETC2 sit within ~1.3× of simply touching the bytes; BC1's
371
+ refit rounds keep it ALU-bound. Two faster ETC2 variants live in git
372
+ history and were deliberately not shipped: a two-pass 2 B/px prepared source (encode pass
360
373
  0.115 ms, but the prep pass is also bandwidth-bound and cannot overlap, so
361
374
  the per-texture total regressed) and an O(1) hedged table pick (−3% for
362
375
  −0.5 dB — a poor trade against the scored search).
363
376
 
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.
377
+ Single-dispatch timestamps are coarse and Apple GPU clock states swing
378
+ timings by up to ~2× across page loads, so compare variants only within a
379
+ single session, interleaved (as `/eval` and `/ab` do).
367
380
 
368
381
  ## Testing
369
382
 
@@ -382,6 +395,19 @@ time per format across image sizes (256²–4096²) — the numbers that matter
382
395
  for runtime streaming, where host overhead dominates small textures
383
396
  (results on `window.__GPUTEX_BENCH__`).
384
397
 
398
+ For shader work, `/eval` compares WGSL variants on speed AND quality in one
399
+ session: `example/scripts/ab-sync.sh` snapshots the working-tree shaders
400
+ (`<fmt>_work`) and git HEAD's (`<fmt>_head`) into `example/public/ab/`, and
401
+ `/eval?shaders=bc7_f16_head,bc7_f16_work` times them interleaved and scores
402
+ each image through the GPU's own hardware decoder (the encoded blocks are
403
+ copied into a real compressed texture and sampled), reporting PSNR, blocks
404
+ that got worse/better, the worst regression on blocks the first variant
405
+ encodes near-losslessly, and byte-identical coverage — over the full
406
+ 1K/2K/4K texture corpus in seconds (`window.__GPUTEX_EVAL__`). For host-side
407
+ changes, `example/scripts/ab-lib.sh` builds git HEAD's library and the
408
+ working tree side by side (`/ab/gputex_head.js`, `/ab/gputex_work.js`) so
409
+ both can be driven in one page with alternating calls.
410
+
385
411
  The page runs three groups against the live WebGPU device and renders
386
412
  PASS/FAIL tables (machine-readable copy on `window.__GPUTEX_TESTS__`):
387
413
 
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'. */
@@ -605,4 +613,141 @@ interface RasterizeSvgOptions {
605
613
  */
606
614
  declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
607
615
 
608
- export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, type BC7EncoderOptions, BC7WebGLEncoder, type Capabilities, ETC2Encoder, type EncodeBytesResult, type EncodeCallOptions, type EncodeMipChainResult, type EncodedLevelBytes, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatQuality, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };
616
+ /**
617
+ * Everything `compressTexture()` can take as an image source. A superset
618
+ * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
619
+ * and Blob / File objects — the common cases in a web app.
620
+ *
621
+ * SVG works through all of these: a URL to an `.svg` file, a string of
622
+ * inline SVG markup (detected by a leading `<`), an SVG Blob/File, or an
623
+ * HTMLImageElement whose src is SVG. Vector sources are rasterised to RGBA
624
+ * before encoding — see the `svgSize` option.
625
+ */
626
+ type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
627
+ interface CompressOptions {
628
+ /** How the texture will be used. Drives format selection. Default 'color'. */
629
+ hint?: TextureHint;
630
+ /**
631
+ * Prefer a specific format over the default choice when the device
632
+ * supports it; falls back to the normal selection (BC7 → ASTC → ETC2 →
633
+ * RGBA8) when it doesn't. Currently only 'bc1': half the memory of BC7
634
+ * for opaque colour textures, at lower quality. Only honoured with
635
+ * `hint: 'color'` — BC1 can't carry real alpha or normal maps.
636
+ */
637
+ preferredFormat?: PreferredFormat;
638
+ /**
639
+ * Memory/fidelity trade-off for opaque colour textures. Default 'high'
640
+ * (BC7 / ASTC 4×4, 1 byte/pixel). 'low' picks the 4-bpp formats when the
641
+ * device has one — BC1 on desktop-class GPUs, ETC2 RGB8 on mobile-class
642
+ * ones — halving GPU memory at visibly lower quality on smooth content.
643
+ * Ignored for `hint: 'colorWithAlpha'` and `hint: 'normal'` (the 4-bpp
644
+ * formats can't carry them). On the WebGL fallback tier only BC1 is
645
+ * available at 'low'.
646
+ */
647
+ quality?: FormatQuality;
648
+ /** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
649
+ colorSpace?: 'srgb' | 'linear';
650
+ /**
651
+ * Rasterisation size for SVG sources. A number scales the SVG so its
652
+ * longest side matches (aspect ratio preserved); `{ width, height }`
653
+ * rasterises at exactly that size. Default: the SVG's intrinsic size
654
+ * (absolute width/height attributes, else the viewBox dimensions).
655
+ * Ignored for non-SVG sources.
656
+ */
657
+ svgSize?: SvgRasterSize;
658
+ /** Flip the image vertically before encoding. Default true (matches Three.js convention). */
659
+ flipY?: boolean;
660
+ /** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
661
+ mipmaps?: boolean;
662
+ /** Reuse an existing device (e.g. Three.js's renderer device) instead
663
+ * of creating a new one. WebGPU path only. When provided, the encoder
664
+ * never destroys it. */
665
+ device?: GPUDevice;
666
+ adapter?: GPUAdapter;
667
+ /**
668
+ * Keep the compressed bytes in a session-scoped in-memory LRU and reuse
669
+ * them on repeat calls, skipping BOTH the image decode and the encode —
670
+ * the dominant costs. Re-loading a texture later in the session (e.g.
671
+ * two worlds sharing an atlas) becomes a few ms. Keyed by source
672
+ * identity + selected format + encode options; capped at 256 MiB of
673
+ * compressed bytes by default (`setTranscodeCacheLimit()` to tune) and
674
+ * never touches persistent storage. Default false.
675
+ *
676
+ * URL and Blob/File sources get an identity automatically (URL string or
677
+ * content hash). Pixel sources (ImageBitmap, canvas, ImageData) are only
678
+ * cached when `cacheKey` is provided.
679
+ */
680
+ cache?: boolean;
681
+ /**
682
+ * Explicit cache identity for the source, overriding the derived one.
683
+ * Use when you already know a stable name (e.g. an asset path) and want
684
+ * to skip content hashing, or to make pixel sources cacheable.
685
+ */
686
+ cacheKey?: string;
687
+ }
688
+ interface CompressResult {
689
+ /**
690
+ * Encoded compressed mip levels (`levels[0]` is the base level), ready to
691
+ * upload to a compressed texture. Null on the RGBA8 fallback path — use
692
+ * `fallbackBitmap` instead.
693
+ */
694
+ levels: EncodedLevelBytes[] | null;
695
+ /**
696
+ * Decoded RGBA8 bitmap, set only when `fallbackUncompressed` (no compressed
697
+ * format was available on either backend). Upload it as a plain RGBA8
698
+ * texture; the caller applies colour space / flipY at the texture level.
699
+ */
700
+ fallbackBitmap: ImageBitmap | null;
701
+ /** The compressed format selected, or null when we fell back to RGBA8. */
702
+ format: TextureFormat | null;
703
+ /** True iff we fell back to an uncompressed RGBA8 bitmap because no encoder fit. */
704
+ fallbackUncompressed: boolean;
705
+ /**
706
+ * Which backend produced the result. 'webgpu' = compute path, 'webgl' =
707
+ * fragment-shader fallback, 'none' = uncompressed RGBA8.
708
+ */
709
+ backend: 'webgpu' | 'webgl' | 'none';
710
+ /**
711
+ * True iff the chosen format is ASTC and the hint was 'normal'. The
712
+ * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
713
+ * has no 2-channel mode, so normal maps ride the RGBA path.
714
+ */
715
+ astcNormalRemap: boolean;
716
+ width: number;
717
+ height: number;
718
+ mipLevels: number;
719
+ /** Wall-clock time of GPU encoding, summed across mip levels. */
720
+ encodeMs: number;
721
+ /**
722
+ * Wall-clock time to turn the source into decoded RGBA pixels: fetch /
723
+ * base64 decode, image decode, SVG rasterisation. Usually the dominant
724
+ * cost for large images — when a load feels slower than `encodeMs`
725
+ * suggests, this is where the time went.
726
+ */
727
+ decodeMs: number;
728
+ /** Wall-clock time of the whole `compressTexture()` call: decode + CPU
729
+ * mip generation + encode + texture assembly. */
730
+ totalMs: number;
731
+ /** True when the result came from the in-memory transcode cache (the
732
+ * `cache` option) — no decode or encode ran; decodeMs/encodeMs are 0. */
733
+ cacheHit: boolean;
734
+ }
735
+ /**
736
+ * Destroy the WebGPU device and encoders that `compressTexture()` shares
737
+ * across calls (created lazily when neither the `device` nor the `adapter`
738
+ * option is passed). Safe to call at any time — in-flight encodes on the
739
+ * shared device will fail, and the next `compressTexture()` call recreates
740
+ * everything. No-op when nothing is cached.
741
+ */
742
+ declare function releaseSharedGpuResources(): void;
743
+ declare function compressTextureToBytes(source: CompressTextureSource, options?: CompressOptions): Promise<CompressResult>;
744
+
745
+ /**
746
+ * Cap the cache's total compressed payload in bytes (default 256 MiB).
747
+ * Lower it to evict immediately; 0 disables caching entirely.
748
+ */
749
+ declare function setTranscodeCacheLimit(bytes: number): void;
750
+ /** Drop every cached transcode. Textures already built from entries are unaffected. */
751
+ declare function clearTranscodeCache(): void;
752
+
753
+ export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, type BC7EncoderOptions, BC7WebGLEncoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, ETC2Encoder, type EncodeBytesResult, type EncodeCallOptions, type EncodeMipChainResult, type EncodedLevelBytes, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatQuality, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, clearTranscodeCache, compressTextureToBytes, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, releaseSharedGpuResources, selectFormat, selectWebGLFormat, setTranscodeCacheLimit };