gputex 0.3.3 → 0.4.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
@@ -46,14 +46,14 @@ alpha or a normal map.
46
46
 
47
47
  ## WebGL fallback
48
48
 
49
- WebGPU is the primary path. When it's unavailable (older Safari, Firefox without WebGPU, locked-down environments) `compressTexture()` automatically falls back to a **WebGL2** path that runs the same family of block encoders as fragment shaders — each 4×4 block is computed in one fragment, written to an `RGBA32UI` render target, and read back. The WebGPU fast paths have since been rewritten for speed (projection assignment, f16), so the two backends are no longer byte-identical, but they implement the same algorithms at the same quality level and the resulting `CompressedTexture` looks the same under either renderer.
49
+ WebGPU is the primary path. When it's unavailable (older Safari, Firefox without WebGPU, locked-down environments) `compressTexture()` automatically falls back to a **WebGL2** path that runs the same family of block encoders as fragment shaders — each 4×4 block is computed in one fragment, written to an `RGBA32UI` render target, and read back. The two backends are not byte-identical (the WebGPU shaders use f16 where available), but they implement the same algorithms at the same quality level and the resulting `CompressedTexture` looks the same under either renderer.
50
50
 
51
51
  The fallback chain is **WebGPU → WebGL2 → uncompressed RGBA8**. The `backend` field on the result (`'webgpu' | 'webgl' | 'none'`) tells you which path ran.
52
52
 
53
53
  Notes on the WebGL path:
54
54
 
55
55
  - It needs the matching WebGL2 compressed-texture extension to be sampleable: `EXT_texture_compression_bptc` (BC7), `EXT_texture_compression_rgtc` (BC5), `WEBGL_compressed_texture_astc` (ASTC), or `WEBGL_compressed_texture_s3tc` (BC1). Selection mirrors the WebGPU side, with BC1 added as a broadly-available last resort for **opaque** colour when neither BPTC nor ASTC is present.
56
- - It always uses the **fast** encoders — the `quality: 'high'` option and the `device` / `adapter` options apply to the WebGPU path only.
56
+ - The `device` / `adapter` options apply to the WebGPU path only.
57
57
  - All encoding happens on one shared, off-screen WebGL2 context; nothing is drawn to a visible canvas.
58
58
 
59
59
  ## Usage
@@ -67,31 +67,33 @@ const { texture, format } = await compressTexture('/cobblestone.avif', {
67
67
  hint: 'color', // 'color' | 'colorWithAlpha' | 'normal'
68
68
  colorSpace: 'srgb',
69
69
  mipmaps: true,
70
- quality: 'fast', // 'fast' (default) | 'high'
71
70
  })
72
71
 
73
72
  material.map = texture
74
73
  ```
75
74
 
76
- #### Quality
77
-
78
- `quality` trades encode speed against compression accuracy:
79
-
80
- - **`'fast'` (default)** — a bounding-box endpoint seed plus projection-based
81
- index assignment (each pixel is projected onto the colinear endpoint line in
82
- O(1) instead of searching every palette entry) with a single fused
83
- least-squares refit (accepted per block only when it lowers the error), and
84
- the block bits packed with straight-line constant shifts. On GPUs that
85
- report the `shader-f16` feature the whole fast path (all four formats, BC1
86
- included) runs in f16 — the f32 path is the automatic fallback. Net vs
87
- `'high'` on an Apple GPU: roughly **10–30× faster** depending on format,
88
- for a PSNR cost of **≤0.65 dB on smooth/flat content** (imperceptible) and
89
- up to a few dB on adversarial high-frequency noise, where the bbox seed
90
- trails `'high'`'s exhaustive search. See the benchmark table below.
91
- - **`'high'`** — exhaustive endpoint search (farthest-pair seed, full nearest
92
- search, p-bit search); matches the CPU reference encoders block-for-block
93
- (byte-identical on >96% of blocks; the rest are equal-error FP tie-breaks,
94
- enforced by the GPU test suite).
75
+ #### The encoding algorithm
76
+
77
+ There is a single encode mode, built to be both fast and high quality: a
78
+ principal-axis endpoint seed (per-block covariance power-iteration — unlike
79
+ a bbox diagonal it follows anti-correlated channels, worth **+2–4 dB on
80
+ normal-map-like content**) plus projection-based index assignment (each
81
+ pixel is projected onto the colinear endpoint line in O(1) instead of
82
+ searching every palette entry), and the block bits packed with
83
+ straight-line constant shifts. The formats with coarse 4-level palettes
84
+ (BC1, ASTC) add up to two least-squares endpoint refit rounds accepted per
85
+ block only when they lower the error, and BC5 one; BC7's 16-level mode-6
86
+ palette makes the refit redundant on a principal-axis seed (≤0.05 dB), so
87
+ it skips it and stays the cheapest per pixel. On GPUs that report the
88
+ `shader-f16` feature everything runs in f16 — the f32 shaders are the
89
+ automatic fallback.
90
+
91
+ On the repo's test cards this lands within **≤0.1 dB** of the exhaustive
92
+ per-block reference encoders (BC5 matches the reference exactly; ASTC and
93
+ BC1-on-normal-maps measure slightly above it), trailing only on adversarial
94
+ high-frequency noise, where any single-line seed loses to an exhaustive
95
+ search — while encoding an order of magnitude faster. See the benchmark
96
+ table below.
95
97
 
96
98
  #### SVG sources
97
99
 
@@ -252,35 +254,26 @@ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
252
254
  Measured with the repo's GPU test suite (see below) on an Apple Silicon GPU
253
255
  (`metal-3`) in Chrome, encoding a 2048×2048 image. **GPU pass** is the compute
254
256
  shader alone (WebGPU timestamp queries, median of 20 runs); end-to-end wall
255
- time adds ~3–4 ms of image upload + result readback regardless of format.
256
- "Before" is the shader generation prior to the 2026-07 optimization pass
257
- (projection-based index assignment everywhere, on-the-fly bit packing, an f16
258
- BC1 fast shader, straight-line block assembly).
259
-
260
- | Format | Quality | Shader | GPU pass before | GPU pass after | Speedup |
261
- | -------- | -------------- | ------ | --------------- | -------------- | --------- |
262
- | BC1 | fast (default) | f16 | — (had no f16) | **0.26 ms** | **2.8×**¹ |
263
- | BC1 | fast | f32 | 0.72 ms | 0.33 ms | 2.2× |
264
- | BC5 | fast (default) | f16 | 0.33 ms | **0.20 ms** | 1.7× |
265
- | BC5 | fast | f32 | 0.79 ms | 0.39 ms | 2.0× |
266
- | BC7 | fast (default) | f16 | 1.38 ms | **0.52 ms** | 2.7× |
267
- | BC7 | fast | f32 | 2.65 ms | 1.70 ms | 1.6× |
268
- | ASTC 4×4 | fast (default) | f16 | 0.33 ms | **0.20 ms** | 1.7× |
269
- | ASTC 4×4 | fast | f32 | 0.72 ms | 0.66 ms | 1.1× |
270
- | BC1 | high | f32 | 2.9 ms | 3.0 ms | unchanged |
271
- | BC5 | high | f32 | 3.4 ms | 3.4 ms | unchanged |
272
- | BC7 | high | f32 | 14.1 ms | 14.1 ms | unchanged |
273
- | ASTC 4×4 | high | f32 | 2.6 ms | 2.4 ms | unchanged |
274
-
275
- ¹ vs the old f32 fast shader, which was the only BC1 fast path before. The
276
- BC1 rows were measured with old and new pipelines interleaved in one session
277
- (the most noise-robust method); the others are cross-run suite medians.
278
-
279
- Per-quadrant PSNR on the committed 512² test card is equal to or better than
280
- the previous fast encoders everywhere (flat tiles bit-identical, gradients
281
- +0.01 dB, noise −0.01 dB); the `high` paths still match the CPU reference
282
- encoders. Timestamps are quantised to 100 µs by Chrome, so sub-millisecond
283
- figures are ±0.05–0.1 ms.
257
+ time adds ~2–4 ms of image upload + result readback regardless of format.
258
+ Each encoder caches its GPU resources (source texture, output/staging
259
+ buffers, bind group) across encodes, so repeated encodes — including mip
260
+ chains — skip per-call allocation: in an interleaved A/B this cuts BC7
261
+ end-to-end wall time by ~10% at 512², ~20% at 1024–2048² and ~35% at 4096².
262
+
263
+ | Format | Shader | GPU pass |
264
+ | -------- | ------------- | ----------- |
265
+ | BC1 | f16 (default) | **0.26 ms** |
266
+ | BC1 | f32 | 0.46 ms |
267
+ | BC5 | f16 (default) | **0.26 ms** |
268
+ | BC5 | f32 | 0.26 ms |
269
+ | BC7 | f16 (default) | **0.26 ms** |
270
+ | BC7 | f32 | 0.59 ms |
271
+ | ASTC 4×4 | f16 (default) | **0.26 ms** |
272
+ | ASTC 4×4 | f32 | 0.56 ms |
273
+
274
+ Timestamps are quantised to 100 µs by Chrome and Apple GPU clock states swing
275
+ timings by ~2×, so sub-millisecond figures are indicative (±0.1 ms); compare
276
+ variants only within a single session.
284
277
 
285
278
  ## Testing
286
279
 
@@ -294,28 +287,32 @@ bun run --filter gputex build # build the library the example consumes
294
287
  cd example && bunx next dev # then open http://localhost:3000/test
295
288
  ```
296
289
 
290
+ A second page, `/bench`, measures median end-to-end `encodeToBytes()` wall
291
+ time per format across image sizes (256²–4096²) — the numbers that matter
292
+ for runtime streaming, where host overhead dominates small textures
293
+ (results on `window.__GPUTEX_BENCH__`).
294
+
297
295
  The page runs three groups against the live WebGPU device and renders
298
296
  PASS/FAIL tables (machine-readable copy on `window.__GPUTEX_TESTS__`):
299
297
 
300
- - **Correctness** — `quality: 'high'` output is compared block-by-block
301
- against the CPU reference encoders (`gputex/testing`), including a
302
- non-multiple-of-4 image for the clamp-to-edge padding path. Differing blocks
303
- must have equal decoded error (FP tie-break tolerance) and the aggregate
304
- PSNR delta must be ≤0.05 dB. Plus determinism checks (same input twice →
305
- identical bytes).
306
- - **Quality** — `'fast'` and `'high'` output is CPU-decoded and validated on
307
- the FULL 512² test cards (every quadrant stresses a different failure mode)
308
- with two gates, for both the f16 and (force-disabled-f16) f32 shaders:
309
- aggregate PSNR must beat per-format thresholds pinned ~0.15 dB under the
310
- measured baseline, and — because a handful of catastrophically wrong blocks
311
- barely moves aggregate PSNR — the worst _easy_ block (one that `'high'`
312
- encodes near-losslessly) must not exceed `'high'`'s error by more than a
313
- small per-format limit.
298
+ - **Correctness** — determinism (same input twice → identical bytes) and the
299
+ clamp-to-edge padding path: a non-multiple-of-4 image must land within a
300
+ couple of dB of the exhaustive CPU reference encode (`gputex/testing`) — a
301
+ padding bug craters it.
302
+ - **Quality** — GPU output is CPU-decoded and validated on the FULL 1024²
303
+ test cards (every tile stresses a different failure mode) with two gates,
304
+ for both the f16 and (force-disabled-f16) f32 shaders: aggregate PSNR must
305
+ beat per-format thresholds pinned ~0.15 dB under the measured baseline,
306
+ and — because a handful of catastrophically wrong blocks barely moves
307
+ aggregate PSNR — the worst _easy_ block (one the exhaustive CPU reference
308
+ encodes near-losslessly) must not exceed the reference's error by more
309
+ than a small per-format limit.
314
310
  - **Performance** — the benchmark table above: wall + GPU-pass time per
315
- format × quality × shader variant.
311
+ format × shader variant.
316
312
 
317
313
  The `gputex/testing` entry point exports the CPU reference
318
- encoders/decoders (`encodeBC7Mode6Block`, `decodeASTC4x4Block`, …) so any
314
+ encoders/decoders (`encodeBC7Mode6Block`, `decodeASTC4x4Block`, …) — the
315
+ exhaustive per-block yardstick the GPU shaders are gated against — so any
319
316
  consumer can run the same validation.
320
317
 
321
318
  ## Requirements
package/dist/index.d.ts CHANGED
@@ -49,19 +49,9 @@ interface EncoderOptions {
49
49
  */
50
50
  disableF16?: boolean;
51
51
  }
52
- /**
53
- * Encoder quality level. 'fast' (default) uses the projection-based paths in
54
- * the shaders — an order of magnitude faster for a ≤0.65 dB PSNR cost. 'high'
55
- * runs the exhaustive search, matching the CPU reference encoders
56
- * block-for-block (byte-identical up to FP tie-breaks with equal error).
57
- * BC1's 'high' adds a principal-axis endpoint seed and iterative refit.
58
- */
59
- type EncodeQuality = 'fast' | 'high';
60
52
  interface EncodeCallOptions {
61
53
  /** Tags the output color space. Forced 'linear' for encoders with supportsSrgb=false. */
62
54
  colorSpace?: 'srgb' | 'linear';
63
- /** Encode quality / speed trade-off. Default 'fast'. */
64
- quality?: EncodeQuality;
65
55
  }
66
56
  /**
67
57
  * Result of a raw bytes-only encode. This is the encoder's native output: the
@@ -126,19 +116,18 @@ declare abstract class Encoder {
126
116
  readonly adapter?: GPUAdapter;
127
117
  readonly ownsDevice: boolean;
128
118
  readonly disableF16: boolean;
129
- protected _module: GPUShaderModule;
130
- protected _moduleF16: GPUShaderModule | null;
131
- protected _pipelineF16: GPUComputePipeline | null;
132
119
  protected _pipeline: GPUComputePipeline;
133
- protected _pipelineCache: Map<EncodeQuality, GPUComputePipeline>;
120
+ private _cachedSrcTex;
121
+ private _cachedSrcW;
122
+ private _cachedSrcH;
123
+ private _cachedDst;
124
+ private _cachedStaging;
125
+ private _cachedParams;
126
+ private _lastParams;
127
+ private _cachedBindGroup;
128
+ private _resourcesBusy;
134
129
  constructor({ device, adapter, ownsDevice, disableF16 }: EncoderOptions);
135
130
  protected _buildPipeline(): void;
136
- /**
137
- * Pipeline for a given quality level. Encoders that don't declare a
138
- * `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
139
- * argument and reuse the single pipeline. Specialised pipelines are cached.
140
- */
141
- protected _getPipeline(quality: EncodeQuality): GPUComputePipeline;
142
131
  destroy(): void;
143
132
  /** Short lowercase identifier used in GPU object labels and errors. */
144
133
  abstract get label(): string;
@@ -149,20 +138,14 @@ declare abstract class Encoder {
149
138
  /** Whether this format has an sRGB variant. Default true. */
150
139
  get supportsSrgb(): boolean;
151
140
  /**
152
- * Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
153
- * (i.e. has distinct fast/high search paths). Default false (e.g. a stub or a
154
- * format with a single path); BC1/BC5/BC7/ASTC override it to true.
155
- */
156
- get supportsQuality(): boolean;
157
- /**
158
- * Optional f16 WGSL for the 'fast' path. Used only when the device reports the
159
- * `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
160
- * `'high'` always uses it. Returns null when there's no f16 variant.
141
+ * Optional f16 WGSL variant. Used only when the device reports the
142
+ * `shader-f16` feature; the format's f32 `wgslSource()` is the automatic
143
+ * fallback. Returns null when there's no f16 variant.
161
144
  */
162
145
  wgslSourceFastF16(): string | null;
163
- /** Whether the f16 fast path is both available and supported on this device. */
146
+ /** Whether the f16 shader is both available and supported on this device. */
164
147
  protected get _useF16(): boolean;
165
- /** WGSL compute-shader source. */
148
+ /** WGSL compute-shader source (f32; the fallback when f16 is unavailable). */
166
149
  abstract wgslSource(): string;
167
150
  /** e.g. 'bc1-rgba-unorm-srgb'. */
168
151
  abstract gpuTextureFormat(opts: FormatVariant): GPUTextureFormat;
@@ -179,9 +162,8 @@ declare abstract class Encoder {
179
162
  * bytes into a `CompressedTexture`; callers targeting another engine feed
180
163
  * `data` into that engine's compressed-texture upload directly.
181
164
  */
182
- encodeToBytes(source: EncoderImageSource, { flipY, quality, withGpuTime, }?: {
165
+ encodeToBytes(source: EncoderImageSource, { flipY, withGpuTime }?: {
183
166
  flipY?: boolean;
184
- quality?: EncodeQuality;
185
167
  withGpuTime?: boolean;
186
168
  }): Promise<EncodeBytesResult>;
187
169
  }
@@ -192,7 +174,6 @@ declare class BC1Encoder extends Encoder {
192
174
  get label(): string;
193
175
  get bytesPerBlock(): number;
194
176
  get supportsSrgb(): boolean;
195
- get supportsQuality(): boolean;
196
177
  wgslSource(): string;
197
178
  wgslSourceFastF16(): string | null;
198
179
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
@@ -204,7 +185,6 @@ declare class BC5Encoder extends Encoder {
204
185
  get label(): string;
205
186
  get bytesPerBlock(): number;
206
187
  get supportsSrgb(): boolean;
207
- get supportsQuality(): boolean;
208
188
  wgslSource(): string;
209
189
  wgslSourceFastF16(): string;
210
190
  gpuTextureFormat(): GPUTextureFormat;
@@ -216,7 +196,6 @@ declare class BC7Encoder extends Encoder {
216
196
  get label(): string;
217
197
  get bytesPerBlock(): number;
218
198
  get supportsSrgb(): boolean;
219
- get supportsQuality(): boolean;
220
199
  wgslSource(): string;
221
200
  wgslSourceFastF16(): string;
222
201
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
@@ -228,7 +207,6 @@ declare class ASTC4x4Encoder extends Encoder {
228
207
  get label(): string;
229
208
  get bytesPerBlock(): number;
230
209
  get supportsSrgb(): boolean;
231
- get supportsQuality(): boolean;
232
210
  wgslSource(): string;
233
211
  wgslSourceFastF16(): string;
234
212
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
@@ -463,4 +441,4 @@ interface RasterizeSvgOptions {
463
441
  */
464
442
  declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
465
443
 
466
- export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, 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, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };
444
+ export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type EncodeBytesResult, type EncodeCallOptions, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, 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, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };