gputex 0.3.4 → 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 two backends are not byte-identical (the WebGPU fast paths use projection assignment and f16 where available), 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,38 +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 principal-axis endpoint seed (per-block
81
- covariance power-iteration — unlike a bbox diagonal it follows
82
- anti-correlated channels, worth **+2–4 dB on normal-map-like content**)
83
- plus projection-based index assignment (each pixel is projected onto the
84
- colinear endpoint line in O(1) instead of searching every palette entry),
85
- and the block bits packed with straight-line constant shifts. The formats
86
- with coarse 4-level palettes (BC1, ASTC) and BC5 add a least-squares
87
- endpoint refit accepted per block only when it lowers the error; BC7's
88
- 16-level mode-6 palette makes the refit redundant on a principal-axis seed
89
- (≤0.05 dB), so its fast path skips it and stays the cheapest per pixel. On
90
- GPUs that report the `shader-f16` feature the whole fast path (all four
91
- formats, BC1 included) runs in f16 — the f32 path is the automatic
92
- fallback. Net vs `'high'` on an Apple GPU: roughly **10–30× faster**
93
- depending on format, for a PSNR cost of **≤0.35 dB on the test cards**
94
- (BC7 within 0.1 dB of `'high'`; ASTC fast actually measures slightly above
95
- it) and up to a few dB on adversarial high-frequency noise, where any
96
- single-line seed trails `'high'`'s exhaustive search. See the benchmark
97
- table below.
98
- - **`'high'`** — exhaustive endpoint search (farthest-pair seed, full nearest
99
- search, p-bit search); matches the CPU reference encoders block-for-block
100
- (byte-identical on >96% of blocks; the rest are equal-error FP tie-breaks,
101
- 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.
102
97
 
103
98
  #### SVG sources
104
99
 
@@ -265,20 +260,16 @@ buffers, bind group) across encodes, so repeated encodes — including mip
265
260
  chains — skip per-call allocation: in an interleaved A/B this cuts BC7
266
261
  end-to-end wall time by ~10% at 512², ~20% at 1024–2048² and ~35% at 4096².
267
262
 
268
- | Format | Quality | Shader | GPU pass |
269
- | -------- | -------------- | ------ | ----------- |
270
- | BC1 | fast (default) | f16 | **0.26 ms** |
271
- | BC1 | fast | f32 | 0.46 ms |
272
- | BC5 | fast (default) | f16 | **0.26 ms** |
273
- | BC5 | fast | f32 | 0.26 ms |
274
- | BC7 | fast (default) | f16 | **0.26 ms** |
275
- | BC7 | fast | f32 | 0.59 ms |
276
- | ASTC 4×4 | fast (default) | f16 | **0.26 ms** |
277
- | ASTC 4×4 | fast | f32 | 0.56 ms |
278
- | BC1 | high | f32 | 2.7 ms |
279
- | BC5 | high | f32 | 1.0 ms |
280
- | BC7 | high | f32 | 10.4 ms |
281
- | ASTC 4×4 | high | f32 | 1.6 ms |
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 |
282
273
 
283
274
  Timestamps are quantised to 100 µs by Chrome and Apple GPU clock states swing
284
275
  timings by ~2×, so sub-millisecond figures are indicative (±0.1 ms); compare
@@ -297,32 +288,31 @@ cd example && bunx next dev # then open http://localhost:3000/test
297
288
  ```
298
289
 
299
290
  A second page, `/bench`, measures median end-to-end `encodeToBytes()` wall
300
- time per format across image sizes (256²–4096²) at fast quality — the
301
- numbers that matter for runtime streaming, where host overhead dominates
302
- small textures (results on `window.__GPUTEX_BENCH__`).
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__`).
303
294
 
304
295
  The page runs three groups against the live WebGPU device and renders
305
296
  PASS/FAIL tables (machine-readable copy on `window.__GPUTEX_TESTS__`):
306
297
 
307
- - **Correctness** — `quality: 'high'` output is compared block-by-block
308
- against the CPU reference encoders (`gputex/testing`), including a
309
- non-multiple-of-4 image for the clamp-to-edge padding path. Differing blocks
310
- must have equal decoded error (FP tie-break tolerance) and the aggregate
311
- PSNR delta must be ≤0.05 dB. Plus determinism checks (same input twice →
312
- identical bytes).
313
- - **Quality** — `'fast'` and `'high'` output is CPU-decoded and validated on
314
- the FULL 512² test cards (every quadrant stresses a different failure mode)
315
- with two gates, for both the f16 and (force-disabled-f16) f32 shaders:
316
- aggregate PSNR must beat per-format thresholds pinned ~0.15 dB under the
317
- measured baseline, and — because a handful of catastrophically wrong blocks
318
- barely moves aggregate PSNR — the worst _easy_ block (one that `'high'`
319
- encodes near-losslessly) must not exceed `'high'`'s error by more than a
320
- 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.
321
310
  - **Performance** — the benchmark table above: wall + GPU-pass time per
322
- format × quality × shader variant.
311
+ format × shader variant.
323
312
 
324
313
  The `gputex/testing` entry point exports the CPU reference
325
- 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
326
316
  consumer can run the same validation.
327
317
 
328
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,11 +116,7 @@ declare abstract class Encoder {
126
116
  readonly adapter?: GPUAdapter;
127
117
  readonly ownsDevice: boolean;
128
118
  readonly disableF16: boolean;
129
- protected _module: GPUShaderModule | null;
130
- protected _moduleF16: GPUShaderModule | null;
131
- protected _pipelineF16: GPUComputePipeline | null;
132
119
  protected _pipeline: GPUComputePipeline;
133
- protected _pipelineCache: Map<EncodeQuality, GPUComputePipeline>;
134
120
  private _cachedSrcTex;
135
121
  private _cachedSrcW;
136
122
  private _cachedSrcH;
@@ -138,18 +124,10 @@ declare abstract class Encoder {
138
124
  private _cachedStaging;
139
125
  private _cachedParams;
140
126
  private _lastParams;
141
- private _bindGroupCache;
127
+ private _cachedBindGroup;
142
128
  private _resourcesBusy;
143
129
  constructor({ device, adapter, ownsDevice, disableF16 }: EncoderOptions);
144
130
  protected _buildPipeline(): void;
145
- /** The f32 module, parsed on first use (see `_module`). */
146
- protected _ensureModule(): GPUShaderModule;
147
- /**
148
- * Pipeline for a given quality level. Encoders that don't declare a
149
- * `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
150
- * argument and reuse the single pipeline. Specialised pipelines are cached.
151
- */
152
- protected _getPipeline(quality: EncodeQuality): GPUComputePipeline;
153
131
  destroy(): void;
154
132
  /** Short lowercase identifier used in GPU object labels and errors. */
155
133
  abstract get label(): string;
@@ -160,20 +138,14 @@ declare abstract class Encoder {
160
138
  /** Whether this format has an sRGB variant. Default true. */
161
139
  get supportsSrgb(): boolean;
162
140
  /**
163
- * Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
164
- * (i.e. has distinct fast/high search paths). Default false (e.g. a stub or a
165
- * format with a single path); BC1/BC5/BC7/ASTC override it to true.
166
- */
167
- get supportsQuality(): boolean;
168
- /**
169
- * Optional f16 WGSL for the 'fast' path. Used only when the device reports the
170
- * `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
171
- * `'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.
172
144
  */
173
145
  wgslSourceFastF16(): string | null;
174
- /** 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. */
175
147
  protected get _useF16(): boolean;
176
- /** WGSL compute-shader source. */
148
+ /** WGSL compute-shader source (f32; the fallback when f16 is unavailable). */
177
149
  abstract wgslSource(): string;
178
150
  /** e.g. 'bc1-rgba-unorm-srgb'. */
179
151
  abstract gpuTextureFormat(opts: FormatVariant): GPUTextureFormat;
@@ -190,9 +162,8 @@ declare abstract class Encoder {
190
162
  * bytes into a `CompressedTexture`; callers targeting another engine feed
191
163
  * `data` into that engine's compressed-texture upload directly.
192
164
  */
193
- encodeToBytes(source: EncoderImageSource, { flipY, quality, withGpuTime, }?: {
165
+ encodeToBytes(source: EncoderImageSource, { flipY, withGpuTime }?: {
194
166
  flipY?: boolean;
195
- quality?: EncodeQuality;
196
167
  withGpuTime?: boolean;
197
168
  }): Promise<EncodeBytesResult>;
198
169
  }
@@ -203,7 +174,6 @@ declare class BC1Encoder extends Encoder {
203
174
  get label(): string;
204
175
  get bytesPerBlock(): number;
205
176
  get supportsSrgb(): boolean;
206
- get supportsQuality(): boolean;
207
177
  wgslSource(): string;
208
178
  wgslSourceFastF16(): string | null;
209
179
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
@@ -215,7 +185,6 @@ declare class BC5Encoder extends Encoder {
215
185
  get label(): string;
216
186
  get bytesPerBlock(): number;
217
187
  get supportsSrgb(): boolean;
218
- get supportsQuality(): boolean;
219
188
  wgslSource(): string;
220
189
  wgslSourceFastF16(): string;
221
190
  gpuTextureFormat(): GPUTextureFormat;
@@ -227,7 +196,6 @@ declare class BC7Encoder extends Encoder {
227
196
  get label(): string;
228
197
  get bytesPerBlock(): number;
229
198
  get supportsSrgb(): boolean;
230
- get supportsQuality(): boolean;
231
199
  wgslSource(): string;
232
200
  wgslSourceFastF16(): string;
233
201
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
@@ -239,7 +207,6 @@ declare class ASTC4x4Encoder extends Encoder {
239
207
  get label(): string;
240
208
  get bytesPerBlock(): number;
241
209
  get supportsSrgb(): boolean;
242
- get supportsQuality(): boolean;
243
210
  wgslSource(): string;
244
211
  wgslSourceFastF16(): string;
245
212
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
@@ -474,4 +441,4 @@ interface RasterizeSvgOptions {
474
441
  */
475
442
  declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
476
443
 
477
- 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 };