gputex 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GPUtex
2
2
 
3
- Runtime GPU texture compression via WebGPU compute shaders. Feed it a PNG/JPG/WebP/AVIF and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
3
+ Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
4
4
 
5
5
  ⚠️: This is 100% vibe-coded. The code is completely unreviewed, under-tested, it will probably crash on many devices, and this library is very likely to go unmaintained. Do not use for anything important.
6
6
 
@@ -27,6 +27,18 @@ bun add gputex
27
27
 
28
28
  Format selection is automatic: BC7/BC5 on desktop, ASTC on mobile, uncompressed RGBA8 fallback otherwise.
29
29
 
30
+ ## WebGL fallback
31
+
32
+ WebGPU is the primary path. When it's unavailable (older Safari, Firefox without WebGPU, locked-down environments) `compressTexture()` automatically falls back to a **WebGL2** path that runs the same block encoders as fragment shaders — each 4×4 block is computed in one fragment, written to an `RGBA32UI` render target, and read back. The output bytes are identical to the WebGPU encoders, so the resulting `CompressedTexture` looks the same under either renderer.
33
+
34
+ The fallback chain is **WebGPU → WebGL2 → uncompressed RGBA8**. The `backend` field on the result (`'webgpu' | 'webgl' | 'none'`) tells you which path ran.
35
+
36
+ Notes on the WebGL path:
37
+
38
+ - It needs the matching WebGL2 compressed-texture extension to be sampleable: `EXT_texture_compression_bptc` (BC7), `EXT_texture_compression_rgtc` (BC5), `WEBGL_compressed_texture_astc` (ASTC), or `WEBGL_compressed_texture_s3tc` (BC1). Selection mirrors the WebGPU side, with BC1 added as a broadly-available last resort for **opaque** colour when neither BPTC nor ASTC is present.
39
+ - It always uses the **fast** encoders — the `quality: 'high'` option and the `device` / `adapter` options apply to the WebGPU path only.
40
+ - All encoding happens on one shared, off-screen WebGL2 context; nothing is drawn to a visible canvas.
41
+
30
42
  ## Usage
31
43
 
32
44
  ### `compressTexture` — direct API
@@ -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
@@ -244,6 +244,163 @@ declare class ASTC4x4Encoder extends Encoder {
244
244
  threeTextureFormat(): CompressedPixelFormat;
245
245
  }
246
246
 
247
+ /** One encoded mip level. The fields both encoder backends already produce. */
248
+ interface EncodedLevel {
249
+ /** Logical (pre-padding) dimensions, surfaced on the texture's userData. */
250
+ width: number;
251
+ height: number;
252
+ /** Block-aligned dimensions the compressed `data` actually covers. */
253
+ paddedWidth: number;
254
+ paddedHeight: number;
255
+ data: Uint8Array;
256
+ }
257
+
258
+ /** Raw RGBA8 pixel data (e.g. a CPU-generated mip level). */
259
+ interface RawPixelSource {
260
+ data: ArrayBufferView;
261
+ width: number;
262
+ height: number;
263
+ }
264
+ /** Anything the WebGL encoders can upload as the source image. */
265
+ type WebGLEncoderImageSource = ImageBitmap | ImageData | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | RawPixelSource;
266
+ interface WebGLEncodeBytesResult {
267
+ width: number;
268
+ height: number;
269
+ paddedWidth: number;
270
+ paddedHeight: number;
271
+ data: Uint8Array;
272
+ encodeMs: number;
273
+ }
274
+ interface WebGLEncoderOptions {
275
+ gl: WebGL2RenderingContext;
276
+ }
277
+ /**
278
+ * Constructor shape for the concrete subclasses; lets the static `create()`
279
+ * narrow its return type to the subclass (as the WebGPU `EncoderConstructor`
280
+ * does).
281
+ */
282
+ type WebGLEncoderConstructor<T extends WebGLBlockEncoder = WebGLBlockEncoder> = {
283
+ new (opts: WebGLEncoderOptions): T;
284
+ create(gl?: WebGL2RenderingContext | null): T;
285
+ };
286
+ declare abstract class WebGLBlockEncoder {
287
+ /**
288
+ * Create an encoder on the shared process-wide context (or a caller-supplied
289
+ * one). Throws when WebGL2 is unavailable. The `this:` annotation lets
290
+ * `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
291
+ */
292
+ static create<T extends WebGLBlockEncoder>(this: WebGLEncoderConstructor<T>, gl?: WebGL2RenderingContext | null): T;
293
+ readonly gl: WebGL2RenderingContext;
294
+ protected _program: WebGLProgram;
295
+ protected _vao: WebGLVertexArrayObject;
296
+ protected _uSrc: WebGLUniformLocation | null;
297
+ protected _uSrcSize: WebGLUniformLocation | null;
298
+ protected _uFlipY: WebGLUniformLocation | null;
299
+ constructor({ gl }: WebGLEncoderOptions);
300
+ /** Short lowercase identifier for labels / errors. */
301
+ abstract get label(): string;
302
+ /** 8 for BC1, 16 for BC5/BC7/ASTC 4×4. */
303
+ abstract get bytesPerBlock(): number;
304
+ /** Whether this format has an sRGB variant (false for BC5). */
305
+ abstract get supportsSrgb(): boolean;
306
+ /** GLSL ES 3.00 fragment-shader source. */
307
+ abstract fragSource(): string;
308
+ /** Three.js `CompressedPixelFormat` constant; sRGB is carried by colorSpace. */
309
+ abstract threeTextureFormat(): CompressedPixelFormat;
310
+ protected _buildProgram(): void;
311
+ /** Release the GL program + VAO. The shared context itself is left intact. */
312
+ destroy(): void;
313
+ /**
314
+ * Upload the source image to a freshly created RGBA8 texture bound on unit 0.
315
+ * Raw pixel sources (ImageData / mip levels) go through the typed-array
316
+ * overload; DOM sources (ImageBitmap / canvas / image) through the element
317
+ * overload. No flip / premultiply / colour conversion — flipY is applied in
318
+ * the shader so each mip level flips by its own height.
319
+ */
320
+ protected _uploadSource(source: WebGLEncoderImageSource, width: number, height: number): WebGLTexture;
321
+ /**
322
+ * Encode one image source to raw compressed bytes. `flipY` samples the source
323
+ * bottom-up (matching Three.js's convention) and is applied in the shader;
324
+ * the high-level mipped path bakes the flip into level 0 and passes false.
325
+ */
326
+ encodeToBytes(source: WebGLEncoderImageSource, { flipY }?: {
327
+ flipY?: boolean;
328
+ }): WebGLEncodeBytesResult;
329
+ /** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
330
+ buildMippedTexture(levels: readonly EncodedLevel[], { colorSpace }?: {
331
+ colorSpace?: 'srgb' | 'linear';
332
+ }): CompressedTexture;
333
+ }
334
+
335
+ declare class BC1WebGLEncoder extends WebGLBlockEncoder {
336
+ get label(): string;
337
+ get bytesPerBlock(): number;
338
+ get supportsSrgb(): boolean;
339
+ fragSource(): string;
340
+ threeTextureFormat(): CompressedPixelFormat;
341
+ }
342
+
343
+ declare class BC5WebGLEncoder extends WebGLBlockEncoder {
344
+ get label(): string;
345
+ get bytesPerBlock(): number;
346
+ get supportsSrgb(): boolean;
347
+ fragSource(): string;
348
+ threeTextureFormat(): CompressedPixelFormat;
349
+ }
350
+
351
+ declare class BC7WebGLEncoder extends WebGLBlockEncoder {
352
+ get label(): string;
353
+ get bytesPerBlock(): number;
354
+ get supportsSrgb(): boolean;
355
+ fragSource(): string;
356
+ threeTextureFormat(): CompressedPixelFormat;
357
+ }
358
+
359
+ declare class ASTC4x4WebGLEncoder extends WebGLBlockEncoder {
360
+ get label(): string;
361
+ get bytesPerBlock(): number;
362
+ get supportsSrgb(): boolean;
363
+ fragSource(): string;
364
+ threeTextureFormat(): CompressedPixelFormat;
365
+ }
366
+
367
+ /**
368
+ * Create a fresh WebGL2 context backed by an off-screen 1×1 canvas. Returns
369
+ * null when neither `OffscreenCanvas` nor `document` is available (e.g. a
370
+ * non-DOM worker without OffscreenCanvas) or when the platform has no WebGL2.
371
+ */
372
+ declare function createWebGLContext(): WebGL2RenderingContext | null;
373
+ /**
374
+ * Lazily-created context shared across the high-level fallback path. Re-created
375
+ * if the previous one was lost (tab backgrounding, GPU reset). Returns null
376
+ * when WebGL2 is unavailable on this platform.
377
+ */
378
+ declare function getSharedWebGLContext(): WebGL2RenderingContext | null;
379
+ /** True when a WebGL2 context can be created on this platform. */
380
+ declare function isWebGLAvailable(): boolean;
381
+
382
+ /**
383
+ * Minimal structural type for the extension source: only `getExtension` is
384
+ * touched, so tests can pass a `{ getExtension }` stub instead of a real
385
+ * `WebGL2RenderingContext`.
386
+ */
387
+ interface ExtensionProvider {
388
+ getExtension(name: string): unknown;
389
+ }
390
+ interface WebGLCapabilities {
391
+ /** EXT_texture_compression_bptc → BC7 (BPTC). */
392
+ bptc: boolean;
393
+ /** EXT_texture_compression_rgtc → BC5 (RGTC2). */
394
+ rgtc: boolean;
395
+ /** WEBGL_compressed_texture_s3tc → BC1 (DXT1). */
396
+ s3tc: boolean;
397
+ /** WEBGL_compressed_texture_s3tc_srgb → sRGB DXT1 (separate extension). */
398
+ s3tcSrgb: boolean;
399
+ /** WEBGL_compressed_texture_astc → ASTC 4×4 (and other footprints). */
400
+ astc: boolean;
401
+ }
402
+ declare function detectWebGLCapabilities(gl: ExtensionProvider): WebGLCapabilities;
403
+
247
404
  /**
248
405
  * How the texture will be used in the renderer. Drives format choice.
249
406
  * • 'color' — RGB albedo-like data (alpha optional / ignored).
@@ -269,6 +426,16 @@ interface FormatSelection {
269
426
  }
270
427
  declare function selectFormat(adapter: FeatureProvider, hint: TextureHint, options?: SelectFormatOptions): FormatSelection;
271
428
 
429
+ interface WebGLFormatSelection {
430
+ /** null = no compressed path available on this WebGL context. */
431
+ format: TextureFormat | null;
432
+ /** null when `format` is null. */
433
+ encoderClass: WebGLEncoderConstructor | null;
434
+ /** True when the chosen path is ASTC *and* the hint is 'normal' (caller must pre-swizzle). */
435
+ astcNormalRemap: boolean;
436
+ }
437
+ declare function selectWebGLFormat(caps: WebGLCapabilities, hint: TextureHint, options?: SelectFormatOptions): WebGLFormatSelection;
438
+
272
439
  /**
273
440
  * Everything `compressTexture()` can take as an image source. A superset
274
441
  * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
@@ -287,21 +454,28 @@ interface CompressOptions {
287
454
  /**
288
455
  * Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
289
456
  * ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
290
- * the CPU reference encoders). No effect on BC1.
457
+ * the CPU reference encoders). No effect on BC1 or on the WebGL fallback
458
+ * (which always uses the fast encoders).
291
459
  */
292
460
  quality?: EncodeQuality;
293
461
  /** 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. */
462
+ * of creating a new one. WebGPU path only. When provided, the encoder
463
+ * never destroys it. */
295
464
  device?: GPUDevice;
296
465
  adapter?: GPUAdapter;
297
466
  }
298
467
  interface CompressResult {
299
- /** CompressedTexture on the compressed path; Texture on RGBA8 fallback. */
468
+ /** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
300
469
  texture: Texture | CompressedTexture;
301
470
  /** The compressed format selected, or null when we fell back to RGBA8. */
302
471
  format: TextureFormat | null;
303
472
  /** True iff we returned an uncompressed Texture because no encoder fit. */
304
473
  fallbackUncompressed: boolean;
474
+ /**
475
+ * Which backend produced the result. 'webgpu' = compute path, 'webgl' =
476
+ * fragment-shader fallback, 'none' = uncompressed RGBA8.
477
+ */
478
+ backend: 'webgpu' | 'webgl' | 'none';
305
479
  /**
306
480
  * True iff the chosen format is ASTC and the hint was 'normal'. The
307
481
  * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
@@ -376,4 +550,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
376
550
  */
377
551
  declare function padToBlockMultiple(level: MipLevel): MipLevel;
378
552
 
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 };
553
+ export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, compressTexture, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat };
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) {
@@ -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
 
@@ -585,6 +594,373 @@ var ASTC4x4Encoder = class extends Encoder {
585
594
  }
586
595
  };
587
596
 
597
+ // src/webgl/glsl/fullscreen.vert.glsl
598
+ 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";
599
+
600
+ // src/webgl/webglContext.ts
601
+ var CONTEXT_ATTRS = {
602
+ alpha: false,
603
+ antialias: false,
604
+ depth: false,
605
+ stencil: false,
606
+ premultipliedAlpha: false,
607
+ preserveDrawingBuffer: false,
608
+ // Encoding is GPU-bound; prefer the discrete GPU when the browser exposes a
609
+ // choice. Ignored where unsupported.
610
+ powerPreference: "high-performance"
611
+ };
612
+ function createWebGLContext() {
613
+ if (typeof OffscreenCanvas !== "undefined") {
614
+ const gl = new OffscreenCanvas(1, 1).getContext("webgl2", CONTEXT_ATTRS);
615
+ return gl ?? null;
616
+ }
617
+ if (typeof document !== "undefined") {
618
+ return document.createElement("canvas").getContext("webgl2", CONTEXT_ATTRS);
619
+ }
620
+ return null;
621
+ }
622
+ var sharedContext;
623
+ function getSharedWebGLContext() {
624
+ if (sharedContext === void 0 || sharedContext !== null && sharedContext.isContextLost()) {
625
+ sharedContext = createWebGLContext();
626
+ }
627
+ return sharedContext;
628
+ }
629
+ function isWebGLAvailable() {
630
+ return getSharedWebGLContext() !== null;
631
+ }
632
+
633
+ // src/webgl/WebGLBlockEncoder.ts
634
+ function compileShader(gl, type, source, label) {
635
+ const shader = gl.createShader(type);
636
+ if (!shader) throw new Error(`${label}: gl.createShader failed`);
637
+ gl.shaderSource(shader, source);
638
+ gl.compileShader(shader);
639
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
640
+ const log = gl.getShaderInfoLog(shader);
641
+ gl.deleteShader(shader);
642
+ const kind = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
643
+ throw new Error(`${label}: ${kind} shader compile failed: ${log}`);
644
+ }
645
+ return shader;
646
+ }
647
+ var WebGLBlockEncoder = class {
648
+ /**
649
+ * Create an encoder on the shared process-wide context (or a caller-supplied
650
+ * one). Throws when WebGL2 is unavailable. The `this:` annotation lets
651
+ * `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
652
+ */
653
+ static create(gl) {
654
+ const ctx = gl ?? getSharedWebGLContext();
655
+ if (!ctx) throw new Error("WebGL2 not available in this environment");
656
+ return new this({ gl: ctx });
657
+ }
658
+ gl;
659
+ // Set in _buildProgram(), which the constructor calls.
660
+ _program;
661
+ _vao;
662
+ _uSrc = null;
663
+ _uSrcSize = null;
664
+ _uFlipY = null;
665
+ constructor({ gl }) {
666
+ this.gl = gl;
667
+ this._buildProgram();
668
+ }
669
+ _buildProgram() {
670
+ const gl = this.gl;
671
+ const program = gl.createProgram();
672
+ const vao = gl.createVertexArray();
673
+ if (!program || !vao) throw new Error(`${this.label}: failed to allocate WebGL program/VAO`);
674
+ const vert = compileShader(gl, gl.VERTEX_SHADER, fullscreen_vert_default, this.label);
675
+ const frag = compileShader(gl, gl.FRAGMENT_SHADER, this.fragSource(), this.label);
676
+ gl.attachShader(program, vert);
677
+ gl.attachShader(program, frag);
678
+ gl.linkProgram(program);
679
+ gl.deleteShader(vert);
680
+ gl.deleteShader(frag);
681
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
682
+ const log = gl.getProgramInfoLog(program);
683
+ gl.deleteProgram(program);
684
+ throw new Error(`${this.label}: WebGL program link failed: ${log}`);
685
+ }
686
+ this._program = program;
687
+ this._vao = vao;
688
+ this._uSrc = gl.getUniformLocation(program, "uSrc");
689
+ this._uSrcSize = gl.getUniformLocation(program, "uSrcSize");
690
+ this._uFlipY = gl.getUniformLocation(program, "uFlipY");
691
+ }
692
+ /** Release the GL program + VAO. The shared context itself is left intact. */
693
+ destroy() {
694
+ const gl = this.gl;
695
+ if (gl.isContextLost()) return;
696
+ gl.deleteProgram(this._program);
697
+ gl.deleteVertexArray(this._vao);
698
+ }
699
+ /**
700
+ * Upload the source image to a freshly created RGBA8 texture bound on unit 0.
701
+ * Raw pixel sources (ImageData / mip levels) go through the typed-array
702
+ * overload; DOM sources (ImageBitmap / canvas / image) through the element
703
+ * overload. No flip / premultiply / colour conversion — flipY is applied in
704
+ * the shader so each mip level flips by its own height.
705
+ */
706
+ _uploadSource(source, width, height) {
707
+ const gl = this.gl;
708
+ const tex = gl.createTexture();
709
+ if (!tex) throw new Error(`${this.label}: gl.createTexture failed`);
710
+ gl.activeTexture(gl.TEXTURE0);
711
+ gl.bindTexture(gl.TEXTURE_2D, tex);
712
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
713
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
714
+ gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
715
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
716
+ const raw = source;
717
+ if (raw.data && ArrayBuffer.isView(raw.data)) {
718
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, raw.data);
719
+ } else {
720
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, source);
721
+ }
722
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
723
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
724
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
725
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
726
+ return tex;
727
+ }
728
+ /**
729
+ * Encode one image source to raw compressed bytes. `flipY` samples the source
730
+ * bottom-up (matching Three.js's convention) and is applied in the shader;
731
+ * the high-level mipped path bakes the flip into level 0 and passes false.
732
+ */
733
+ encodeToBytes(source, { flipY = false } = {}) {
734
+ const gl = this.gl;
735
+ if (gl.isContextLost()) throw new Error(`${this.label}WebGLEncoder: WebGL context lost`);
736
+ const width = source.width;
737
+ const height = source.height;
738
+ if (!width || !height) {
739
+ throw new Error(`${this.label}WebGLEncoder: source has no dimensions`);
740
+ }
741
+ const paddedWidth = width + 3 & ~3;
742
+ const paddedHeight = height + 3 & ~3;
743
+ const blocksX = paddedWidth >> 2;
744
+ const blocksY = paddedHeight >> 2;
745
+ const blockCount = blocksX * blocksY;
746
+ const outByteLen = blockCount * this.bytesPerBlock;
747
+ const t0 = performance.now();
748
+ const srcTex = this._uploadSource(source, width, height);
749
+ const outTex = gl.createTexture();
750
+ const fbo = gl.createFramebuffer();
751
+ if (!outTex || !fbo) {
752
+ gl.deleteTexture(srcTex);
753
+ if (outTex) gl.deleteTexture(outTex);
754
+ if (fbo) gl.deleteFramebuffer(fbo);
755
+ throw new Error(`${this.label}: failed to allocate output texture/framebuffer`);
756
+ }
757
+ gl.bindTexture(gl.TEXTURE_2D, outTex);
758
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32UI, blocksX, blocksY);
759
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
760
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
761
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
762
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outTex, 0);
763
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
764
+ if (status !== gl.FRAMEBUFFER_COMPLETE) {
765
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
766
+ gl.deleteFramebuffer(fbo);
767
+ gl.deleteTexture(outTex);
768
+ gl.deleteTexture(srcTex);
769
+ throw new Error(`${this.label}: integer framebuffer incomplete (0x${status.toString(16)})`);
770
+ }
771
+ gl.useProgram(this._program);
772
+ gl.bindVertexArray(this._vao);
773
+ gl.activeTexture(gl.TEXTURE0);
774
+ gl.bindTexture(gl.TEXTURE_2D, srcTex);
775
+ gl.uniform1i(this._uSrc, 0);
776
+ gl.uniform2i(this._uSrcSize, width, height);
777
+ gl.uniform1i(this._uFlipY, flipY ? 1 : 0);
778
+ gl.disable(gl.BLEND);
779
+ gl.disable(gl.DEPTH_TEST);
780
+ gl.disable(gl.SCISSOR_TEST);
781
+ gl.viewport(0, 0, blocksX, blocksY);
782
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
783
+ const words = new Uint32Array(blockCount * 4);
784
+ gl.readPixels(0, 0, blocksX, blocksY, gl.RGBA_INTEGER, gl.UNSIGNED_INT, words);
785
+ let data;
786
+ if (this.bytesPerBlock === 16) {
787
+ data = new Uint8Array(words.buffer, 0, outByteLen);
788
+ } else {
789
+ const packed = new Uint32Array(blockCount * 2);
790
+ for (let k = 0; k < blockCount; k++) {
791
+ packed[k * 2] = words[k * 4];
792
+ packed[k * 2 + 1] = words[k * 4 + 1];
793
+ }
794
+ data = new Uint8Array(packed.buffer, 0, outByteLen);
795
+ }
796
+ const encodeMs = performance.now() - t0;
797
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
798
+ gl.bindTexture(gl.TEXTURE_2D, null);
799
+ gl.bindVertexArray(null);
800
+ gl.deleteFramebuffer(fbo);
801
+ gl.deleteTexture(outTex);
802
+ gl.deleteTexture(srcTex);
803
+ return { width, height, paddedWidth, paddedHeight, data, encodeMs };
804
+ }
805
+ /** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
806
+ buildMippedTexture(levels, { colorSpace = "srgb" } = {}) {
807
+ if (levels.length === 0) {
808
+ throw new Error(`${this.label}WebGLEncoder.buildMippedTexture: no levels provided`);
809
+ }
810
+ const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
811
+ return assembleCompressedTexture(levels, this.threeTextureFormat(), effectiveSrgb);
812
+ }
813
+ };
814
+
815
+ // src/webgl/BC1WebGLEncoder.ts
816
+ import { RGBA_S3TC_DXT1_Format as RGBA_S3TC_DXT1_Format2 } from "three";
817
+
818
+ // src/webgl/glsl/bc1.frag.glsl
819
+ var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl.\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. Algorithm mirrors bc1.wgsl line-for-line:\n// bbox endpoints, 1/16 inset, RGB565 quantisation, forced 4-colour mode, full\n// L2 index search. See bc1.wgsl for the detailed rationale.\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\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 float r8 = (r * 527.0 + 23.0) / 256.0;\n float g8 = (g * 259.0 + 33.0) / 256.0;\n float b8 = (b * 527.0 + 23.0) / 256.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 bounding box by ~half an RGB565 cell (1/16) to tighten the\n // quantised 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\n // 4-colour mode requires c0 > c1.\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 vec3 p0 = from565(c0);\n vec3 p1 = from565(c1);\n vec3 p2 = (2.0 * p0 + p1) * (1.0 / 3.0);\n vec3 p3 = (p0 + 2.0 * p1) * (1.0 / 3.0);\n\n uint indices = 0u;\n for (int i = 0; i < 16; i++) {\n vec3 c = pixels[i];\n float d0 = dot(c - p0, c - p0);\n float d1 = dot(c - p1, c - p1);\n float d2 = dot(c - p2, c - p2);\n float d3 = dot(c - p3, c - p3);\n\n float bestD = d0;\n uint bestI = 0u;\n if (d1 < bestD) { bestD = d1; bestI = 1u; }\n if (d2 < bestD) { bestD = d2; bestI = 2u; }\n if (d3 < bestD) { bestD = d3; bestI = 3u; }\n\n indices = indices | (bestI << (i * 2));\n }\n\n outColor = uvec4(c0 | (c1 << 16), indices, 0u, 0u);\n}\n";
820
+
821
+ // src/webgl/BC1WebGLEncoder.ts
822
+ var BC1WebGLEncoder = class extends WebGLBlockEncoder {
823
+ get label() {
824
+ return "bc1";
825
+ }
826
+ get bytesPerBlock() {
827
+ return 8;
828
+ }
829
+ get supportsSrgb() {
830
+ return true;
831
+ }
832
+ fragSource() {
833
+ return bc1_frag_default;
834
+ }
835
+ threeTextureFormat() {
836
+ return RGBA_S3TC_DXT1_Format2;
837
+ }
838
+ };
839
+
840
+ // src/webgl/BC5WebGLEncoder.ts
841
+ import { RED_GREEN_RGTC2_Format as RED_GREEN_RGTC2_Format2 } from "three";
842
+
843
+ // src/webgl/glsl/bc5.frag.glsl
844
+ 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";
845
+
846
+ // src/webgl/BC5WebGLEncoder.ts
847
+ var BC5WebGLEncoder = class extends WebGLBlockEncoder {
848
+ get label() {
849
+ return "bc5";
850
+ }
851
+ get bytesPerBlock() {
852
+ return 16;
853
+ }
854
+ get supportsSrgb() {
855
+ return false;
856
+ }
857
+ fragSource() {
858
+ return bc5_frag_default;
859
+ }
860
+ threeTextureFormat() {
861
+ return RED_GREEN_RGTC2_Format2;
862
+ }
863
+ };
864
+
865
+ // src/webgl/BC7WebGLEncoder.ts
866
+ import { RGBA_BPTC_Format as RGBA_BPTC_Format2 } from "three";
867
+
868
+ // src/webgl/glsl/bc7.frag.glsl
869
+ 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";
870
+
871
+ // src/webgl/BC7WebGLEncoder.ts
872
+ var BC7WebGLEncoder = class extends WebGLBlockEncoder {
873
+ get label() {
874
+ return "bc7";
875
+ }
876
+ get bytesPerBlock() {
877
+ return 16;
878
+ }
879
+ get supportsSrgb() {
880
+ return true;
881
+ }
882
+ fragSource() {
883
+ return bc7_frag_default;
884
+ }
885
+ threeTextureFormat() {
886
+ return RGBA_BPTC_Format2;
887
+ }
888
+ };
889
+
890
+ // src/webgl/ASTC4x4WebGLEncoder.ts
891
+ import { RGBA_ASTC_4x4_Format as RGBA_ASTC_4x4_Format2 } from "three";
892
+
893
+ // src/webgl/glsl/astc4x4.frag.glsl
894
+ 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";
895
+
896
+ // src/webgl/ASTC4x4WebGLEncoder.ts
897
+ var ASTC4x4WebGLEncoder = class extends WebGLBlockEncoder {
898
+ get label() {
899
+ return "astc4x4";
900
+ }
901
+ get bytesPerBlock() {
902
+ return 16;
903
+ }
904
+ get supportsSrgb() {
905
+ return true;
906
+ }
907
+ fragSource() {
908
+ return astc4x4_frag_default;
909
+ }
910
+ threeTextureFormat() {
911
+ return RGBA_ASTC_4x4_Format2;
912
+ }
913
+ };
914
+
915
+ // src/webgl/webglCapabilities.ts
916
+ function detectWebGLCapabilities(gl) {
917
+ if (!gl || typeof gl.getExtension !== "function") {
918
+ throw new TypeError("detectWebGLCapabilities: a WebGL2 context (or { getExtension }) is required");
919
+ }
920
+ const has = (name) => gl.getExtension(name) != null;
921
+ return {
922
+ bptc: has("EXT_texture_compression_bptc"),
923
+ rgtc: has("EXT_texture_compression_rgtc"),
924
+ s3tc: has("WEBGL_compressed_texture_s3tc"),
925
+ s3tcSrgb: has("WEBGL_compressed_texture_s3tc_srgb"),
926
+ astc: has("WEBGL_compressed_texture_astc")
927
+ };
928
+ }
929
+
930
+ // src/webgl/selectWebGLFormat.ts
931
+ var NONE = { format: null, encoderClass: null, astcNormalRemap: false };
932
+ function selectWebGLFormat(caps, hint, options = {}) {
933
+ const { colorSpace = "srgb" } = options;
934
+ const srgb = colorSpace === "srgb";
935
+ const astc = (astcNormalRemap) => ({
936
+ format: srgb ? TextureFormat.ASTC_4x4_SRGB : TextureFormat.ASTC_4x4,
937
+ encoderClass: ASTC4x4WebGLEncoder,
938
+ astcNormalRemap
939
+ });
940
+ if (hint === "normal") {
941
+ if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
942
+ if (caps.astc) return astc(true);
943
+ return NONE;
944
+ }
945
+ if (caps.bptc) {
946
+ return {
947
+ format: srgb ? TextureFormat.BC7_SRGB : TextureFormat.BC7,
948
+ encoderClass: BC7WebGLEncoder,
949
+ astcNormalRemap: false
950
+ };
951
+ }
952
+ if (caps.astc) return astc(false);
953
+ if (hint === "color") {
954
+ if (srgb && caps.s3tcSrgb) {
955
+ return { format: TextureFormat.BC1_SRGB, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
956
+ }
957
+ if (!srgb && caps.s3tc) {
958
+ return { format: TextureFormat.BC1, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
959
+ }
960
+ }
961
+ return NONE;
962
+ }
963
+
588
964
  // src/selectFormat.ts
589
965
  function selectFormat(adapter, hint, options = {}) {
590
966
  const { colorSpace = "srgb" } = options;
@@ -612,7 +988,7 @@ function selectFormat(adapter, hint, options = {}) {
612
988
  }
613
989
 
614
990
  // src/compressTexture.ts
615
- import { LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, RepeatWrapping as RepeatWrapping2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
991
+ import { LinearFilter as LinearFilter3, LinearSRGBColorSpace as LinearSRGBColorSpace3, RepeatWrapping as RepeatWrapping3, SRGBColorSpace as SRGBColorSpace3, Texture } from "three";
616
992
 
617
993
  // src/mipgen.ts
618
994
  function generateMipChain(level0) {
@@ -722,10 +1098,10 @@ function mipLevelToImageData(level) {
722
1098
  }
723
1099
  function wrapUncompressed(bitmap, srgb, flipY) {
724
1100
  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;
1101
+ tex.colorSpace = srgb ? SRGBColorSpace3 : LinearSRGBColorSpace3;
1102
+ tex.magFilter = LinearFilter3;
1103
+ tex.minFilter = LinearFilter3;
1104
+ tex.wrapS = tex.wrapT = RepeatWrapping3;
729
1105
  tex.generateMipmaps = false;
730
1106
  tex.flipY = flipY;
731
1107
  tex.needsUpdate = true;
@@ -743,124 +1119,159 @@ async function compressTexture(source, options = {}) {
743
1119
  } = options;
744
1120
  const srgb = colorSpace === "srgb";
745
1121
  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();
1122
+ const viaWebGPU = await encodeViaWebGPU();
1123
+ if (viaWebGPU) return viaWebGPU;
1124
+ const viaWebGL = encodeViaWebGL();
1125
+ if (viaWebGL) return viaWebGL;
1126
+ console.warn(
1127
+ "[compressTexture] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
1128
+ );
1129
+ const tex = wrapUncompressed(bitmap, srgb, flipY);
1130
+ return {
1131
+ texture: tex,
1132
+ format: null,
1133
+ fallbackUncompressed: true,
1134
+ backend: "none",
1135
+ astcNormalRemap: false,
1136
+ width: bitmap.width,
1137
+ height: bitmap.height,
1138
+ mipLevels: 1,
1139
+ encodeMs: 0,
1140
+ destroy: () => {
1141
+ tex.dispose();
1142
+ }
1143
+ };
1144
+ async function encodeViaWebGPU() {
1145
+ if (!("gpu" in navigator)) return null;
1146
+ const adapter = providedAdapter ?? await navigator.gpu.requestAdapter();
1147
+ if (!adapter) return null;
1148
+ const selection = selectFormat(adapter, hint, { colorSpace });
1149
+ if (!selection.format || !selection.encoderClass) return null;
1150
+ let encoder;
1151
+ if (providedDevice) {
1152
+ const EncoderCtor = selection.encoderClass;
1153
+ encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
1154
+ } else {
1155
+ encoder = await selection.encoderClass.create();
1156
+ }
1157
+ try {
1158
+ const needsWriteTexture = needsWriteTextureWorkaround(adapter);
1159
+ if (!mipmaps) {
1160
+ let bytes;
1161
+ if (needsWriteTexture) {
1162
+ const level02 = bitmapToMipLevel(bitmap, flipY);
1163
+ const imageData = mipLevelToImageData(level02);
1164
+ bytes = await encoder.encodeToBytes(imageData, { quality });
1165
+ } else {
1166
+ bytes = await encoder.encodeToBytes(bitmap, { flipY, quality });
1167
+ }
1168
+ const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
1169
+ return {
1170
+ texture: tex3,
1171
+ format: selection.format,
1172
+ fallbackUncompressed: false,
1173
+ backend: "webgpu",
1174
+ astcNormalRemap: selection.astcNormalRemap,
1175
+ width: bytes.width,
1176
+ height: bytes.height,
1177
+ mipLevels: 1,
1178
+ encodeMs: bytes.encodeMs,
1179
+ destroy: () => {
1180
+ tex3.dispose();
1181
+ encoder.destroy();
1182
+ }
1183
+ };
760
1184
  }
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();
1185
+ const level0 = bitmapToMipLevel(bitmap, flipY);
1186
+ const chain = generateMipChain(level0);
1187
+ const encodedLevels = [];
1188
+ let totalEncodeMs = 0;
1189
+ for (const level of chain) {
1190
+ const padded = padToBlockMultiple(level);
1191
+ const imageData = mipLevelToImageData(padded);
1192
+ const bytes = await encoder.encodeToBytes(imageData, { quality });
1193
+ encodedLevels.push(bytes);
1194
+ totalEncodeMs += bytes.encodeMs;
778
1195
  }
779
- };
1196
+ const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
1197
+ return {
1198
+ texture: tex2,
1199
+ format: selection.format,
1200
+ fallbackUncompressed: false,
1201
+ backend: "webgpu",
1202
+ astcNormalRemap: selection.astcNormalRemap,
1203
+ width: level0.width,
1204
+ height: level0.height,
1205
+ mipLevels: encodedLevels.length,
1206
+ encodeMs: totalEncodeMs,
1207
+ destroy: () => {
1208
+ tex2.dispose();
1209
+ encoder.destroy();
1210
+ }
1211
+ };
1212
+ } catch (e) {
1213
+ encoder.destroy();
1214
+ throw e;
1215
+ }
780
1216
  }
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();
1217
+ function encodeViaWebGL() {
1218
+ const gl = getSharedWebGLContext();
1219
+ if (!gl) return null;
1220
+ const caps = detectWebGLCapabilities(gl);
1221
+ const selection = selectWebGLFormat(caps, hint, { colorSpace });
1222
+ if (!selection.format || !selection.encoderClass) return null;
1223
+ const encoder = selection.encoderClass.create(gl);
1224
+ try {
1225
+ if (!mipmaps) {
1226
+ const bytes = encoder.encodeToBytes(bitmap, { flipY });
1227
+ const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
1228
+ return {
1229
+ texture: tex3,
1230
+ format: selection.format,
1231
+ fallbackUncompressed: false,
1232
+ backend: "webgl",
1233
+ astcNormalRemap: selection.astcNormalRemap,
1234
+ width: bytes.width,
1235
+ height: bytes.height,
1236
+ mipLevels: 1,
1237
+ encodeMs: bytes.encodeMs,
1238
+ destroy: () => {
1239
+ tex3.dispose();
1240
+ encoder.destroy();
1241
+ }
1242
+ };
798
1243
  }
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 });
1244
+ const level0 = bitmapToMipLevel(bitmap, flipY);
1245
+ const chain = generateMipChain(level0);
1246
+ const encodedLevels = [];
1247
+ let totalEncodeMs = 0;
1248
+ for (const level of chain) {
1249
+ const padded = padToBlockMultiple(level);
1250
+ const bytes = encoder.encodeToBytes(padded);
1251
+ encodedLevels.push(bytes);
1252
+ totalEncodeMs += bytes.encodeMs;
818
1253
  }
819
- const tex2 = encoder.buildMippedTexture([bytes], { colorSpace });
1254
+ const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
820
1255
  return {
821
1256
  texture: tex2,
822
1257
  format: selection.format,
823
1258
  fallbackUncompressed: false,
1259
+ backend: "webgl",
824
1260
  astcNormalRemap: selection.astcNormalRemap,
825
- width: bytes.width,
826
- height: bytes.height,
827
- mipLevels: 1,
828
- encodeMs: bytes.encodeMs,
1261
+ width: level0.width,
1262
+ height: level0.height,
1263
+ mipLevels: encodedLevels.length,
1264
+ encodeMs: totalEncodeMs,
829
1265
  destroy: () => {
830
1266
  tex2.dispose();
831
1267
  encoder.destroy();
832
1268
  }
833
1269
  };
1270
+ } catch (e) {
1271
+ encoder.destroy();
1272
+ console.warn("[compressTexture] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
1273
+ return null;
834
1274
  }
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
1275
  }
865
1276
  }
866
1277
 
@@ -913,6 +1324,7 @@ var GputexLoader = class extends Loader {
913
1324
  result.texture.userData.gputex = {
914
1325
  format: result.format,
915
1326
  fallbackUncompressed: result.fallbackUncompressed,
1327
+ backend: result.backend,
916
1328
  astcNormalRemap: result.astcNormalRemap,
917
1329
  width: result.width,
918
1330
  height: result.height,
@@ -933,16 +1345,26 @@ var GputexLoader = class extends Loader {
933
1345
  };
934
1346
  export {
935
1347
  ASTC4x4Encoder,
1348
+ ASTC4x4WebGLEncoder,
936
1349
  BC1Encoder,
1350
+ BC1WebGLEncoder,
937
1351
  BC5Encoder,
1352
+ BC5WebGLEncoder,
938
1353
  BC7Encoder,
1354
+ BC7WebGLEncoder,
939
1355
  Encoder,
940
1356
  GputexLoader,
941
1357
  TextureFormat,
1358
+ WebGLBlockEncoder,
942
1359
  WebGPUFeature,
943
1360
  compressTexture,
1361
+ createWebGLContext,
944
1362
  detectCapabilities,
1363
+ detectWebGLCapabilities,
945
1364
  generateMipChain,
1365
+ getSharedWebGLContext,
1366
+ isWebGLAvailable,
946
1367
  padToBlockMultiple,
947
- selectFormat
1368
+ selectFormat,
1369
+ selectWebGLFormat
948
1370
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gputex",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"