gputex 0.0.5 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -4
- package/dist/index.d.ts +226 -5
- package/dist/index.js +745 -143
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -42,14 +42,39 @@ function detectCapabilities(adapter) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/Encoder.ts
|
|
45
|
+
import { CompressedTexture as CompressedTexture2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, RepeatWrapping as RepeatWrapping2 } from "three";
|
|
46
|
+
|
|
47
|
+
// src/textureAssembly.ts
|
|
45
48
|
import {
|
|
46
49
|
CompressedTexture,
|
|
47
50
|
LinearFilter,
|
|
48
51
|
LinearMipmapLinearFilter,
|
|
49
52
|
LinearSRGBColorSpace,
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
RepeatWrapping,
|
|
54
|
+
SRGBColorSpace
|
|
52
55
|
} from "three";
|
|
56
|
+
function assembleCompressedTexture(levels, threeFormat, effectiveSrgb) {
|
|
57
|
+
if (levels.length === 0) {
|
|
58
|
+
throw new Error("assembleCompressedTexture: no levels provided");
|
|
59
|
+
}
|
|
60
|
+
const mipmaps = levels.map((l) => ({
|
|
61
|
+
data: l.data,
|
|
62
|
+
width: l.paddedWidth,
|
|
63
|
+
height: l.paddedHeight
|
|
64
|
+
}));
|
|
65
|
+
const base = levels[0];
|
|
66
|
+
const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
|
|
67
|
+
texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
68
|
+
texture.magFilter = LinearFilter;
|
|
69
|
+
texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
|
|
70
|
+
texture.generateMipmaps = false;
|
|
71
|
+
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
72
|
+
texture.needsUpdate = true;
|
|
73
|
+
texture.userData.logicalWidth = base.width;
|
|
74
|
+
texture.userData.logicalHeight = base.height;
|
|
75
|
+
texture.userData.mipLevels = levels.length;
|
|
76
|
+
return texture;
|
|
77
|
+
}
|
|
53
78
|
|
|
54
79
|
// src/workarounds.ts
|
|
55
80
|
function needsWriteTextureWorkaround(adapter) {
|
|
@@ -96,6 +121,9 @@ var Encoder = class {
|
|
|
96
121
|
if (this.requiredFeature && adapter.features.has(this.requiredFeature)) {
|
|
97
122
|
requiredFeatures.push(this.requiredFeature);
|
|
98
123
|
}
|
|
124
|
+
if (adapter.features.has("shader-f16")) {
|
|
125
|
+
requiredFeatures.push("shader-f16");
|
|
126
|
+
}
|
|
99
127
|
const device = await adapter.requestDevice({ requiredFeatures });
|
|
100
128
|
return new this({ device, adapter, ownsDevice: true });
|
|
101
129
|
}
|
|
@@ -105,7 +133,14 @@ var Encoder = class {
|
|
|
105
133
|
// `!:` because these are set in `_buildPipeline()` which the constructor
|
|
106
134
|
// calls; TypeScript's flow analysis doesn't see through method calls.
|
|
107
135
|
_module;
|
|
136
|
+
// f16 'fast' module — built only when the device supports shader-f16 and the
|
|
137
|
+
// subclass provides an f16 source. null otherwise (falls back to _module).
|
|
138
|
+
_moduleF16 = null;
|
|
139
|
+
_pipelineF16 = null;
|
|
140
|
+
// Default pipeline (fast). Kept as a field for back-compat; the per-quality
|
|
141
|
+
// cache below holds the specialised pipelines for encoders that support it.
|
|
108
142
|
_pipeline;
|
|
143
|
+
_pipelineCache = /* @__PURE__ */ new Map();
|
|
109
144
|
constructor({ device, adapter, ownsDevice = false }) {
|
|
110
145
|
this.device = device;
|
|
111
146
|
this.adapter = adapter;
|
|
@@ -119,11 +154,52 @@ var Encoder = class {
|
|
|
119
154
|
label: `${this.label}-encoder`,
|
|
120
155
|
code
|
|
121
156
|
});
|
|
122
|
-
this.
|
|
123
|
-
|
|
157
|
+
if (this._useF16) {
|
|
158
|
+
this._moduleF16 = device.createShaderModule({
|
|
159
|
+
label: `${this.label}-encoder-f16`,
|
|
160
|
+
code: this.wgslSourceFastF16()
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (this.supportsQuality) {
|
|
164
|
+
this._pipeline = this._getPipeline("fast");
|
|
165
|
+
} else {
|
|
166
|
+
this._pipeline = device.createComputePipeline({
|
|
167
|
+
label: `${this.label}-encoder-pipeline`,
|
|
168
|
+
layout: "auto",
|
|
169
|
+
compute: { module: this._module, entryPoint: "encode" }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Pipeline for a given quality level. Encoders that don't declare a
|
|
175
|
+
* `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
|
|
176
|
+
* argument and reuse the single pipeline. Specialised pipelines are cached.
|
|
177
|
+
*/
|
|
178
|
+
_getPipeline(quality) {
|
|
179
|
+
if (!this.supportsQuality) return this._pipeline;
|
|
180
|
+
if (quality === "fast" && this._moduleF16) {
|
|
181
|
+
if (!this._pipelineF16) {
|
|
182
|
+
this._pipelineF16 = this.device.createComputePipeline({
|
|
183
|
+
label: `${this.label}-encoder-pipeline-fast-f16`,
|
|
184
|
+
layout: "auto",
|
|
185
|
+
compute: { module: this._moduleF16, entryPoint: "encode" }
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return this._pipelineF16;
|
|
189
|
+
}
|
|
190
|
+
const cached = this._pipelineCache.get(quality);
|
|
191
|
+
if (cached) return cached;
|
|
192
|
+
const pipeline = this.device.createComputePipeline({
|
|
193
|
+
label: `${this.label}-encoder-pipeline-${quality}`,
|
|
124
194
|
layout: "auto",
|
|
125
|
-
compute: {
|
|
195
|
+
compute: {
|
|
196
|
+
module: this._module,
|
|
197
|
+
entryPoint: "encode",
|
|
198
|
+
constants: { QUALITY_HIGH: quality === "high" ? 1 : 0 }
|
|
199
|
+
}
|
|
126
200
|
});
|
|
201
|
+
this._pipelineCache.set(quality, pipeline);
|
|
202
|
+
return pipeline;
|
|
127
203
|
}
|
|
128
204
|
destroy() {
|
|
129
205
|
if (this.ownsDevice) this.device.destroy();
|
|
@@ -136,6 +212,26 @@ var Encoder = class {
|
|
|
136
212
|
get supportsSrgb() {
|
|
137
213
|
return true;
|
|
138
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
|
|
217
|
+
* (i.e. has distinct fast/high search paths). BC1 is already single-pass and
|
|
218
|
+
* leaves this false; BC5/BC7/ASTC override it to true.
|
|
219
|
+
*/
|
|
220
|
+
get supportsQuality() {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Optional f16 WGSL for the 'fast' path. Used only when the device reports the
|
|
225
|
+
* `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
|
|
226
|
+
* `'high'` always uses it. Returns null when there's no f16 variant (BC1).
|
|
227
|
+
*/
|
|
228
|
+
wgslSourceFastF16() {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
/** Whether the f16 fast path is both available and supported on this device. */
|
|
232
|
+
get _useF16() {
|
|
233
|
+
return this.wgslSourceFastF16() !== null && this.device.features.has("shader-f16");
|
|
234
|
+
}
|
|
139
235
|
/**
|
|
140
236
|
* True if the device reports the feature the output texture needs.
|
|
141
237
|
* The encoder itself only writes to a storage buffer, so this is about
|
|
@@ -148,21 +244,21 @@ var Encoder = class {
|
|
|
148
244
|
// ------------------------------------------------------------------ //
|
|
149
245
|
// Shared encode() — pad, upload, dispatch, readback, wrap. //
|
|
150
246
|
// ------------------------------------------------------------------ //
|
|
151
|
-
async encode(source, { colorSpace = "srgb" } = {}) {
|
|
247
|
+
async encode(source, { colorSpace = "srgb", quality = "fast" } = {}) {
|
|
152
248
|
const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
|
|
153
|
-
const bytes = await this.encodeToBytes(source);
|
|
249
|
+
const bytes = await this.encodeToBytes(source, { quality });
|
|
154
250
|
const threeFormat = this.threeTextureFormat({ colorSpace: effectiveSrgb ? "srgb" : "linear" });
|
|
155
251
|
const mip = {
|
|
156
252
|
data: bytes.data,
|
|
157
253
|
width: bytes.paddedWidth,
|
|
158
254
|
height: bytes.paddedHeight
|
|
159
255
|
};
|
|
160
|
-
const texture = new
|
|
161
|
-
texture.colorSpace = effectiveSrgb ?
|
|
162
|
-
texture.magFilter =
|
|
163
|
-
texture.minFilter =
|
|
256
|
+
const texture = new CompressedTexture2([mip], bytes.paddedWidth, bytes.paddedHeight, threeFormat);
|
|
257
|
+
texture.colorSpace = effectiveSrgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
|
|
258
|
+
texture.magFilter = LinearFilter2;
|
|
259
|
+
texture.minFilter = LinearFilter2;
|
|
164
260
|
texture.generateMipmaps = false;
|
|
165
|
-
texture.wrapS = texture.wrapT =
|
|
261
|
+
texture.wrapS = texture.wrapT = RepeatWrapping2;
|
|
166
262
|
texture.needsUpdate = true;
|
|
167
263
|
texture.userData.logicalWidth = bytes.width;
|
|
168
264
|
texture.userData.logicalHeight = bytes.height;
|
|
@@ -186,7 +282,7 @@ var Encoder = class {
|
|
|
186
282
|
* encoder boundary. Still safe to call from outside — it just does
|
|
187
283
|
* less work than `encode()` and the caller assembles the texture.
|
|
188
284
|
*/
|
|
189
|
-
async encodeToBytes(source, { flipY = false } = {}) {
|
|
285
|
+
async encodeToBytes(source, { flipY = false, quality = "fast" } = {}) {
|
|
190
286
|
const device = this.device;
|
|
191
287
|
const width = source.width;
|
|
192
288
|
const height = source.height;
|
|
@@ -219,9 +315,10 @@ var Encoder = class {
|
|
|
219
315
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
220
316
|
});
|
|
221
317
|
device.queue.writeBuffer(paramsBuffer, 0, new Uint32Array([blocksX, blocksY, paddedWidth, paddedHeight]));
|
|
318
|
+
const pipeline = this._getPipeline(quality);
|
|
222
319
|
const bindGroup = device.createBindGroup({
|
|
223
320
|
label: `${this.label}-bg`,
|
|
224
|
-
layout:
|
|
321
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
225
322
|
entries: [
|
|
226
323
|
{ binding: 0, resource: srcTex.createView() },
|
|
227
324
|
{ binding: 1, resource: { buffer: dstBuffer } },
|
|
@@ -232,7 +329,7 @@ var Encoder = class {
|
|
|
232
329
|
const t0 = performance.now();
|
|
233
330
|
const enc = device.createCommandEncoder({ label: `${this.label}-encode` });
|
|
234
331
|
const pass = enc.beginComputePass();
|
|
235
|
-
pass.setPipeline(
|
|
332
|
+
pass.setPipeline(pipeline);
|
|
236
333
|
pass.setBindGroup(0, bindGroup);
|
|
237
334
|
pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(blocksY / wgY), 1);
|
|
238
335
|
pass.end();
|
|
@@ -269,23 +366,7 @@ var Encoder = class {
|
|
|
269
366
|
}
|
|
270
367
|
const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
|
|
271
368
|
const threeFormat = this.threeTextureFormat({ colorSpace: effectiveSrgb ? "srgb" : "linear" });
|
|
272
|
-
|
|
273
|
-
data: l.data,
|
|
274
|
-
width: l.paddedWidth,
|
|
275
|
-
height: l.paddedHeight
|
|
276
|
-
}));
|
|
277
|
-
const base = levels[0];
|
|
278
|
-
const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
|
|
279
|
-
texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
280
|
-
texture.magFilter = LinearFilter;
|
|
281
|
-
texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
|
|
282
|
-
texture.generateMipmaps = false;
|
|
283
|
-
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
284
|
-
texture.needsUpdate = true;
|
|
285
|
-
texture.userData.logicalWidth = base.width;
|
|
286
|
-
texture.userData.logicalHeight = base.height;
|
|
287
|
-
texture.userData.mipLevels = levels.length;
|
|
288
|
-
return texture;
|
|
369
|
+
return assembleCompressedTexture(levels, threeFormat, effectiveSrgb);
|
|
289
370
|
}
|
|
290
371
|
};
|
|
291
372
|
|
|
@@ -323,7 +404,10 @@ var BC1Encoder = class extends Encoder {
|
|
|
323
404
|
import { RED_GREEN_RGTC2_Format } from "three";
|
|
324
405
|
|
|
325
406
|
// src/bc5.wgsl
|
|
326
|
-
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//
|
|
407
|
+
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";
|
|
408
|
+
|
|
409
|
+
// src/bc5_fast_f16.wgsl
|
|
410
|
+
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';
|
|
327
411
|
|
|
328
412
|
// src/BC5Encoder.ts
|
|
329
413
|
var BC5Encoder = class extends Encoder {
|
|
@@ -338,9 +422,15 @@ var BC5Encoder = class extends Encoder {
|
|
|
338
422
|
get supportsSrgb() {
|
|
339
423
|
return false;
|
|
340
424
|
}
|
|
425
|
+
get supportsQuality() {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
341
428
|
wgslSource() {
|
|
342
429
|
return bc5_default;
|
|
343
430
|
}
|
|
431
|
+
wgslSourceFastF16() {
|
|
432
|
+
return bc5_fast_f16_default;
|
|
433
|
+
}
|
|
344
434
|
gpuTextureFormat() {
|
|
345
435
|
return "bc5-rg-unorm";
|
|
346
436
|
}
|
|
@@ -353,7 +443,87 @@ var BC5Encoder = class extends Encoder {
|
|
|
353
443
|
import { RGBA_BPTC_Format } from "three";
|
|
354
444
|
|
|
355
445
|
// src/bc7.wgsl
|
|
356
|
-
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";
|
|
446
|
+
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";
|
|
447
|
+
|
|
448
|
+
// src/bc7_fast_f16.wgsl
|
|
449
|
+
var bc7_fast_f16_default = `// bc7 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
450
|
+
// Identical algorithm to the f32 fast path in bc7.wgsl, but the projection +
|
|
451
|
+
// least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput
|
|
452
|
+
// (e.g. Apple) this is ~2x faster at the same quality; endpoints are still
|
|
453
|
+
// quantised to exact 8-bit. The host selects this module only when the device
|
|
454
|
+
// reports shader-f16, falling back to bc7.wgsl otherwise. "high" never uses this.
|
|
455
|
+
//
|
|
456
|
+
// BC7 mode 6 fast path in f16 (Apple GPUs run f16 at 2x). All math in the [0,1]
|
|
457
|
+
// domain so dot products stay well under f16's range; endpoints quantised to
|
|
458
|
+
// 8-bit at the end. Same bbox seed + projection + fused LSQ refit + reproject.
|
|
459
|
+
enable f16;
|
|
460
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
461
|
+
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
462
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
463
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
464
|
+
alias h = f16;
|
|
465
|
+
alias h4 = vec4<f16>;
|
|
466
|
+
struct Ep { seven: vec4<i32>, eight: h4, p: u32 };
|
|
467
|
+
// quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit; returns 8-bit eff in [0,1].
|
|
468
|
+
fn pick_ep(ideal01: h4) -> Ep {
|
|
469
|
+
let ideal = ideal01 * h(255.0);
|
|
470
|
+
let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0
|
|
471
|
+
let e0 = q0 * h(2.0);
|
|
472
|
+
let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1
|
|
473
|
+
let e1 = q1 * h(2.0) + h(1.0);
|
|
474
|
+
let d0 = e0 - ideal; let d1 = e1 - ideal;
|
|
475
|
+
if (dot(d1,d1) < dot(d0,d0)) { return Ep(vec4<i32>(q1), e1 * h(1.0/255.0), 1u); }
|
|
476
|
+
return Ep(vec4<i32>(q0), e0 * h(1.0/255.0), 0u);
|
|
477
|
+
}
|
|
478
|
+
struct Fit { e0: h4, e1: h4, valid: bool };
|
|
479
|
+
fn proj_assign(pix: ptr<function, array<h4,16>>, e0: h4, e1: h4, out_idx: ptr<function, array<u32,16>>, fit: bool) -> Fit {
|
|
480
|
+
var out: Fit; let dir = e1 - e0; let dd = dot(dir,dir);
|
|
481
|
+
if (dd == h(0.0)) { for(var k:u32=0u;k<16u;k=k+1u){(*out_idx)[k]=0u;} out.valid=false; return out; }
|
|
482
|
+
let inv = h(15.0) / dd;
|
|
483
|
+
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);
|
|
484
|
+
for(var k:u32=0u;k<16u;k=k+1u){
|
|
485
|
+
let v=(*pix)[k];
|
|
486
|
+
let s = clamp(floor(dot(v - e0, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
487
|
+
(*out_idx)[k] = u32(s);
|
|
488
|
+
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; }
|
|
489
|
+
}
|
|
490
|
+
if(!fit){ out.valid=false; return out; }
|
|
491
|
+
let det = sAA*sBB - sAB*sAB; if (abs(det) < h(0.0001)) { out.valid=false; return out; }
|
|
492
|
+
out.e0 = clamp((sBB*sAV - sAB*sBV)/det, h4(0.0), h4(1.0));
|
|
493
|
+
out.e1 = clamp((sAA*sBV - sAB*sAV)/det, h4(0.0), h4(1.0));
|
|
494
|
+
out.valid=true; return out;
|
|
495
|
+
}
|
|
496
|
+
fn write_bits(block: ptr<function, array<u32,4>>, pos: u32, n_bits: u32, value: u32) {
|
|
497
|
+
let v=value&((1u<<n_bits)-1u); let wl=pos/32u; let bl=pos%32u; let il=min(n_bits,32u-bl);
|
|
498
|
+
let ml=((1u<<il)-1u)<<bl; (*block)[wl]=((*block)[wl]&~ml)|((v<<bl)&ml);
|
|
499
|
+
if(il<n_bits){ let ih=n_bits-il; let mh=(1u<<ih)-1u; (*block)[wl+1u]=((*block)[wl+1u]&~mh)|((v>>il)&mh); }
|
|
500
|
+
}
|
|
501
|
+
@compute @workgroup_size(8,8,1)
|
|
502
|
+
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
503
|
+
if(gid.x>=params.blocks_x||gid.y>=params.blocks_y){return;}
|
|
504
|
+
let bi=gid.y*params.blocks_x+gid.x;
|
|
505
|
+
let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);
|
|
506
|
+
var pix: array<h4,16>; var lo=h4(1.0); var hi=h4(0.0);
|
|
507
|
+
for(var i:u32=0u;i<16u;i=i+1u){
|
|
508
|
+
let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx);
|
|
509
|
+
let px=h4(textureLoad(src_tex,p,0)); pix[i]=px; lo=min(lo,px); hi=max(hi,px);
|
|
510
|
+
}
|
|
511
|
+
var ep0=pick_ep(lo); var ep1=pick_ep(hi); var indices: array<u32,16>;
|
|
512
|
+
let r=proj_assign(&pix,ep0.eight,ep1.eight,&indices,true);
|
|
513
|
+
if(r.valid){ ep0=pick_ep(r.e0); ep1=pick_ep(r.e1); proj_assign(&pix,ep0.eight,ep1.eight,&indices,false); }
|
|
514
|
+
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];} }
|
|
515
|
+
var block: array<u32,4>; block[0]=0u;block[1]=0u;block[2]=0u;block[3]=0u; var pos:u32=0u;
|
|
516
|
+
write_bits(&block,pos,7u,0x40u);pos=pos+7u;
|
|
517
|
+
write_bits(&block,pos,7u,u32(ep0.seven.x));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.x));pos=pos+7u;
|
|
518
|
+
write_bits(&block,pos,7u,u32(ep0.seven.y));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.y));pos=pos+7u;
|
|
519
|
+
write_bits(&block,pos,7u,u32(ep0.seven.z));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.z));pos=pos+7u;
|
|
520
|
+
write_bits(&block,pos,7u,u32(ep0.seven.w));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.w));pos=pos+7u;
|
|
521
|
+
write_bits(&block,pos,1u,ep0.p);pos=pos+1u; write_bits(&block,pos,1u,ep1.p);pos=pos+1u;
|
|
522
|
+
write_bits(&block,pos,3u,indices[0]&0x7u);pos=pos+3u;
|
|
523
|
+
for(var k:u32=1u;k<16u;k=k+1u){write_bits(&block,pos,4u,indices[k]&0xFu);pos=pos+4u;}
|
|
524
|
+
let o=bi*4u; dst[o]=block[0];dst[o+1u]=block[1];dst[o+2u]=block[2];dst[o+3u]=block[3];
|
|
525
|
+
}
|
|
526
|
+
`;
|
|
357
527
|
|
|
358
528
|
// src/BC7Encoder.ts
|
|
359
529
|
var BC7Encoder = class extends Encoder {
|
|
@@ -368,9 +538,15 @@ var BC7Encoder = class extends Encoder {
|
|
|
368
538
|
get supportsSrgb() {
|
|
369
539
|
return true;
|
|
370
540
|
}
|
|
541
|
+
get supportsQuality() {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
371
544
|
wgslSource() {
|
|
372
545
|
return bc7_default;
|
|
373
546
|
}
|
|
547
|
+
wgslSourceFastF16() {
|
|
548
|
+
return bc7_fast_f16_default;
|
|
549
|
+
}
|
|
374
550
|
gpuTextureFormat({ colorSpace }) {
|
|
375
551
|
return colorSpace === "srgb" ? "bc7-rgba-unorm-srgb" : "bc7-rgba-unorm";
|
|
376
552
|
}
|
|
@@ -383,7 +559,10 @@ var BC7Encoder = class extends Encoder {
|
|
|
383
559
|
import { RGBA_ASTC_4x4_Format } from "three";
|
|
384
560
|
|
|
385
561
|
// src/astc4x4.wgsl
|
|
386
|
-
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";
|
|
562
|
+
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";
|
|
563
|
+
|
|
564
|
+
// src/astc4x4_fast_f16.wgsl
|
|
565
|
+
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';
|
|
387
566
|
|
|
388
567
|
// src/ASTC4x4Encoder.ts
|
|
389
568
|
var ASTC4x4Encoder = class extends Encoder {
|
|
@@ -398,9 +577,15 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
398
577
|
get supportsSrgb() {
|
|
399
578
|
return true;
|
|
400
579
|
}
|
|
580
|
+
get supportsQuality() {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
401
583
|
wgslSource() {
|
|
402
584
|
return astc4x4_default;
|
|
403
585
|
}
|
|
586
|
+
wgslSourceFastF16() {
|
|
587
|
+
return astc4x4_fast_f16_default;
|
|
588
|
+
}
|
|
404
589
|
gpuTextureFormat({ colorSpace }) {
|
|
405
590
|
return colorSpace === "srgb" ? "astc-4x4-unorm-srgb" : "astc-4x4-unorm";
|
|
406
591
|
}
|
|
@@ -409,6 +594,373 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
409
594
|
}
|
|
410
595
|
};
|
|
411
596
|
|
|
597
|
+
// src/webgl/glsl/fullscreen.vert.glsl
|
|
598
|
+
var fullscreen_vert_default = "#version 300 es\n// Fullscreen-triangle vertex shader for the WebGL block encoders.\n//\n// Draws a single oversized triangle covering the viewport from gl_VertexID\n// alone \u2014 no vertex buffers / attributes needed (drawArrays(TRIANGLES, 0, 3)).\n// The encoder sets the viewport to (blocks_x \xD7 blocks_y), so each rasterised\n// fragment corresponds to exactly one 4\xD74 output block.\n//\n// id 0 -> (-1,-1) id 1 -> ( 3,-1) id 2 -> (-1, 3)\n\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
|
|
599
|
+
|
|
600
|
+
// src/webgl/webglContext.ts
|
|
601
|
+
var CONTEXT_ATTRS = {
|
|
602
|
+
alpha: false,
|
|
603
|
+
antialias: false,
|
|
604
|
+
depth: false,
|
|
605
|
+
stencil: false,
|
|
606
|
+
premultipliedAlpha: false,
|
|
607
|
+
preserveDrawingBuffer: false,
|
|
608
|
+
// Encoding is GPU-bound; prefer the discrete GPU when the browser exposes a
|
|
609
|
+
// choice. Ignored where unsupported.
|
|
610
|
+
powerPreference: "high-performance"
|
|
611
|
+
};
|
|
612
|
+
function createWebGLContext() {
|
|
613
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
614
|
+
const gl = new OffscreenCanvas(1, 1).getContext("webgl2", CONTEXT_ATTRS);
|
|
615
|
+
return gl ?? null;
|
|
616
|
+
}
|
|
617
|
+
if (typeof document !== "undefined") {
|
|
618
|
+
return document.createElement("canvas").getContext("webgl2", CONTEXT_ATTRS);
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
var sharedContext;
|
|
623
|
+
function getSharedWebGLContext() {
|
|
624
|
+
if (sharedContext === void 0 || sharedContext !== null && sharedContext.isContextLost()) {
|
|
625
|
+
sharedContext = createWebGLContext();
|
|
626
|
+
}
|
|
627
|
+
return sharedContext;
|
|
628
|
+
}
|
|
629
|
+
function isWebGLAvailable() {
|
|
630
|
+
return getSharedWebGLContext() !== null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/webgl/WebGLBlockEncoder.ts
|
|
634
|
+
function compileShader(gl, type, source, label) {
|
|
635
|
+
const shader = gl.createShader(type);
|
|
636
|
+
if (!shader) throw new Error(`${label}: gl.createShader failed`);
|
|
637
|
+
gl.shaderSource(shader, source);
|
|
638
|
+
gl.compileShader(shader);
|
|
639
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
640
|
+
const log = gl.getShaderInfoLog(shader);
|
|
641
|
+
gl.deleteShader(shader);
|
|
642
|
+
const kind = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
643
|
+
throw new Error(`${label}: ${kind} shader compile failed: ${log}`);
|
|
644
|
+
}
|
|
645
|
+
return shader;
|
|
646
|
+
}
|
|
647
|
+
var WebGLBlockEncoder = class {
|
|
648
|
+
/**
|
|
649
|
+
* Create an encoder on the shared process-wide context (or a caller-supplied
|
|
650
|
+
* one). Throws when WebGL2 is unavailable. The `this:` annotation lets
|
|
651
|
+
* `BC7WebGLEncoder.create()` return `BC7WebGLEncoder`.
|
|
652
|
+
*/
|
|
653
|
+
static create(gl) {
|
|
654
|
+
const ctx = gl ?? getSharedWebGLContext();
|
|
655
|
+
if (!ctx) throw new Error("WebGL2 not available in this environment");
|
|
656
|
+
return new this({ gl: ctx });
|
|
657
|
+
}
|
|
658
|
+
gl;
|
|
659
|
+
// Set in _buildProgram(), which the constructor calls.
|
|
660
|
+
_program;
|
|
661
|
+
_vao;
|
|
662
|
+
_uSrc = null;
|
|
663
|
+
_uSrcSize = null;
|
|
664
|
+
_uFlipY = null;
|
|
665
|
+
constructor({ gl }) {
|
|
666
|
+
this.gl = gl;
|
|
667
|
+
this._buildProgram();
|
|
668
|
+
}
|
|
669
|
+
_buildProgram() {
|
|
670
|
+
const gl = this.gl;
|
|
671
|
+
const program = gl.createProgram();
|
|
672
|
+
const vao = gl.createVertexArray();
|
|
673
|
+
if (!program || !vao) throw new Error(`${this.label}: failed to allocate WebGL program/VAO`);
|
|
674
|
+
const vert = compileShader(gl, gl.VERTEX_SHADER, fullscreen_vert_default, this.label);
|
|
675
|
+
const frag = compileShader(gl, gl.FRAGMENT_SHADER, this.fragSource(), this.label);
|
|
676
|
+
gl.attachShader(program, vert);
|
|
677
|
+
gl.attachShader(program, frag);
|
|
678
|
+
gl.linkProgram(program);
|
|
679
|
+
gl.deleteShader(vert);
|
|
680
|
+
gl.deleteShader(frag);
|
|
681
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
682
|
+
const log = gl.getProgramInfoLog(program);
|
|
683
|
+
gl.deleteProgram(program);
|
|
684
|
+
throw new Error(`${this.label}: WebGL program link failed: ${log}`);
|
|
685
|
+
}
|
|
686
|
+
this._program = program;
|
|
687
|
+
this._vao = vao;
|
|
688
|
+
this._uSrc = gl.getUniformLocation(program, "uSrc");
|
|
689
|
+
this._uSrcSize = gl.getUniformLocation(program, "uSrcSize");
|
|
690
|
+
this._uFlipY = gl.getUniformLocation(program, "uFlipY");
|
|
691
|
+
}
|
|
692
|
+
/** Release the GL program + VAO. The shared context itself is left intact. */
|
|
693
|
+
destroy() {
|
|
694
|
+
const gl = this.gl;
|
|
695
|
+
if (gl.isContextLost()) return;
|
|
696
|
+
gl.deleteProgram(this._program);
|
|
697
|
+
gl.deleteVertexArray(this._vao);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Upload the source image to a freshly created RGBA8 texture bound on unit 0.
|
|
701
|
+
* Raw pixel sources (ImageData / mip levels) go through the typed-array
|
|
702
|
+
* overload; DOM sources (ImageBitmap / canvas / image) through the element
|
|
703
|
+
* overload. No flip / premultiply / colour conversion — flipY is applied in
|
|
704
|
+
* the shader so each mip level flips by its own height.
|
|
705
|
+
*/
|
|
706
|
+
_uploadSource(source, width, height) {
|
|
707
|
+
const gl = this.gl;
|
|
708
|
+
const tex = gl.createTexture();
|
|
709
|
+
if (!tex) throw new Error(`${this.label}: gl.createTexture failed`);
|
|
710
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
711
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
712
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
713
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
714
|
+
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
|
|
715
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
716
|
+
const raw = source;
|
|
717
|
+
if (raw.data && ArrayBuffer.isView(raw.data)) {
|
|
718
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, raw.data);
|
|
719
|
+
} else {
|
|
720
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
721
|
+
}
|
|
722
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
723
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
724
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
725
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
726
|
+
return tex;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Encode one image source to raw compressed bytes. `flipY` samples the source
|
|
730
|
+
* bottom-up (matching Three.js's convention) and is applied in the shader;
|
|
731
|
+
* the high-level mipped path bakes the flip into level 0 and passes false.
|
|
732
|
+
*/
|
|
733
|
+
encodeToBytes(source, { flipY = false } = {}) {
|
|
734
|
+
const gl = this.gl;
|
|
735
|
+
if (gl.isContextLost()) throw new Error(`${this.label}WebGLEncoder: WebGL context lost`);
|
|
736
|
+
const width = source.width;
|
|
737
|
+
const height = source.height;
|
|
738
|
+
if (!width || !height) {
|
|
739
|
+
throw new Error(`${this.label}WebGLEncoder: source has no dimensions`);
|
|
740
|
+
}
|
|
741
|
+
const paddedWidth = width + 3 & ~3;
|
|
742
|
+
const paddedHeight = height + 3 & ~3;
|
|
743
|
+
const blocksX = paddedWidth >> 2;
|
|
744
|
+
const blocksY = paddedHeight >> 2;
|
|
745
|
+
const blockCount = blocksX * blocksY;
|
|
746
|
+
const outByteLen = blockCount * this.bytesPerBlock;
|
|
747
|
+
const t0 = performance.now();
|
|
748
|
+
const srcTex = this._uploadSource(source, width, height);
|
|
749
|
+
const outTex = gl.createTexture();
|
|
750
|
+
const fbo = gl.createFramebuffer();
|
|
751
|
+
if (!outTex || !fbo) {
|
|
752
|
+
gl.deleteTexture(srcTex);
|
|
753
|
+
if (outTex) gl.deleteTexture(outTex);
|
|
754
|
+
if (fbo) gl.deleteFramebuffer(fbo);
|
|
755
|
+
throw new Error(`${this.label}: failed to allocate output texture/framebuffer`);
|
|
756
|
+
}
|
|
757
|
+
gl.bindTexture(gl.TEXTURE_2D, outTex);
|
|
758
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32UI, blocksX, blocksY);
|
|
759
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
760
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
761
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
762
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outTex, 0);
|
|
763
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
764
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
765
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
766
|
+
gl.deleteFramebuffer(fbo);
|
|
767
|
+
gl.deleteTexture(outTex);
|
|
768
|
+
gl.deleteTexture(srcTex);
|
|
769
|
+
throw new Error(`${this.label}: integer framebuffer incomplete (0x${status.toString(16)})`);
|
|
770
|
+
}
|
|
771
|
+
gl.useProgram(this._program);
|
|
772
|
+
gl.bindVertexArray(this._vao);
|
|
773
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
774
|
+
gl.bindTexture(gl.TEXTURE_2D, srcTex);
|
|
775
|
+
gl.uniform1i(this._uSrc, 0);
|
|
776
|
+
gl.uniform2i(this._uSrcSize, width, height);
|
|
777
|
+
gl.uniform1i(this._uFlipY, flipY ? 1 : 0);
|
|
778
|
+
gl.disable(gl.BLEND);
|
|
779
|
+
gl.disable(gl.DEPTH_TEST);
|
|
780
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
781
|
+
gl.viewport(0, 0, blocksX, blocksY);
|
|
782
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
783
|
+
const words = new Uint32Array(blockCount * 4);
|
|
784
|
+
gl.readPixels(0, 0, blocksX, blocksY, gl.RGBA_INTEGER, gl.UNSIGNED_INT, words);
|
|
785
|
+
let data;
|
|
786
|
+
if (this.bytesPerBlock === 16) {
|
|
787
|
+
data = new Uint8Array(words.buffer, 0, outByteLen);
|
|
788
|
+
} else {
|
|
789
|
+
const packed = new Uint32Array(blockCount * 2);
|
|
790
|
+
for (let k = 0; k < blockCount; k++) {
|
|
791
|
+
packed[k * 2] = words[k * 4];
|
|
792
|
+
packed[k * 2 + 1] = words[k * 4 + 1];
|
|
793
|
+
}
|
|
794
|
+
data = new Uint8Array(packed.buffer, 0, outByteLen);
|
|
795
|
+
}
|
|
796
|
+
const encodeMs = performance.now() - t0;
|
|
797
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
798
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
799
|
+
gl.bindVertexArray(null);
|
|
800
|
+
gl.deleteFramebuffer(fbo);
|
|
801
|
+
gl.deleteTexture(outTex);
|
|
802
|
+
gl.deleteTexture(srcTex);
|
|
803
|
+
return { width, height, paddedWidth, paddedHeight, data, encodeMs };
|
|
804
|
+
}
|
|
805
|
+
/** Wrap pre-encoded levels into a CompressedTexture. Shared with the WebGPU path. */
|
|
806
|
+
buildMippedTexture(levels, { colorSpace = "srgb" } = {}) {
|
|
807
|
+
if (levels.length === 0) {
|
|
808
|
+
throw new Error(`${this.label}WebGLEncoder.buildMippedTexture: no levels provided`);
|
|
809
|
+
}
|
|
810
|
+
const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
|
|
811
|
+
return assembleCompressedTexture(levels, this.threeTextureFormat(), effectiveSrgb);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/webgl/BC1WebGLEncoder.ts
|
|
816
|
+
import { RGBA_S3TC_DXT1_Format as RGBA_S3TC_DXT1_Format2 } from "three";
|
|
817
|
+
|
|
818
|
+
// src/webgl/glsl/bc1.frag.glsl
|
|
819
|
+
var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl.\n//\n// One fragment per 4\xD74 block. Output is the 8-byte BC1 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused); the encoder reads back RGBA32UI and keeps\n// the low two words per block. Algorithm mirrors bc1.wgsl line-for-line:\n// bbox endpoints, 1/16 inset, RGB565 quantisation, forced 4-colour mode, full\n// L2 index search. See bc1.wgsl for the detailed rationale.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize; // original (unpadded) width, height\nuniform int uFlipY; // 1 = sample bottom-up (matches Three.js flipY)\n\nlayout(location = 0) out uvec4 outColor;\n\nuint to565(vec3 c) {\n uint r = uint(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n uint g = uint(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n uint b = uint(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11) | (g << 5) | b;\n}\n\nvec3 from565(uint c) {\n float r = float((c >> 11) & 31u);\n float g = float((c >> 5) & 63u);\n float b = float(c & 31u);\n float r8 = (r * 527.0 + 23.0) / 256.0;\n float g8 = (g * 259.0 + 33.0) / 256.0;\n float b8 = (b * 527.0 + 23.0) / 256.0;\n return vec3(r8, g8, b8) / 255.0;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n vec3 pixels[16];\n vec3 bbMin = vec3(1.0);\n vec3 bbMax = vec3(0.0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec3 c = texelFetch(uSrc, ivec2(p.x, sy), 0).rgb;\n pixels[i] = c;\n bbMin = min(bbMin, c);\n bbMax = max(bbMax, c);\n }\n\n // Inset the bounding box by ~half an RGB565 cell (1/16) to tighten the\n // quantised palette around the real data range.\n vec3 inset = (bbMax - bbMin) / 16.0;\n vec3 hi = clamp(bbMax - inset, vec3(0.0), vec3(1.0));\n vec3 lo = clamp(bbMin + inset, vec3(0.0), vec3(1.0));\n\n uint c0 = to565(hi);\n uint c1 = to565(lo);\n\n // 4-colour mode requires c0 > c1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n uint tmp = c0; c0 = c1; c1 = tmp;\n }\n\n vec3 p0 = from565(c0);\n vec3 p1 = from565(c1);\n vec3 p2 = (2.0 * p0 + p1) * (1.0 / 3.0);\n vec3 p3 = (p0 + 2.0 * p1) * (1.0 / 3.0);\n\n uint indices = 0u;\n for (int i = 0; i < 16; i++) {\n vec3 c = pixels[i];\n float d0 = dot(c - p0, c - p0);\n float d1 = dot(c - p1, c - p1);\n float d2 = dot(c - p2, c - p2);\n float d3 = dot(c - p3, c - p3);\n\n float bestD = d0;\n uint bestI = 0u;\n if (d1 < bestD) { bestD = d1; bestI = 1u; }\n if (d2 < bestD) { bestD = d2; bestI = 2u; }\n if (d3 < bestD) { bestD = d3; bestI = 3u; }\n\n indices = indices | (bestI << (i * 2));\n }\n\n outColor = uvec4(c0 | (c1 << 16), indices, 0u, 0u);\n}\n";
|
|
820
|
+
|
|
821
|
+
// src/webgl/BC1WebGLEncoder.ts
|
|
822
|
+
var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
823
|
+
get label() {
|
|
824
|
+
return "bc1";
|
|
825
|
+
}
|
|
826
|
+
get bytesPerBlock() {
|
|
827
|
+
return 8;
|
|
828
|
+
}
|
|
829
|
+
get supportsSrgb() {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
fragSource() {
|
|
833
|
+
return bc1_frag_default;
|
|
834
|
+
}
|
|
835
|
+
threeTextureFormat() {
|
|
836
|
+
return RGBA_S3TC_DXT1_Format2;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/webgl/BC5WebGLEncoder.ts
|
|
841
|
+
import { RED_GREEN_RGTC2_Format as RED_GREEN_RGTC2_Format2 } from "three";
|
|
842
|
+
|
|
843
|
+
// src/webgl/glsl/bc5.frag.glsl
|
|
844
|
+
var bc5_frag_default = "#version 300 es\n// BC5 (RGTC2) fragment-shader encoder \u2014 WebGL2 port of bc5.wgsl (fast path).\n//\n// One fragment per 4\xD74 block \u2192 16-byte BC5 block as 4 \xD7 u32 in outColor.\n// BC5 = two BC4 halves (R then G). This is the *fast* path only: bbox\n// endpoints + a single full-L2 index assignment per channel, no LSQ refit\n// (the WGSL `QUALITY_HIGH` branch). Always emits 6-interpolation mode\n// (red0 > red1). See bc5.wgsl for the full derivation.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// 6-interpolation-mode palette weights: pal[j] = W0_6[j]*r0 + W1_6[j]*r1.\nconst float W0_6[8] = float[8](1.0, 0.0, 6.0 / 7.0, 5.0 / 7.0, 4.0 / 7.0, 3.0 / 7.0, 2.0 / 7.0, 1.0 / 7.0);\nconst float W1_6[8] = float[8](0.0, 1.0, 1.0 / 7.0, 2.0 / 7.0, 3.0 / 7.0, 4.0 / 7.0, 5.0 / 7.0, 6.0 / 7.0);\n\nuint quantize8(float v) {\n return uint(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// Encode 16 single-channel values into an 8-byte BC4 block (two little-endian\n// u32s). Mirrors encode_bc4() in bc5.wgsl with the refit pass omitted.\nuvec2 encodeBC4(float values[16]) {\n float vmin = 1.0;\n float vmax = 0.0;\n for (int k = 0; k < 16; k++) {\n vmin = min(vmin, values[k]);\n vmax = max(vmax, values[k]);\n }\n uint r0 = quantize8(vmax);\n uint r1 = quantize8(vmin);\n if (r0 == r1) {\n if (r1 > 0u) { r1 = r1 - 1u; } else { r0 = r0 + 1u; }\n }\n\n float pal[8];\n float r0f = float(r0) / 255.0;\n float r1f = float(r1) / 255.0;\n for (int j = 0; j < 8; j++) {\n pal[j] = W0_6[j] * r0f + W1_6[j] * r1f;\n }\n\n uint indices[16];\n for (int k = 0; k < 16; k++) {\n float v = values[k];\n uint bestJ = 0u;\n float bestD = 1e20;\n for (int j = 0; j < 8; j++) {\n float d = pal[j] - v;\n float d2 = d * d;\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n indices[k] = bestJ;\n }\n\n // Pack the 48-bit index field (bytes 2..7) split across two u32 halves.\n uint idxLo = 0u;\n uint idxHi = 0u;\n for (int k = 0; k < 16; k++) {\n uint bit = 3u * uint(k);\n uint v = indices[k] & 7u;\n if (bit + 3u <= 32u) {\n idxLo = idxLo | (v << bit);\n } else if (bit >= 32u) {\n idxHi = idxHi | (v << (bit - 32u));\n } else {\n idxLo = idxLo | (v << bit);\n idxHi = idxHi | (v >> (32u - bit));\n }\n }\n\n uint outLo = r0 | (r1 << 8) | ((idxLo & 0xFFFFu) << 16);\n uint outHi = (idxLo >> 16) | (idxHi << 16);\n return uvec2(outLo, outHi);\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n float rValues[16];\n float gValues[16];\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec4 c = texelFetch(uSrc, ivec2(p.x, sy), 0);\n rValues[i] = c.r;\n gValues[i] = c.g;\n }\n\n uvec2 rBlock = encodeBC4(rValues);\n uvec2 gBlock = encodeBC4(gValues);\n outColor = uvec4(rBlock.x, rBlock.y, gBlock.x, gBlock.y);\n}\n";
|
|
845
|
+
|
|
846
|
+
// src/webgl/BC5WebGLEncoder.ts
|
|
847
|
+
var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|
|
848
|
+
get label() {
|
|
849
|
+
return "bc5";
|
|
850
|
+
}
|
|
851
|
+
get bytesPerBlock() {
|
|
852
|
+
return 16;
|
|
853
|
+
}
|
|
854
|
+
get supportsSrgb() {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
fragSource() {
|
|
858
|
+
return bc5_frag_default;
|
|
859
|
+
}
|
|
860
|
+
threeTextureFormat() {
|
|
861
|
+
return RED_GREEN_RGTC2_Format2;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// src/webgl/BC7WebGLEncoder.ts
|
|
866
|
+
import { RGBA_BPTC_Format as RGBA_BPTC_Format2 } from "three";
|
|
867
|
+
|
|
868
|
+
// src/webgl/glsl/bc7.frag.glsl
|
|
869
|
+
var bc7_frag_default = "#version 300 es\n// BC7 (BPTC) mode-6 fragment-shader encoder \u2014 WebGL2 port of bc7.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Fast path\n// only: O(N) bbox seed \u2192 endpoints fitted by a single least-squares pass whose\n// normal-equation sums are accumulated during a projection-based index\n// assignment (palette is colinear, so the nearest entry is found by projecting\n// onto the endpoint line \u2014 O(1) per pixel). Mirrors the `QUALITY_HIGH == 0`\n// branch of bc7.wgsl; see that file for the mode-6 bit layout and rationale.\n//\n// Determinism note: the WGSL refit uses round() (half-to-even); here we use\n// floor(x + 0.5) for portability. The two differ only at exact .5 ties, a\n// sub-LSB endpoint nudge that is visually identical.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// Per-invocation scratch (mirrors the WGSL function-scope arrays passed by ptr).\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct QuantPair { ivec4 seven; ivec4 eight; };\nstruct Ep { ivec4 seven; ivec4 eight; uint p; };\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\nint dist2(ivec4 a, ivec4 b) {\n ivec4 d = a - b;\n ivec4 e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit) under\n// a fixed p-bit, all four channels at once.\nQuantPair quantizeEndpoint(ivec4 ideal8, uint p) {\n ivec4 q = ivec4(clamp(floor((vec4(ideal8) - float(p)) / 2.0 + 0.5), vec4(0.0), vec4(127.0)));\n // eff = (q << 1) | p. q*2 is even and p \u2208 {0,1}, so q*2 + p is identical and\n // avoids any vector-shift-by-scalar portability question.\n ivec4 eff = q * 2 + ivec4(int(p));\n return QuantPair(q, eff);\n}\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nEp pickEp(ivec4 ideal) {\n QuantPair a = quantizeEndpoint(ideal, 0u);\n QuantPair b = quantizeEndpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) {\n return Ep(b.seven, b.eight, 1u);\n }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Projection index assignment over gPixels \u2192 gIdx. When `fit`, accumulate the\n// LSQ normal-equation sums in the same pass and return refitted endpoints.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 15.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 15.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 15.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n Ep ep0 = pickEp(lo);\n Ep ep1 = pickEp(hi);\n Fit r = projAssign(ep0.eight, ep1.eight, true);\n if (r.valid) {\n ep0 = pickEp(r.e0);\n ep1 = pickEp(r.e1);\n projAssign(ep0.eight, ep1.eight, false);\n }\n ivec4 e0_7 = ep0.seven;\n ivec4 e1_7 = ep1.seven;\n uint p0 = ep0.p;\n uint p1 = ep1.p;\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0; otherwise swap endpoints and\n // reflect every index (decoded image unchanged).\n if ((gIdx[0] & 0x8u) != 0u) {\n ivec4 t = e0_7; e0_7 = e1_7; e1_7 = t;\n uint tp = p0; p0 = p1; p1 = tp;\n for (int k = 0; k < 16; k++) { gIdx[k] = 15u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n uint pos = 0u;\n writeBits(block, pos, 7u, 0x40u); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.w)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.w)); pos += 7u;\n writeBits(block, pos, 1u, p0); pos += 1u;\n writeBits(block, pos, 1u, p1); pos += 1u;\n writeBits(block, pos, 3u, gIdx[0] & 0x7u); pos += 3u;\n for (int k = 1; k < 16; k++) {\n writeBits(block, pos, 4u, gIdx[k] & 0xFu);\n pos += 4u;\n }\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
|
|
870
|
+
|
|
871
|
+
// src/webgl/BC7WebGLEncoder.ts
|
|
872
|
+
var BC7WebGLEncoder = class extends WebGLBlockEncoder {
|
|
873
|
+
get label() {
|
|
874
|
+
return "bc7";
|
|
875
|
+
}
|
|
876
|
+
get bytesPerBlock() {
|
|
877
|
+
return 16;
|
|
878
|
+
}
|
|
879
|
+
get supportsSrgb() {
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
fragSource() {
|
|
883
|
+
return bc7_frag_default;
|
|
884
|
+
}
|
|
885
|
+
threeTextureFormat() {
|
|
886
|
+
return RGBA_BPTC_Format2;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/webgl/ASTC4x4WebGLEncoder.ts
|
|
891
|
+
import { RGBA_ASTC_4x4_Format as RGBA_ASTC_4x4_Format2 } from "three";
|
|
892
|
+
|
|
893
|
+
// src/webgl/glsl/astc4x4.frag.glsl
|
|
894
|
+
var astc4x4_frag_default = "#version 300 es\n// ASTC 4\xD74 LDR fragment-shader encoder \u2014 WebGL2 port of astc4x4.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Restricted\n// subset: single partition, no dual-plane, CEM 12 (LDR RGBA direct), 4\xD74 weight\n// grid with 2-bit weights (QUANT_4), 8-bit endpoints (QUANT_256). Fast path:\n// bbox seed \u2192 one LSQ refit fused into a projection weight assignment (4 colinear\n// levels). Mirrors the `QUALITY_HIGH == 0` branch of astc4x4.wgsl; see that file\n// for the 128-bit block layout.\n//\n// Determinism note: floor(x + 0.5) replaces WGSL round() for the refit endpoints\n// (sub-LSB difference at exact .5 ties only).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\n// Projection weight assignment over 4 levels (QUANT_4 \u2248 thirds), with the LSQ\n// normal-equation sums accumulated in the same pass for a fused refit.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 3.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 3.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 3.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n ivec4 e0 = lo;\n ivec4 e1 = hi;\n Fit r = projAssign(e0, e1, true);\n if (r.valid) {\n e0 = r.e0;\n e1 = r.e1;\n projAssign(e0, e1, false);\n }\n\n // Endpoint ordering so the decoder doesn't apply blue contraction.\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n ivec4 t = e0; e0 = e1; e1 = t;\n for (int k = 0; k < 16; k++) { gIdx[k] = 3u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n writeBits(block, 0u, 11u, 0x042u);\n writeBits(block, 11u, 2u, 0u);\n writeBits(block, 13u, 4u, 12u);\n writeBits(block, 17u + 0u * 8u, 8u, uint(e0.x));\n writeBits(block, 17u + 1u * 8u, 8u, uint(e1.x));\n writeBits(block, 17u + 2u * 8u, 8u, uint(e0.y));\n writeBits(block, 17u + 3u * 8u, 8u, uint(e1.y));\n writeBits(block, 17u + 4u * 8u, 8u, uint(e0.z));\n writeBits(block, 17u + 5u * 8u, 8u, uint(e1.z));\n writeBits(block, 17u + 6u * 8u, 8u, uint(e0.w));\n writeBits(block, 17u + 7u * 8u, 8u, uint(e1.w));\n\n uint w3 = 0u;\n for (int k = 0; k < 16; k++) {\n uint w = gIdx[k] & 0x3u;\n w3 = w3 | ((w & 1u) << (31u - 2u * uint(k))) | (((w >> 1u) & 1u) << (30u - 2u * uint(k)));\n }\n block[3] = w3;\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
|
|
895
|
+
|
|
896
|
+
// src/webgl/ASTC4x4WebGLEncoder.ts
|
|
897
|
+
var ASTC4x4WebGLEncoder = class extends WebGLBlockEncoder {
|
|
898
|
+
get label() {
|
|
899
|
+
return "astc4x4";
|
|
900
|
+
}
|
|
901
|
+
get bytesPerBlock() {
|
|
902
|
+
return 16;
|
|
903
|
+
}
|
|
904
|
+
get supportsSrgb() {
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
fragSource() {
|
|
908
|
+
return astc4x4_frag_default;
|
|
909
|
+
}
|
|
910
|
+
threeTextureFormat() {
|
|
911
|
+
return RGBA_ASTC_4x4_Format2;
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// src/webgl/webglCapabilities.ts
|
|
916
|
+
function detectWebGLCapabilities(gl) {
|
|
917
|
+
if (!gl || typeof gl.getExtension !== "function") {
|
|
918
|
+
throw new TypeError("detectWebGLCapabilities: a WebGL2 context (or { getExtension }) is required");
|
|
919
|
+
}
|
|
920
|
+
const has = (name) => gl.getExtension(name) != null;
|
|
921
|
+
return {
|
|
922
|
+
bptc: has("EXT_texture_compression_bptc"),
|
|
923
|
+
rgtc: has("EXT_texture_compression_rgtc"),
|
|
924
|
+
s3tc: has("WEBGL_compressed_texture_s3tc"),
|
|
925
|
+
s3tcSrgb: has("WEBGL_compressed_texture_s3tc_srgb"),
|
|
926
|
+
astc: has("WEBGL_compressed_texture_astc")
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/webgl/selectWebGLFormat.ts
|
|
931
|
+
var NONE = { format: null, encoderClass: null, astcNormalRemap: false };
|
|
932
|
+
function selectWebGLFormat(caps, hint, options = {}) {
|
|
933
|
+
const { colorSpace = "srgb" } = options;
|
|
934
|
+
const srgb = colorSpace === "srgb";
|
|
935
|
+
const astc = (astcNormalRemap) => ({
|
|
936
|
+
format: srgb ? TextureFormat.ASTC_4x4_SRGB : TextureFormat.ASTC_4x4,
|
|
937
|
+
encoderClass: ASTC4x4WebGLEncoder,
|
|
938
|
+
astcNormalRemap
|
|
939
|
+
});
|
|
940
|
+
if (hint === "normal") {
|
|
941
|
+
if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
|
|
942
|
+
if (caps.astc) return astc(true);
|
|
943
|
+
return NONE;
|
|
944
|
+
}
|
|
945
|
+
if (caps.bptc) {
|
|
946
|
+
return {
|
|
947
|
+
format: srgb ? TextureFormat.BC7_SRGB : TextureFormat.BC7,
|
|
948
|
+
encoderClass: BC7WebGLEncoder,
|
|
949
|
+
astcNormalRemap: false
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
if (caps.astc) return astc(false);
|
|
953
|
+
if (hint === "color") {
|
|
954
|
+
if (srgb && caps.s3tcSrgb) {
|
|
955
|
+
return { format: TextureFormat.BC1_SRGB, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
|
|
956
|
+
}
|
|
957
|
+
if (!srgb && caps.s3tc) {
|
|
958
|
+
return { format: TextureFormat.BC1, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return NONE;
|
|
962
|
+
}
|
|
963
|
+
|
|
412
964
|
// src/selectFormat.ts
|
|
413
965
|
function selectFormat(adapter, hint, options = {}) {
|
|
414
966
|
const { colorSpace = "srgb" } = options;
|
|
@@ -436,7 +988,7 @@ function selectFormat(adapter, hint, options = {}) {
|
|
|
436
988
|
}
|
|
437
989
|
|
|
438
990
|
// src/compressTexture.ts
|
|
439
|
-
import { LinearFilter as
|
|
991
|
+
import { LinearFilter as LinearFilter3, LinearSRGBColorSpace as LinearSRGBColorSpace3, RepeatWrapping as RepeatWrapping3, SRGBColorSpace as SRGBColorSpace3, Texture } from "three";
|
|
440
992
|
|
|
441
993
|
// src/mipgen.ts
|
|
442
994
|
function generateMipChain(level0) {
|
|
@@ -546,10 +1098,10 @@ function mipLevelToImageData(level) {
|
|
|
546
1098
|
}
|
|
547
1099
|
function wrapUncompressed(bitmap, srgb, flipY) {
|
|
548
1100
|
const tex = new Texture(bitmap);
|
|
549
|
-
tex.colorSpace = srgb ?
|
|
550
|
-
tex.magFilter =
|
|
551
|
-
tex.minFilter =
|
|
552
|
-
tex.wrapS = tex.wrapT =
|
|
1101
|
+
tex.colorSpace = srgb ? SRGBColorSpace3 : LinearSRGBColorSpace3;
|
|
1102
|
+
tex.magFilter = LinearFilter3;
|
|
1103
|
+
tex.minFilter = LinearFilter3;
|
|
1104
|
+
tex.wrapS = tex.wrapT = RepeatWrapping3;
|
|
553
1105
|
tex.generateMipmaps = false;
|
|
554
1106
|
tex.flipY = flipY;
|
|
555
1107
|
tex.needsUpdate = true;
|
|
@@ -561,129 +1113,165 @@ async function compressTexture(source, options = {}) {
|
|
|
561
1113
|
colorSpace = "srgb",
|
|
562
1114
|
flipY = true,
|
|
563
1115
|
mipmaps = false,
|
|
1116
|
+
quality = "fast",
|
|
564
1117
|
device: providedDevice,
|
|
565
1118
|
adapter: providedAdapter
|
|
566
1119
|
} = options;
|
|
567
1120
|
const srgb = colorSpace === "srgb";
|
|
568
1121
|
const bitmap = await sourceToBitmap(source);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1122
|
+
const viaWebGPU = await encodeViaWebGPU();
|
|
1123
|
+
if (viaWebGPU) return viaWebGPU;
|
|
1124
|
+
const viaWebGL = encodeViaWebGL();
|
|
1125
|
+
if (viaWebGL) return viaWebGL;
|
|
1126
|
+
console.warn(
|
|
1127
|
+
"[compressTexture] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
|
|
1128
|
+
);
|
|
1129
|
+
const tex = wrapUncompressed(bitmap, srgb, flipY);
|
|
1130
|
+
return {
|
|
1131
|
+
texture: tex,
|
|
1132
|
+
format: null,
|
|
1133
|
+
fallbackUncompressed: true,
|
|
1134
|
+
backend: "none",
|
|
1135
|
+
astcNormalRemap: false,
|
|
1136
|
+
width: bitmap.width,
|
|
1137
|
+
height: bitmap.height,
|
|
1138
|
+
mipLevels: 1,
|
|
1139
|
+
encodeMs: 0,
|
|
1140
|
+
destroy: () => {
|
|
1141
|
+
tex.dispose();
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
async function encodeViaWebGPU() {
|
|
1145
|
+
if (!("gpu" in navigator)) return null;
|
|
1146
|
+
const adapter = providedAdapter ?? await navigator.gpu.requestAdapter();
|
|
1147
|
+
if (!adapter) return null;
|
|
1148
|
+
const selection = selectFormat(adapter, hint, { colorSpace });
|
|
1149
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
1150
|
+
let encoder;
|
|
1151
|
+
if (providedDevice) {
|
|
1152
|
+
const EncoderCtor = selection.encoderClass;
|
|
1153
|
+
encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
|
|
1154
|
+
} else {
|
|
1155
|
+
encoder = await selection.encoderClass.create();
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
1159
|
+
if (!mipmaps) {
|
|
1160
|
+
let bytes;
|
|
1161
|
+
if (needsWriteTexture) {
|
|
1162
|
+
const level02 = bitmapToMipLevel(bitmap, flipY);
|
|
1163
|
+
const imageData = mipLevelToImageData(level02);
|
|
1164
|
+
bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
1165
|
+
} else {
|
|
1166
|
+
bytes = await encoder.encodeToBytes(bitmap, { flipY, quality });
|
|
1167
|
+
}
|
|
1168
|
+
const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
|
|
1169
|
+
return {
|
|
1170
|
+
texture: tex3,
|
|
1171
|
+
format: selection.format,
|
|
1172
|
+
fallbackUncompressed: false,
|
|
1173
|
+
backend: "webgpu",
|
|
1174
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
1175
|
+
width: bytes.width,
|
|
1176
|
+
height: bytes.height,
|
|
1177
|
+
mipLevels: 1,
|
|
1178
|
+
encodeMs: bytes.encodeMs,
|
|
1179
|
+
destroy: () => {
|
|
1180
|
+
tex3.dispose();
|
|
1181
|
+
encoder.destroy();
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
583
1184
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
astcNormalRemap: false,
|
|
595
|
-
width: bitmap.width,
|
|
596
|
-
height: bitmap.height,
|
|
597
|
-
mipLevels: 1,
|
|
598
|
-
encodeMs: 0,
|
|
599
|
-
destroy: () => {
|
|
600
|
-
tex.dispose();
|
|
1185
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
1186
|
+
const chain = generateMipChain(level0);
|
|
1187
|
+
const encodedLevels = [];
|
|
1188
|
+
let totalEncodeMs = 0;
|
|
1189
|
+
for (const level of chain) {
|
|
1190
|
+
const padded = padToBlockMultiple(level);
|
|
1191
|
+
const imageData = mipLevelToImageData(padded);
|
|
1192
|
+
const bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
1193
|
+
encodedLevels.push(bytes);
|
|
1194
|
+
totalEncodeMs += bytes.encodeMs;
|
|
601
1195
|
}
|
|
602
|
-
|
|
1196
|
+
const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
|
|
1197
|
+
return {
|
|
1198
|
+
texture: tex2,
|
|
1199
|
+
format: selection.format,
|
|
1200
|
+
fallbackUncompressed: false,
|
|
1201
|
+
backend: "webgpu",
|
|
1202
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
1203
|
+
width: level0.width,
|
|
1204
|
+
height: level0.height,
|
|
1205
|
+
mipLevels: encodedLevels.length,
|
|
1206
|
+
encodeMs: totalEncodeMs,
|
|
1207
|
+
destroy: () => {
|
|
1208
|
+
tex2.dispose();
|
|
1209
|
+
encoder.destroy();
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
encoder.destroy();
|
|
1214
|
+
throw e;
|
|
1215
|
+
}
|
|
603
1216
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1217
|
+
function encodeViaWebGL() {
|
|
1218
|
+
const gl = getSharedWebGLContext();
|
|
1219
|
+
if (!gl) return null;
|
|
1220
|
+
const caps = detectWebGLCapabilities(gl);
|
|
1221
|
+
const selection = selectWebGLFormat(caps, hint, { colorSpace });
|
|
1222
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
1223
|
+
const encoder = selection.encoderClass.create(gl);
|
|
1224
|
+
try {
|
|
1225
|
+
if (!mipmaps) {
|
|
1226
|
+
const bytes = encoder.encodeToBytes(bitmap, { flipY });
|
|
1227
|
+
const tex3 = encoder.buildMippedTexture([bytes], { colorSpace });
|
|
1228
|
+
return {
|
|
1229
|
+
texture: tex3,
|
|
1230
|
+
format: selection.format,
|
|
1231
|
+
fallbackUncompressed: false,
|
|
1232
|
+
backend: "webgl",
|
|
1233
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
1234
|
+
width: bytes.width,
|
|
1235
|
+
height: bytes.height,
|
|
1236
|
+
mipLevels: 1,
|
|
1237
|
+
encodeMs: bytes.encodeMs,
|
|
1238
|
+
destroy: () => {
|
|
1239
|
+
tex3.dispose();
|
|
1240
|
+
encoder.destroy();
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
621
1243
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
try {
|
|
632
|
-
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
633
|
-
if (!mipmaps) {
|
|
634
|
-
let bytes;
|
|
635
|
-
if (needsWriteTexture) {
|
|
636
|
-
const level02 = bitmapToMipLevel(bitmap, flipY);
|
|
637
|
-
const imageData = mipLevelToImageData(level02);
|
|
638
|
-
bytes = await encoder.encodeToBytes(imageData);
|
|
639
|
-
} else {
|
|
640
|
-
bytes = await encoder.encodeToBytes(bitmap, { flipY });
|
|
1244
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
1245
|
+
const chain = generateMipChain(level0);
|
|
1246
|
+
const encodedLevels = [];
|
|
1247
|
+
let totalEncodeMs = 0;
|
|
1248
|
+
for (const level of chain) {
|
|
1249
|
+
const padded = padToBlockMultiple(level);
|
|
1250
|
+
const bytes = encoder.encodeToBytes(padded);
|
|
1251
|
+
encodedLevels.push(bytes);
|
|
1252
|
+
totalEncodeMs += bytes.encodeMs;
|
|
641
1253
|
}
|
|
642
|
-
const tex2 = encoder.buildMippedTexture(
|
|
1254
|
+
const tex2 = encoder.buildMippedTexture(encodedLevels, { colorSpace });
|
|
643
1255
|
return {
|
|
644
1256
|
texture: tex2,
|
|
645
1257
|
format: selection.format,
|
|
646
1258
|
fallbackUncompressed: false,
|
|
1259
|
+
backend: "webgl",
|
|
647
1260
|
astcNormalRemap: selection.astcNormalRemap,
|
|
648
|
-
width:
|
|
649
|
-
height:
|
|
650
|
-
mipLevels:
|
|
651
|
-
encodeMs:
|
|
1261
|
+
width: level0.width,
|
|
1262
|
+
height: level0.height,
|
|
1263
|
+
mipLevels: encodedLevels.length,
|
|
1264
|
+
encodeMs: totalEncodeMs,
|
|
652
1265
|
destroy: () => {
|
|
653
1266
|
tex2.dispose();
|
|
654
1267
|
encoder.destroy();
|
|
655
1268
|
}
|
|
656
1269
|
};
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
encoder.destroy();
|
|
1272
|
+
console.warn("[compressTexture] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
|
|
1273
|
+
return null;
|
|
657
1274
|
}
|
|
658
|
-
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
659
|
-
const chain = generateMipChain(level0);
|
|
660
|
-
const encodedLevels = [];
|
|
661
|
-
let totalEncodeMs = 0;
|
|
662
|
-
for (const level of chain) {
|
|
663
|
-
const padded = padToBlockMultiple(level);
|
|
664
|
-
const imageData = mipLevelToImageData(padded);
|
|
665
|
-
const bytes = await encoder.encodeToBytes(imageData);
|
|
666
|
-
encodedLevels.push(bytes);
|
|
667
|
-
totalEncodeMs += bytes.encodeMs;
|
|
668
|
-
}
|
|
669
|
-
const tex = encoder.buildMippedTexture(encodedLevels, { colorSpace });
|
|
670
|
-
return {
|
|
671
|
-
texture: tex,
|
|
672
|
-
format: selection.format,
|
|
673
|
-
fallbackUncompressed: false,
|
|
674
|
-
astcNormalRemap: selection.astcNormalRemap,
|
|
675
|
-
width: level0.width,
|
|
676
|
-
height: level0.height,
|
|
677
|
-
mipLevels: encodedLevels.length,
|
|
678
|
-
encodeMs: totalEncodeMs,
|
|
679
|
-
destroy: () => {
|
|
680
|
-
tex.dispose();
|
|
681
|
-
encoder.destroy();
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
} catch (e) {
|
|
685
|
-
encoder.destroy();
|
|
686
|
-
throw e;
|
|
687
1275
|
}
|
|
688
1276
|
}
|
|
689
1277
|
|
|
@@ -698,6 +1286,8 @@ var GputexLoader = class extends Loader {
|
|
|
698
1286
|
flipY = true;
|
|
699
1287
|
/** Generate + encode a full mip chain. Default false. */
|
|
700
1288
|
mipmaps = false;
|
|
1289
|
+
/** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
|
|
1290
|
+
quality = "fast";
|
|
701
1291
|
/**
|
|
702
1292
|
* Optional pre-existing WebGPU device. Reusing the renderer's device
|
|
703
1293
|
* avoids spinning up a second WebGPU context for encoding.
|
|
@@ -724,6 +1314,7 @@ var GputexLoader = class extends Loader {
|
|
|
724
1314
|
colorSpace: this.colorSpace,
|
|
725
1315
|
flipY: this.flipY,
|
|
726
1316
|
mipmaps: this.mipmaps,
|
|
1317
|
+
quality: this.quality,
|
|
727
1318
|
device: this.device,
|
|
728
1319
|
adapter: this.adapter
|
|
729
1320
|
}).then(
|
|
@@ -733,6 +1324,7 @@ var GputexLoader = class extends Loader {
|
|
|
733
1324
|
result.texture.userData.gputex = {
|
|
734
1325
|
format: result.format,
|
|
735
1326
|
fallbackUncompressed: result.fallbackUncompressed,
|
|
1327
|
+
backend: result.backend,
|
|
736
1328
|
astcNormalRemap: result.astcNormalRemap,
|
|
737
1329
|
width: result.width,
|
|
738
1330
|
height: result.height,
|
|
@@ -753,16 +1345,26 @@ var GputexLoader = class extends Loader {
|
|
|
753
1345
|
};
|
|
754
1346
|
export {
|
|
755
1347
|
ASTC4x4Encoder,
|
|
1348
|
+
ASTC4x4WebGLEncoder,
|
|
756
1349
|
BC1Encoder,
|
|
1350
|
+
BC1WebGLEncoder,
|
|
757
1351
|
BC5Encoder,
|
|
1352
|
+
BC5WebGLEncoder,
|
|
758
1353
|
BC7Encoder,
|
|
1354
|
+
BC7WebGLEncoder,
|
|
759
1355
|
Encoder,
|
|
760
1356
|
GputexLoader,
|
|
761
1357
|
TextureFormat,
|
|
1358
|
+
WebGLBlockEncoder,
|
|
762
1359
|
WebGPUFeature,
|
|
763
1360
|
compressTexture,
|
|
1361
|
+
createWebGLContext,
|
|
764
1362
|
detectCapabilities,
|
|
1363
|
+
detectWebGLCapabilities,
|
|
765
1364
|
generateMipChain,
|
|
1365
|
+
getSharedWebGLContext,
|
|
1366
|
+
isWebGLAvailable,
|
|
766
1367
|
padToBlockMultiple,
|
|
767
|
-
selectFormat
|
|
1368
|
+
selectFormat,
|
|
1369
|
+
selectWebGLFormat
|
|
768
1370
|
};
|