gputex 0.0.5 → 0.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # GPUtex
2
2
 
3
- Runtime GPU texture compression via WebGPU compute shaders. 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.
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
  ⚠️: This is 100% vibe-coded. The code is completely unreviewed, under-tested, it will probably crash on many devices, and this library is very likely to go unmaintained. Do not use for anything important.
6
6
 
@@ -27,6 +27,18 @@ bun add gputex
27
27
 
28
28
  Format selection is automatic: BC7/BC5 on desktop, ASTC on mobile, uncompressed RGBA8 fallback otherwise.
29
29
 
30
+ ## WebGL fallback
31
+
32
+ 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 output bytes are identical to the WebGPU encoders, so the resulting `CompressedTexture` looks the same under either renderer.
33
+
34
+ The fallback chain is **WebGPU → WebGL2 → uncompressed RGBA8**. The `backend` field on the result (`'webgpu' | 'webgl' | 'none'`) tells you which path ran.
35
+
36
+ Notes on the WebGL path:
37
+
38
+ - 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.
39
+ - It always uses the **fast** encoders — the `quality: 'high'` option and the `device` / `adapter` options apply to the WebGPU path only.
40
+ - All encoding happens on one shared, off-screen WebGL2 context; nothing is drawn to a visible canvas.
41
+
30
42
  ## Usage
31
43
 
32
44
  ### `compressTexture` — direct API
@@ -38,11 +50,28 @@ const { texture, format } = await compressTexture('/cobblestone.avif', {
38
50
  hint: 'color', // 'color' | 'colorWithAlpha' | 'normal'
39
51
  colorSpace: 'srgb',
40
52
  mipmaps: true,
53
+ quality: 'fast', // 'fast' (default) | 'high'
41
54
  })
42
55
 
43
56
  material.map = texture
44
57
  ```
45
58
 
59
+ #### Quality
60
+
61
+ `quality` trades encode speed against compression accuracy:
62
+
63
+ - **`'fast'` (default)** — a bounding-box endpoint seed plus projection-based
64
+ index assignment (each pixel is projected onto the colinear endpoint line in
65
+ O(1) instead of searching every palette entry) with a single fused
66
+ least-squares refit. On GPUs that report the `shader-f16` feature the whole
67
+ fast path runs in f16 (≈2× on Apple) — the f32 path is the automatic
68
+ fallback. Net vs `'high'` on an Apple GPU: **BC7 ~50×**, **ASTC ~9×**,
69
+ **BC5 ~5×** faster, for a PSNR cost of **≤0.45 dB** (imperceptible). BC1 is
70
+ single-pass and unaffected.
71
+ - **`'high'`** — exhaustive endpoint search (farthest-pair seed, full nearest
72
+ search, p-bit search); output is byte-for-byte identical to the CPU reference
73
+ encoders.
74
+
46
75
  ### `GputexLoader` — Three.js Loader
47
76
 
48
77
  ```ts
@@ -154,9 +183,11 @@ encoder.destroy()
154
183
 
155
184
  ## Requirements
156
185
 
157
- - A browser with WebGPU support
158
- - `texture-compression-bc` (desktop) or `texture-compression-astc` (mobile) for compressed output
159
- - Falls back to uncompressed RGBA8 when neither is available
186
+ - WebGPU (primary) **or** WebGL2 (fallback) — almost every current browser has at least one
187
+ - A compressed-texture capability for compressed output:
188
+ - WebGPU: `texture-compression-bc` (desktop) or `texture-compression-astc` (mobile)
189
+ - WebGL2: `EXT_texture_compression_bptc` / `_rgtc`, `WEBGL_compressed_texture_astc`, or `WEBGL_compressed_texture_s3tc`
190
+ - Falls back to uncompressed RGBA8 when no compressed format is available on either backend
160
191
 
161
192
  ## Device-specific workarounds
162
193
 
package/dist/index.d.ts CHANGED
@@ -45,9 +45,18 @@ interface EncoderOptions {
45
45
  adapter?: GPUAdapter;
46
46
  ownsDevice?: boolean;
47
47
  }
48
+ /**
49
+ * Encoder quality level. 'fast' (default) uses the cheaper search paths in the
50
+ * shaders — measured ~2–4× faster for ≤0.36 dB PSNR. 'high' runs the exhaustive
51
+ * search, producing output byte-identical to the CPU reference encoders.
52
+ * No effect on BC1 (already single-pass; both levels are identical).
53
+ */
54
+ type EncodeQuality = 'fast' | 'high';
48
55
  interface EncodeCallOptions {
49
56
  /** Tags the output color space. Forced 'linear' for encoders with supportsSrgb=false. */
50
57
  colorSpace?: 'srgb' | 'linear';
58
+ /** Encode quality / speed trade-off. Default 'fast'. */
59
+ quality?: EncodeQuality;
51
60
  }
52
61
  interface EncodeResult {
53
62
  width: number;
@@ -110,9 +119,18 @@ declare abstract class Encoder {
110
119
  readonly adapter?: GPUAdapter;
111
120
  readonly ownsDevice: boolean;
112
121
  protected _module: GPUShaderModule;
122
+ protected _moduleF16: GPUShaderModule | null;
123
+ protected _pipelineF16: GPUComputePipeline | null;
113
124
  protected _pipeline: GPUComputePipeline;
125
+ protected _pipelineCache: Map<EncodeQuality, GPUComputePipeline>;
114
126
  constructor({ device, adapter, ownsDevice }: EncoderOptions);
115
127
  protected _buildPipeline(): void;
128
+ /**
129
+ * Pipeline for a given quality level. Encoders that don't declare a
130
+ * `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
131
+ * argument and reuse the single pipeline. Specialised pipelines are cached.
132
+ */
133
+ protected _getPipeline(quality: EncodeQuality): GPUComputePipeline;
116
134
  destroy(): void;
117
135
  /** Short lowercase identifier used in GPU object labels and errors. */
118
136
  abstract get label(): string;
@@ -122,6 +140,20 @@ declare abstract class Encoder {
122
140
  get workgroupSize(): readonly [number, number, number];
123
141
  /** Whether this format has an sRGB variant. Default true. */
124
142
  get supportsSrgb(): boolean;
143
+ /**
144
+ * Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
145
+ * (i.e. has distinct fast/high search paths). BC1 is already single-pass and
146
+ * leaves this false; BC5/BC7/ASTC override it to true.
147
+ */
148
+ get supportsQuality(): boolean;
149
+ /**
150
+ * Optional f16 WGSL for the 'fast' path. Used only when the device reports the
151
+ * `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
152
+ * `'high'` always uses it. Returns null when there's no f16 variant (BC1).
153
+ */
154
+ wgslSourceFastF16(): string | null;
155
+ /** Whether the f16 fast path is both available and supported on this device. */
156
+ protected get _useF16(): boolean;
125
157
  /** WGSL compute-shader source. */
126
158
  abstract wgslSource(): string;
127
159
  /** e.g. 'bc1-rgba-unorm-srgb'. */
@@ -134,7 +166,7 @@ declare abstract class Encoder {
134
166
  * whether the result can actually be sampled.
135
167
  */
136
168
  get supportsSampling(): boolean;
137
- encode(source: EncoderImageSource, { colorSpace }?: EncodeCallOptions): Promise<EncodeResult>;
169
+ encode(source: EncoderImageSource, { colorSpace, quality }?: EncodeCallOptions): Promise<EncodeResult>;
138
170
  /**
139
171
  * Encode one image source to raw compressed bytes, skipping the
140
172
  * `CompressedTexture` wrap. Used by the public `encode()` above and by
@@ -145,8 +177,9 @@ declare abstract class Encoder {
145
177
  * encoder boundary. Still safe to call from outside — it just does
146
178
  * less work than `encode()` and the caller assembles the texture.
147
179
  */
148
- encodeToBytes(source: EncoderImageSource, { flipY }?: {
180
+ encodeToBytes(source: EncoderImageSource, { flipY, quality }?: {
149
181
  flipY?: boolean;
182
+ quality?: EncodeQuality;
150
183
  }): Promise<EncodeBytesResult>;
151
184
  /**
152
185
  * Assemble a `CompressedTexture` from pre-encoded mip levels. Called
@@ -178,7 +211,9 @@ declare class BC5Encoder extends Encoder {
178
211
  get label(): string;
179
212
  get bytesPerBlock(): number;
180
213
  get supportsSrgb(): boolean;
214
+ get supportsQuality(): boolean;
181
215
  wgslSource(): string;
216
+ wgslSourceFastF16(): string;
182
217
  gpuTextureFormat(): GPUTextureFormat;
183
218
  threeTextureFormat(): CompressedPixelFormat;
184
219
  }
@@ -189,7 +224,9 @@ declare class BC7Encoder extends Encoder {
189
224
  get label(): string;
190
225
  get bytesPerBlock(): number;
191
226
  get supportsSrgb(): boolean;
227
+ get supportsQuality(): boolean;
192
228
  wgslSource(): string;
229
+ wgslSourceFastF16(): string;
193
230
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
194
231
  threeTextureFormat(): CompressedPixelFormat;
195
232
  }
@@ -200,11 +237,170 @@ declare class ASTC4x4Encoder extends Encoder {
200
237
  get label(): string;
201
238
  get bytesPerBlock(): number;
202
239
  get supportsSrgb(): boolean;
240
+ get supportsQuality(): boolean;
203
241
  wgslSource(): string;
242
+ wgslSourceFastF16(): string;
204
243
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
205
244
  threeTextureFormat(): CompressedPixelFormat;
206
245
  }
207
246
 
247
+ /** One encoded mip level. The fields both encoder backends already produce. */
248
+ interface EncodedLevel {
249
+ /** Logical (pre-padding) dimensions, surfaced on the texture's userData. */
250
+ width: number;
251
+ height: number;
252
+ /** Block-aligned dimensions the compressed `data` actually covers. */
253
+ paddedWidth: number;
254
+ paddedHeight: number;
255
+ data: Uint8Array;
256
+ }
257
+
258
+ /** Raw RGBA8 pixel data (e.g. a CPU-generated mip level). */
259
+ interface RawPixelSource {
260
+ data: ArrayBufferView;
261
+ width: number;
262
+ height: number;
263
+ }
264
+ /** Anything the WebGL encoders can upload as the source image. */
265
+ type WebGLEncoderImageSource = ImageBitmap | ImageData | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | RawPixelSource;
266
+ interface WebGLEncodeBytesResult {
267
+ width: number;
268
+ height: number;
269
+ paddedWidth: number;
270
+ paddedHeight: number;
271
+ data: Uint8Array;
272
+ encodeMs: number;
273
+ }
274
+ interface WebGLEncoderOptions {
275
+ gl: WebGL2RenderingContext;
276
+ }
277
+ /**
278
+ * Constructor shape for the concrete subclasses; lets the static `create()`
279
+ * narrow its return type to the subclass (as the WebGPU `EncoderConstructor`
280
+ * does).
281
+ */
282
+ type WebGLEncoderConstructor<T extends WebGLBlockEncoder = WebGLBlockEncoder> = {
283
+ new (opts: WebGLEncoderOptions): T;
284
+ create(gl?: WebGL2RenderingContext | null): T;
285
+ };
286
+ declare abstract class WebGLBlockEncoder {
287
+ /**
288
+ * Create an encoder on the shared process-wide context (or a caller-supplied
289
+ * one). Throws when WebGL2 is unavailable. The `this:` annotation lets
290
+ * `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
291
+ */
292
+ static create<T extends WebGLBlockEncoder>(this: WebGLEncoderConstructor<T>, gl?: WebGL2RenderingContext | null): T;
293
+ readonly gl: WebGL2RenderingContext;
294
+ protected _program: WebGLProgram;
295
+ protected _vao: WebGLVertexArrayObject;
296
+ protected _uSrc: WebGLUniformLocation | null;
297
+ protected _uSrcSize: WebGLUniformLocation | null;
298
+ protected _uFlipY: WebGLUniformLocation | null;
299
+ constructor({ gl }: WebGLEncoderOptions);
300
+ /** Short lowercase identifier for labels / errors. */
301
+ abstract get label(): string;
302
+ /** 8 for BC1, 16 for BC5/BC7/ASTC 4×4. */
303
+ abstract get bytesPerBlock(): number;
304
+ /** Whether this format has an sRGB variant (false for BC5). */
305
+ abstract get supportsSrgb(): boolean;
306
+ /** GLSL ES 3.00 fragment-shader source. */
307
+ abstract fragSource(): string;
308
+ /** Three.js `CompressedPixelFormat` constant; sRGB is carried by colorSpace. */
309
+ abstract threeTextureFormat(): CompressedPixelFormat;
310
+ protected _buildProgram(): void;
311
+ /** Release the GL program + VAO. The shared context itself is left intact. */
312
+ destroy(): void;
313
+ /**
314
+ * Upload the source image to a freshly created RGBA8 texture bound on unit 0.
315
+ * Raw pixel sources (ImageData / mip levels) go through the typed-array
316
+ * overload; DOM sources (ImageBitmap / canvas / image) through the element
317
+ * overload. No flip / premultiply / colour conversion — flipY is applied in
318
+ * the shader so each mip level flips by its own height.
319
+ */
320
+ protected _uploadSource(source: WebGLEncoderImageSource, width: number, height: number): WebGLTexture;
321
+ /**
322
+ * Encode one image source to raw compressed bytes. `flipY` samples the source
323
+ * bottom-up (matching Three.js's convention) and is applied in the shader;
324
+ * the high-level mipped path bakes the flip into level 0 and passes false.
325
+ */
326
+ encodeToBytes(source: WebGLEncoderImageSource, { flipY }?: {
327
+ flipY?: boolean;
328
+ }): WebGLEncodeBytesResult;
329
+ /** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
330
+ buildMippedTexture(levels: readonly EncodedLevel[], { colorSpace }?: {
331
+ colorSpace?: 'srgb' | 'linear';
332
+ }): CompressedTexture;
333
+ }
334
+
335
+ declare class BC1WebGLEncoder extends WebGLBlockEncoder {
336
+ get label(): string;
337
+ get bytesPerBlock(): number;
338
+ get supportsSrgb(): boolean;
339
+ fragSource(): string;
340
+ threeTextureFormat(): CompressedPixelFormat;
341
+ }
342
+
343
+ declare class BC5WebGLEncoder extends WebGLBlockEncoder {
344
+ get label(): string;
345
+ get bytesPerBlock(): number;
346
+ get supportsSrgb(): boolean;
347
+ fragSource(): string;
348
+ threeTextureFormat(): CompressedPixelFormat;
349
+ }
350
+
351
+ declare class BC7WebGLEncoder extends WebGLBlockEncoder {
352
+ get label(): string;
353
+ get bytesPerBlock(): number;
354
+ get supportsSrgb(): boolean;
355
+ fragSource(): string;
356
+ threeTextureFormat(): CompressedPixelFormat;
357
+ }
358
+
359
+ declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
360
+ get label(): string;
361
+ get bytesPerBlock(): number;
362
+ get supportsSrgb(): boolean;
363
+ fragSource(): string;
364
+ threeTextureFormat(): CompressedPixelFormat;
365
+ }
366
+
367
+ /**
368
+ * Create a fresh WebGL2 context backed by an off-screen 1×1 canvas. Returns
369
+ * null when neither `OffscreenCanvas` nor `document` is available (e.g. a
370
+ * non-DOM worker without OffscreenCanvas) or when the platform has no WebGL2.
371
+ */
372
+ declare function createWebGLContext(): WebGL2RenderingContext | null;
373
+ /**
374
+ * Lazily-created context shared across the high-level fallback path. Re-created
375
+ * if the previous one was lost (tab backgrounding, GPU reset). Returns null
376
+ * when WebGL2 is unavailable on this platform.
377
+ */
378
+ declare function getSharedWebGLContext(): WebGL2RenderingContext | null;
379
+ /** True when a WebGL2 context can be created on this platform. */
380
+ declare function isWebGLAvailable(): boolean;
381
+
382
+ /**
383
+ * Minimal structural type for the extension source: only `getExtension` is
384
+ * touched, so tests can pass a `{ getExtension }` stub instead of a real
385
+ * `WebGL2RenderingContext`.
386
+ */
387
+ interface ExtensionProvider {
388
+ getExtension(name: string): unknown;
389
+ }
390
+ interface WebGLCapabilities {
391
+ /** EXT_texture_compression_bptc → BC7 (BPTC). */
392
+ bptc: boolean;
393
+ /** EXT_texture_compression_rgtc → BC5 (RGTC2). */
394
+ rgtc: boolean;
395
+ /** WEBGL_compressed_texture_s3tc → BC1 (DXT1). */
396
+ s3tc: boolean;
397
+ /** WEBGL_compressed_texture_s3tc_srgb → sRGB DXT1 (separate extension). */
398
+ s3tcSrgb: boolean;
399
+ /** WEBGL_compressed_texture_astc → ASTC 4×4 (and other footprints). */
400
+ astc: boolean;
401
+ }
402
+ declare function detectWebGLCapabilities(gl: ExtensionProvider): WebGLCapabilities;
403
+
208
404
  /**
209
405
  * How the texture will be used in the renderer. Drives format choice.
210
406
  * • 'color' — RGB albedo-like data (alpha optional / ignored).
@@ -230,6 +426,16 @@ interface FormatSelection {
230
426
  }
231
427
  declare function selectFormat(adapter: FeatureProvider, hint: TextureHint, options?: SelectFormatOptions): FormatSelection;
232
428
 
429
+ interface WebGLFormatSelection {
430
+ /** null = no compressed path available on this WebGL context. */
431
+ format: TextureFormat | null;
432
+ /** null when `format` is null. */
433
+ encoderClass: WebGLEncoderConstructor | null;
434
+ /** True when the chosen path is ASTC *and* the hint is 'normal' (caller must pre-swizzle). */
435
+ astcNormalRemap: boolean;
436
+ }
437
+ declare function selectWebGLFormat(caps: WebGLCapabilities, hint: TextureHint, options?: SelectFormatOptions): WebGLFormatSelection;
438
+
233
439
  /**
234
440
  * Everything `compressTexture()` can take as an image source. A superset
235
441
  * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
@@ -245,18 +451,31 @@ interface CompressOptions {
245
451
  flipY?: boolean;
246
452
  /** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
247
453
  mipmaps?: boolean;
454
+ /**
455
+ * Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
456
+ * ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
457
+ * the CPU reference encoders). No effect on BC1 or on the WebGL fallback
458
+ * (which always uses the fast encoders).
459
+ */
460
+ quality?: EncodeQuality;
248
461
  /** Reuse an existing device (e.g. Three.js's renderer device) instead
249
- * of creating a new one. When provided, the encoder never destroys it. */
462
+ * of creating a new one. WebGPU path only. When provided, the encoder
463
+ * never destroys it. */
250
464
  device?: GPUDevice;
251
465
  adapter?: GPUAdapter;
252
466
  }
253
467
  interface CompressResult {
254
- /** CompressedTexture on the compressed path; Texture on RGBA8 fallback. */
468
+ /** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
255
469
  texture: Texture | CompressedTexture;
256
470
  /** The compressed format selected, or null when we fell back to RGBA8. */
257
471
  format: TextureFormat | null;
258
472
  /** True iff we returned an uncompressed Texture because no encoder fit. */
259
473
  fallbackUncompressed: boolean;
474
+ /**
475
+ * Which backend produced the result. 'webgpu' = compute path, 'webgl' =
476
+ * fragment-shader fallback, 'none' = uncompressed RGBA8.
477
+ */
478
+ backend: 'webgpu' | 'webgl' | 'none';
260
479
  /**
261
480
  * True iff the chosen format is ASTC and the hint was 'normal'. The
262
481
  * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
@@ -283,6 +502,8 @@ declare class GputexLoader extends Loader<Texture> {
283
502
  flipY: boolean;
284
503
  /** Generate + encode a full mip chain. Default false. */
285
504
  mipmaps: boolean;
505
+ /** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
506
+ quality: EncodeQuality;
286
507
  /**
287
508
  * Optional pre-existing WebGPU device. Reusing the renderer's device
288
509
  * avoids spinning up a second WebGPU context for encoding.
@@ -329,4 +550,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
329
550
  */
330
551
  declare function padToBlockMultiple(level: MipLevel): MipLevel;
331
552
 
332
- export { ASTC4x4Encoder, BC1Encoder, BC5Encoder, BC7Encoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type SelectFormatOptions, TextureFormat, type TextureHint, WebGPUFeature, compressTexture, detectCapabilities, generateMipChain, padToBlockMultiple, selectFormat };
553
+ export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, compressTexture, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat };