gputex 0.1.0 → 0.1.2

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
@@ -171,9 +183,11 @@ encoder.destroy()
171
183
 
172
184
  ## Requirements
173
185
 
174
- - A browser with WebGPU support
175
- - `texture-compression-bc` (desktop) or `texture-compression-astc` (mobile) for compressed output
176
- - 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
177
191
 
178
192
  ## Device-specific workarounds
179
193
 
package/dist/index.d.ts CHANGED
@@ -48,8 +48,9 @@ interface EncoderOptions {
48
48
  /**
49
49
  * Encoder quality level. 'fast' (default) uses the cheaper search paths in the
50
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).
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
54
  */
54
55
  type EncodeQuality = 'fast' | 'high';
55
56
  interface EncodeCallOptions {
@@ -142,8 +143,8 @@ declare abstract class Encoder {
142
143
  get supportsSrgb(): boolean;
143
144
  /**
144
145
  * 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.
146
+ * (i.e. has distinct fast/high search paths). Default false (e.g. a stub or a
147
+ * format with a single path); BC1/BC5/BC7/ASTC override it to true.
147
148
  */
148
149
  get supportsQuality(): boolean;
149
150
  /**
@@ -200,6 +201,7 @@ declare class BC1Encoder extends Encoder {
200
201
  get label(): string;
201
202
  get bytesPerBlock(): number;
202
203
  get supportsSrgb(): boolean;
204
+ get supportsQuality(): boolean;
203
205
  wgslSource(): string;
204
206
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
205
207
  threeTextureFormat(): CompressedPixelFormat;
@@ -244,6 +246,163 @@ declare class ASTC4x4Encoder extends Encoder {
244
246
  threeTextureFormat(): CompressedPixelFormat;
245
247
  }
246
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
+ }
259
+
260
+ /** Raw RGBA8 pixel data (e.g. a CPU-generated mip level). */
261
+ interface RawPixelSource {
262
+ data: ArrayBufferView;
263
+ width: number;
264
+ height: number;
265
+ }
266
+ /** Anything the WebGL encoders can upload as the source image. */
267
+ type WebGLEncoderImageSource = ImageBitmap | ImageData | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | RawPixelSource;
268
+ interface WebGLEncodeBytesResult {
269
+ width: number;
270
+ height: number;
271
+ paddedWidth: number;
272
+ paddedHeight: number;
273
+ data: Uint8Array;
274
+ encodeMs: number;
275
+ }
276
+ interface WebGLEncoderOptions {
277
+ gl: WebGL2RenderingContext;
278
+ }
279
+ /**
280
+ * Constructor shape for the concrete subclasses; lets the static `create()`
281
+ * narrow its return type to the subclass (as the WebGPU `EncoderConstructor`
282
+ * does).
283
+ */
284
+ type WebGLEncoderConstructor<T extends WebGLBlockEncoder = WebGLBlockEncoder> = {
285
+ new (opts: WebGLEncoderOptions): T;
286
+ create(gl?: WebGL2RenderingContext | null): T;
287
+ };
288
+ declare abstract class WebGLBlockEncoder {
289
+ /**
290
+ * Create an encoder on the shared process-wide context (or a caller-supplied
291
+ * one). Throws when WebGL2 is unavailable. The `this:` annotation lets
292
+ * `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
293
+ */
294
+ static create<T extends WebGLBlockEncoder>(this: WebGLEncoderConstructor<T>, gl?: WebGL2RenderingContext | null): T;
295
+ readonly gl: WebGL2RenderingContext;
296
+ protected _program: WebGLProgram;
297
+ protected _vao: WebGLVertexArrayObject;
298
+ protected _uSrc: WebGLUniformLocation | null;
299
+ protected _uSrcSize: WebGLUniformLocation | null;
300
+ protected _uFlipY: WebGLUniformLocation | null;
301
+ constructor({ gl }: WebGLEncoderOptions);
302
+ /** Short lowercase identifier for labels / errors. */
303
+ abstract get label(): string;
304
+ /** 8 for BC1, 16 for BC5/BC7/ASTC 4×4. */
305
+ abstract get bytesPerBlock(): number;
306
+ /** Whether this format has an sRGB variant (false for BC5). */
307
+ abstract get supportsSrgb(): boolean;
308
+ /** GLSL ES 3.00 fragment-shader source. */
309
+ abstract fragSource(): string;
310
+ /** Three.js `CompressedPixelFormat` constant; sRGB is carried by colorSpace. */
311
+ abstract threeTextureFormat(): CompressedPixelFormat;
312
+ protected _buildProgram(): void;
313
+ /** Release the GL program + VAO. The shared context itself is left intact. */
314
+ destroy(): void;
315
+ /**
316
+ * Upload the source image to a freshly created RGBA8 texture bound on unit 0.
317
+ * Raw pixel sources (ImageData / mip levels) go through the typed-array
318
+ * overload; DOM sources (ImageBitmap / canvas / image) through the element
319
+ * overload. No flip / premultiply / colour conversion — flipY is applied in
320
+ * the shader so each mip level flips by its own height.
321
+ */
322
+ protected _uploadSource(source: WebGLEncoderImageSource, width: number, height: number): WebGLTexture;
323
+ /**
324
+ * Encode one image source to raw compressed bytes. `flipY` samples the source
325
+ * bottom-up (matching Three.js's convention) and is applied in the shader;
326
+ * the high-level mipped path bakes the flip into level 0 and passes false.
327
+ */
328
+ encodeToBytes(source: WebGLEncoderImageSource, { flipY }?: {
329
+ flipY?: boolean;
330
+ }): 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
+ }
336
+
337
+ declare class BC1WebGLEncoder extends WebGLBlockEncoder {
338
+ get label(): string;
339
+ get bytesPerBlock(): number;
340
+ get supportsSrgb(): boolean;
341
+ fragSource(): string;
342
+ threeTextureFormat(): CompressedPixelFormat;
343
+ }
344
+
345
+ declare class BC5WebGLEncoder extends WebGLBlockEncoder {
346
+ get label(): string;
347
+ get bytesPerBlock(): number;
348
+ get supportsSrgb(): boolean;
349
+ fragSource(): string;
350
+ threeTextureFormat(): CompressedPixelFormat;
351
+ }
352
+
353
+ declare class BC7WebGLEncoder extends WebGLBlockEncoder {
354
+ get label(): string;
355
+ get bytesPerBlock(): number;
356
+ get supportsSrgb(): boolean;
357
+ fragSource(): string;
358
+ threeTextureFormat(): CompressedPixelFormat;
359
+ }
360
+
361
+ declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
362
+ get label(): string;
363
+ get bytesPerBlock(): number;
364
+ get supportsSrgb(): boolean;
365
+ fragSource(): string;
366
+ threeTextureFormat(): CompressedPixelFormat;
367
+ }
368
+
369
+ /**
370
+ * Create a fresh WebGL2 context backed by an off-screen 1×1 canvas. Returns
371
+ * null when neither `OffscreenCanvas` nor `document` is available (e.g. a
372
+ * non-DOM worker without OffscreenCanvas) or when the platform has no WebGL2.
373
+ */
374
+ declare function createWebGLContext(): WebGL2RenderingContext | null;
375
+ /**
376
+ * Lazily-created context shared across the high-level fallback path. Re-created
377
+ * if the previous one was lost (tab backgrounding, GPU reset). Returns null
378
+ * when WebGL2 is unavailable on this platform.
379
+ */
380
+ declare function getSharedWebGLContext(): WebGL2RenderingContext | null;
381
+ /** True when a WebGL2 context can be created on this platform. */
382
+ declare function isWebGLAvailable(): boolean;
383
+
384
+ /**
385
+ * Minimal structural type for the extension source: only `getExtension` is
386
+ * touched, so tests can pass a `{ getExtension }` stub instead of a real
387
+ * `WebGL2RenderingContext`.
388
+ */
389
+ interface ExtensionProvider {
390
+ getExtension(name: string): unknown;
391
+ }
392
+ interface WebGLCapabilities {
393
+ /** EXT_texture_compression_bptc → BC7 (BPTC). */
394
+ bptc: boolean;
395
+ /** EXT_texture_compression_rgtc → BC5 (RGTC2). */
396
+ rgtc: boolean;
397
+ /** WEBGL_compressed_texture_s3tc → BC1 (DXT1). */
398
+ s3tc: boolean;
399
+ /** WEBGL_compressed_texture_s3tc_srgb → sRGB DXT1 (separate extension). */
400
+ s3tcSrgb: boolean;
401
+ /** WEBGL_compressed_texture_astc → ASTC 4×4 (and other footprints). */
402
+ astc: boolean;
403
+ }
404
+ declare function detectWebGLCapabilities(gl: ExtensionProvider): WebGLCapabilities;
405
+
247
406
  /**
248
407
  * How the texture will be used in the renderer. Drives format choice.
249
408
  * • 'color' — RGB albedo-like data (alpha optional / ignored).
@@ -269,6 +428,16 @@ interface FormatSelection {
269
428
  }
270
429
  declare function selectFormat(adapter: FeatureProvider, hint: TextureHint, options?: SelectFormatOptions): FormatSelection;
271
430
 
431
+ interface WebGLFormatSelection {
432
+ /** null = no compressed path available on this WebGL context. */
433
+ format: TextureFormat | null;
434
+ /** null when `format` is null. */
435
+ encoderClass: WebGLEncoderConstructor | null;
436
+ /** True when the chosen path is ASTC *and* the hint is 'normal' (caller must pre-swizzle). */
437
+ astcNormalRemap: boolean;
438
+ }
439
+ declare function selectWebGLFormat(caps: WebGLCapabilities, hint: TextureHint, options?: SelectFormatOptions): WebGLFormatSelection;
440
+
272
441
  /**
273
442
  * Everything `compressTexture()` can take as an image source. A superset
274
443
  * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
@@ -287,21 +456,29 @@ interface CompressOptions {
287
456
  /**
288
457
  * Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
289
458
  * ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
290
- * the CPU reference encoders). No effect on BC1.
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).
291
462
  */
292
463
  quality?: EncodeQuality;
293
464
  /** Reuse an existing device (e.g. Three.js's renderer device) instead
294
- * of creating a new one. When provided, the encoder never destroys it. */
465
+ * of creating a new one. WebGPU path only. When provided, the encoder
466
+ * never destroys it. */
295
467
  device?: GPUDevice;
296
468
  adapter?: GPUAdapter;
297
469
  }
298
470
  interface CompressResult {
299
- /** CompressedTexture on the compressed path; Texture on RGBA8 fallback. */
471
+ /** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
300
472
  texture: Texture | CompressedTexture;
301
473
  /** The compressed format selected, or null when we fell back to RGBA8. */
302
474
  format: TextureFormat | null;
303
475
  /** True iff we returned an uncompressed Texture because no encoder fit. */
304
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';
305
482
  /**
306
483
  * True iff the chosen format is ASTC and the hint was 'normal'. The
307
484
  * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
@@ -376,4 +553,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
376
553
  */
377
554
  declare function padToBlockMultiple(level: MipLevel): MipLevel;
378
555
 
379
- export { ASTC4x4Encoder, BC1Encoder, BC5Encoder, BC7Encoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, 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 };
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 };
package/dist/index.js CHANGED
@@ -42,14 +42,39 @@ function detectCapabilities(adapter) {
42
42
  }
43
43
 
44
44
  // src/Encoder.ts
45
+ import { CompressedTexture as CompressedTexture2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, RepeatWrapping as RepeatWrapping2 } from "three";
46
+
47
+ // src/textureAssembly.ts
45
48
  import {
46
49
  CompressedTexture,
47
50
  LinearFilter,
48
51
  LinearMipmapLinearFilter,
49
52
  LinearSRGBColorSpace,
50
- SRGBColorSpace,
51
- RepeatWrapping
53
+ RepeatWrapping,
54
+ SRGBColorSpace
52
55
  } from "three";
56
+ function assembleCompressedTexture(levels, threeFormat, effectiveSrgb) {
57
+ if (levels.length === 0) {
58
+ throw new Error("assembleCompressedTexture: no levels provided");
59
+ }
60
+ const mipmaps = levels.map((l) => ({
61
+ data: l.data,
62
+ width: l.paddedWidth,
63
+ height: l.paddedHeight
64
+ }));
65
+ const base = levels[0];
66
+ const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
67
+ texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
68
+ texture.magFilter = LinearFilter;
69
+ texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
70
+ texture.generateMipmaps = false;
71
+ texture.wrapS = texture.wrapT = RepeatWrapping;
72
+ texture.needsUpdate = true;
73
+ texture.userData.logicalWidth = base.width;
74
+ texture.userData.logicalHeight = base.height;
75
+ texture.userData.mipLevels = levels.length;
76
+ return texture;
77
+ }
53
78
 
54
79
  // src/workarounds.ts
55
80
  function needsWriteTextureWorkaround(adapter) {
@@ -189,8 +214,8 @@ var Encoder = class {
189
214
  }
190
215
  /**
191
216
  * Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
192
- * (i.e. has distinct fast/high search paths). BC1 is already single-pass and
193
- * leaves this false; BC5/BC7/ASTC override it to true.
217
+ * (i.e. has distinct fast/high search paths). Default false (e.g. a stub or a
218
+ * format with a single path); BC1/BC5/BC7/ASTC override it to true.
194
219
  */
195
220
  get supportsQuality() {
196
221
  return false;
@@ -228,12 +253,12 @@ var Encoder = class {
228
253
  width: bytes.paddedWidth,
229
254
  height: bytes.paddedHeight
230
255
  };
231
- const texture = new CompressedTexture([mip], bytes.paddedWidth, bytes.paddedHeight, threeFormat);
232
- texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
233
- texture.magFilter = LinearFilter;
234
- texture.minFilter = LinearFilter;
256
+ const texture = new CompressedTexture2([mip], bytes.paddedWidth, bytes.paddedHeight, threeFormat);
257
+ texture.colorSpace = effectiveSrgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
258
+ texture.magFilter = LinearFilter2;
259
+ texture.minFilter = LinearFilter2;
235
260
  texture.generateMipmaps = false;
236
- texture.wrapS = texture.wrapT = RepeatWrapping;
261
+ texture.wrapS = texture.wrapT = RepeatWrapping2;
237
262
  texture.needsUpdate = true;
238
263
  texture.userData.logicalWidth = bytes.width;
239
264
  texture.userData.logicalHeight = bytes.height;
@@ -341,23 +366,7 @@ var Encoder = class {
341
366
  }
342
367
  const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
343
368
  const threeFormat = this.threeTextureFormat({ colorSpace: effectiveSrgb ? "srgb" : "linear" });
344
- const mipmaps = levels.map((l) => ({
345
- data: l.data,
346
- width: l.paddedWidth,
347
- height: l.paddedHeight
348
- }));
349
- const base = levels[0];
350
- const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
351
- texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
352
- texture.magFilter = LinearFilter;
353
- texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
354
- texture.generateMipmaps = false;
355
- texture.wrapS = texture.wrapT = RepeatWrapping;
356
- texture.needsUpdate = true;
357
- texture.userData.logicalWidth = base.width;
358
- texture.userData.logicalHeight = base.height;
359
- texture.userData.mipLevels = levels.length;
360
- return texture;
369
+ return assembleCompressedTexture(levels, threeFormat, effectiveSrgb);
361
370
  }
362
371
  };
363
372
 
@@ -365,7 +374,7 @@ var Encoder = class {
365
374
  import { RGBA_S3TC_DXT1_Format } from "three";
366
375
 
367
376
  // src/bc1.wgsl
368
- var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// When color0 > color1 (numeric 16-bit), the 4-color mode is used:\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n// We always force the 4-color mode here.\n//\n// Algorithm:\n// 1. Compute the bounding box (min/max RGB) of the block.\n// 2. Inset slightly to account for endpoint quantization rounding;\n// this is a well-known heuristic from rygorous/stb_dxt that\n// improves quality cheaply.\n// 3. Quantize endpoints to RGB565 and ensure color0 > color1.\n// 4. Reconstruct the 4-color palette in floating point and assign\n// the closest palette entry to each pixel (full L2 search).\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = f32((c >> 11u) & 31u);\n let g = f32((c >> 5u) & 63u);\n let b = f32( c & 31u);\n // Expand to 8-bit then normalize, matching typical BC1 decoder behavior.\n let r8 = (r * 527.0 + 23.0) / 256.0; // = round(r * 255 / 31)\n let g8 = (g * 259.0 + 33.0) / 256.0; // = round(g * 255 / 63)\n let b8 = (b * 527.0 + 23.0) / 256.0;\n return vec3<f32>(r8, g8, b8) / 255.0;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n }\n\n // Inset the bounding box. The magic constant 1/16 approximates half\n // the width of an RGB565 quantization cell; insetting by that much\n // moves the endpoints toward each other so the quantized 4-color\n // palette covers the real data range more tightly.\n let inset = (bb_max - bb_min) / 16.0;\n var hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n var lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n\n var c0 = to565(hi);\n var c1 = to565(lo);\n\n // 4-color mode requires c0 > c1. If equal (flat block), we still use\n // 4-color mode by nudging c1 down when possible; if c1 == 0 the block\n // is truly black so all indices stay 0 and the decoded value is 0.\n if (c0 == c1) {\n if (c1 > 0u) {\n c1 = c1 - 1u;\n } else {\n c0 = c0 + 1u;\n }\n } else if (c0 < c1) {\n let tmp = c0;\n c0 = c1;\n c1 = tmp;\n }\n\n // Build the palette in the decoded colour space so index selection\n // matches what the hardware decoder will produce.\n let p0 = from565(c0);\n let p1 = from565(c1);\n let p2 = (2.0 * p0 + p1) * (1.0 / 3.0);\n let p3 = (p0 + 2.0 * p1) * (1.0 / 3.0);\n\n var indices: u32 = 0u;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let c = pixels[i];\n let d0 = dot(c - p0, c - p0);\n let d1 = dot(c - p1, c - p1);\n let d2 = dot(c - p2, c - p2);\n let d3 = dot(c - p3, c - p3);\n\n var best_d: f32 = d0;\n var best_i: u32 = 0u;\n if (d1 < best_d) { best_d = d1; best_i = 1u; }\n if (d2 < best_d) { best_d = d2; best_i = 2u; }\n if (d3 < best_d) { best_d = d3; best_i = 3u; }\n\n indices = indices | (best_i << (i * 2u));\n }\n\n let out = block_index * 2u;\n dst[out] = c0 | (c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
377
+ var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// We always force the 4-color mode (color0 > color1, numeric 16-bit):\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): bounding-box endpoints, inset by ~half a 565 cell, then\n// a single least-squares endpoint refit (the refit is accepted only if it\n// lowers the block's squared error). This is what the WebGL2 fragment\n// fallback runs too.\n// high (1): endpoints are seeded from the block's principal colour axis\n// (covariance power-iteration) as well as the bbox diagonal, each refined by\n// several least-squares passes; the lower-error family wins. Mirrors\n// bc1_ref.ts. Strictly \u2265 fast in quality, at the cost of the eigen-solve.\n//\n// Algorithm per block:\n// 1. Load the 16 pixels; compute the bounding box (and, for high, the mean).\n// 2. Seed endpoints (bbox diagonal; high also tries the principal axis).\n// 3. Quantize to RGB565, force 4-color mode, assign each pixel its nearest\n// palette entry (full 4-entry L2 search in the decoded colour space).\n// 4. Least-squares refit: re-solve the endpoints for the current indices,\n// re-quantize, re-assign; keep the result only when error decreases.\n\n// 0 = fast (default), 1 = high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = f32((c >> 11u) & 31u);\n let g = f32((c >> 5u) & 63u);\n let b = f32( c & 31u);\n // 5/6-bit -> 8-bit. floor((x*527+23)/64) == (x<<3)|(x>>2), i.e. the exact\n // bit-replication a BC1 decoder performs (white -> 255). The inputs are\n // small integers and /64 is exact in f32, so this matches the hardware and\n // is portable. Selecting indices against this palette is what makes the\n // encoder agree with what the GPU will actually sample.\n let r8 = floor((r * 527.0 + 23.0) / 64.0);\n let g8 = floor((g * 259.0 + 33.0) / 64.0);\n let b8 = floor((b * 527.0 + 23.0) / 64.0);\n return vec3<f32>(r8, g8, b8) / 255.0;\n}\n\n// 4-color-mode interpolation weights: palette[j] = wa(j)*c0 + wb(j)*c1.\nfn wa(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 2.0 / 3.0; }\n default: { return 1.0 / 3.0; } // case 3u\n }\n}\nfn wb(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 3.0; }\n default: { return 2.0 / 3.0; } // case 3u\n }\n}\n\nfn build_palette(c0: u32, c1: u32, pal: ptr<function, array<vec3<f32>, 4>>) {\n let p0 = from565(c0);\n let p1 = from565(c1);\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n (*pal)[j] = wa(j) * p0 + wb(j) * p1;\n }\n}\n\n// Assign each of the 16 pixels its nearest palette entry (full 4-entry L2),\n// writing indices into `out_idx` and returning the total squared error.\nfn assign_indices(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n pal: ptr<function, array<vec3<f32>, 4>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> f32 {\n var err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let c = (*pixels)[k];\n var best_j: u32 = 0u;\n var best_d: f32 = 1e30;\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n let d = (*pal)[j] - c;\n let d2 = dot(d, d);\n if (d2 < best_d) {\n best_d = d2;\n best_j = j;\n }\n }\n (*out_idx)[k] = best_j;\n err = err + best_d;\n }\n return err;\n}\n\n// One least-squares refit pass: solve the 2x2 normal equations for the endpoint\n// colours that minimise \u03A3\u2016wa\xB7e0 + wb\xB7e1 \u2212 c\u2016\xB2 under the current indices. The\n// three channels share the scalar sums, so it's one 2x2 solve with vec3 RHS.\nstruct RefitResult { e0: vec3<f32>, e1: vec3<f32>, valid: bool };\nfn refit(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec3<f32> = vec3<f32>(0.0);\n var sBV: vec3<f32> = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = wa((*indices)[k]);\n let b = wb((*indices)[k]);\n let v = (*pixels)[k];\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n var out: RefitResult;\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n out.e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.valid = true;\n return out;\n}\n\n// Candidate solution tracked across endpoint seeds / refit passes.\nstruct Best { c0: u32, c1: u32, indices: array<u32, 16>, err: f32 };\n\n// Quantize (hi, lo) to 565, force 4-color mode, assign indices, then refine with\n// up to `max_refits` least-squares passes. Commits to `*best` only on strict\n// improvement.\nfn fit_from_endpoints(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n hi: vec3<f32>,\n lo: vec3<f32>,\n max_refits: u32,\n best: ptr<function, Best>,\n) {\n var c0 = to565(hi);\n var c1 = to565(lo);\n // 4-color mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n\n var pal: array<vec3<f32>, 4>;\n var idx: array<u32, 16>;\n build_palette(c0, c1, &pal);\n var err = assign_indices(pixels, &pal, &idx);\n if (err < (*best).err) {\n (*best).c0 = c0; (*best).c1 = c1; (*best).indices = idx; (*best).err = err;\n }\n\n for (var rp: u32 = 0u; rp < max_refits; rp = rp + 1u) {\n let r = refit(pixels, &idx);\n if (!r.valid) { break; }\n var nc0 = to565(r.e0);\n var nc1 = to565(r.e1);\n // A refit that flips/equalises the endpoints would change decode mode;\n // keep 4-color mode, and stop once it stops moving.\n if (nc0 < nc1) { let t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 == nc1) { break; }\n if (nc0 == c0 && nc1 == c1) { break; }\n build_palette(nc0, nc1, &pal);\n let nerr = assign_indices(pixels, &pal, &idx);\n c0 = nc0; c1 = nc1; err = nerr;\n if (nerr < (*best).err) {\n (*best).c0 = nc0; (*best).c1 = nc1; (*best).indices = idx; (*best).err = nerr;\n }\n }\n}\n\n// Principal colour axis via covariance power-iteration, seeded with the bbox\n// diagonal. Returns a unit axis, or vec3(0) for a degenerate (constant) block.\nfn principal_axis(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\n // Symmetric 3x3 covariance, stored as its three rows.\n var c0v = vec3<f32>(0.0);\n var c1v = vec3<f32>(0.0);\n var c2v = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (*pixels)[k] - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec3<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec3<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n var mean = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n mean = mean + c;\n }\n mean = mean * (1.0 / 16.0);\n\n // Inset the bounding box by ~half an RGB565 cell (1/16) so the quantized\n // 4-color palette covers the real data range more tightly (stb_dxt heuristic).\n let inset = (bb_max - bb_min) / 16.0;\n let bbox_hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n let bbox_lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n\n var best: Best;\n best.err = 1e30;\n\n if (QUALITY_HIGH != 0u) {\n // Seed from the principal colour axis: project all texels onto it, take the\n // extreme projections as endpoints, inset along the axis. Then also try the\n // bbox seed and keep whichever family yields the lower error.\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n let pad = (t_max - t_min) / 16.0;\n let pca_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n let pca_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n fit_from_endpoints(&pixels, pca_hi, pca_lo, 3u, &best);\n }\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 3u, &best);\n } else {\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 1u, &best);\n }\n\n var indices: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices = indices | ((best.indices[k] & 3u) << (k * 2u));\n }\n\n let out = block_index * 2u;\n dst[out] = best.c0 | (best.c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
369
378
 
370
379
  // src/BC1Encoder.ts
371
380
  var BC1Encoder = class extends Encoder {
@@ -380,6 +389,9 @@ var BC1Encoder = class extends Encoder {
380
389
  get supportsSrgb() {
381
390
  return true;
382
391
  }
392
+ get supportsQuality() {
393
+ return true;
394
+ }
383
395
  wgslSource() {
384
396
  return bc1_default;
385
397
  }
@@ -585,6 +597,373 @@ var ASTC4x4Encoder = class extends Encoder {
585
597
  }
586
598
  };
587
599
 
600
+ // src/webgl/glsl/fullscreen.vert.glsl
601
+ var fullscreen_vert_default = "#version 300 es\n// Fullscreen-triangle vertex shader for the WebGL block encoders.\n//\n// Draws a single oversized triangle covering the viewport from gl_VertexID\n// alone \u2014 no vertex buffers / attributes needed (drawArrays(TRIANGLES, 0, 3)).\n// The encoder sets the viewport to (blocks_x \xD7 blocks_y), so each rasterised\n// fragment corresponds to exactly one 4\xD74 output block.\n//\n// id 0 -> (-1,-1) id 1 -> ( 3,-1) id 2 -> (-1, 3)\n\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
602
+
603
+ // src/webgl/webglContext.ts
604
+ var CONTEXT_ATTRS = {
605
+ alpha: false,
606
+ antialias: false,
607
+ depth: false,
608
+ stencil: false,
609
+ premultipliedAlpha: false,
610
+ preserveDrawingBuffer: false,
611
+ // Encoding is GPU-bound; prefer the discrete GPU when the browser exposes a
612
+ // choice. Ignored where unsupported.
613
+ powerPreference: "high-performance"
614
+ };
615
+ function createWebGLContext() {
616
+ if (typeof OffscreenCanvas !== "undefined") {
617
+ const gl = new OffscreenCanvas(1, 1).getContext("webgl2", CONTEXT_ATTRS);
618
+ return gl ?? null;
619
+ }
620
+ if (typeof document !== "undefined") {
621
+ return document.createElement("canvas").getContext("webgl2", CONTEXT_ATTRS);
622
+ }
623
+ return null;
624
+ }
625
+ var sharedContext;
626
+ function getSharedWebGLContext() {
627
+ if (sharedContext === void 0 || sharedContext !== null && sharedContext.isContextLost()) {
628
+ sharedContext = createWebGLContext();
629
+ }
630
+ return sharedContext;
631
+ }
632
+ function isWebGLAvailable() {
633
+ return getSharedWebGLContext() !== null;
634
+ }
635
+
636
+ // src/webgl/WebGLBlockEncoder.ts
637
+ function compileShader(gl, type, source, label) {
638
+ const shader = gl.createShader(type);
639
+ if (!shader) throw new Error(`${label}: gl.createShader failed`);
640
+ gl.shaderSource(shader, source);
641
+ gl.compileShader(shader);
642
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
643
+ const log = gl.getShaderInfoLog(shader);
644
+ gl.deleteShader(shader);
645
+ const kind = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
646
+ throw new Error(`${label}: ${kind} shader compile failed: ${log}`);
647
+ }
648
+ return shader;
649
+ }
650
+ var WebGLBlockEncoder = class {
651
+ /**
652
+ * Create an encoder on the shared process-wide context (or a caller-supplied
653
+ * one). Throws when WebGL2 is unavailable. The `this:` annotation lets
654
+ * `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
655
+ */
656
+ static create(gl) {
657
+ const ctx = gl ?? getSharedWebGLContext();
658
+ if (!ctx) throw new Error("WebGL2 not available in this environment");
659
+ return new this({ gl: ctx });
660
+ }
661
+ gl;
662
+ // Set in _buildProgram(), which the constructor calls.
663
+ _program;
664
+ _vao;
665
+ _uSrc = null;
666
+ _uSrcSize = null;
667
+ _uFlipY = null;
668
+ constructor({ gl }) {
669
+ this.gl = gl;
670
+ this._buildProgram();
671
+ }
672
+ _buildProgram() {
673
+ const gl = this.gl;
674
+ const program = gl.createProgram();
675
+ const vao = gl.createVertexArray();
676
+ if (!program || !vao) throw new Error(`${this.label}: failed to allocate WebGL program/VAO`);
677
+ const vert = compileShader(gl, gl.VERTEX_SHADER, fullscreen_vert_default, this.label);
678
+ const frag = compileShader(gl, gl.FRAGMENT_SHADER, this.fragSource(), this.label);
679
+ gl.attachShader(program, vert);
680
+ gl.attachShader(program, frag);
681
+ gl.linkProgram(program);
682
+ gl.deleteShader(vert);
683
+ gl.deleteShader(frag);
684
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
685
+ const log = gl.getProgramInfoLog(program);
686
+ gl.deleteProgram(program);
687
+ throw new Error(`${this.label}: WebGL program link failed: ${log}`);
688
+ }
689
+ this._program = program;
690
+ this._vao = vao;
691
+ this._uSrc = gl.getUniformLocation(program, "uSrc");
692
+ this._uSrcSize = gl.getUniformLocation(program, "uSrcSize");
693
+ this._uFlipY = gl.getUniformLocation(program, "uFlipY");
694
+ }
695
+ /** Release the GL program + VAO. The shared context itself is left intact. */
696
+ destroy() {
697
+ const gl = this.gl;
698
+ if (gl.isContextLost()) return;
699
+ gl.deleteProgram(this._program);
700
+ gl.deleteVertexArray(this._vao);
701
+ }
702
+ /**
703
+ * Upload the source image to a freshly created RGBA8 texture bound on unit 0.
704
+ * Raw pixel sources (ImageData / mip levels) go through the typed-array
705
+ * overload; DOM sources (ImageBitmap / canvas / image) through the element
706
+ * overload. No flip / premultiply / colour conversion — flipY is applied in
707
+ * the shader so each mip level flips by its own height.
708
+ */
709
+ _uploadSource(source, width, height) {
710
+ const gl = this.gl;
711
+ const tex = gl.createTexture();
712
+ if (!tex) throw new Error(`${this.label}: gl.createTexture failed`);
713
+ gl.activeTexture(gl.TEXTURE0);
714
+ gl.bindTexture(gl.TEXTURE_2D, tex);
715
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
716
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
717
+ gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
718
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
719
+ const raw = source;
720
+ if (raw.data && ArrayBuffer.isView(raw.data)) {
721
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, raw.data);
722
+ } else {
723
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, source);
724
+ }
725
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
726
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
727
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
728
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
729
+ return tex;
730
+ }
731
+ /**
732
+ * Encode one image source to raw compressed bytes. `flipY` samples the source
733
+ * bottom-up (matching Three.js's convention) and is applied in the shader;
734
+ * the high-level mipped path bakes the flip into level 0 and passes false.
735
+ */
736
+ encodeToBytes(source, { flipY = false } = {}) {
737
+ const gl = this.gl;
738
+ if (gl.isContextLost()) throw new Error(`${this.label}WebGLEncoder: WebGL context lost`);
739
+ const width = source.width;
740
+ const height = source.height;
741
+ if (!width || !height) {
742
+ throw new Error(`${this.label}WebGLEncoder: source has no dimensions`);
743
+ }
744
+ const paddedWidth = width + 3 & ~3;
745
+ const paddedHeight = height + 3 & ~3;
746
+ const blocksX = paddedWidth >> 2;
747
+ const blocksY = paddedHeight >> 2;
748
+ const blockCount = blocksX * blocksY;
749
+ const outByteLen = blockCount * this.bytesPerBlock;
750
+ const t0 = performance.now();
751
+ const srcTex = this._uploadSource(source, width, height);
752
+ const outTex = gl.createTexture();
753
+ const fbo = gl.createFramebuffer();
754
+ if (!outTex || !fbo) {
755
+ gl.deleteTexture(srcTex);
756
+ if (outTex) gl.deleteTexture(outTex);
757
+ if (fbo) gl.deleteFramebuffer(fbo);
758
+ throw new Error(`${this.label}: failed to allocate output texture/framebuffer`);
759
+ }
760
+ gl.bindTexture(gl.TEXTURE_2D, outTex);
761
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32UI, blocksX, blocksY);
762
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
763
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
764
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
765
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outTex, 0);
766
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
767
+ if (status !== gl.FRAMEBUFFER_COMPLETE) {
768
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
769
+ gl.deleteFramebuffer(fbo);
770
+ gl.deleteTexture(outTex);
771
+ gl.deleteTexture(srcTex);
772
+ throw new Error(`${this.label}: integer framebuffer incomplete (0x${status.toString(16)})`);
773
+ }
774
+ gl.useProgram(this._program);
775
+ gl.bindVertexArray(this._vao);
776
+ gl.activeTexture(gl.TEXTURE0);
777
+ gl.bindTexture(gl.TEXTURE_2D, srcTex);
778
+ gl.uniform1i(this._uSrc, 0);
779
+ gl.uniform2i(this._uSrcSize, width, height);
780
+ gl.uniform1i(this._uFlipY, flipY ? 1 : 0);
781
+ gl.disable(gl.BLEND);
782
+ gl.disable(gl.DEPTH_TEST);
783
+ gl.disable(gl.SCISSOR_TEST);
784
+ gl.viewport(0, 0, blocksX, blocksY);
785
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
786
+ const words = new Uint32Array(blockCount * 4);
787
+ gl.readPixels(0, 0, blocksX, blocksY, gl.RGBA_INTEGER, gl.UNSIGNED_INT, words);
788
+ let data;
789
+ if (this.bytesPerBlock === 16) {
790
+ data = new Uint8Array(words.buffer, 0, outByteLen);
791
+ } else {
792
+ const packed = new Uint32Array(blockCount * 2);
793
+ for (let k = 0; k < blockCount; k++) {
794
+ packed[k * 2] = words[k * 4];
795
+ packed[k * 2 + 1] = words[k * 4 + 1];
796
+ }
797
+ data = new Uint8Array(packed.buffer, 0, outByteLen);
798
+ }
799
+ const encodeMs = performance.now() - t0;
800
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
801
+ gl.bindTexture(gl.TEXTURE_2D, null);
802
+ gl.bindVertexArray(null);
803
+ gl.deleteFramebuffer(fbo);
804
+ gl.deleteTexture(outTex);
805
+ gl.deleteTexture(srcTex);
806
+ return { width, height, paddedWidth, paddedHeight, data, encodeMs };
807
+ }
808
+ /** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
809
+ buildMippedTexture(levels, { colorSpace = "srgb" } = {}) {
810
+ if (levels.length === 0) {
811
+ throw new Error(`${this.label}WebGLEncoder.buildMippedTexture: no levels provided`);
812
+ }
813
+ const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
814
+ return assembleCompressedTexture(levels, this.threeTextureFormat(), effectiveSrgb);
815
+ }
816
+ };
817
+
818
+ // src/webgl/BC1WebGLEncoder.ts
819
+ import { RGBA_S3TC_DXT1_Format as RGBA_S3TC_DXT1_Format2 } from "three";
820
+
821
+ // src/webgl/glsl/bc1.frag.glsl
822
+ var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl (fast path).\n//\n// One fragment per 4\xD74 block. Output is the 8-byte BC1 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused); the encoder reads back RGBA32UI and keeps\n// the low two words per block. This is the *fast* path only (the WGSL\n// `QUALITY_HIGH == 0` branch): bbox endpoints, 1/16 inset, RGB565 quantisation,\n// forced 4-colour mode, full 4-entry L2 index search, then a single\n// least-squares endpoint refit accepted only when it lowers the block's error.\n// See bc1.wgsl for the full derivation.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize; // original (unpadded) width, height\nuniform int uFlipY; // 1 = sample bottom-up (matches Three.js flipY)\n\nlayout(location = 0) out uvec4 outColor;\n\n// 4-colour-mode interpolation weights: pal[j] = WA[j]*c0 + WB[j]*c1.\nconst float WA[4] = float[4](1.0, 0.0, 2.0 / 3.0, 1.0 / 3.0);\nconst float WB[4] = float[4](0.0, 1.0, 1.0 / 3.0, 2.0 / 3.0);\n\nuint to565(vec3 c) {\n uint r = uint(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n uint g = uint(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n uint b = uint(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11) | (g << 5) | b;\n}\n\nvec3 from565(uint c) {\n float r = float((c >> 11) & 31u);\n float g = float((c >> 5) & 63u);\n float b = float(c & 31u);\n // 5/6-bit \u2192 8-bit. floor((x*527+23)/64) == (x<<3)|(x>>2): exact hardware\n // bit-replication (white \u2192 255), so index selection matches the GPU decode.\n float r8 = floor((r * 527.0 + 23.0) / 64.0);\n float g8 = floor((g * 259.0 + 33.0) / 64.0);\n float b8 = floor((b * 527.0 + 23.0) / 64.0);\n return vec3(r8, g8, b8) / 255.0;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n vec3 pixels[16];\n vec3 bbMin = vec3(1.0);\n vec3 bbMax = vec3(0.0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec3 c = texelFetch(uSrc, ivec2(p.x, sy), 0).rgb;\n pixels[i] = c;\n bbMin = min(bbMin, c);\n bbMax = max(bbMax, c);\n }\n\n // Inset the bbox by ~half an RGB565 cell (1/16) to tighten the quantised\n // 4-colour palette around the real data range.\n vec3 inset = (bbMax - bbMin) / 16.0;\n vec3 hi = clamp(bbMax - inset, vec3(0.0), vec3(1.0));\n vec3 lo = clamp(bbMin + inset, vec3(0.0), vec3(1.0));\n\n uint c0 = to565(hi);\n uint c1 = to565(lo);\n // 4-colour mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n uint tmp = c0; c0 = c1; c1 = tmp;\n }\n\n // Build the palette in decoded space, assign each pixel its nearest entry.\n vec3 pal[4];\n vec3 p0 = from565(c0);\n vec3 p1 = from565(c1);\n for (int j = 0; j < 4; j++) pal[j] = WA[j] * p0 + WB[j] * p1;\n\n uint idx[16];\n float err = 0.0;\n for (int k = 0; k < 16; k++) {\n vec3 c = pixels[k];\n uint bestJ = 0u;\n float bestD = 1e30;\n for (int j = 0; j < 4; j++) {\n vec3 d = pal[j] - c;\n float d2 = dot(d, d);\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n idx[k] = bestJ;\n err += bestD;\n }\n\n // One least-squares refit: re-solve the endpoints for the current indices,\n // re-quantise, re-assign; keep it only if the squared error drops.\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec3 sAV = vec3(0.0), sBV = vec3(0.0);\n for (int k = 0; k < 16; k++) {\n float a = WA[int(idx[k])];\n float b = WB[int(idx[k])];\n vec3 v = pixels[k];\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) > 1e-9) {\n vec3 e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3(0.0), vec3(1.0));\n vec3 e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3(0.0), vec3(1.0));\n uint nc0 = to565(e0);\n uint nc1 = to565(e1);\n if (nc0 < nc1) { uint t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 != nc1 && !(nc0 == c0 && nc1 == c1)) {\n vec3 q0 = from565(nc0);\n vec3 q1 = from565(nc1);\n vec3 pal2[4];\n for (int j = 0; j < 4; j++) pal2[j] = WA[j] * q0 + WB[j] * q1;\n uint idx2[16];\n float nerr = 0.0;\n for (int k = 0; k < 16; k++) {\n vec3 c = pixels[k];\n uint bestJ = 0u;\n float bestD = 1e30;\n for (int j = 0; j < 4; j++) {\n vec3 d = pal2[j] - c;\n float d2 = dot(d, d);\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n idx2[k] = bestJ;\n nerr += bestD;\n }\n if (nerr < err) {\n c0 = nc0; c1 = nc1;\n for (int k = 0; k < 16; k++) idx[k] = idx2[k];\n }\n }\n }\n\n uint indices = 0u;\n for (int k = 0; k < 16; k++) indices |= (idx[k] & 3u) << (uint(k) * 2u);\n\n outColor = uvec4(c0 | (c1 << 16), indices, 0u, 0u);\n}\n";
823
+
824
+ // src/webgl/BC1WebGLEncoder.ts
825
+ var BC1WebGLEncoder = class extends WebGLBlockEncoder {
826
+ get label() {
827
+ return "bc1";
828
+ }
829
+ get bytesPerBlock() {
830
+ return 8;
831
+ }
832
+ get supportsSrgb() {
833
+ return true;
834
+ }
835
+ fragSource() {
836
+ return bc1_frag_default;
837
+ }
838
+ threeTextureFormat() {
839
+ return RGBA_S3TC_DXT1_Format2;
840
+ }
841
+ };
842
+
843
+ // src/webgl/BC5WebGLEncoder.ts
844
+ import { RED_GREEN_RGTC2_Format as RED_GREEN_RGTC2_Format2 } from "three";
845
+
846
+ // src/webgl/glsl/bc5.frag.glsl
847
+ var bc5_frag_default = "#version 300 es\n// BC5 (RGTC2) fragment-shader encoder \u2014 WebGL2 port of bc5.wgsl (fast path).\n//\n// One fragment per 4\xD74 block \u2192 16-byte BC5 block as 4 \xD7 u32 in outColor.\n// BC5 = two BC4 halves (R then G). This is the *fast* path only: bbox\n// endpoints + a single full-L2 index assignment per channel, no LSQ refit\n// (the WGSL `QUALITY_HIGH` branch). Always emits 6-interpolation mode\n// (red0 > red1). See bc5.wgsl for the full derivation.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// 6-interpolation-mode palette weights: pal[j] = W0_6[j]*r0 + W1_6[j]*r1.\nconst float W0_6[8] = float[8](1.0, 0.0, 6.0 / 7.0, 5.0 / 7.0, 4.0 / 7.0, 3.0 / 7.0, 2.0 / 7.0, 1.0 / 7.0);\nconst float W1_6[8] = float[8](0.0, 1.0, 1.0 / 7.0, 2.0 / 7.0, 3.0 / 7.0, 4.0 / 7.0, 5.0 / 7.0, 6.0 / 7.0);\n\nuint quantize8(float v) {\n return uint(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// Encode 16 single-channel values into an 8-byte BC4 block (two little-endian\n// u32s). Mirrors encode_bc4() in bc5.wgsl with the refit pass omitted.\nuvec2 encodeBC4(float values[16]) {\n float vmin = 1.0;\n float vmax = 0.0;\n for (int k = 0; k < 16; k++) {\n vmin = min(vmin, values[k]);\n vmax = max(vmax, values[k]);\n }\n uint r0 = quantize8(vmax);\n uint r1 = quantize8(vmin);\n if (r0 == r1) {\n if (r1 > 0u) { r1 = r1 - 1u; } else { r0 = r0 + 1u; }\n }\n\n float pal[8];\n float r0f = float(r0) / 255.0;\n float r1f = float(r1) / 255.0;\n for (int j = 0; j < 8; j++) {\n pal[j] = W0_6[j] * r0f + W1_6[j] * r1f;\n }\n\n uint indices[16];\n for (int k = 0; k < 16; k++) {\n float v = values[k];\n uint bestJ = 0u;\n float bestD = 1e20;\n for (int j = 0; j < 8; j++) {\n float d = pal[j] - v;\n float d2 = d * d;\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n indices[k] = bestJ;\n }\n\n // Pack the 48-bit index field (bytes 2..7) split across two u32 halves.\n uint idxLo = 0u;\n uint idxHi = 0u;\n for (int k = 0; k < 16; k++) {\n uint bit = 3u * uint(k);\n uint v = indices[k] & 7u;\n if (bit + 3u <= 32u) {\n idxLo = idxLo | (v << bit);\n } else if (bit >= 32u) {\n idxHi = idxHi | (v << (bit - 32u));\n } else {\n idxLo = idxLo | (v << bit);\n idxHi = idxHi | (v >> (32u - bit));\n }\n }\n\n uint outLo = r0 | (r1 << 8) | ((idxLo & 0xFFFFu) << 16);\n uint outHi = (idxLo >> 16) | (idxHi << 16);\n return uvec2(outLo, outHi);\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n float rValues[16];\n float gValues[16];\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec4 c = texelFetch(uSrc, ivec2(p.x, sy), 0);\n rValues[i] = c.r;\n gValues[i] = c.g;\n }\n\n uvec2 rBlock = encodeBC4(rValues);\n uvec2 gBlock = encodeBC4(gValues);\n outColor = uvec4(rBlock.x, rBlock.y, gBlock.x, gBlock.y);\n}\n";
848
+
849
+ // src/webgl/BC5WebGLEncoder.ts
850
+ var BC5WebGLEncoder = class extends WebGLBlockEncoder {
851
+ get label() {
852
+ return "bc5";
853
+ }
854
+ get bytesPerBlock() {
855
+ return 16;
856
+ }
857
+ get supportsSrgb() {
858
+ return false;
859
+ }
860
+ fragSource() {
861
+ return bc5_frag_default;
862
+ }
863
+ threeTextureFormat() {
864
+ return RED_GREEN_RGTC2_Format2;
865
+ }
866
+ };
867
+
868
+ // src/webgl/BC7WebGLEncoder.ts
869
+ import { RGBA_BPTC_Format as RGBA_BPTC_Format2 } from "three";
870
+
871
+ // src/webgl/glsl/bc7.frag.glsl
872
+ var bc7_frag_default = "#version 300 es\n// BC7 (BPTC) mode-6 fragment-shader encoder \u2014 WebGL2 port of bc7.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Fast path\n// only: O(N) bbox seed \u2192 endpoints fitted by a single least-squares pass whose\n// normal-equation sums are accumulated during a projection-based index\n// assignment (palette is colinear, so the nearest entry is found by projecting\n// onto the endpoint line \u2014 O(1) per pixel). Mirrors the `QUALITY_HIGH == 0`\n// branch of bc7.wgsl; see that file for the mode-6 bit layout and rationale.\n//\n// Determinism note: the WGSL refit uses round() (half-to-even); here we use\n// floor(x + 0.5) for portability. The two differ only at exact .5 ties, a\n// sub-LSB endpoint nudge that is visually identical.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// Per-invocation scratch (mirrors the WGSL function-scope arrays passed by ptr).\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct QuantPair { ivec4 seven; ivec4 eight; };\nstruct Ep { ivec4 seven; ivec4 eight; uint p; };\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\nint dist2(ivec4 a, ivec4 b) {\n ivec4 d = a - b;\n ivec4 e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit) under\n// a fixed p-bit, all four channels at once.\nQuantPair quantizeEndpoint(ivec4 ideal8, uint p) {\n ivec4 q = ivec4(clamp(floor((vec4(ideal8) - float(p)) / 2.0 + 0.5), vec4(0.0), vec4(127.0)));\n // eff = (q << 1) | p. q*2 is even and p \u2208 {0,1}, so q*2 + p is identical and\n // avoids any vector-shift-by-scalar portability question.\n ivec4 eff = q * 2 + ivec4(int(p));\n return QuantPair(q, eff);\n}\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nEp pickEp(ivec4 ideal) {\n QuantPair a = quantizeEndpoint(ideal, 0u);\n QuantPair b = quantizeEndpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) {\n return Ep(b.seven, b.eight, 1u);\n }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Projection index assignment over gPixels \u2192 gIdx. When `fit`, accumulate the\n// LSQ normal-equation sums in the same pass and return refitted endpoints.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 15.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 15.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 15.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n Ep ep0 = pickEp(lo);\n Ep ep1 = pickEp(hi);\n Fit r = projAssign(ep0.eight, ep1.eight, true);\n if (r.valid) {\n ep0 = pickEp(r.e0);\n ep1 = pickEp(r.e1);\n projAssign(ep0.eight, ep1.eight, false);\n }\n ivec4 e0_7 = ep0.seven;\n ivec4 e1_7 = ep1.seven;\n uint p0 = ep0.p;\n uint p1 = ep1.p;\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0; otherwise swap endpoints and\n // reflect every index (decoded image unchanged).\n if ((gIdx[0] & 0x8u) != 0u) {\n ivec4 t = e0_7; e0_7 = e1_7; e1_7 = t;\n uint tp = p0; p0 = p1; p1 = tp;\n for (int k = 0; k < 16; k++) { gIdx[k] = 15u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n uint pos = 0u;\n writeBits(block, pos, 7u, 0x40u); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.w)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.w)); pos += 7u;\n writeBits(block, pos, 1u, p0); pos += 1u;\n writeBits(block, pos, 1u, p1); pos += 1u;\n writeBits(block, pos, 3u, gIdx[0] & 0x7u); pos += 3u;\n for (int k = 1; k < 16; k++) {\n writeBits(block, pos, 4u, gIdx[k] & 0xFu);\n pos += 4u;\n }\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
873
+
874
+ // src/webgl/BC7WebGLEncoder.ts
875
+ var BC7WebGLEncoder = class extends WebGLBlockEncoder {
876
+ get label() {
877
+ return "bc7";
878
+ }
879
+ get bytesPerBlock() {
880
+ return 16;
881
+ }
882
+ get supportsSrgb() {
883
+ return true;
884
+ }
885
+ fragSource() {
886
+ return bc7_frag_default;
887
+ }
888
+ threeTextureFormat() {
889
+ return RGBA_BPTC_Format2;
890
+ }
891
+ };
892
+
893
+ // src/webgl/ASTC4x4WebGLEncoder.ts
894
+ import { RGBA_ASTC_4x4_Format as RGBA_ASTC_4x4_Format2 } from "three";
895
+
896
+ // src/webgl/glsl/astc4x4.frag.glsl
897
+ var astc4x4_frag_default = "#version 300 es\n// ASTC 4\xD74 LDR fragment-shader encoder \u2014 WebGL2 port of astc4x4.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Restricted\n// subset: single partition, no dual-plane, CEM 12 (LDR RGBA direct), 4\xD74 weight\n// grid with 2-bit weights (QUANT_4), 8-bit endpoints (QUANT_256). Fast path:\n// bbox seed \u2192 one LSQ refit fused into a projection weight assignment (4 colinear\n// levels). Mirrors the `QUALITY_HIGH == 0` branch of astc4x4.wgsl; see that file\n// for the 128-bit block layout.\n//\n// Determinism note: floor(x + 0.5) replaces WGSL round() for the refit endpoints\n// (sub-LSB difference at exact .5 ties only).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\n// Projection weight assignment over 4 levels (QUANT_4 \u2248 thirds), with the LSQ\n// normal-equation sums accumulated in the same pass for a fused refit.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 3.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 3.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 3.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n ivec4 e0 = lo;\n ivec4 e1 = hi;\n Fit r = projAssign(e0, e1, true);\n if (r.valid) {\n e0 = r.e0;\n e1 = r.e1;\n projAssign(e0, e1, false);\n }\n\n // Endpoint ordering so the decoder doesn't apply blue contraction.\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n ivec4 t = e0; e0 = e1; e1 = t;\n for (int k = 0; k < 16; k++) { gIdx[k] = 3u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n writeBits(block, 0u, 11u, 0x042u);\n writeBits(block, 11u, 2u, 0u);\n writeBits(block, 13u, 4u, 12u);\n writeBits(block, 17u + 0u * 8u, 8u, uint(e0.x));\n writeBits(block, 17u + 1u * 8u, 8u, uint(e1.x));\n writeBits(block, 17u + 2u * 8u, 8u, uint(e0.y));\n writeBits(block, 17u + 3u * 8u, 8u, uint(e1.y));\n writeBits(block, 17u + 4u * 8u, 8u, uint(e0.z));\n writeBits(block, 17u + 5u * 8u, 8u, uint(e1.z));\n writeBits(block, 17u + 6u * 8u, 8u, uint(e0.w));\n writeBits(block, 17u + 7u * 8u, 8u, uint(e1.w));\n\n uint w3 = 0u;\n for (int k = 0; k < 16; k++) {\n uint w = gIdx[k] & 0x3u;\n w3 = w3 | ((w & 1u) << (31u - 2u * uint(k))) | (((w >> 1u) & 1u) << (30u - 2u * uint(k)));\n }\n block[3] = w3;\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
898
+
899
+ // src/webgl/ASTC4x4WebGLEncoder.ts
900
+ var ASTC4x4WebGLEncoder = class extends WebGLBlockEncoder {
901
+ get label() {
902
+ return "astc4x4";
903
+ }
904
+ get bytesPerBlock() {
905
+ return 16;
906
+ }
907
+ get supportsSrgb() {
908
+ return true;
909
+ }
910
+ fragSource() {
911
+ return astc4x4_frag_default;
912
+ }
913
+ threeTextureFormat() {
914
+ return RGBA_ASTC_4x4_Format2;
915
+ }
916
+ };
917
+
918
+ // src/webgl/webglCapabilities.ts
919
+ function detectWebGLCapabilities(gl) {
920
+ if (!gl || typeof gl.getExtension !== "function") {
921
+ throw new TypeError("detectWebGLCapabilities: a WebGL2 context (or { getExtension }) is required");
922
+ }
923
+ const has = (name) => gl.getExtension(name) != null;
924
+ return {
925
+ bptc: has("EXT_texture_compression_bptc"),
926
+ rgtc: has("EXT_texture_compression_rgtc"),
927
+ s3tc: has("WEBGL_compressed_texture_s3tc"),
928
+ s3tcSrgb: has("WEBGL_compressed_texture_s3tc_srgb"),
929
+ astc: has("WEBGL_compressed_texture_astc")
930
+ };
931
+ }
932
+
933
+ // src/webgl/selectWebGLFormat.ts
934
+ var NONE = { format: null, encoderClass: null, astcNormalRemap: false };
935
+ function selectWebGLFormat(caps, hint, options = {}) {
936
+ const { colorSpace = "srgb" } = options;
937
+ const srgb = colorSpace === "srgb";
938
+ const astc = (astcNormalRemap) => ({
939
+ format: srgb ? TextureFormat.ASTC_4x4_SRGB : TextureFormat.ASTC_4x4,
940
+ encoderClass: ASTC4x4WebGLEncoder,
941
+ astcNormalRemap
942
+ });
943
+ if (hint === "normal") {
944
+ if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
945
+ if (caps.astc) return astc(true);
946
+ return NONE;
947
+ }
948
+ if (caps.bptc) {
949
+ return {
950
+ format: srgb ? TextureFormat.BC7_SRGB : TextureFormat.BC7,
951
+ encoderClass: BC7WebGLEncoder,
952
+ astcNormalRemap: false
953
+ };
954
+ }
955
+ if (caps.astc) return astc(false);
956
+ if (hint === "color") {
957
+ if (srgb && caps.s3tcSrgb) {
958
+ return { format: TextureFormat.BC1_SRGB, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
959
+ }
960
+ if (!srgb && caps.s3tc) {
961
+ return { format: TextureFormat.BC1, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
962
+ }
963
+ }
964
+ return NONE;
965
+ }
966
+
588
967
  // src/selectFormat.ts
589
968
  function selectFormat(adapter, hint, options = {}) {
590
969
  const { colorSpace = "srgb" } = options;
@@ -612,7 +991,7 @@ function selectFormat(adapter, hint, options = {}) {
612
991
  }
613
992
 
614
993
  // src/compressTexture.ts
615
- import { LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, RepeatWrapping as RepeatWrapping2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
994
+ import { LinearFilter as LinearFilter3, LinearSRGBColorSpace as LinearSRGBColorSpace3, RepeatWrapping as RepeatWrapping3, SRGBColorSpace as SRGBColorSpace3, Texture } from "three";
616
995
 
617
996
  // src/mipgen.ts
618
997
  function generateMipChain(level0) {
@@ -722,10 +1101,10 @@ function mipLevelToImageData(level) {
722
1101
  }
723
1102
  function wrapUncompressed(bitmap, srgb, flipY) {
724
1103
  const tex = new Texture(bitmap);
725
- tex.colorSpace = srgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
726
- tex.magFilter = LinearFilter2;
727
- tex.minFilter = LinearFilter2;
728
- tex.wrapS = tex.wrapT = RepeatWrapping2;
1104
+ tex.colorSpace = srgb ? SRGBColorSpace3 : LinearSRGBColorSpace3;
1105
+ tex.magFilter = LinearFilter3;
1106
+ tex.minFilter = LinearFilter3;
1107
+ tex.wrapS = tex.wrapT = RepeatWrapping3;
729
1108
  tex.generateMipmaps = false;
730
1109
  tex.flipY = flipY;
731
1110
  tex.needsUpdate = true;
@@ -743,124 +1122,159 @@ async function compressTexture(source, options = {}) {
743
1122
  } = options;
744
1123
  const srgb = colorSpace === "srgb";
745
1124
  const bitmap = await sourceToBitmap(source);
746
- if (!("gpu" in navigator)) {
747
- console.warn("[compressTexture] WebGPU unavailable; returning uncompressed RGBA8.");
748
- const tex = wrapUncompressed(bitmap, srgb, flipY);
749
- return {
750
- texture: tex,
751
- format: null,
752
- fallbackUncompressed: true,
753
- astcNormalRemap: false,
754
- width: bitmap.width,
755
- height: bitmap.height,
756
- mipLevels: 1,
757
- encodeMs: 0,
758
- destroy: () => {
759
- tex.dispose();
1125
+ const viaWebGPU = await encodeViaWebGPU();
1126
+ if (viaWebGPU) return viaWebGPU;
1127
+ const viaWebGL = encodeViaWebGL();
1128
+ if (viaWebGL) return viaWebGL;
1129
+ console.warn(
1130
+ "[compressTexture] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
1131
+ );
1132
+ const tex = wrapUncompressed(bitmap, srgb, flipY);
1133
+ return {
1134
+ texture: tex,
1135
+ format: null,
1136
+ fallbackUncompressed: true,
1137
+ backend: "none",
1138
+ astcNormalRemap: false,
1139
+ width: bitmap.width,
1140
+ height: bitmap.height,
1141
+ mipLevels: 1,
1142
+ encodeMs: 0,
1143
+ destroy: () => {
1144
+ tex.dispose();
1145
+ }
1146
+ };
1147
+ async function encodeViaWebGPU() {
1148
+ if (!("gpu" in navigator)) return null;
1149
+ const adapter = providedAdapter ?? await navigator.gpu.requestAdapter();
1150
+ if (!adapter) return null;
1151
+ const selection = selectFormat(adapter, hint, { colorSpace });
1152
+ if (!selection.format || !selection.encoderClass) return null;
1153
+ let encoder;
1154
+ if (providedDevice) {
1155
+ const EncoderCtor = selection.encoderClass;
1156
+ encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
1157
+ } else {
1158
+ encoder = await selection.encoderClass.create();
1159
+ }
1160
+ try {
1161
+ const needsWriteTexture = needsWriteTextureWorkaround(adapter);
1162
+ if (!mipmaps) {
1163
+ let bytes;
1164
+ if (needsWriteTexture) {
1165
+ const level02 = bitmapToMipLevel(bitmap, flipY);
1166
+ const imageData = mipLevelToImageData(level02);
1167
+ bytes = await encoder.encodeToBytes(imageData, { quality });
1168
+ } else {
1169
+ bytes = await encoder.encodeToBytes(bitmap, { flipY, quality });
1170
+ }
1171
+ const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
1172
+ return {
1173
+ texture: tex3,
1174
+ format: selection.format,
1175
+ fallbackUncompressed: false,
1176
+ backend: "webgpu",
1177
+ astcNormalRemap: selection.astcNormalRemap,
1178
+ width: bytes.width,
1179
+ height: bytes.height,
1180
+ mipLevels: 1,
1181
+ encodeMs: bytes.encodeMs,
1182
+ destroy: () => {
1183
+ tex3.dispose();
1184
+ encoder.destroy();
1185
+ }
1186
+ };
760
1187
  }
761
- };
762
- }
763
- const adapter = providedAdapter ?? await navigator.gpu.requestAdapter();
764
- if (!adapter) {
765
- console.warn("[compressTexture] No WebGPU adapter; returning uncompressed RGBA8.");
766
- const tex = wrapUncompressed(bitmap, srgb, flipY);
767
- return {
768
- texture: tex,
769
- format: null,
770
- fallbackUncompressed: true,
771
- astcNormalRemap: false,
772
- width: bitmap.width,
773
- height: bitmap.height,
774
- mipLevels: 1,
775
- encodeMs: 0,
776
- destroy: () => {
777
- tex.dispose();
1188
+ const level0 = bitmapToMipLevel(bitmap, flipY);
1189
+ const chain = generateMipChain(level0);
1190
+ const encodedLevels = [];
1191
+ let totalEncodeMs = 0;
1192
+ for (const level of chain) {
1193
+ const padded = padToBlockMultiple(level);
1194
+ const imageData = mipLevelToImageData(padded);
1195
+ const bytes = await encoder.encodeToBytes(imageData, { quality });
1196
+ encodedLevels.push(bytes);
1197
+ totalEncodeMs += bytes.encodeMs;
778
1198
  }
779
- };
1199
+ const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
1200
+ return {
1201
+ texture: tex2,
1202
+ format: selection.format,
1203
+ fallbackUncompressed: false,
1204
+ backend: "webgpu",
1205
+ astcNormalRemap: selection.astcNormalRemap,
1206
+ width: level0.width,
1207
+ height: level0.height,
1208
+ mipLevels: encodedLevels.length,
1209
+ encodeMs: totalEncodeMs,
1210
+ destroy: () => {
1211
+ tex2.dispose();
1212
+ encoder.destroy();
1213
+ }
1214
+ };
1215
+ } catch (e) {
1216
+ encoder.destroy();
1217
+ throw e;
1218
+ }
780
1219
  }
781
- const selection = selectFormat(adapter, hint, { colorSpace });
782
- if (!selection.format || !selection.encoderClass) {
783
- console.warn(
784
- "[compressTexture] Adapter reports neither texture-compression-bc nor texture-compression-astc; returning uncompressed RGBA8."
785
- );
786
- const tex = wrapUncompressed(bitmap, srgb, flipY);
787
- return {
788
- texture: tex,
789
- format: null,
790
- fallbackUncompressed: true,
791
- astcNormalRemap: false,
792
- width: bitmap.width,
793
- height: bitmap.height,
794
- mipLevels: 1,
795
- encodeMs: 0,
796
- destroy: () => {
797
- tex.dispose();
1220
+ function encodeViaWebGL() {
1221
+ const gl = getSharedWebGLContext();
1222
+ if (!gl) return null;
1223
+ const caps = detectWebGLCapabilities(gl);
1224
+ const selection = selectWebGLFormat(caps, hint, { colorSpace });
1225
+ if (!selection.format || !selection.encoderClass) return null;
1226
+ const encoder = selection.encoderClass.create(gl);
1227
+ try {
1228
+ if (!mipmaps) {
1229
+ const bytes = encoder.encodeToBytes(bitmap, { flipY });
1230
+ const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
1231
+ return {
1232
+ texture: tex3,
1233
+ format: selection.format,
1234
+ fallbackUncompressed: false,
1235
+ backend: "webgl",
1236
+ astcNormalRemap: selection.astcNormalRemap,
1237
+ width: bytes.width,
1238
+ height: bytes.height,
1239
+ mipLevels: 1,
1240
+ encodeMs: bytes.encodeMs,
1241
+ destroy: () => {
1242
+ tex3.dispose();
1243
+ encoder.destroy();
1244
+ }
1245
+ };
798
1246
  }
799
- };
800
- }
801
- let encoder;
802
- if (providedDevice) {
803
- const EncoderCtor = selection.encoderClass;
804
- encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
805
- } else {
806
- encoder = await selection.encoderClass.create();
807
- }
808
- try {
809
- const needsWriteTexture = needsWriteTextureWorkaround(adapter);
810
- if (!mipmaps) {
811
- let bytes;
812
- if (needsWriteTexture) {
813
- const level02 = bitmapToMipLevel(bitmap, flipY);
814
- const imageData = mipLevelToImageData(level02);
815
- bytes = await encoder.encodeToBytes(imageData, { quality });
816
- } else {
817
- bytes = await encoder.encodeToBytes(bitmap, { flipY, quality });
1247
+ const level0 = bitmapToMipLevel(bitmap, flipY);
1248
+ const chain = generateMipChain(level0);
1249
+ const encodedLevels = [];
1250
+ let totalEncodeMs = 0;
1251
+ for (const level of chain) {
1252
+ const padded = padToBlockMultiple(level);
1253
+ const bytes = encoder.encodeToBytes(padded);
1254
+ encodedLevels.push(bytes);
1255
+ totalEncodeMs += bytes.encodeMs;
818
1256
  }
819
- const tex2 = encoder.buildMippedTexture([bytes], { colorSpace });
1257
+ const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
820
1258
  return {
821
1259
  texture: tex2,
822
1260
  format: selection.format,
823
1261
  fallbackUncompressed: false,
1262
+ backend: "webgl",
824
1263
  astcNormalRemap: selection.astcNormalRemap,
825
- width: bytes.width,
826
- height: bytes.height,
827
- mipLevels: 1,
828
- encodeMs: bytes.encodeMs,
1264
+ width: level0.width,
1265
+ height: level0.height,
1266
+ mipLevels: encodedLevels.length,
1267
+ encodeMs: totalEncodeMs,
829
1268
  destroy: () => {
830
1269
  tex2.dispose();
831
1270
  encoder.destroy();
832
1271
  }
833
1272
  };
1273
+ } catch (e) {
1274
+ encoder.destroy();
1275
+ console.warn("[compressTexture] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
1276
+ return null;
834
1277
  }
835
- const level0 = bitmapToMipLevel(bitmap, flipY);
836
- const chain = generateMipChain(level0);
837
- const encodedLevels = [];
838
- let totalEncodeMs = 0;
839
- for (const level of chain) {
840
- const padded = padToBlockMultiple(level);
841
- const imageData = mipLevelToImageData(padded);
842
- const bytes = await encoder.encodeToBytes(imageData, { quality });
843
- encodedLevels.push(bytes);
844
- totalEncodeMs += bytes.encodeMs;
845
- }
846
- const tex = encoder.buildMippedTexture(encodedLevels, { colorSpace });
847
- return {
848
- texture: tex,
849
- format: selection.format,
850
- fallbackUncompressed: false,
851
- astcNormalRemap: selection.astcNormalRemap,
852
- width: level0.width,
853
- height: level0.height,
854
- mipLevels: encodedLevels.length,
855
- encodeMs: totalEncodeMs,
856
- destroy: () => {
857
- tex.dispose();
858
- encoder.destroy();
859
- }
860
- };
861
- } catch (e) {
862
- encoder.destroy();
863
- throw e;
864
1278
  }
865
1279
  }
866
1280
 
@@ -913,6 +1327,7 @@ var GputexLoader = class extends Loader {
913
1327
  result.texture.userData.gputex = {
914
1328
  format: result.format,
915
1329
  fallbackUncompressed: result.fallbackUncompressed,
1330
+ backend: result.backend,
916
1331
  astcNormalRemap: result.astcNormalRemap,
917
1332
  width: result.width,
918
1333
  height: result.height,
@@ -933,16 +1348,26 @@ var GputexLoader = class extends Loader {
933
1348
  };
934
1349
  export {
935
1350
  ASTC4x4Encoder,
1351
+ ASTC4x4WebGLEncoder,
936
1352
  BC1Encoder,
1353
+ BC1WebGLEncoder,
937
1354
  BC5Encoder,
1355
+ BC5WebGLEncoder,
938
1356
  BC7Encoder,
1357
+ BC7WebGLEncoder,
939
1358
  Encoder,
940
1359
  GputexLoader,
941
1360
  TextureFormat,
1361
+ WebGLBlockEncoder,
942
1362
  WebGPUFeature,
943
1363
  compressTexture,
1364
+ createWebGLContext,
944
1365
  detectCapabilities,
1366
+ detectWebGLCapabilities,
945
1367
  generateMipChain,
1368
+ getSharedWebGLContext,
1369
+ isWebGLAvailable,
946
1370
  padToBlockMultiple,
947
- selectFormat
1371
+ selectFormat,
1372
+ selectWebGLFormat
948
1373
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gputex",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"