gputex 0.1.2 → 0.3.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
@@ -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
- ⚠️: 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.
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
 
@@ -14,7 +16,15 @@ pnpm add gputex
14
16
  bun add gputex
15
17
  ```
16
18
 
17
- `three` is a peer dependency (`>=0.180`).
19
+ ### Entry points
20
+
21
+ `gputex` ships three entry points:
22
+
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()`.
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)).
26
+
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.
18
28
 
19
29
  ## Formats
20
30
 
@@ -29,7 +39,7 @@ Format selection is automatic: BC7/BC5 on desktop, ASTC on mobile, uncompressed
29
39
 
30
40
  ## WebGL fallback
31
41
 
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.
42
+ 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.
33
43
 
34
44
  The fallback chain is **WebGPU → WebGL2 → uncompressed RGBA8**. The `backend` field on the result (`'webgpu' | 'webgl' | 'none'`) tells you which path ran.
35
45
 
@@ -44,7 +54,7 @@ Notes on the WebGL path:
44
54
  ### `compressTexture` — direct API
45
55
 
46
56
  ```ts
47
- import { compressTexture } from 'gputex'
57
+ import { compressTexture } from 'gputex/three'
48
58
 
49
59
  const { texture, format } = await compressTexture('/cobblestone.avif', {
50
60
  hint: 'color', // 'color' | 'colorWithAlpha' | 'normal'
@@ -63,19 +73,23 @@ material.map = texture
63
73
  - **`'fast'` (default)** — a bounding-box endpoint seed plus projection-based
64
74
  index assignment (each pixel is projected onto the colinear endpoint line in
65
75
  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.
76
+ least-squares refit (accepted per block only when it lowers the error), and
77
+ the block bits packed with straight-line constant shifts. On GPUs that
78
+ report the `shader-f16` feature the whole fast path (all four formats, BC1
79
+ included) runs in f16 — the f32 path is the automatic fallback. Net vs
80
+ `'high'` on an Apple GPU: roughly **10–30× faster** depending on format,
81
+ for a PSNR cost of **≤0.65 dB on smooth/flat content** (imperceptible) and
82
+ up to a few dB on adversarial high-frequency noise, where the bbox seed
83
+ trails `'high'`'s exhaustive search. See the benchmark table below.
71
84
  - **`'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.
85
+ search, p-bit search); matches the CPU reference encoders block-for-block
86
+ (byte-identical on >96% of blocks; the rest are equal-error FP tie-breaks,
87
+ enforced by the GPU test suite).
74
88
 
75
89
  ### `GputexLoader` — Three.js Loader
76
90
 
77
91
  ```ts
78
- import { GputexLoader } from 'gputex'
92
+ import { GputexLoader } from 'gputex/three'
79
93
 
80
94
  const loader = new GputexLoader()
81
95
  loader.hint = 'normal'
@@ -90,7 +104,7 @@ The `GputexLoader` works with R3F's `useLoader`:
90
104
 
91
105
  ```tsx
92
106
  import { useLoader } from '@react-three/fiber'
93
- import { GputexLoader } from 'gputex'
107
+ import { GputexLoader } from 'gputex/three'
94
108
 
95
109
  function Scene() {
96
110
  const texture = useLoader(GputexLoader, '/cobblestone.avif', loader => {
@@ -113,7 +127,7 @@ For a reusable hook with metadata access:
113
127
  ```tsx
114
128
  import { useLayoutEffect } from 'react'
115
129
  import { useLoader } from '@react-three/fiber'
116
- import { GputexLoader } from 'gputex'
130
+ import { GputexLoader } from 'gputex/three'
117
131
  import type { TextureHint } from 'gputex'
118
132
 
119
133
  function useGputex(url: string, options?: { hint?: TextureHint; colorSpace?: 'srgb' | 'linear'; mipmaps?: boolean }) {
@@ -157,18 +171,35 @@ function Scene() {
157
171
  }
158
172
  ```
159
173
 
160
- ### Low-level encoders
174
+ ### Low-level encoders (any engine)
161
175
 
162
- Individual encoder classes are exported for direct control:
176
+ The individual encoder classes live in the engine-agnostic core (`gputex`). `encodeToBytes()` returns raw compressed block bytes with no Three.js involvement — feed them into whatever compressed-texture upload your engine exposes (Babylon.js, raw WebGPU/WebGL, …):
163
177
 
164
178
  ```ts
165
179
  import { BC7Encoder, BC5Encoder, ASTC4x4Encoder, BC1Encoder } from 'gputex'
166
180
 
167
181
  const encoder = await BC7Encoder.create()
168
- const { data, width, height } = await encoder.encodeToBytes(imageBitmap)
182
+ const { data, width, height, paddedWidth, paddedHeight } = await encoder.encodeToBytes(imageBitmap)
183
+ // `data` is a Uint8Array of BC7 blocks covering paddedWidth × paddedHeight.
169
184
  encoder.destroy()
170
185
  ```
171
186
 
187
+ To turn an encoder's output into a Three.js `CompressedTexture` directly, use the helpers in `gputex/three`:
188
+
189
+ ```ts
190
+ import { BC7Encoder, TextureFormat } from 'gputex'
191
+ import { encodeToTexture, buildCompressedTexture } from 'gputex/three'
192
+
193
+ const encoder = await BC7Encoder.create()
194
+
195
+ // One-shot: image → CompressedTexture (plus the raw byte metadata)
196
+ const { texture } = await encodeToTexture(encoder, imageBitmap, { colorSpace: 'srgb' })
197
+
198
+ // …or assemble a texture from bytes you already have (e.g. a mip chain):
199
+ const bytes = await encoder.encodeToBytes(imageBitmap)
200
+ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
201
+ ```
202
+
172
203
  ## Options
173
204
 
174
205
  ### `compressTexture` options
@@ -181,6 +212,77 @@ encoder.destroy()
181
212
  | `mipmaps` | `boolean` | `false` | Generate full mip chain down to 1x1 |
182
213
  | `device` | `GPUDevice` | — | Reuse an existing WebGPU device instead of creating one |
183
214
 
215
+ ## Benchmarks
216
+
217
+ Measured with the repo's GPU test suite (see below) on an Apple Silicon GPU
218
+ (`metal-3`) in Chrome, encoding a 2048×2048 image. **GPU pass** is the compute
219
+ shader alone (WebGPU timestamp queries, median of 20 runs); end-to-end wall
220
+ time adds ~3–4 ms of image upload + result readback regardless of format.
221
+ "Before" is the shader generation prior to the 2026-07 optimization pass
222
+ (projection-based index assignment everywhere, on-the-fly bit packing, an f16
223
+ BC1 fast shader, straight-line block assembly).
224
+
225
+ | Format | Quality | Shader | GPU pass before | GPU pass after | Speedup |
226
+ | -------- | -------------- | ------ | --------------- | -------------- | --------- |
227
+ | BC1 | fast (default) | f16 | — (had no f16) | **0.26 ms** | **2.8×**¹ |
228
+ | BC1 | fast | f32 | 0.72 ms | 0.33 ms | 2.2× |
229
+ | BC5 | fast (default) | f16 | 0.33 ms | **0.20 ms** | 1.7× |
230
+ | BC5 | fast | f32 | 0.79 ms | 0.39 ms | 2.0× |
231
+ | BC7 | fast (default) | f16 | 1.38 ms | **0.52 ms** | 2.7× |
232
+ | BC7 | fast | f32 | 2.65 ms | 1.70 ms | 1.6× |
233
+ | ASTC 4×4 | fast (default) | f16 | 0.33 ms | **0.20 ms** | 1.7× |
234
+ | ASTC 4×4 | fast | f32 | 0.72 ms | 0.66 ms | 1.1× |
235
+ | BC1 | high | f32 | 2.9 ms | 3.0 ms | unchanged |
236
+ | BC5 | high | f32 | 3.4 ms | 3.4 ms | unchanged |
237
+ | BC7 | high | f32 | 14.1 ms | 14.1 ms | unchanged |
238
+ | ASTC 4×4 | high | f32 | 2.6 ms | 2.4 ms | unchanged |
239
+
240
+ ¹ vs the old f32 fast shader, which was the only BC1 fast path before. The
241
+ BC1 rows were measured with old and new pipelines interleaved in one session
242
+ (the most noise-robust method); the others are cross-run suite medians.
243
+
244
+ Per-quadrant PSNR on the committed 512² test card is equal to or better than
245
+ the previous fast encoders everywhere (flat tiles bit-identical, gradients
246
+ +0.01 dB, noise −0.01 dB); the `high` paths still match the CPU reference
247
+ encoders. Timestamps are quantised to 100 µs by Chrome, so sub-millisecond
248
+ figures are ±0.05–0.1 ms.
249
+
250
+ ## Testing
251
+
252
+ Unit tests (`bun test`) cover the CPU reference encoders and metadata, but the
253
+ WGSL shaders can only be validated on a real GPU. The repo ships a browser
254
+ test + benchmark suite at `example/pages/test.tsx` (logic in
255
+ `example/lib/gpuTestSuite.ts`):
256
+
257
+ ```sh
258
+ bun run --filter gputex build # build the library the example consumes
259
+ cd example && bunx next dev # then open http://localhost:3000/test
260
+ ```
261
+
262
+ The page runs three groups against the live WebGPU device and renders
263
+ PASS/FAIL tables (machine-readable copy on `window.__GPUTEX_TESTS__`):
264
+
265
+ - **Correctness** — `quality: 'high'` output is compared block-by-block
266
+ against the CPU reference encoders (`gputex/testing`), including a
267
+ non-multiple-of-4 image for the clamp-to-edge padding path. Differing blocks
268
+ must have equal decoded error (FP tie-break tolerance) and the aggregate
269
+ PSNR delta must be ≤0.05 dB. Plus determinism checks (same input twice →
270
+ identical bytes).
271
+ - **Quality** — `'fast'` and `'high'` output is CPU-decoded and validated on
272
+ the FULL 512² test cards (every quadrant stresses a different failure mode)
273
+ with two gates, for both the f16 and (force-disabled-f16) f32 shaders:
274
+ aggregate PSNR must beat per-format thresholds pinned ~0.15 dB under the
275
+ measured baseline, and — because a handful of catastrophically wrong blocks
276
+ barely moves aggregate PSNR — the worst _easy_ block (one that `'high'`
277
+ encodes near-losslessly) must not exceed `'high'`'s error by more than a
278
+ small per-format limit.
279
+ - **Performance** — the benchmark table above: wall + GPU-pass time per
280
+ format × quality × shader variant.
281
+
282
+ The `gputex/testing` entry point exports the CPU reference
283
+ encoders/decoders (`encodeBC7Mode6Block`, `decodeASTC4x4Block`, …) so any
284
+ consumer can run the same validation.
285
+
184
286
  ## Requirements
185
287
 
186
288
  - WebGPU (primary) **or** WebGL2 (fallback) — almost every current browser has at least one
@@ -195,4 +297,4 @@ encoder.destroy()
195
297
 
196
298
  ## Acknowledgements
197
299
 
198
- The concept of encoding images on the GPU on the fly via compute shaders was first introduced by [spark.js](https://ludicon.com/sparkjs/), which is a much more robust solution for users who can afford its license. GPUtex is not derived from Spark and its encoders have been implemented from scratch using official references, which have been ported to TypeScript, and then converted to WGSL via AI. For any serious production use of GPU-compressed textures, Spark is the recommended choice over GPUtex.
300
+ 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
@@ -1,5 +1,3 @@
1
- import { CompressedPixelFormat, CompressedTexture, Texture, Loader } from 'three';
2
-
3
1
  declare const TextureFormat: {
4
2
  readonly BC1: "BC1";
5
3
  readonly BC1_SRGB: "BC1_SRGB";
@@ -44,13 +42,19 @@ interface EncoderOptions {
44
42
  device: GPUDevice;
45
43
  adapter?: GPUAdapter;
46
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;
47
51
  }
48
52
  /**
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 (BC5/
52
- * BC7/ASTC). BC1's 'high' adds a principal-axis endpoint seed and iterative
53
- * refit on top of the 'fast' bbox+refit path.
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.
54
58
  */
55
59
  type EncodeQuality = 'fast' | 'high';
56
60
  interface EncodeCallOptions {
@@ -59,19 +63,11 @@ interface EncodeCallOptions {
59
63
  /** Encode quality / speed trade-off. Default 'fast'. */
60
64
  quality?: EncodeQuality;
61
65
  }
62
- interface EncodeResult {
63
- width: number;
64
- height: number;
65
- paddedWidth: number;
66
- paddedHeight: number;
67
- data: Uint8Array;
68
- texture: CompressedTexture;
69
- encodeMs: number;
70
- }
71
66
  /**
72
- * Result of a raw bytes-only encode (no wrapping in a `CompressedTexture`).
73
- * Used by the mipped encode path to bundle multiple levels into a single
74
- * `CompressedTexture` at the end.
67
+ * Result of a raw bytes-only encode. This is the encoder's native output: the
68
+ * compressed block bytes plus dimensions, with no Three.js (or any engine)
69
+ * involvement. Feed `data` into whatever renderer's compressed-texture upload
70
+ * you like, or use `buildCompressedTexture()` from `gputex/three`.
75
71
  */
76
72
  interface EncodeBytesResult {
77
73
  width: number;
@@ -80,6 +76,14 @@ interface EncodeBytesResult {
80
76
  paddedHeight: number;
81
77
  data: Uint8Array;
82
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;
83
87
  }
84
88
  interface FormatVariant {
85
89
  colorSpace: 'srgb' | 'linear';
@@ -96,6 +100,8 @@ interface FormatVariant {
96
100
  type EncoderConstructor<T extends Encoder = Encoder> = {
97
101
  new (opts: EncoderOptions): T;
98
102
  requiredFeature: GPUFeatureName | null;
103
+ /** Logical formats this encoder can emit (linear first, then sRGB variant). */
104
+ readonly textureFormats: readonly TextureFormat[];
99
105
  create(): Promise<T>;
100
106
  };
101
107
  declare abstract class Encoder {
@@ -119,12 +125,13 @@ declare abstract class Encoder {
119
125
  readonly device: GPUDevice;
120
126
  readonly adapter?: GPUAdapter;
121
127
  readonly ownsDevice: boolean;
128
+ readonly disableF16: boolean;
122
129
  protected _module: GPUShaderModule;
123
130
  protected _moduleF16: GPUShaderModule | null;
124
131
  protected _pipelineF16: GPUComputePipeline | null;
125
132
  protected _pipeline: GPUComputePipeline;
126
133
  protected _pipelineCache: Map<EncodeQuality, GPUComputePipeline>;
127
- constructor({ device, adapter, ownsDevice }: EncoderOptions);
134
+ constructor({ device, adapter, ownsDevice, disableF16 }: EncoderOptions);
128
135
  protected _buildPipeline(): void;
129
136
  /**
130
137
  * Pipeline for a given quality level. Encoders that don't declare a
@@ -150,7 +157,7 @@ declare abstract class Encoder {
150
157
  /**
151
158
  * Optional f16 WGSL for the 'fast' path. Used only when the device reports the
152
159
  * `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
153
- * `'high'` always uses it. Returns null when there's no f16 variant (BC1).
160
+ * `'high'` always uses it. Returns null when there's no f16 variant.
154
161
  */
155
162
  wgslSourceFastF16(): string | null;
156
163
  /** Whether the f16 fast path is both available and supported on this device. */
@@ -159,40 +166,24 @@ declare abstract class Encoder {
159
166
  abstract wgslSource(): string;
160
167
  /** e.g. 'bc1-rgba-unorm-srgb'. */
161
168
  abstract gpuTextureFormat(opts: FormatVariant): GPUTextureFormat;
162
- /** Three.js `CompressedPixelFormat` constant for `CompressedTexture`. */
163
- abstract threeTextureFormat(opts: FormatVariant): CompressedPixelFormat;
164
169
  /**
165
170
  * True if the device reports the feature the output texture needs.
166
171
  * The encoder itself only writes to a storage buffer, so this is about
167
172
  * whether the result can actually be sampled.
168
173
  */
169
174
  get supportsSampling(): boolean;
170
- encode(source: EncoderImageSource, { colorSpace, quality }?: EncodeCallOptions): Promise<EncodeResult>;
171
175
  /**
172
- * Encode one image source to raw compressed bytes, skipping the
173
- * `CompressedTexture` wrap. Used by the public `encode()` above and by
174
- * the mipped encode path in `compressTexture()` so N mip levels end up
175
- * in a single `CompressedTexture` instead of N throwaway wrappers.
176
- *
177
- * Public (not protected) because `compressTexture()` calls it across the
178
- * encoder boundary. Still safe to call from outside — it just does
179
- * less work than `encode()` and the caller assembles the texture.
176
+ * Encode one image source to raw compressed bytes. This is the encoder's
177
+ * native, engine-agnostic output. `compressTexture()` and
178
+ * `encodeToTexture()` (both in `gputex/three`) call it and then wrap the
179
+ * bytes into a `CompressedTexture`; callers targeting another engine feed
180
+ * `data` into that engine's compressed-texture upload directly.
180
181
  */
181
- encodeToBytes(source: EncoderImageSource, { flipY, quality }?: {
182
+ encodeToBytes(source: EncoderImageSource, { flipY, quality, withGpuTime, }?: {
182
183
  flipY?: boolean;
183
184
  quality?: EncodeQuality;
185
+ withGpuTime?: boolean;
184
186
  }): Promise<EncodeBytesResult>;
185
- /**
186
- * Assemble a `CompressedTexture` from pre-encoded mip levels. Called
187
- * by `compressTexture()` after it has run each level through
188
- * `encodeToBytes()`. Centralised here so the single-level and mipped
189
- * paths share the same format / colour-space / wrap settings.
190
- *
191
- * `levels[0]` is the base level; its padded dimensions become the
192
- * texture's overall size. Filter setup assumes at least 2 levels →
193
- * trilinear; 1 level → bilinear.
194
- */
195
- buildMippedTexture(levels: readonly EncodeBytesResult[], { colorSpace }?: EncodeCallOptions): CompressedTexture;
196
187
  }
197
188
 
198
189
  declare class BC1Encoder extends Encoder {
@@ -203,8 +194,8 @@ declare class BC1Encoder extends Encoder {
203
194
  get supportsSrgb(): boolean;
204
195
  get supportsQuality(): boolean;
205
196
  wgslSource(): string;
197
+ wgslSourceFastF16(): string | null;
206
198
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
207
- threeTextureFormat(): CompressedPixelFormat;
208
199
  }
209
200
 
210
201
  declare class BC5Encoder extends Encoder {
@@ -217,7 +208,6 @@ declare class BC5Encoder extends Encoder {
217
208
  wgslSource(): string;
218
209
  wgslSourceFastF16(): string;
219
210
  gpuTextureFormat(): GPUTextureFormat;
220
- threeTextureFormat(): CompressedPixelFormat;
221
211
  }
222
212
 
223
213
  declare class BC7Encoder extends Encoder {
@@ -230,7 +220,6 @@ declare class BC7Encoder extends Encoder {
230
220
  wgslSource(): string;
231
221
  wgslSourceFastF16(): string;
232
222
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
233
- threeTextureFormat(): CompressedPixelFormat;
234
223
  }
235
224
 
236
225
  declare class ASTC4x4Encoder extends Encoder {
@@ -243,18 +232,6 @@ declare class ASTC4x4Encoder extends Encoder {
243
232
  wgslSource(): string;
244
233
  wgslSourceFastF16(): string;
245
234
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
246
- threeTextureFormat(): CompressedPixelFormat;
247
- }
248
-
249
- /** One encoded mip level. The fields both encoder backends already produce. */
250
- interface EncodedLevel {
251
- /** Logical (pre-padding) dimensions, surfaced on the texture's userData. */
252
- width: number;
253
- height: number;
254
- /** Block-aligned dimensions the compressed `data` actually covers. */
255
- paddedWidth: number;
256
- paddedHeight: number;
257
- data: Uint8Array;
258
235
  }
259
236
 
260
237
  /** Raw RGBA8 pixel data (e.g. a CPU-generated mip level). */
@@ -307,8 +284,6 @@ declare abstract class WebGLBlockEncoder {
307
284
  abstract get supportsSrgb(): boolean;
308
285
  /** GLSL ES 3.00 fragment-shader source. */
309
286
  abstract fragSource(): string;
310
- /** Three.js `CompressedPixelFormat` constant; sRGB is carried by colorSpace. */
311
- abstract threeTextureFormat(): CompressedPixelFormat;
312
287
  protected _buildProgram(): void;
313
288
  /** Release the GL program + VAO. The shared context itself is left intact. */
314
289
  destroy(): void;
@@ -328,10 +303,6 @@ declare abstract class WebGLBlockEncoder {
328
303
  encodeToBytes(source: WebGLEncoderImageSource, { flipY }?: {
329
304
  flipY?: boolean;
330
305
  }): WebGLEncodeBytesResult;
331
- /** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
332
- buildMippedTexture(levels: readonly EncodedLevel[], { colorSpace }?: {
333
- colorSpace?: 'srgb' | 'linear';
334
- }): CompressedTexture;
335
306
  }
336
307
 
337
308
  declare class BC1WebGLEncoder extends WebGLBlockEncoder {
@@ -339,7 +310,6 @@ declare class BC1WebGLEncoder extends WebGLBlockEncoder {
339
310
  get bytesPerBlock(): number;
340
311
  get supportsSrgb(): boolean;
341
312
  fragSource(): string;
342
- threeTextureFormat(): CompressedPixelFormat;
343
313
  }
344
314
 
345
315
  declare class BC5WebGLEncoder extends WebGLBlockEncoder {
@@ -347,7 +317,6 @@ declare class BC5WebGLEncoder extends WebGLBlockEncoder {
347
317
  get bytesPerBlock(): number;
348
318
  get supportsSrgb(): boolean;
349
319
  fragSource(): string;
350
- threeTextureFormat(): CompressedPixelFormat;
351
320
  }
352
321
 
353
322
  declare class BC7WebGLEncoder extends WebGLBlockEncoder {
@@ -355,7 +324,6 @@ declare class BC7WebGLEncoder extends WebGLBlockEncoder {
355
324
  get bytesPerBlock(): number;
356
325
  get supportsSrgb(): boolean;
357
326
  fragSource(): string;
358
- threeTextureFormat(): CompressedPixelFormat;
359
327
  }
360
328
 
361
329
  declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
@@ -363,7 +331,6 @@ declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
363
331
  get bytesPerBlock(): number;
364
332
  get supportsSrgb(): boolean;
365
333
  fragSource(): string;
366
- threeTextureFormat(): CompressedPixelFormat;
367
334
  }
368
335
 
369
336
  /**
@@ -438,96 +405,6 @@ interface WebGLFormatSelection {
438
405
  }
439
406
  declare function selectWebGLFormat(caps: WebGLCapabilities, hint: TextureHint, options?: SelectFormatOptions): WebGLFormatSelection;
440
407
 
441
- /**
442
- * Everything `compressTexture()` can take as an image source. A superset
443
- * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
444
- * and Blob / File objects — the common cases in a web app.
445
- */
446
- type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
447
- interface CompressOptions {
448
- /** How the texture will be used. Drives format selection. Default 'color'. */
449
- hint?: TextureHint;
450
- /** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
451
- colorSpace?: 'srgb' | 'linear';
452
- /** Flip the image vertically before encoding. Default true (matches Three.js convention). */
453
- flipY?: boolean;
454
- /** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
455
- mipmaps?: boolean;
456
- /**
457
- * Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
458
- * ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
459
- * the CPU reference encoders; for BC1, a principal-axis seed + iterative
460
- * refit). No effect on the WebGL fallback (which always uses the fast
461
- * encoders).
462
- */
463
- quality?: EncodeQuality;
464
- /** Reuse an existing device (e.g. Three.js's renderer device) instead
465
- * of creating a new one. WebGPU path only. When provided, the encoder
466
- * never destroys it. */
467
- device?: GPUDevice;
468
- adapter?: GPUAdapter;
469
- }
470
- interface CompressResult {
471
- /** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
472
- texture: Texture | CompressedTexture;
473
- /** The compressed format selected, or null when we fell back to RGBA8. */
474
- format: TextureFormat | null;
475
- /** True iff we returned an uncompressed Texture because no encoder fit. */
476
- fallbackUncompressed: boolean;
477
- /**
478
- * Which backend produced the result. 'webgpu' = compute path, 'webgl' =
479
- * fragment-shader fallback, 'none' = uncompressed RGBA8.
480
- */
481
- backend: 'webgpu' | 'webgl' | 'none';
482
- /**
483
- * True iff the chosen format is ASTC and the hint was 'normal'. The
484
- * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
485
- * has no 2-channel mode, so normal maps ride the RGBA path.
486
- */
487
- astcNormalRemap: boolean;
488
- width: number;
489
- height: number;
490
- mipLevels: number;
491
- /** Wall-clock time of GPU encoding, summed across mip levels. */
492
- encodeMs: number;
493
- /** Release the encoder's internal GPU resources. No-op if `device` was
494
- * passed in by the caller. */
495
- destroy(): void;
496
- }
497
- declare function compressTexture(source: CompressTextureSource, options?: CompressOptions): Promise<CompressResult>;
498
-
499
- declare class GputexLoader extends Loader<Texture> {
500
- /** Format-selection hint. Default 'color'. */
501
- hint: TextureHint;
502
- /** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
503
- colorSpace: 'srgb' | 'linear';
504
- /** Flip the image vertically before encoding. Default true (matches Three.js convention). */
505
- flipY: boolean;
506
- /** Generate + encode a full mip chain. Default false. */
507
- mipmaps: boolean;
508
- /** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
509
- quality: EncodeQuality;
510
- /**
511
- * Optional pre-existing WebGPU device. Reusing the renderer's device
512
- * avoids spinning up a second WebGPU context for encoding.
513
- */
514
- device?: GPUDevice;
515
- adapter?: GPUAdapter;
516
- /**
517
- * Most recent full encode result. Useful when the caller wants format
518
- * / mipLevels / astcNormalRemap metadata without threading a separate
519
- * callback through `load()`. Cleared when a new load starts.
520
- */
521
- lastResult: CompressResult | null;
522
- /**
523
- * THREE.Loader contract: returns void, drives callbacks. `loadAsync`
524
- * (inherited from the base class) wraps this with Promise semantics.
525
- * Errors routed through `manager.itemError` so the LoadingManager's
526
- * aggregate state stays accurate.
527
- */
528
- load(url: string, onLoad?: (texture: Texture) => void, _onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): void;
529
- }
530
-
531
408
  /** One level of a mip chain. 4 bytes per pixel (RGBA8). */
532
409
  interface MipLevel {
533
410
  data: Uint8ClampedArray;
@@ -553,4 +430,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
553
430
  */
554
431
  declare function padToBlockMultiple(level: MipLevel): MipLevel;
555
432
 
556
- 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 };
433
+ 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 };