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 +63 -37
- package/dist/index.d.ts +153 -8
- package/dist/index.js +1038 -596
- package/dist/testing.d.ts +8 -5
- package/dist/testing.js +190 -34
- package/dist/three.d.ts +9 -130
- package/dist/three.js +659 -724
- package/package.json +1 -1
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.
|
|
87
|
-
|
|
88
|
-
block only when they lower the error
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
113
|
-
per-block reference encoders (
|
|
114
|
-
BC1
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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.
|
|
346
|
-
| BC1 | f32 | 0.
|
|
347
|
-
| BC5 | f16 (default) | **0.
|
|
348
|
-
| BC5 | f32 | 0.
|
|
349
|
-
| BC7 | f16 (default) | **0.
|
|
350
|
-
| BC7 | f32 | 0.
|
|
351
|
-
| ASTC 4×4 | f16 (default) | **0.
|
|
352
|
-
| ASTC 4×4 | f32 | 0.
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
365
|
-
timings by ~2
|
|
366
|
-
|
|
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
|
|
168
|
+
private _cachedStagings;
|
|
169
169
|
private _cachedParams;
|
|
170
|
-
private
|
|
171
|
-
private
|
|
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
|
|
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
|
-
/**
|
|
301
|
-
*
|
|
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
|
-
|
|
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 };
|