gputex 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,11 +38,28 @@ const { texture, format } = await compressTexture('/cobblestone.avif', {
38
38
  hint: 'color', // 'color' | 'colorWithAlpha' | 'normal'
39
39
  colorSpace: 'srgb',
40
40
  mipmaps: true,
41
+ quality: 'fast', // 'fast' (default) | 'high'
41
42
  })
42
43
 
43
44
  material.map = texture
44
45
  ```
45
46
 
47
+ #### Quality
48
+
49
+ `quality` trades encode speed against compression accuracy:
50
+
51
+ - **`'fast'` (default)** — a bounding-box endpoint seed plus projection-based
52
+ index assignment (each pixel is projected onto the colinear endpoint line in
53
+ O(1) instead of searching every palette entry) with a single fused
54
+ least-squares refit. On GPUs that report the `shader-f16` feature the whole
55
+ fast path runs in f16 (≈2× on Apple) — the f32 path is the automatic
56
+ fallback. Net vs `'high'` on an Apple GPU: **BC7 ~50×**, **ASTC ~9×**,
57
+ **BC5 ~5×** faster, for a PSNR cost of **≤0.45 dB** (imperceptible). BC1 is
58
+ single-pass and unaffected.
59
+ - **`'high'`** — exhaustive endpoint search (farthest-pair seed, full nearest
60
+ search, p-bit search); output is byte-for-byte identical to the CPU reference
61
+ encoders.
62
+
46
63
  ### `GputexLoader` — Three.js Loader
47
64
 
48
65
  ```ts
@@ -158,9 +175,9 @@ encoder.destroy()
158
175
  - `texture-compression-bc` (desktop) or `texture-compression-astc` (mobile) for compressed output
159
176
  - Falls back to uncompressed RGBA8 when neither is available
160
177
 
161
- ## Known issues
178
+ ## Device-specific workarounds
162
179
 
163
- - Black texture on Google Pixel 10
180
+ - Black texture on Google Pixel 10: `copyExternalImageToTexture` produces black textures on the Pixel 10's PowerVR DXT GPU (vendor `img-tec`, architecture `d-series`). Worked around by uploading via `writeTexture` with rasterised pixel data instead.
164
181
 
165
182
  ## Acknowledgements
166
183
 
package/dist/index.d.ts CHANGED
@@ -45,9 +45,18 @@ interface EncoderOptions {
45
45
  adapter?: GPUAdapter;
46
46
  ownsDevice?: boolean;
47
47
  }
48
+ /**
49
+ * Encoder quality level. 'fast' (default) uses the cheaper search paths in the
50
+ * shaders — measured ~2–4× faster for ≤0.36 dB PSNR. 'high' runs the exhaustive
51
+ * search, producing output byte-identical to the CPU reference encoders.
52
+ * No effect on BC1 (already single-pass; both levels are identical).
53
+ */
54
+ type EncodeQuality = 'fast' | 'high';
48
55
  interface EncodeCallOptions {
49
56
  /** Tags the output color space. Forced 'linear' for encoders with supportsSrgb=false. */
50
57
  colorSpace?: 'srgb' | 'linear';
58
+ /** Encode quality / speed trade-off. Default 'fast'. */
59
+ quality?: EncodeQuality;
51
60
  }
52
61
  interface EncodeResult {
53
62
  width: number;
@@ -110,9 +119,18 @@ declare abstract class Encoder {
110
119
  readonly adapter?: GPUAdapter;
111
120
  readonly ownsDevice: boolean;
112
121
  protected _module: GPUShaderModule;
122
+ protected _moduleF16: GPUShaderModule | null;
123
+ protected _pipelineF16: GPUComputePipeline | null;
113
124
  protected _pipeline: GPUComputePipeline;
125
+ protected _pipelineCache: Map<EncodeQuality, GPUComputePipeline>;
114
126
  constructor({ device, adapter, ownsDevice }: EncoderOptions);
115
127
  protected _buildPipeline(): void;
128
+ /**
129
+ * Pipeline for a given quality level. Encoders that don't declare a
130
+ * `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
131
+ * argument and reuse the single pipeline. Specialised pipelines are cached.
132
+ */
133
+ protected _getPipeline(quality: EncodeQuality): GPUComputePipeline;
116
134
  destroy(): void;
117
135
  /** Short lowercase identifier used in GPU object labels and errors. */
118
136
  abstract get label(): string;
@@ -122,6 +140,20 @@ declare abstract class Encoder {
122
140
  get workgroupSize(): readonly [number, number, number];
123
141
  /** Whether this format has an sRGB variant. Default true. */
124
142
  get supportsSrgb(): boolean;
143
+ /**
144
+ * Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
145
+ * (i.e. has distinct fast/high search paths). BC1 is already single-pass and
146
+ * leaves this false; BC5/BC7/ASTC override it to true.
147
+ */
148
+ get supportsQuality(): boolean;
149
+ /**
150
+ * Optional f16 WGSL for the 'fast' path. Used only when the device reports the
151
+ * `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
152
+ * `'high'` always uses it. Returns null when there's no f16 variant (BC1).
153
+ */
154
+ wgslSourceFastF16(): string | null;
155
+ /** Whether the f16 fast path is both available and supported on this device. */
156
+ protected get _useF16(): boolean;
125
157
  /** WGSL compute-shader source. */
126
158
  abstract wgslSource(): string;
127
159
  /** e.g. 'bc1-rgba-unorm-srgb'. */
@@ -134,7 +166,7 @@ declare abstract class Encoder {
134
166
  * whether the result can actually be sampled.
135
167
  */
136
168
  get supportsSampling(): boolean;
137
- encode(source: EncoderImageSource, { colorSpace }?: EncodeCallOptions): Promise<EncodeResult>;
169
+ encode(source: EncoderImageSource, { colorSpace, quality }?: EncodeCallOptions): Promise<EncodeResult>;
138
170
  /**
139
171
  * Encode one image source to raw compressed bytes, skipping the
140
172
  * `CompressedTexture` wrap. Used by the public `encode()` above and by
@@ -145,8 +177,9 @@ declare abstract class Encoder {
145
177
  * encoder boundary. Still safe to call from outside — it just does
146
178
  * less work than `encode()` and the caller assembles the texture.
147
179
  */
148
- encodeToBytes(source: EncoderImageSource, { flipY }?: {
180
+ encodeToBytes(source: EncoderImageSource, { flipY, quality }?: {
149
181
  flipY?: boolean;
182
+ quality?: EncodeQuality;
150
183
  }): Promise<EncodeBytesResult>;
151
184
  /**
152
185
  * Assemble a `CompressedTexture` from pre-encoded mip levels. Called
@@ -178,7 +211,9 @@ declare class BC5Encoder extends Encoder {
178
211
  get label(): string;
179
212
  get bytesPerBlock(): number;
180
213
  get supportsSrgb(): boolean;
214
+ get supportsQuality(): boolean;
181
215
  wgslSource(): string;
216
+ wgslSourceFastF16(): string;
182
217
  gpuTextureFormat(): GPUTextureFormat;
183
218
  threeTextureFormat(): CompressedPixelFormat;
184
219
  }
@@ -189,7 +224,9 @@ declare class BC7Encoder extends Encoder {
189
224
  get label(): string;
190
225
  get bytesPerBlock(): number;
191
226
  get supportsSrgb(): boolean;
227
+ get supportsQuality(): boolean;
192
228
  wgslSource(): string;
229
+ wgslSourceFastF16(): string;
193
230
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
194
231
  threeTextureFormat(): CompressedPixelFormat;
195
232
  }
@@ -200,7 +237,9 @@ declare class ASTC4x4Encoder extends Encoder {
200
237
  get label(): string;
201
238
  get bytesPerBlock(): number;
202
239
  get supportsSrgb(): boolean;
240
+ get supportsQuality(): boolean;
203
241
  wgslSource(): string;
242
+ wgslSourceFastF16(): string;
204
243
  gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
205
244
  threeTextureFormat(): CompressedPixelFormat;
206
245
  }
@@ -245,6 +284,12 @@ interface CompressOptions {
245
284
  flipY?: boolean;
246
285
  /** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
247
286
  mipmaps?: boolean;
287
+ /**
288
+ * Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
289
+ * ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
290
+ * the CPU reference encoders). No effect on BC1.
291
+ */
292
+ quality?: EncodeQuality;
248
293
  /** Reuse an existing device (e.g. Three.js's renderer device) instead
249
294
  * of creating a new one. When provided, the encoder never destroys it. */
250
295
  device?: GPUDevice;
@@ -283,6 +328,8 @@ declare class GputexLoader extends Loader<Texture> {
283
328
  flipY: boolean;
284
329
  /** Generate + encode a full mip chain. Default false. */
285
330
  mipmaps: boolean;
331
+ /** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
332
+ quality: EncodeQuality;
286
333
  /**
287
334
  * Optional pre-existing WebGPU device. Reusing the renderer's device
288
335
  * avoids spinning up a second WebGPU context for encoding.
@@ -329,4 +376,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
329
376
  */
330
377
  declare function padToBlockMultiple(level: MipLevel): MipLevel;
331
378
 
332
- export { ASTC4x4Encoder, BC1Encoder, BC5Encoder, BC7Encoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type SelectFormatOptions, TextureFormat, type TextureHint, WebGPUFeature, compressTexture, detectCapabilities, generateMipChain, padToBlockMultiple, selectFormat };
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 };
package/dist/index.js CHANGED
@@ -50,6 +50,25 @@ import {
50
50
  SRGBColorSpace,
51
51
  RepeatWrapping
52
52
  } from "three";
53
+
54
+ // src/workarounds.ts
55
+ function needsWriteTextureWorkaround(adapter) {
56
+ const { vendor, architecture } = adapter.info ?? {};
57
+ return vendor === "img-tec" && architecture === "d-series";
58
+ }
59
+ function uploadSourceTexture(device, srcTex, source, width, height, flipY, useWriteTexture) {
60
+ if (useWriteTexture && source instanceof ImageData) {
61
+ device.queue.writeTexture({ texture: srcTex }, source.data, { bytesPerRow: width * 4 }, [width, height, 1]);
62
+ } else {
63
+ device.queue.copyExternalImageToTexture({ source, flipY }, { texture: srcTex }, [
64
+ width,
65
+ height,
66
+ 1
67
+ ]);
68
+ }
69
+ }
70
+
71
+ // src/Encoder.ts
53
72
  var Encoder = class {
54
73
  /**
55
74
  * Subclasses set this to the WebGPU feature string the output texture
@@ -77,6 +96,9 @@ var Encoder = class {
77
96
  if (this.requiredFeature && adapter.features.has(this.requiredFeature)) {
78
97
  requiredFeatures.push(this.requiredFeature);
79
98
  }
99
+ if (adapter.features.has("shader-f16")) {
100
+ requiredFeatures.push("shader-f16");
101
+ }
80
102
  const device = await adapter.requestDevice({ requiredFeatures });
81
103
  return new this({ device, adapter, ownsDevice: true });
82
104
  }
@@ -86,7 +108,14 @@ var Encoder = class {
86
108
  // `!:` because these are set in `_buildPipeline()` which the constructor
87
109
  // calls; TypeScript's flow analysis doesn't see through method calls.
88
110
  _module;
111
+ // f16 'fast' module — built only when the device supports shader-f16 and the
112
+ // subclass provides an f16 source. null otherwise (falls back to _module).
113
+ _moduleF16 = null;
114
+ _pipelineF16 = null;
115
+ // Default pipeline (fast). Kept as a field for back-compat; the per-quality
116
+ // cache below holds the specialised pipelines for encoders that support it.
89
117
  _pipeline;
118
+ _pipelineCache = /* @__PURE__ */ new Map();
90
119
  constructor({ device, adapter, ownsDevice = false }) {
91
120
  this.device = device;
92
121
  this.adapter = adapter;
@@ -100,11 +129,52 @@ var Encoder = class {
100
129
  label: `${this.label}-encoder`,
101
130
  code
102
131
  });
103
- this._pipeline = device.createComputePipeline({
104
- label: `${this.label}-encoder-pipeline`,
132
+ if (this._useF16) {
133
+ this._moduleF16 = device.createShaderModule({
134
+ label: `${this.label}-encoder-f16`,
135
+ code: this.wgslSourceFastF16()
136
+ });
137
+ }
138
+ if (this.supportsQuality) {
139
+ this._pipeline = this._getPipeline("fast");
140
+ } else {
141
+ this._pipeline = device.createComputePipeline({
142
+ label: `${this.label}-encoder-pipeline`,
143
+ layout: "auto",
144
+ compute: { module: this._module, entryPoint: "encode" }
145
+ });
146
+ }
147
+ }
148
+ /**
149
+ * Pipeline for a given quality level. Encoders that don't declare a
150
+ * `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
151
+ * argument and reuse the single pipeline. Specialised pipelines are cached.
152
+ */
153
+ _getPipeline(quality) {
154
+ if (!this.supportsQuality) return this._pipeline;
155
+ if (quality === "fast" && this._moduleF16) {
156
+ if (!this._pipelineF16) {
157
+ this._pipelineF16 = this.device.createComputePipeline({
158
+ label: `${this.label}-encoder-pipeline-fast-f16`,
159
+ layout: "auto",
160
+ compute: { module: this._moduleF16, entryPoint: "encode" }
161
+ });
162
+ }
163
+ return this._pipelineF16;
164
+ }
165
+ const cached = this._pipelineCache.get(quality);
166
+ if (cached) return cached;
167
+ const pipeline = this.device.createComputePipeline({
168
+ label: `${this.label}-encoder-pipeline-${quality}`,
105
169
  layout: "auto",
106
- compute: { module: this._module, entryPoint: "encode" }
170
+ compute: {
171
+ module: this._module,
172
+ entryPoint: "encode",
173
+ constants: { QUALITY_HIGH: quality === "high" ? 1 : 0 }
174
+ }
107
175
  });
176
+ this._pipelineCache.set(quality, pipeline);
177
+ return pipeline;
108
178
  }
109
179
  destroy() {
110
180
  if (this.ownsDevice) this.device.destroy();
@@ -117,6 +187,26 @@ var Encoder = class {
117
187
  get supportsSrgb() {
118
188
  return true;
119
189
  }
190
+ /**
191
+ * 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.
194
+ */
195
+ get supportsQuality() {
196
+ return false;
197
+ }
198
+ /**
199
+ * Optional f16 WGSL for the 'fast' path. Used only when the device reports the
200
+ * `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
201
+ * `'high'` always uses it. Returns null when there's no f16 variant (BC1).
202
+ */
203
+ wgslSourceFastF16() {
204
+ return null;
205
+ }
206
+ /** Whether the f16 fast path is both available and supported on this device. */
207
+ get _useF16() {
208
+ return this.wgslSourceFastF16() !== null && this.device.features.has("shader-f16");
209
+ }
120
210
  /**
121
211
  * True if the device reports the feature the output texture needs.
122
212
  * The encoder itself only writes to a storage buffer, so this is about
@@ -129,9 +219,9 @@ var Encoder = class {
129
219
  // ------------------------------------------------------------------ //
130
220
  // Shared encode() — pad, upload, dispatch, readback, wrap. //
131
221
  // ------------------------------------------------------------------ //
132
- async encode(source, { colorSpace = "srgb" } = {}) {
222
+ async encode(source, { colorSpace = "srgb", quality = "fast" } = {}) {
133
223
  const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
134
- const bytes = await this.encodeToBytes(source);
224
+ const bytes = await this.encodeToBytes(source, { quality });
135
225
  const threeFormat = this.threeTextureFormat({ colorSpace: effectiveSrgb ? "srgb" : "linear" });
136
226
  const mip = {
137
227
  data: bytes.data,
@@ -167,7 +257,7 @@ var Encoder = class {
167
257
  * encoder boundary. Still safe to call from outside — it just does
168
258
  * less work than `encode()` and the caller assembles the texture.
169
259
  */
170
- async encodeToBytes(source, { flipY = false } = {}) {
260
+ async encodeToBytes(source, { flipY = false, quality = "fast" } = {}) {
171
261
  const device = this.device;
172
262
  const width = source.width;
173
263
  const height = source.height;
@@ -184,9 +274,11 @@ var Encoder = class {
184
274
  label: `${this.label}-src`,
185
275
  size: [paddedWidth, paddedHeight, 1],
186
276
  format: "rgba8unorm",
277
+ // RENDER_ATTACHMENT is required by copyExternalImageToTexture
278
+ // (internally a blit) even though we never render into this texture.
187
279
  usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
188
280
  });
189
- device.queue.copyExternalImageToTexture({ source, flipY }, { texture: srcTex }, [width, height, 1]);
281
+ uploadSourceTexture(device, srcTex, source, width, height, flipY, source instanceof ImageData);
190
282
  const dstBuffer = device.createBuffer({
191
283
  label: `${this.label}-dst`,
192
284
  size: outByteLen,
@@ -198,9 +290,10 @@ var Encoder = class {
198
290
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
199
291
  });
200
292
  device.queue.writeBuffer(paramsBuffer, 0, new Uint32Array([blocksX, blocksY, paddedWidth, paddedHeight]));
293
+ const pipeline = this._getPipeline(quality);
201
294
  const bindGroup = device.createBindGroup({
202
295
  label: `${this.label}-bg`,
203
- layout: this._pipeline.getBindGroupLayout(0),
296
+ layout: pipeline.getBindGroupLayout(0),
204
297
  entries: [
205
298
  { binding: 0, resource: srcTex.createView() },
206
299
  { binding: 1, resource: { buffer: dstBuffer } },
@@ -211,7 +304,7 @@ var Encoder = class {
211
304
  const t0 = performance.now();
212
305
  const enc = device.createCommandEncoder({ label: `${this.label}-encode` });
213
306
  const pass = enc.beginComputePass();
214
- pass.setPipeline(this._pipeline);
307
+ pass.setPipeline(pipeline);
215
308
  pass.setBindGroup(0, bindGroup);
216
309
  pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(blocksY / wgY), 1);
217
310
  pass.end();
@@ -302,7 +395,10 @@ var BC1Encoder = class extends Encoder {
302
395
  import { RED_GREEN_RGTC2_Format } from "three";
303
396
 
304
397
  // src/bc5.wgsl
305
- var bc5_default = "// BC5 (RGTC2) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into a 16-byte BC5 block\n// written as 4 x u32 into the destination storage buffer.\n//\n// BC5 = two BC4 blocks concatenated:\n// block bytes 0..7 : BC4 of R channel (normal.x for tangent-space normals)\n// block bytes 8..15 : BC4 of G channel (normal.y)\n//\n// Each BC4 half-block (8 bytes):\n// byte 0 : red0 (8-bit endpoint)\n// byte 1 : red1 (8-bit endpoint)\n// bytes 2..7 : 16 \xD7 3-bit indices, LSB-first, pixel 0 at bit 0\n//\n// We always produce the 6-interpolation mode (red0 > red1). See\n// `bc4_ref.js` for the reasoning and the CPU reference this shader is\n// ported from \u2014 the algorithm and edge cases mirror it line-for-line.\n//\n// Pipeline per channel:\n// 1. Load 16 single-channel values, find min/max \u2192 initial endpoints.\n// 2. Quantize to 8-bit. Nudge apart if equal (forces 6-interp mode).\n// 3. Build palette, assign each texel its nearest entry (full L2).\n// 4. One-pass least-squares refinement: solve the 2\xD72 normal equations\n// for the (r0, r1) that minimizes \u03A3(palette[i_k] \u2212 v_k)\xB2. Accept\n// only if quantized endpoints still satisfy r0 > r1 AND total\n// squared error decreased.\n// 5. Pack 2 endpoint bytes + 48 bits of indices into the 8-byte block.\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\n// 6-interpolation-mode palette weights. palette[j] = W0_6[j]*r0 + W1_6[j]*r1.\n// Expressed as a switch so we don't rely on module-scope const arrays.\nfn w0_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 6.0 / 7.0; }\n case 3u: { return 5.0 / 7.0; }\n case 4u: { return 4.0 / 7.0; }\n case 5u: { return 3.0 / 7.0; }\n case 6u: { return 2.0 / 7.0; }\n default: { return 1.0 / 7.0; } // case 7u\n }\n}\n\nfn w1_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 7.0; }\n case 3u: { return 2.0 / 7.0; }\n case 4u: { return 3.0 / 7.0; }\n case 5u: { return 4.0 / 7.0; }\n case 6u: { return 5.0 / 7.0; }\n default: { return 6.0 / 7.0; } // case 7u\n }\n}\n\nfn quantize8(v: f32) -> u32 {\n // Round-to-nearest, clamp to [0, 255]. floor(x + 0.5) is the same\n // rounding rule the CPU reference uses.\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// Nearest-palette-index search over the 8-entry palette, returning the\n// index and squared error. `palette` is stored in function memory so we\n// pass by pointer.\nfn nearest_index(v: f32, palette: ptr<function, array<f32, 8>>) -> vec2<f32> {\n // x = best index (encoded as f32), y = best squared error.\n var best_j: u32 = 0u;\n var best_d: f32 = 1e20;\n for (var j: u32 = 0u; j < 8u; j = j + 1u) {\n let d = (*palette)[j] - v;\n let d2 = d * d;\n if (d2 < best_d) {\n best_d = d2;\n best_j = j;\n }\n }\n return vec2<f32>(f32(best_j), best_d);\n}\n\n// Encode 16 single-channel values into an 8-byte BC4 block, packed as\n// two little-endian u32s (u32[0] = bytes 0..3, u32[1] = bytes 4..7).\nfn encode_bc4(values: ptr<function, array<f32, 16>>) -> vec2<u32> {\n // ---------------- 1. Initial endpoints: bbox of input ----------------\n var vmin: f32 = 1.0;\n var vmax: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n vmin = min(vmin, (*values)[k]);\n vmax = max(vmax, (*values)[k]);\n }\n var r0: u32 = quantize8(vmax);\n var r1: u32 = quantize8(vmin);\n // Force 6-interp mode: red0 > red1 strictly.\n if (r0 == r1) {\n if (r1 > 0u) { r1 = r1 - 1u; }\n else { r0 = r0 + 1u; }\n }\n\n // ---------------- 2. Initial palette + indices + error --------------\n var palette: array<f32, 8>;\n let r0f = f32(r0) / 255.0;\n let r1f = f32(r1) / 255.0;\n for (var j: u32 = 0u; j < 8u; j = j + 1u) {\n palette[j] = w0_6(j) * r0f + w1_6(j) * r1f;\n }\n var indices: array<u32, 16>;\n var err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let sel = nearest_index((*values)[k], &palette);\n indices[k] = u32(sel.x);\n err = err + sel.y;\n }\n\n var best_r0 = r0;\n var best_r1 = r1;\n var best_indices = indices;\n var best_err = err;\n\n // ---------------- 3. Refinement: least-squares on (r0, r1) ----------\n // Normal equations for palette[j] = a_j * r0 + b_j * r1:\n // [\u03A3AA \u03A3AB] [r0] [\u03A3AV]\n // [\u03A3AB \u03A3BB] [r1] = [\u03A3BV]\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: f32 = 0.0; var sBV: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = w0_6(indices[k]);\n let b = w1_6(indices[k]);\n let v = (*values)[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 let det = sAA * sBB - sAB * sAB;\n // Degenerate system \u2192 skip refinement.\n if (abs(det) > 1e-9) {\n let new_r0 = clamp((sBB * sAV - sAB * sBV) / det, 0.0, 1.0);\n let new_r1 = clamp((sAA * sBV - sAB * sAV) / det, 0.0, 1.0);\n let qR0 = quantize8(new_r0);\n let qR1 = quantize8(new_r1);\n // Only accept refinements that stay in 6-interp mode. A refinement\n // that flips or equalizes the endpoints would change decode mode.\n if (qR0 > qR1) {\n var pal2: array<f32, 8>;\n let r0f2 = f32(qR0) / 255.0;\n let r1f2 = f32(qR1) / 255.0;\n for (var j: u32 = 0u; j < 8u; j = j + 1u) {\n pal2[j] = w0_6(j) * r0f2 + w1_6(j) * r1f2;\n }\n var idx2: array<u32, 16>;\n var err2: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let sel = nearest_index((*values)[k], &pal2);\n idx2[k] = u32(sel.x);\n err2 = err2 + sel.y;\n }\n if (err2 < best_err) {\n best_r0 = qR0;\n best_r1 = qR1;\n best_indices = idx2;\n best_err = err2;\n }\n }\n }\n\n // ---------------- 4. Pack 48-bit index field + 2 endpoint bytes -----\n // The 48-bit index field spans block bytes 2..7. Split into idx_lo\n // (low 32 bits of the field) and idx_hi (high 16 bits). An index at\n // bit position 3k straddles the 32-bit boundary iff 3k < 32 < 3k+3\n // (only k = 10, 11 straddle: bits 30..32 and 33..35; actually k=10\n // is bits 30..32, k=11 is 33..35 \u2014 so k=10 straddles). We handle\n // straddles by writing to both halves.\n var idx_lo: u32 = 0u;\n var idx_hi: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let bit = 3u * k;\n let v = best_indices[k] & 7u;\n if (bit + 3u <= 32u) {\n idx_lo = idx_lo | (v << bit);\n } else if (bit >= 32u) {\n idx_hi = idx_hi | (v << (bit - 32u));\n } else {\n // Straddle: low part into idx_lo's top, high part into idx_hi's bottom.\n idx_lo = idx_lo | (v << bit);\n idx_hi = idx_hi | (v >> (32u - bit));\n }\n }\n\n // Final u32s, both little-endian:\n // u32[0] bytes = red0, red1, idx_lo[7:0], idx_lo[15:8]\n // u32[1] bytes = idx_lo[23:16], idx_lo[31:24], idx_hi[7:0], idx_hi[15:8]\n let out_lo = best_r0 | (best_r1 << 8u) | ((idx_lo & 0xFFFFu) << 16u);\n let out_hi = (idx_lo >> 16u) | (idx_hi << 16u);\n\n return vec2<u32>(out_lo, out_hi);\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 // Load 4\xD74 RG values, splitting into per-channel arrays so each can\n // be handed to encode_bc4 independently.\n var r_values: array<f32, 16>;\n var g_values: array<f32, 16>;\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 input sizes.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0);\n r_values[i] = c.r;\n g_values[i] = c.g;\n }\n\n let r_block = encode_bc4(&r_values);\n let g_block = encode_bc4(&g_values);\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n let out = block_index * 4u;\n dst[out + 0u] = r_block.x;\n dst[out + 1u] = r_block.y;\n dst[out + 2u] = g_block.x;\n dst[out + 3u] = g_block.y;\n}\n";
398
+ var bc5_default = "// BC5 (RGTC2) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into a 16-byte BC5 block\n// written as 4 x u32 into the destination storage buffer.\n//\n// BC5 = two BC4 blocks concatenated:\n// block bytes 0..7 : BC4 of R channel (normal.x for tangent-space normals)\n// block bytes 8..15 : BC4 of G channel (normal.y)\n//\n// Each BC4 half-block (8 bytes):\n// byte 0 : red0 (8-bit endpoint)\n// byte 1 : red1 (8-bit endpoint)\n// bytes 2..7 : 16 \xD7 3-bit indices, LSB-first, pixel 0 at bit 0\n//\n// We always produce the 6-interpolation mode (red0 > red1). See\n// `bc4_ref.js` for the reasoning and the CPU reference this shader is\n// ported from \u2014 the algorithm and edge cases mirror it line-for-line.\n//\n// Pipeline per channel:\n// 1. Load 16 single-channel values, find min/max \u2192 initial endpoints.\n// 2. Quantize to 8-bit. Nudge apart if equal (forces 6-interp mode).\n// 3. Build palette, assign each texel its nearest entry (full L2).\n// 4. One-pass least-squares refinement: solve the 2\xD72 normal equations\n// for the (r0, r1) that minimizes \u03A3(palette[i_k] \u2212 v_k)\xB2. Accept\n// only if quantized endpoints still satisfy r0 > r1 AND total\n// squared error decreased.\n// 5. Pack 2 endpoint bytes + 48 bits of indices into the 8-byte block.\n//\n// The candidate endpoints/indices/error are tracked in place \u2014 the refit\n// overwrites them only when accepted \u2014 so no 16-entry index array is ever\n// copied across a function return.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): bbox endpoints + a single nearest-search assignment per\n// channel. The LSQ refit pass below is the bulk of the kernel and buys\n// only ~0.36 dB, so it is skipped \u2014 ~3.8\xD7 faster.\n// high (1): runs the refit, byte-for-byte identical to bc4_ref/bc5_ref.\n\n// 0 = fast (default), 1 = exhaustive/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\n// 6-interpolation-mode palette weights. palette[j] = W0_6[j]*r0 + W1_6[j]*r1.\n// Expressed as a switch so we don't rely on module-scope const arrays.\nfn w0_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 6.0 / 7.0; }\n case 3u: { return 5.0 / 7.0; }\n case 4u: { return 4.0 / 7.0; }\n case 5u: { return 3.0 / 7.0; }\n case 6u: { return 2.0 / 7.0; }\n default: { return 1.0 / 7.0; } // case 7u\n }\n}\n\nfn w1_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 7.0; }\n case 3u: { return 2.0 / 7.0; }\n case 4u: { return 3.0 / 7.0; }\n case 5u: { return 4.0 / 7.0; }\n case 6u: { return 5.0 / 7.0; }\n default: { return 6.0 / 7.0; } // case 7u\n }\n}\n\nfn quantize8(v: f32) -> u32 {\n // Round-to-nearest, clamp to [0, 255]. floor(x + 0.5) is the same\n // rounding rule the CPU reference uses.\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// Build the 8-entry palette for endpoints (r0f, r1f) in normalised space.\nfn build_pal(r0f: f32, r1f: f32, pal: ptr<function, array<f32, 8>>) {\n for (var j: u32 = 0u; j < 8u; j = j + 1u) {\n (*pal)[j] = w0_6(j) * r0f + w1_6(j) * r1f;\n }\n}\n\n// Assign each of the 16 values its nearest palette entry (full 8-entry L2),\n// writing indices into `out_idx` and returning the total squared error.\nfn assign_all(\n values: ptr<function, array<f32, 16>>,\n pal: ptr<function, array<f32, 8>>,\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 v = (*values)[k];\n var best_j: u32 = 0u;\n var best_d: f32 = 1e20;\n for (var j: u32 = 0u; j < 8u; j = j + 1u) {\n let d = (*pal)[j] - v;\n let d2 = 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// Encode 16 single-channel values into an 8-byte BC4 block, packed as\n// two little-endian u32s (u32[0] = bytes 0..3, u32[1] = bytes 4..7).\nfn encode_bc4(values: ptr<function, array<f32, 16>>) -> vec2<u32> {\n // ---------------- 1. Initial endpoints: bbox of input ----------------\n var vmin: f32 = 1.0;\n var vmax: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n vmin = min(vmin, (*values)[k]);\n vmax = max(vmax, (*values)[k]);\n }\n var r0: u32 = quantize8(vmax);\n var r1: u32 = quantize8(vmin);\n // Force 6-interp mode: red0 > red1 strictly.\n if (r0 == r1) {\n if (r1 > 0u) { r1 = r1 - 1u; }\n else { r0 = r0 + 1u; }\n }\n\n // ---------------- 2. Initial palette + indices + error --------------\n var pal: array<f32, 8>;\n build_pal(f32(r0) / 255.0, f32(r1) / 255.0, &pal);\n var indices: array<u32, 16>;\n var err = assign_all(values, &pal, &indices);\n\n // ---------------- 3. Refinement: least-squares on (r0, r1) ----------\n // High-quality only \u2014 the refit is the bulk of the per-channel cost and the\n // branch is resolved at pipeline-compile time, so the fast path skips all of\n // it (the sums loop included), not just the acceptance test.\n if (QUALITY_HIGH != 0u) {\n // Normal equations for palette[j] = a_j * r0 + b_j * r1:\n // [\u03A3AA \u03A3AB] [r0] [\u03A3AV]\n // [\u03A3AB \u03A3BB] [r1] = [\u03A3BV]\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: f32 = 0.0; var sBV: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = w0_6(indices[k]);\n let b = w1_6(indices[k]);\n let v = (*values)[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 let det = sAA * sBB - sAB * sAB;\n // Degenerate system \u2192 skip refinement.\n if (abs(det) > 1e-9) {\n let new_r0 = clamp((sBB * sAV - sAB * sBV) / det, 0.0, 1.0);\n let new_r1 = clamp((sAA * sBV - sAB * sAV) / det, 0.0, 1.0);\n let qR0 = quantize8(new_r0);\n let qR1 = quantize8(new_r1);\n // Only accept refinements that stay in 6-interp mode. A refinement\n // that flips or equalizes the endpoints would change decode mode.\n if (qR0 > qR1) {\n build_pal(f32(qR0) / 255.0, f32(qR1) / 255.0, &pal);\n var idx2: array<u32, 16>;\n let err2 = assign_all(values, &pal, &idx2);\n if (err2 < err) {\n r0 = qR0;\n r1 = qR1;\n indices = idx2;\n err = err2;\n }\n }\n }\n }\n\n // ---------------- 4. Pack 48-bit index field + 2 endpoint bytes -----\n // The 48-bit index field spans block bytes 2..7. Split into idx_lo\n // (low 32 bits of the field) and idx_hi (high 16 bits). An index at\n // bit position 3k straddles the 32-bit boundary iff 3k < 32 < 3k+3\n // (only k = 10, 11 straddle: bits 30..32 and 33..35; actually k=10\n // is bits 30..32, k=11 is 33..35 \u2014 so k=10 straddles). We handle\n // straddles by writing to both halves.\n var idx_lo: u32 = 0u;\n var idx_hi: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let bit = 3u * k;\n let v = indices[k] & 7u;\n if (bit + 3u <= 32u) {\n idx_lo = idx_lo | (v << bit);\n } else if (bit >= 32u) {\n idx_hi = idx_hi | (v << (bit - 32u));\n } else {\n // Straddle: low part into idx_lo's top, high part into idx_hi's bottom.\n idx_lo = idx_lo | (v << bit);\n idx_hi = idx_hi | (v >> (32u - bit));\n }\n }\n\n // Final u32s, both little-endian:\n // u32[0] bytes = red0, red1, idx_lo[7:0], idx_lo[15:8]\n // u32[1] bytes = idx_lo[23:16], idx_lo[31:24], idx_hi[7:0], idx_hi[15:8]\n let out_lo = r0 | (r1 << 8u) | ((idx_lo & 0xFFFFu) << 16u);\n let out_hi = (idx_lo >> 16u) | (idx_hi << 16u);\n\n return vec2<u32>(out_lo, out_hi);\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 // Load 4\xD74 RG values, splitting into per-channel arrays so each can\n // be handed to encode_bc4 independently.\n var r_values: array<f32, 16>;\n var g_values: array<f32, 16>;\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 input sizes.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0);\n r_values[i] = c.r;\n g_values[i] = c.g;\n }\n\n let r_block = encode_bc4(&r_values);\n let g_block = encode_bc4(&g_values);\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n let out = block_index * 4u;\n dst[out + 0u] = r_block.x;\n dst[out + 1u] = r_block.y;\n dst[out + 2u] = g_block.x;\n dst[out + 3u] = g_block.y;\n}\n";
399
+
400
+ // src/bc5_fast_f16.wgsl
401
+ var bc5_fast_f16_default = '// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).\n// Identical algorithm to the f32 fast path in bc5.wgsl, but the projection +\n// least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput\n// (e.g. Apple) this is ~2x faster at the same quality; endpoints are still\n// quantised to exact 8-bit. The host selects this module only when the device\n// reports shader-f16, falling back to bc5.wgsl otherwise. "high" never uses this.\n//\n// BC5 fast path in f16 (two BC4 halves, no refit). f16 halves the per-channel ALU.\nenable f16;\nalias h = f16;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };\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;\nfn w0(j: u32) -> h { switch j { case 0u:{return h(1.0);} case 1u:{return h(0.0);} case 2u:{return h(6.0/7.0);} case 3u:{return h(5.0/7.0);} case 4u:{return h(4.0/7.0);} case 5u:{return h(3.0/7.0);} case 6u:{return h(2.0/7.0);} default:{return h(1.0/7.0);} } }\nfn w1(j: u32) -> h { switch j { case 0u:{return h(0.0);} case 1u:{return h(1.0);} case 2u:{return h(1.0/7.0);} case 3u:{return h(2.0/7.0);} case 4u:{return h(3.0/7.0);} case 5u:{return h(4.0/7.0);} case 6u:{return h(5.0/7.0);} default:{return h(6.0/7.0);} } }\nfn q8(v: h) -> u32 { return u32(clamp(floor(v*h(255.0)+h(0.5)), h(0.0), h(255.0))); }\nfn encode_bc4(values: ptr<function, array<h,16>>) -> vec2<u32> {\n var vmin=h(1.0); var vmax=h(0.0);\n for(var k:u32=0u;k<16u;k=k+1u){ vmin=min(vmin,(*values)[k]); vmax=max(vmax,(*values)[k]); }\n var r0=q8(vmax); var r1=q8(vmin);\n if(r0==r1){ if(r1>0u){r1=r1-1u;}else{r0=r0+1u;} }\n var pal: array<h,8>; let r0f=h(f32(r0)/255.0); let r1f=h(f32(r1)/255.0);\n for(var j:u32=0u;j<8u;j=j+1u){ pal[j]=w0(j)*r0f+w1(j)*r1f; }\n var indices: array<u32,16>;\n for(var k:u32=0u;k<16u;k=k+1u){ let v=(*values)[k]; var bj=0u; var bd=h(1e4); for(var j:u32=0u;j<8u;j=j+1u){ let d=pal[j]-v; let d2=d*d; if(d2<bd){bd=d2;bj=j;} } indices[k]=bj; }\n var lo=0u; var hi=0u;\n for(var k:u32=0u;k<16u;k=k+1u){ let bit=3u*k; let v=indices[k]&7u;\n if(bit+3u<=32u){ lo=lo|(v<<bit); } else if(bit>=32u){ hi=hi|(v<<(bit-32u)); } else { lo=lo|(v<<bit); hi=hi|(v>>(32u-bit)); } }\n return vec2<u32>(r0 | (r1<<8u) | ((lo&0xFFFFu)<<16u), (lo>>16u) | (hi<<16u));\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){return;}\n let bi=gid.y*params.blocks_x+gid.x;\n let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);\n var rv: array<h,16>; var gv: array<h,16>;\n for(var i:u32=0u;i<16u;i=i+1u){ let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx); let c=textureLoad(src_tex,p,0); rv[i]=h(c.r); gv[i]=h(c.g); }\n let rb=encode_bc4(&rv); let gb=encode_bc4(&gv);\n let o=bi*4u; dst[o]=rb.x; dst[o+1u]=rb.y; dst[o+2u]=gb.x; dst[o+3u]=gb.y;\n}\n';
306
402
 
307
403
  // src/BC5Encoder.ts
308
404
  var BC5Encoder = class extends Encoder {
@@ -317,9 +413,15 @@ var BC5Encoder = class extends Encoder {
317
413
  get supportsSrgb() {
318
414
  return false;
319
415
  }
416
+ get supportsQuality() {
417
+ return true;
418
+ }
320
419
  wgslSource() {
321
420
  return bc5_default;
322
421
  }
422
+ wgslSourceFastF16() {
423
+ return bc5_fast_f16_default;
424
+ }
323
425
  gpuTextureFormat() {
324
426
  return "bc5-rg-unorm";
325
427
  }
@@ -332,7 +434,87 @@ var BC5Encoder = class extends Encoder {
332
434
  import { RGBA_BPTC_Format } from "three";
333
435
 
334
436
  // src/bc7.wgsl
335
- var bc7_default = "// BC7 (BPTC) mode 6 compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// This shader mirrors `bc7_ref.ts` function-by-function; see that file for\n// the end-to-end algorithm rationale and the full mode 6 bitstream layout\n// (summarised below).\n//\n// MODE 6 LAYOUT (LSB-first, bit 0 = byte 0's bit 0)\n// bits 0..6 mode field (0b0000001 \u2014 only bit 6 is 1)\n// bits 7..13 R0 (7-bit)\n// bits 14..20 R1\n// bits 21..27 G0\n// bits 28..34 G1 \u2190 straddles the word 0 / word 1 boundary\n// bits 35..41 B0\n// bits 42..48 B1\n// bits 49..55 A0\n// bits 56..62 A1\n// bit 63 P0 (shared p-bit for endpoint 0)\n// bit 64 P1\n// bits 65..67 pixel 0 index (3 bits; anchor, MSB implicit 0)\n// bits 68..71 pixel 1 index (4 bits)\n// ...\n// bits 124..127 pixel 15 index\n//\n// Effective 8-bit endpoint channel = (7_bit_value << 1) | p_bit.\n// Palette[i] = ((64 \u2212 W4[i]) \xD7 e0_8 + W4[i] \xD7 e1_8 + 32) >> 6, integer.\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\n// Mode 6 interpolation weights (\xD7 1/64), fixed by the spec. Same table as\n// the CPU reference (`W4` in bc7_ref.ts).\nfn w4(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 4u; }\n case 2u: { return 9u; }\n case 3u: { return 13u; }\n case 4u: { return 17u; }\n case 5u: { return 21u; }\n case 6u: { return 26u; }\n case 7u: { return 30u; }\n case 8u: { return 34u; }\n case 9u: { return 38u; }\n case 10u: { return 43u; }\n case 11u: { return 47u; }\n case 12u: { return 51u; }\n case 13u: { return 55u; }\n case 14u: { return 60u; }\n default: { return 64u; } // case 15u\n }\n}\n\n// Hardware-exact integer interpolation.\nfn interp8(e0: u32, e1: u32, w: u32) -> u32 {\n return ((64u - w) * e0 + w * e1 + 32u) >> 6u;\n}\n\n// f32-normalised [0,1] \u2192 clamped 8-bit.\nfn to8(v: f32) -> u32 {\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// -------------------------- Farthest-pair seed -------------------------- //\n\nstruct PairResult { i0: u32, i1: u32 };\n\n// 4-channel L2 distance squared, u32 domain (bounded by 4 \xD7 255\xB2 = 260 100).\nfn pixel_dist_sq(a: vec4<u32>, b: vec4<u32>) -> u32 {\n let d = vec4<i32>(a) - vec4<i32>(b);\n let d2 = d * d;\n return u32(d2.x + d2.y + d2.z + d2.w);\n}\n\n// O(N\xB2) = 120 comparisons. See bc7_ref.ts `farthestPair` for why bbox\n// corners aren't safe initial endpoints when channels vary in different\n// directions along the data line.\nfn farthest_pair(pixels: ptr<function, array<vec4<u32>, 16>>) -> PairResult {\n var best_d: u32 = 0u;\n var best_i: u32 = 0u;\n var best_j: u32 = 1u;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = pixel_dist_sq((*pixels)[i], (*pixels)[j]);\n if (d > best_d) { best_d = d; best_i = i; best_j = j; }\n }\n }\n return PairResult(best_i, best_j);\n}\n\n// -------------------------- Palette + assignment ------------------------ //\n\n// Build the 16-entry RGBA palette from 8-bit endpoints.\nfn build_palette_6(e0: vec4<u32>, e1: vec4<u32>, pal: ptr<function, array<vec4<u32>, 16>>) {\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let w = w4(i);\n (*pal)[i] = vec4<u32>(\n interp8(e0.x, e1.x, w),\n interp8(e0.y, e1.y, w),\n interp8(e0.z, e1.z, w),\n interp8(e0.w, e1.w, w),\n );\n }\n}\n\n// Nearest-palette-entry search for one pixel. Full 16-entry L2 search.\nfn nearest_index_6(pixel: vec4<u32>, pal: ptr<function, array<vec4<u32>, 16>>) -> vec2<u32> {\n var best_i: u32 = 0u;\n var best_d: u32 = 0xFFFFFFFFu;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let d = pixel_dist_sq(pixel, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n // x = best index, y = its squared error.\n return vec2<u32>(best_i, best_d);\n}\n\n// Assign all 16 pixels to nearest palette entries, accumulate total error.\nstruct AssignResult { indices: array<u32, 16>, err: u32 };\n\nfn assign_all(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n pal: ptr<function, array<vec4<u32>, 16>>,\n) -> AssignResult {\n var out: AssignResult;\n out.err = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let sel = nearest_index_6((*pixels)[k], pal);\n out.indices[k] = sel.x;\n out.err = out.err + sel.y;\n }\n return out;\n}\n\n// -------------------------- Endpoint quantisation ----------------------- //\n\n// Quantize one 8-bit ideal channel to (7-bit value, reconstructed 8-bit)\n// under a fixed p-bit. Matches the CPU reference.\nfn quantize_ch(ideal8: u32, p: u32) -> vec2<u32> {\n // q7 = round((ideal8 \u2212 p) / 2), clamp to [0, 127].\n let q = u32(clamp(\n floor((f32(ideal8) - f32(p)) / 2.0 + 0.5),\n 0.0, 127.0,\n ));\n let eff = (q << 1u) | p;\n return vec2<u32>(q, eff);\n}\n\nstruct QuantPair { seven: vec4<u32>, eight: vec4<u32> };\n\nfn quantize_endpoint(ideal8: vec4<u32>, p: u32) -> QuantPair {\n let r = quantize_ch(ideal8.x, p);\n let g = quantize_ch(ideal8.y, p);\n let b = quantize_ch(ideal8.z, p);\n let a = quantize_ch(ideal8.w, p);\n return QuantPair(\n vec4<u32>(r.x, g.x, b.x, a.x),\n vec4<u32>(r.y, g.y, b.y, a.y),\n );\n}\n\n// Try all four p-bit combos (p0, p1) \u2208 {0,1}\xB2 and return the best\n// quantised-endpoint-plus-indices triple.\nstruct BestMode6 {\n e0_7: vec4<u32>, e1_7: vec4<u32>,\n p0: u32, p1: u32,\n indices: array<u32, 16>,\n err: u32,\n};\n\nfn try_pbit_combos(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n ideal0: vec4<u32>,\n ideal1: vec4<u32>,\n) -> BestMode6 {\n var best: BestMode6;\n best.err = 0xFFFFFFFFu;\n for (var p0: u32 = 0u; p0 < 2u; p0 = p0 + 1u) {\n let q0 = quantize_endpoint(ideal0, p0);\n for (var p1: u32 = 0u; p1 < 2u; p1 = p1 + 1u) {\n let q1 = quantize_endpoint(ideal1, p1);\n var pal: array<vec4<u32>, 16>;\n build_palette_6(q0.eight, q1.eight, &pal);\n let assigned = assign_all(pixels, &pal);\n if (assigned.err < best.err) {\n best.e0_7 = q0.seven;\n best.e1_7 = q1.seven;\n best.p0 = p0;\n best.p1 = p1;\n best.indices = assigned.indices;\n best.err = assigned.err;\n }\n }\n }\n return best;\n}\n\n// ---------------------- Least-squares endpoint refit -------------------- //\n\n// Channel-independent LSQ fit of (e0, e1) given current indices. Normal\n// equations: see bc7_ref.ts `refitEndpointsMode6`. Returns 8-bit ideal\n// endpoints (before p-bit quantisation). `valid` = false for a degenerate\n// system (all texels on one palette entry).\nstruct RefitResult { e0: vec4<u32>, e1: vec4<u32>, valid: bool };\n\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let i = (*indices)[k];\n let a = f32(64u - w4(i)) / 64.0;\n let b = f32(w4(i)) / 64.0;\n let v = vec4<f32>((*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 let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<u32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<u32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\n// Write `n_bits` LSBs of `value` at bit position `pos` in a 128-bit field\n// split across 4 u32s. Straddles the word boundary when necessary.\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry --------------------------------- //\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 // 1. Load 16 RGBA pixels in 8-bit integer domain.\n var pixels: array<vec4<u32>, 16>;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0);\n pixels[i] = vec4<u32>(to8(c.r), to8(c.g), to8(c.b), to8(c.a));\n }\n\n // 2. Farthest-pair \u2192 initial endpoints.\n let fp = farthest_pair(&pixels);\n let ideal0_init = pixels[fp.i0];\n let ideal1_init = pixels[fp.i1];\n\n // 3. First p-bit search over the farthest-pair seed.\n var best = try_pbit_combos(&pixels, ideal0_init, ideal1_init);\n\n // 4. One-pass LSQ refit + second p-bit search; accept if error decreases.\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n let cand = try_pbit_combos(&pixels, refit.e0, refit.e1);\n if (cand.err < best.err) {\n best = cand;\n }\n }\n\n // 5. Anchor rule \u2014 pixel 0's index MSB must be 0. If not, swap endpoints\n // and reflect every index (new_i = 15 \u2212 old_i). The decoded palette\n // reverses, so the reconstructed image is unchanged.\n if ((best.indices[0] & 0x8u) != 0u) {\n let tmp7 = best.e0_7; best.e0_7 = best.e1_7; best.e1_7 = tmp7;\n let tmpP = best.p0; best.p0 = best.p1; best.p1 = tmpP;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n best.indices[k] = 15u - best.indices[k];\n }\n }\n\n // 6. Pack into 128 bits = 4 u32s.\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n\n var pos: u32 = 0u;\n // Mode 6: six zero bits followed by a 1 (LSB-first).\n write_bits(&block, pos, 7u, 0x40u); pos = pos + 7u;\n // Endpoints: R0, R1, G0, G1, B0, B1, A0, A1 \u2014 7 bits each.\n write_bits(&block, pos, 7u, best.e0_7.x); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.x); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e0_7.y); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.y); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e0_7.z); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.z); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e0_7.w); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.w); pos = pos + 7u;\n // P-bits.\n write_bits(&block, pos, 1u, best.p0); pos = pos + 1u;\n write_bits(&block, pos, 1u, best.p1); pos = pos + 1u;\n // Pixel 0: 3-bit anchor (MSB implicit 0).\n write_bits(&block, pos, 3u, best.indices[0] & 0x7u); pos = pos + 3u;\n // Pixels 1..15: 4 bits each.\n for (var k: u32 = 1u; k < 16u; k = k + 1u) {\n write_bits(&block, pos, 4u, best.indices[k] & 0xFu);\n pos = pos + 4u;\n }\n\n // 7. Store.\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
437
+ var bc7_default = "// BC7 (BPTC) mode 6 compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): O(N) bounding-box seed \u2192 endpoints fitted by a single\n// least-squares pass whose normal-equation sums are accumulated *during* a\n// projection-based index assignment. The 16 palette entries are colinear\n// (pal[i] = lerp(e0,e1,w[i])), so the nearest index is found by projecting\n// each pixel onto the endpoint line \u2014 O(1) per pixel, no palette build and\n// no 16-entry search. Profiled ~20\xD7 faster than `high` for ~0.4 dB PSNR.\n// high (1): farthest-pair seed, exhaustive p-bit search over all four\n// (p0,p1) \u2208 {0,1}\xB2 combos, full 16-entry nearest search, one LSQ refit \u2014\n// byte-for-byte identical to bc7_ref.ts.\n//\n// Both paths run in the i32 domain. The fast path's branch is selected at\n// pipeline-compile time, so the driver eliminates the unused (high) code.\n//\n// MODE 6 LAYOUT (LSB-first, bit 0 = byte 0's bit 0)\n// bits 0..6 mode field (0b0000001 \u2014 only bit 6 is 1)\n// bits 7..13 R0 (7-bit) bits 14..20 R1 bits 21..27 G0 bits 28..34 G1\n// bits 35..41 B0 bits 42..48 B1 bits 49..55 A0 bits 56..62 A1\n// bit 63 P0 bit 64 P1\n// bits 65..67 pixel 0 index (3 bits; anchor, MSB implicit 0)\n// bits 68..71 pixel 1 index (4 bits) ... bits 124..127 pixel 15 index\n//\n// Effective 8-bit endpoint channel = (7_bit_value << 1) | p_bit.\n// Palette[i] = ((64 \u2212 W4[i]) \xD7 e0_8 + W4[i] \xD7 e1_8 + 32) >> 6, integer.\n\n// 0 = fast (default), 1 = exhaustive/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\n// Mode 6 interpolation weights (\xD7 1/64), fixed by the spec (`W4` in bc7_ref.ts).\nfn w4(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 4u; }\n case 2u: { return 9u; }\n case 3u: { return 13u; }\n case 4u: { return 17u; }\n case 5u: { return 21u; }\n case 6u: { return 26u; }\n case 7u: { return 30u; }\n case 8u: { return 34u; }\n case 9u: { return 38u; }\n case 10u: { return 43u; }\n case 11u: { return 47u; }\n case 12u: { return 51u; }\n case 13u: { return 55u; }\n case 14u: { return 60u; }\n default: { return 64u; } // case 15u\n }\n}\n\nfn interp4(e0: vec4<i32>, e1: vec4<i32>, w: i32) -> vec4<i32> {\n return ((64 - w) * e0 + w * e1 + vec4<i32>(32)) >> vec4<u32>(6u);\n}\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\nfn dist2(a: vec4<i32>, b: vec4<i32>) -> i32 {\n let d = a - b;\n let 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. q7 = round((ideal8 \u2212 p)/2); used by\n// both paths.\nstruct QuantPair { seven: vec4<i32>, eight: vec4<i32> };\nfn quantize_endpoint(ideal8: vec4<i32>, p: u32) -> QuantPair {\n let q = vec4<i32>(clamp(\n floor((vec4<f32>(ideal8) - f32(p)) / 2.0 + 0.5),\n vec4<f32>(0.0), vec4<f32>(127.0),\n ));\n let eff = (q << vec4<u32>(1u)) | vec4<i32>(i32(p));\n return QuantPair(q, eff);\n}\n\n// ============================ FAST PATH ================================ //\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nstruct Ep { seven: vec4<i32>, eight: vec4<i32>, p: u32 };\nfn pick_ep(ideal: vec4<i32>) -> Ep {\n let a = quantize_endpoint(ideal, 0u);\n let b = quantize_endpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) { return Ep(b.seven, b.eight, 1u); }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Projection index assignment. The palette is colinear, so the nearest entry is\n// found by projecting onto the endpoint line \u2014 O(1) per pixel. When `fit`, the\n// LSQ normal-equation sums are accumulated in the same pass for a fused refit\n// (uniform weight i/15 \u2014 within a fraction of a code of the exact w4 table).\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_assign(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n e0: vec4<i32>, e1: vec4<i32>,\n out_idx: ptr<function, array<u32, 16>>,\n fit: bool,\n) -> Fit {\n var out: Fit;\n let dir = e1 - e0;\n let dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (var k: u32 = 0u; k < 16u; k = k + 1u) { (*out_idx)[k] = 0u; }\n out.valid = false;\n return out;\n }\n let inv = 15.0 / f32(dd);\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0); var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let q = (*pixels)[k] - e0;\n let s = clamp(floor(f32(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv + 0.5), 0.0, 15.0);\n (*out_idx)[k] = u32(s);\n if (fit) {\n let v = vec4<f32>((*pixels)[k]);\n let b = s / 15.0; let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b; sAV = sAV + a * v; sBV = sBV + b * v;\n }\n }\n if (!fit) { out.valid = false; return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { out.valid = false; return out; }\n out.e0 = vec4<i32>(clamp(round((sBB * sAV - sAB * sBV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round((sAA * sBV - sAB * sAV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ============================ HIGH PATH ================================ //\n\nstruct Pair { a: vec4<i32>, b: vec4<i32> };\nfn farthest_pair(pixels: ptr<function, array<vec4<i32>, 16>>) -> Pair {\n var best_d: i32 = 0;\n var pa = (*pixels)[0];\n var pb = (*pixels)[1];\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let xi = (*pixels)[i];\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = dist2(xi, (*pixels)[j]);\n if (d > best_d) { best_d = d; pa = xi; pb = (*pixels)[j]; }\n }\n }\n return Pair(pa, pb);\n}\n\nfn build_palette_6(e0: vec4<i32>, e1: vec4<i32>, pal: ptr<function, array<vec4<i32>, 16>>) {\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n (*pal)[i] = interp4(e0, e1, i32(w4(i)));\n }\n}\n\nfn assign_all(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n pal: ptr<function, array<vec4<i32>, 16>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> i32 {\n var err: i32 = 0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let px = (*pixels)[k];\n var best_i: u32 = 0u;\n var best_d: i32 = 2147483647;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let d = dist2(px, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n (*out_idx)[k] = best_i;\n err = err + best_d;\n }\n return err;\n}\n\nstruct BestMode6 {\n e0_7: vec4<i32>, e1_7: vec4<i32>,\n p0: u32, p1: u32,\n indices: array<u32, 16>,\n err: i32,\n};\n\n// Exhaustive p-bit search (high path); commits to `*best` only on improvement.\nfn try_pbit_combos(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n ideal0: vec4<i32>,\n ideal1: vec4<i32>,\n best: ptr<function, BestMode6>,\n) {\n var local_best = (*best).err;\n var pal: array<vec4<i32>, 16>;\n var tmp: array<u32, 16>;\n for (var p0: u32 = 0u; p0 < 2u; p0 = p0 + 1u) {\n let q0 = quantize_endpoint(ideal0, p0);\n for (var p1: u32 = 0u; p1 < 2u; p1 = p1 + 1u) {\n let q1 = quantize_endpoint(ideal1, p1);\n build_palette_6(q0.eight, q1.eight, &pal);\n let err = assign_all(pixels, &pal, &tmp);\n if (err < local_best) {\n local_best = err;\n (*best).e0_7 = q0.seven;\n (*best).e1_7 = q1.seven;\n (*best).p0 = p0;\n (*best).p1 = p1;\n (*best).indices = tmp;\n (*best).err = err;\n }\n }\n }\n}\n\n// Exact-weight LSQ refit (high path); matches bc7_ref.ts `refitEndpointsMode6`.\nstruct RefitResult { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let i = (*indices)[k];\n let a = f32(64u - w4(i)) / 64.0;\n let b = f32(w4(i)) / 64.0;\n let v = vec4<f32>((*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 let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<i32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry --------------------------------- //\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 // Load 16 RGBA pixels (8-bit integer domain) and the per-channel bbox.\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let px = to8(textureLoad(src_tex, p, 0));\n pixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n var e0_7: vec4<i32>;\n var e1_7: vec4<i32>;\n var p0: u32;\n var p1: u32;\n var indices: array<u32, 16>;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n var best: BestMode6;\n best.err = 2147483647;\n try_pbit_combos(&pixels, fp.a, fp.b, &best);\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n try_pbit_combos(&pixels, refit.e0, refit.e1, &best);\n }\n e0_7 = best.e0_7; e1_7 = best.e1_7; p0 = best.p0; p1 = best.p1; indices = best.indices;\n } else {\n var ep0 = pick_ep(lo);\n var ep1 = pick_ep(hi);\n let r = proj_assign(&pixels, ep0.eight, ep1.eight, &indices, true);\n if (r.valid) {\n ep0 = pick_ep(r.e0);\n ep1 = pick_ep(r.e1);\n proj_assign(&pixels, ep0.eight, ep1.eight, &indices, false);\n }\n e0_7 = ep0.seven; e1_7 = ep1.seven; p0 = ep0.p; p1 = ep1.p;\n }\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0. If not, swap endpoints and\n // reflect every index (new_i = 15 \u2212 old_i); decoded image is unchanged.\n if ((indices[0] & 0x8u) != 0u) {\n let t7 = e0_7; e0_7 = e1_7; e1_7 = t7;\n let tp = p0; p0 = p1; p1 = tp;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices[k] = 15u - indices[k];\n }\n }\n\n // Pack into 128 bits = 4 u32s.\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n var pos: u32 = 0u;\n write_bits(&block, pos, 7u, 0x40u); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.x)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.x)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.y)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.y)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.z)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.z)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.w)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.w)); pos = pos + 7u;\n write_bits(&block, pos, 1u, p0); pos = pos + 1u;\n write_bits(&block, pos, 1u, p1); pos = pos + 1u;\n write_bits(&block, pos, 3u, indices[0] & 0x7u); pos = pos + 3u;\n for (var k: u32 = 1u; k < 16u; k = k + 1u) {\n write_bits(&block, pos, 4u, indices[k] & 0xFu);\n pos = pos + 4u;\n }\n\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
438
+
439
+ // src/bc7_fast_f16.wgsl
440
+ var bc7_fast_f16_default = `// bc7 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
441
+ // Identical algorithm to the f32 fast path in bc7.wgsl, but the projection +
442
+ // least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput
443
+ // (e.g. Apple) this is ~2x faster at the same quality; endpoints are still
444
+ // quantised to exact 8-bit. The host selects this module only when the device
445
+ // reports shader-f16, falling back to bc7.wgsl otherwise. "high" never uses this.
446
+ //
447
+ // BC7 mode 6 fast path in f16 (Apple GPUs run f16 at 2x). All math in the [0,1]
448
+ // domain so dot products stay well under f16's range; endpoints quantised to
449
+ // 8-bit at the end. Same bbox seed + projection + fused LSQ refit + reproject.
450
+ enable f16;
451
+ struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
452
+ @group(0) @binding(0) var src_tex: texture_2d<f32>;
453
+ @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
454
+ @group(0) @binding(2) var<uniform> params: Params;
455
+ alias h = f16;
456
+ alias h4 = vec4<f16>;
457
+ struct Ep { seven: vec4<i32>, eight: h4, p: u32 };
458
+ // quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit; returns 8-bit eff in [0,1].
459
+ fn pick_ep(ideal01: h4) -> Ep {
460
+ let ideal = ideal01 * h(255.0);
461
+ let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0
462
+ let e0 = q0 * h(2.0);
463
+ let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1
464
+ let e1 = q1 * h(2.0) + h(1.0);
465
+ let d0 = e0 - ideal; let d1 = e1 - ideal;
466
+ if (dot(d1,d1) < dot(d0,d0)) { return Ep(vec4<i32>(q1), e1 * h(1.0/255.0), 1u); }
467
+ return Ep(vec4<i32>(q0), e0 * h(1.0/255.0), 0u);
468
+ }
469
+ struct Fit { e0: h4, e1: h4, valid: bool };
470
+ fn proj_assign(pix: ptr<function, array<h4,16>>, e0: h4, e1: h4, out_idx: ptr<function, array<u32,16>>, fit: bool) -> Fit {
471
+ var out: Fit; let dir = e1 - e0; let dd = dot(dir,dir);
472
+ if (dd == h(0.0)) { for(var k:u32=0u;k<16u;k=k+1u){(*out_idx)[k]=0u;} out.valid=false; return out; }
473
+ let inv = h(15.0) / dd;
474
+ var sAA=h(0.0); var sBB=h(0.0); var sAB=h(0.0); var sAV=h4(0.0); var sBV=h4(0.0);
475
+ for(var k:u32=0u;k<16u;k=k+1u){
476
+ let v=(*pix)[k];
477
+ let s = clamp(floor(dot(v - e0, dir) * inv + h(0.5)), h(0.0), h(15.0));
478
+ (*out_idx)[k] = u32(s);
479
+ if(fit){ let b=s*h(1.0/15.0); let a=h(1.0)-b; sAA=sAA+a*a; sBB=sBB+b*b; sAB=sAB+a*b; sAV=sAV+a*v; sBV=sBV+b*v; }
480
+ }
481
+ if(!fit){ out.valid=false; return out; }
482
+ let det = sAA*sBB - sAB*sAB; if (abs(det) < h(0.0001)) { out.valid=false; return out; }
483
+ out.e0 = clamp((sBB*sAV - sAB*sBV)/det, h4(0.0), h4(1.0));
484
+ out.e1 = clamp((sAA*sBV - sAB*sAV)/det, h4(0.0), h4(1.0));
485
+ out.valid=true; return out;
486
+ }
487
+ fn write_bits(block: ptr<function, array<u32,4>>, pos: u32, n_bits: u32, value: u32) {
488
+ let v=value&((1u<<n_bits)-1u); let wl=pos/32u; let bl=pos%32u; let il=min(n_bits,32u-bl);
489
+ let ml=((1u<<il)-1u)<<bl; (*block)[wl]=((*block)[wl]&~ml)|((v<<bl)&ml);
490
+ if(il<n_bits){ let ih=n_bits-il; let mh=(1u<<ih)-1u; (*block)[wl+1u]=((*block)[wl+1u]&~mh)|((v>>il)&mh); }
491
+ }
492
+ @compute @workgroup_size(8,8,1)
493
+ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
494
+ if(gid.x>=params.blocks_x||gid.y>=params.blocks_y){return;}
495
+ let bi=gid.y*params.blocks_x+gid.x;
496
+ let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);
497
+ var pix: array<h4,16>; var lo=h4(1.0); var hi=h4(0.0);
498
+ for(var i:u32=0u;i<16u;i=i+1u){
499
+ let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx);
500
+ let px=h4(textureLoad(src_tex,p,0)); pix[i]=px; lo=min(lo,px); hi=max(hi,px);
501
+ }
502
+ var ep0=pick_ep(lo); var ep1=pick_ep(hi); var indices: array<u32,16>;
503
+ let r=proj_assign(&pix,ep0.eight,ep1.eight,&indices,true);
504
+ if(r.valid){ ep0=pick_ep(r.e0); ep1=pick_ep(r.e1); proj_assign(&pix,ep0.eight,ep1.eight,&indices,false); }
505
+ if((indices[0]&0x8u)!=0u){ let t=ep0; ep0=ep1; ep1=t; for(var k:u32=0u;k<16u;k=k+1u){indices[k]=15u-indices[k];} }
506
+ var block: array<u32,4>; block[0]=0u;block[1]=0u;block[2]=0u;block[3]=0u; var pos:u32=0u;
507
+ write_bits(&block,pos,7u,0x40u);pos=pos+7u;
508
+ write_bits(&block,pos,7u,u32(ep0.seven.x));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.x));pos=pos+7u;
509
+ write_bits(&block,pos,7u,u32(ep0.seven.y));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.y));pos=pos+7u;
510
+ write_bits(&block,pos,7u,u32(ep0.seven.z));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.z));pos=pos+7u;
511
+ write_bits(&block,pos,7u,u32(ep0.seven.w));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.w));pos=pos+7u;
512
+ write_bits(&block,pos,1u,ep0.p);pos=pos+1u; write_bits(&block,pos,1u,ep1.p);pos=pos+1u;
513
+ write_bits(&block,pos,3u,indices[0]&0x7u);pos=pos+3u;
514
+ for(var k:u32=1u;k<16u;k=k+1u){write_bits(&block,pos,4u,indices[k]&0xFu);pos=pos+4u;}
515
+ let o=bi*4u; dst[o]=block[0];dst[o+1u]=block[1];dst[o+2u]=block[2];dst[o+3u]=block[3];
516
+ }
517
+ `;
336
518
 
337
519
  // src/BC7Encoder.ts
338
520
  var BC7Encoder = class extends Encoder {
@@ -347,9 +529,15 @@ var BC7Encoder = class extends Encoder {
347
529
  get supportsSrgb() {
348
530
  return true;
349
531
  }
532
+ get supportsQuality() {
533
+ return true;
534
+ }
350
535
  wgslSource() {
351
536
  return bc7_default;
352
537
  }
538
+ wgslSourceFastF16() {
539
+ return bc7_fast_f16_default;
540
+ }
353
541
  gpuTextureFormat({ colorSpace }) {
354
542
  return colorSpace === "srgb" ? "bc7-rgba-unorm-srgb" : "bc7-rgba-unorm";
355
543
  }
@@ -362,7 +550,10 @@ var BC7Encoder = class extends Encoder {
362
550
  import { RGBA_ASTC_4x4_Format } from "three";
363
551
 
364
552
  // src/astc4x4.wgsl
365
- var astc4x4_default = "// ASTC 4\xD74 LDR compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// This shader mirrors `astc4x4_ref.ts` function-by-function; see that\n// file for the end-to-end algorithm rationale and the full block layout\n// / block-mode derivation. A short recap follows.\n//\n// RESTRICTED SUBSET (both CPU ref and this shader):\n// \u2022 Single partition, no dual-plane\n// \u2022 CEM 12 (LDR RGBA, direct)\n// \u2022 Weight grid 4\xD74 (no upsampling), 2-bit weights (QUANT_4)\n// \u2022 8-bit endpoints (QUANT_256 \u2014 bit-replication is a no-op)\n//\n// BLOCK LAYOUT (128 bits, LSB-first)\n// bits [10:0] block mode = 0x042\n// bits [12:11] partition count \u2212 1 = 0\n// bits [16:13] CEM = 12\n// bits [80:17] endpoints: R0 R1 G0 G1 B0 B1 A0 A1 (8-bit each)\n// bits [95:81] unused (zero padding)\n// bits [127:96] 16 \xD7 2-bit weights; for weight k \u2208 [0,15]:\n// block_bit(127 \u2212 2k) = weight_k[0] (LSB)\n// block_bit(126 \u2212 2k) = weight_k[1] (MSB)\n//\n// ENDPOINT ORDERING: after fitting, if sum(e0.rgb) > sum(e1.rgb) we swap\n// endpoints and reflect indices (w' = 3 \u2212 w). This keeps the decoder out\n// of the blue-contraction branch (see CPU ref file header for the full\n// decoder behaviour).\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\n// QUANT_4 weight unquantisation: q \u2208 [0,3] \u2192 unq \u2208 [0, 21, 43, 64].\n// Switch keeps us off a module-scope const array (some backends reject\n// those inside function-call bodies).\nfn weight_unq(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 21u; }\n case 2u: { return 43u; }\n default: { return 64u; } // case 3u\n }\n}\n\n// Hardware-exact integer interpolation. Identical to BC7's; matches the\n// CPU reference bit-for-bit.\nfn interp8(e0: u32, e1: u32, w: u32) -> u32 {\n return ((64u - w) * e0 + w * e1 + 32u) >> 6u;\n}\n\n// Normalised [0, 1] \u2192 clamped 8-bit. Same rounding rule (floor(v + 0.5))\n// as the CPU reference's Math.round.\nfn to8(v: f32) -> u32 {\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// 4-channel L2 distance squared in u32 domain. Bounded by 4 \xB7 255\xB2 = 260,100.\nfn pixel_dist_sq(a: vec4<u32>, b: vec4<u32>) -> u32 {\n let d = vec4<i32>(a) - vec4<i32>(b);\n let d2 = d * d;\n return u32(d2.x + d2.y + d2.z + d2.w);\n}\n\n// -------------------------- Farthest-pair seed -------------------------- //\n\nstruct PairResult { i0: u32, i1: u32 };\n\n// O(N\xB2) = 120 comparisons. Same rationale as BC7's `farthest_pair`:\n// bounding-box corners aren't safe initial endpoints when channels vary\n// in different directions along the data line.\nfn farthest_pair(pixels: ptr<function, array<vec4<u32>, 16>>) -> PairResult {\n var best_d: u32 = 0u;\n var best_i: u32 = 0u;\n var best_j: u32 = 1u;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = pixel_dist_sq((*pixels)[i], (*pixels)[j]);\n if (d > best_d) { best_d = d; best_i = i; best_j = j; }\n }\n }\n return PairResult(best_i, best_j);\n}\n\n// -------------------------- Palette + assignment ------------------------ //\n\n// Build the 4-entry RGBA palette from 8-bit endpoints. Uses the same\n// integer interpolation formula as decode, so assignments made against\n// this palette match the hardware round-trip.\nfn build_palette(\n e0: vec4<u32>, e1: vec4<u32>,\n pal: ptr<function, array<vec4<u32>, 4>>,\n) {\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n let w = weight_unq(i);\n (*pal)[i] = vec4<u32>(\n interp8(e0.x, e1.x, w),\n interp8(e0.y, e1.y, w),\n interp8(e0.z, e1.z, w),\n interp8(e0.w, e1.w, w),\n );\n }\n}\n\n// Nearest palette entry for a single RGBA pixel. Full 4-way L2 search.\n// Returns (best_index, its squared error).\nfn nearest_index(pixel: vec4<u32>, pal: ptr<function, array<vec4<u32>, 4>>) -> vec2<u32> {\n var best_i: u32 = 0u;\n var best_d: u32 = 0xFFFFFFFFu;\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n let d = pixel_dist_sq(pixel, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n return vec2<u32>(best_i, best_d);\n}\n\n// Assign all 16 texels to nearest palette entries; accumulate squared error.\nstruct AssignResult { indices: array<u32, 16>, err: u32 };\n\nfn assign_all(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n pal: ptr<function, array<vec4<u32>, 4>>,\n) -> AssignResult {\n var out: AssignResult;\n out.err = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let sel = nearest_index((*pixels)[k], pal);\n out.indices[k] = sel.x;\n out.err = out.err + sel.y;\n }\n return out;\n}\n\n// ---------------------- Least-squares endpoint refit -------------------- //\n\n// Given current indices, solve the per-channel 2\xD72 normal equations for\n// (e0, e1). See the CPU reference's `refitEndpoints` for the derivation.\n// `valid = false` signals a degenerate system (all texels on one palette\n// entry) and the caller keeps the farthest-pair seed.\nstruct RefitResult { e0: vec4<u32>, e1: vec4<u32>, valid: bool };\n\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let unq = weight_unq((*indices)[k]);\n let a = f32(64u - unq) / 64.0;\n let b = f32(unq) / 64.0;\n let v = vec4<f32>((*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 let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<u32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<u32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\n// Write `n_bits` LSBs of `value` at bit position `pos` of a 128-bit field\n// represented as `array<u32, 4>`. Handles word-boundary straddles.\n// Lifted from the BC7 shader verbatim; the layout contract is identical.\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry ---------------------------------- //\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 // 1. Load 16 RGBA texels in 8-bit integer domain.\n var pixels: array<vec4<u32>, 16>;\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 input sizes.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0);\n pixels[i] = vec4<u32>(to8(c.r), to8(c.g), to8(c.b), to8(c.a));\n }\n\n // 2. Farthest-pair seed \u2192 initial endpoints.\n let fp = farthest_pair(&pixels);\n var e0 = pixels[fp.i0];\n var e1 = pixels[fp.i1];\n\n // 3. Initial assignment against the seed endpoints.\n var pal: array<vec4<u32>, 4>;\n build_palette(e0, e1, &pal);\n var best = assign_all(&pixels, &pal);\n\n // 4. One LSQ refit pass. Accept only if the squared error strictly\n // decreases \u2014 matches the CPU reference.\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n var pal2: array<vec4<u32>, 4>;\n build_palette(refit.e0, refit.e1, &pal2);\n let cand = assign_all(&pixels, &pal2);\n if (cand.err < best.err) {\n e0 = refit.e0;\n e1 = refit.e1;\n best = cand;\n }\n }\n\n // 5. Endpoint ordering so the decoder doesn't apply blue contraction.\n // Strict '>' avoids a gratuitous swap on ties.\n let s0 = e0.x + e0.y + e0.z;\n let s1 = e1.x + e1.y + e1.z;\n if (s0 > s1) {\n let tmp = e0; e0 = e1; e1 = tmp;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n // w' = 3 \u2212 w reflects the palette; decoded colour unchanged.\n best.indices[k] = 3u - best.indices[k];\n }\n }\n\n // 6. Pack 128 bits.\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n\n // Config header.\n write_bits(&block, 0u, 11u, 0x042u); // block mode: 4\xD74 grid, QUANT_4 weights\n write_bits(&block, 11u, 2u, 0u); // partition count \u2212 1\n write_bits(&block, 13u, 4u, 12u); // CEM 12: LDR RGBA direct\n\n // Endpoints in the CEM 12 value order: R0 R1 G0 G1 B0 B1 A0 A1.\n write_bits(&block, 17u + 0u * 8u, 8u, e0.x);\n write_bits(&block, 17u + 1u * 8u, 8u, e1.x);\n write_bits(&block, 17u + 2u * 8u, 8u, e0.y);\n write_bits(&block, 17u + 3u * 8u, 8u, e1.y);\n write_bits(&block, 17u + 4u * 8u, 8u, e0.z);\n write_bits(&block, 17u + 5u * 8u, 8u, e1.z);\n write_bits(&block, 17u + 6u * 8u, 8u, e0.w);\n write_bits(&block, 17u + 7u * 8u, 8u, e1.w);\n\n // Weights at the top of the block. Two 1-bit writes per weight keeps\n // the LSB-at-127 convention visible at every call site; the cost over\n // a batched write is negligible next to the full encode.\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = best.indices[k] & 0x3u;\n write_bits(&block, 127u - 2u * k, 1u, w & 1u);\n write_bits(&block, 126u - 2u * k, 1u, (w >> 1u) & 1u);\n }\n\n // 7. Store as 4 u32s.\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
553
+ var astc4x4_default = "// ASTC 4\xD74 LDR compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): O(N) bounding-box seed \u2192 endpoints fitted by a single\n// least-squares pass whose sums are accumulated during a projection-based\n// weight assignment (the 4 palette entries are colinear, so the nearest is\n// found by projecting onto the endpoint line \u2014 no per-entry search).\n// Profiled ~4\xD7 faster than `high` for ~0.36 dB PSNR.\n// high (1): O(N\xB2) farthest-pair seed, full 4-entry nearest search, one LSQ\n// refit \u2014 byte-for-byte identical to astc4x4_ref.ts.\n// The fast branch is selected at pipeline-compile time; the driver eliminates\n// the unused (high) code.\n//\n// RESTRICTED SUBSET: single partition, no dual-plane, CEM 12 (LDR RGBA direct),\n// 4\xD74 weight grid with 2-bit weights (QUANT_4), 8-bit endpoints (QUANT_256).\n//\n// BLOCK LAYOUT (128 bits, LSB-first)\n// bits [10:0] block mode = 0x042\n// bits [12:11] partition count \u2212 1 = 0\n// bits [16:13] CEM = 12\n// bits [80:17] endpoints: R0 R1 G0 G1 B0 B1 A0 A1 (8-bit each)\n// bits [127:96] 16 \xD7 2-bit weights; weight k: bit(127\u22122k)=lsb, bit(126\u22122k)=msb\n//\n// ENDPOINT ORDERING: if sum(e0.rgb) > sum(e1.rgb) swap endpoints and reflect\n// indices (w' = 3 \u2212 w) to keep the decoder out of blue contraction.\n\n// 0 = fast (default), 1 = exhaustive/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 weight_unq(i: u32) -> i32 {\n switch i {\n case 0u: { return 0; }\n case 1u: { return 21; }\n case 2u: { return 43; }\n default: { return 64; } // case 3u\n }\n}\n\nfn interp4(e0: vec4<i32>, e1: vec4<i32>, w: i32) -> vec4<i32> {\n return ((64 - w) * e0 + w * e1 + vec4<i32>(32)) >> vec4<u32>(6u);\n}\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\nfn dist2(a: vec4<i32>, b: vec4<i32>) -> i32 {\n let d = a - b;\n let e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// ============================ FAST PATH ================================ //\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.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_assign(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n e0: vec4<i32>, e1: vec4<i32>,\n out_idx: ptr<function, array<u32, 16>>,\n fit: bool,\n) -> Fit {\n var out: Fit;\n let dir = e1 - e0;\n let dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (var k: u32 = 0u; k < 16u; k = k + 1u) { (*out_idx)[k] = 0u; }\n out.valid = false;\n return out;\n }\n let inv = 3.0 / f32(dd);\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0); var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let q = (*pixels)[k] - e0;\n let s = clamp(floor(f32(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv + 0.5), 0.0, 3.0);\n (*out_idx)[k] = u32(s);\n if (fit) {\n let v = vec4<f32>((*pixels)[k]);\n let b = s / 3.0; let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b; sAV = sAV + a * v; sBV = sBV + b * v;\n }\n }\n if (!fit) { out.valid = false; return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { out.valid = false; return out; }\n out.e0 = vec4<i32>(clamp(round((sBB * sAV - sAB * sBV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round((sAA * sBV - sAB * sAV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ============================ HIGH PATH ================================ //\n\nstruct Pair { a: vec4<i32>, b: vec4<i32> };\nfn farthest_pair(pixels: ptr<function, array<vec4<i32>, 16>>) -> Pair {\n var best_d: i32 = 0;\n var pa = (*pixels)[0];\n var pb = (*pixels)[1];\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let xi = (*pixels)[i];\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = dist2(xi, (*pixels)[j]);\n if (d > best_d) { best_d = d; pa = xi; pb = (*pixels)[j]; }\n }\n }\n return Pair(pa, pb);\n}\n\nfn build_palette(e0: vec4<i32>, e1: vec4<i32>, pal: ptr<function, array<vec4<i32>, 4>>) {\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n (*pal)[i] = interp4(e0, e1, weight_unq(i));\n }\n}\n\nfn assign_all(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n pal: ptr<function, array<vec4<i32>, 4>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> i32 {\n var err: i32 = 0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let px = (*pixels)[k];\n var best_i: u32 = 0u;\n var best_d: i32 = 2147483647;\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n let d = dist2(px, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n (*out_idx)[k] = best_i;\n err = err + best_d;\n }\n return err;\n}\n\nstruct RefitResult { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let unq = weight_unq((*indices)[k]);\n let a = f32(64 - unq) / 64.0;\n let b = f32(unq) / 64.0;\n let v = vec4<f32>((*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 let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<i32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry ---------------------------------- //\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<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0, 0), max_xy);\n let px = to8(textureLoad(src_tex, p, 0));\n pixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n var e0: vec4<i32>;\n var e1: vec4<i32>;\n var indices: array<u32, 16>;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n e0 = fp.a;\n e1 = fp.b;\n var pal: array<vec4<i32>, 4>;\n build_palette(e0, e1, &pal);\n var err = assign_all(&pixels, &pal, &indices);\n let refit = refit_endpoints(&pixels, &indices);\n if (refit.valid) {\n build_palette(refit.e0, refit.e1, &pal);\n var idx2: array<u32, 16>;\n let err2 = assign_all(&pixels, &pal, &idx2);\n if (err2 < err) {\n e0 = refit.e0;\n e1 = refit.e1;\n indices = idx2;\n err = err2;\n }\n }\n } else {\n e0 = lo;\n e1 = hi;\n let r = proj_assign(&pixels, e0, e1, &indices, true);\n if (r.valid) {\n e0 = r.e0;\n e1 = r.e1;\n proj_assign(&pixels, e0, e1, &indices, false);\n }\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 let tmp = e0; e0 = e1; e1 = tmp;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices[k] = 3u - indices[k];\n }\n }\n\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n write_bits(&block, 0u, 11u, 0x042u);\n write_bits(&block, 11u, 2u, 0u);\n write_bits(&block, 13u, 4u, 12u);\n write_bits(&block, 17u + 0u * 8u, 8u, u32(e0.x));\n write_bits(&block, 17u + 1u * 8u, 8u, u32(e1.x));\n write_bits(&block, 17u + 2u * 8u, 8u, u32(e0.y));\n write_bits(&block, 17u + 3u * 8u, 8u, u32(e1.y));\n write_bits(&block, 17u + 4u * 8u, 8u, u32(e0.z));\n write_bits(&block, 17u + 5u * 8u, 8u, u32(e1.z));\n write_bits(&block, 17u + 6u * 8u, 8u, u32(e0.w));\n write_bits(&block, 17u + 7u * 8u, 8u, u32(e1.w));\n var w3: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = indices[k] & 0x3u;\n w3 = w3 | ((w & 1u) << (31u - 2u * k)) | (((w >> 1u) & 1u) << (30u - 2u * k));\n }\n block[3] = w3;\n\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
554
+
555
+ // src/astc4x4_fast_f16.wgsl
556
+ var astc4x4_fast_f16_default = '// astc4x4 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).\n// Identical algorithm to the f32 fast path in astc4x4.wgsl, but the projection +\n// least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput\n// (e.g. Apple) this is ~2x faster at the same quality; endpoints are still\n// quantised to exact 8-bit. The host selects this module only when the device\n// reports shader-f16, falling back to astc4x4.wgsl otherwise. "high" never uses this.\n//\nenable f16;\nalias h = f16; alias h4 = vec4<f16>;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };\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;\nstruct Fit { e0: h4, e1: h4, valid: bool };\nfn proj(pix: ptr<function, array<h4,16>>, e0: h4, e1: h4, out_idx: ptr<function, array<u32,16>>, fit: bool) -> Fit {\n var out: Fit; let dir=e1-e0; let dd=dot(dir,dir);\n if(dd==h(0.0)){ for(var k:u32=0u;k<16u;k=k+1u){(*out_idx)[k]=0u;} out.valid=false; return out; }\n let inv=h(3.0)/dd;\n var sAA=h(0.0); var sBB=h(0.0); var sAB=h(0.0); var sAV=h4(0.0); var sBV=h4(0.0);\n for(var k:u32=0u;k<16u;k=k+1u){ let v=(*pix)[k]; let s=clamp(floor(dot(v-e0,dir)*inv+h(0.5)),h(0.0),h(3.0)); (*out_idx)[k]=u32(s);\n if(fit){ let b=s*h(1.0/3.0); let a=h(1.0)-b; sAA=sAA+a*a; sBB=sBB+b*b; sAB=sAB+a*b; sAV=sAV+a*v; sBV=sBV+b*v; } }\n if(!fit){ out.valid=false; return out; }\n let det=sAA*sBB-sAB*sAB; if(abs(det)<h(0.0001)){out.valid=false;return out;}\n out.e0=clamp((sBB*sAV-sAB*sBV)/det,h4(0.0),h4(1.0)); out.e1=clamp((sAA*sBV-sAB*sAV)/det,h4(0.0),h4(1.0)); out.valid=true; return out;\n}\nfn write_bits(block: ptr<function, array<u32,4>>, pos: u32, n_bits: u32, value: u32) {\n let v=value&((1u<<n_bits)-1u); let wl=pos/32u; let bl=pos%32u; let il=min(n_bits,32u-bl);\n let ml=((1u<<il)-1u)<<bl; (*block)[wl]=((*block)[wl]&~ml)|((v<<bl)&ml);\n if(il<n_bits){ let ih=n_bits-il; let mh=(1u<<ih)-1u; (*block)[wl+1u]=((*block)[wl+1u]&~mh)|((v>>il)&mh); }\n}\nfn q8(e: h4) -> vec4<i32> { return vec4<i32>(clamp(floor(e*h(255.0)+h(0.5)), h4(0.0), h4(255.0))); }\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){return;}\n let bi=gid.y*params.blocks_x+gid.x;\n let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);\n var pix: array<h4,16>; var lo=h4(1.0); var hi=h4(0.0);\n for(var i:u32=0u;i<16u;i=i+1u){ let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx); let px=h4(textureLoad(src_tex,p,0)); pix[i]=px; lo=min(lo,px); hi=max(hi,px); }\n var e0=lo; var e1=hi; var indices: array<u32,16>;\n let r=proj(&pix,e0,e1,&indices,true);\n if(r.valid){ e0=r.e0; e1=r.e1; proj(&pix,e0,e1,&indices,false); }\n var E0=q8(e0); var E1=q8(e1);\n if(E0.x+E0.y+E0.z > E1.x+E1.y+E1.z){ let t=E0; E0=E1; E1=t; for(var k:u32=0u;k<16u;k=k+1u){indices[k]=3u-indices[k];} }\n var block: array<u32,4>; block[0]=0u;block[1]=0u;block[2]=0u;block[3]=0u;\n write_bits(&block,0u,11u,0x042u); write_bits(&block,11u,2u,0u); write_bits(&block,13u,4u,12u);\n write_bits(&block,17u,8u,u32(E0.x)); write_bits(&block,25u,8u,u32(E1.x));\n write_bits(&block,33u,8u,u32(E0.y)); write_bits(&block,41u,8u,u32(E1.y));\n write_bits(&block,49u,8u,u32(E0.z)); write_bits(&block,57u,8u,u32(E1.z));\n write_bits(&block,65u,8u,u32(E0.w)); write_bits(&block,73u,8u,u32(E1.w));\n var w3:u32=0u; for(var k:u32=0u;k<16u;k=k+1u){ let w=indices[k]&3u; w3=w3|((w&1u)<<(31u-2u*k))|(((w>>1u)&1u)<<(30u-2u*k)); } block[3]=w3;\n let o=bi*4u; dst[o]=block[0];dst[o+1u]=block[1];dst[o+2u]=block[2];dst[o+3u]=block[3];\n}\n';
366
557
 
367
558
  // src/ASTC4x4Encoder.ts
368
559
  var ASTC4x4Encoder = class extends Encoder {
@@ -377,9 +568,15 @@ var ASTC4x4Encoder = class extends Encoder {
377
568
  get supportsSrgb() {
378
569
  return true;
379
570
  }
571
+ get supportsQuality() {
572
+ return true;
573
+ }
380
574
  wgslSource() {
381
575
  return astc4x4_default;
382
576
  }
577
+ wgslSourceFastF16() {
578
+ return astc4x4_fast_f16_default;
579
+ }
383
580
  gpuTextureFormat({ colorSpace }) {
384
581
  return colorSpace === "srgb" ? "astc-4x4-unorm-srgb" : "astc-4x4-unorm";
385
582
  }
@@ -540,6 +737,7 @@ async function compressTexture(source, options = {}) {
540
737
  colorSpace = "srgb",
541
738
  flipY = true,
542
739
  mipmaps = false,
740
+ quality = "fast",
543
741
  device: providedDevice,
544
742
  adapter: providedAdapter
545
743
  } = options;
@@ -608,8 +806,16 @@ async function compressTexture(source, options = {}) {
608
806
  encoder = await selection.encoderClass.create();
609
807
  }
610
808
  try {
809
+ const needsWriteTexture = needsWriteTextureWorkaround(adapter);
611
810
  if (!mipmaps) {
612
- const bytes = await encoder.encodeToBytes(bitmap, { flipY });
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 });
818
+ }
613
819
  const tex2 = encoder.buildMippedTexture([bytes], { colorSpace });
614
820
  return {
615
821
  texture: tex2,
@@ -633,7 +839,7 @@ async function compressTexture(source, options = {}) {
633
839
  for (const level of chain) {
634
840
  const padded = padToBlockMultiple(level);
635
841
  const imageData = mipLevelToImageData(padded);
636
- const bytes = await encoder.encodeToBytes(imageData);
842
+ const bytes = await encoder.encodeToBytes(imageData, { quality });
637
843
  encodedLevels.push(bytes);
638
844
  totalEncodeMs += bytes.encodeMs;
639
845
  }
@@ -669,6 +875,8 @@ var GputexLoader = class extends Loader {
669
875
  flipY = true;
670
876
  /** Generate + encode a full mip chain. Default false. */
671
877
  mipmaps = false;
878
+ /** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
879
+ quality = "fast";
672
880
  /**
673
881
  * Optional pre-existing WebGPU device. Reusing the renderer's device
674
882
  * avoids spinning up a second WebGPU context for encoding.
@@ -695,6 +903,7 @@ var GputexLoader = class extends Loader {
695
903
  colorSpace: this.colorSpace,
696
904
  flipY: this.flipY,
697
905
  mipmaps: this.mipmaps,
906
+ quality: this.quality,
698
907
  device: this.device,
699
908
  adapter: this.adapter
700
909
  }).then(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gputex",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"
@@ -23,7 +23,7 @@
23
23
  "@webgpu/types": "^0.1.69",
24
24
  "three": "^0.183.2",
25
25
  "tsup": "8.5.1",
26
- "typescript": "5.9.3"
26
+ "typescript": "6.0.3"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "three": ">=0.170"