gputex 0.2.0 → 0.3.1
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 +106 -20
- package/dist/index.d.ts +36 -9
- package/dist/index.js +601 -71
- package/dist/testing.d.ts +120 -0
- package/dist/testing.js +945 -0
- package/dist/three.d.ts +16 -2
- package/dist/three.js +611 -73
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# GPUtex
|
|
1
|
+
# GPUtex | On-the-fly GPU texture encoding
|
|
2
2
|
|
|
3
3
|
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
⚠️ 100% vibe-coded. The code is completely unreviewed and under-tested. Do not use for anything important.
|
|
6
|
+
|
|
7
|
+
🚀 Used in production on [Mana Blade](https://manablade.com).
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
@@ -16,10 +18,11 @@ bun add gputex
|
|
|
16
18
|
|
|
17
19
|
### Entry points
|
|
18
20
|
|
|
19
|
-
`gputex` ships
|
|
21
|
+
`gputex` ships three entry points:
|
|
20
22
|
|
|
21
23
|
- **`gputex`** — the engine-agnostic core: the `*Encoder` classes, capability / format detection, and mip helpers. Nothing here imports `three`, so it works with Babylon.js, raw WebGPU/WebGL, workers, etc. Encoders return raw compressed block bytes via `encodeToBytes()`.
|
|
22
24
|
- **`gputex/three`** — the Three.js layer. Re-exports the entire core **plus** `compressTexture()`, `GputexLoader`, and `buildCompressedTexture()` / `encodeToTexture()`. This is the only entry that imports `three`.
|
|
25
|
+
- **`gputex/testing`** — the CPU reference encoders/decoders the GPU shaders are validated against. Test-suite material, not runtime API (see [Testing](#testing)).
|
|
23
26
|
|
|
24
27
|
`three` is an **optional** peer dependency (`>=0.170`): install it only if you import `gputex/three`. Pure-core consumers (e.g. Babylon.js) can skip it entirely.
|
|
25
28
|
|
|
@@ -30,13 +33,20 @@ bun add gputex
|
|
|
30
33
|
| **BC7** | 16 (8 bpp) | Color / RGBA on desktop (`texture-compression-bc`) |
|
|
31
34
|
| **BC5** | 16 (8 bpp) | Normal maps — RG only (`texture-compression-bc`) |
|
|
32
35
|
| **ASTC 4x4** | 16 (8 bpp) | Color / RGBA on mobile / iOS (`texture-compression-astc`) |
|
|
33
|
-
| **BC1** | 8 (4 bpp) |
|
|
36
|
+
| **BC1** | 8 (4 bpp) | Opaque color at half BC7's size (opt-in) |
|
|
34
37
|
|
|
35
38
|
Format selection is automatic: BC7/BC5 on desktop, ASTC on mobile, uncompressed RGBA8 fallback otherwise.
|
|
36
39
|
|
|
40
|
+
BC1 is never picked by default — it's half the memory of BC7 but visibly lower
|
|
41
|
+
quality, a trade-off only the application can make. Opt in per-texture with
|
|
42
|
+
`preferredFormat: 'bc1'`: on BC-capable devices the texture encodes as BC1;
|
|
43
|
+
everywhere else (e.g. ASTC-only mobile) selection proceeds as normal. The
|
|
44
|
+
preference is only honoured with `hint: 'color'`, since BC1 can't carry real
|
|
45
|
+
alpha or a normal map.
|
|
46
|
+
|
|
37
47
|
## WebGL fallback
|
|
38
48
|
|
|
39
|
-
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 block encoders as fragment shaders — each 4×4 block is computed in one fragment, written to an `RGBA32UI` render target, and read back. The
|
|
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.
|
|
40
50
|
|
|
41
51
|
The fallback chain is **WebGPU → WebGL2 → uncompressed RGBA8**. The `backend` field on the result (`'webgpu' | 'webgl' | 'none'`) tells you which path ran.
|
|
42
52
|
|
|
@@ -70,14 +80,18 @@ material.map = texture
|
|
|
70
80
|
- **`'fast'` (default)** — a bounding-box endpoint seed plus projection-based
|
|
71
81
|
index assignment (each pixel is projected onto the colinear endpoint line in
|
|
72
82
|
O(1) instead of searching every palette entry) with a single fused
|
|
73
|
-
least-squares refit
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
78
91
|
- **`'high'`** — exhaustive endpoint search (farthest-pair seed, full nearest
|
|
79
|
-
search, p-bit search);
|
|
80
|
-
|
|
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).
|
|
81
95
|
|
|
82
96
|
### `GputexLoader` — Three.js Loader
|
|
83
97
|
|
|
@@ -197,13 +211,85 @@ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
|
|
|
197
211
|
|
|
198
212
|
### `compressTexture` options
|
|
199
213
|
|
|
200
|
-
| Option
|
|
201
|
-
|
|
|
202
|
-
| `hint`
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
214
|
+
| Option | Type | Default | Description |
|
|
215
|
+
| ----------------- | -------------------- | --------- | ------------------------------------------------------------------------------------------------ |
|
|
216
|
+
| `hint` | `TextureHint` | `'color'` | `'color'`, `'colorWithAlpha'`, or `'normal'` |
|
|
217
|
+
| `preferredFormat` | `'bc1'` | — | Prefer BC1 (half of BC7's size) when supported; normal selection otherwise. `hint: 'color'` only |
|
|
218
|
+
| `colorSpace` | `'srgb' \| 'linear'` | `'srgb'` | Use the sRGB or linear variant of the chosen format |
|
|
219
|
+
| `flipY` | `boolean` | `true` | Flip vertically (matches Three.js convention) |
|
|
220
|
+
| `mipmaps` | `boolean` | `false` | Generate full mip chain down to 1x1 |
|
|
221
|
+
| `device` | `GPUDevice` | — | Reuse an existing WebGPU device instead of creating one |
|
|
222
|
+
|
|
223
|
+
## Benchmarks
|
|
224
|
+
|
|
225
|
+
Measured with the repo's GPU test suite (see below) on an Apple Silicon GPU
|
|
226
|
+
(`metal-3`) in Chrome, encoding a 2048×2048 image. **GPU pass** is the compute
|
|
227
|
+
shader alone (WebGPU timestamp queries, median of 20 runs); end-to-end wall
|
|
228
|
+
time adds ~3–4 ms of image upload + result readback regardless of format.
|
|
229
|
+
"Before" is the shader generation prior to the 2026-07 optimization pass
|
|
230
|
+
(projection-based index assignment everywhere, on-the-fly bit packing, an f16
|
|
231
|
+
BC1 fast shader, straight-line block assembly).
|
|
232
|
+
|
|
233
|
+
| Format | Quality | Shader | GPU pass before | GPU pass after | Speedup |
|
|
234
|
+
| -------- | -------------- | ------ | --------------- | -------------- | --------- |
|
|
235
|
+
| BC1 | fast (default) | f16 | — (had no f16) | **0.26 ms** | **2.8×**¹ |
|
|
236
|
+
| BC1 | fast | f32 | 0.72 ms | 0.33 ms | 2.2× |
|
|
237
|
+
| BC5 | fast (default) | f16 | 0.33 ms | **0.20 ms** | 1.7× |
|
|
238
|
+
| BC5 | fast | f32 | 0.79 ms | 0.39 ms | 2.0× |
|
|
239
|
+
| BC7 | fast (default) | f16 | 1.38 ms | **0.52 ms** | 2.7× |
|
|
240
|
+
| BC7 | fast | f32 | 2.65 ms | 1.70 ms | 1.6× |
|
|
241
|
+
| ASTC 4×4 | fast (default) | f16 | 0.33 ms | **0.20 ms** | 1.7× |
|
|
242
|
+
| ASTC 4×4 | fast | f32 | 0.72 ms | 0.66 ms | 1.1× |
|
|
243
|
+
| BC1 | high | f32 | 2.9 ms | 3.0 ms | unchanged |
|
|
244
|
+
| BC5 | high | f32 | 3.4 ms | 3.4 ms | unchanged |
|
|
245
|
+
| BC7 | high | f32 | 14.1 ms | 14.1 ms | unchanged |
|
|
246
|
+
| ASTC 4×4 | high | f32 | 2.6 ms | 2.4 ms | unchanged |
|
|
247
|
+
|
|
248
|
+
¹ vs the old f32 fast shader, which was the only BC1 fast path before. The
|
|
249
|
+
BC1 rows were measured with old and new pipelines interleaved in one session
|
|
250
|
+
(the most noise-robust method); the others are cross-run suite medians.
|
|
251
|
+
|
|
252
|
+
Per-quadrant PSNR on the committed 512² test card is equal to or better than
|
|
253
|
+
the previous fast encoders everywhere (flat tiles bit-identical, gradients
|
|
254
|
+
+0.01 dB, noise −0.01 dB); the `high` paths still match the CPU reference
|
|
255
|
+
encoders. Timestamps are quantised to 100 µs by Chrome, so sub-millisecond
|
|
256
|
+
figures are ±0.05–0.1 ms.
|
|
257
|
+
|
|
258
|
+
## Testing
|
|
259
|
+
|
|
260
|
+
Unit tests (`bun test`) cover the CPU reference encoders and metadata, but the
|
|
261
|
+
WGSL shaders can only be validated on a real GPU. The repo ships a browser
|
|
262
|
+
test + benchmark suite at `example/pages/test.tsx` (logic in
|
|
263
|
+
`example/lib/gpuTestSuite.ts`):
|
|
264
|
+
|
|
265
|
+
```sh
|
|
266
|
+
bun run --filter gputex build # build the library the example consumes
|
|
267
|
+
cd example && bunx next dev # then open http://localhost:3000/test
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The page runs three groups against the live WebGPU device and renders
|
|
271
|
+
PASS/FAIL tables (machine-readable copy on `window.__GPUTEX_TESTS__`):
|
|
272
|
+
|
|
273
|
+
- **Correctness** — `quality: 'high'` output is compared block-by-block
|
|
274
|
+
against the CPU reference encoders (`gputex/testing`), including a
|
|
275
|
+
non-multiple-of-4 image for the clamp-to-edge padding path. Differing blocks
|
|
276
|
+
must have equal decoded error (FP tie-break tolerance) and the aggregate
|
|
277
|
+
PSNR delta must be ≤0.05 dB. Plus determinism checks (same input twice →
|
|
278
|
+
identical bytes).
|
|
279
|
+
- **Quality** — `'fast'` and `'high'` output is CPU-decoded and validated on
|
|
280
|
+
the FULL 512² test cards (every quadrant stresses a different failure mode)
|
|
281
|
+
with two gates, for both the f16 and (force-disabled-f16) f32 shaders:
|
|
282
|
+
aggregate PSNR must beat per-format thresholds pinned ~0.15 dB under the
|
|
283
|
+
measured baseline, and — because a handful of catastrophically wrong blocks
|
|
284
|
+
barely moves aggregate PSNR — the worst _easy_ block (one that `'high'`
|
|
285
|
+
encodes near-losslessly) must not exceed `'high'`'s error by more than a
|
|
286
|
+
small per-format limit.
|
|
287
|
+
- **Performance** — the benchmark table above: wall + GPU-pass time per
|
|
288
|
+
format × quality × shader variant.
|
|
289
|
+
|
|
290
|
+
The `gputex/testing` entry point exports the CPU reference
|
|
291
|
+
encoders/decoders (`encodeBC7Mode6Block`, `decodeASTC4x4Block`, …) so any
|
|
292
|
+
consumer can run the same validation.
|
|
207
293
|
|
|
208
294
|
## Requirements
|
|
209
295
|
|
|
@@ -219,4 +305,4 @@ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
|
|
|
219
305
|
|
|
220
306
|
## Acknowledgements
|
|
221
307
|
|
|
222
|
-
The concept of encoding images on the GPU on the fly via compute shaders was first introduced by [spark.js](https://ludicon.com/sparkjs/)
|
|
308
|
+
The concept of encoding images on the GPU on the fly via compute shaders was first introduced by [spark.js](https://ludicon.com/sparkjs/). GPUtex is not derived from Spark. Its encoders have been implemented from scratch using official references, which have been ported to TypeScript, and then converted to WGSL and GLSL via AI. Spark was never mentioned or used as reference at any point of the implementation, and multiple reviews have found the implementations to be completely independent. For any serious production use of GPU-compressed textures, Spark is the recommended choice over GPUtex.
|
package/dist/index.d.ts
CHANGED
|
@@ -42,13 +42,19 @@ interface EncoderOptions {
|
|
|
42
42
|
device: GPUDevice;
|
|
43
43
|
adapter?: GPUAdapter;
|
|
44
44
|
ownsDevice?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Force the f32 'fast' shader even when the device supports shader-f16.
|
|
47
|
+
* For tests/benchmarks that need to exercise the f32 fallback path on
|
|
48
|
+
* f16-capable hardware. Default false.
|
|
49
|
+
*/
|
|
50
|
+
disableF16?: boolean;
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
|
-
* Encoder quality level. 'fast' (default) uses the
|
|
48
|
-
* shaders —
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
52
58
|
*/
|
|
53
59
|
type EncodeQuality = 'fast' | 'high';
|
|
54
60
|
interface EncodeCallOptions {
|
|
@@ -70,6 +76,14 @@ interface EncodeBytesResult {
|
|
|
70
76
|
paddedHeight: number;
|
|
71
77
|
data: Uint8Array;
|
|
72
78
|
encodeMs: number;
|
|
79
|
+
/**
|
|
80
|
+
* GPU-side compute-pass time in ms, measured with timestamp queries.
|
|
81
|
+
* Present only when the encode was called with `withGpuTime: true` and the
|
|
82
|
+
* device has the 'timestamp-query' feature (requested automatically by
|
|
83
|
+
* `create()` when available). Browsers quantise timestamps (Chrome: 100µs),
|
|
84
|
+
* so treat small values as approximate.
|
|
85
|
+
*/
|
|
86
|
+
gpuMs?: number;
|
|
73
87
|
}
|
|
74
88
|
interface FormatVariant {
|
|
75
89
|
colorSpace: 'srgb' | 'linear';
|
|
@@ -111,12 +125,13 @@ declare abstract class Encoder {
|
|
|
111
125
|
readonly device: GPUDevice;
|
|
112
126
|
readonly adapter?: GPUAdapter;
|
|
113
127
|
readonly ownsDevice: boolean;
|
|
128
|
+
readonly disableF16: boolean;
|
|
114
129
|
protected _module: GPUShaderModule;
|
|
115
130
|
protected _moduleF16: GPUShaderModule | null;
|
|
116
131
|
protected _pipelineF16: GPUComputePipeline | null;
|
|
117
132
|
protected _pipeline: GPUComputePipeline;
|
|
118
133
|
protected _pipelineCache: Map<EncodeQuality, GPUComputePipeline>;
|
|
119
|
-
constructor({ device, adapter, ownsDevice }: EncoderOptions);
|
|
134
|
+
constructor({ device, adapter, ownsDevice, disableF16 }: EncoderOptions);
|
|
120
135
|
protected _buildPipeline(): void;
|
|
121
136
|
/**
|
|
122
137
|
* Pipeline for a given quality level. Encoders that don't declare a
|
|
@@ -142,7 +157,7 @@ declare abstract class Encoder {
|
|
|
142
157
|
/**
|
|
143
158
|
* Optional f16 WGSL for the 'fast' path. Used only when the device reports the
|
|
144
159
|
* `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
|
|
145
|
-
* `'high'` always uses it. Returns null when there's no f16 variant
|
|
160
|
+
* `'high'` always uses it. Returns null when there's no f16 variant.
|
|
146
161
|
*/
|
|
147
162
|
wgslSourceFastF16(): string | null;
|
|
148
163
|
/** Whether the f16 fast path is both available and supported on this device. */
|
|
@@ -164,9 +179,10 @@ declare abstract class Encoder {
|
|
|
164
179
|
* bytes into a `CompressedTexture`; callers targeting another engine feed
|
|
165
180
|
* `data` into that engine's compressed-texture upload directly.
|
|
166
181
|
*/
|
|
167
|
-
encodeToBytes(source: EncoderImageSource, { flipY, quality }?: {
|
|
182
|
+
encodeToBytes(source: EncoderImageSource, { flipY, quality, withGpuTime, }?: {
|
|
168
183
|
flipY?: boolean;
|
|
169
184
|
quality?: EncodeQuality;
|
|
185
|
+
withGpuTime?: boolean;
|
|
170
186
|
}): Promise<EncodeBytesResult>;
|
|
171
187
|
}
|
|
172
188
|
|
|
@@ -178,6 +194,7 @@ declare class BC1Encoder extends Encoder {
|
|
|
178
194
|
get supportsSrgb(): boolean;
|
|
179
195
|
get supportsQuality(): boolean;
|
|
180
196
|
wgslSource(): string;
|
|
197
|
+
wgslSourceFastF16(): string | null;
|
|
181
198
|
gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
|
|
182
199
|
}
|
|
183
200
|
|
|
@@ -360,9 +377,19 @@ declare function detectWebGLCapabilities(gl: ExtensionProvider): WebGLCapabiliti
|
|
|
360
377
|
* • 'normal' — tangent-space normal map (R=x, G=y, z reconstructed).
|
|
361
378
|
*/
|
|
362
379
|
type TextureHint = 'color' | 'colorWithAlpha' | 'normal';
|
|
380
|
+
/**
|
|
381
|
+
* Optional format preference — a wish, not a demand. Applied when the
|
|
382
|
+
* device supports the format and the hint is compatible; otherwise
|
|
383
|
+
* selection proceeds normally (BC7 → ASTC → null). Currently only
|
|
384
|
+
* 'bc1': half the memory of BC7 (0.5 vs 1 byte/pixel) for opaque
|
|
385
|
+
* colour, at visibly lower quality on smooth content.
|
|
386
|
+
*/
|
|
387
|
+
type PreferredFormat = 'bc1';
|
|
363
388
|
interface SelectFormatOptions {
|
|
364
389
|
/** Pick the sRGB variant when the format has one. Default 'srgb'. */
|
|
365
390
|
colorSpace?: 'srgb' | 'linear';
|
|
391
|
+
/** Prefer a specific format when supported. See `PreferredFormat`. */
|
|
392
|
+
preferredFormat?: PreferredFormat;
|
|
366
393
|
}
|
|
367
394
|
interface FormatSelection {
|
|
368
395
|
/** null = no compressed path on this adapter; caller should fall back. */
|
|
@@ -413,4 +440,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
|
|
|
413
440
|
*/
|
|
414
441
|
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
415
442
|
|
|
416
|
-
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 RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat };
|
|
443
|
+
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 RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat };
|