gputex 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -12
- package/dist/index.d.ts +25 -8
- package/dist/index.js +416 -91
- package/dist/testing.d.ts +120 -0
- package/dist/testing.js +945 -0
- package/dist/three.js +416 -91
- package/package.json +5 -1
package/dist/three.js
CHANGED
|
@@ -89,12 +89,16 @@ var Encoder = class {
|
|
|
89
89
|
if (adapter.features.has("shader-f16")) {
|
|
90
90
|
requiredFeatures.push("shader-f16");
|
|
91
91
|
}
|
|
92
|
+
if (adapter.features.has("timestamp-query")) {
|
|
93
|
+
requiredFeatures.push("timestamp-query");
|
|
94
|
+
}
|
|
92
95
|
const device = await adapter.requestDevice({ requiredFeatures });
|
|
93
96
|
return new this({ device, adapter, ownsDevice: true });
|
|
94
97
|
}
|
|
95
98
|
device;
|
|
96
99
|
adapter;
|
|
97
100
|
ownsDevice;
|
|
101
|
+
disableF16;
|
|
98
102
|
// `!:` because these are set in `_buildPipeline()` which the constructor
|
|
99
103
|
// calls; TypeScript's flow analysis doesn't see through method calls.
|
|
100
104
|
_module;
|
|
@@ -106,10 +110,11 @@ var Encoder = class {
|
|
|
106
110
|
// cache below holds the specialised pipelines for encoders that support it.
|
|
107
111
|
_pipeline;
|
|
108
112
|
_pipelineCache = /* @__PURE__ */ new Map();
|
|
109
|
-
constructor({ device, adapter, ownsDevice = false }) {
|
|
113
|
+
constructor({ device, adapter, ownsDevice = false, disableF16 = false }) {
|
|
110
114
|
this.device = device;
|
|
111
115
|
this.adapter = adapter;
|
|
112
116
|
this.ownsDevice = ownsDevice;
|
|
117
|
+
this.disableF16 = disableF16;
|
|
113
118
|
this._buildPipeline();
|
|
114
119
|
}
|
|
115
120
|
_buildPipeline() {
|
|
@@ -188,14 +193,14 @@ var Encoder = class {
|
|
|
188
193
|
/**
|
|
189
194
|
* Optional f16 WGSL for the 'fast' path. Used only when the device reports the
|
|
190
195
|
* `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
|
|
191
|
-
* `'high'` always uses it. Returns null when there's no f16 variant
|
|
196
|
+
* `'high'` always uses it. Returns null when there's no f16 variant.
|
|
192
197
|
*/
|
|
193
198
|
wgslSourceFastF16() {
|
|
194
199
|
return null;
|
|
195
200
|
}
|
|
196
201
|
/** Whether the f16 fast path is both available and supported on this device. */
|
|
197
202
|
get _useF16() {
|
|
198
|
-
return this.wgslSourceFastF16() !== null && this.device.features.has("shader-f16");
|
|
203
|
+
return !this.disableF16 && this.wgslSourceFastF16() !== null && this.device.features.has("shader-f16");
|
|
199
204
|
}
|
|
200
205
|
/**
|
|
201
206
|
* True if the device reports the feature the output texture needs.
|
|
@@ -216,7 +221,11 @@ var Encoder = class {
|
|
|
216
221
|
* bytes into a `CompressedTexture`; callers targeting another engine feed
|
|
217
222
|
* `data` into that engine's compressed-texture upload directly.
|
|
218
223
|
*/
|
|
219
|
-
async encodeToBytes(source, {
|
|
224
|
+
async encodeToBytes(source, {
|
|
225
|
+
flipY = false,
|
|
226
|
+
quality = "fast",
|
|
227
|
+
withGpuTime = false
|
|
228
|
+
} = {}) {
|
|
220
229
|
const device = this.device;
|
|
221
230
|
const width = source.width;
|
|
222
231
|
const height = source.height;
|
|
@@ -248,7 +257,7 @@ var Encoder = class {
|
|
|
248
257
|
size: 16,
|
|
249
258
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
250
259
|
});
|
|
251
|
-
device.queue.writeBuffer(paramsBuffer, 0, new Uint32Array([blocksX, blocksY,
|
|
260
|
+
device.queue.writeBuffer(paramsBuffer, 0, new Uint32Array([blocksX, blocksY, width, height]));
|
|
252
261
|
const pipeline = this._getPipeline(quality);
|
|
253
262
|
const bindGroup = device.createBindGroup({
|
|
254
263
|
label: `${this.label}-bg`,
|
|
@@ -259,35 +268,221 @@ var Encoder = class {
|
|
|
259
268
|
{ binding: 2, resource: { buffer: paramsBuffer } }
|
|
260
269
|
]
|
|
261
270
|
});
|
|
271
|
+
const useTimestamps = withGpuTime && device.features.has("timestamp-query");
|
|
272
|
+
const querySet = useTimestamps ? device.createQuerySet({ type: "timestamp", count: 2 }) : null;
|
|
273
|
+
const queryBuffer = useTimestamps ? device.createBuffer({
|
|
274
|
+
label: `${this.label}-ts-resolve`,
|
|
275
|
+
size: 16,
|
|
276
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
|
|
277
|
+
}) : null;
|
|
262
278
|
const [wgX, wgY] = this.workgroupSize;
|
|
263
279
|
const t0 = performance.now();
|
|
264
280
|
const enc = device.createCommandEncoder({ label: `${this.label}-encode` });
|
|
265
|
-
const pass = enc.beginComputePass(
|
|
281
|
+
const pass = enc.beginComputePass(
|
|
282
|
+
querySet ? { timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 } } : void 0
|
|
283
|
+
);
|
|
266
284
|
pass.setPipeline(pipeline);
|
|
267
285
|
pass.setBindGroup(0, bindGroup);
|
|
268
286
|
pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(blocksY / wgY), 1);
|
|
269
287
|
pass.end();
|
|
288
|
+
if (querySet && queryBuffer) enc.resolveQuerySet(querySet, 0, 2, queryBuffer, 0);
|
|
270
289
|
const staging = device.createBuffer({
|
|
271
290
|
label: `${this.label}-staging`,
|
|
272
291
|
size: outByteLen,
|
|
273
292
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
274
293
|
});
|
|
275
294
|
enc.copyBufferToBuffer(dstBuffer, 0, staging, 0, outByteLen);
|
|
295
|
+
const tsStaging = querySet && queryBuffer ? device.createBuffer({
|
|
296
|
+
label: `${this.label}-ts-staging`,
|
|
297
|
+
size: 16,
|
|
298
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
299
|
+
}) : null;
|
|
300
|
+
if (tsStaging && queryBuffer) enc.copyBufferToBuffer(queryBuffer, 0, tsStaging, 0, 16);
|
|
276
301
|
device.queue.submit([enc.finish()]);
|
|
277
302
|
await staging.mapAsync(GPUMapMode.READ);
|
|
278
303
|
const data = new Uint8Array(staging.getMappedRange().slice(0));
|
|
279
304
|
staging.unmap();
|
|
280
305
|
const encodeMs = performance.now() - t0;
|
|
306
|
+
let gpuMs;
|
|
307
|
+
if (tsStaging) {
|
|
308
|
+
await tsStaging.mapAsync(GPUMapMode.READ);
|
|
309
|
+
const [begin, end] = new BigUint64Array(tsStaging.getMappedRange().slice(0));
|
|
310
|
+
tsStaging.unmap();
|
|
311
|
+
tsStaging.destroy();
|
|
312
|
+
if (end !== void 0 && begin !== void 0 && end > begin) {
|
|
313
|
+
gpuMs = Number(end - begin) / 1e6;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
querySet?.destroy();
|
|
317
|
+
queryBuffer?.destroy();
|
|
281
318
|
srcTex.destroy();
|
|
282
319
|
dstBuffer.destroy();
|
|
283
320
|
staging.destroy();
|
|
284
321
|
paramsBuffer.destroy();
|
|
285
|
-
return { width, height, paddedWidth, paddedHeight, data, encodeMs };
|
|
322
|
+
return { width, height, paddedWidth, paddedHeight, data, encodeMs, gpuMs };
|
|
286
323
|
}
|
|
287
324
|
};
|
|
288
325
|
|
|
289
326
|
// src/bc1.wgsl
|
|
290
|
-
var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// We always force the 4-color mode (color0 > color1, numeric 16-bit):\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): bounding-box endpoints, inset by ~half a 565 cell, then\n// a single least-squares endpoint refit (the refit is accepted only if it\n// lowers the block's squared error). This is what the WebGL2 fragment\n// fallback runs too.\n// high (1): endpoints are seeded from the block's principal colour axis\n// (covariance power-iteration) as well as the bbox diagonal, each refined by\n// several least-squares passes; the lower-error family wins. Mirrors\n// bc1_ref.ts. Strictly \u2265 fast in quality, at the cost of the eigen-solve.\n//\n// Algorithm per block:\n// 1. Load the 16 pixels; compute the bounding box (and, for high, the mean).\n// 2. Seed endpoints (bbox diagonal; high also tries the principal axis).\n// 3. Quantize to RGB565, force 4-color mode, assign each pixel its nearest\n// palette entry (full 4-entry L2 search in the decoded colour space).\n// 4. Least-squares refit: re-solve the endpoints for the current indices,\n// re-quantize, re-assign; keep the result only when error decreases.\n\n// 0 = fast (default), 1 = high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = f32((c >> 11u) & 31u);\n let g = f32((c >> 5u) & 63u);\n let b = f32( c & 31u);\n // 5/6-bit -> 8-bit. floor((x*527+23)/64) == (x<<3)|(x>>2), i.e. the exact\n // bit-replication a BC1 decoder performs (white -> 255). The inputs are\n // small integers and /64 is exact in f32, so this matches the hardware and\n // is portable. Selecting indices against this palette is what makes the\n // encoder agree with what the GPU will actually sample.\n let r8 = floor((r * 527.0 + 23.0) / 64.0);\n let g8 = floor((g * 259.0 + 33.0) / 64.0);\n let b8 = floor((b * 527.0 + 23.0) / 64.0);\n return vec3<f32>(r8, g8, b8) / 255.0;\n}\n\n// 4-color-mode interpolation weights: palette[j] = wa(j)*c0 + wb(j)*c1.\nfn wa(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 2.0 / 3.0; }\n default: { return 1.0 / 3.0; } // case 3u\n }\n}\nfn wb(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 3.0; }\n default: { return 2.0 / 3.0; } // case 3u\n }\n}\n\nfn build_palette(c0: u32, c1: u32, pal: ptr<function, array<vec3<f32>, 4>>) {\n let p0 = from565(c0);\n let p1 = from565(c1);\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n (*pal)[j] = wa(j) * p0 + wb(j) * p1;\n }\n}\n\n// Assign each of the 16 pixels its nearest palette entry (full 4-entry L2),\n// writing indices into `out_idx` and returning the total squared error.\nfn assign_indices(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n pal: ptr<function, array<vec3<f32>, 4>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> f32 {\n var err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let c = (*pixels)[k];\n var best_j: u32 = 0u;\n var best_d: f32 = 1e30;\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n let d = (*pal)[j] - c;\n let d2 = dot(d, d);\n if (d2 < best_d) {\n best_d = d2;\n best_j = j;\n }\n }\n (*out_idx)[k] = best_j;\n err = err + best_d;\n }\n return err;\n}\n\n// One least-squares refit pass: solve the 2x2 normal equations for the endpoint\n// colours that minimise \u03A3\u2016wa\xB7e0 + wb\xB7e1 \u2212 c\u2016\xB2 under the current indices. The\n// three channels share the scalar sums, so it's one 2x2 solve with vec3 RHS.\nstruct RefitResult { e0: vec3<f32>, e1: vec3<f32>, valid: bool };\nfn refit(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec3<f32> = vec3<f32>(0.0);\n var sBV: vec3<f32> = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = wa((*indices)[k]);\n let b = wb((*indices)[k]);\n let v = (*pixels)[k];\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n var out: RefitResult;\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n out.e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.valid = true;\n return out;\n}\n\n// Candidate solution tracked across endpoint seeds / refit passes.\nstruct Best { c0: u32, c1: u32, indices: array<u32, 16>, err: f32 };\n\n// Quantize (hi, lo) to 565, force 4-color mode, assign indices, then refine with\n// up to `max_refits` least-squares passes. Commits to `*best` only on strict\n// improvement.\nfn fit_from_endpoints(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n hi: vec3<f32>,\n lo: vec3<f32>,\n max_refits: u32,\n best: ptr<function, Best>,\n) {\n var c0 = to565(hi);\n var c1 = to565(lo);\n // 4-color mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n\n var pal: array<vec3<f32>, 4>;\n var idx: array<u32, 16>;\n build_palette(c0, c1, &pal);\n var err = assign_indices(pixels, &pal, &idx);\n if (err < (*best).err) {\n (*best).c0 = c0; (*best).c1 = c1; (*best).indices = idx; (*best).err = err;\n }\n\n for (var rp: u32 = 0u; rp < max_refits; rp = rp + 1u) {\n let r = refit(pixels, &idx);\n if (!r.valid) { break; }\n var nc0 = to565(r.e0);\n var nc1 = to565(r.e1);\n // A refit that flips/equalises the endpoints would change decode mode;\n // keep 4-color mode, and stop once it stops moving.\n if (nc0 < nc1) { let t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 == nc1) { break; }\n if (nc0 == c0 && nc1 == c1) { break; }\n build_palette(nc0, nc1, &pal);\n let nerr = assign_indices(pixels, &pal, &idx);\n c0 = nc0; c1 = nc1; err = nerr;\n if (nerr < (*best).err) {\n (*best).c0 = nc0; (*best).c1 = nc1; (*best).indices = idx; (*best).err = nerr;\n }\n }\n}\n\n// Principal colour axis via covariance power-iteration, seeded with the bbox\n// diagonal. Returns a unit axis, or vec3(0) for a degenerate (constant) block.\nfn principal_axis(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\n // Symmetric 3x3 covariance, stored as its three rows.\n var c0v = vec3<f32>(0.0);\n var c1v = vec3<f32>(0.0);\n var c2v = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (*pixels)[k] - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec3<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec3<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n var mean = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n mean = mean + c;\n }\n mean = mean * (1.0 / 16.0);\n\n // Inset the bounding box by ~half an RGB565 cell (1/16) so the quantized\n // 4-color palette covers the real data range more tightly (stb_dxt heuristic).\n let inset = (bb_max - bb_min) / 16.0;\n let bbox_hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n let bbox_lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n\n var best: Best;\n best.err = 1e30;\n\n if (QUALITY_HIGH != 0u) {\n // Seed from the principal colour axis: project all texels onto it, take the\n // extreme projections as endpoints, inset along the axis. Then also try the\n // bbox seed and keep whichever family yields the lower error.\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n let pad = (t_max - t_min) / 16.0;\n let pca_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n let pca_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n fit_from_endpoints(&pixels, pca_hi, pca_lo, 3u, &best);\n }\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 3u, &best);\n } else {\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 1u, &best);\n }\n\n var indices: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices = indices | ((best.indices[k] & 3u) << (k * 2u));\n }\n\n let out = block_index * 2u;\n dst[out] = best.c0 | (best.c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
|
|
327
|
+
var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// We always force the 4-color mode (color0 > color1, numeric 16-bit):\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): bounding-box endpoints inset by ~half a 565 cell, then\n// ONE fused pass that projects every pixel onto the decoded-endpoint line\n// (the 4 palette entries are colinear and evenly spaced, so the nearest\n// entry is the rounded projection \u2014 no 4-entry search) while accumulating\n// the least-squares refit sums; the refit endpoints are re-quantised and a\n// final projection pass assigns the indices, packed on the fly.\n// high (1): endpoints are seeded from the block's principal colour axis\n// (covariance power-iteration) as well as the bbox diagonal, each refined by\n// several least-squares passes with full 4-entry searches; the lower-error\n// family wins. Mirrors bc1_ref.ts.\n\n// 0 = fast (default), 1 = high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = (c >> 11u) & 31u;\n let g = (c >> 5u) & 63u;\n let b = c & 31u;\n // 5/6-bit -> 8-bit: (x*527+23)>>6 (6-bit: 259/33) \u2014 round-to-nearest\n // scaling, matching bc1_ref.ts and typical hardware decoders (white ->\n // 255). Integer u32 math is exact. NOTE: this is NOT plain bit-replication\n // ((x<<3)|(x>>2)) \u2014 they differ for some codes (e.g. 5-bit 3 -> 25 vs 24).\n // Selecting indices against this palette is what makes the encoder agree\n // with what the GPU will actually sample.\n let r8 = (r * 527u + 23u) >> 6u;\n let g8 = (g * 259u + 33u) >> 6u;\n let b8 = (b * 527u + 23u) >> 6u;\n return vec3<f32>(vec3<u32>(r8, g8, b8)) / 255.0;\n}\n\n// 4-color-mode interpolation weights: palette[j] = wa(j)*c0 + wb(j)*c1.\nfn wa(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 2.0 / 3.0; }\n default: { return 1.0 / 3.0; } // case 3u\n }\n}\nfn wb(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 3.0; }\n default: { return 2.0 / 3.0; } // case 3u\n }\n}\n\nfn build_palette(c0: u32, c1: u32, pal: ptr<function, array<vec3<f32>, 4>>) {\n let p0 = from565(c0);\n let p1 = from565(c1);\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n (*pal)[j] = wa(j) * p0 + wb(j) * p1;\n }\n}\n\n// Assign each of the 16 pixels its nearest palette entry (full 4-entry L2),\n// writing indices into `out_idx` and returning the total squared error.\nfn assign_indices(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n pal: ptr<function, array<vec3<f32>, 4>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> f32 {\n var err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let c = (*pixels)[k];\n var best_j: u32 = 0u;\n var best_d: f32 = 1e30;\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n let d = (*pal)[j] - c;\n let d2 = dot(d, d);\n if (d2 < best_d) {\n best_d = d2;\n best_j = j;\n }\n }\n (*out_idx)[k] = best_j;\n err = err + best_d;\n }\n return err;\n}\n\n// One least-squares refit pass: solve the 2x2 normal equations for the endpoint\n// colours that minimise \u03A3\u2016wa\xB7e0 + wb\xB7e1 \u2212 c\u2016\xB2 under the current indices. The\n// three channels share the scalar sums, so it's one 2x2 solve with vec3 RHS.\nstruct RefitResult { e0: vec3<f32>, e1: vec3<f32>, valid: bool };\nfn refit(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec3<f32> = vec3<f32>(0.0);\n var sBV: vec3<f32> = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = wa((*indices)[k]);\n let b = wb((*indices)[k]);\n let v = (*pixels)[k];\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n var out: RefitResult;\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n out.e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.valid = true;\n return out;\n}\n\n// Candidate solution tracked across endpoint seeds / refit passes.\nstruct Best { c0: u32, c1: u32, indices: array<u32, 16>, err: f32 };\n\n// Quantize (hi, lo) to 565, force 4-color mode, assign indices, then refine with\n// up to `max_refits` least-squares passes. Commits to `*best` only on strict\n// improvement.\nfn fit_from_endpoints(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n hi: vec3<f32>,\n lo: vec3<f32>,\n max_refits: u32,\n best: ptr<function, Best>,\n) {\n var c0 = to565(hi);\n var c1 = to565(lo);\n // 4-color mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n\n var pal: array<vec3<f32>, 4>;\n var idx: array<u32, 16>;\n build_palette(c0, c1, &pal);\n var err = assign_indices(pixels, &pal, &idx);\n if (err < (*best).err) {\n (*best).c0 = c0; (*best).c1 = c1; (*best).indices = idx; (*best).err = err;\n }\n\n for (var rp: u32 = 0u; rp < max_refits; rp = rp + 1u) {\n let r = refit(pixels, &idx);\n if (!r.valid) { break; }\n var nc0 = to565(r.e0);\n var nc1 = to565(r.e1);\n // A refit that flips/equalises the endpoints would change decode mode;\n // keep 4-color mode, and stop once it stops moving.\n if (nc0 < nc1) { let t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 == nc1) { break; }\n if (nc0 == c0 && nc1 == c1) { break; }\n build_palette(nc0, nc1, &pal);\n let nerr = assign_indices(pixels, &pal, &idx);\n c0 = nc0; c1 = nc1; err = nerr;\n if (nerr < (*best).err) {\n (*best).c0 = nc0; (*best).c1 = nc1; (*best).indices = idx; (*best).err = nerr;\n }\n }\n}\n\n// Principal colour axis via covariance power-iteration, seeded with the bbox\n// diagonal. Returns a unit axis, or vec3(0) for a degenerate (constant) block.\nfn principal_axis(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\n // Symmetric 3x3 covariance, stored as its three rows.\n var c0v = vec3<f32>(0.0);\n var c1v = vec3<f32>(0.0);\n var c2v = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (*pixels)[k] - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec3<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec3<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n var mean = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n mean = mean + c;\n }\n mean = mean * (1.0 / 16.0);\n\n // Inset the bounding box by ~half an RGB565 cell (1/16) so the quantized\n // 4-color palette covers the real data range more tightly (stb_dxt heuristic).\n let inset = (bb_max - bb_min) / 16.0;\n let bbox_hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n let bbox_lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n\n if (QUALITY_HIGH == 0u) {\n // -------- fast: projection + fused LSQ refit + reprojection --------\n var c0 = to565(bbox_hi);\n var c1 = to565(bbox_lo);\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n let p0 = from565(c0);\n let p1 = from565(c1);\n\n // Fused pass: projection assignment + LSQ sums + the seed solution's\n // packed indices and squared error. Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922,\n // 2\u21923, 3\u21921 (c1); as a packed LUT: (0x78 >> 2L) & 3.\n var idx_bits: u32 = 0u;\n let dir = p1 - p0;\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let inv = 3.0 / dd;\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV = vec3<f32>(0.0); var sBV = vec3<f32>(0.0);\n var s_min = 3.0; var s_max = 0.0;\n var seed_err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = pixels[k];\n let s = clamp(floor(dot(v - p0, dir) * inv + 0.5), 0.0, 3.0);\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n let e = v - (p0 + b * dir);\n seed_err = seed_err + dot(e, e);\n idx_bits = idx_bits | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));\n }\n let det = sAA * sBB - sAB * sAB;\n // Refit only on a well-conditioned system: when every pixel lands on\n // ONE level (flat blocks \u2014 the 4-colour nudge forces c0 \u2260 c1 even\n // then) the system is rank-1 and det/numerators are pure float noise;\n // the solve would return garbage endpoints. With \u22652 levels\n // det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 ~1.67, so 1e-3 is a safe guard.\n if (s_min < s_max && abs(det) > 1e-3) {\n let e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n let e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n var nc0 = to565(e0);\n var nc1 = to565(e1);\n if (nc0 == nc1) {\n if (nc1 > 0u) { nc1 = nc1 - 1u; } else { nc0 = nc0 + 1u; }\n } else if (nc0 < nc1) {\n let t = nc0; nc0 = nc1; nc1 = t;\n }\n let np0 = from565(nc0);\n let np1 = from565(nc1);\n let ndir = np1 - np0;\n let ndd = dot(ndir, ndir);\n if (ndd > 0.0 && !(nc0 == c0 && nc1 == c1)) {\n // Reproject against the refit endpoints and accept them only if\n // the block error actually decreases (the refit minimises a\n // continuous objective; after 565 quantisation it can lose).\n let ninv = 3.0 / ndd;\n var refit_err: f32 = 0.0;\n var nidx_bits: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = pixels[k];\n let s = clamp(floor(dot(v - np0, ndir) * ninv + 0.5), 0.0, 3.0);\n let e = v - (np0 + s * (1.0 / 3.0) * ndir);\n refit_err = refit_err + dot(e, e);\n nidx_bits = nidx_bits | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));\n }\n if (refit_err < seed_err) {\n c0 = nc0; c1 = nc1;\n idx_bits = nidx_bits;\n }\n }\n }\n }\n\n let out = block_index * 2u;\n dst[out] = c0 | (c1 << 16u);\n dst[out + 1u] = idx_bits;\n return;\n }\n\n // ------------------------------ high --------------------------------- //\n var best: Best;\n best.err = 1e30;\n\n // Seed from the principal colour axis: project all texels onto it, take the\n // extreme projections as endpoints, inset along the axis. Then also try the\n // bbox seed and keep whichever family yields the lower error.\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n let pad = (t_max - t_min) / 16.0;\n let pca_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n let pca_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n fit_from_endpoints(&pixels, pca_hi, pca_lo, 3u, &best);\n }\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 3u, &best);\n\n var indices: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices = indices | ((best.indices[k] & 3u) << (k * 2u));\n }\n\n let out = block_index * 2u;\n dst[out] = best.c0 | (best.c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
|
|
328
|
+
|
|
329
|
+
// src/bc1_fast_f16.wgsl
|
|
330
|
+
var bc1_fast_f16_default = `// bc1 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
331
|
+
//
|
|
332
|
+
// BC1 quantises endpoints to RGB565 anyway, so the fast path needs nothing
|
|
333
|
+
// f32 can do that f16 can't \u2014 all projection / least-squares math runs in
|
|
334
|
+
// f16 ([0,1] domain). The algorithm is the same family as the BC7/ASTC fast
|
|
335
|
+
// paths rather than a port of bc1.wgsl's fast branch:
|
|
336
|
+
//
|
|
337
|
+
// 1. bbox endpoints, inset by ~half a 565 cell (stb_dxt heuristic)
|
|
338
|
+
// 2. quantise to 565, force 4-colour mode (c0 > c1)
|
|
339
|
+
// 3. ONE fused pass: project every pixel onto the decoded-endpoint line
|
|
340
|
+
// (the 4 palette entries are colinear and evenly spaced, so the nearest
|
|
341
|
+
// entry is the rounded projection \u2014 no 4-entry search) while
|
|
342
|
+
// accumulating the least-squares refit sums, the seed solution's packed
|
|
343
|
+
// indices and its squared error
|
|
344
|
+
// 4. re-quantise the refit endpoints, reproject (indices packed on the
|
|
345
|
+
// fly), and accept the refit only if the block error decreases \u2014
|
|
346
|
+
// flat/single-level blocks skip this pass entirely
|
|
347
|
+
//
|
|
348
|
+
// vs the pre-projection fast branch (build palette + full 4-entry search \xD7 3
|
|
349
|
+
// passes + refit sums pass) this does roughly half the ALU per block. The
|
|
350
|
+
// 565 decode uses exact integer math, so the palette base points are exact.
|
|
351
|
+
//
|
|
352
|
+
// The host selects this module only when the device reports shader-f16,
|
|
353
|
+
// falling back to bc1.wgsl otherwise. "high" never uses this.
|
|
354
|
+
enable f16;
|
|
355
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
356
|
+
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
357
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
358
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
359
|
+
alias h = f16;
|
|
360
|
+
alias h3 = vec3<f16>;
|
|
361
|
+
|
|
362
|
+
fn to565(c: h3) -> u32 {
|
|
363
|
+
let r = u32(clamp(floor(c.r * h(31.0) + h(0.5)), h(0.0), h(31.0)));
|
|
364
|
+
let g = u32(clamp(floor(c.g * h(63.0) + h(0.5)), h(0.0), h(63.0)));
|
|
365
|
+
let b = u32(clamp(floor(c.b * h(31.0) + h(0.5)), h(0.0), h(31.0)));
|
|
366
|
+
return (r << 11u) | (g << 5u) | b;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Decode a 565 endpoint to [0,1]: (x*527+23)>>6 (6-bit: 259/33) \u2014
|
|
370
|
+
// round-to-nearest scaling, matching bc1_ref.ts / bc1.wgsl and typical
|
|
371
|
+
// hardware decoders. Exact in u32 integer math (f16 could not evaluate the
|
|
372
|
+
// products exactly).
|
|
373
|
+
fn from565(c: u32) -> h3 {
|
|
374
|
+
let r = (c >> 11u) & 31u;
|
|
375
|
+
let g = (c >> 5u) & 63u;
|
|
376
|
+
let b = c & 31u;
|
|
377
|
+
let r8 = (r * 527u + 23u) >> 6u;
|
|
378
|
+
let g8 = (g * 259u + 33u) >> 6u;
|
|
379
|
+
let b8 = (b * 527u + 23u) >> 6u;
|
|
380
|
+
return h3(vec3<f32>(vec3<u32>(r8, g8, b8))) * h(1.0 / 255.0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Force 4-colour mode: c0 > c1 strictly.
|
|
384
|
+
fn order565(a: u32, b: u32) -> vec2<u32> {
|
|
385
|
+
var c0 = a; var c1 = b;
|
|
386
|
+
if (c0 == c1) {
|
|
387
|
+
if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }
|
|
388
|
+
} else if (c0 < c1) {
|
|
389
|
+
let t = c0; c0 = c1; c1 = t;
|
|
390
|
+
}
|
|
391
|
+
return vec2<u32>(c0, c1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@compute @workgroup_size(8, 8, 1)
|
|
395
|
+
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
396
|
+
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
397
|
+
let bi = gid.y * params.blocks_x + gid.x;
|
|
398
|
+
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
399
|
+
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
400
|
+
|
|
401
|
+
var pix: array<h3, 16>;
|
|
402
|
+
var mn = h3(1.0);
|
|
403
|
+
var mxv = h3(0.0);
|
|
404
|
+
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
405
|
+
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
406
|
+
let px = h3(textureLoad(src_tex, p, 0).rgb);
|
|
407
|
+
pix[i] = px; mn = min(mn, px); mxv = max(mxv, px);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Inset bbox by ~half a 565 cell so the quantised palette hugs the data.
|
|
411
|
+
let inset = (mxv - mn) * h(1.0 / 16.0);
|
|
412
|
+
let seed = order565(to565(clamp(mxv - inset, h3(0.0), h3(1.0))), to565(clamp(mn + inset, h3(0.0), h3(1.0))));
|
|
413
|
+
var c0 = seed.x;
|
|
414
|
+
var c1 = seed.y;
|
|
415
|
+
let p0 = from565(c0);
|
|
416
|
+
let p1 = from565(c1);
|
|
417
|
+
|
|
418
|
+
// Fused pass: projection assignment + LSQ normal-equation sums + the seed
|
|
419
|
+
// solution's packed indices and squared error. Levels s run 0..3 along
|
|
420
|
+
// p0\u2192p1 (palette = p0, p0+\u2153d, p0+\u2154d, p1 \u2014 colinear, evenly spaced, so
|
|
421
|
+
// rounding the projection IS the nearest-entry search). Level \u2192 BC1 index:
|
|
422
|
+
// 0\u21920 (c0), 1\u21922 (\u2154c0+\u2153c1), 2\u21923, 3\u21921 (c1); as a packed LUT: (0x78 >> 2L) & 3.
|
|
423
|
+
var indices: u32 = 0u;
|
|
424
|
+
let dir = p1 - p0;
|
|
425
|
+
let dd = dot(dir, dir);
|
|
426
|
+
if (dd > h(0.0)) {
|
|
427
|
+
let inv = h(3.0) / dd;
|
|
428
|
+
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
429
|
+
var sAV = h3(0.0); var sBV = h3(0.0);
|
|
430
|
+
var s_min = h(3.0); var s_max = h(0.0);
|
|
431
|
+
var seed_err = h(0.0);
|
|
432
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
433
|
+
let v = pix[k];
|
|
434
|
+
let s = clamp(floor(dot(v - p0, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
435
|
+
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
436
|
+
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
437
|
+
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
438
|
+
sAV = sAV + a * v; sBV = sBV + b * v;
|
|
439
|
+
let e = v - (p0 + b * dir);
|
|
440
|
+
seed_err = seed_err + dot(e, e);
|
|
441
|
+
indices = indices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));
|
|
442
|
+
}
|
|
443
|
+
let det = sAA * sBB - sAB * sAB;
|
|
444
|
+
// Refit only on a well-conditioned system. When every pixel lands on ONE
|
|
445
|
+
// level (flat / near-flat blocks \u2014 note the 4-colour-mode nudge forces
|
|
446
|
+
// c0 \u2260 c1 even for perfectly flat blocks) the system is rank-1: det is 0
|
|
447
|
+
// in exact math and the f16-accumulated det/numerators are pure rounding
|
|
448
|
+
// noise, so the solve returns garbage endpoints. With \u22652 distinct levels
|
|
449
|
+
// det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15\xB7(1/3)\xB2 \u2248 1.67, far above the ~0.05 f16
|
|
450
|
+
// noise floor \u2014 0.5 separates the two regimes cleanly.
|
|
451
|
+
if (s_min < s_max && abs(det) > h(0.5)) {
|
|
452
|
+
let e0 = clamp((sBB * sAV - sAB * sBV) / det, h3(0.0), h3(1.0));
|
|
453
|
+
let e1 = clamp((sAA * sBV - sAB * sAV) / det, h3(0.0), h3(1.0));
|
|
454
|
+
let refit = order565(to565(e0), to565(e1));
|
|
455
|
+
let np0 = from565(refit.x);
|
|
456
|
+
let np1 = from565(refit.y);
|
|
457
|
+
let ndir = np1 - np0;
|
|
458
|
+
let ndd = dot(ndir, ndir);
|
|
459
|
+
if (ndd > h(0.0) && !(refit.x == c0 && refit.y == c1)) {
|
|
460
|
+
// Reproject against the refit endpoints and accept them only if the
|
|
461
|
+
// block's squared error actually decreases (the refit minimises a
|
|
462
|
+
// continuous objective; after 565 quantisation it can lose).
|
|
463
|
+
let ninv = h(3.0) / ndd;
|
|
464
|
+
var refit_err = h(0.0);
|
|
465
|
+
var nindices: u32 = 0u;
|
|
466
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
467
|
+
let v = pix[k];
|
|
468
|
+
let s = clamp(floor(dot(v - np0, ndir) * ninv + h(0.5)), h(0.0), h(3.0));
|
|
469
|
+
let e = v - (np0 + s * h(1.0 / 3.0) * ndir);
|
|
470
|
+
refit_err = refit_err + dot(e, e);
|
|
471
|
+
nindices = nindices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));
|
|
472
|
+
}
|
|
473
|
+
if (refit_err < seed_err) {
|
|
474
|
+
c0 = refit.x; c1 = refit.y;
|
|
475
|
+
indices = nindices;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let o = bi * 2u;
|
|
482
|
+
dst[o] = c0 | (c1 << 16u);
|
|
483
|
+
dst[o + 1u] = indices;
|
|
484
|
+
}
|
|
485
|
+
`;
|
|
291
486
|
|
|
292
487
|
// src/BC1Encoder.ts
|
|
293
488
|
var BC1Encoder = class extends Encoder {
|
|
@@ -308,16 +503,106 @@ var BC1Encoder = class extends Encoder {
|
|
|
308
503
|
wgslSource() {
|
|
309
504
|
return bc1_default;
|
|
310
505
|
}
|
|
506
|
+
wgslSourceFastF16() {
|
|
507
|
+
return bc1_fast_f16_default;
|
|
508
|
+
}
|
|
311
509
|
gpuTextureFormat({ colorSpace }) {
|
|
312
510
|
return colorSpace === "srgb" ? "bc1-rgba-unorm-srgb" : "bc1-rgba-unorm";
|
|
313
511
|
}
|
|
314
512
|
};
|
|
315
513
|
|
|
316
514
|
// src/bc5.wgsl
|
|
317
|
-
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";
|
|
515
|
+
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 + O(1) projection assignment per texel.\n// The 8-entry palette in 6-interp mode is colinear and EVENLY spaced from\n// r0 to r1 (levels 0..7 in palette order 0,2,3,4,5,6,7,1), so the nearest\n// entry is the rounded projection onto the r0\u2192r1 axis \u2014 no 8-entry\n// search, and the 3-bit indices are packed on the fly. The LSQ refit is\n// skipped (buys only ~0.36 dB).\n// high (1): full nearest search + refit, matches 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 if (QUALITY_HIGH == 0u) {\n // -------- fast: projection assignment, indices packed on the fly ----\n // level = round(7\xB7(v \u2212 r0)/(r1 \u2212 r0)); level \u2192 BC4 index LUT (0,2,3,4,\n // 5,6,7,1) packed as 3-bit entries in 0x3F58D0. Pixel k's 3 bits start\n // at bit 3k+16 of the (w0,w1) pair (bytes 0..1 are the endpoints).\n let r0f = f32(r0) / 255.0;\n let scale = 7.0 / (f32(r1) / 255.0 - r0f);\n var w0 = r0 | (r1 << 8u);\n var w1 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let L = u32(clamp(floor(((*values)[k] - r0f) * scale + 0.5), 0.0, 7.0));\n let idx = (0x3F58D0u >> (L * 3u)) & 7u;\n let bit = 3u * k + 16u;\n if (bit <= 29u) {\n w0 = w0 | (idx << bit);\n } else if (bit >= 32u) {\n w1 = w1 | (idx << (bit - 32u));\n } else {\n // k = 5 straddles the word boundary (bits 31..33).\n w0 = w0 | (idx << bit);\n w1 = w1 | (idx >> (32u - bit));\n }\n }\n return vec2<u32>(w0, w1);\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";
|
|
318
516
|
|
|
319
517
|
// src/bc5_fast_f16.wgsl
|
|
320
|
-
var bc5_fast_f16_default =
|
|
518
|
+
var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
519
|
+
// Two BC4 halves (R and G), no refit \u2014 same output family as bc5.wgsl's fast
|
|
520
|
+
// branch, tuned for throughput:
|
|
521
|
+
//
|
|
522
|
+
// \u2022 The 8-entry palette in 6-interpolation mode is COLINEAR and EVENLY
|
|
523
|
+
// spaced from r0 to r1 (levels 0..7 in palette order 0,2,3,4,5,6,7,1),
|
|
524
|
+
// so the nearest entry is the rounded projection of v onto the r0\u2192r1
|
|
525
|
+
// axis \u2014 O(1) per pixel instead of an 8-entry distance search.
|
|
526
|
+
// \u2022 Math runs in the exact-integer [0,255] f16 domain: endpoints and pixel
|
|
527
|
+
// values are whole numbers \u2264 255 (exact in f16), so the only rounding is
|
|
528
|
+
// the single 1/(r1\u2212r0) division.
|
|
529
|
+
// \u2022 3-bit indices are packed into the 48-bit field on the fly \u2014 no
|
|
530
|
+
// array<u32,16> private array and no separate packing loop.
|
|
531
|
+
//
|
|
532
|
+
// Level \u2192 BC4 index (0\u2192r0 ... 7\u2192r1): 0,2,3,4,5,6,7,1 \u2014 packed 3-bit LUT
|
|
533
|
+
// 0x3F58D0 = sum(idx[L] << 3L).
|
|
534
|
+
//
|
|
535
|
+
// The host selects this module only when the device reports shader-f16,
|
|
536
|
+
// falling back to bc5.wgsl otherwise. "high" never uses this.
|
|
537
|
+
enable f16;
|
|
538
|
+
alias h = f16;
|
|
539
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
540
|
+
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
541
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
542
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
543
|
+
|
|
544
|
+
// Encode one channel (16 values in exact-integer [0,255] f16) to a BC4 half.
|
|
545
|
+
fn encode_bc4(values: ptr<function, array<h, 16>>) -> vec2<u32> {
|
|
546
|
+
var vmin = h(255.0);
|
|
547
|
+
var vmax = h(0.0);
|
|
548
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
549
|
+
vmin = min(vmin, (*values)[k]);
|
|
550
|
+
vmax = max(vmax, (*values)[k]);
|
|
551
|
+
}
|
|
552
|
+
var r0 = u32(vmax); // values are exact integers \u2014 no rounding needed
|
|
553
|
+
var r1 = u32(vmin);
|
|
554
|
+
if (r0 == r1) {
|
|
555
|
+
// Flat block: nudge to keep 6-interp mode (r0 > r1 strictly).
|
|
556
|
+
if (r1 > 0u) { r1 = r1 - 1u; } else { r0 = r0 + 1u; }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Projection assignment: level = round(7\xB7(v \u2212 r0)/(r1 \u2212 r0)), clamped.
|
|
560
|
+
// |v \u2212 r0| \u2264 r0 \u2212 r1 for every in-block value, so the product stays \u2264 7.
|
|
561
|
+
let r0f = h(f32(r0));
|
|
562
|
+
let scale = h(7.0) / (h(f32(r1)) - r0f);
|
|
563
|
+
var lo: u32 = 0u;
|
|
564
|
+
var hi = r0 | (r1 << 8u); // endpoint bytes live in the low 16 bits of u32[0]
|
|
565
|
+
// Pixel k's 3-bit index starts at bit 3k of the 48-bit field, i.e. bit
|
|
566
|
+
// 3k+16 of u32[0] for k \u2264 4, straddling into u32[1] from k = 5 (bit 31).
|
|
567
|
+
var w0 = hi;
|
|
568
|
+
var w1 = 0u;
|
|
569
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
570
|
+
let L = u32(clamp(floor(((*values)[k] - r0f) * scale + h(0.5)), h(0.0), h(7.0)));
|
|
571
|
+
let idx = (0x3F58D0u >> (L * 3u)) & 7u;
|
|
572
|
+
let bit = 3u * k + 16u;
|
|
573
|
+
if (bit <= 29u) {
|
|
574
|
+
w0 = w0 | (idx << bit);
|
|
575
|
+
} else if (bit >= 32u) {
|
|
576
|
+
w1 = w1 | (idx << (bit - 32u));
|
|
577
|
+
} else {
|
|
578
|
+
// k = 5 straddles the word boundary (bits 31..33).
|
|
579
|
+
w0 = w0 | (idx << bit);
|
|
580
|
+
w1 = w1 | (idx >> (32u - bit));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return vec2<u32>(w0, w1);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
@compute @workgroup_size(8, 8, 1)
|
|
587
|
+
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
588
|
+
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
589
|
+
let bi = gid.y * params.blocks_x + gid.x;
|
|
590
|
+
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
591
|
+
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
592
|
+
var rv: array<h, 16>;
|
|
593
|
+
var gv: array<h, 16>;
|
|
594
|
+
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
595
|
+
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
596
|
+
let c = textureLoad(src_tex, p, 0);
|
|
597
|
+
rv[i] = h(c.r * 255.0);
|
|
598
|
+
gv[i] = h(c.g * 255.0);
|
|
599
|
+
}
|
|
600
|
+
let rb = encode_bc4(&rv);
|
|
601
|
+
let gb = encode_bc4(&gv);
|
|
602
|
+
let o = bi * 4u;
|
|
603
|
+
dst[o] = rb.x; dst[o + 1u] = rb.y; dst[o + 2u] = gb.x; dst[o + 3u] = gb.y;
|
|
604
|
+
}
|
|
605
|
+
`;
|
|
321
606
|
|
|
322
607
|
// src/BC5Encoder.ts
|
|
323
608
|
var BC5Encoder = class extends Encoder {
|
|
@@ -347,87 +632,10 @@ var BC5Encoder = class extends Encoder {
|
|
|
347
632
|
};
|
|
348
633
|
|
|
349
634
|
// src/bc7.wgsl
|
|
350
|
-
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";
|
|
635
|
+
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 one fused pass that projects\n// each pixel onto the endpoint line (the 16 palette entries are colinear,\n// so the nearest index is the rounded projection \u2014 no palette build, no\n// 16-entry search) while accumulating the least-squares refit sums, then\n// a reprojection against the quantised refit endpoints for the final\n// indices, packed on the fly into two nibble words.\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// matches bc7_ref.ts up to FP tie-breaks.\n//\n// The fast/high branch is selected at pipeline-compile time, so the driver\n// eliminates the unused code entirely.\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// The block is assembled with straight-line constant shifts (see the layout\n// summary in bc7_fast_f16.wgsl) \u2014 a generic write_bits() helper's dynamic\n// word indexing keeps the output array out of registers.\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// One pass over the block: project every pixel onto the e0\u2192e1 line and\n// accumulate the least-squares normal-equation sums; solve for the refit\n// endpoints (in 8-bit space). Indices are not produced here \u2014 the caller\n// reprojects against the quantised refit endpoints anyway.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_fit(pixels: ptr<function, array<vec4<i32>, 16>>, e0: vec4<i32>, e1: vec4<i32>) -> Fit {\n var out: Fit;\n out.valid = false;\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd == 0.0) { return out; }\n let e0f = vec4<f32>(e0);\n let inv = 15.0 / 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 var s_min = 15.0; var s_max = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = vec4<f32>((*pixels)[k]);\n let s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 15.0);\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 15.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det and the numerators are pure float rounding noise and the\n // solve returns garbage endpoints. With \u22652 levels det \u2265 15/225 \u2248 0.067.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { 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// ------------------------------- 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 // Both branches produce: 7-bit endpoints + p-bits, and the 16 4-bit indices\n // packed LSB-first into two nibble words (pixel k \u2192 bits 4k..4k+3).\n var e0_7: vec4<i32>;\n var e1_7: vec4<i32>;\n var p0: u32;\n var p1: u32;\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\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;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n ilo = ilo | (best.indices[k] << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n ihi = ihi | (best.indices[k] << ((k - 8u) * 4u));\n }\n } else {\n // Seed the fused LSQ fit from the raw bbox, then quantise the refit\n // endpoints and reproject for the final indices.\n let r = proj_fit(&pixels, lo, hi);\n var ep0: Ep;\n var ep1: Ep;\n if (r.valid) { ep0 = pick_ep(r.e0); ep1 = pick_ep(r.e1); }\n else { ep0 = pick_ep(lo); ep1 = pick_ep(hi); }\n let dir = vec4<f32>(ep1.eight - ep0.eight);\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let e0f = vec4<f32>(ep0.eight);\n let inv = 15.0 / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ilo = ilo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ihi = ihi | (u32(s) << ((k - 8u) * 4u));\n }\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. Swapping endpoints reflects\n // every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.\n if ((ilo & 0x8u) != 0u) {\n let t7 = e0_7; e0_7 = e1_7; e1_7 = t7;\n let tp = p0; p0 = p1; p1 = tp;\n ilo = ~ilo; ihi = ~ihi;\n }\n\n // Straight-line mode-6 packing (see layout at the top of the file).\n let e0 = vec4<u32>(e0_7);\n let e1 = vec4<u32>(e1_7);\n let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (p0 << 31u);\n let w2 = p1 | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n let w3 = ihi;\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
351
636
|
|
|
352
637
|
// src/bc7_fast_f16.wgsl
|
|
353
|
-
var bc7_fast_f16_default =
|
|
354
|
-
// Identical algorithm to the f32 fast path in bc7.wgsl, but the projection +
|
|
355
|
-
// least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput
|
|
356
|
-
// (e.g. Apple) this is ~2x faster at the same quality; endpoints are still
|
|
357
|
-
// quantised to exact 8-bit. The host selects this module only when the device
|
|
358
|
-
// reports shader-f16, falling back to bc7.wgsl otherwise. "high" never uses this.
|
|
359
|
-
//
|
|
360
|
-
// BC7 mode 6 fast path in f16 (Apple GPUs run f16 at 2x). All math in the [0,1]
|
|
361
|
-
// domain so dot products stay well under f16's range; endpoints quantised to
|
|
362
|
-
// 8-bit at the end. Same bbox seed + projection + fused LSQ refit + reproject.
|
|
363
|
-
enable f16;
|
|
364
|
-
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
365
|
-
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
366
|
-
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
367
|
-
@group(0) @binding(2) var<uniform> params: Params;
|
|
368
|
-
alias h = f16;
|
|
369
|
-
alias h4 = vec4<f16>;
|
|
370
|
-
struct Ep { seven: vec4<i32>, eight: h4, p: u32 };
|
|
371
|
-
// quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit; returns 8-bit eff in [0,1].
|
|
372
|
-
fn pick_ep(ideal01: h4) -> Ep {
|
|
373
|
-
let ideal = ideal01 * h(255.0);
|
|
374
|
-
let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0
|
|
375
|
-
let e0 = q0 * h(2.0);
|
|
376
|
-
let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1
|
|
377
|
-
let e1 = q1 * h(2.0) + h(1.0);
|
|
378
|
-
let d0 = e0 - ideal; let d1 = e1 - ideal;
|
|
379
|
-
if (dot(d1,d1) < dot(d0,d0)) { return Ep(vec4<i32>(q1), e1 * h(1.0/255.0), 1u); }
|
|
380
|
-
return Ep(vec4<i32>(q0), e0 * h(1.0/255.0), 0u);
|
|
381
|
-
}
|
|
382
|
-
struct Fit { e0: h4, e1: h4, valid: bool };
|
|
383
|
-
fn proj_assign(pix: ptr<function, array<h4,16>>, e0: h4, e1: h4, out_idx: ptr<function, array<u32,16>>, fit: bool) -> Fit {
|
|
384
|
-
var out: Fit; let dir = e1 - e0; let dd = dot(dir,dir);
|
|
385
|
-
if (dd == h(0.0)) { for(var k:u32=0u;k<16u;k=k+1u){(*out_idx)[k]=0u;} out.valid=false; return out; }
|
|
386
|
-
let inv = h(15.0) / dd;
|
|
387
|
-
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);
|
|
388
|
-
for(var k:u32=0u;k<16u;k=k+1u){
|
|
389
|
-
let v=(*pix)[k];
|
|
390
|
-
let s = clamp(floor(dot(v - e0, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
391
|
-
(*out_idx)[k] = u32(s);
|
|
392
|
-
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; }
|
|
393
|
-
}
|
|
394
|
-
if(!fit){ out.valid=false; return out; }
|
|
395
|
-
let det = sAA*sBB - sAB*sAB; if (abs(det) < h(0.0001)) { out.valid=false; return out; }
|
|
396
|
-
out.e0 = clamp((sBB*sAV - sAB*sBV)/det, h4(0.0), h4(1.0));
|
|
397
|
-
out.e1 = clamp((sAA*sBV - sAB*sAV)/det, h4(0.0), h4(1.0));
|
|
398
|
-
out.valid=true; return out;
|
|
399
|
-
}
|
|
400
|
-
fn write_bits(block: ptr<function, array<u32,4>>, pos: u32, n_bits: u32, value: u32) {
|
|
401
|
-
let v=value&((1u<<n_bits)-1u); let wl=pos/32u; let bl=pos%32u; let il=min(n_bits,32u-bl);
|
|
402
|
-
let ml=((1u<<il)-1u)<<bl; (*block)[wl]=((*block)[wl]&~ml)|((v<<bl)&ml);
|
|
403
|
-
if(il<n_bits){ let ih=n_bits-il; let mh=(1u<<ih)-1u; (*block)[wl+1u]=((*block)[wl+1u]&~mh)|((v>>il)&mh); }
|
|
404
|
-
}
|
|
405
|
-
@compute @workgroup_size(8,8,1)
|
|
406
|
-
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
407
|
-
if(gid.x>=params.blocks_x||gid.y>=params.blocks_y){return;}
|
|
408
|
-
let bi=gid.y*params.blocks_x+gid.x;
|
|
409
|
-
let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);
|
|
410
|
-
var pix: array<h4,16>; var lo=h4(1.0); var hi=h4(0.0);
|
|
411
|
-
for(var i:u32=0u;i<16u;i=i+1u){
|
|
412
|
-
let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx);
|
|
413
|
-
let px=h4(textureLoad(src_tex,p,0)); pix[i]=px; lo=min(lo,px); hi=max(hi,px);
|
|
414
|
-
}
|
|
415
|
-
var ep0=pick_ep(lo); var ep1=pick_ep(hi); var indices: array<u32,16>;
|
|
416
|
-
let r=proj_assign(&pix,ep0.eight,ep1.eight,&indices,true);
|
|
417
|
-
if(r.valid){ ep0=pick_ep(r.e0); ep1=pick_ep(r.e1); proj_assign(&pix,ep0.eight,ep1.eight,&indices,false); }
|
|
418
|
-
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];} }
|
|
419
|
-
var block: array<u32,4>; block[0]=0u;block[1]=0u;block[2]=0u;block[3]=0u; var pos:u32=0u;
|
|
420
|
-
write_bits(&block,pos,7u,0x40u);pos=pos+7u;
|
|
421
|
-
write_bits(&block,pos,7u,u32(ep0.seven.x));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.x));pos=pos+7u;
|
|
422
|
-
write_bits(&block,pos,7u,u32(ep0.seven.y));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.y));pos=pos+7u;
|
|
423
|
-
write_bits(&block,pos,7u,u32(ep0.seven.z));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.z));pos=pos+7u;
|
|
424
|
-
write_bits(&block,pos,7u,u32(ep0.seven.w));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.w));pos=pos+7u;
|
|
425
|
-
write_bits(&block,pos,1u,ep0.p);pos=pos+1u; write_bits(&block,pos,1u,ep1.p);pos=pos+1u;
|
|
426
|
-
write_bits(&block,pos,3u,indices[0]&0x7u);pos=pos+3u;
|
|
427
|
-
for(var k:u32=1u;k<16u;k=k+1u){write_bits(&block,pos,4u,indices[k]&0xFu);pos=pos+4u;}
|
|
428
|
-
let o=bi*4u; dst[o]=block[0];dst[o+1u]=block[1];dst[o+2u]=block[2];dst[o+3u]=block[3];
|
|
429
|
-
}
|
|
430
|
-
`;
|
|
638
|
+
var bc7_fast_f16_default = '// bc7 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).\n// Same algorithm family as the f32 fast path in bc7.wgsl (bbox seed \u2192\n// projection-based index assignment with a fused least-squares refit \u2192\n// reproject), tuned for throughput:\n//\n// \u2022 All projection / refit math in f16 ([0,1] domain, so dot products stay\n// well inside f16 range). ~2\xD7 ALU throughput on f16-capable GPUs.\n// \u2022 The LSQ seed pass projects against the RAW bbox endpoints \u2014 quantising\n// the seed first (pick_ep) costs two extra quantisation searches and\n// doesn\'t measurably change where the refit lands.\n// \u2022 Indices are packed into two u32 nibble words ON THE FLY during the\n// final projection pass \u2014 no array<u32,16> private array. The BC7 anchor\n// reflection (i \u2192 15\u2212i) is then just a bitwise NOT of both words.\n// \u2022 The 128-bit block is assembled with straight-line constant shifts\n// instead of a generic write_bits() helper (whose dynamic word indexing\n// defeats register promotion of the output array).\n//\n// The host selects this module only when the device reports shader-f16,\n// falling back to bc7.wgsl otherwise. "high" never uses this.\n//\n// MODE 6 BIT LAYOUT (LSB-first): see bc7.wgsl. Summary:\n// w0: mode(7 bits, 0x40) R0 R1 G0 G1[3:0]\n// w1: G1[6:4] B0 B1 A0 A1 P0\n// w2: P1, pixel0 index (3 bits), pixels 1..7 (4 bits each)\n// w3: pixels 8..15 (4 bits each)\nenable 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;\nalias h = f16;\nalias h4 = vec4<f16>;\n\n// Quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit, choosing the\n// p-bit with the lower quantisation error. `eight` is the decoded value the\n// hardware will interpolate with, back in [0,1].\nstruct Ep { seven: vec4<u32>, eight: h4, p: u32 };\nfn pick_ep(ideal01: h4) -> Ep {\n let ideal = ideal01 * h(255.0);\n let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0\n let e0 = q0 * h(2.0);\n let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1\n let e1 = q1 * h(2.0) + h(1.0);\n let d0 = e0 - ideal; let d1 = e1 - ideal;\n if (dot(d1, d1) < dot(d0, d0)) { return Ep(vec4<u32>(q1), e1 * h(1.0 / 255.0), 1u); }\n return Ep(vec4<u32>(q0), e0 * h(1.0 / 255.0), 0u);\n}\n\n// One pass over the block: project every pixel onto the e0\u2192e1 line and\n// accumulate the least-squares normal-equation sums; solve for the refit\n// endpoints. Indices are NOT produced here \u2014 the caller reprojects against\n// the quantised refit endpoints anyway.\nstruct Fit { e0: h4, e1: h4, valid: bool };\nfn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {\n var out: Fit;\n out.valid = false;\n let dir = e1 - e0;\n let dd = dot(dir, dir);\n if (dd == h(0.0)) { return out; }\n let inv = h(15.0) / dd;\n var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);\n var sAV = h4(0.0); var sBV = h4(0.0);\n var s_min = h(15.0); var s_max = h(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = (*pix)[k];\n let s = clamp(floor(dot(v - e0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * h(1.0 / 15.0); let a = h(1.0) - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det/numerators are pure f16 rounding noise and the solve\n // returns garbage endpoints. With \u22652 distinct levels\n // det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15/225 \u2248 0.067, so 0.02 is a safe floor.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < h(0.02)) { return out; }\n out.e0 = clamp((sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));\n out.e1 = clamp((sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));\n out.valid = true;\n return out;\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) { return; }\n let bi = gid.y * params.blocks_x + gid.x;\n let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pix: array<h4, 16>;\n var lo = h4(1.0);\n var hi = h4(0.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), mx);\n let px = h4(textureLoad(src_tex, p, 0));\n pix[i] = px; lo = min(lo, px); hi = max(hi, px);\n }\n\n // Seed fit from the raw bbox, then quantise the refit endpoints.\n let r = proj_fit(&pix, lo, hi);\n var ep0: Ep;\n var ep1: Ep;\n if (r.valid) { ep0 = pick_ep(r.e0); ep1 = pick_ep(r.e1); }\n else { ep0 = pick_ep(lo); ep1 = pick_ep(hi); }\n\n // Final projection against the decoded endpoints, packing the 4-bit indices\n // into two nibble words as we go (pixel k \u2192 bits 4k..4k+3 of ilo/ihi).\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\n let dir = ep1.eight - ep0.eight;\n let dd = dot(dir, dir);\n if (dd > h(0.0)) {\n let inv = h(15.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));\n ilo = ilo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));\n ihi = ihi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n\n // Anchor rule \u2014 pixel 0\'s index MSB must be 0. Swapping endpoints reflects\n // every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.\n if ((ilo & 0x8u) != 0u) {\n let t = ep0; ep0 = ep1; ep1 = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n\n // Straight-line mode-6 packing (see layout above).\n let e0 = ep0.seven;\n let e1 = ep1.seven;\n let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);\n let w2 = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n let w3 = ihi;\n\n let o = bi * 4u;\n dst[o] = w0; dst[o + 1u] = w1; dst[o + 2u] = w2; dst[o + 3u] = w3;\n}\n';
|
|
431
639
|
|
|
432
640
|
// src/BC7Encoder.ts
|
|
433
641
|
var BC7Encoder = class extends Encoder {
|
|
@@ -457,10 +665,127 @@ var BC7Encoder = class extends Encoder {
|
|
|
457
665
|
};
|
|
458
666
|
|
|
459
667
|
// src/astc4x4.wgsl
|
|
460
|
-
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";
|
|
668
|
+
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 one fused pass that projects\n// each pixel onto the endpoint line (the 4 palette entries are colinear,\n// so the nearest is the rounded projection \u2014 no per-entry search) while\n// accumulating the least-squares refit sums, then a reprojection against\n// the quantised refit endpoints with the weights packed on the fly. The\n// endpoint ordering rule is applied before the weight pass, so no\n// reflection is needed.\n// high (1): O(N\xB2) farthest-pair seed, full 4-entry nearest search, one LSQ\n// refit \u2014 matches astc4x4_ref.ts up to FP tie-breaks.\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// One pass over the block: project every pixel onto the e0\u2192e1 line (4 levels,\n// QUANT_4 \u2248 thirds) and accumulate the least-squares normal-equation sums;\n// solve for the refit endpoints. Weights are not produced here \u2014 the caller\n// reprojects against the quantised refit endpoints anyway.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_fit(pixels: ptr<function, array<vec4<i32>, 16>>, e0: vec4<i32>, e1: vec4<i32>) -> Fit {\n var out: Fit;\n out.valid = false;\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd == 0.0) { return out; }\n let e0f = vec4<f32>(e0);\n let inv = 3.0 / 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 var s_min = 3.0; var s_max = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = vec4<f32>((*pixels)[k]);\n let s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 3.0);\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det and the numerators are pure float rounding noise and the\n // solve returns garbage endpoints. With \u22652 levels det \u2265 15\xB7(1/3)\xB2 \u2248 1.67.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { 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// ------------------------------- 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 // Both branches produce the final endpoints (already ordered so the\n // decoder doesn't apply blue contraction) and the packed weight word\n // (weight k's lsb at bit 31\u22122k, msb at bit 30\u22122k).\n var e0: vec4<i32>;\n var e1: vec4<i32>;\n var w3: u32 = 0u;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n e0 = fp.a;\n e1 = fp.b;\n var indices: array<u32, 16>;\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 // Endpoint ordering, reflecting the assigned weights.\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 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 } else {\n // Fused LSQ fit seeded from the raw bbox, quantised refit endpoints,\n // ordering applied BEFORE the weight pass so no reflection is needed.\n let r = proj_fit(&pixels, lo, hi);\n e0 = lo;\n e1 = hi;\n if (r.valid) { e0 = r.e0; e1 = r.e1; }\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n let tmp = e0; e0 = e1; e1 = tmp;\n }\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let e0f = vec4<f32>(e0);\n let inv = 3.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let s = u32(clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 3.0));\n w3 = w3 | ((s & 1u) << (31u - 2u * k)) | (((s >> 1u) & 1u) << (30u - 2u * k));\n }\n }\n }\n\n // Straight-line packing: block mode 0x042 @0, partitions\u22121=0 @11, CEM 12\n // @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 (8 bits each) from bit 17,\n // weights in the last word.\n let E0 = vec4<u32>(e0);\n let E1 = vec4<u32>(e1);\n let w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);\n let w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);\n let w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
461
669
|
|
|
462
670
|
// src/astc4x4_fast_f16.wgsl
|
|
463
|
-
var astc4x4_fast_f16_default =
|
|
671
|
+
var astc4x4_fast_f16_default = `// astc4x4 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
672
|
+
// Same algorithm family as the f32 fast path in astc4x4.wgsl (bbox seed \u2192
|
|
673
|
+
// projection weight assignment with a fused least-squares refit \u2192
|
|
674
|
+
// reproject), tuned for throughput:
|
|
675
|
+
//
|
|
676
|
+
// \u2022 All projection / refit math in f16 ([0,1] domain).
|
|
677
|
+
// \u2022 The seed pass only accumulates the LSQ sums (no weight output) \u2014 the
|
|
678
|
+
// final weights come from reprojecting against the refit endpoints.
|
|
679
|
+
// \u2022 Endpoint ordering (the blue-contraction rule: sum(e0.rgb) must not
|
|
680
|
+
// exceed sum(e1.rgb)) is applied BEFORE the final projection, so no
|
|
681
|
+
// weight-reflection pass is needed.
|
|
682
|
+
// \u2022 Weights are packed into the reversed-bit-order field on the fly, and
|
|
683
|
+
// the 128-bit block is assembled with straight-line constant shifts
|
|
684
|
+
// instead of a generic write_bits() helper.
|
|
685
|
+
//
|
|
686
|
+
// RESTRICTED SUBSET + BLOCK LAYOUT: see astc4x4.wgsl (single partition,
|
|
687
|
+
// CEM 12, 8-bit endpoints, 2-bit weights).
|
|
688
|
+
//
|
|
689
|
+
// The host selects this module only when the device reports shader-f16,
|
|
690
|
+
// falling back to astc4x4.wgsl otherwise. "high" never uses this.
|
|
691
|
+
enable f16;
|
|
692
|
+
alias h = f16;
|
|
693
|
+
alias h4 = vec4<f16>;
|
|
694
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
695
|
+
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
696
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
697
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
698
|
+
|
|
699
|
+
struct Fit { e0: h4, e1: h4, valid: bool };
|
|
700
|
+
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
701
|
+
var out: Fit;
|
|
702
|
+
out.valid = false;
|
|
703
|
+
let dir = e1 - e0;
|
|
704
|
+
let dd = dot(dir, dir);
|
|
705
|
+
if (dd == h(0.0)) { return out; }
|
|
706
|
+
let inv = h(3.0) / dd;
|
|
707
|
+
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
708
|
+
var sAV = h4(0.0); var sBV = h4(0.0);
|
|
709
|
+
var s_min = h(3.0); var s_max = h(0.0);
|
|
710
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
711
|
+
let v = (*pix)[k];
|
|
712
|
+
let s = clamp(floor(dot(v - e0, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
713
|
+
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
714
|
+
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
715
|
+
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
716
|
+
sAV = sAV + a * v; sBV = sBV + b * v;
|
|
717
|
+
}
|
|
718
|
+
// Rank-1 guard: if every pixel projects to ONE level the system is
|
|
719
|
+
// singular \u2014 det/numerators are pure f16 rounding noise and the solve
|
|
720
|
+
// returns garbage endpoints. With \u22652 distinct levels
|
|
721
|
+
// det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15\xB7(1/3)\xB2 \u2248 1.67 \u2014 0.5 separates cleanly.
|
|
722
|
+
if (s_min == s_max) { return out; }
|
|
723
|
+
let det = sAA * sBB - sAB * sAB;
|
|
724
|
+
if (abs(det) < h(0.5)) { return out; }
|
|
725
|
+
out.e0 = clamp((sBB * sAV - sAB * sBV) / det, h4(0.0), h4(1.0));
|
|
726
|
+
out.e1 = clamp((sAA * sBV - sAB * sAV) / det, h4(0.0), h4(1.0));
|
|
727
|
+
out.valid = true;
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
fn q8(e: h4) -> vec4<u32> {
|
|
732
|
+
return vec4<u32>(clamp(floor(e * h(255.0) + h(0.5)), h4(0.0), h4(255.0)));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
@compute @workgroup_size(8, 8, 1)
|
|
736
|
+
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
737
|
+
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
738
|
+
let bi = gid.y * params.blocks_x + gid.x;
|
|
739
|
+
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
740
|
+
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
741
|
+
|
|
742
|
+
var pix: array<h4, 16>;
|
|
743
|
+
var lo = h4(1.0);
|
|
744
|
+
var hi = h4(0.0);
|
|
745
|
+
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
746
|
+
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
747
|
+
let px = h4(textureLoad(src_tex, p, 0));
|
|
748
|
+
pix[i] = px; lo = min(lo, px); hi = max(hi, px);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let r = proj_fit(&pix, lo, hi);
|
|
752
|
+
var e0 = lo;
|
|
753
|
+
var e1 = hi;
|
|
754
|
+
if (r.valid) { e0 = r.e0; e1 = r.e1; }
|
|
755
|
+
var E0 = q8(e0);
|
|
756
|
+
var E1 = q8(e1);
|
|
757
|
+
|
|
758
|
+
// Blue-contraction ordering, applied before the weight pass so weights are
|
|
759
|
+
// already oriented (no reflection needed).
|
|
760
|
+
if (E0.x + E0.y + E0.z > E1.x + E1.y + E1.z) {
|
|
761
|
+
let t = E0; E0 = E1; E1 = t;
|
|
762
|
+
}
|
|
763
|
+
let d0 = h4(vec4<f32>(E0)) * h(1.0 / 255.0);
|
|
764
|
+
let d1 = h4(vec4<f32>(E1)) * h(1.0 / 255.0);
|
|
765
|
+
|
|
766
|
+
// Weight pass, packing on the fly: weight k's lsb at bit 31\u22122k of the last
|
|
767
|
+
// word, msb at bit 30\u22122k.
|
|
768
|
+
var w3: u32 = 0u;
|
|
769
|
+
let dir = d1 - d0;
|
|
770
|
+
let dd = dot(dir, dir);
|
|
771
|
+
if (dd > h(0.0)) {
|
|
772
|
+
let inv = h(3.0) / dd;
|
|
773
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
774
|
+
let s = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0)));
|
|
775
|
+
w3 = w3 | ((s & 1u) << (31u - 2u * k)) | (((s >> 1u) & 1u) << (30u - 2u * k));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Straight-line packing: block mode 0x042 @0, partitions\u22121=0 @11, CEM 12
|
|
780
|
+
// @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 (8 bits each) from bit 17.
|
|
781
|
+
let w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
782
|
+
let w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
783
|
+
let w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);
|
|
784
|
+
|
|
785
|
+
let o = bi * 4u;
|
|
786
|
+
dst[o] = w0; dst[o + 1u] = w1; dst[o + 2u] = w2; dst[o + 3u] = w3;
|
|
787
|
+
}
|
|
788
|
+
`;
|
|
464
789
|
|
|
465
790
|
// src/ASTC4x4Encoder.ts
|
|
466
791
|
var ASTC4x4Encoder = class extends Encoder {
|