gputex 0.8.0 → 0.9.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 +34 -4
- package/dist/index.d.ts +90 -14
- package/dist/index.js +433 -235
- package/dist/testing.js +7 -1
- package/dist/three.d.ts +4 -1
- package/dist/three.js +440 -238
- package/package.json +1 -1
package/dist/three.js
CHANGED
|
@@ -226,6 +226,15 @@ var Encoder = class {
|
|
|
226
226
|
this._pipelineReady.catch(() => {
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Resolves once the encoder's compute pipeline(s) have compiled. Encodes
|
|
231
|
+
* await this themselves; call it to compile ahead of first use (see
|
|
232
|
+
* `prewarmCompressTexture()`). Rejects with the compile error, if any.
|
|
233
|
+
*/
|
|
234
|
+
async ready() {
|
|
235
|
+
await this._pipelineReady;
|
|
236
|
+
if (this._prepPipelineReady) await this._prepPipelineReady;
|
|
237
|
+
}
|
|
229
238
|
destroy() {
|
|
230
239
|
this._cachedSrcTex?.destroy();
|
|
231
240
|
if (this._cachedPrepPlanes) for (const t of this._cachedPrepPlanes) t.destroy();
|
|
@@ -1089,7 +1098,7 @@ var BC1Encoder = class extends Encoder {
|
|
|
1089
1098
|
};
|
|
1090
1099
|
|
|
1091
1100
|
// src/bc5.wgsl
|
|
1092
|
-
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. This is the f32\n// fallback; bc5_fast_f16.wgsl is the same algorithm and is preferred when\n// the device reports shader-f16.\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 `bc4_ref.ts`\n// for the reference this encoder is validated against.\n//\n// Same algorithm as bc5_fast_f16.wgsl (see that file for the full notes):\n// \u2022 both channels processed as
|
|
1101
|
+
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. This is the f32\n// fallback; bc5_fast_f16.wgsl is the same algorithm and is preferred when\n// the device reports shader-f16.\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 `bc4_ref.ts`\n// for the reference this encoder is validated against.\n//\n// Same algorithm as bc5_fast_f16.wgsl (see that file for the full notes):\n// \u2022 both channels processed in the same fused passes, texels held as\n// quad-major vec4s per channel (the gather layout);\n// \u2022 pass 1 accumulates MOMENTS (\u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d with d = v \u2212 r0) from\n// which every LSQ normal-equation sum is an O(1) per-block expression;\n// the seed covers the data, so pass 1 needs no clamp; the rank guard\n// is the exact 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2 test;\n// \u2022 the closed-form refit prices the nearest rounding of the solve\n// through E(\u03B4) = err \u2212 2(\u03B40\xB7sAR + \u03B41\xB7sBR) + \u03B40\xB2sAA + 2\u03B40\u03B41\xB7sAB\n// + \u03B41\xB2sBB, accept-if-better, both channels as branch-free vec2 lanes;\n// \u2022 pass 2 derives the levels ONCE, against the FINAL endpoints \u2014 full\n// reprojection quality;\n// \u2022 the 16 texel reads are 8 textureGather fetches (4 quads \xD7 R,G)\n// through a clamp-to-edge sampler, byte-identical to per-texel loads;\n// \u2022 3-bit levels pack as \u03A3 L\xB78^k in f32 (exact below 2^24), then one\n// SWAR level \u2192 BC4 index map (0\u21920, 7\u21921, L\u2192L+1) per 24-bit word.\n// Values are kept in the [0,255] f32 domain throughout.\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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@group(0) @binding(3) var smp: sampler;\n\n// 8 packed 3-bit levels \u2192 BC4 indices (0\u21920, 7\u21921, L\u2192L+1 otherwise).\nfn lvl_to_idx(x: u32) -> u32 {\n let y = ((x & 0x6DB6DBu) + 0x249249u) ^ (x & 0x924924u);\n return y ^ (~((y >> 1u) | (y >> 2u)) & 0x249249u);\n}\n\n// Per-quad pixel weights 8^k (gather order x,y,z,w = (0,1),(1,1),(1,0),(0,0)).\nconst W0 = vec4<f32>(4096.0, 32768.0, 8.0, 1.0);\nconst W1 = vec4<f32>(262144.0, 2097152.0, 512.0, 64.0);\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n let bi = gid.y * params.blocks_x + gid.x;\n let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n\n // Load 4\xD74 R and G as quad-major vec4s in gather order (w=(0,0) z=(1,0)\n // x=(0,1) y=(1,1) of quad q = (x \u2265 2) + 2\xB7(y \u2265 2)). Interior blocks read\n // via 8 gathers normalised by the PHYSICAL (padded) texture size; blocks\n // straddling the source edge of a non-multiple-of-4 image fall back to\n // per-texel loads clamped to the last real texel (the padding strip is\n // zero-initialised \u2014 see bc5_fast_f16.wgsl).\n var vr: array<vec4<f32>, 4>;\n var vg: array<vec4<f32>, 4>;\n if (u32(base.x) + 4u <= params.width && u32(base.y) + 4u <= params.height) {\n let inv_size = vec2<f32>(1.0, 1.0) / vec2<f32>(textureDimensions(src_tex));\n for (var q: u32 = 0u; q < 4u; q = q + 1u) {\n let qo = vec2<u32>((q & 1u) * 2u, (q >> 1u) * 2u);\n let cc = (vec2<f32>(base) + vec2<f32>(qo) + vec2<f32>(1.0, 1.0)) * inv_size;\n vr[q] = textureGather(0, src_tex, smp, cc) * 255.0;\n vg[q] = textureGather(1, src_tex, smp, cc) * 255.0;\n }\n } else {\n let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n for (var q: u32 = 0u; q < 4u; q = q + 1u) {\n let qo = base + vec2<i32>(i32(q & 1u) * 2, i32(q >> 1u) * 2);\n let cx = textureLoad(src_tex, clamp(qo + vec2<i32>(0, 1), vec2<i32>(0), mx), 0);\n let cy = textureLoad(src_tex, clamp(qo + vec2<i32>(1, 1), vec2<i32>(0), mx), 0);\n let cz = textureLoad(src_tex, clamp(qo + vec2<i32>(1, 0), vec2<i32>(0), mx), 0);\n let cw = textureLoad(src_tex, clamp(qo, vec2<i32>(0), mx), 0);\n vr[q] = vec4<f32>(cx.r, cy.r, cz.r, cw.r) * 255.0;\n vg[q] = vec4<f32>(cx.g, cy.g, cz.g, cw.g) * 255.0;\n }\n }\n let mnr = min(min(vr[0], vr[1]), min(vr[2], vr[3]));\n let mxr = max(max(vr[0], vr[1]), max(vr[2], vr[3]));\n let mng = min(min(vg[0], vg[1]), min(vg[2], vg[3]));\n let mxg = max(max(vg[0], vg[1]), max(vg[2], vg[3]));\n let vmin = vec2<f32>(min(min(mnr.x, mnr.y), min(mnr.z, mnr.w)), min(min(mng.x, mng.y), min(mng.z, mng.w)));\n let vmax = vec2<f32>(max(max(mxr.x, mxr.y), max(mxr.z, mxr.w)), max(max(mxg.x, mxg.y), max(mxg.z, mxg.w)));\n\n // Seed endpoints at the exact per-channel extremes (round-to-nearest, the\n // same rule the CPU reference uses). Flat blocks get nudged apart to keep\n // the 6-interp mode (r0 > r1 strictly).\n var r0 = vec2<u32>(clamp(floor(vmax + 0.5), vec2<f32>(0.0), vec2<f32>(255.0)));\n var r1 = vec2<u32>(clamp(floor(vmin + 0.5), vec2<f32>(0.0), vec2<f32>(255.0)));\n if (r0.x == r1.x) { if (r1.x > 0u) { r1.x = r1.x - 1u; } else { r0.x = r0.x + 1u; } }\n if (r0.y == r1.y) { if (r1.y > 0u) { r1.y = r1.y - 1u; } else { r0.y = r0.y + 1u; } }\n\n let r0f = vec2<f32>(r0);\n let r1f = vec2<f32>(r1);\n let dir = r1f - r0f;\n let scale = vec2<f32>(7.0) / dir;\n\n // Pass 1, both channels \u2014 MOMENTS only. t = d\xB7scale \u2208 [0,7] (the seed\n // covers the data, so no clamp), L = round(t).\n var sL = vec2<f32>(0.0); var sLL = vec2<f32>(0.0);\n var sd = vec2<f32>(0.0); var sLd = vec2<f32>(0.0);\n for (var q: u32 = 0u; q < 4u; q = q + 1u) {\n let dr = vr[q] - r0f.x;\n let dg = vg[q] - r0f.y;\n let Lr = floor(dr * scale.x + 0.5);\n let Lg = floor(dg * scale.y + 0.5);\n sL = sL + vec2<f32>(dot(Lr, vec4<f32>(1.0)), dot(Lg, vec4<f32>(1.0)));\n sLL = sLL + vec2<f32>(dot(Lr, Lr), dot(Lg, Lg));\n sd = sd + vec2<f32>(dot(dr, vec4<f32>(1.0)), dot(dg, vec4<f32>(1.0)));\n sLd = sLd + vec2<f32>(dot(Lr, dr), dot(Lg, dg));\n }\n\n // Per-block refit off the moments (see bc5_fast_f16.wgsl for the\n // identities): \u03A3\u03C1 = s\xB7\u03A3d \u2212 \u03A3L, \u03A3L\u03C1 = s\xB7\u03A3Ld \u2212 \u03A3L\xB2.\n let pR = scale * sd - sL;\n let pLR = scale * sLd - sLL;\n let sBB = sLL * (1.0 / 49.0);\n let sAB = sL * (1.0 / 7.0) - sBB;\n let sAA = vec2<f32>(16.0) - 2.0 * sL * (1.0 / 7.0) + sBB;\n let sBR = pLR * dir * (1.0 / 49.0);\n let sAR = (pR - pLR * (1.0 / 7.0)) * dir * (1.0 / 7.0);\n let spread = 16.0 * sLL != sL * sL;\n\n // Both channels at once, branch-free: nearest rounding of the LSQ\n // solve, accepted when it stays in 6-interp mode, moves, and prices\n // strictly better on the current indices. Endpoints clamp to [0,255],\n // NOT the block's value range: for a scalar channel, endpoints beyond\n // the data range are often genuinely optimal and there is no colour\n // axis to bend.\n let det = sAA * sBB - sAB * sAB;\n let idet = 1.0 / det;\n let q0f = floor(clamp(r0f + (sBB * sAR - sAB * sBR) * idet, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);\n let q1f = floor(clamp(r1f + (sAA * sBR - sAB * sAR) * idet, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);\n let dd0 = q0f - r0f;\n let dd1 = q1f - r1f;\n let eNew = -2.0 * (dd0 * sAR + dd1 * sBR) + dd0 * dd0 * sAA + 2.0 * dd0 * dd1 * sAB + dd1 * dd1 * sBB;\n let acc = spread & (abs(det) > vec2<f32>(1e-3)) & (q0f > q1f) & (eNew < vec2<f32>(0.0));\n let n0 = select(r0, vec2<u32>(q0f), acc);\n let n1 = select(r1, vec2<u32>(q1f), acc);\n\n // Pass 2, both channels \u2014 levels against the FINAL endpoints (rejected\n // channels re-derive their seed assignment), packed as \u03A3 L\xB78^k:\n // iA = pixels 0..7, iB = pixels 8..15.\n let n0f = vec2<f32>(n0);\n let sc2 = vec2<f32>(7.0) / (vec2<f32>(n1) - n0f);\n var Lr: array<vec4<f32>, 4>;\n var Lg: array<vec4<f32>, 4>;\n for (var q: u32 = 0u; q < 4u; q = q + 1u) {\n Lr[q] = clamp(floor((vr[q] - n0f.x) * sc2.x + 0.5), vec4<f32>(0.0), vec4<f32>(7.0));\n Lg[q] = clamp(floor((vg[q] - n0f.y) * sc2.y + 0.5), vec4<f32>(0.0), vec4<f32>(7.0));\n }\n let iAx = lvl_to_idx(u32(dot(Lr[0], W0) + dot(Lr[1], W1)));\n let iBx = lvl_to_idx(u32(dot(Lr[2], W0) + dot(Lr[3], W1)));\n let iAy = lvl_to_idx(u32(dot(Lg[0], W0) + dot(Lg[1], W1)));\n let iBy = lvl_to_idx(u32(dot(Lg[2], W0) + dot(Lg[3], W1)));\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n let o = bi * 4u;\n dst[o] = n0.x | (n1.x << 8u) | (iAx << 16u);\n dst[o + 1u] = (iAx >> 16u) | (iBx << 8u);\n dst[o + 2u] = n0.y | (n1.y << 8u) | (iAy << 16u);\n dst[o + 3u] = (iAy >> 16u) | (iBy << 8u);\n}\n";
|
|
1093
1102
|
|
|
1094
1103
|
// src/bc5_fast_f16.wgsl
|
|
1095
1104
|
var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
@@ -1103,9 +1112,9 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
1103
1112
|
// \u2022 Math runs in the exact-integer [0,255] f16 domain: endpoints and pixel
|
|
1104
1113
|
// values are whole numbers \u2264 255 (exact in f16), so the only rounding is
|
|
1105
1114
|
// the single 1/(r1\u2212r0) division.
|
|
1106
|
-
// \u2022 BOTH channels ride the same fused passes
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1115
|
+
// \u2022 BOTH channels ride the same fused passes \u2014 each loop computes
|
|
1116
|
+
// projections and moments for R and G at once instead of two scalar
|
|
1117
|
+
// encode_bc4 calls.
|
|
1109
1118
|
// \u2022 The 16 texel reads are 8 textureGather fetches (4 quads \xD7 R,G) for
|
|
1110
1119
|
// interior blocks \u2014 byte-identical output to per-texel loads, \u22123.6%
|
|
1111
1120
|
// GPU on 4096\xB2 (/ab, 2026-07). Blocks straddling the source edge of a
|
|
@@ -1113,19 +1122,20 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
1113
1122
|
// upload pads the texture with ZEROS, so a normalised-coordinate
|
|
1114
1123
|
// gather there would read padding (or mis-scale against the padded
|
|
1115
1124
|
// size) instead of replicating the last real texel.
|
|
1116
|
-
// \u2022 Pass 1 accumulates MOMENTS, not normal-equation sums
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1125
|
+
// \u2022 Pass 1 accumulates MOMENTS, not normal-equation sums: \u03A3L, \u03A3L\xB2, \u03A3d and
|
|
1126
|
+
// \u03A3L\xB7d per channel, with d = v \u2212 r0 (exact integers) and L the seed
|
|
1127
|
+
// level. With b = L/7, t = d\xB77/(r1\u2212r0) = d\xB7s and the level-space
|
|
1128
|
+
// residual \u03C1 = t \u2212 L, the residual moments are O(1) per block \u2014
|
|
1129
|
+
// \u03A3\u03C1 = s\xB7\u03A3d \u2212 \u03A3L, \u03A3L\u03C1 = s\xB7\u03A3Ld \u2212 \u03A3L\xB2 \u2014 and so is every LSQ sum:
|
|
1120
1130
|
// sBB = \u03A3L\xB2/49 sAB = \u03A3L/7 \u2212 \u03A3L\xB2/49 sAA = 16 \u2212 2\u03A3L/7 + \u03A3L\xB2/49
|
|
1121
1131
|
// sBR = \u03A3L\u03C1\xB7dir/49 sAR = (\u03A3\u03C1 \u2212 \u03A3L\u03C1/7)\xB7dir/7
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1132
|
+
// \u03A3L \u2264 112 and \u03A3L\xB2 \u2264 784 are exact f16 integers; \u03A3L\xB7d (\u2264 28560) and \u03A3d
|
|
1133
|
+
// accumulate in f32 so they stay exact too. The seed covers the data,
|
|
1134
|
+
// so t \u2208 [0,7] and pass 1 needs no clamp. The per-BLOCK refit math
|
|
1135
|
+
// (solve, E(\u03B4) pricing) runs in f32 \u2014 free at block granularity.
|
|
1136
|
+
// \u2022 Texels live as quad-major vec4<f16> per channel (the gather layout),
|
|
1137
|
+
// so min/max reduce as vectors and both passes run 4-wide; edge blocks
|
|
1138
|
+
// load into the same layout.
|
|
1129
1139
|
// \u2022 The rank guard is EXACT: all pixels on one level \u27FA 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2
|
|
1130
1140
|
// (integers, so the comparison is precise in f32) \u2014 no lmin/lmax
|
|
1131
1141
|
// tracking in the loop.
|
|
@@ -1134,78 +1144,43 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
1134
1144
|
// of re-quantised endpoints ON THE CURRENT INDICES is
|
|
1135
1145
|
// E(\u03B4) = err \u2212 2(\u03B40\xB7sAR + \u03B41\xB7sBR) + \u03B40\xB2sAA + 2\u03B40\u03B41\xB7sAB + \u03B41\xB2sBB
|
|
1136
1146
|
// with \u03B4 = quantised endpoint \u2212 base endpoint, compared as the delta
|
|
1137
|
-
// form E \u2212 err < 0.
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
// (+14%, register cliff, \u22480 gain on smooth content \u2014 round 2 only pays
|
|
1153
|
-
// on noise); full normal-equation sums in both passes (+49%).
|
|
1154
|
-
// \u2022 3-bit indices are packed BRANCH-FREE: pixels 0..7 accumulate into a
|
|
1155
|
-
// 24-bit word, pixels 8..15 into another, recombined with constant
|
|
1156
|
-
// shifts into the 48-bit field (w0 gets field bits 0..15 above the two
|
|
1157
|
-
// endpoint bytes, w1 gets field bits 16..47) \u2014 no per-pixel straddle
|
|
1158
|
-
// branches.
|
|
1159
|
-
//
|
|
1160
|
-
// Level \u2192 BC4 index (0\u2192r0 ... 7\u2192r1): 0,2,3,4,5,6,7,1 \u2014 packed 3-bit LUT
|
|
1161
|
-
// 0x3F58D0 = sum(idx[L] << 3L).
|
|
1147
|
+
// form E \u2212 err < 0. Both channels run as vec2 lanes, branch-free (one
|
|
1148
|
+
// reciprocal of det, select on the accept mask) \u2014 the per-channel
|
|
1149
|
+
// function with early returns measured ~2% slower. Only the NEAREST
|
|
1150
|
+
// rounding of the fractional solve is priced: pricing all four
|
|
1151
|
+
// floor/ceil combinations measured \u22640.015 dB on every content class
|
|
1152
|
+
// but ~8% GPU on smooth content.
|
|
1153
|
+
// \u2022 Pass 2 derives the shipped levels ONCE, against the FINAL endpoints \u2014
|
|
1154
|
+
// full reprojection quality.
|
|
1155
|
+
// \u2022 3-bit indices are packed as FLOAT: each group of 8 pixels'
|
|
1156
|
+
// levels accumulates as \u03A3 L\xB78^k in f32 (\u2264 2^24 \u2212 1, exact) \u2014 one fma
|
|
1157
|
+
// per texel \u2014 and the level \u2192 BC4 index map (0\u21920, 7\u21921, L\u2192L+1) is
|
|
1158
|
+
// applied to the whole 24-bit word with SWAR bit tricks.
|
|
1159
|
+
// These passes measured \u221222% GPU vs the previous kernel (rg8 source; \u221217%
|
|
1160
|
+
// on rgba8, where 2K/4K normal maps sit at the read floor either way) at
|
|
1161
|
+
// equal PSNR \u2014 the 1K case is ALU-bound, not read-bound (/eval 2026-09).
|
|
1162
1162
|
//
|
|
1163
1163
|
// The host selects this module only when the device reports shader-f16,
|
|
1164
1164
|
// falling back to bc5.wgsl otherwise.
|
|
1165
1165
|
enable f16;
|
|
1166
1166
|
alias h = f16;
|
|
1167
|
-
alias
|
|
1167
|
+
alias h4 = vec4<f16>;
|
|
1168
1168
|
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, y0: u32, };
|
|
1169
1169
|
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
1170
1170
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1171
1171
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
1172
1172
|
@group(0) @binding(3) var smp: sampler;
|
|
1173
1173
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
// the current indices, the current integer endpoints b0 > b1, and the rank
|
|
1179
|
-
// guard; prices the nearest rounding of the fractional solve via E(\u03B4) and
|
|
1180
|
-
// returns it when it strictly improves and stays in 6-interp mode
|
|
1181
|
-
// (q0 > q1), or (b0,b1) unchanged. Endpoints clamp to [0,255], NOT the
|
|
1182
|
-
// block's value range: for a scalar channel, endpoints beyond the data
|
|
1183
|
-
// range are often genuinely optimal and there is no colour axis to bend
|
|
1184
|
-
// (the bbox clamp the colour formats need costs ~0.3 dB here).
|
|
1185
|
-
fn refine(sAA: f32, sBB: f32, sAB: f32, sAR: f32, sBR: f32, b0: u32, b1: u32, spread: bool) -> vec2<u32> {
|
|
1186
|
-
var out = vec2<u32>(b0, b1);
|
|
1187
|
-
let det = sAA * sBB - sAB * sAB;
|
|
1188
|
-
if (!spread || abs(det) <= 1e-3) { return out; }
|
|
1189
|
-
let b0f = f32(b0);
|
|
1190
|
-
let b1f = f32(b1);
|
|
1191
|
-
let e0 = clamp(b0f + (sBB * sAR - sAB * sBR) / det, 0.0, 255.0);
|
|
1192
|
-
let e1 = clamp(b1f + (sAA * sBR - sAB * sAR) / det, 0.0, 255.0);
|
|
1193
|
-
let q0f = floor(e0 + 0.5);
|
|
1194
|
-
let q1f = floor(e1 + 0.5);
|
|
1195
|
-
let q0 = u32(q0f);
|
|
1196
|
-
let q1 = u32(q1f);
|
|
1197
|
-
if (q0 > q1 && !(q0 == b0 && q1 == b1)) {
|
|
1198
|
-
let dd0 = q0f - b0f;
|
|
1199
|
-
let dd1 = q1f - b1f;
|
|
1200
|
-
let eNew = -2.0 * (dd0 * sAR + dd1 * sBR)
|
|
1201
|
-
+ dd0 * dd0 * sAA + 2.0 * dd0 * dd1 * sAB + dd1 * dd1 * sBB;
|
|
1202
|
-
if (eNew < 0.0) {
|
|
1203
|
-
out = vec2<u32>(q0, q1);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return out;
|
|
1174
|
+
// 8 packed 3-bit levels \u2192 BC4 indices (0\u21920, 7\u21921, L\u2192L+1 otherwise).
|
|
1175
|
+
fn lvl_to_idx(x: u32) -> u32 {
|
|
1176
|
+
let y = ((x & 0x6DB6DBu) + 0x249249u) ^ (x & 0x924924u);
|
|
1177
|
+
return y ^ (~((y >> 1u) | (y >> 2u)) & 0x249249u);
|
|
1207
1178
|
}
|
|
1208
1179
|
|
|
1180
|
+
// Per-quad pixel weights 8^k (gather order x,y,z,w = (0,1),(1,1),(1,0),(0,0)).
|
|
1181
|
+
const W0 = vec4<f32>(4096.0, 32768.0, 8.0, 1.0);
|
|
1182
|
+
const W1 = vec4<f32>(262144.0, 2097152.0, 512.0, 64.0);
|
|
1183
|
+
|
|
1209
1184
|
@compute @workgroup_size(8, 8, 1)
|
|
1210
1185
|
fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
1211
1186
|
// Row-band encodes dispatch a slice of the block grid starting at row y0.
|
|
@@ -1214,102 +1189,120 @@ fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
|
1214
1189
|
let bi = gid.y * params.blocks_x + gid.x;
|
|
1215
1190
|
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
1216
1191
|
|
|
1217
|
-
// Load 4\xD74 R
|
|
1192
|
+
// Load 4\xD74 R and G as quad-major vec4s in gather order: component
|
|
1193
|
+
// w=(0,0) z=(1,0) x=(0,1) y=(1,1) of quad q = (x \u2265 2) + 2\xB7(y \u2265 2).
|
|
1218
1194
|
// INTERIOR blocks \u2014 every block when the source is a multiple of 4, so
|
|
1219
1195
|
// the branch is wavefront-uniform on benchmark-shaped content \u2014 read via
|
|
1220
|
-
// 8 gathers
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
var
|
|
1228
|
-
var
|
|
1229
|
-
var vmax = h2(0.0);
|
|
1196
|
+
// 8 gathers; the gather point (base+quad+1) normalised by the PHYSICAL
|
|
1197
|
+
// (padded) texture size sits exactly between the quad's texel centers,
|
|
1198
|
+
// and interior quads never touch the zero-initialised padding strip.
|
|
1199
|
+
// Blocks straddling the source edge of a non-multiple-of-4 image fall
|
|
1200
|
+
// back to per-texel loads clamped to the last real texel (gather cannot
|
|
1201
|
+
// replicate an edge texel mid-quad). \xD7255 then f16 lands every value on
|
|
1202
|
+
// an exact integer.
|
|
1203
|
+
var vr: array<h4, 4>;
|
|
1204
|
+
var vg: array<h4, 4>;
|
|
1230
1205
|
if (u32(base.x) + 4u <= params.width && u32(base.y) + 4u <= params.height) {
|
|
1231
1206
|
let inv_size = vec2<f32>(1.0, 1.0) / vec2<f32>(textureDimensions(src_tex));
|
|
1232
1207
|
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1233
1208
|
let qo = vec2<u32>((q & 1u) * 2u, (q >> 1u) * 2u);
|
|
1234
1209
|
let cc = (vec2<f32>(base) + vec2<f32>(qo) + vec2<f32>(1.0, 1.0)) * inv_size;
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
let i = qo.y * 4u + qo.x;
|
|
1238
|
-
let vw = h2(h(r4.w), h(g4.w));
|
|
1239
|
-
let vz = h2(h(r4.z), h(g4.z));
|
|
1240
|
-
let vx = h2(h(r4.x), h(g4.x));
|
|
1241
|
-
let vy = h2(h(r4.y), h(g4.y));
|
|
1242
|
-
v[i] = vw; v[i + 1u] = vz; v[i + 4u] = vx; v[i + 5u] = vy;
|
|
1243
|
-
vmin = min(min(vmin, min(vw, vz)), min(vx, vy));
|
|
1244
|
-
vmax = max(max(vmax, max(vw, vz)), max(vx, vy));
|
|
1210
|
+
vr[q] = h4(textureGather(0, src_tex, smp, cc) * 255.0);
|
|
1211
|
+
vg[q] = h4(textureGather(1, src_tex, smp, cc) * 255.0);
|
|
1245
1212
|
}
|
|
1246
1213
|
} else {
|
|
1247
1214
|
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
1248
|
-
for (var
|
|
1249
|
-
let
|
|
1250
|
-
let
|
|
1251
|
-
let
|
|
1252
|
-
|
|
1215
|
+
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1216
|
+
let qo = base + vec2<i32>(i32(q & 1u) * 2, i32(q >> 1u) * 2);
|
|
1217
|
+
let cx = textureLoad(src_tex, clamp(qo + vec2<i32>(0, 1), vec2<i32>(0), mx), 0);
|
|
1218
|
+
let cy = textureLoad(src_tex, clamp(qo + vec2<i32>(1, 1), vec2<i32>(0), mx), 0);
|
|
1219
|
+
let cz = textureLoad(src_tex, clamp(qo + vec2<i32>(1, 0), vec2<i32>(0), mx), 0);
|
|
1220
|
+
let cw = textureLoad(src_tex, clamp(qo, vec2<i32>(0), mx), 0);
|
|
1221
|
+
vr[q] = h4(vec4<f32>(cx.r, cy.r, cz.r, cw.r) * 255.0);
|
|
1222
|
+
vg[q] = h4(vec4<f32>(cx.g, cy.g, cz.g, cw.g) * 255.0);
|
|
1253
1223
|
}
|
|
1254
1224
|
}
|
|
1225
|
+
let mnr = min(min(vr[0], vr[1]), min(vr[2], vr[3]));
|
|
1226
|
+
let mxr = max(max(vr[0], vr[1]), max(vr[2], vr[3]));
|
|
1227
|
+
let mng = min(min(vg[0], vg[1]), min(vg[2], vg[3]));
|
|
1228
|
+
let mxg = max(max(vg[0], vg[1]), max(vg[2], vg[3]));
|
|
1229
|
+
let vmin = vec2<h>(min(min(mnr.x, mnr.y), min(mnr.z, mnr.w)), min(min(mng.x, mng.y), min(mng.z, mng.w)));
|
|
1230
|
+
let vmax = vec2<h>(max(max(mxr.x, mxr.y), max(mxr.z, mxr.w)), max(max(mxg.x, mxg.y), max(mxg.z, mxg.w)));
|
|
1255
1231
|
|
|
1256
|
-
// Seed endpoints at the exact per-channel extremes
|
|
1257
|
-
//
|
|
1258
|
-
// the 6-interp mode (r0 > r1 strictly).
|
|
1232
|
+
// Seed endpoints at the exact per-channel extremes. Flat blocks get
|
|
1233
|
+
// nudged apart to keep the 6-interp mode (r0 > r1 strictly).
|
|
1259
1234
|
var r0 = vec2<u32>(vmax);
|
|
1260
1235
|
var r1 = vec2<u32>(vmin);
|
|
1261
1236
|
if (r0.x == r1.x) { if (r1.x > 0u) { r1.x = r1.x - 1u; } else { r0.x = r0.x + 1u; } }
|
|
1262
1237
|
if (r0.y == r1.y) { if (r1.y > 0u) { r1.y = r1.y - 1u; } else { r0.y = r0.y + 1u; } }
|
|
1263
1238
|
|
|
1264
|
-
let
|
|
1265
|
-
let
|
|
1266
|
-
let
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1271
|
-
var
|
|
1272
|
-
for (var
|
|
1273
|
-
let
|
|
1274
|
-
let
|
|
1275
|
-
let
|
|
1276
|
-
|
|
1277
|
-
|
|
1239
|
+
let r0h = vec2<h>(vec2<f32>(r0));
|
|
1240
|
+
let dirf = vec2<f32>(r1) - vec2<f32>(r0);
|
|
1241
|
+
let scale = vec2<h>(vec2<f32>(7.0) / dirf);
|
|
1242
|
+
|
|
1243
|
+
// Pass 1 \u2014 t = d\xB7scale \u2208 [0,7] by construction (seed covers the data),
|
|
1244
|
+
// so L = floor(t + \xBD) needs no clamp.
|
|
1245
|
+
var sLr = h(0.0); var sLLr = h(0.0); var sdr = 0.0; var sLdr = 0.0;
|
|
1246
|
+
var sLg = h(0.0); var sLLg = h(0.0); var sdg = 0.0; var sLdg = 0.0;
|
|
1247
|
+
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1248
|
+
let dr = vr[q] - r0h.x;
|
|
1249
|
+
let dg = vg[q] - r0h.y;
|
|
1250
|
+
let Lr = floor(dr * scale.x + h(0.5));
|
|
1251
|
+
let Lg = floor(dg * scale.y + h(0.5));
|
|
1252
|
+
sLr = sLr + dot(Lr, h4(1.0)); sLLr = sLLr + dot(Lr, Lr);
|
|
1253
|
+
sLg = sLg + dot(Lg, h4(1.0)); sLLg = sLLg + dot(Lg, Lg);
|
|
1254
|
+
sdr = sdr + f32(dot(dr, h4(1.0)));
|
|
1255
|
+
sdg = sdg + f32(dot(dg, h4(1.0)));
|
|
1256
|
+
sLdr = sLdr + dot(vec4<f32>(Lr), vec4<f32>(dr));
|
|
1257
|
+
sLdg = sLdg + dot(vec4<f32>(Lg), vec4<f32>(dg));
|
|
1278
1258
|
}
|
|
1279
1259
|
|
|
1280
1260
|
// Per-block refit in f32 off the moments (see header for the identities).
|
|
1281
|
-
let sLf = vec2<f32>(
|
|
1282
|
-
let sLLf = vec2<f32>(
|
|
1283
|
-
let
|
|
1261
|
+
let sLf = vec2<f32>(f32(sLr), f32(sLg));
|
|
1262
|
+
let sLLf = vec2<f32>(f32(sLLr), f32(sLLg));
|
|
1263
|
+
let s32 = vec2<f32>(7.0) / dirf;
|
|
1264
|
+
let pR = s32 * vec2<f32>(sdr, sdg) - sLf;
|
|
1265
|
+
let pLR = s32 * vec2<f32>(sLdr, sLdg) - sLLf;
|
|
1284
1266
|
let sBB = sLLf * (1.0 / 49.0);
|
|
1285
1267
|
let sAB = sLf * (1.0 / 7.0) - sBB;
|
|
1286
1268
|
let sAA = vec2<f32>(16.0) - 2.0 * sLf * (1.0 / 7.0) + sBB;
|
|
1287
|
-
let sBR =
|
|
1288
|
-
let sAR = (
|
|
1269
|
+
let sBR = pLR * dirf * (1.0 / 49.0);
|
|
1270
|
+
let sAR = (pR - pLR * (1.0 / 7.0)) * dirf * (1.0 / 7.0);
|
|
1289
1271
|
let spread = 16.0 * sLLf != sLf * sLf;
|
|
1290
1272
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
let
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
let
|
|
1300
|
-
let
|
|
1301
|
-
let
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1273
|
+
// Both channels at once, branch-free: nearest rounding of the LSQ
|
|
1274
|
+
// solve, accepted when it stays in 6-interp mode, moves, and prices
|
|
1275
|
+
// strictly better on the current indices.
|
|
1276
|
+
let r0f = vec2<f32>(r0);
|
|
1277
|
+
let r1f = vec2<f32>(r1);
|
|
1278
|
+
let det = sAA * sBB - sAB * sAB;
|
|
1279
|
+
let idet = 1.0 / det;
|
|
1280
|
+
let q0f = floor(clamp(r0f + (sBB * sAR - sAB * sBR) * idet, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);
|
|
1281
|
+
let q1f = floor(clamp(r1f + (sAA * sBR - sAB * sAR) * idet, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);
|
|
1282
|
+
let dd0 = q0f - r0f;
|
|
1283
|
+
let dd1 = q1f - r1f;
|
|
1284
|
+
let eNew = -2.0 * (dd0 * sAR + dd1 * sBR) + dd0 * dd0 * sAA + 2.0 * dd0 * dd1 * sAB + dd1 * dd1 * sBB;
|
|
1285
|
+
let acc = spread & (abs(det) > vec2<f32>(1e-3)) & (q0f > q1f) & (eNew < vec2<f32>(0.0));
|
|
1286
|
+
let n0 = select(r0, vec2<u32>(q0f), acc);
|
|
1287
|
+
let n1 = select(r1, vec2<u32>(q1f), acc);
|
|
1288
|
+
|
|
1289
|
+
// Pass 2 \u2014 levels against the FINAL endpoints (rejected channels
|
|
1290
|
+
// re-derive their seed assignment), accumulated as 3-bit fields in f32
|
|
1291
|
+
// (\u03A3 L\xB78^k \u2264 2^24 \u2212 1, exact): iA = pixels 0..7, iB = pixels 8..15.
|
|
1292
|
+
let n0h = vec2<h>(vec2<f32>(n0));
|
|
1293
|
+
let sc2 = vec2<h>(vec2<f32>(7.0) / (vec2<f32>(n1) - vec2<f32>(n0)));
|
|
1294
|
+
let L0r = clamp(floor((vr[0] - n0h.x) * sc2.x + h(0.5)), h4(0.0), h4(7.0));
|
|
1295
|
+
let L1r = clamp(floor((vr[1] - n0h.x) * sc2.x + h(0.5)), h4(0.0), h4(7.0));
|
|
1296
|
+
let L2r = clamp(floor((vr[2] - n0h.x) * sc2.x + h(0.5)), h4(0.0), h4(7.0));
|
|
1297
|
+
let L3r = clamp(floor((vr[3] - n0h.x) * sc2.x + h(0.5)), h4(0.0), h4(7.0));
|
|
1298
|
+
let L0g = clamp(floor((vg[0] - n0h.y) * sc2.y + h(0.5)), h4(0.0), h4(7.0));
|
|
1299
|
+
let L1g = clamp(floor((vg[1] - n0h.y) * sc2.y + h(0.5)), h4(0.0), h4(7.0));
|
|
1300
|
+
let L2g = clamp(floor((vg[2] - n0h.y) * sc2.y + h(0.5)), h4(0.0), h4(7.0));
|
|
1301
|
+
let L3g = clamp(floor((vg[3] - n0h.y) * sc2.y + h(0.5)), h4(0.0), h4(7.0));
|
|
1302
|
+
let iAx = lvl_to_idx(u32(dot(vec4<f32>(L0r), W0) + dot(vec4<f32>(L1r), W1)));
|
|
1303
|
+
let iBx = lvl_to_idx(u32(dot(vec4<f32>(L2r), W0) + dot(vec4<f32>(L3r), W1)));
|
|
1304
|
+
let iAy = lvl_to_idx(u32(dot(vec4<f32>(L0g), W0) + dot(vec4<f32>(L1g), W1)));
|
|
1305
|
+
let iBy = lvl_to_idx(u32(dot(vec4<f32>(L2g), W0) + dot(vec4<f32>(L3g), W1)));
|
|
1313
1306
|
|
|
1314
1307
|
// BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.
|
|
1315
1308
|
let o = bi * 4u;
|
|
@@ -1353,10 +1346,10 @@ var BC5Encoder = class extends Encoder {
|
|
|
1353
1346
|
};
|
|
1354
1347
|
|
|
1355
1348
|
// src/bc7.wgsl
|
|
1356
|
-
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]`. This is the f32 fallback;\n// bc7_fast_f16.wgsl is the same algorithm and is preferred when the device\n// reports shader-f16.\n//\n// ALGORITHM: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) at the exact projection extents, quantised directly \u2014\n// no LSQ refit; with the seed on the principal axis, mode 6's 16-level\n// palette leaves the refit under 0.15 dB, unlike the 4-level BC1/ASTC\n// encoders which keep theirs \u2014 then one pass that projects each pixel onto\n// the endpoint line (the 16 palette entries are colinear, so the nearest\n// index is the rounded projection \u2014 no palette build, no 16-entry search),\n// packed on the fly into two nibble words.\n//\n// A MODE 1 (2-subset) candidate was built and evaluated (2026-07) and\n// dropped: ~+1.3 dB on multi-modal content but up to ~3\xD7 the pass cost on\n// exactly that content \u2014 see bc7_fast_f16.wgsl. The CPU reference decoder\n// keeps mode 1 support (bc7_ref.ts).\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\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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 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)\n// under a fixed p-bit, all four channels at once. q7 = round((ideal8 \u2212 p)/2).\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// 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// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0) for\n// a degenerate (constant) block. Same family as bc1.wgsl's principal_axis \u2014\n// the bbox diagonal alone is sign-blind and points across anti-correlated\n// data (normal maps, hue edges) instead of along it.\nfn principal_axis4(\n c0v: vec4<f32>,\n c1v: vec4<f32>,\n c2v: vec4<f32>,\n c3v: vec4<f32>,\n seed: vec4<f32>,\n) -> vec4<f32> {\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n\n// ------------------------------- Entry --------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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 // with the covariance moments FUSED in: d = px \u2212 pixel0 (first-pixel-\n // relative, so the sums scale with the block's span; d is integer-valued\n // and \u2264255, exact in f32).\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n var gd = 0;\n var p0f = vec4<f32>(0.0);\n var sd = vec4<f32>(0.0);\n var c0v = vec4<f32>(0.0);\n var c1v = vec4<f32>(0.0);\n var c2v = vec4<f32>(0.0);\n var c3v = vec4<f32>(0.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 gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n if (i == 0u) { p0f = vec4<f32>(px); }\n let d = vec4<f32>(px) - p0f;\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0f + sd * (1.0 / 16.0);\n\n // Seed endpoints from the block's principal colour axis at the exact\n // projection extents (see header), quantise, and assign indices in one\n // projection pass.\n // Mean-correct the fused moments: C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16.\n let sd16 = sd * (1.0 / 16.0);\n let r0v = c0v - sd.x * sd16;\n let r1v = c1v - sd.y * sd16;\n let r2v = c2v - sd.z * sd16;\n let r3v = c3v - sd.w * sd16;\n var seed0 = lo;\n var seed1 = hi;\n // Gray + opaque blocks: the axis is analytically (1,1,1,0)/\u221A3 with\n // extents at the luma min/max \u2014 skip iteration + extents pass entirely\n // (see bc7_fast_f16.wgsl).\n let gray = lo.w == 255 && gd == 0;\n if (gray) {\n seed0 = vec4<i32>(lo.x, lo.x, lo.x, 255);\n seed1 = vec4<i32>(hi.x, hi.x, hi.x, 255);\n } else {\n let axis = principal_axis4(r0v, r1v, r2v, r3v, vec4<f32>(hi - lo));\n if (dot(axis, axis) > 0.0) {\n // Exact projection extents along the axis. (A Rayleigh-quotient span\n // estimate was tried in place of this pass \u2014 it saves 16 dots but\n // costs 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3\n // misjudges two-cluster and outlier blocks. The pass stays.)\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(vec4<f32>(pixels[k]) - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed0 = vec4<i32>(clamp(round(mean + t_min * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n seed1 = vec4<i32>(clamp(round(mean + t_max * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n }\n }\n\n // The 16 4-bit indices, packed LSB-first into two nibble words\n // (pixel k \u2192 bits 4k..4k+3).\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\n var ep0 = pick_ep(seed0);\n var ep1 = pick_ep(seed1);\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 var e0_7 = ep0.seven;\n var e1_7 = ep1.seven;\n var p0 = ep0.p;\n var p1 = ep1.p;\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";
|
|
1349
|
+
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]`. This is the f32 fallback;\n// bc7_fast_f16.wgsl is the same algorithm and is preferred when the device\n// reports shader-f16.\n//\n// ALGORITHM: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) at the exact projection extents, quantised directly \u2014\n// no LSQ refit; with the seed on the principal axis, mode 6's 16-level\n// palette leaves the refit under 0.15 dB, unlike the 4-level BC1/ASTC\n// encoders which keep theirs \u2014 then one pass that projects each pixel onto\n// the endpoint line (the 16 palette entries are colinear, so the nearest\n// index is the rounded projection \u2014 no palette build, no 16-entry search),\n// packed on the fly into two nibble words. Gray + opaque blocks take an\n// integer 1-D tail (see bc7_fast_f16.wgsl): lossless for spans \u2264 15 with\n// odd endpoints (alpha exactly 255), alpha-aware scalar LSQ refit above.\n// The covariance moments are accumulated after the load loop, on the colour\n// path only \u2014 fused into the loads they cost this module ~30% GPU on colour\n// content (register pressure).\n//\n// A MODE 1 (2-subset) candidate was built and evaluated (2026-07) and\n// dropped: ~+1.3 dB on multi-modal content but up to ~3\xD7 the pass cost on\n// exactly that content \u2014 see bc7_fast_f16.wgsl. The CPU reference decoder\n// keeps mode 1 support (bc7_ref.ts).\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\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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 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)\n// under a fixed p-bit, all four channels at once. q7 = round((ideal8 \u2212 p)/2).\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// 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// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0) for\n// a degenerate (constant) block. Same family as bc1.wgsl's principal_axis \u2014\n// the bbox diagonal alone is sign-blind and points across anti-correlated\n// data (normal maps, hue edges) instead of along it.\nfn principal_axis4(\n c0v: vec4<f32>,\n c1v: vec4<f32>,\n c2v: vec4<f32>,\n c3v: vec4<f32>,\n seed: vec4<f32>,\n) -> vec4<f32> {\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n\n// ------------------------------- Entry --------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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), the per-channel bbox and\n // the gray test.\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n var gd = 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 gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n\n // Gray + opaque blocks: 1-D, own tail (see bc7_fast_f16.wgsl) \u2014 lossless\n // for spans \u2264 15 with odd endpoints (alpha exactly 255), closed-form\n // scalar LSQ refit with alpha-aware p-bit pricing above that.\n if (lo.w == 255 && gd == 0) {\n let vmin = f32(lo.x);\n let vmax = f32(hi.x);\n var e0 = vmin;\n var e1 = vmax;\n if (vmax - vmin <= 15.0) {\n if (fract(e0 * 0.5) == 0.0 && e0 > 0.0 && e1 - e0 < 15.0) { e0 = e0 - 1.0; }\n if (fract(e1 * 0.5) == 0.0 && e1 < 255.0 && e1 - e0 < 15.0) { e1 = e1 + 1.0; }\n } else {\n let k1 = 15.0 / (vmax - vmin);\n let k0 = 0.5 - vmin * k1;\n var sL = 0.0;\n var sLL = 0.0;\n var sv = 0.0;\n var sLv = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = f32(pixels[k].x);\n let L = floor(v * k1 + k0);\n sL = sL + L;\n sLL = sLL + L * L;\n sv = sv + v;\n sLv = sLv + L * v;\n }\n let C = sLL * (1.0 / 225.0);\n let B = sL * (1.0 / 15.0) - C;\n let A = 16.0 - sL * (2.0 / 15.0) + C;\n let Y = sLv * (1.0 / 15.0);\n let X = sv - Y;\n let det = A * C - B * B;\n if (det > 1e-3) {\n let s0 = clamp((C * X - B * Y) / det, 0.0, 255.0);\n let s1 = clamp((A * Y - B * X) / det, 0.0, 255.0);\n // price(e0, e1, p0, p1) up to the block constant: RGB \xD73 + alpha.\n let ps0 = vmin - 2.0 * floor(vmin * 0.5);\n let ps1 = vmax - 2.0 * floor(vmax * 0.5);\n var best = 3.0 * (A * vmin * vmin + 2.0 * B * vmin * vmax + C * vmax * vmax - 2.0 * (X * vmin + Y * vmax))\n + A * (1.0 - ps0) + 2.0 * B * (1.0 - ps0) * (1.0 - ps1) + C * (1.0 - ps1);\n for (var pc: u32 = 0u; pc < 4u; pc = pc + 1u) {\n let p0 = f32(pc & 1u);\n let p1 = f32(pc >> 1u);\n let c0 = 2.0 * clamp(floor((s0 - p0) * 0.5 + 0.5), 0.0, 127.0) + p0;\n let c1 = 2.0 * clamp(floor((s1 - p1) * 0.5 + 0.5), 0.0, 127.0) + p1;\n let pr = 3.0 * (A * c0 * c0 + 2.0 * B * c0 * c1 + C * c1 * c1 - 2.0 * (X * c0 + Y * c1))\n + A * (1.0 - p0) + 2.0 * B * (1.0 - p0) * (1.0 - p1) + C * (1.0 - p1);\n if (pr < best) {\n best = pr;\n e0 = c0;\n e1 = c1;\n }\n }\n }\n }\n var glo = 0u;\n var ghi = 0u;\n if (e1 != e0) {\n let k1 = 15.0 / (e1 - e0);\n let k0 = 0.5 - e0 * k1;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let sg = clamp(floor(f32(pixels[k].x) * k1 + k0), 0.0, 15.0);\n glo = glo | (u32(sg) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let sg = clamp(floor(f32(pixels[k].x) * k1 + k0), 0.0, 15.0);\n ghi = ghi | (u32(sg) << ((k - 8u) * 4u));\n }\n }\n var u0 = u32(e0);\n var u1 = u32(e1);\n if ((glo & 0x8u) != 0u) {\n let t = u0; u0 = u1; u1 = t;\n glo = ~glo; ghi = ~ghi;\n }\n let q0 = u0 >> 1u;\n let q1 = u1 >> 1u;\n let og = block_index * 4u;\n dst[og] = 0x40u | (q0 << 7u) | (q1 << 14u) | (q0 << 21u) | (q1 << 28u);\n dst[og + 1u] = (q1 >> 4u) | (q0 << 3u) | (q1 << 10u) | (127u << 17u) | (127u << 24u) | ((u0 & 1u) << 31u);\n dst[og + 2u] = (u1 & 1u) | ((glo & 0x7u) << 1u) | (glo & 0xFFFFFFF0u);\n dst[og + 3u] = ghi;\n return;\n }\n\n // Covariance moments: d = px \u2212 pixel0 (first-pixel-relative, so the sums\n // scale with the block's span; d is integer-valued and \u2264255, exact in\n // f32).\n let p0f = vec4<f32>(pixels[0]);\n var sd = vec4<f32>(0.0);\n var c0v = vec4<f32>(0.0);\n var c1v = vec4<f32>(0.0);\n var c2v = vec4<f32>(0.0);\n var c3v = vec4<f32>(0.0);\n for (var i: u32 = 1u; i < 16u; i = i + 1u) {\n let d = vec4<f32>(pixels[i]) - p0f;\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0f + sd * (1.0 / 16.0);\n\n // Seed endpoints from the block's principal colour axis at the exact\n // projection extents (see header), quantise, and assign indices in one\n // projection pass.\n // Mean-correct the fused moments: C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16.\n let sd16 = sd * (1.0 / 16.0);\n let r0v = c0v - sd.x * sd16;\n let r1v = c1v - sd.y * sd16;\n let r2v = c2v - sd.z * sd16;\n let r3v = c3v - sd.w * sd16;\n var seed0 = lo;\n var seed1 = hi;\n {\n let axis = principal_axis4(r0v, r1v, r2v, r3v, vec4<f32>(hi - lo));\n if (dot(axis, axis) > 0.0) {\n // Exact projection extents along the axis. (A Rayleigh-quotient span\n // estimate was tried in place of this pass \u2014 it saves 16 dots but\n // costs 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3\n // misjudges two-cluster and outlier blocks. The pass stays.)\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(vec4<f32>(pixels[k]) - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed0 = vec4<i32>(clamp(round(mean + t_min * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n seed1 = vec4<i32>(clamp(round(mean + t_max * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n }\n }\n\n // The 16 4-bit indices, packed LSB-first into two nibble words\n // (pixel k \u2192 bits 4k..4k+3).\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\n var ep0 = pick_ep(seed0);\n var ep1 = pick_ep(seed1);\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 var e0_7 = ep0.seven;\n var e1_7 = ep1.seven;\n var p0 = ep0.p;\n var p1 = ep1.p;\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";
|
|
1357
1350
|
|
|
1358
1351
|
// src/bc7_fast_f16.wgsl
|
|
1359
|
-
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 (principal-axis\n// seed at the exact projection extents \u2192 quantise \u2192 one projection-based\n// index-assignment pass), tuned for throughput:\n//\n// \u2022 All projection math in f16 ([0,1] domain). ~2\xD7 ALU throughput on\n// f16-capable GPUs. The projection direction is pre-scaled by 32:\n// a shallow block (endpoints ~1/255 apart) has dd = dot(dir,dir) \u2248 1.5e-5,\n// where 15/dd \u2248 10\u2076 overflows f16 (max 65504) to +inf and the products\n// inside the projection dot are subnormal \u2014 the indices turn to garbage\n// (visible as banding on smooth gradients). Scaling dir by 32 multiplies\n// the dots by 32 and dd by 1024; s = dot\xB7(32\xB7L/dd\u2083\u2082) is the same\n// quantity with every intermediate in f16's normal range (worst case\n// inv = 480/0.0157 \u2248 3.0e4 < 65504).\n// \u2022 TWO MODES (mode 4 OPT-IN via the enable_mode4 override constant,\n// default off and dead-coded at pipeline creation \u2014 see the constant's\n// comment for the measured cost/benefit), decided per block BEFORE\n// encoding \u2014 never encoded both:\n// mode 6 (single RGBA line, 4-bit indices) by default, mode 4 (rotation:\n// one channel split into its own scalar plane with 3-bit indices, the\n// remaining three on a 2-bit line) when the principal axis leaves a\n// large share of the block's variance unexplained \u2014 decorrelated data\n// (normal maps, channel-packed atlases) where any single 4-D line fails.\n// The decision reads the covariance already in registers (\u03BB = axis\u1D40Ca,\n// residual = trace \u2212 \u03BB) and costs no extra pass. An encode-both-and-\n// compare trial was priced at ~2\xD7 on exactly this content (see the mode\n// 1 postmortem below) \u2014 deciding first keeps it at ~1.2\xD7.\n// \u2022 The two modes SHARE the per-pixel passes (axis matvecs, projection\n// extents, the index/weight pass runs once with per-thread level count,\n// index width and packing split) so warps holding a mix of mode-4 and\n// mode-6 blocks do not execute two disjoint kernels back to back \u2014 a\n// first cut with separate per-mode passes measured 1.77\xD7 on normal maps\n// from exactly that divergence; the only mode-4-extra 16-pixel work is\n// the cheap scalar-plane pass.\n// \u2022 GRAY + opaque blocks (every texel R == G == B, A == 1) have their\n// principal axis analytically: (1,1,1,0)/\u221A3, with projection extents at\n// the luma min/max. They skip the power iteration AND the extents pass\n// (\u221234% GPU on roughness/AO/displacement content) and always take\n// mode 6 \u2014 a gray single line fits gray data exactly.\n// \u2022 NO least-squares refit, unlike the BC1/BC5/ASTC fast paths: with the\n// seed already on the principal axis at the exact projection extents,\n// mode 6's fine 16-level palette leaves the refit \u22640.05 dB on the colour\n// card, \u22640.15 dB on the normal card and +0.03 dB on the channel-packed\n// packed-materials atlas \u2014 not worth its two extra 16-pixel passes. The\n// coarse 4-level formats DO need it (dropping it there costs 0.5\u20131.3 dB).\n// \u2022 A MODE 1 (2-subset) candidate was built and evaluated (2026-07): it\n// buys ~+1.3 dB on multi-modal content but its candidate evaluation\n// costs up to ~3\xD7 the mode-6 pass on exactly that content \u2014 dropped in\n// favour of the decided (not compared) mode 4 above, which covers the\n// decorrelated-channel share of that content at a fraction of the cost.\n// The CPU reference decoder keeps mode 1 support (bc7_ref.ts).\n// \u2022 Indices are packed into two u32 words ON THE FLY during the\n// projection pass \u2014 no array<u32,16> private array. The BC7 anchor\n// reflection is then just a bitwise NOT of the packed 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.\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)\n// MODE 4 BIT LAYOUT (LSB-first): mode 0b00001, rotation @5 (channel swapped\n// with alpha), idxMode @7 (0 = colour \u2192 2-bit set, scalar \u2192 3-bit set),\n// colour endpoints 6\xD75 bits @8, alpha endpoints 2\xD76 @38, 31-bit 2-bit index\n// field @50 (pixel 0 anchored to 1 bit), 47-bit 3-bit index field @81\n// (pixel 0 anchored to 2 bits). Validated bit-exact against hardware\n// bc7-rgba-unorm sampling; decode reference in bc7_ref.ts.\nenable f16;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, y0: 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// Mode-4 gate: encode mode 4 when the principal axis leaves more than\n// MODE4_THETA of the (\xD7256-scaled) total variance unexplained and the block\n// isn't near-flat. Tuned against per-content mode histograms and PSNR.\nconst MODE4_THETA: f16 = 0.2;\nconst MODE4_FLOOR: f16 = 1.0;\nconst MODE4_CONC: f16 = 0.5;\n\n// OPT-IN adaptive mode 4, folded at pipeline creation (WebGPU override\n// constant; default OFF dead-codes the whole path \u2014 measured at exact par\n// with the mode-6-only kernel). Rationale: the quality is real (+2.5\u20132.9 dB\n// on normal maps, +1.9\u20132.4 on channel-packed atlases) but any warp holding\n// one mode-4 block executes both modes' passes, and content that benefits\n// runs 1.4\u20131.5\xD7; a \u03B8 sweep showed quality and warp-poisoning scale together\n// (no per-block middle ground without subgroup ballots). So the trade is\n// the CALLER's: BC7Encoder({ adaptiveMode4: true }).\noverride enable_mode4: bool = false;\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@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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 // Load pass, with the covariance moments FUSED in (no separate 16-pixel\n // pass): d = (px \u2212 pixel0)\xB716, relative to the block's first pixel so the\n // accumulators scale with the block's span \u2014 raw \u03A3v\xB7v\u1D40 moments would\n // cancel catastrophically in f16 \u2014 and pre-scaled \xD716 so shallow blocks\n // (span ~1/255 \u2192 d\xB2 \u2248 1e-3) clear the subnormal floor while full-range\n // sums stay \u22644096. C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16 is the \xD7256-scaled covariance.\n var pix: array<h4, 16>;\n var lo = h4(1.0);\n var hi = h4(0.0);\n var gd = h(0.0);\n var p0v = h4(0.0);\n var sd = h4(0.0);\n var c0v = h4(0.0);\n var c1v = h4(0.0);\n var c2v = h4(0.0);\n var c3v = 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 gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n if (i == 0u) { p0v = px; }\n let d = (px - p0v) * h(16.0);\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0v + sd * h(1.0 / 256.0);\n // Mean-correction via sd4\xB7sd4\u1D40 with sd4 = \u03A3d/4: (\u03A3d)(\u03A3d)\u1D40/16 with every\n // product \u22644096 (a direct \u03A3d\xB7\u03A3d\u1D40 could hit 65536 and overflow f16).\n let sd4 = sd * h(0.25);\n c0v = c0v - sd4.x * sd4;\n c1v = c1v - sd4.y * sd4;\n c2v = c2v - sd4.z * sd4;\n c3v = c3v - sd4.w * sd4;\n\n // Seed endpoints + per-block mode decision (see header).\n var seed_lo = lo;\n var seed_hi = hi;\n var use4 = false;\n var cmask = h4(1.0);\n var ch = 0u;\n if (lo.w == h(1.0) && gd == h(0.0)) {\n // GRAY + opaque: analytic axis (1,1,1,0)/\u221A3, extents at luma min/max,\n // always mode 6 \u2014 and a fully specialised tail: gray textures are\n // warp-uniform, and routing them through the parametric shared loop\n // below (runtime index width/split) measured +28% on displacement\n // content purely from the lost constant-shift codegen.\n var ep0g = pick_ep(h4(lo.x, lo.x, lo.x, h(1.0)));\n var ep1g = pick_ep(h4(hi.x, hi.x, hi.x, h(1.0)));\n var ilo = 0u;\n var ihi = 0u;\n let dirg = (ep1g.eight - ep0g.eight) * h(32.0);\n let ddg = dot(dirg, dirg);\n if (ddg >= h(0.008)) {\n let invg = h(480.0) / ddg;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let sg = clamp(floor(dot(pix[k] - ep0g.eight, dirg) * invg + h(0.5)), h(0.0), h(15.0));\n ilo = ilo | (u32(sg) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let sg = clamp(floor(dot(pix[k] - ep0g.eight, dirg) * invg + h(0.5)), h(0.0), h(15.0));\n ihi = ihi | (u32(sg) << ((k - 8u) * 4u));\n }\n }\n if ((ilo & 0x8u) != 0u) {\n let t = ep0g; ep0g = ep1g; ep1g = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let e0g = ep0g.seven;\n let e1g = ep1g.seven;\n let og = bi * 4u;\n dst[og] = 0x40u | (e0g.x << 7u) | (e1g.x << 14u) | (e0g.y << 21u) | (e1g.y << 28u);\n dst[og + 1u] = (e1g.y >> 4u) | (e0g.z << 3u) | (e1g.z << 10u) | (e0g.w << 17u) | (e1g.w << 24u) | (ep0g.p << 31u);\n dst[og + 2u] = ep1g.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[og + 3u] = ihi;\n return;\n }\n {\n var axis = hi - lo;\n var axis_ok = true;\n // 8 iterations: 4 was under-converged on noisy 4-D blocks (heavily\n // downscaled photographic/channel-packed content) \u2014 going to 8 measured\n // +0.75 dB on the normal card, +0.12 colour, +0.08 packed-materials, and\n // matches the f32 fallback's iteration count. Four extra 4-dot matvecs\n // per block are noise next to the index pass.\n for (var it: u32 = 0u; it < 8u; it = it + 1u) {\n let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { axis_ok = false; break; }\n axis = nv / m;\n }\n if (axis_ok) {\n axis = axis / length(axis);\n var axisF = axis;\n\n // Mode decision from the covariance already in registers: \u03BB is the\n // variance the mode-6 line explains, trace \u2212 \u03BB what it cannot.\n let Ca = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let lam = dot(Ca, axis);\n let diag = h4(c0v.x, c1v.y, c2v.z, c3v.w);\n let trace = diag.x + diag.y + diag.z + diag.w;\n let resid = trace - lam;\n let rc = diag - lam * axis * axis;\n var rbest = rc.x;\n if (rc.y > rbest) { ch = 1u; rbest = rc.y; }\n if (rc.z > rbest) { ch = 2u; rbest = rc.z; }\n if (rc.w > rbest) { ch = 3u; rbest = rc.w; }\n use4 = enable_mode4 && resid > MODE4_THETA * trace && trace > MODE4_FLOOR && rbest > MODE4_CONC * resid;\n if (use4) {\n // The colour plane is the remaining three channels, handled as\n // masked 4-vectors so every vec4 pass below applies unchanged.\n // Branchless mask build \u2014 a dynamic component store spills the\n // vector to scratch on some compilers.\n cmask = h4(1.0) - h4(h(f32(u32(ch == 0u))), h(f32(u32(ch == 1u))), h(f32(u32(ch == 2u))), h(f32(u32(ch == 3u))));\n var a3 = (hi - lo) * cmask;\n var ok3 = true;\n for (var it: u32 = 0u; it < 2u; it = it + 1u) {\n let nv = h4(dot(c0v, a3), dot(c1v, a3), dot(c2v, a3), dot(c3v, a3)) * cmask;\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { ok3 = false; break; }\n a3 = nv / m;\n }\n if (ok3) {\n axisF = a3 / length(a3);\n } else {\n use4 = false;\n cmask = h4(1.0);\n }\n }\n\n // Exact projection extents along the fit axis \u2014 ONE shared pass for\n // both modes (for mode 4 axisF[ch] = 0, so the scalar plane is\n // invisible to it). (A Rayleigh-quotient span estimate was tried in\n // place of this pass \u2014 it saves 16 dots but costs 0.1\u20130.8 dB and\n // 4\u201310\xD7 on the worst-easy-block gate.)\n var t_min = h(4.0);\n var t_max = h(-4.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pix[k] - mean, axisF);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed_lo = clamp(mean + t_min * axisF, h4(0.0), h4(1.0));\n seed_hi = clamp(mean + t_max * axisF, h4(0.0), h4(1.0));\n }\n }\n\n // Endpoints, per mode. d0/d1 are the DECODED values the weight pass\n // projects against.\n var ep0: Ep;\n var ep1: Ep;\n var q0c = vec4<u32>(0u);\n var q1c = vec4<u32>(0u);\n var A0 = 0u;\n var A1 = 0u;\n var iA = 0u;\n var iB = 0u;\n var d0: h4;\n var d1: h4;\n var d0a = h(0.0);\n var sca = h(0.0);\n let chs = h4(1.0) - cmask;\n ep0 = pick_ep(seed_lo);\n ep1 = pick_ep(seed_hi);\n d0 = ep0.eight;\n d1 = ep1.eight;\n if (use4) {\n // Scalar plane (3-bit index set): 6-bit endpoints at the channel's\n // exact extremes. Its projection is FUSED into the shared weight pass\n // below \u2014 a separate 16-pixel pass here measured +46% on normal maps\n // (mixed warps paid it wholesale); fused, the marginal cost is one dot\n // per pixel under a warp-uniform predicate.\n let a0q = u32(floor(dot(lo, chs) * h(63.0) + h(0.5)));\n let a1q = u32(floor(dot(hi, chs) * h(63.0) + h(0.5)));\n A0 = a0q;\n A1 = a1q;\n let d0av = h(f32((a0q << 2u) | (a0q >> 4u))) * h(1.0 / 255.0);\n let d1av = h(f32((a1q << 2u) | (a1q >> 4u))) * h(1.0 / 255.0);\n let aspan = d1av - d0av;\n if (aspan > h(0.001)) {\n d0a = d0av;\n sca = h(7.0) / aspan;\n }\n // Colour plane: 5-bit endpoints from the masked extents seed.\n q0c = vec4<u32>(clamp(floor(seed_lo * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n q1c = vec4<u32>(clamp(floor(seed_hi * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n d0 = h4(vec4<f32>((q0c << vec4<u32>(3u)) | (q0c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n d1 = h4(vec4<f32>((q1c << vec4<u32>(3u)) | (q1c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n }\n\n // Index/weight pass: per-mode SPECIALISED loops (constant level counts\n // and shifts, so each unrolls cleanly \u2014 a single parametric loop with\n // runtime width/split measured +22% on pure mode-6 photo content).\n // Mixed warps execute both loops; the mode-4 one carries the fused\n // scalar-plane projection. For mode 4 pix[ch]\xB7dir[ch] = 0, so the\n // scalar plane never perturbs the colour projection.\n var a_lo = 0u;\n var a_hi = 0u;\n // Same \xD732 pre-scale as the extents math; distinct quantised endpoints\n // are \u22651/255 apart (dd\u2083\u2082 \u2265 0.0157), so the flat-block threshold only\n // catches truly identical ones.\n let dir = (d1 - d0) * h(32.0);\n let dd = dot(dir, dir);\n let live = dd >= h(0.008);\n if (use4) {\n if (live) {\n let inv = h(96.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iA = iA | (u32(sv) << (k * 3u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iB = iB | (u32(sv) << ((k - 8u) * 3u));\n }\n }\n } else if (live) {\n let inv = h(480.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_lo = a_lo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_hi = a_hi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n\n // Anchors + packing. Mode 6 packs unconditionally (one-sided branches\n // compile better than two-sided divergence); mode-4 threads overwrite.\n let o = bi * 4u;\n {\n var ilo = a_lo;\n var ihi = a_hi;\n if ((ilo & 0x8u) != 0u) {\n let t = ep0; ep0 = ep1; ep1 = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let e0 = ep0.seven;\n let e1 = ep1.seven;\n dst[o] = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n dst[o + 1u] = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);\n dst[o + 2u] = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[o + 3u] = ihi;\n }\n if (use4) {\n // 3-bit anchor: pixel 0's MSB must be 0; reflect = bitwise NOT.\n if ((iA & 4u) != 0u) {\n let tA = A0;\n A0 = A1;\n A1 = tA;\n iA = ~iA & 0xFFFFFFu;\n iB = ~iB & 0xFFFFFFu;\n }\n var c2 = a_lo;\n // 2-bit anchor: pixel 0's MSB must be 0.\n if ((c2 & 2u) != 0u) {\n let tq = q0c;\n q0c = q1c;\n q1c = tq;\n c2 = ~c2;\n }\n // Rotated-space RGB: position ch carries the original alpha.\n let R0 = select(q0c.x, q0c.w, ch == 0u);\n let G0 = select(q0c.y, q0c.w, ch == 1u);\n let B0 = select(q0c.z, q0c.w, ch == 2u);\n let R1 = select(q1c.x, q1c.w, ch == 0u);\n let G1 = select(q1c.y, q1c.w, ch == 1u);\n let B1 = select(q1c.z, q1c.w, ch == 2u);\n let rot = (ch + 1u) & 3u;\n // Index fields drop the anchors' MSBs: 31 bits (2-bit set) and 47 bits\n // (3-bit set).\n let field2 = (c2 & 1u) | ((c2 >> 2u) << 1u);\n let f_lo = (iA & 3u) | ((iA >> 3u) << 2u) | (iB << 23u);\n let f_hi = iB >> 9u;\n dst[o] = 0x10u | (rot << 5u) | (R0 << 8u) | (R1 << 13u) | (G0 << 18u) | (G1 << 23u) | (B0 << 28u);\n dst[o + 1u] = (B0 >> 4u) | (B1 << 1u) | (A0 << 6u) | (A1 << 12u) | ((field2 & 0x3FFFu) << 18u);\n dst[o + 2u] = (field2 >> 14u) | (f_lo << 17u);\n dst[o + 3u] = (f_lo >> 15u) | (f_hi << 17u);\n }\n}\n";
|
|
1352
|
+
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 (principal-axis\n// seed at the exact projection extents \u2192 quantise \u2192 one projection-based\n// index-assignment pass), tuned for throughput:\n//\n// \u2022 All projection math in f16 ([0,1] domain). ~2\xD7 ALU throughput on\n// f16-capable GPUs. The projection direction is pre-scaled by 32:\n// a shallow block (endpoints ~1/255 apart) has dd = dot(dir,dir) \u2248 1.5e-5,\n// where 15/dd \u2248 10\u2076 overflows f16 (max 65504) to +inf and the products\n// inside the projection dot are subnormal \u2014 the indices turn to garbage\n// (visible as banding on smooth gradients). Scaling dir by 32 multiplies\n// the dots by 32 and dd by 1024; s = dot\xB7(32\xB7L/dd\u2083\u2082) is the same\n// quantity with every intermediate in f16's normal range (worst case\n// inv = 480/0.0157 \u2248 3.0e4 < 65504).\n// \u2022 TWO MODES (mode 4 OPT-IN via the enable_mode4 override constant,\n// default off and dead-coded at pipeline creation \u2014 see the constant's\n// comment for the measured cost/benefit), decided per block BEFORE\n// encoding \u2014 never encoded both:\n// mode 6 (single RGBA line, 4-bit indices) by default, mode 4 (rotation:\n// one channel split into its own scalar plane with 3-bit indices, the\n// remaining three on a 2-bit line) when the principal axis leaves a\n// large share of the block's variance unexplained \u2014 decorrelated data\n// (normal maps, channel-packed atlases) where any single 4-D line fails.\n// The decision reads the covariance already in registers (\u03BB = axis\u1D40Ca,\n// residual = trace \u2212 \u03BB) and costs no extra pass. An encode-both-and-\n// compare trial was priced at ~2\xD7 on exactly this content (see the mode\n// 1 postmortem below) \u2014 deciding first keeps it at ~1.2\xD7.\n// \u2022 The two modes SHARE the per-pixel passes (axis matvecs, projection\n// extents, the index/weight pass runs once with per-thread level count,\n// index width and packing split) so warps holding a mix of mode-4 and\n// mode-6 blocks do not execute two disjoint kernels back to back \u2014 a\n// first cut with separate per-mode passes measured 1.77\xD7 on normal maps\n// from exactly that divergence; the only mode-4-extra 16-pixel work is\n// the cheap scalar-plane pass.\n// \u2022 GRAY + opaque blocks (every texel R == G == B, A == 1) are a 1-D\n// problem and take their own tail in the integer domain (no power\n// iteration, no extents pass, no covariance \u2014 \u221234% GPU on roughness/\n// AO/displacement content vs the generic path):\n// \u2013 span \u2264 15: LOSSLESS. With integer endpoints \u2264 15 apart, mode 6's\n// rounded palette covers every integer between them and\n// round(15\xB7(v \u2212 e0)/d) selects it (exhaustively verified, either\n// tie rounding). Even endpoints step outward while the span stays\n// \u2264 15 so both p-bits are 1 and alpha decodes to exactly 255 \u2014\n// free for RGB (alpha = 254 + p is otherwise wrong on up to half\n// the texels, which capped RGBA PSNR at ~51 dB on smooth maps).\n// \u2013 span > 15: closed-form scalar LSQ refit off BC5-style moments\n// (\u03A3L, \u03A3L\xB2, \u03A3v, \u03A3L\xB7v of the seed levels), with all four p-bit\n// combinations priced INCLUDING the alpha term, accept-if-better.\n// RGBA PSNR vs the previous analytic-axis tail (/eval 2026-09): rock\n// displacement 4K +19.3 dB, wood displacement +3.6, AO +2.0,\n// roughness +0.8..+1.7; RGB alone improves too. GPU cost \u2248 0: the\n// covariance moved out of the load loop into the colour path pays for\n// the refit, and the index pass packs levels as float nibble fields.\n// \u2022 NO least-squares refit, unlike the BC1/BC5/ASTC fast paths: with the\n// seed already on the principal axis at the exact projection extents,\n// mode 6's fine 16-level palette leaves the refit \u22640.05 dB on the colour\n// card, \u22640.15 dB on the normal card and +0.03 dB on the channel-packed\n// packed-materials atlas \u2014 not worth its two extra 16-pixel passes. The\n// coarse 4-level formats DO need it (dropping it there costs 0.5\u20131.3 dB).\n// \u2022 A MODE 1 (2-subset) candidate was built and evaluated (2026-07): it\n// buys ~+1.3 dB on multi-modal content but its candidate evaluation\n// costs up to ~3\xD7 the mode-6 pass on exactly that content \u2014 dropped in\n// favour of the decided (not compared) mode 4 above, which covers the\n// decorrelated-channel share of that content at a fraction of the cost.\n// The CPU reference decoder keeps mode 1 support (bc7_ref.ts).\n// \u2022 Indices are packed into two u32 words ON THE FLY during the\n// projection pass \u2014 no array<u32,16> private array. The BC7 anchor\n// reflection is then just a bitwise NOT of the packed 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.\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)\n// MODE 4 BIT LAYOUT (LSB-first): mode 0b00001, rotation @5 (channel swapped\n// with alpha), idxMode @7 (0 = colour \u2192 2-bit set, scalar \u2192 3-bit set),\n// colour endpoints 6\xD75 bits @8, alpha endpoints 2\xD76 @38, 31-bit 2-bit index\n// field @50 (pixel 0 anchored to 1 bit), 47-bit 3-bit index field @81\n// (pixel 0 anchored to 2 bits). Validated bit-exact against hardware\n// bc7-rgba-unorm sampling; decode reference in bc7_ref.ts.\nenable f16;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, y0: 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// Mode-4 gate: encode mode 4 when the principal axis leaves more than\n// MODE4_THETA of the (\xD7256-scaled) total variance unexplained and the block\n// isn't near-flat. Tuned against per-content mode histograms and PSNR.\nconst MODE4_THETA: f16 = 0.2;\nconst MODE4_FLOOR: f16 = 1.0;\nconst MODE4_CONC: f16 = 0.5;\n\n// OPT-IN adaptive mode 4, folded at pipeline creation (WebGPU override\n// constant; default OFF dead-codes the whole path \u2014 measured at exact par\n// with the mode-6-only kernel). Rationale: the quality is real (+2.5\u20132.9 dB\n// on normal maps, +1.9\u20132.4 on channel-packed atlases) but any warp holding\n// one mode-4 block executes both modes' passes, and content that benefits\n// runs 1.4\u20131.5\xD7; a \u03B8 sweep showed quality and warp-poisoning scale together\n// (no per-block middle ground without subgroup ballots). So the trade is\n// the CALLER's: BC7Encoder({ adaptiveMode4: true }).\noverride enable_mode4: bool = false;\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@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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 // Load pass: texels, bbox and the gray test. The covariance moments are\n // accumulated only on the colour path below (gray blocks never use them).\n var pix: array<h4, 16>;\n var lo = h4(1.0);\n var hi = h4(0.0);\n var gd = h(0.0);\n var p0v = h4(0.0);\n var sd = h4(0.0);\n var c0v = h4(0.0);\n var c1v = h4(0.0);\n var c2v = h4(0.0);\n var c3v = 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 gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n\n // Seed endpoints + per-block mode decision (see header).\n var seed_lo = lo;\n var seed_hi = hi;\n var use4 = false;\n var cmask = h4(1.0);\n var ch = 0u;\n if (lo.w == h(1.0) && gd == h(0.0)) {\n // GRAY + opaque: always mode 6, fully specialised tail (see header) \u2014\n // gray textures are warp-uniform, and routing them through the\n // parametric shared loop below (runtime index width/split) measured\n // +28% on displacement content purely from the lost constant-shift\n // codegen. Integer domain: f16 holds k/255 to \xB10.06 levels, so the\n // rounding recovers the exact 8-bit value. 8-bit endpoints E = 2q + p;\n // RGB share q, alpha is 254 + p.\n var gv: array<f32, 16>;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) { gv[k] = floor(f32(pix[k].x) * 255.0 + 0.5); }\n let vmin = floor(f32(lo.x) * 255.0 + 0.5);\n let vmax = floor(f32(hi.x) * 255.0 + 0.5);\n var e0 = vmin;\n var e1 = vmax;\n if (vmax - vmin <= 15.0) {\n // LOSSLESS: with integer endpoints \u2264 15 apart, the rounded palette\n // covers every integer in [e0, e1] and round(15\xB7(v \u2212 e0)/d) picks it\n // (exhaustively verified, either tie rounding). Odd endpoints keep\n // alpha exactly 255 (p = 1), so even ones step outward while the\n // span stays \u2264 15 \u2014 free for RGB.\n if (fract(e0 * 0.5) == 0.0 && e0 > 0.0 && e1 - e0 < 15.0) { e0 = e0 - 1.0; }\n if (fract(e1 * 0.5) == 0.0 && e1 < 255.0 && e1 - e0 < 15.0) { e1 = e1 + 1.0; }\n } else {\n // Closed-form scalar LSQ refit (BC5-style moments): seed levels\n // L = round(15\xB7(v \u2212 vmin)/d) against the exact extremes, then the\n // endpoint pair minimising \u03A3(v \u2212 (1\u2212t)e0 \u2212 t\xB7e1)\xB2 (t = L/15), priced\n // for all four p-bit combinations INCLUDING the alpha channel\n // (254 + p vs 255) and accepted only if it beats the seed.\n let k1 = 15.0 / (vmax - vmin);\n let k0 = 0.5 - vmin * k1;\n var sL = 0.0;\n var sLL = 0.0;\n var sv = 0.0;\n var sLv = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = gv[k];\n let L = floor(v * k1 + k0);\n sL = sL + L;\n sLL = sLL + L * L;\n sv = sv + v;\n sLv = sLv + L * v;\n }\n let C = sLL * (1.0 / 225.0);\n let B = sL * (1.0 / 15.0) - C;\n let A = 16.0 - sL * (2.0 / 15.0) + C;\n let Y = sLv * (1.0 / 15.0);\n let X = sv - Y;\n let det = A * C - B * B;\n if (det > 1e-3) {\n let s0 = clamp((C * X - B * Y) / det, 0.0, 255.0);\n let s1 = clamp((A * Y - B * X) / det, 0.0, 255.0);\n // price(e0, e1, p0, p1) \u2212 \u03A3v\xB2\xB73, RGB \xD73 plus alpha.\n let ps0 = vmin - 2.0 * floor(vmin * 0.5);\n let ps1 = vmax - 2.0 * floor(vmax * 0.5);\n var best = 3.0 * (A * vmin * vmin + 2.0 * B * vmin * vmax + C * vmax * vmax - 2.0 * (X * vmin + Y * vmax))\n + A * (1.0 - ps0) + 2.0 * B * (1.0 - ps0) * (1.0 - ps1) + C * (1.0 - ps1);\n for (var pc: u32 = 0u; pc < 4u; pc = pc + 1u) {\n let p0 = f32(pc & 1u);\n let p1 = f32(pc >> 1u);\n let c0 = 2.0 * clamp(floor((s0 - p0) * 0.5 + 0.5), 0.0, 127.0) + p0;\n let c1 = 2.0 * clamp(floor((s1 - p1) * 0.5 + 0.5), 0.0, 127.0) + p1;\n let pr = 3.0 * (A * c0 * c0 + 2.0 * B * c0 * c1 + C * c1 * c1 - 2.0 * (X * c0 + Y * c1))\n + A * (1.0 - p0) + 2.0 * B * (1.0 - p0) * (1.0 - p1) + C * (1.0 - p1);\n if (pr < best) {\n best = pr;\n e0 = c0;\n e1 = c1;\n }\n }\n }\n }\n // Index pass against the final endpoints: levels accumulate as float\n // nibble fields (\u2264 2^24, exact), 6 + 6 + 4 pixels per accumulator.\n var ilo = 0u;\n var ihi = 0u;\n if (e1 != e0) {\n let k1 = 15.0 / (e1 - e0);\n let k0 = 0.5 - e0 * k1;\n var fa = 0.0;\n var fb = 0.0;\n var fc = 0.0;\n var w = 1.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let sg = clamp(floor(gv[k] * k1 + k0), 0.0, 15.0);\n if (k < 6u) { fa = fa + sg * w; } else if (k < 12u) { fb = fb + sg * w; } else { fc = fc + sg * w; }\n w = select(w * 16.0, 1.0, k == 5u || k == 11u);\n }\n let ua = u32(fa);\n let ub = u32(fb);\n let uc = u32(fc);\n ilo = ua | (ub << 24u);\n ihi = (ub >> 8u) | (uc << 16u);\n }\n var u0 = u32(e0);\n var u1 = u32(e1);\n if ((ilo & 0x8u) != 0u) {\n let t = u0; u0 = u1; u1 = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let q0 = u0 >> 1u;\n let q1 = u1 >> 1u;\n let og = bi * 4u;\n dst[og] = 0x40u | (q0 << 7u) | (q1 << 14u) | (q0 << 21u) | (q1 << 28u);\n dst[og + 1u] = (q1 >> 4u) | (q0 << 3u) | (q1 << 10u) | (127u << 17u) | (127u << 24u) | ((u0 & 1u) << 31u);\n dst[og + 2u] = (u1 & 1u) | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[og + 3u] = ihi;\n return;\n }\n // Covariance moments: d = (px \u2212 pixel0)\xB716, relative to the block's first\n // pixel so the accumulators scale with the block's span \u2014 raw \u03A3v\xB7v\u1D40\n // moments would cancel catastrophically in f16 \u2014 and pre-scaled \xD716 so\n // shallow blocks (span ~1/255 \u2192 d\xB2 \u2248 1e-3) clear the subnormal floor\n // while full-range sums stay \u22644096. C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16 is the\n // \xD7256-scaled covariance.\n p0v = pix[0];\n for (var i: u32 = 1u; i < 16u; i = i + 1u) {\n let d = (pix[i] - p0v) * h(16.0);\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0v + sd * h(1.0 / 256.0);\n // Mean-correction via sd4\xB7sd4\u1D40 with sd4 = \u03A3d/4: (\u03A3d)(\u03A3d)\u1D40/16 with every\n // product \u22644096 (a direct \u03A3d\xB7\u03A3d\u1D40 could hit 65536 and overflow f16).\n let sd4 = sd * h(0.25);\n c0v = c0v - sd4.x * sd4;\n c1v = c1v - sd4.y * sd4;\n c2v = c2v - sd4.z * sd4;\n c3v = c3v - sd4.w * sd4;\n {\n var axis = hi - lo;\n var axis_ok = true;\n // 8 iterations: 4 was under-converged on noisy 4-D blocks (heavily\n // downscaled photographic/channel-packed content) \u2014 going to 8 measured\n // +0.75 dB on the normal card, +0.12 colour, +0.08 packed-materials, and\n // matches the f32 fallback's iteration count. Four extra 4-dot matvecs\n // per block are noise next to the index pass.\n for (var it: u32 = 0u; it < 8u; it = it + 1u) {\n let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { axis_ok = false; break; }\n axis = nv / m;\n }\n if (axis_ok) {\n axis = axis / length(axis);\n var axisF = axis;\n\n // Mode decision from the covariance already in registers: \u03BB is the\n // variance the mode-6 line explains, trace \u2212 \u03BB what it cannot.\n let Ca = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let lam = dot(Ca, axis);\n let diag = h4(c0v.x, c1v.y, c2v.z, c3v.w);\n let trace = diag.x + diag.y + diag.z + diag.w;\n let resid = trace - lam;\n let rc = diag - lam * axis * axis;\n var rbest = rc.x;\n if (rc.y > rbest) { ch = 1u; rbest = rc.y; }\n if (rc.z > rbest) { ch = 2u; rbest = rc.z; }\n if (rc.w > rbest) { ch = 3u; rbest = rc.w; }\n use4 = enable_mode4 && resid > MODE4_THETA * trace && trace > MODE4_FLOOR && rbest > MODE4_CONC * resid;\n if (use4) {\n // The colour plane is the remaining three channels, handled as\n // masked 4-vectors so every vec4 pass below applies unchanged.\n // Branchless mask build \u2014 a dynamic component store spills the\n // vector to scratch on some compilers.\n cmask = h4(1.0) - h4(h(f32(u32(ch == 0u))), h(f32(u32(ch == 1u))), h(f32(u32(ch == 2u))), h(f32(u32(ch == 3u))));\n var a3 = (hi - lo) * cmask;\n var ok3 = true;\n for (var it: u32 = 0u; it < 2u; it = it + 1u) {\n let nv = h4(dot(c0v, a3), dot(c1v, a3), dot(c2v, a3), dot(c3v, a3)) * cmask;\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { ok3 = false; break; }\n a3 = nv / m;\n }\n if (ok3) {\n axisF = a3 / length(a3);\n } else {\n use4 = false;\n cmask = h4(1.0);\n }\n }\n\n // Exact projection extents along the fit axis \u2014 ONE shared pass for\n // both modes (for mode 4 axisF[ch] = 0, so the scalar plane is\n // invisible to it). (A Rayleigh-quotient span estimate was tried in\n // place of this pass \u2014 it saves 16 dots but costs 0.1\u20130.8 dB and\n // 4\u201310\xD7 on the worst-easy-block gate.)\n var t_min = h(4.0);\n var t_max = h(-4.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pix[k] - mean, axisF);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed_lo = clamp(mean + t_min * axisF, h4(0.0), h4(1.0));\n seed_hi = clamp(mean + t_max * axisF, h4(0.0), h4(1.0));\n }\n }\n\n // Endpoints, per mode. d0/d1 are the DECODED values the weight pass\n // projects against.\n var ep0: Ep;\n var ep1: Ep;\n var q0c = vec4<u32>(0u);\n var q1c = vec4<u32>(0u);\n var A0 = 0u;\n var A1 = 0u;\n var iA = 0u;\n var iB = 0u;\n var d0: h4;\n var d1: h4;\n var d0a = h(0.0);\n var sca = h(0.0);\n let chs = h4(1.0) - cmask;\n ep0 = pick_ep(seed_lo);\n ep1 = pick_ep(seed_hi);\n d0 = ep0.eight;\n d1 = ep1.eight;\n if (use4) {\n // Scalar plane (3-bit index set): 6-bit endpoints at the channel's\n // exact extremes. Its projection is FUSED into the shared weight pass\n // below \u2014 a separate 16-pixel pass here measured +46% on normal maps\n // (mixed warps paid it wholesale); fused, the marginal cost is one dot\n // per pixel under a warp-uniform predicate.\n let a0q = u32(floor(dot(lo, chs) * h(63.0) + h(0.5)));\n let a1q = u32(floor(dot(hi, chs) * h(63.0) + h(0.5)));\n A0 = a0q;\n A1 = a1q;\n let d0av = h(f32((a0q << 2u) | (a0q >> 4u))) * h(1.0 / 255.0);\n let d1av = h(f32((a1q << 2u) | (a1q >> 4u))) * h(1.0 / 255.0);\n let aspan = d1av - d0av;\n if (aspan > h(0.001)) {\n d0a = d0av;\n sca = h(7.0) / aspan;\n }\n // Colour plane: 5-bit endpoints from the masked extents seed.\n q0c = vec4<u32>(clamp(floor(seed_lo * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n q1c = vec4<u32>(clamp(floor(seed_hi * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n d0 = h4(vec4<f32>((q0c << vec4<u32>(3u)) | (q0c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n d1 = h4(vec4<f32>((q1c << vec4<u32>(3u)) | (q1c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n }\n\n // Index/weight pass: per-mode SPECIALISED loops (constant level counts\n // and shifts, so each unrolls cleanly \u2014 a single parametric loop with\n // runtime width/split measured +22% on pure mode-6 photo content).\n // Mixed warps execute both loops; the mode-4 one carries the fused\n // scalar-plane projection. For mode 4 pix[ch]\xB7dir[ch] = 0, so the\n // scalar plane never perturbs the colour projection.\n var a_lo = 0u;\n var a_hi = 0u;\n // Same \xD732 pre-scale as the extents math; distinct quantised endpoints\n // are \u22651/255 apart (dd\u2083\u2082 \u2265 0.0157), so the flat-block threshold only\n // catches truly identical ones.\n let dir = (d1 - d0) * h(32.0);\n let dd = dot(dir, dir);\n let live = dd >= h(0.008);\n if (use4) {\n if (live) {\n let inv = h(96.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iA = iA | (u32(sv) << (k * 3u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iB = iB | (u32(sv) << ((k - 8u) * 3u));\n }\n }\n } else if (live) {\n let inv = h(480.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_lo = a_lo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_hi = a_hi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n\n // Anchors + packing. Mode 6 packs unconditionally (one-sided branches\n // compile better than two-sided divergence); mode-4 threads overwrite.\n let o = bi * 4u;\n {\n var ilo = a_lo;\n var ihi = a_hi;\n if ((ilo & 0x8u) != 0u) {\n let t = ep0; ep0 = ep1; ep1 = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let e0 = ep0.seven;\n let e1 = ep1.seven;\n dst[o] = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n dst[o + 1u] = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);\n dst[o + 2u] = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[o + 3u] = ihi;\n }\n if (use4) {\n // 3-bit anchor: pixel 0's MSB must be 0; reflect = bitwise NOT.\n if ((iA & 4u) != 0u) {\n let tA = A0;\n A0 = A1;\n A1 = tA;\n iA = ~iA & 0xFFFFFFu;\n iB = ~iB & 0xFFFFFFu;\n }\n var c2 = a_lo;\n // 2-bit anchor: pixel 0's MSB must be 0.\n if ((c2 & 2u) != 0u) {\n let tq = q0c;\n q0c = q1c;\n q1c = tq;\n c2 = ~c2;\n }\n // Rotated-space RGB: position ch carries the original alpha.\n let R0 = select(q0c.x, q0c.w, ch == 0u);\n let G0 = select(q0c.y, q0c.w, ch == 1u);\n let B0 = select(q0c.z, q0c.w, ch == 2u);\n let R1 = select(q1c.x, q1c.w, ch == 0u);\n let G1 = select(q1c.y, q1c.w, ch == 1u);\n let B1 = select(q1c.z, q1c.w, ch == 2u);\n let rot = (ch + 1u) & 3u;\n // Index fields drop the anchors' MSBs: 31 bits (2-bit set) and 47 bits\n // (3-bit set).\n let field2 = (c2 & 1u) | ((c2 >> 2u) << 1u);\n let f_lo = (iA & 3u) | ((iA >> 3u) << 2u) | (iB << 23u);\n let f_hi = iB >> 9u;\n dst[o] = 0x10u | (rot << 5u) | (R0 << 8u) | (R1 << 13u) | (G0 << 18u) | (G1 << 23u) | (B0 << 28u);\n dst[o + 1u] = (B0 >> 4u) | (B1 << 1u) | (A0 << 6u) | (A1 << 12u) | ((field2 & 0x3FFFu) << 18u);\n dst[o + 2u] = (field2 >> 14u) | (f_lo << 17u);\n dst[o + 3u] = (f_lo >> 15u) | (f_hi << 17u);\n }\n}\n";
|
|
1360
1353
|
|
|
1361
1354
|
// src/BC7Encoder.ts
|
|
1362
1355
|
var BC7Encoder = class extends Encoder {
|
|
@@ -1852,10 +1845,10 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
1852
1845
|
};
|
|
1853
1846
|
|
|
1854
1847
|
// src/etc2.wgsl
|
|
1855
|
-
var etc2_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. This is the f32 fallback; the f16 module\n// (etc2_fast_f16.wgsl) is an EXACT-VALUE port of it \u2014 byte-identical where\n// the sampler's unorm\u2192float conversion is exact; see its header.\n//\n// ALGORITHM \u2014 scalar-luma selection:\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests, and \u03A3||e||\xB2 per subblock is O(1) from the quadrant sums. The\n// block-constant \u03A3||p||\xB2 is dropped from EVERY estimate (ETC1 flips and\n// planar alike): only differences between estimates are ever used.\n// The estimate is exact for unclamped decode and an upper bound on the\n// true clamped error.\n// \u2022 Loads: 4 textureGather quads \xD7 R,G,B for interior blocks (the gather\n// point, normalised by the PHYSICAL texture size, sits exactly between\n// the quad's texel centres; interior quads never touch the zeroed\n// padding strip). Blocks straddling the edge of a non-multiple-of-4\n// image fall back to clamped per-texel loads. Lumas are kept as 4\n// COLUMN vectors \u2014 wire pixel order is x\xB74 + y \u2014 so both flips' half-\n// blocks and the index packing use only constant indexing.\n// \u2022 Flip preselect, O(1): per subblock the residual after continuous luma\n// modulation is within-variance \u2212 \u03BA\xB7(luma variance)/3, \u03BA = 0.9. \u03BA = 1\n// is the exact chroma residual; keeping a tenth of the luma variance\n// prefers the split with less luma spread for the 4-level tables to\n// cover (+0.07-0.10 dB on photo colour vs \u03BA = 1, free). Only the chosen\n// flip is searched.\n// \u2022 Exactly-gray blocks (every quadrant's R, G and B sums equal) have no\n// chroma to steer the preselect, so both flips are scored \u2014 worth\n// ~0.3 dB on roughness/AO content over any O(1) proxy tried (luma\n// variance, luma range, squared range all land at \u22120.30 dB). They use\n// a one-channel copy of the fit (fit_gray) and the second flip is a\n// separate straight-line call: the older single-call-site loop cost\n// 7-9% even on colour content that never ran its second iteration.\n// Widening the second evaluation to chroma near-ties (the previous\n// rule) cost 12-25% on colour textures through warp divergence for\n// \u2264 0.015 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~0.7-2.9 dB; all eight gain \u2264 0.05 dB. Scores use\n// the min form: per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) is the\n// threshold rule exactly, and its a3 part sums in closed form.\n// \u2022 NO base refit (worth ~0.2 dB on photo colour for \u2265 13% GPU).\n// \u2022 PLANAR runs unconditionally: the LSQ solve is O(1) from the block sum\n// and the first moments \u03A3x\xB7p, \u03A3y\xB7p (the Gram inverse of the fixed\n// sample positions is a constant; folding it into fewer coefficients\n// saved ~1% but resolved rounding ties unlike the CPU mirror on ~9% of\n// the colour card's blocks), and its residual is the closed form\n// \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED, clamped corners \u2014\n// clamp-aware, which a continuous-corner estimate is not. Gating the\n// quantised evaluation on the continuous plane's residual (an exact\n// lower bound) is byte-identical but measured 0-1%: ~half the warps\n// still hold a block that needs it.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks and needs a clustering pass.\n//\n// Numeric notes: every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact;\n// est values are integer sums held exactly in f32 (< 2^24) apart from the\n// planar solve's decimal weights.\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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@group(0) @binding(3) var smp: sampler;\n\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n// Fraction of the luma variance the flip preselect treats as absorbed.\nconst KAPPA = 0.9;\nconst ONE3 = vec3<f32>(1.0);\nconst ONE4 = vec4<f32>(1.0);\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nfn max4(v: vec4<f32>) -> f32 {\n return max(max(v.x, v.y), max(v.z, v.w));\n}\n\n// Base colours from subblock SUMS (8 texels each): codes (as floats) and\n// their 8-bit expansions. Differential mode when the 5-bit codes are within\n// the 3-bit delta range, else individual 4-bit. Expansions in float:\n// (q<<3)|(q>>2) = floor(8.25\xB7q) for 5 bits, (q<<4)|q = 17\xB7q for 4 bits.\nstruct Bases {\n c0: vec3<f32>,\n c1: vec3<f32>,\n b0: vec3<f32>,\n b1: vec3<f32>,\n diff: bool,\n};\nfn quantise_bases(sum0: vec3<f32>, sum1: vec3<f32>) -> Bases {\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n var o: Bases;\n o.diff = all(d >= vec3<f32>(-4.0)) && all(d <= vec3<f32>(3.0));\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.c0 = select(i0, q0, o.diff);\n o.c1 = select(i1, q1, o.diff);\n o.b0 = select(i0 * 17.0, floor(q0 * 8.25), o.diff);\n o.b1 = select(i1 * 17.0, floor(q1 * 8.25), o.diff);\n return o;\n}\n\n// Subblock error (\xD73) of table t under the threshold rule, in min form:\n// per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) = (a3\xB2 \u2212 2\xB7a3\xB7ad) +\n// min(0, (b3\xB2 \u2212 a3\xB2) \u2212 2\xB7(b3 \u2212 a3)\xB7ad); the a3 part sums in closed form\n// from sad = \u03A3 ad.\nfn table_score(au: vec4<f32>, av: vec4<f32>, sad: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let dk = b3 * b3 - a3 * a3;\n let dm = -2.0 * (b3 - a3);\n let eu = min(vec4<f32>(0.0), au * dm + dk);\n let ev = min(vec4<f32>(0.0), av * dm + dk);\n return 8.0 * a3 * a3 - 2.0 * a3 * sad + dot(eu + ev, ONE4);\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// One subblock (lumas u, v) against base luma lb.\nfn sb_search(u: vec4<f32>, v: vec4<f32>, lb: f32) -> SearchOut {\n let au = abs(u - lb);\n let av = abs(v - lb);\n let mx = max(max4(au), max4(av));\n let sad = dot(au + av, ONE4);\n // cover = #{B3[k] < mx : k < 7}, the first table whose large modifier\n // reaches mx \u2014 a binary search over the 7 thresholds.\n let s1 = mx > 126.0;\n let s2 = mx > select(51.0, 240.0, s1);\n let s3 = mx > select(select(24.0, 87.0, s2), select(180.0, 318.0, s2), s1);\n let cover = select(0u, 4u, s1) + select(0u, 2u, s2) + select(0u, 1u, s3);\n let t_lo = max(cover, 1u) - 1u;\n let acc_lo = table_score(au, av, sad, t_lo);\n let acc_hi = table_score(au, av, sad, cover);\n let lo_wins = acc_lo <= acc_hi;\n var out: SearchOut;\n out.table = select(cover, t_lo, lo_wins);\n out.acc = select(acc_hi, acc_lo, lo_wins);\n return out;\n}\n\n// One flip's fit: base quantisation + table search, and its estimate\n// (\u03A3||p||\xB2 omitted).\nstruct FlipFit {\n est: f32,\n bases: Bases,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn fit_flip(\n s0u: vec4<f32>,\n s0v: vec4<f32>,\n s1u: vec4<f32>,\n s1v: vec4<f32>,\n sum0: vec3<f32>,\n sum1: vec3<f32>,\n) -> FlipFit {\n var out: FlipFit;\n out.bases = quantise_bases(sum0, sum1);\n let b0 = out.bases.b0;\n let b1 = out.bases.b1;\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = dot(b0, 8.0 * b0 - 2.0 * sum0) + dot(b1, 8.0 * b1 - 2.0 * sum1) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// fit_flip for exactly-gray blocks (r = g = b): the same arithmetic on one\n// channel; sum0/sum1 are one channel's subblock sums.\nfn fit_gray(\n s0u: vec4<f32>,\n s0v: vec4<f32>,\n s1u: vec4<f32>,\n s1v: vec4<f32>,\n sum0: f32,\n sum1: f32,\n) -> FlipFit {\n var out: FlipFit;\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n let diff = d >= -4.0 && d <= 3.0;\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n out.bases.diff = diff;\n out.bases.c0 = vec3<f32>(select(i0, q0, diff));\n out.bases.c1 = vec3<f32>(select(i1, q1, diff));\n let b0 = select(i0 * 17.0, floor(q0 * 8.25), diff);\n let b1 = select(i1 * 17.0, floor(q1 * 8.25), diff);\n out.lb0 = 3.0 * b0;\n out.lb1 = 3.0 * b1;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = 3.0 * (b0 * (8.0 * b0 - 2.0 * sum0) + b1 * (8.0 * b1 - 2.0 * sum1)) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// One gathered 2\xD72 quad: per-texel luma (gather order), channel sums, and\n// the sums of its right column and bottom row (the planar moments' local\n// parts). Gather order: w=(0,0) z=(1,0) x=(0,1) y=(1,1).\nstruct Quad {\n l: vec4<f32>,\n s: vec3<f32>,\n right: vec3<f32>,\n bottom: vec3<f32>,\n};\nfn gather_quad(cc: vec2<f32>) -> Quad {\n let r = textureGather(0, src_tex, smp, cc) * 255.0;\n let g = textureGather(1, src_tex, smp, cc) * 255.0;\n let b = textureGather(2, src_tex, smp, cc) * 255.0;\n var o: Quad;\n o.l = r + g + b;\n o.right = vec3<f32>(r.z + r.y, g.z + g.y, b.z + b.y);\n o.s = o.right + vec3<f32>(r.w + r.x, g.w + g.x, b.w + b.x);\n o.bottom = vec3<f32>(r.x + r.y, g.x + g.y, b.x + b.y);\n return o;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n\n // Luma by column: col[x][y]. Quadrant q = (x >= 2) | (y >= 2) << 1.\n var col: array<vec4<f32>, 4>;\n var qsum: array<vec3<f32>, 4>;\n // Planar right-hand sides: \u03A3 x\xB7p and \u03A3 y\xB7p.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n if (u32(base_xy.x) + 4u <= params.width && u32(base_xy.y) + 4u <= params.height) {\n let inv = vec2<f32>(1.0) / vec2<f32>(textureDimensions(src_tex));\n let c0 = (vec2<f32>(base_xy) + 1.0) * inv;\n let q0 = gather_quad(c0);\n let q1 = gather_quad(c0 + vec2<f32>(2.0, 0.0) * inv);\n let q2 = gather_quad(c0 + vec2<f32>(0.0, 2.0) * inv);\n let q3 = gather_quad(c0 + vec2<f32>(2.0, 2.0) * inv);\n qsum[0] = q0.s;\n qsum[1] = q1.s;\n qsum[2] = q2.s;\n qsum[3] = q3.s;\n sxp = q0.right + q2.right + 2.0 * (q1.s + q3.s) + q1.right + q3.right;\n syp = q0.bottom + q1.bottom + 2.0 * (q2.s + q3.s) + q2.bottom + q3.bottom;\n col[0] = vec4<f32>(q0.l.w, q0.l.x, q2.l.w, q2.l.x);\n col[1] = vec4<f32>(q0.l.z, q0.l.y, q2.l.z, q2.l.y);\n col[2] = vec4<f32>(q1.l.w, q1.l.x, q3.l.w, q3.l.x);\n col[3] = vec4<f32>(q1.l.z, q1.l.y, q3.l.z, q3.l.y);\n } else {\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i & 3u;\n let ly = i >> 2u;\n let p = clamp(base_xy + vec2<i32>(i32(lx), i32(ly)), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n col[lx][ly] = c.r + c.g + c.b;\n let q = u32(lx >= 2u) | (u32(ly >= 2u) << 1u);\n qsum[q] = qsum[q] + c;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n }\n\n // ------------------------------------------------------------ planar --\n // LSQ plane in closed form: rhs rA = \u03A3(1 \u2212 x/4 \u2212 y/4)\xB7p, rB = \u03A3(x/4)\xB7p,\n // rC = \u03A3(y/4)\xB7p times the constant inverse Gram matrix (the same\n // coefficient form as the CPU mirror, so rounding ties resolve alike);\n // estimate with the quantised, clamped corners: \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n let qo = clamp(floor(po * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qh = clamp(floor(ph * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qv = clamp(floor(pv * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n // 6-bit expand (q<<2)|(q>>4) = floor(4.0625\xB7q); 7-bit (q<<1)|(q>>6) = floor(2.015625\xB7q).\n let xk = vec3<f32>(4.0625, 2.015625, 4.0625);\n let eo = floor(qo * xk);\n let eh = floor(qh * xk);\n let ev = floor(qv * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n let planar_est = dot(gram - 2.0 * (eo * rA + eh * rB + ev * rC), ONE3) + PLANAR_FUDGE;\n\n // ------------------------------------------------ flip + base selection --\n // Flip 0 splits columns (sum0a = left half), flip 1 splits rows (sum0b =\n // top half). Per flip, the preselect residual minus the flip-independent\n // \u03A3||p||\xB2 and \u03A3\u2113\xB2 terms: \u2212\u03A3||s||\xB2/8 + \u03BA\xB7(\u03A3\u2113)\xB2/24 over its two subblocks.\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let l0a = dot(sum0a, ONE3);\n let l1a = dot(sum1a, ONE3);\n let l0b = dot(sum0b, ONE3);\n let l1b = dot(sum1b, ONE3);\n let res_a = KAPPA / 24.0 * (l0a * l0a + l1a * l1a) - 0.125 * (dot(sum0a, sum0a) + dot(sum1a, sum1a));\n let res_b = KAPPA / 24.0 * (l0b * l0b + l1b * l1b) - 0.125 * (dot(sum0b, sum0b) + dot(sum1b, sum1b));\n let gray = all(qsum[0].rg == qsum[0].gb) && all(qsum[1].rg == qsum[1].gb) &&\n all(qsum[2].rg == qsum[2].gb) && all(qsum[3].rg == qsum[3].gb);\n\n var bflip = 0u;\n var sel: FlipFit;\n if (gray) {\n sel = fit_gray(col[0], col[1], col[2], col[3], sum0a.r, sum1a.r);\n let alt = fit_gray(\n vec4<f32>(col[0].xy, col[1].xy),\n vec4<f32>(col[2].xy, col[3].xy),\n vec4<f32>(col[0].zw, col[1].zw),\n vec4<f32>(col[2].zw, col[3].zw),\n sum0b.r,\n sum1b.r,\n );\n if (alt.est < sel.est) {\n sel = alt;\n bflip = 1u;\n }\n } else {\n let fb = res_b < res_a;\n bflip = select(0u, 1u, fb);\n sel = fit_flip(\n select(col[0], vec4<f32>(col[0].xy, col[1].xy), fb),\n select(col[1], vec4<f32>(col[2].xy, col[3].xy), fb),\n select(col[2], vec4<f32>(col[0].zw, col[1].zw), fb),\n select(col[3], vec4<f32>(col[2].zw, col[3].zw), fb),\n select(sum0a, sum0b, fb),\n select(sum1a, sum1b, fb),\n );\n }\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (sel.est <= planar_est) {\n let codes0 = vec3<u32>(sel.bases.c0);\n let codes1 = vec3<u32>(sel.bases.c1);\n let t0 = sel.t0;\n let t1 = sel.t1;\n if (sel.bases.diff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n // Wire indices, column by column (bit x\xB74 + y): flip 0 gives columns\n // 0,1 subblock 0; flip 1 gives rows 0,1 (lanes x, y) subblock 0.\n // LSB = large modifier, MSB = negative.\n let fb = bflip == 1u;\n let lb0 = sel.lb0;\n let lb1 = sel.lb1;\n let th0 = THR[t0];\n let th1 = THR[t1];\n let lb_rows = vec4<f32>(lb0, lb0, lb1, lb1);\n let th_rows = vec4<f32>(th0, th0, th1, th1);\n let lb_l = select(vec4<f32>(lb0), lb_rows, fb);\n let lb_r = select(vec4<f32>(lb1), lb_rows, fb);\n let th_l = select(vec4<f32>(th0), th_rows, fb);\n let th_r = select(vec4<f32>(th1), th_rows, fb);\n let bitv = vec4<u32>(1u, 2u, 4u, 8u);\n var lsb = 0u;\n var msb = 0u;\n for (var c: u32 = 0u; c < 4u; c = c + 1u) {\n let d = col[c] - select(lb_l, lb_r, c >= 2u);\n let large = select(vec4<u32>(0u), bitv, abs(d) > select(th_l, th_r, c >= 2u));\n let neg = select(vec4<u32>(0u), bitv, d < vec4<f32>(0.0));\n lsb = lsb | ((large.x | large.y | large.z | large.w) << (c * 4u));\n msb = msb | ((neg.x | neg.y | neg.z | neg.w) << (c * 4u));\n }\n lo = lsb | (msb << 16u);\n } else {\n let ro = u32(qo.r); let go = u32(qo.g); let bo = u32(qo.b);\n let rh = u32(qh.r); let gh = u32(qh.g); let bh = u32(qh.b);\n let rv = u32(qv.r); let gv = u32(qv.g); let bv = u32(qv.b);\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1848
|
+
var etc2_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. This is the f32 fallback; the f16 module\n// (etc2_fast_f16.wgsl) is an EXACT-VALUE port of it \u2014 byte-identical where\n// the sampler's unorm\u2192float conversion is exact; see its header.\n//\n// ALGORITHM \u2014 scalar-luma selection:\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests, and \u03A3||e||\xB2 per subblock is O(1) from the quadrant sums. The\n// block-constant \u03A3||p||\xB2 is dropped from EVERY estimate (ETC1 flips and\n// planar alike): only differences between estimates are ever used.\n// The estimate is exact for unclamped decode and an upper bound on the\n// true clamped error.\n// \u2022 Loads: 4 textureGather quads \xD7 R,G,B for interior blocks (the gather\n// point, normalised by the PHYSICAL texture size, sits exactly between\n// the quad's texel centres; interior quads never touch the zeroed\n// padding strip). Blocks straddling the edge of a non-multiple-of-4\n// image fall back to clamped per-texel loads. Lumas are kept as 4\n// COLUMN vectors \u2014 wire pixel order is x\xB74 + y \u2014 so both flips' half-\n// blocks and the index packing use only constant indexing.\n// \u2022 Flip preselect, O(1): per subblock the residual after continuous luma\n// modulation is within-variance \u2212 \u03BA\xB7(luma variance)/3, \u03BA = 0.9. \u03BA = 1\n// is the exact chroma residual; keeping a tenth of the luma variance\n// prefers the split with less luma spread for the 4-level tables to\n// cover (+0.07-0.10 dB on photo colour vs \u03BA = 1, free). Only the chosen\n// flip is searched.\n// \u2022 Exactly-gray blocks (every quadrant's R, G and B sums AND both planar\n// moments equal) have no chroma to steer the preselect, so both flips\n// are scored \u2014 worth ~0.3 dB on roughness/AO content over any O(1)\n// proxy tried (luma variance, luma range, squared range all land at\n// \u22120.30 dB; deciding the flip on the cover table's score alone loses\n// 0.3-0.5 dB). They use a one-channel copy of the fit (fit_gray), the\n// second flip is a separate straight-line call (the older\n// single-call-site loop cost 7-9% even on colour content that never ran\n// its second iteration), and their planar solve runs on two channels:\n// R and B share the 6-bit code, G takes the 7-bit one (\u22124..5% on gray\n// maps).\n// Widening the second evaluation to chroma near-ties (the previous\n// rule) cost 12-25% on colour textures through warp divergence for\n// \u2264 0.015 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~0.7-2.9 dB; all eight gain \u2264 0.05 dB. Scores use\n// the min form: per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) is the\n// threshold rule exactly, and its a3 part sums in closed form. A flip's\n// two subblocks are searched together (sb_pair): the lower-neighbour\n// scores sit behind ONE branch, skipped when both covers are table 0 \u2014\n// then the lower neighbour IS the cover table. Smooth content skips it\n// wholesale (\u22129..16% on displacement maps at 2K/4K); a per-subblock\n// branch cost 2-4% on noisy content by splitting the score pair.\n// \u2022 NO base refit (worth ~0.2 dB on photo colour for \u2265 13% GPU).\n// \u2022 PLANAR runs unconditionally: the LSQ solve is O(1) from the block sum\n// and the first moments \u03A3x\xB7p, \u03A3y\xB7p (the Gram inverse of the fixed\n// sample positions is a constant; folding it into fewer coefficients\n// saved ~1% but resolved rounding ties unlike the CPU mirror on ~9% of\n// the colour card's blocks), and its residual is the closed form\n// \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED, clamped corners \u2014\n// clamp-aware, which a continuous-corner estimate is not. Gating the\n// quantised evaluation on the continuous plane's residual (an exact\n// lower bound) is byte-identical but measured 0-1%: ~half the warps\n// still hold a block that needs it.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks and needs a clustering pass.\n//\n// Numeric notes: every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact;\n// est values are integer sums held exactly in f32 (< 2^24) apart from the\n// planar solve's decimal weights.\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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@group(0) @binding(3) var smp: sampler;\n\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n// Fraction of the luma variance the flip preselect treats as absorbed.\nconst KAPPA = 0.9;\nconst ONE3 = vec3<f32>(1.0);\nconst ONE4 = vec4<f32>(1.0);\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nfn max4(v: vec4<f32>) -> f32 {\n return max(max(v.x, v.y), max(v.z, v.w));\n}\n\n// Base colours from subblock SUMS (8 texels each): codes (as floats) and\n// their 8-bit expansions. Differential mode when the 5-bit codes are within\n// the 3-bit delta range, else individual 4-bit. Expansions in float:\n// (q<<3)|(q>>2) = floor(8.25\xB7q) for 5 bits, (q<<4)|q = 17\xB7q for 4 bits.\nstruct Bases {\n c0: vec3<f32>,\n c1: vec3<f32>,\n b0: vec3<f32>,\n b1: vec3<f32>,\n diff: bool,\n};\nfn quantise_bases(sum0: vec3<f32>, sum1: vec3<f32>) -> Bases {\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n var o: Bases;\n o.diff = all(d >= vec3<f32>(-4.0)) && all(d <= vec3<f32>(3.0));\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.c0 = select(i0, q0, o.diff);\n o.c1 = select(i1, q1, o.diff);\n o.b0 = select(i0 * 17.0, floor(q0 * 8.25), o.diff);\n o.b1 = select(i1 * 17.0, floor(q1 * 8.25), o.diff);\n return o;\n}\n\n// Subblock error (\xD73) of table t under the threshold rule, in min form:\n// per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) = (a3\xB2 \u2212 2\xB7a3\xB7ad) +\n// min(0, (b3\xB2 \u2212 a3\xB2) \u2212 2\xB7(b3 \u2212 a3)\xB7ad); the a3 part sums in closed form\n// from sad = \u03A3 ad.\n// Per-table score constants: DK = b3\xB2 \u2212 a3\xB2, DM = \u22122(b3 \u2212 a3), A8 = 8\xB7a3\xB2,\n// AM = \u22122\xB7a3 (precomputed: \u22121.5% GPU over deriving them per call).\nconst DK = array<f32, 8>(540.0, 2376.0, 6840.0, 14355.0, 29484.0, 52416.0, 91323.0, 281520.0);\nconst DM = array<f32, 8>(-36.0, -72.0, -120.0, -174.0, -252.0, -336.0, -438.0, -816.0);\nconst A8 = array<f32, 8>(288.0, 1800.0, 5832.0, 12168.0, 23328.0, 41472.0, 78408.0, 159048.0);\nconst AM = array<f32, 8>(-12.0, -30.0, -54.0, -78.0, -108.0, -144.0, -198.0, -282.0);\nfn table_score(au: vec4<f32>, av: vec4<f32>, sad: f32, t: u32) -> f32 {\n let dk = DK[t];\n let dm = DM[t];\n let eu = min(vec4<f32>(0.0), au * dm + dk);\n let ev = min(vec4<f32>(0.0), av * dm + dk);\n return A8[t] + AM[t] * sad + dot(eu + ev, ONE4);\n}\n\n// Both subblocks of one flip (lumas u, v against base luma lb): cover\n// tables and their scores, then the lower neighbours behind ONE branch \u2014\n// skipped when both covers are table 0 (the lower neighbour IS the cover\n// table; smooth content), branch-free inside so the two scores interleave.\nstruct PairOut {\n t0: u32,\n t1: u32,\n acc: f32,\n};\nfn sb_pair(u0: vec4<f32>, v0: vec4<f32>, lbf0: f32, u1: vec4<f32>, v1: vec4<f32>, lbf1: f32) -> PairOut {\n let au0 = abs(u0 - lbf0);\n let av0 = abs(v0 - lbf0);\n let au1 = abs(u1 - lbf1);\n let av1 = abs(v1 - lbf1);\n let mx = vec2<f32>(max(max4(au0), max4(av0)), max(max4(au1), max4(av1)));\n let sad0 = dot(au0 + av0, ONE4);\n let sad1 = dot(au1 + av1, ONE4);\n // cover = #{B3[k] < mx : k < 7}, the first table whose large modifier\n // reaches mx \u2014 a binary search over the 7 thresholds.\n let s1 = mx > vec2<f32>(126.0);\n let s2 = mx > select(vec2<f32>(51.0), vec2<f32>(240.0), s1);\n let s3 = mx > select(select(vec2<f32>(24.0), vec2<f32>(87.0), s2), select(vec2<f32>(180.0), vec2<f32>(318.0), s2), s1);\n let cover = select(vec2<u32>(0u), vec2<u32>(4u), s1) + select(vec2<u32>(0u), vec2<u32>(2u), s2) + select(vec2<u32>(0u), vec2<u32>(1u), s3);\n let hi0 = table_score(au0, av0, sad0, cover.x);\n let hi1 = table_score(au1, av1, sad1, cover.y);\n var out: PairOut;\n out.t0 = cover.x;\n out.t1 = cover.y;\n out.acc = hi0 + hi1;\n if (any(cover != vec2<u32>(0u))) {\n let t_lo = max(cover, vec2<u32>(1u)) - vec2<u32>(1u);\n let lo0 = table_score(au0, av0, sad0, t_lo.x);\n let lo1 = table_score(au1, av1, sad1, t_lo.y);\n let w0 = lo0 <= hi0;\n let w1 = lo1 <= hi1;\n out.t0 = select(cover.x, t_lo.x, w0);\n out.t1 = select(cover.y, t_lo.y, w1);\n out.acc = select(hi0, lo0, w0) + select(hi1, lo1, w1);\n }\n return out;\n}\n\n// One flip's fit: base quantisation + table search, and its estimate\n// (\u03A3||p||\xB2 omitted).\nstruct FlipFit {\n est: f32,\n bases: Bases,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn fit_flip(\n s0u: vec4<f32>,\n s0v: vec4<f32>,\n s1u: vec4<f32>,\n s1v: vec4<f32>,\n sum0: vec3<f32>,\n sum1: vec3<f32>,\n) -> FlipFit {\n var out: FlipFit;\n out.bases = quantise_bases(sum0, sum1);\n let b0 = out.bases.b0;\n let b1 = out.bases.b1;\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let pp = sb_pair(s0u, s0v, out.lb0, s1u, s1v, out.lb1);\n out.t0 = pp.t0;\n out.t1 = pp.t1;\n out.est = dot(b0, 8.0 * b0 - 2.0 * sum0) + dot(b1, 8.0 * b1 - 2.0 * sum1) + pp.acc * (1.0 / 3.0);\n return out;\n}\n\n// fit_flip for exactly-gray blocks (r = g = b): the same arithmetic on one\n// channel; sum0/sum1 are one channel's subblock sums.\nfn fit_gray(\n s0u: vec4<f32>,\n s0v: vec4<f32>,\n s1u: vec4<f32>,\n s1v: vec4<f32>,\n sum0: f32,\n sum1: f32,\n) -> FlipFit {\n var out: FlipFit;\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n let diff = d >= -4.0 && d <= 3.0;\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n out.bases.diff = diff;\n out.bases.c0 = vec3<f32>(select(i0, q0, diff));\n out.bases.c1 = vec3<f32>(select(i1, q1, diff));\n let b0 = select(i0 * 17.0, floor(q0 * 8.25), diff);\n let b1 = select(i1 * 17.0, floor(q1 * 8.25), diff);\n out.lb0 = 3.0 * b0;\n out.lb1 = 3.0 * b1;\n let pp = sb_pair(s0u, s0v, out.lb0, s1u, s1v, out.lb1);\n out.t0 = pp.t0;\n out.t1 = pp.t1;\n out.est = 3.0 * (b0 * (8.0 * b0 - 2.0 * sum0) + b1 * (8.0 * b1 - 2.0 * sum1)) + pp.acc * (1.0 / 3.0);\n return out;\n}\n\n// One gathered 2\xD72 quad: per-texel luma (gather order), channel sums, and\n// the sums of its right column and bottom row (the planar moments' local\n// parts). Gather order: w=(0,0) z=(1,0) x=(0,1) y=(1,1).\nstruct Quad {\n l: vec4<f32>,\n s: vec3<f32>,\n right: vec3<f32>,\n bottom: vec3<f32>,\n};\nfn gather_quad(cc: vec2<f32>) -> Quad {\n let r = textureGather(0, src_tex, smp, cc) * 255.0;\n let g = textureGather(1, src_tex, smp, cc) * 255.0;\n let b = textureGather(2, src_tex, smp, cc) * 255.0;\n var o: Quad;\n o.l = r + g + b;\n o.right = vec3<f32>(r.z + r.y, g.z + g.y, b.z + b.y);\n o.s = o.right + vec3<f32>(r.w + r.x, g.w + g.x, b.w + b.x);\n o.bottom = vec3<f32>(r.x + r.y, g.x + g.y, b.x + b.y);\n return o;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n\n // Luma by column: col[x][y]. Quadrant q = (x >= 2) | (y >= 2) << 1.\n var col: array<vec4<f32>, 4>;\n var qsum: array<vec3<f32>, 4>;\n // Planar right-hand sides: \u03A3 x\xB7p and \u03A3 y\xB7p.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n if (u32(base_xy.x) + 4u <= params.width && u32(base_xy.y) + 4u <= params.height) {\n let inv = vec2<f32>(1.0) / vec2<f32>(textureDimensions(src_tex));\n let c0 = (vec2<f32>(base_xy) + 1.0) * inv;\n let q0 = gather_quad(c0);\n let q1 = gather_quad(c0 + vec2<f32>(2.0, 0.0) * inv);\n let q2 = gather_quad(c0 + vec2<f32>(0.0, 2.0) * inv);\n let q3 = gather_quad(c0 + vec2<f32>(2.0, 2.0) * inv);\n qsum[0] = q0.s;\n qsum[1] = q1.s;\n qsum[2] = q2.s;\n qsum[3] = q3.s;\n sxp = q0.right + q2.right + 2.0 * (q1.s + q3.s) + q1.right + q3.right;\n syp = q0.bottom + q1.bottom + 2.0 * (q2.s + q3.s) + q2.bottom + q3.bottom;\n col[0] = vec4<f32>(q0.l.w, q0.l.x, q2.l.w, q2.l.x);\n col[1] = vec4<f32>(q0.l.z, q0.l.y, q2.l.z, q2.l.y);\n col[2] = vec4<f32>(q1.l.w, q1.l.x, q3.l.w, q3.l.x);\n col[3] = vec4<f32>(q1.l.z, q1.l.y, q3.l.z, q3.l.y);\n } else {\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i & 3u;\n let ly = i >> 2u;\n let p = clamp(base_xy + vec2<i32>(i32(lx), i32(ly)), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n col[lx][ly] = c.r + c.g + c.b;\n let q = u32(lx >= 2u) | (u32(ly >= 2u) << 1u);\n qsum[q] = qsum[q] + c;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n }\n\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n // Exactly gray: every quadrant sum AND both planar moments equal across\n // R, G, B \u2014 then R and B planar corners coincide (same 6-bit code) and\n // only R (6-bit) and G (7-bit) need solving.\n let gray = all(qsum[0].rg == qsum[0].gb) && all(qsum[1].rg == qsum[1].gb) &&\n all(qsum[2].rg == qsum[2].gb) && all(qsum[3].rg == qsum[3].gb) &&\n all(sxp.rg == sxp.gb) && all(syp.rg == syp.gb);\n\n var planar_est: f32;\n var qo: vec3<f32>;\n var qh: vec3<f32>;\n var qv: vec3<f32>;\n var bflip = 0u;\n var sel: FlipFit;\n if (gray) {\n // Planar on two channels: R and B share the 6-bit solve.\n let rB = sxp.r * 0.25;\n let rC = syp.r * 0.25;\n let rA = total.r - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec2<f32>(63.0, 127.0);\n let qo2 = clamp(floor(po * (pmax / 255.0) + 0.5), vec2<f32>(0.0), pmax);\n let qh2 = clamp(floor(ph * (pmax / 255.0) + 0.5), vec2<f32>(0.0), pmax);\n let qv2 = clamp(floor(pv * (pmax / 255.0) + 0.5), vec2<f32>(0.0), pmax);\n let xk = vec2<f32>(4.0625, 2.015625);\n let eo = floor(qo2 * xk);\n let eh = floor(qh2 * xk);\n let ev = floor(qv2 * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n let pe = gram - 2.0 * (eo * rA + eh * rB + ev * rC);\n planar_est = 2.0 * pe.x + pe.y + PLANAR_FUDGE;\n qo = qo2.xyx;\n qh = qh2.xyx;\n qv = qv2.xyx;\n\n let sum0a = qsum[0].r + qsum[2].r;\n let sum1a = qsum[1].r + qsum[3].r;\n let sum0b = qsum[0].r + qsum[1].r;\n let sum1b = qsum[2].r + qsum[3].r;\n sel = fit_gray(col[0], col[1], col[2], col[3], sum0a, sum1a);\n let alt = fit_gray(\n vec4<f32>(col[0].xy, col[1].xy),\n vec4<f32>(col[2].xy, col[3].xy),\n vec4<f32>(col[0].zw, col[1].zw),\n vec4<f32>(col[2].zw, col[3].zw),\n sum0b,\n sum1b,\n );\n if (alt.est < sel.est) {\n sel = alt;\n bflip = 1u;\n }\n } else {\n // LSQ plane in closed form: rhs rA = \u03A3(1 \u2212 x/4 \u2212 y/4)\xB7p, rB = \u03A3(x/4)\xB7p,\n // rC = \u03A3(y/4)\xB7p times the constant inverse Gram matrix (the same\n // coefficient form as the CPU mirror, so rounding ties resolve alike);\n // estimate with the quantised, clamped corners: \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8.\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n qo = clamp(floor(po * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n qh = clamp(floor(ph * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n qv = clamp(floor(pv * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n // 6-bit expand (q<<2)|(q>>4) = floor(4.0625\xB7q); 7-bit (q<<1)|(q>>6) = floor(2.015625\xB7q).\n let xk = vec3<f32>(4.0625, 2.015625, 4.0625);\n let eo = floor(qo * xk);\n let eh = floor(qh * xk);\n let ev = floor(qv * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n planar_est = dot(gram - 2.0 * (eo * rA + eh * rB + ev * rC), ONE3) + PLANAR_FUDGE;\n\n // Flip 0 splits columns (sum0a = left half), flip 1 splits rows (sum0b =\n // top half). Per flip, the preselect residual minus the flip-independent\n // \u03A3||p||\xB2 and \u03A3\u2113\xB2 terms: \u2212\u03A3||s||\xB2/8 + \u03BA\xB7(\u03A3\u2113)\xB2/24 over its two subblocks.\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let l0a = dot(sum0a, ONE3);\n let l1a = dot(sum1a, ONE3);\n let l0b = dot(sum0b, ONE3);\n let l1b = dot(sum1b, ONE3);\n let res_a = KAPPA / 24.0 * (l0a * l0a + l1a * l1a) - 0.125 * (dot(sum0a, sum0a) + dot(sum1a, sum1a));\n let res_b = KAPPA / 24.0 * (l0b * l0b + l1b * l1b) - 0.125 * (dot(sum0b, sum0b) + dot(sum1b, sum1b));\n let fb = res_b < res_a;\n bflip = select(0u, 1u, fb);\n sel = fit_flip(\n select(col[0], vec4<f32>(col[0].xy, col[1].xy), fb),\n select(col[1], vec4<f32>(col[2].xy, col[3].xy), fb),\n select(col[2], vec4<f32>(col[0].zw, col[1].zw), fb),\n select(col[3], vec4<f32>(col[2].zw, col[3].zw), fb),\n select(sum0a, sum0b, fb),\n select(sum1a, sum1b, fb),\n );\n }\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (sel.est <= planar_est) {\n let codes0 = vec3<u32>(sel.bases.c0);\n let codes1 = vec3<u32>(sel.bases.c1);\n let t0 = sel.t0;\n let t1 = sel.t1;\n if (sel.bases.diff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n // Wire indices, column by column (bit x\xB74 + y): flip 0 gives columns\n // 0,1 subblock 0; flip 1 gives rows 0,1 (lanes x, y) subblock 0.\n // LSB = large modifier, MSB = negative.\n let fb = bflip == 1u;\n let lb0 = sel.lb0;\n let lb1 = sel.lb1;\n let th0 = THR[t0];\n let th1 = THR[t1];\n let lb_rows = vec4<f32>(lb0, lb0, lb1, lb1);\n let th_rows = vec4<f32>(th0, th0, th1, th1);\n let lb_l = select(vec4<f32>(lb0), lb_rows, fb);\n let lb_r = select(vec4<f32>(lb1), lb_rows, fb);\n let th_l = select(vec4<f32>(th0), th_rows, fb);\n let th_r = select(vec4<f32>(th1), th_rows, fb);\n let bitv = vec4<u32>(1u, 2u, 4u, 8u);\n var lsb = 0u;\n var msb = 0u;\n for (var c: u32 = 0u; c < 4u; c = c + 1u) {\n let d = col[c] - select(lb_l, lb_r, c >= 2u);\n let large = select(vec4<u32>(0u), bitv, abs(d) > select(th_l, th_r, c >= 2u));\n let neg = select(vec4<u32>(0u), bitv, d < vec4<f32>(0.0));\n lsb = lsb | ((large.x | large.y | large.z | large.w) << (c * 4u));\n msb = msb | ((neg.x | neg.y | neg.z | neg.w) << (c * 4u));\n }\n lo = lsb | (msb << 16u);\n } else {\n let ro = u32(qo.r); let go = u32(qo.g); let bo = u32(qo.b);\n let rh = u32(qh.r); let gh = u32(qh.g); let bh = u32(qh.b);\n let rv = u32(qv.r); let gv = u32(qv.g); let bv = u32(qv.b);\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1856
1849
|
|
|
1857
1850
|
// src/etc2_fast_f16.wgsl
|
|
1858
|
-
var etc2_fast_f16_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. This is the f16 module; etc2.wgsl is the f32\n// fallback with the same algorithm.\n//\n// EXACT-VALUE f16: unlike the other formats' f16 fast paths (which accept\n// float rounding in a [0,1] domain), the f16 values here are integers (or\n// half-integers) that f16 represents exactly \u2014 per-texel lumas and base\n// lumas (<= 765), luma deviations |D| (<= 765) and the index thresholds\n// (<= 345, halves included) all sit below f16's exactness limits. Sums,\n// scores and estimates stay f32 (they reach ~1e6). Where the sampler's\n// unorm\u2192float conversion is exact (verified on Apple/metal-3), the f16 and\n// f32 modules are BYTE-IDENTICAL; elsewhere they can differ only on exact\n// decision ties. f16 buys register space (the 16 lumas are 4 \xD7 vec4<f16>)\n// and measured 1-3% faster than the f32 module on Apple.\n//\n// ALGORITHM \u2014 scalar-luma selection:\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests, and \u03A3||e||\xB2 per subblock is O(1) from the quadrant sums. The\n// block-constant \u03A3||p||\xB2 is dropped from EVERY estimate (ETC1 flips and\n// planar alike): only differences between estimates are ever used.\n// The estimate is exact for unclamped decode and an upper bound on the\n// true clamped error.\n// \u2022 Loads: 4 textureGather quads \xD7 R,G,B for interior blocks (the gather\n// point, normalised by the PHYSICAL texture size, sits exactly between\n// the quad's texel centres; interior quads never touch the zeroed\n// padding strip). Blocks straddling the edge of a non-multiple-of-4\n// image fall back to clamped per-texel loads. Lumas are kept as 4\n// COLUMN vectors \u2014 wire pixel order is x\xB74 + y \u2014 so both flips' half-\n// blocks and the index packing use only constant indexing.\n// \u2022 Flip preselect, O(1): per subblock the residual after continuous luma\n// modulation is within-variance \u2212 \u03BA\xB7(luma variance)/3, \u03BA = 0.9. \u03BA = 1\n// is the exact chroma residual; keeping a tenth of the luma variance\n// prefers the split with less luma spread for the 4-level tables to\n// cover (+0.07-0.10 dB on photo colour vs \u03BA = 1, free). Only the chosen\n// flip is searched.\n// \u2022 Exactly-gray blocks (every quadrant's R, G and B sums equal) have no\n// chroma to steer the preselect, so both flips are scored \u2014 worth\n// ~0.3 dB on roughness/AO content over any O(1) proxy tried (luma\n// variance, luma range, squared range all land at \u22120.30 dB). They use\n// a one-channel copy of the fit (fit_gray) and the second flip is a\n// separate straight-line call: the older single-call-site loop cost\n// 7-9% even on colour content that never ran its second iteration.\n// Widening the second evaluation to chroma near-ties (the previous\n// rule) cost 12-25% on colour textures through warp divergence for\n// \u2264 0.015 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~0.7-2.9 dB; all eight gain \u2264 0.05 dB. Scores use\n// the min form: per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) is the\n// threshold rule exactly, and its a3 part sums in closed form.\n// \u2022 NO base refit (worth ~0.2 dB on photo colour for \u2265 13% GPU).\n// \u2022 PLANAR runs unconditionally: the LSQ solve is O(1) from the block sum\n// and the first moments \u03A3x\xB7p, \u03A3y\xB7p (the Gram inverse of the fixed\n// sample positions is a constant; folding it into fewer coefficients\n// saved ~1% but resolved rounding ties unlike the CPU mirror on ~9% of\n// the colour card's blocks), and its residual is the closed form\n// \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED, clamped corners \u2014\n// clamp-aware, which a continuous-corner estimate is not. Gating the\n// quantised evaluation on the continuous plane's residual (an exact\n// lower bound) is byte-identical but measured 0-1%: ~half the warps\n// still hold a block that needs it.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks and needs a clustering pass.\n//\n// Numeric notes: every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact;\n// est values are integer sums held exactly in f32 (< 2^24) apart from the\n// planar solve's decimal weights.\n\nenable f16;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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@group(0) @binding(3) var smp: sampler;\n\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n// Fraction of the luma variance the flip preselect treats as absorbed.\nconst KAPPA = 0.9;\nconst ONE3 = vec3<f32>(1.0);\nconst ONE4 = vec4<f32>(1.0);\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nfn max4(v: vec4<f16>) -> f16 {\n return max(max(v.x, v.y), max(v.z, v.w));\n}\n\n// Base colours from subblock SUMS (8 texels each): codes (as floats) and\n// their 8-bit expansions. Differential mode when the 5-bit codes are within\n// the 3-bit delta range, else individual 4-bit. Expansions in float:\n// (q<<3)|(q>>2) = floor(8.25\xB7q) for 5 bits, (q<<4)|q = 17\xB7q for 4 bits.\nstruct Bases {\n c0: vec3<f32>,\n c1: vec3<f32>,\n b0: vec3<f32>,\n b1: vec3<f32>,\n diff: bool,\n};\nfn quantise_bases(sum0: vec3<f32>, sum1: vec3<f32>) -> Bases {\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n var o: Bases;\n o.diff = all(d >= vec3<f32>(-4.0)) && all(d <= vec3<f32>(3.0));\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.c0 = select(i0, q0, o.diff);\n o.c1 = select(i1, q1, o.diff);\n o.b0 = select(i0 * 17.0, floor(q0 * 8.25), o.diff);\n o.b1 = select(i1 * 17.0, floor(q1 * 8.25), o.diff);\n return o;\n}\n\n// Subblock error (\xD73) of table t under the threshold rule, in min form:\n// per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) = (a3\xB2 \u2212 2\xB7a3\xB7ad) +\n// min(0, (b3\xB2 \u2212 a3\xB2) \u2212 2\xB7(b3 \u2212 a3)\xB7ad); the a3 part sums in closed form\n// from sad = \u03A3 ad.\nfn table_score(au: vec4<f32>, av: vec4<f32>, sad: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let dk = b3 * b3 - a3 * a3;\n let dm = -2.0 * (b3 - a3);\n let eu = min(vec4<f32>(0.0), au * dm + dk);\n let ev = min(vec4<f32>(0.0), av * dm + dk);\n return 8.0 * a3 * a3 - 2.0 * a3 * sad + dot(eu + ev, ONE4);\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// One subblock (lumas u, v) against base luma lb. |D| is exact in f16; the\n// scores need f32.\nfn sb_search(u: vec4<f16>, v: vec4<f16>, lbf: f32) -> SearchOut {\n let lb = f16(lbf);\n let ah = abs(u - lb);\n let bh = abs(v - lb);\n let mx = max(max4(ah), max4(bh));\n let au = vec4<f32>(ah);\n let av = vec4<f32>(bh);\n let sad = dot(au + av, ONE4);\n // cover = #{B3[k] < mx : k < 7}, the first table whose large modifier\n // reaches mx \u2014 a binary search over the 7 thresholds.\n let s1 = mx > 126.0h;\n let s2 = mx > select(51.0h, 240.0h, s1);\n let s3 = mx > select(select(24.0h, 87.0h, s2), select(180.0h, 318.0h, s2), s1);\n let cover = select(0u, 4u, s1) + select(0u, 2u, s2) + select(0u, 1u, s3);\n let t_lo = max(cover, 1u) - 1u;\n let acc_lo = table_score(au, av, sad, t_lo);\n let acc_hi = table_score(au, av, sad, cover);\n let lo_wins = acc_lo <= acc_hi;\n var out: SearchOut;\n out.table = select(cover, t_lo, lo_wins);\n out.acc = select(acc_hi, acc_lo, lo_wins);\n return out;\n}\n\n// One flip's fit: base quantisation + table search, and its estimate\n// (\u03A3||p||\xB2 omitted).\nstruct FlipFit {\n est: f32,\n bases: Bases,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn fit_flip(\n s0u: vec4<f16>,\n s0v: vec4<f16>,\n s1u: vec4<f16>,\n s1v: vec4<f16>,\n sum0: vec3<f32>,\n sum1: vec3<f32>,\n) -> FlipFit {\n var out: FlipFit;\n out.bases = quantise_bases(sum0, sum1);\n let b0 = out.bases.b0;\n let b1 = out.bases.b1;\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = dot(b0, 8.0 * b0 - 2.0 * sum0) + dot(b1, 8.0 * b1 - 2.0 * sum1) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// fit_flip for exactly-gray blocks (r = g = b): the same arithmetic on one\n// channel; sum0/sum1 are one channel's subblock sums.\nfn fit_gray(\n s0u: vec4<f16>,\n s0v: vec4<f16>,\n s1u: vec4<f16>,\n s1v: vec4<f16>,\n sum0: f32,\n sum1: f32,\n) -> FlipFit {\n var out: FlipFit;\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n let diff = d >= -4.0 && d <= 3.0;\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n out.bases.diff = diff;\n out.bases.c0 = vec3<f32>(select(i0, q0, diff));\n out.bases.c1 = vec3<f32>(select(i1, q1, diff));\n let b0 = select(i0 * 17.0, floor(q0 * 8.25), diff);\n let b1 = select(i1 * 17.0, floor(q1 * 8.25), diff);\n out.lb0 = 3.0 * b0;\n out.lb1 = 3.0 * b1;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = 3.0 * (b0 * (8.0 * b0 - 2.0 * sum0) + b1 * (8.0 * b1 - 2.0 * sum1)) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// One gathered 2\xD72 quad: per-texel luma (gather order), channel sums, and\n// the sums of its right column and bottom row (the planar moments' local\n// parts). Gather order: w=(0,0) z=(1,0) x=(0,1) y=(1,1).\nstruct Quad {\n l: vec4<f16>,\n s: vec3<f32>,\n right: vec3<f32>,\n bottom: vec3<f32>,\n};\nfn gather_quad(cc: vec2<f32>) -> Quad {\n let r = textureGather(0, src_tex, smp, cc) * 255.0;\n let g = textureGather(1, src_tex, smp, cc) * 255.0;\n let b = textureGather(2, src_tex, smp, cc) * 255.0;\n var o: Quad;\n o.l = vec4<f16>(r + g + b);\n o.right = vec3<f32>(r.z + r.y, g.z + g.y, b.z + b.y);\n o.s = o.right + vec3<f32>(r.w + r.x, g.w + g.x, b.w + b.x);\n o.bottom = vec3<f32>(r.x + r.y, g.x + g.y, b.x + b.y);\n return o;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n\n // Luma by column: col[x][y]. Quadrant q = (x >= 2) | (y >= 2) << 1.\n var col: array<vec4<f16>, 4>;\n var qsum: array<vec3<f32>, 4>;\n // Planar right-hand sides: \u03A3 x\xB7p and \u03A3 y\xB7p.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n if (u32(base_xy.x) + 4u <= params.width && u32(base_xy.y) + 4u <= params.height) {\n let inv = vec2<f32>(1.0) / vec2<f32>(textureDimensions(src_tex));\n let c0 = (vec2<f32>(base_xy) + 1.0) * inv;\n let q0 = gather_quad(c0);\n let q1 = gather_quad(c0 + vec2<f32>(2.0, 0.0) * inv);\n let q2 = gather_quad(c0 + vec2<f32>(0.0, 2.0) * inv);\n let q3 = gather_quad(c0 + vec2<f32>(2.0, 2.0) * inv);\n qsum[0] = q0.s;\n qsum[1] = q1.s;\n qsum[2] = q2.s;\n qsum[3] = q3.s;\n sxp = q0.right + q2.right + 2.0 * (q1.s + q3.s) + q1.right + q3.right;\n syp = q0.bottom + q1.bottom + 2.0 * (q2.s + q3.s) + q2.bottom + q3.bottom;\n col[0] = vec4<f16>(q0.l.w, q0.l.x, q2.l.w, q2.l.x);\n col[1] = vec4<f16>(q0.l.z, q0.l.y, q2.l.z, q2.l.y);\n col[2] = vec4<f16>(q1.l.w, q1.l.x, q3.l.w, q3.l.x);\n col[3] = vec4<f16>(q1.l.z, q1.l.y, q3.l.z, q3.l.y);\n } else {\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i & 3u;\n let ly = i >> 2u;\n let p = clamp(base_xy + vec2<i32>(i32(lx), i32(ly)), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n col[lx][ly] = f16(c.r + c.g + c.b);\n let q = u32(lx >= 2u) | (u32(ly >= 2u) << 1u);\n qsum[q] = qsum[q] + c;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n }\n\n // ------------------------------------------------------------ planar --\n // LSQ plane in closed form: rhs rA = \u03A3(1 \u2212 x/4 \u2212 y/4)\xB7p, rB = \u03A3(x/4)\xB7p,\n // rC = \u03A3(y/4)\xB7p times the constant inverse Gram matrix (the same\n // coefficient form as the CPU mirror, so rounding ties resolve alike);\n // estimate with the quantised, clamped corners: \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n let qo = clamp(floor(po * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qh = clamp(floor(ph * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qv = clamp(floor(pv * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n // 6-bit expand (q<<2)|(q>>4) = floor(4.0625\xB7q); 7-bit (q<<1)|(q>>6) = floor(2.015625\xB7q).\n let xk = vec3<f32>(4.0625, 2.015625, 4.0625);\n let eo = floor(qo * xk);\n let eh = floor(qh * xk);\n let ev = floor(qv * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n let planar_est = dot(gram - 2.0 * (eo * rA + eh * rB + ev * rC), ONE3) + PLANAR_FUDGE;\n\n // ------------------------------------------------ flip + base selection --\n // Flip 0 splits columns (sum0a = left half), flip 1 splits rows (sum0b =\n // top half). Per flip, the preselect residual minus the flip-independent\n // \u03A3||p||\xB2 and \u03A3\u2113\xB2 terms: \u2212\u03A3||s||\xB2/8 + \u03BA\xB7(\u03A3\u2113)\xB2/24 over its two subblocks.\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let l0a = dot(sum0a, ONE3);\n let l1a = dot(sum1a, ONE3);\n let l0b = dot(sum0b, ONE3);\n let l1b = dot(sum1b, ONE3);\n let res_a = KAPPA / 24.0 * (l0a * l0a + l1a * l1a) - 0.125 * (dot(sum0a, sum0a) + dot(sum1a, sum1a));\n let res_b = KAPPA / 24.0 * (l0b * l0b + l1b * l1b) - 0.125 * (dot(sum0b, sum0b) + dot(sum1b, sum1b));\n let gray = all(qsum[0].rg == qsum[0].gb) && all(qsum[1].rg == qsum[1].gb) &&\n all(qsum[2].rg == qsum[2].gb) && all(qsum[3].rg == qsum[3].gb);\n\n var bflip = 0u;\n var sel: FlipFit;\n if (gray) {\n sel = fit_gray(col[0], col[1], col[2], col[3], sum0a.r, sum1a.r);\n let alt = fit_gray(\n vec4<f16>(col[0].xy, col[1].xy),\n vec4<f16>(col[2].xy, col[3].xy),\n vec4<f16>(col[0].zw, col[1].zw),\n vec4<f16>(col[2].zw, col[3].zw),\n sum0b.r,\n sum1b.r,\n );\n if (alt.est < sel.est) {\n sel = alt;\n bflip = 1u;\n }\n } else {\n let fb = res_b < res_a;\n bflip = select(0u, 1u, fb);\n sel = fit_flip(\n select(col[0], vec4<f16>(col[0].xy, col[1].xy), fb),\n select(col[1], vec4<f16>(col[2].xy, col[3].xy), fb),\n select(col[2], vec4<f16>(col[0].zw, col[1].zw), fb),\n select(col[3], vec4<f16>(col[2].zw, col[3].zw), fb),\n select(sum0a, sum0b, fb),\n select(sum1a, sum1b, fb),\n );\n }\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (sel.est <= planar_est) {\n let codes0 = vec3<u32>(sel.bases.c0);\n let codes1 = vec3<u32>(sel.bases.c1);\n let t0 = sel.t0;\n let t1 = sel.t1;\n if (sel.bases.diff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n // Wire indices, column by column (bit x\xB74 + y): flip 0 gives columns\n // 0,1 subblock 0; flip 1 gives rows 0,1 (lanes x, y) subblock 0.\n // LSB = large modifier, MSB = negative.\n let fb = bflip == 1u;\n let lb0 = f16(sel.lb0);\n let lb1 = f16(sel.lb1);\n let th0 = f16(THR[t0]);\n let th1 = f16(THR[t1]);\n let lb_rows = vec4<f16>(lb0, lb0, lb1, lb1);\n let th_rows = vec4<f16>(th0, th0, th1, th1);\n let lb_l = select(vec4<f16>(lb0), lb_rows, fb);\n let lb_r = select(vec4<f16>(lb1), lb_rows, fb);\n let th_l = select(vec4<f16>(th0), th_rows, fb);\n let th_r = select(vec4<f16>(th1), th_rows, fb);\n let bitv = vec4<u32>(1u, 2u, 4u, 8u);\n var lsb = 0u;\n var msb = 0u;\n for (var c: u32 = 0u; c < 4u; c = c + 1u) {\n let d = col[c] - select(lb_l, lb_r, c >= 2u);\n let large = select(vec4<u32>(0u), bitv, abs(d) > select(th_l, th_r, c >= 2u));\n let neg = select(vec4<u32>(0u), bitv, d < vec4<f16>(0.0));\n lsb = lsb | ((large.x | large.y | large.z | large.w) << (c * 4u));\n msb = msb | ((neg.x | neg.y | neg.z | neg.w) << (c * 4u));\n }\n lo = lsb | (msb << 16u);\n } else {\n let ro = u32(qo.r); let go = u32(qo.g); let bo = u32(qo.b);\n let rh = u32(qh.r); let gh = u32(qh.g); let bh = u32(qh.b);\n let rv = u32(qv.r); let gv = u32(qv.g); let bv = u32(qv.b);\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1851
|
+
var etc2_fast_f16_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. This is the f16 module; etc2.wgsl is the f32\n// fallback with the same algorithm.\n//\n// EXACT-VALUE f16: unlike the other formats' f16 fast paths (which accept\n// float rounding in a [0,1] domain), the f16 values here are integers (or\n// half-integers) that f16 represents exactly \u2014 per-texel lumas and base\n// lumas (<= 765), luma deviations |D| (<= 765) and the index thresholds\n// (<= 345, halves included) all sit below f16's exactness limits. Sums,\n// scores and estimates stay f32 (they reach ~1e6). Where the sampler's\n// unorm\u2192float conversion is exact (verified on Apple/metal-3), the f16 and\n// f32 modules are BYTE-IDENTICAL; elsewhere they can differ only on exact\n// decision ties. f16 buys register space (the 16 lumas are 4 \xD7 vec4<f16>)\n// and measured 1-3% faster than the f32 module on Apple.\n//\n// ALGORITHM \u2014 scalar-luma selection:\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests, and \u03A3||e||\xB2 per subblock is O(1) from the quadrant sums. The\n// block-constant \u03A3||p||\xB2 is dropped from EVERY estimate (ETC1 flips and\n// planar alike): only differences between estimates are ever used.\n// The estimate is exact for unclamped decode and an upper bound on the\n// true clamped error.\n// \u2022 Loads: 4 textureGather quads \xD7 R,G,B for interior blocks (the gather\n// point, normalised by the PHYSICAL texture size, sits exactly between\n// the quad's texel centres; interior quads never touch the zeroed\n// padding strip). Blocks straddling the edge of a non-multiple-of-4\n// image fall back to clamped per-texel loads. Lumas are kept as 4\n// COLUMN vectors \u2014 wire pixel order is x\xB74 + y \u2014 so both flips' half-\n// blocks and the index packing use only constant indexing.\n// \u2022 Flip preselect, O(1): per subblock the residual after continuous luma\n// modulation is within-variance \u2212 \u03BA\xB7(luma variance)/3, \u03BA = 0.9. \u03BA = 1\n// is the exact chroma residual; keeping a tenth of the luma variance\n// prefers the split with less luma spread for the 4-level tables to\n// cover (+0.07-0.10 dB on photo colour vs \u03BA = 1, free). Only the chosen\n// flip is searched.\n// \u2022 Exactly-gray blocks (every quadrant's R, G and B sums AND both planar\n// moments equal) have no chroma to steer the preselect, so both flips\n// are scored \u2014 worth ~0.3 dB on roughness/AO content over any O(1)\n// proxy tried (luma variance, luma range, squared range all land at\n// \u22120.30 dB; deciding the flip on the cover table's score alone loses\n// 0.3-0.5 dB). They use a one-channel copy of the fit (fit_gray), the\n// second flip is a separate straight-line call (the older\n// single-call-site loop cost 7-9% even on colour content that never ran\n// its second iteration), and their planar solve runs on two channels:\n// R and B share the 6-bit code, G takes the 7-bit one (\u22124..5% on gray\n// maps).\n// Widening the second evaluation to chroma near-ties (the previous\n// rule) cost 12-25% on colour textures through warp divergence for\n// \u2264 0.015 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~0.7-2.9 dB; all eight gain \u2264 0.05 dB. Scores use\n// the min form: per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) is the\n// threshold rule exactly, and its a3 part sums in closed form. A flip's\n// two subblocks are searched together (sb_pair): the lower-neighbour\n// scores sit behind ONE branch, skipped when both covers are table 0 \u2014\n// then the lower neighbour IS the cover table. Smooth content skips it\n// wholesale (\u22129..16% on displacement maps at 2K/4K); a per-subblock\n// branch cost 2-4% on noisy content by splitting the score pair.\n// \u2022 NO base refit (worth ~0.2 dB on photo colour for \u2265 13% GPU).\n// \u2022 PLANAR runs unconditionally: the LSQ solve is O(1) from the block sum\n// and the first moments \u03A3x\xB7p, \u03A3y\xB7p (the Gram inverse of the fixed\n// sample positions is a constant; folding it into fewer coefficients\n// saved ~1% but resolved rounding ties unlike the CPU mirror on ~9% of\n// the colour card's blocks), and its residual is the closed form\n// \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED, clamped corners \u2014\n// clamp-aware, which a continuous-corner estimate is not. Gating the\n// quantised evaluation on the continuous plane's residual (an exact\n// lower bound) is byte-identical but measured 0-1%: ~half the warps\n// still hold a block that needs it.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks and needs a clustering pass.\n//\n// Numeric notes: every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact;\n// est values are integer sums held exactly in f32 (< 2^24) apart from the\n// planar solve's decimal weights.\n\nenable f16;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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@group(0) @binding(3) var smp: sampler;\n\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n// Fraction of the luma variance the flip preselect treats as absorbed.\nconst KAPPA = 0.9;\nconst ONE3 = vec3<f32>(1.0);\nconst ONE4 = vec4<f32>(1.0);\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nfn max4(v: vec4<f16>) -> f16 {\n return max(max(v.x, v.y), max(v.z, v.w));\n}\n\n// Base colours from subblock SUMS (8 texels each): codes (as floats) and\n// their 8-bit expansions. Differential mode when the 5-bit codes are within\n// the 3-bit delta range, else individual 4-bit. Expansions in float:\n// (q<<3)|(q>>2) = floor(8.25\xB7q) for 5 bits, (q<<4)|q = 17\xB7q for 4 bits.\nstruct Bases {\n c0: vec3<f32>,\n c1: vec3<f32>,\n b0: vec3<f32>,\n b1: vec3<f32>,\n diff: bool,\n};\nfn quantise_bases(sum0: vec3<f32>, sum1: vec3<f32>) -> Bases {\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n var o: Bases;\n o.diff = all(d >= vec3<f32>(-4.0)) && all(d <= vec3<f32>(3.0));\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.c0 = select(i0, q0, o.diff);\n o.c1 = select(i1, q1, o.diff);\n o.b0 = select(i0 * 17.0, floor(q0 * 8.25), o.diff);\n o.b1 = select(i1 * 17.0, floor(q1 * 8.25), o.diff);\n return o;\n}\n\n// Subblock error (\xD73) of table t under the threshold rule, in min form:\n// per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) = (a3\xB2 \u2212 2\xB7a3\xB7ad) +\n// min(0, (b3\xB2 \u2212 a3\xB2) \u2212 2\xB7(b3 \u2212 a3)\xB7ad); the a3 part sums in closed form\n// from sad = \u03A3 ad.\n// Per-table score constants: DK = b3\xB2 \u2212 a3\xB2, DM = \u22122(b3 \u2212 a3), A8 = 8\xB7a3\xB2,\n// AM = \u22122\xB7a3 (precomputed: \u22121.5% GPU over deriving them per call).\nconst DK = array<f32, 8>(540.0, 2376.0, 6840.0, 14355.0, 29484.0, 52416.0, 91323.0, 281520.0);\nconst DM = array<f32, 8>(-36.0, -72.0, -120.0, -174.0, -252.0, -336.0, -438.0, -816.0);\nconst A8 = array<f32, 8>(288.0, 1800.0, 5832.0, 12168.0, 23328.0, 41472.0, 78408.0, 159048.0);\nconst AM = array<f32, 8>(-12.0, -30.0, -54.0, -78.0, -108.0, -144.0, -198.0, -282.0);\nfn table_score(au: vec4<f32>, av: vec4<f32>, sad: f32, t: u32) -> f32 {\n let dk = DK[t];\n let dm = DM[t];\n let eu = min(vec4<f32>(0.0), au * dm + dk);\n let ev = min(vec4<f32>(0.0), av * dm + dk);\n return A8[t] + AM[t] * sad + dot(eu + ev, ONE4);\n}\n\n// Both subblocks of one flip (lumas u, v against base luma lb): cover\n// tables and their scores, then the lower neighbours behind ONE branch \u2014\n// skipped when both covers are table 0 (the lower neighbour IS the cover\n// table; smooth content), branch-free inside so the two scores interleave.\n// |D| is exact in f16; the scores need f32.\nstruct PairOut {\n t0: u32,\n t1: u32,\n acc: f32,\n};\nfn sb_pair(u0: vec4<f16>, v0: vec4<f16>, lbf0: f32, u1: vec4<f16>, v1: vec4<f16>, lbf1: f32) -> PairOut {\n let lb0 = f16(lbf0);\n let lb1 = f16(lbf1);\n let ah0 = abs(u0 - lb0);\n let bh0 = abs(v0 - lb0);\n let ah1 = abs(u1 - lb1);\n let bh1 = abs(v1 - lb1);\n let mx = vec2<f16>(max(max4(ah0), max4(bh0)), max(max4(ah1), max4(bh1)));\n let au0 = vec4<f32>(ah0);\n let av0 = vec4<f32>(bh0);\n let au1 = vec4<f32>(ah1);\n let av1 = vec4<f32>(bh1);\n let sad0 = dot(au0 + av0, ONE4);\n let sad1 = dot(au1 + av1, ONE4);\n // cover = #{B3[k] < mx : k < 7}, the first table whose large modifier\n // reaches mx \u2014 a binary search over the 7 thresholds.\n let s1 = mx > vec2<f16>(126.0h);\n let s2 = mx > select(vec2<f16>(51.0h), vec2<f16>(240.0h), s1);\n let s3 = mx > select(select(vec2<f16>(24.0h), vec2<f16>(87.0h), s2), select(vec2<f16>(180.0h), vec2<f16>(318.0h), s2), s1);\n let cover = select(vec2<u32>(0u), vec2<u32>(4u), s1) + select(vec2<u32>(0u), vec2<u32>(2u), s2) + select(vec2<u32>(0u), vec2<u32>(1u), s3);\n let hi0 = table_score(au0, av0, sad0, cover.x);\n let hi1 = table_score(au1, av1, sad1, cover.y);\n var out: PairOut;\n out.t0 = cover.x;\n out.t1 = cover.y;\n out.acc = hi0 + hi1;\n if (any(cover != vec2<u32>(0u))) {\n let t_lo = max(cover, vec2<u32>(1u)) - vec2<u32>(1u);\n let lo0 = table_score(au0, av0, sad0, t_lo.x);\n let lo1 = table_score(au1, av1, sad1, t_lo.y);\n let w0 = lo0 <= hi0;\n let w1 = lo1 <= hi1;\n out.t0 = select(cover.x, t_lo.x, w0);\n out.t1 = select(cover.y, t_lo.y, w1);\n out.acc = select(hi0, lo0, w0) + select(hi1, lo1, w1);\n }\n return out;\n}\n\n// One flip's fit: base quantisation + table search, and its estimate\n// (\u03A3||p||\xB2 omitted).\nstruct FlipFit {\n est: f32,\n bases: Bases,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn fit_flip(\n s0u: vec4<f16>,\n s0v: vec4<f16>,\n s1u: vec4<f16>,\n s1v: vec4<f16>,\n sum0: vec3<f32>,\n sum1: vec3<f32>,\n) -> FlipFit {\n var out: FlipFit;\n out.bases = quantise_bases(sum0, sum1);\n let b0 = out.bases.b0;\n let b1 = out.bases.b1;\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let pp = sb_pair(s0u, s0v, out.lb0, s1u, s1v, out.lb1);\n out.t0 = pp.t0;\n out.t1 = pp.t1;\n out.est = dot(b0, 8.0 * b0 - 2.0 * sum0) + dot(b1, 8.0 * b1 - 2.0 * sum1) + pp.acc * (1.0 / 3.0);\n return out;\n}\n\n// fit_flip for exactly-gray blocks (r = g = b): the same arithmetic on one\n// channel; sum0/sum1 are one channel's subblock sums.\nfn fit_gray(\n s0u: vec4<f16>,\n s0v: vec4<f16>,\n s1u: vec4<f16>,\n s1v: vec4<f16>,\n sum0: f32,\n sum1: f32,\n) -> FlipFit {\n var out: FlipFit;\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n let diff = d >= -4.0 && d <= 3.0;\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n out.bases.diff = diff;\n out.bases.c0 = vec3<f32>(select(i0, q0, diff));\n out.bases.c1 = vec3<f32>(select(i1, q1, diff));\n let b0 = select(i0 * 17.0, floor(q0 * 8.25), diff);\n let b1 = select(i1 * 17.0, floor(q1 * 8.25), diff);\n out.lb0 = 3.0 * b0;\n out.lb1 = 3.0 * b1;\n let pp = sb_pair(s0u, s0v, out.lb0, s1u, s1v, out.lb1);\n out.t0 = pp.t0;\n out.t1 = pp.t1;\n out.est = 3.0 * (b0 * (8.0 * b0 - 2.0 * sum0) + b1 * (8.0 * b1 - 2.0 * sum1)) + pp.acc * (1.0 / 3.0);\n return out;\n}\n\n// One gathered 2\xD72 quad: per-texel luma (gather order), channel sums, and\n// the sums of its right column and bottom row (the planar moments' local\n// parts). Gather order: w=(0,0) z=(1,0) x=(0,1) y=(1,1).\nstruct Quad {\n l: vec4<f16>,\n s: vec3<f32>,\n right: vec3<f32>,\n bottom: vec3<f32>,\n};\nfn gather_quad(cc: vec2<f32>) -> Quad {\n let r = textureGather(0, src_tex, smp, cc) * 255.0;\n let g = textureGather(1, src_tex, smp, cc) * 255.0;\n let b = textureGather(2, src_tex, smp, cc) * 255.0;\n var o: Quad;\n o.l = vec4<f16>(r + g + b);\n o.right = vec3<f32>(r.z + r.y, g.z + g.y, b.z + b.y);\n o.s = o.right + vec3<f32>(r.w + r.x, g.w + g.x, b.w + b.x);\n o.bottom = vec3<f32>(r.x + r.y, g.x + g.y, b.x + b.y);\n return o;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n\n // Luma by column: col[x][y]. Quadrant q = (x >= 2) | (y >= 2) << 1.\n var col: array<vec4<f16>, 4>;\n var qsum: array<vec3<f32>, 4>;\n // Planar right-hand sides: \u03A3 x\xB7p and \u03A3 y\xB7p.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n if (u32(base_xy.x) + 4u <= params.width && u32(base_xy.y) + 4u <= params.height) {\n let inv = vec2<f32>(1.0) / vec2<f32>(textureDimensions(src_tex));\n let c0 = (vec2<f32>(base_xy) + 1.0) * inv;\n let q0 = gather_quad(c0);\n let q1 = gather_quad(c0 + vec2<f32>(2.0, 0.0) * inv);\n let q2 = gather_quad(c0 + vec2<f32>(0.0, 2.0) * inv);\n let q3 = gather_quad(c0 + vec2<f32>(2.0, 2.0) * inv);\n qsum[0] = q0.s;\n qsum[1] = q1.s;\n qsum[2] = q2.s;\n qsum[3] = q3.s;\n sxp = q0.right + q2.right + 2.0 * (q1.s + q3.s) + q1.right + q3.right;\n syp = q0.bottom + q1.bottom + 2.0 * (q2.s + q3.s) + q2.bottom + q3.bottom;\n col[0] = vec4<f16>(q0.l.w, q0.l.x, q2.l.w, q2.l.x);\n col[1] = vec4<f16>(q0.l.z, q0.l.y, q2.l.z, q2.l.y);\n col[2] = vec4<f16>(q1.l.w, q1.l.x, q3.l.w, q3.l.x);\n col[3] = vec4<f16>(q1.l.z, q1.l.y, q3.l.z, q3.l.y);\n } else {\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i & 3u;\n let ly = i >> 2u;\n let p = clamp(base_xy + vec2<i32>(i32(lx), i32(ly)), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n col[lx][ly] = f16(c.r + c.g + c.b);\n let q = u32(lx >= 2u) | (u32(ly >= 2u) << 1u);\n qsum[q] = qsum[q] + c;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n }\n\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n // Exactly gray: every quadrant sum AND both planar moments equal across\n // R, G, B \u2014 then R and B planar corners coincide (same 6-bit code) and\n // only R (6-bit) and G (7-bit) need solving.\n let gray = all(qsum[0].rg == qsum[0].gb) && all(qsum[1].rg == qsum[1].gb) &&\n all(qsum[2].rg == qsum[2].gb) && all(qsum[3].rg == qsum[3].gb) &&\n all(sxp.rg == sxp.gb) && all(syp.rg == syp.gb);\n\n var planar_est: f32;\n var qo: vec3<f32>;\n var qh: vec3<f32>;\n var qv: vec3<f32>;\n var bflip = 0u;\n var sel: FlipFit;\n if (gray) {\n // Planar on two channels: R and B share the 6-bit solve.\n let rB = sxp.r * 0.25;\n let rC = syp.r * 0.25;\n let rA = total.r - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec2<f32>(63.0, 127.0);\n let qo2 = clamp(floor(po * (pmax / 255.0) + 0.5), vec2<f32>(0.0), pmax);\n let qh2 = clamp(floor(ph * (pmax / 255.0) + 0.5), vec2<f32>(0.0), pmax);\n let qv2 = clamp(floor(pv * (pmax / 255.0) + 0.5), vec2<f32>(0.0), pmax);\n let xk = vec2<f32>(4.0625, 2.015625);\n let eo = floor(qo2 * xk);\n let eh = floor(qh2 * xk);\n let ev = floor(qv2 * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n let pe = gram - 2.0 * (eo * rA + eh * rB + ev * rC);\n planar_est = 2.0 * pe.x + pe.y + PLANAR_FUDGE;\n qo = qo2.xyx;\n qh = qh2.xyx;\n qv = qv2.xyx;\n\n let sum0a = qsum[0].r + qsum[2].r;\n let sum1a = qsum[1].r + qsum[3].r;\n let sum0b = qsum[0].r + qsum[1].r;\n let sum1b = qsum[2].r + qsum[3].r;\n sel = fit_gray(col[0], col[1], col[2], col[3], sum0a, sum1a);\n let alt = fit_gray(\n vec4<f16>(col[0].xy, col[1].xy),\n vec4<f16>(col[2].xy, col[3].xy),\n vec4<f16>(col[0].zw, col[1].zw),\n vec4<f16>(col[2].zw, col[3].zw),\n sum0b,\n sum1b,\n );\n if (alt.est < sel.est) {\n sel = alt;\n bflip = 1u;\n }\n } else {\n // LSQ plane in closed form: rhs rA = \u03A3(1 \u2212 x/4 \u2212 y/4)\xB7p, rB = \u03A3(x/4)\xB7p,\n // rC = \u03A3(y/4)\xB7p times the constant inverse Gram matrix (the same\n // coefficient form as the CPU mirror, so rounding ties resolve alike);\n // estimate with the quantised, clamped corners: \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8.\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n qo = clamp(floor(po * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n qh = clamp(floor(ph * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n qv = clamp(floor(pv * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n // 6-bit expand (q<<2)|(q>>4) = floor(4.0625\xB7q); 7-bit (q<<1)|(q>>6) = floor(2.015625\xB7q).\n let xk = vec3<f32>(4.0625, 2.015625, 4.0625);\n let eo = floor(qo * xk);\n let eh = floor(qh * xk);\n let ev = floor(qv * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n planar_est = dot(gram - 2.0 * (eo * rA + eh * rB + ev * rC), ONE3) + PLANAR_FUDGE;\n\n // Flip 0 splits columns (sum0a = left half), flip 1 splits rows (sum0b =\n // top half). Per flip, the preselect residual minus the flip-independent\n // \u03A3||p||\xB2 and \u03A3\u2113\xB2 terms: \u2212\u03A3||s||\xB2/8 + \u03BA\xB7(\u03A3\u2113)\xB2/24 over its two subblocks.\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let l0a = dot(sum0a, ONE3);\n let l1a = dot(sum1a, ONE3);\n let l0b = dot(sum0b, ONE3);\n let l1b = dot(sum1b, ONE3);\n let res_a = KAPPA / 24.0 * (l0a * l0a + l1a * l1a) - 0.125 * (dot(sum0a, sum0a) + dot(sum1a, sum1a));\n let res_b = KAPPA / 24.0 * (l0b * l0b + l1b * l1b) - 0.125 * (dot(sum0b, sum0b) + dot(sum1b, sum1b));\n let fb = res_b < res_a;\n bflip = select(0u, 1u, fb);\n sel = fit_flip(\n select(col[0], vec4<f16>(col[0].xy, col[1].xy), fb),\n select(col[1], vec4<f16>(col[2].xy, col[3].xy), fb),\n select(col[2], vec4<f16>(col[0].zw, col[1].zw), fb),\n select(col[3], vec4<f16>(col[2].zw, col[3].zw), fb),\n select(sum0a, sum0b, fb),\n select(sum1a, sum1b, fb),\n );\n }\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (sel.est <= planar_est) {\n let codes0 = vec3<u32>(sel.bases.c0);\n let codes1 = vec3<u32>(sel.bases.c1);\n let t0 = sel.t0;\n let t1 = sel.t1;\n if (sel.bases.diff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n // Wire indices, column by column (bit x\xB74 + y): flip 0 gives columns\n // 0,1 subblock 0; flip 1 gives rows 0,1 (lanes x, y) subblock 0.\n // LSB = large modifier, MSB = negative.\n let fb = bflip == 1u;\n let lb0 = f16(sel.lb0);\n let lb1 = f16(sel.lb1);\n let th0 = f16(THR[t0]);\n let th1 = f16(THR[t1]);\n let lb_rows = vec4<f16>(lb0, lb0, lb1, lb1);\n let th_rows = vec4<f16>(th0, th0, th1, th1);\n let lb_l = select(vec4<f16>(lb0), lb_rows, fb);\n let lb_r = select(vec4<f16>(lb1), lb_rows, fb);\n let th_l = select(vec4<f16>(th0), th_rows, fb);\n let th_r = select(vec4<f16>(th1), th_rows, fb);\n let bitv = vec4<u32>(1u, 2u, 4u, 8u);\n var lsb = 0u;\n var msb = 0u;\n for (var c: u32 = 0u; c < 4u; c = c + 1u) {\n let d = col[c] - select(lb_l, lb_r, c >= 2u);\n let large = select(vec4<u32>(0u), bitv, abs(d) > select(th_l, th_r, c >= 2u));\n let neg = select(vec4<u32>(0u), bitv, d < vec4<f16>(0.0));\n lsb = lsb | ((large.x | large.y | large.z | large.w) << (c * 4u));\n msb = msb | ((neg.x | neg.y | neg.z | neg.w) << (c * 4u));\n }\n lo = lsb | (msb << 16u);\n } else {\n let ro = u32(qo.r); let go = u32(qo.g); let bo = u32(qo.b);\n let rh = u32(qh.r); let gh = u32(qh.g); let bh = u32(qh.b);\n let rv = u32(qv.r); let gv = u32(qv.g); let bv = u32(qv.b);\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1859
1852
|
|
|
1860
1853
|
// src/ETC2Encoder.ts
|
|
1861
1854
|
var ETC2Encoder = class extends Encoder {
|
|
@@ -1923,12 +1916,6 @@ function compileShader(gl, type, source, label) {
|
|
|
1923
1916
|
if (!shader) throw new Error(`${label}: gl.createShader failed`);
|
|
1924
1917
|
gl.shaderSource(shader, source);
|
|
1925
1918
|
gl.compileShader(shader);
|
|
1926
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1927
|
-
const log = gl.getShaderInfoLog(shader);
|
|
1928
|
-
gl.deleteShader(shader);
|
|
1929
|
-
const kind = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
1930
|
-
throw new Error(`${label}: ${kind} shader compile failed: ${log}`);
|
|
1931
|
-
}
|
|
1932
1919
|
return shader;
|
|
1933
1920
|
}
|
|
1934
1921
|
var WebGLBlockEncoder = class {
|
|
@@ -1949,6 +1936,31 @@ var WebGLBlockEncoder = class {
|
|
|
1949
1936
|
_uSrc = null;
|
|
1950
1937
|
_uSrcSize = null;
|
|
1951
1938
|
_uFlipY = null;
|
|
1939
|
+
// Program compile/link runs asynchronously in the driver where
|
|
1940
|
+
// KHR_parallel_shader_compile is available; the link status (and the
|
|
1941
|
+
// uniform lookups, which would block on it) are deferred to first use.
|
|
1942
|
+
_parallel = null;
|
|
1943
|
+
_shaders = [];
|
|
1944
|
+
_linked = false;
|
|
1945
|
+
// Per-encoder resource cache (mirrors the WebGPU Encoder). Sequential
|
|
1946
|
+
// encodes reuse the source texture while its size is stable and the
|
|
1947
|
+
// RGBA32UI target + framebuffer while the block grid is stable, so a
|
|
1948
|
+
// same-sized encode allocates nothing. encodeToBytes() is synchronous, so
|
|
1949
|
+
// there is no concurrent-encode case to guard.
|
|
1950
|
+
_srcTex = null;
|
|
1951
|
+
_srcW = 0;
|
|
1952
|
+
_srcH = 0;
|
|
1953
|
+
// Upload memoisation: the ImageBitmap whose pixels _srcTex currently
|
|
1954
|
+
// holds. ImageBitmaps are immutable, so re-encoding the same bitmap
|
|
1955
|
+
// (format A/B, quality-ladder re-encodes, benchmark loops) skips the
|
|
1956
|
+
// upload — ~14-19 ms of a ~22-28 ms 4096² encode. Mutable sources
|
|
1957
|
+
// (ImageData, raw pixels, canvases, images) are always re-uploaded. flipY
|
|
1958
|
+
// is applied in the shader, so it doesn't affect the texture contents.
|
|
1959
|
+
_srcBitmap = null;
|
|
1960
|
+
_outTex = null;
|
|
1961
|
+
_fbo = null;
|
|
1962
|
+
_outBX = 0;
|
|
1963
|
+
_outBY = 0;
|
|
1952
1964
|
constructor({ gl }) {
|
|
1953
1965
|
this.gl = gl;
|
|
1954
1966
|
this._buildProgram();
|
|
@@ -1958,59 +1970,113 @@ var WebGLBlockEncoder = class {
|
|
|
1958
1970
|
const program = gl.createProgram();
|
|
1959
1971
|
const vao = gl.createVertexArray();
|
|
1960
1972
|
if (!program || !vao) throw new Error(`${this.label}: failed to allocate WebGL program/VAO`);
|
|
1973
|
+
this._parallel = gl.getExtension("KHR_parallel_shader_compile");
|
|
1961
1974
|
const vert = compileShader(gl, gl.VERTEX_SHADER, fullscreen_vert_default, this.label);
|
|
1962
1975
|
const frag = compileShader(gl, gl.FRAGMENT_SHADER, this.fragSource(), this.label);
|
|
1963
1976
|
gl.attachShader(program, vert);
|
|
1964
1977
|
gl.attachShader(program, frag);
|
|
1965
1978
|
gl.linkProgram(program);
|
|
1966
|
-
|
|
1967
|
-
gl.deleteShader(frag);
|
|
1968
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1969
|
-
const log = gl.getProgramInfoLog(program);
|
|
1970
|
-
gl.deleteProgram(program);
|
|
1971
|
-
throw new Error(`${this.label}: WebGL program link failed: ${log}`);
|
|
1972
|
-
}
|
|
1979
|
+
this._shaders = [vert, frag];
|
|
1973
1980
|
this._program = program;
|
|
1974
1981
|
this._vao = vao;
|
|
1982
|
+
}
|
|
1983
|
+
/** Check the link (blocking until it completes), then look up uniforms. */
|
|
1984
|
+
_ensureLinked() {
|
|
1985
|
+
if (this._linked) return;
|
|
1986
|
+
const gl = this.gl;
|
|
1987
|
+
const program = this._program;
|
|
1988
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1989
|
+
const logs = [gl.getProgramInfoLog(program), ...this._shaders.map((sh) => gl.getShaderInfoLog(sh))];
|
|
1990
|
+
throw new Error(`${this.label}: WebGL program link failed: ${logs.filter(Boolean).join("\n")}`);
|
|
1991
|
+
}
|
|
1992
|
+
for (const sh of this._shaders) gl.deleteShader(sh);
|
|
1993
|
+
this._shaders = [];
|
|
1975
1994
|
this._uSrc = gl.getUniformLocation(program, "uSrc");
|
|
1976
1995
|
this._uSrcSize = gl.getUniformLocation(program, "uSrcSize");
|
|
1977
1996
|
this._uFlipY = gl.getUniformLocation(program, "uFlipY");
|
|
1997
|
+
this._linked = true;
|
|
1978
1998
|
}
|
|
1979
|
-
/**
|
|
1999
|
+
/**
|
|
2000
|
+
* Resolves once the fragment program has compiled and linked, without
|
|
2001
|
+
* blocking the main thread where the driver exposes
|
|
2002
|
+
* KHR_parallel_shader_compile (elsewhere the final status check waits for
|
|
2003
|
+
* the driver). Encodes check this themselves; call it to compile ahead of
|
|
2004
|
+
* first use (see `prewarmCompressTexture()`). Rejects on a compile/link
|
|
2005
|
+
* error.
|
|
2006
|
+
*/
|
|
2007
|
+
async ready() {
|
|
2008
|
+
const gl = this.gl;
|
|
2009
|
+
const ext = this._parallel;
|
|
2010
|
+
while (ext && !this._linked && !gl.isContextLost() && !gl.getProgramParameter(this._program, ext.COMPLETION_STATUS_KHR)) {
|
|
2011
|
+
await new Promise((resolve) => setTimeout(resolve, 4));
|
|
2012
|
+
}
|
|
2013
|
+
if (gl.isContextLost()) throw new Error(`${this.label}WebGLEncoder: WebGL context lost`);
|
|
2014
|
+
this._ensureLinked();
|
|
2015
|
+
}
|
|
2016
|
+
/** Release the GL program, VAO and cached textures. The shared context itself is left intact. */
|
|
1980
2017
|
destroy() {
|
|
1981
2018
|
const gl = this.gl;
|
|
1982
|
-
if (gl.isContextLost())
|
|
1983
|
-
|
|
1984
|
-
|
|
2019
|
+
if (!gl.isContextLost()) {
|
|
2020
|
+
for (const sh of this._shaders) gl.deleteShader(sh);
|
|
2021
|
+
gl.deleteProgram(this._program);
|
|
2022
|
+
gl.deleteVertexArray(this._vao);
|
|
2023
|
+
if (this._srcTex) gl.deleteTexture(this._srcTex);
|
|
2024
|
+
if (this._outTex) gl.deleteTexture(this._outTex);
|
|
2025
|
+
if (this._fbo) gl.deleteFramebuffer(this._fbo);
|
|
2026
|
+
}
|
|
2027
|
+
this._shaders = [];
|
|
2028
|
+
this._srcTex = null;
|
|
2029
|
+
this._srcBitmap = null;
|
|
2030
|
+
this._outTex = null;
|
|
2031
|
+
this._fbo = null;
|
|
1985
2032
|
}
|
|
1986
2033
|
/**
|
|
1987
|
-
* Upload the source image to
|
|
1988
|
-
* Raw pixel sources (ImageData / mip
|
|
1989
|
-
* overload; DOM sources (ImageBitmap /
|
|
1990
|
-
* overload. No flip / premultiply /
|
|
1991
|
-
* the shader so each mip level
|
|
2034
|
+
* Upload the source image to the cached RGBA8 texture (recreated when the
|
|
2035
|
+
* image size changes), bound on unit 0. Raw pixel sources (ImageData / mip
|
|
2036
|
+
* levels) go through the typed-array overload; DOM sources (ImageBitmap /
|
|
2037
|
+
* canvas / image) through the element overload. No flip / premultiply /
|
|
2038
|
+
* colour conversion — flipY is applied in the shader so each mip level
|
|
2039
|
+
* flips by its own height. Re-encoding the ImageBitmap already held by the
|
|
2040
|
+
* texture skips the upload.
|
|
1992
2041
|
*/
|
|
1993
2042
|
_uploadSource(source, width, height) {
|
|
1994
2043
|
const gl = this.gl;
|
|
1995
|
-
const tex = gl.createTexture();
|
|
1996
|
-
if (!tex) throw new Error(`${this.label}: gl.createTexture failed`);
|
|
1997
2044
|
gl.activeTexture(gl.TEXTURE0);
|
|
1998
|
-
|
|
2045
|
+
const isBitmap = typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap;
|
|
2046
|
+
const sameSize = this._srcTex !== null && this._srcW === width && this._srcH === height;
|
|
2047
|
+
if (sameSize && isBitmap && this._srcBitmap === source) {
|
|
2048
|
+
gl.bindTexture(gl.TEXTURE_2D, this._srcTex);
|
|
2049
|
+
return this._srcTex;
|
|
2050
|
+
}
|
|
2051
|
+
if (!sameSize) {
|
|
2052
|
+
if (this._srcTex) gl.deleteTexture(this._srcTex);
|
|
2053
|
+
const tex = gl.createTexture();
|
|
2054
|
+
if (!tex) throw new Error(`${this.label}: gl.createTexture failed`);
|
|
2055
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
2056
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height);
|
|
2057
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
2058
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
2059
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2060
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2061
|
+
this._srcTex = tex;
|
|
2062
|
+
this._srcW = width;
|
|
2063
|
+
this._srcH = height;
|
|
2064
|
+
} else {
|
|
2065
|
+
gl.bindTexture(gl.TEXTURE_2D, this._srcTex);
|
|
2066
|
+
}
|
|
2067
|
+
this._srcBitmap = null;
|
|
1999
2068
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
2000
2069
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
2001
2070
|
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
|
|
2002
2071
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
2003
2072
|
const raw = source;
|
|
2004
2073
|
if (raw.data && ArrayBuffer.isView(raw.data)) {
|
|
2005
|
-
gl.
|
|
2074
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, raw.data);
|
|
2006
2075
|
} else {
|
|
2007
|
-
gl.
|
|
2076
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
2008
2077
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2012
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2013
|
-
return tex;
|
|
2078
|
+
if (isBitmap) this._srcBitmap = source;
|
|
2079
|
+
return this._srcTex;
|
|
2014
2080
|
}
|
|
2015
2081
|
/**
|
|
2016
2082
|
* Encode one image source to raw compressed bytes. `flipY` samples the source
|
|
@@ -2020,6 +2086,7 @@ var WebGLBlockEncoder = class {
|
|
|
2020
2086
|
encodeToBytes(source, { flipY = false } = {}) {
|
|
2021
2087
|
const gl = this.gl;
|
|
2022
2088
|
if (gl.isContextLost()) throw new Error(`${this.label}WebGLEncoder: WebGL context lost`);
|
|
2089
|
+
this._ensureLinked();
|
|
2023
2090
|
const width = source.width;
|
|
2024
2091
|
const height = source.height;
|
|
2025
2092
|
if (!width || !height) {
|
|
@@ -2033,27 +2100,38 @@ var WebGLBlockEncoder = class {
|
|
|
2033
2100
|
const outByteLen = blockCount * this.bytesPerBlock;
|
|
2034
2101
|
const t0 = performance.now();
|
|
2035
2102
|
const srcTex = this._uploadSource(source, width, height);
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER,
|
|
2053
|
-
gl.
|
|
2054
|
-
gl.
|
|
2055
|
-
gl.
|
|
2056
|
-
|
|
2103
|
+
if (!this._outTex || !this._fbo || this._outBX !== blocksX || this._outBY !== blocksY) {
|
|
2104
|
+
if (this._outTex) gl.deleteTexture(this._outTex);
|
|
2105
|
+
if (this._fbo) gl.deleteFramebuffer(this._fbo);
|
|
2106
|
+
this._outTex = null;
|
|
2107
|
+
this._fbo = null;
|
|
2108
|
+
const outTex = gl.createTexture();
|
|
2109
|
+
const fbo = gl.createFramebuffer();
|
|
2110
|
+
if (!outTex || !fbo) {
|
|
2111
|
+
if (outTex) gl.deleteTexture(outTex);
|
|
2112
|
+
if (fbo) gl.deleteFramebuffer(fbo);
|
|
2113
|
+
throw new Error(`${this.label}: failed to allocate output texture/framebuffer`);
|
|
2114
|
+
}
|
|
2115
|
+
gl.bindTexture(gl.TEXTURE_2D, outTex);
|
|
2116
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32UI, blocksX, blocksY);
|
|
2117
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
2118
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
2119
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
2120
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outTex, 0);
|
|
2121
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
2122
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
2123
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2124
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
2125
|
+
gl.deleteFramebuffer(fbo);
|
|
2126
|
+
gl.deleteTexture(outTex);
|
|
2127
|
+
throw new Error(`${this.label}: integer framebuffer incomplete (0x${status.toString(16)})`);
|
|
2128
|
+
}
|
|
2129
|
+
this._outTex = outTex;
|
|
2130
|
+
this._fbo = fbo;
|
|
2131
|
+
this._outBX = blocksX;
|
|
2132
|
+
this._outBY = blocksY;
|
|
2133
|
+
} else {
|
|
2134
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this._fbo);
|
|
2057
2135
|
}
|
|
2058
2136
|
gl.useProgram(this._program);
|
|
2059
2137
|
gl.bindVertexArray(this._vao);
|
|
@@ -2084,15 +2162,12 @@ var WebGLBlockEncoder = class {
|
|
|
2084
2162
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2085
2163
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
2086
2164
|
gl.bindVertexArray(null);
|
|
2087
|
-
gl.deleteFramebuffer(fbo);
|
|
2088
|
-
gl.deleteTexture(outTex);
|
|
2089
|
-
gl.deleteTexture(srcTex);
|
|
2090
2165
|
return { width, height, paddedWidth, paddedHeight, data, encodeMs };
|
|
2091
2166
|
}
|
|
2092
2167
|
};
|
|
2093
2168
|
|
|
2094
2169
|
// src/webgl/glsl/bc1.frag.glsl
|
|
2095
|
-
var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl
|
|
2170
|
+
var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl.\n//\n// One fragment per 4\xD74 block. Output is the 8-byte BC1 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused); the encoder reads back RGBA32UI and keeps\n// the low two words per block. Same algorithm and arithmetic as bc1.wgsl\n// (see bc1_fast_f16.wgsl for the measurements): near-flat blocks take a\n// solid colour \u2014 per channel the endpoint pair whose \u2154/\u2153 interpolant lands\n// nearest the block mean; other blocks get a principal-axis endpoint seed\n// (covariance power-iteration; inset bbox on degenerate blocks), inset by\n// ~half a 565 cell along the axis. A projection pass assigns every pixel the\n// rounded projection onto the decoded-endpoint line (the 4 palette entries\n// are colinear and evenly spaced, so that is the nearest entry) while\n// accumulating the block error and projection moments, followed by up to\n// TWO least-squares refit rounds solved from those moments (re-quantise,\n// reproject, accept only on lower block error). Always 4-colour mode.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize; // original (unpadded) width, height\nuniform int uFlipY; // 1 = sample bottom-up (matches Three.js flipY)\n\nlayout(location = 0) out uvec4 outColor;\n\nuint to565(vec3 c) {\n uint r = uint(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n uint g = uint(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n uint b = uint(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11) | (g << 5) | b;\n}\n\n// 5/6-bit \u2192 8-bit: (x*527+23)>>6 (6-bit: 259/33) \u2014 round-to-nearest scaling,\n// matching bc1_ref.ts and the hardware decoders (NOT plain bit-replication,\n// which differs for some codes, e.g. 5-bit 3 \u2192 25 vs 24).\nvec3 from565(uint c) {\n uint r = (c >> 11) & 31u;\n uint g = (c >> 5) & 63u;\n uint b = c & 31u;\n uint r8 = (r * 527u + 23u) >> 6;\n uint g8 = (g * 259u + 33u) >> 6;\n uint b8 = (b * 527u + 23u) >> 6;\n return vec3(float(r8), float(g8), float(b8)) / 255.0;\n}\n\n// Force 4-colour mode: c0 > c1 strictly.\nuvec2 order565(uint a, uint b) {\n uint c0 = a;\n uint c1 = b;\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n uint t = c0; c0 = c1; c1 = t;\n }\n return uvec2(c0, c1);\n}\n\n// Per-invocation scratch (GLSL would copy array parameters by value).\nvec3 gPixels[16];\n\n// One projection pass against the decoded endpoints of (c0,c1): levels\n// L = 0..3 along p0\u2192p1, the packed indices, the block's squared error, and\n// the projection MOMENTS a refit needs: \u03A3L, \u03A3L\xB2, \u03A3u, \u03A3L\xB7u (u = v \u2212 p0).\n// Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922, 2\u21923, 3\u21921 (c1): packed LUT (0x78 >> 2L) & 3.\nstruct Moments { float sL; float sLL; vec3 sU; vec3 sLu; uint indices; float err; };\nMoments moments(uint c0, uint c1) {\n vec3 p0 = from565(c0);\n vec3 dir = from565(c1) - p0;\n float inv = 3.0 / dot(dir, dir);\n Moments m = Moments(0.0, 0.0, vec3(0.0), vec3(0.0), 0u, 0.0);\n for (int k = 0; k < 16; k++) {\n vec3 u = gPixels[k] - p0;\n float L = clamp(floor(dot(u, dir) * inv + 0.5), 0.0, 3.0);\n m.sL += L;\n m.sLL += L * L;\n m.sU += u;\n m.sLu += L * u;\n m.indices |= ((0x78u >> (uint(L) * 2u)) & 3u) << (uint(k) * 2u);\n vec3 e = u - L * (1.0 / 3.0) * dir;\n m.err += dot(e, e);\n }\n return m;\n}\n\n// One least-squares refit from moments (b = L/3, a = 1 \u2212 b):\n// sBB = \u03A3L\xB2/9 sAB = \u03A3L/3 \u2212 \u03A3L\xB2/9 sAA = 16 \u2212 2\u03A3L/3 + \u03A3L\xB2/9\n// \u03A3b\xB7u = \u03A3L\xB7u/3 \u03A3a\xB7u = \u03A3u \u2212 \u03A3b\xB7u\n// clamped to [limLo, limHi], re-quantised and ordered. Returns (c0, c1)\n// unchanged when every pixel sits on ONE level (16\xB7\u03A3L\xB2 == (\u03A3L)\xB2, singular).\nuvec2 solve(Moments m, uint c0, uint c1, vec3 limLo, vec3 limHi) {\n if (16.0 * m.sLL == m.sL * m.sL) { return uvec2(c0, c1); }\n float sBB = m.sLL * (1.0 / 9.0);\n float sAB = m.sL * (1.0 / 3.0) - sBB;\n float sAA = 16.0 - m.sL * (2.0 / 3.0) + sBB;\n float det = sAA * sBB - sAB * sAB;\n vec3 p0 = from565(c0);\n vec3 sBu = m.sLu * (1.0 / 3.0);\n vec3 sAu = m.sU - sBu;\n vec3 e0 = clamp(p0 + (sBB * sAu - sAB * sBu) / det, limLo, limHi);\n vec3 e1 = clamp(p0 + (sAA * sBu - sAB * sAu) / det, limLo, limHi);\n return order565(to565(e0), to565(e1));\n}\n\n// Solid-colour channel code: the pair (a, b) of `bits`-bit codes whose \u2154/\u2153\n// interpolant (2\xB7dec(a) + dec(b))/3 \u2014 palette index 2 \u2014 lands nearest v\n// (8-bit units).\nuvec2 solidPair(float v, uint bits) {\n uint maxc = (1u << bits) - 1u;\n uint q = min(uint(v * float(maxc) / 255.0), maxc - 1u);\n float x;\n float y;\n if (bits == 5u) {\n x = float((q * 527u + 23u) >> 6);\n y = float(((q + 1u) * 527u + 23u) >> 6);\n } else {\n x = float((q * 259u + 33u) >> 6);\n y = float(((q + 1u) * 259u + 33u) >> 6);\n }\n uvec2 best = uvec2(q, q);\n float be = abs(x - v);\n float m1 = (2.0 * x + y) / 3.0;\n if (abs(m1 - v) < be) { be = abs(m1 - v); best = uvec2(q, q + 1u); }\n float m2 = (x + 2.0 * y) / 3.0;\n if (abs(m2 - v) < be) { be = abs(m2 - v); best = uvec2(q + 1u, q); }\n if (abs(y - v) < be) { best = uvec2(q + 1u, q + 1u); }\n return best;\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)\n// block. The bbox diagonal alone is sign-blind and points across\n// anti-correlated data (normal maps, hue edges) instead of along it.\nvec3 principalAxis(vec3 mean, vec3 seed) {\n vec3 c0v = vec3(0.0);\n vec3 c1v = vec3(0.0);\n vec3 c2v = vec3(0.0);\n for (int k = 0; k < 16; k++) {\n vec3 d = gPixels[k] - mean;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n }\n vec3 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec3(0.0); }\n v = v / len;\n for (int it = 0; it < 8; it++) {\n vec3 nv = vec3(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3(0.0); }\n v = nv / len;\n }\n return v;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n vec3 bbMin = vec3(1.0);\n vec3 bbMax = vec3(0.0);\n vec3 mean = vec3(0.0);\n float gd = 0.0;\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec3 c = texelFetch(uSrc, ivec2(p.x, sy), 0).rgb;\n gPixels[i] = c;\n bbMin = min(bbMin, c);\n bbMax = max(bbMax, c);\n mean += c;\n gd = max(gd, max(abs(c.x - c.y), abs(c.x - c.z)));\n }\n mean = mean * (1.0 / 16.0);\n // Exactly-gray blocks free the refit from the bbox clamp (no hue to\n // protect; smooth gradients want endpoints outside the data range).\n bool gray = gd == 0.0;\n vec3 limLo = gray ? vec3(0.0) : bbMin;\n vec3 limHi = gray ? vec3(1.0) : bbMax;\n\n // Near-flat blocks (every channel within 3 levels): solid colour at the\n // block mean; they share the index pass below and skip the seed + refits.\n vec3 span = bbMax - bbMin;\n bool flatBlock = max(max(span.x, span.y), span.z) <= 3.0 / 255.0;\n uint c0;\n uint c1;\n if (flatBlock) {\n vec3 m8 = mean * 255.0;\n uvec2 pr = solidPair(m8.x, 5u);\n uvec2 pg = solidPair(m8.y, 6u);\n uvec2 pb = solidPair(m8.z, 5u);\n uint s0 = (pr.x << 11) | (pg.x << 5) | pb.x;\n uint s1 = (pr.y << 11) | (pg.y << 5) | pb.y;\n c0 = max(s0, s1);\n c1 = min(s0, s1);\n } else {\n // Principal-axis seed at the exact projection extents, inset by ~half a\n // 565 cell along the axis. Degenerate blocks keep the inset-bbox seed.\n vec3 seedHi;\n vec3 seedLo;\n vec3 axis = principalAxis(mean, bbMax - bbMin);\n if (dot(axis, axis) > 0.0) {\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(gPixels[k] - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n float pad = (tMax - tMin) / 16.0;\n seedHi = clamp(mean + (tMax - pad) * axis, vec3(0.0), vec3(1.0));\n seedLo = clamp(mean + (tMin + pad) * axis, vec3(0.0), vec3(1.0));\n } else {\n vec3 inset = (bbMax - bbMin) / 16.0;\n seedHi = clamp(bbMax - inset, vec3(0.0), vec3(1.0));\n seedLo = clamp(bbMin + inset, vec3(0.0), vec3(1.0));\n }\n uvec2 seed = order565(to565(seedHi), to565(seedLo));\n c0 = seed.x;\n c1 = seed.y;\n }\n\n // Projection pass on the seed, then up to two least-squares refit rounds,\n // each re-projected and accepted only if the block error drops. Equal\n // flat codes encode the colour itself: index 0.\n uint indices = 0u;\n if (c0 != c1) {\n Moments cur = moments(c0, c1);\n int rounds = flatBlock ? 0 : 2;\n for (int it = 0; it < rounds; it++) {\n uvec2 cand = solve(cur, c0, c1, limLo, limHi);\n if (cand.x == c0 && cand.y == c1) { break; }\n Moments nxt = moments(cand.x, cand.y);\n if (nxt.err >= cur.err) { break; }\n c0 = cand.x;\n c1 = cand.y;\n cur = nxt;\n }\n indices = cur.indices;\n }\n\n outColor = uvec4(c0 | (c1 << 16), indices, 0u, 0u);\n}\n";
|
|
2096
2171
|
|
|
2097
2172
|
// src/webgl/BC1WebGLEncoder.ts
|
|
2098
2173
|
var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -2111,7 +2186,7 @@ var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
2111
2186
|
};
|
|
2112
2187
|
|
|
2113
2188
|
// src/webgl/glsl/bc5.frag.glsl
|
|
2114
|
-
var bc5_frag_default = "#version 300 es\n// BC5 (RGTC2) fragment-shader encoder \u2014 WebGL2 port of bc5.wgsl
|
|
2189
|
+
var bc5_frag_default = "#version 300 es\n// BC5 (RGTC2) fragment-shader encoder \u2014 WebGL2 port of bc5.wgsl.\n//\n// One fragment per 4\xD74 block \u2192 16-byte BC5 block as 4 \xD7 u32 in outColor.\n// BC5 = two BC4 halves (R then G), both channels processed together. Same\n// algorithm and arithmetic order as bc5.wgsl (see bc5_fast_f16.wgsl for the\n// full notes and measurements):\n// \u2022 texels held as quad-major vec4s per channel, in textureGather order\n// (x=(0,1) y=(1,1) z=(1,0) w=(0,0) within each 2\xD72 quad);\n// \u2022 seed endpoints at the per-channel extremes; pass 1 accumulates the\n// MOMENTS \u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d (d = v \u2212 r0, L = the seed level \u2014 the seed\n// covers the data, so no clamp), from which every least-squares sum is\n// O(1) per block; exact rank guard 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2;\n// \u2022 one closed-form refit accepted when it prices better on the seed\n// levels (nearest rounding of the solve, 6-interp mode kept);\n// \u2022 pass 2 derives the shipped levels against the FINAL endpoints, packed\n// as float fields \u03A3 L\xB78^k (exact below 2^24), then one SWAR level \u2192 BC4\n// index map (0\u21920, 7\u21921, L\u2192L+1) per 24-bit word.\n// Always emits 6-interpolation mode (red0 > red1).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// 8 packed 3-bit levels \u2192 BC4 indices (0\u21920, 7\u21921, L\u2192L+1 otherwise).\nuint lvlToIdx(uint x) {\n uint y = ((x & 0x6DB6DBu) + 0x249249u) ^ (x & 0x924924u);\n return y ^ (~((y >> 1u) | (y >> 2u)) & 0x249249u);\n}\n\n// Per-quad pixel weights 8^k (gather order x,y,z,w = (0,1),(1,1),(1,0),(0,0)).\nconst vec4 W0 = vec4(4096.0, 32768.0, 8.0, 1.0);\nconst vec4 W1 = vec4(262144.0, 2097152.0, 512.0, 64.0);\n\nvec4 fetchRG(ivec2 p, ivec2 maxXY) {\n ivec2 c = clamp(p, ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - c.y) : c.y;\n return texelFetch(uSrc, ivec2(c.x, sy), 0);\n}\n\n// Closed-form accept-if-better refit for one channel off the pass-1\n// moments; returns the final (r0, r1).\nuvec2 refit(float sL, float sLL, float sd, float sLd, uint r0, uint r1) {\n float r0f = float(r0);\n float r1f = float(r1);\n float dir = r1f - r0f;\n float scale = 7.0 / dir;\n // \u03A3\u03C1 = s\xB7\u03A3d \u2212 \u03A3L, \u03A3L\u03C1 = s\xB7\u03A3Ld \u2212 \u03A3L\xB2 (\u03C1 = level-space residual).\n float pR = scale * sd - sL;\n float pLR = scale * sLd - sLL;\n float sBB = sLL * (1.0 / 49.0);\n float sAB = sL * (1.0 / 7.0) - sBB;\n float sAA = 16.0 - 2.0 * sL * (1.0 / 7.0) + sBB;\n float sBR = pLR * dir * (1.0 / 49.0);\n float sAR = (pR - pLR * (1.0 / 7.0)) * dir * (1.0 / 7.0);\n bool spread = 16.0 * sLL != sL * sL;\n float det = sAA * sBB - sAB * sAB;\n float idet = 1.0 / det;\n // Endpoints clamp to [0,255], NOT the block's value range: for a scalar\n // channel, endpoints beyond the data range are often genuinely optimal.\n float q0f = floor(clamp(r0f + (sBB * sAR - sAB * sBR) * idet, 0.0, 255.0) + 0.5);\n float q1f = floor(clamp(r1f + (sAA * sBR - sAB * sAR) * idet, 0.0, 255.0) + 0.5);\n float dd0 = q0f - r0f;\n float dd1 = q1f - r1f;\n float eNew = -2.0 * (dd0 * sAR + dd1 * sBR) + dd0 * dd0 * sAA + 2.0 * dd0 * dd1 * sAB + dd1 * dd1 * sBB;\n bool acc = spread && abs(det) > 1e-3 && q0f > q1f && eNew < 0.0;\n return acc ? uvec2(uint(q0f), uint(q1f)) : uvec2(r0, r1);\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n vec4 vr[4];\n vec4 vg[4];\n for (int q = 0; q < 4; q++) {\n ivec2 qo = base + ivec2((q & 1) * 2, (q >> 1) * 2);\n vec4 cx = fetchRG(qo + ivec2(0, 1), maxXY);\n vec4 cy = fetchRG(qo + ivec2(1, 1), maxXY);\n vec4 cz = fetchRG(qo + ivec2(1, 0), maxXY);\n vec4 cw = fetchRG(qo, maxXY);\n vr[q] = vec4(cx.r, cy.r, cz.r, cw.r) * 255.0;\n vg[q] = vec4(cx.g, cy.g, cz.g, cw.g) * 255.0;\n }\n vec4 mnr = min(min(vr[0], vr[1]), min(vr[2], vr[3]));\n vec4 mxr = max(max(vr[0], vr[1]), max(vr[2], vr[3]));\n vec4 mng = min(min(vg[0], vg[1]), min(vg[2], vg[3]));\n vec4 mxg = max(max(vg[0], vg[1]), max(vg[2], vg[3]));\n vec2 vmin = vec2(min(min(mnr.x, mnr.y), min(mnr.z, mnr.w)), min(min(mng.x, mng.y), min(mng.z, mng.w)));\n vec2 vmax = vec2(max(max(mxr.x, mxr.y), max(mxr.z, mxr.w)), max(max(mxg.x, mxg.y), max(mxg.z, mxg.w)));\n\n // Seed endpoints at the exact per-channel extremes (round-to-nearest).\n // Flat blocks get nudged apart to keep the 6-interp mode (r0 > r1).\n uvec2 r0 = uvec2(clamp(floor(vmax + 0.5), vec2(0.0), vec2(255.0)));\n uvec2 r1 = uvec2(clamp(floor(vmin + 0.5), vec2(0.0), vec2(255.0)));\n if (r0.x == r1.x) { if (r1.x > 0u) { r1.x = r1.x - 1u; } else { r0.x = r0.x + 1u; } }\n if (r0.y == r1.y) { if (r1.y > 0u) { r1.y = r1.y - 1u; } else { r0.y = r0.y + 1u; } }\n\n vec2 r0f = vec2(r0);\n vec2 scale = vec2(7.0) / (vec2(r1) - r0f);\n\n // Pass 1 \u2014 moments only.\n vec2 sL = vec2(0.0);\n vec2 sLL = vec2(0.0);\n vec2 sd = vec2(0.0);\n vec2 sLd = vec2(0.0);\n for (int q = 0; q < 4; q++) {\n vec4 dr = vr[q] - r0f.x;\n vec4 dg = vg[q] - r0f.y;\n vec4 Lr = floor(dr * scale.x + 0.5);\n vec4 Lg = floor(dg * scale.y + 0.5);\n sL += vec2(dot(Lr, vec4(1.0)), dot(Lg, vec4(1.0)));\n sLL += vec2(dot(Lr, Lr), dot(Lg, Lg));\n sd += vec2(dot(dr, vec4(1.0)), dot(dg, vec4(1.0)));\n sLd += vec2(dot(Lr, dr), dot(Lg, dg));\n }\n\n uvec2 fr = refit(sL.x, sLL.x, sd.x, sLd.x, r0.x, r1.x);\n uvec2 fg = refit(sL.y, sLL.y, sd.y, sLd.y, r0.y, r1.y);\n uvec2 n0 = uvec2(fr.x, fg.x);\n uvec2 n1 = uvec2(fr.y, fg.y);\n\n // Pass 2 \u2014 levels against the FINAL endpoints, packed as \u03A3 L\xB78^k:\n // iA = pixels 0..7, iB = pixels 8..15.\n vec2 n0f = vec2(n0);\n vec2 sc2 = vec2(7.0) / (vec2(n1) - n0f);\n vec4 Lr[4];\n vec4 Lg[4];\n for (int q = 0; q < 4; q++) {\n Lr[q] = clamp(floor((vr[q] - n0f.x) * sc2.x + 0.5), vec4(0.0), vec4(7.0));\n Lg[q] = clamp(floor((vg[q] - n0f.y) * sc2.y + 0.5), vec4(0.0), vec4(7.0));\n }\n uint iAx = lvlToIdx(uint(dot(Lr[0], W0) + dot(Lr[1], W1)));\n uint iBx = lvlToIdx(uint(dot(Lr[2], W0) + dot(Lr[3], W1)));\n uint iAy = lvlToIdx(uint(dot(Lg[0], W0) + dot(Lg[1], W1)));\n uint iBy = lvlToIdx(uint(dot(Lg[2], W0) + dot(Lg[3], W1)));\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n outColor = uvec4(\n n0.x | (n1.x << 8u) | (iAx << 16u),\n (iAx >> 16u) | (iBx << 8u),\n n0.y | (n1.y << 8u) | (iAy << 16u),\n (iAy >> 16u) | (iBy << 8u)\n );\n}\n";
|
|
2115
2190
|
|
|
2116
2191
|
// src/webgl/BC5WebGLEncoder.ts
|
|
2117
2192
|
var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -2130,7 +2205,7 @@ var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
2130
2205
|
};
|
|
2131
2206
|
|
|
2132
2207
|
// src/webgl/glsl/bc7.frag.glsl
|
|
2133
|
-
var bc7_frag_default = "#version 300 es\n// BC7 (BPTC) mode-6 fragment-shader encoder \u2014 WebGL2 port of bc7.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Fast path\n// only: principal-axis seed (covariance power-iteration; bbox on degenerate\n// blocks) at the exact projection extents, quantised directly (no LSQ refit \u2014\n// mode 6's 16-level palette leaves it under 0.15 dB) \u2192 one projection-based\n// index-assignment pass (palette is colinear, so the nearest entry is found\n// by projecting onto the endpoint line \u2014 O(1) per pixel). Same algorithm as\n// bc7.wgsl; see that file for the mode-6 bit layout and rationale.\n//\n// Determinism note: the WGSL refit uses round() (half-to-even); here we use\n// floor(x + 0.5) for portability. The two differ only at exact .5 ties, a\n// sub-LSB endpoint nudge that is visually identical.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// Per-invocation scratch (mirrors the WGSL function-scope arrays passed by ptr).\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct QuantPair { ivec4 seven; ivec4 eight; };\nstruct Ep { ivec4 seven; ivec4 eight; uint p; };\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\nint dist2(ivec4 a, ivec4 b) {\n ivec4 d = a - b;\n ivec4 e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit) under\n// a fixed p-bit, all four channels at once.\nQuantPair quantizeEndpoint(ivec4 ideal8, uint p) {\n ivec4 q = ivec4(clamp(floor((vec4(ideal8) - float(p)) / 2.0 + 0.5), vec4(0.0), vec4(127.0)));\n // eff = (q << 1) | p. q*2 is even and p \u2208 {0,1}, so q*2 + p is identical and\n // avoids any vector-shift-by-scalar portability question.\n ivec4 eff = q * 2 + ivec4(int(p));\n return QuantPair(q, eff);\n}\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nEp pickEp(ivec4 ideal) {\n QuantPair a = quantizeEndpoint(ideal, 0u);\n QuantPair b = quantizeEndpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) {\n return Ep(b.seven, b.eight, 1u);\n }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0.0)\n// for a degenerate (constant) block. The bbox diagonal alone is sign-blind\n// and points across anti-correlated data (normal maps, hue edges) instead of\n// along it.\nvec4 principalAxis(vec4 c0v, vec4 c1v, vec4 c2v, vec4 c3v, vec4 seed) {\n vec4 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec4(0.0); }\n v /= len;\n for (int it = 0; it < 8; it++) {\n vec4 nv = vec4(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// Projection index assignment over gPixels \u2192 gIdx. When `fit`, accumulate the\n// LSQ normal-equation sums in the same pass and return refitted endpoints.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 15.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 15.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 15.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n // Load pass with the covariance moments FUSED in: d = px \u2212 pixel0\n // (first-pixel-relative, so the sums scale with the block's span).\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n vec4 p0f = vec4(0.0);\n vec4 sd = vec4(0.0);\n vec4 c0v = vec4(0.0);\n vec4 c1v = vec4(0.0);\n vec4 c2v = vec4(0.0);\n vec4 c3v = vec4(0.0);\n int gd = 0;\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n if (i == 0) { p0f = vec4(px); }\n vec4 d = vec4(px) - p0f;\n sd += d;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n c3v += d.w * d;\n }\n vec4 mean = p0f + sd / 16.0;\n // Mean-correct the fused moments: C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16.\n vec4 sd16 = sd / 16.0;\n c0v -= sd.x * sd16;\n c1v -= sd.y * sd16;\n c2v -= sd.z * sd16;\n c3v -= sd.w * sd16;\n\n ivec4 seed0 = lo;\n ivec4 seed1 = hi;\n // Gray + opaque blocks: axis is analytically (1,1,1,0)/\u221A3 with extents\n // at the luma min/max \u2014 skip iteration + extents (see bc7_fast_f16.wgsl).\n if (lo.w == 255 && gd == 0) {\n seed0 = ivec4(lo.x, lo.x, lo.x, 255);\n seed1 = ivec4(hi.x, hi.x, hi.x, 255);\n } else {\n vec4 axis = principalAxis(c0v, c1v, c2v, c3v, vec4(hi - lo));\n if (dot(axis, axis) > 0.0) {\n // Exact projection extents along the axis. (A Rayleigh-quotient span\n // estimate was tried in place of this pass \u2014 it saves 16 dots but costs\n // 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3 misjudges\n // two-cluster and outlier blocks. The pass stays.)\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec4(gPixels[k]) - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n seed0 = ivec4(clamp(floor(mean + tMin * axis + 0.5), vec4(0.0), vec4(255.0)));\n seed1 = ivec4(clamp(floor(mean + tMax * axis + 0.5), vec4(0.0), vec4(255.0)));\n }\n }\n\n // Quantise the PCA-extents seed directly and assign indices in one\n // projection pass \u2014 no LSQ refit: with the seed already on the principal\n // axis, mode 6's 16-level palette leaves the refit under 0.15 dB (the\n // coarse 4-level BC1/ASTC fast paths DO keep theirs).\n Ep ep0 = pickEp(seed0);\n Ep ep1 = pickEp(seed1);\n projAssign(ep0.eight, ep1.eight, false);\n ivec4 e0_7 = ep0.seven;\n ivec4 e1_7 = ep1.seven;\n uint p0 = ep0.p;\n uint p1 = ep1.p;\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0; otherwise swap endpoints and\n // reflect every index (decoded image unchanged).\n if ((gIdx[0] & 0x8u) != 0u) {\n ivec4 t = e0_7; e0_7 = e1_7; e1_7 = t;\n uint tp = p0; p0 = p1; p1 = tp;\n for (int k = 0; k < 16; k++) { gIdx[k] = 15u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n uint pos = 0u;\n writeBits(block, pos, 7u, 0x40u); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.w)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.w)); pos += 7u;\n writeBits(block, pos, 1u, p0); pos += 1u;\n writeBits(block, pos, 1u, p1); pos += 1u;\n writeBits(block, pos, 3u, gIdx[0] & 0x7u); pos += 3u;\n for (int k = 1; k < 16; k++) {\n writeBits(block, pos, 4u, gIdx[k] & 0xFu);\n pos += 4u;\n }\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
|
|
2208
|
+
var bc7_frag_default = "#version 300 es\n// BC7 (BPTC) mode-6 fragment-shader encoder \u2014 WebGL2 port of bc7.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Fast path\n// only: principal-axis seed (covariance power-iteration; bbox on degenerate\n// blocks) at the exact projection extents, quantised directly (no LSQ refit \u2014\n// mode 6's 16-level palette leaves it under 0.15 dB) \u2192 one projection-based\n// index-assignment pass (palette is colinear, so the nearest entry is found\n// by projecting onto the endpoint line \u2014 O(1) per pixel). Gray + opaque\n// blocks take an integer 1-D tail: lossless for spans \u2264 15 with odd\n// endpoints (alpha exactly 255), alpha-aware scalar LSQ refit above. Same\n// algorithm as bc7.wgsl; see that file (and bc7_fast_f16.wgsl) for the\n// mode-6 bit layout and rationale.\n//\n// Same arithmetic as bc7.wgsl (roundEven == WGSL round()), so the two\n// produce the same blocks up to driver float-contraction differences.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// Per-invocation scratch (mirrors the WGSL function-scope array).\nivec4 gPixels[16];\n\nstruct QuantPair { ivec4 seven; ivec4 eight; };\nstruct Ep { ivec4 seven; ivec4 eight; uint p; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\nint dist2(ivec4 a, ivec4 b) {\n ivec4 d = a - b;\n ivec4 e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit) under\n// a fixed p-bit, all four channels at once.\nQuantPair quantizeEndpoint(ivec4 ideal8, uint p) {\n ivec4 q = ivec4(clamp(floor((vec4(ideal8) - float(p)) / 2.0 + 0.5), vec4(0.0), vec4(127.0)));\n // eff = (q << 1) | p. q*2 is even and p \u2208 {0,1}, so q*2 + p is identical and\n // avoids any vector-shift-by-scalar portability question.\n ivec4 eff = q * 2 + ivec4(int(p));\n return QuantPair(q, eff);\n}\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nEp pickEp(ivec4 ideal) {\n QuantPair a = quantizeEndpoint(ideal, 0u);\n QuantPair b = quantizeEndpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) {\n return Ep(b.seven, b.eight, 1u);\n }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0.0)\n// for a degenerate (constant) block. The bbox diagonal alone is sign-blind\n// and points across anti-correlated data (normal maps, hue edges) instead of\n// along it.\nvec4 principalAxis(vec4 c0v, vec4 c1v, vec4 c2v, vec4 c3v, vec4 seed) {\n vec4 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec4(0.0); }\n v /= len;\n for (int it = 0; it < 8; it++) {\n vec4 nv = vec4(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// Gray + opaque block (every texel R == G == B, A == 255) from gPixels[].x,\n// packed straight into the 4 block words. 8-bit endpoints E = 2q + p; RGB\n// share q, alpha is 254 + p.\nuvec4 encodeGray(float vmin, float vmax) {\n float e0 = vmin;\n float e1 = vmax;\n if (vmax - vmin <= 15.0) {\n // LOSSLESS: integer endpoints \u2264 15 apart cover every integer between\n // them and round(15\xB7(v \u2212 e0)/d) selects it; even endpoints step outward\n // while the span stays \u2264 15 so both p-bits are 1 (alpha 255).\n if (fract(e0 * 0.5) == 0.0 && e0 > 0.0 && e1 - e0 < 15.0) { e0 -= 1.0; }\n if (fract(e1 * 0.5) == 0.0 && e1 < 255.0 && e1 - e0 < 15.0) { e1 += 1.0; }\n } else {\n // Moment-form scalar LSQ refit on the seed levels, all four p-bit\n // combinations priced including the alpha term, accept-if-better.\n float k1 = 15.0 / (vmax - vmin);\n float k0 = 0.5 - vmin * k1;\n float sL = 0.0;\n float sLL = 0.0;\n float sv = 0.0;\n float sLv = 0.0;\n for (int k = 0; k < 16; k++) {\n float v = float(gPixels[k].x);\n float L = floor(v * k1 + k0);\n sL += L;\n sLL += L * L;\n sv += v;\n sLv += L * v;\n }\n float C = sLL * (1.0 / 225.0);\n float B = sL * (1.0 / 15.0) - C;\n float A = 16.0 - sL * (2.0 / 15.0) + C;\n float Y = sLv * (1.0 / 15.0);\n float X = sv - Y;\n float det = A * C - B * B;\n if (det > 1e-3) {\n float s0 = clamp((C * X - B * Y) / det, 0.0, 255.0);\n float s1 = clamp((A * Y - B * X) / det, 0.0, 255.0);\n // price(e0, e1, p0, p1) up to the block constant: RGB \xD73 + alpha.\n float ps0 = vmin - 2.0 * floor(vmin * 0.5);\n float ps1 = vmax - 2.0 * floor(vmax * 0.5);\n float best = 3.0 * (A * vmin * vmin + 2.0 * B * vmin * vmax + C * vmax * vmax - 2.0 * (X * vmin + Y * vmax))\n + A * (1.0 - ps0) + 2.0 * B * (1.0 - ps0) * (1.0 - ps1) + C * (1.0 - ps1);\n for (int pc = 0; pc < 4; pc++) {\n float p0 = float(pc & 1);\n float p1 = float(pc >> 1);\n float c0 = 2.0 * clamp(floor((s0 - p0) * 0.5 + 0.5), 0.0, 127.0) + p0;\n float c1 = 2.0 * clamp(floor((s1 - p1) * 0.5 + 0.5), 0.0, 127.0) + p1;\n float pr = 3.0 * (A * c0 * c0 + 2.0 * B * c0 * c1 + C * c1 * c1 - 2.0 * (X * c0 + Y * c1))\n + A * (1.0 - p0) + 2.0 * B * (1.0 - p0) * (1.0 - p1) + C * (1.0 - p1);\n if (pr < best) {\n best = pr;\n e0 = c0;\n e1 = c1;\n }\n }\n }\n }\n uint glo = 0u;\n uint ghi = 0u;\n if (e1 != e0) {\n float k1 = 15.0 / (e1 - e0);\n float k0 = 0.5 - e0 * k1;\n for (int k = 0; k < 8; k++) {\n float sg = clamp(floor(float(gPixels[k].x) * k1 + k0), 0.0, 15.0);\n glo |= uint(sg) << uint(k * 4);\n }\n for (int k = 8; k < 16; k++) {\n float sg = clamp(floor(float(gPixels[k].x) * k1 + k0), 0.0, 15.0);\n ghi |= uint(sg) << uint((k - 8) * 4);\n }\n }\n uint u0 = uint(e0);\n uint u1 = uint(e1);\n // Anchor rule: pixel 0's index MSB must be 0 \u2014 swap + reflect (bitwise NOT).\n if ((glo & 0x8u) != 0u) {\n uint t = u0; u0 = u1; u1 = t;\n glo = ~glo; ghi = ~ghi;\n }\n uint q0 = u0 >> 1u;\n uint q1 = u1 >> 1u;\n return uvec4(\n 0x40u | (q0 << 7u) | (q1 << 14u) | (q0 << 21u) | (q1 << 28u),\n (q1 >> 4u) | (q0 << 3u) | (q1 << 10u) | (127u << 17u) | (127u << 24u) | ((u0 & 1u) << 31u),\n (u1 & 1u) | ((glo & 0x7u) << 1u) | (glo & 0xFFFFFFF0u),\n ghi\n );\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n // Load pass: texels, bbox and the gray test.\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n int gd = 0;\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n\n // Gray + opaque blocks: 1-D, own tail (see bc7_fast_f16.wgsl) \u2014 lossless\n // for spans \u2264 15 with odd endpoints (alpha exactly 255), closed-form\n // scalar LSQ refit with alpha-aware p-bit pricing above that.\n if (lo.w == 255 && gd == 0) {\n outColor = encodeGray(float(lo.x), float(hi.x));\n return;\n }\n\n // Covariance moments: d = px \u2212 pixel0 (first-pixel-relative, so the sums\n // scale with the block's span).\n vec4 p0f = vec4(gPixels[0]);\n vec4 sd = vec4(0.0);\n vec4 c0v = vec4(0.0);\n vec4 c1v = vec4(0.0);\n vec4 c2v = vec4(0.0);\n vec4 c3v = vec4(0.0);\n for (int i = 1; i < 16; i++) {\n vec4 d = vec4(gPixels[i]) - p0f;\n sd += d;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n c3v += d.w * d;\n }\n vec4 mean = p0f + sd / 16.0;\n // Mean-correct the moments: C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16.\n vec4 sd16 = sd / 16.0;\n c0v -= sd.x * sd16;\n c1v -= sd.y * sd16;\n c2v -= sd.z * sd16;\n c3v -= sd.w * sd16;\n\n ivec4 seed0 = lo;\n ivec4 seed1 = hi;\n {\n vec4 axis = principalAxis(c0v, c1v, c2v, c3v, vec4(hi - lo));\n if (dot(axis, axis) > 0.0) {\n // Exact projection extents along the axis. (A Rayleigh-quotient span\n // estimate was tried in place of this pass \u2014 it saves 16 dots but costs\n // 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3 misjudges\n // two-cluster and outlier blocks. The pass stays.)\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec4(gPixels[k]) - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n seed0 = ivec4(clamp(roundEven(mean + tMin * axis), vec4(0.0), vec4(255.0)));\n seed1 = ivec4(clamp(roundEven(mean + tMax * axis), vec4(0.0), vec4(255.0)));\n }\n }\n\n // Quantise the PCA-extents seed directly and assign indices in one\n // projection pass \u2014 no LSQ refit: with the seed already on the principal\n // axis, mode 6's 16-level palette leaves the refit under 0.15 dB (the\n // coarse 4-level BC1/ASTC paths DO keep theirs). The 16 4-bit indices pack\n // on the fly into two nibble words (pixel k \u2192 bits 4k..4k+3).\n uint ilo = 0u;\n uint ihi = 0u;\n Ep ep0 = pickEp(seed0);\n Ep ep1 = pickEp(seed1);\n vec4 dir = vec4(ep1.eight - ep0.eight);\n float dd = dot(dir, dir);\n if (dd > 0.0) {\n vec4 e0f = vec4(ep0.eight);\n float inv = 15.0 / dd;\n for (int k = 0; k < 8; k++) {\n float sk = clamp(floor(dot(vec4(gPixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ilo |= uint(sk) << uint(k * 4);\n }\n for (int k = 8; k < 16; k++) {\n float sk = clamp(floor(dot(vec4(gPixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ihi |= uint(sk) << uint((k - 8) * 4);\n }\n }\n ivec4 e0_7 = ep0.seven;\n ivec4 e1_7 = ep1.seven;\n uint p0 = ep0.p;\n uint p1 = ep1.p;\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0. Swapping endpoints reflects\n // every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.\n if ((ilo & 0x8u) != 0u) {\n ivec4 t = e0_7; e0_7 = e1_7; e1_7 = t;\n uint tp = p0; p0 = p1; p1 = tp;\n ilo = ~ilo; ihi = ~ihi;\n }\n\n // Straight-line mode-6 packing (a generic bit writer's dynamic word\n // indexing keeps the output array out of registers).\n uvec4 e0 = uvec4(e0_7);\n uvec4 e1 = uvec4(e1_7);\n outColor = uvec4(\n 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u),\n (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (p0 << 31u),\n p1 | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u),\n ihi\n );\n}\n";
|
|
2134
2209
|
|
|
2135
2210
|
// src/webgl/BC7WebGLEncoder.ts
|
|
2136
2211
|
var BC7WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -2149,7 +2224,7 @@ var BC7WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
2149
2224
|
};
|
|
2150
2225
|
|
|
2151
2226
|
// src/webgl/glsl/astc4x4.frag.glsl
|
|
2152
|
-
var astc4x4_frag_default = "#version 300 es\n// ASTC 4\xD74 LDR fragment-shader encoder \u2014 WebGL2 port of astc4x4.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor.\n// Restricted subset: single partition, no dual-plane, 8-bit endpoints, with\n// the block class picked per block from the content (see astc4x4_ref.ts for\n// layouts and block-mode derivations):\n// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, mode 0x253\n// opaque \u2192 CEM 8 (RGB), 3-bit weights, mode 0x053\n// translucent \u2192 CEM 12 (RGBA), 2-bit weights, mode 0x042\n// Colour paths: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) \u2192 one LSQ refit fused into a projection weight\n// assignment. Mirrors astc4x4.wgsl.\n//\n// Determinism note: floor(x + 0.5) replaces WGSL round() for the refit endpoints\n// (sub-LSB difference at exact .5 ties only).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\n// GLSL ES 3.00 has no bitfieldReverse; classic 5-step swap. Weight-stream\n// bit q lives at block bit 127 \u2212 q, so a stream word assembled LSB-first\n// maps onto a block word with one reversal (see astc4x4.wgsl).\nuint rev32(uint x) {\n uint v = x;\n v = ((v & 0x55555555u) << 1) | ((v >> 1) & 0x55555555u);\n v = ((v & 0x33333333u) << 2) | ((v >> 2) & 0x33333333u);\n v = ((v & 0x0F0F0F0Fu) << 4) | ((v >> 4) & 0x0F0F0F0Fu);\n v = ((v & 0x00FF00FFu) << 8) | ((v >> 8) & 0x00FF00FFu);\n return (v << 16) | (v >> 16);\n}\n\n// Principal colour axis of gPixels via covariance power-iteration, seeded\n// with the bbox diagonal. Returns a unit axis, or vec4(0.0) for a degenerate\n// (constant) block. The bbox diagonal alone is sign-blind and points across\n// anti-correlated data (normal maps, hue edges) instead of along it.\nvec4 principalAxis(vec4 mean, vec4 seed, int iters) {\n vec4 c0v = vec4(0.0);\n vec4 c1v = vec4(0.0);\n vec4 c2v = vec4(0.0);\n vec4 c3v = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n vec4 d = vec4(gPixels[k]) - mean;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n c3v += d.w * d;\n }\n vec4 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec4(0.0); }\n v /= len;\n for (int it = 0; it < iters; it++) {\n vec4 nv = vec4(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// Projection weight assignment over lmax + 1 colinear levels, with the LSQ\n// normal-equation sums accumulated in the same pass for a fused refit.\nFit projAssign(ivec4 pe0, ivec4 pe1, float lmax, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = lmax / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, lmax);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / lmax;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n ivec4 isum = ivec4(0);\n int gd = 0; // max |R\u2212G|, |R\u2212B| over the block; 0 \u21D4 exactly grayscale\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n isum += px;\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n bool opaque = lo.w == 255;\n\n uint w0 = 0u; uint w1 = 0u; uint w2 = 0u; uint w3 = 0u;\n\n if (opaque && gd == 0) {\n // ---------------- Luminance path: CEM 0, 5-bit weights ----------------\n // Endpoints at the exact extremes; 32 palette levels make an LSQ refit\n // unnecessary.\n uint L0 = uint(lo.x);\n uint L1 = uint(hi.x);\n uint s0 = 0u; uint s1 = 0u; uint s2 = 0u;\n if (L1 > L0) {\n float sc = 64.0 / float(hi.x - lo.x);\n // Exact nearest entry of the QUANT_32 grid: unq = 2w for w \u2264 15,\n // 2w + 2 for w \u2265 16 (4-wide gap at the middle, so uniform rounding\n // is wrong there). Best candidate of each half, keep the closer.\n for (int k = 0; k < 16; k++) {\n float u = clamp(float(gPixels[k].x - lo.x) * sc, 0.0, 64.0);\n float wlo = clamp(floor(u * 0.5 + 0.5), 0.0, 15.0);\n float whi = clamp(floor((u - 2.0) * 0.5 + 0.5), 16.0, 31.0);\n bool pick = abs(u - wlo * 2.0) <= abs(u - (whi * 2.0 + 2.0));\n uint w = uint(pick ? wlo : whi);\n // Stream bit q = 5k + j; straddles handled with constant shifts.\n uint off = 5u * uint(k);\n if (off < 28u) { s0 |= (w << off); }\n else if (off == 30u) { s0 |= (w << 30u); s1 |= (w >> 2u); }\n else if (off < 60u) { s1 |= (w << (off - 32u)); }\n else if (off == 60u) { s1 |= (w << 28u); s2 |= (w >> 4u); }\n else { s2 |= (w << (off - 64u)); }\n }\n }\n // Mode 0x253, partitions\u22121 = 0, CEM 0, L0 @17, L1 @25 (top bit spills\n // into word 1 bit 0); stream words map onto block words via rev32.\n w0 = 0x253u | (L0 << 17u) | (L1 << 25u);\n w1 = (L1 >> 7u) | rev32(s2);\n w2 = rev32(s1);\n w3 = rev32(s0);\n } else {\n // ------------- Colour paths: shared PCA seed ---------------------------\n vec4 mean = vec4(isum) / 16.0;\n float lmax = opaque ? 7.0 : 3.0;\n uint wmax = opaque ? 7u : 3u;\n\n ivec4 e0 = lo;\n ivec4 e1 = hi;\n // 8 iterations for opaque blocks, 4 for translucent (their refit\n // absorbs residual axis error \u2014 see astc4x4_fast_f16.wgsl).\n vec4 axis = principalAxis(mean, vec4(hi - lo), opaque ? 8 : 4);\n if (dot(axis, axis) > 0.0) {\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec4(gPixels[k]) - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n e0 = ivec4(clamp(floor(mean + tMin * axis + 0.5), vec4(0.0), vec4(255.0)));\n e1 = ivec4(clamp(floor(mean + tMax * axis + 0.5), vec4(0.0), vec4(255.0)));\n }\n if (opaque) {\n // CEM 8 ships the quantised PCA extents directly (no LSQ fit \u2014 see\n // astc4x4_fast_f16.wgsl for the measured trade), bbox-clamped like\n // the fit output; one assignment pass fills gIdx for the packer.\n e0 = clamp(e0, lo, hi);\n e1 = clamp(e1, lo, hi);\n projAssign(e0, e1, lmax, false);\n } else {\n Fit r = projAssign(e0, e1, lmax, true);\n if (r.valid) {\n // Clamp the refit to the block bbox: on multi-cluster blocks the\n // unconstrained LSQ solve extrapolates far outside the block's\n // colours and the per-channel [0,255] clamp then bends the hue \u2014\n // fringe pixels decode to colours that exist nowhere in the block.\n // gIdx keeps the fit-pass weights (assigned against the seed line)\n // rather than reassigning against the refit endpoints \u2014 see\n // astc4x4_fast_f16.wgsl for the measured trade.\n e0 = clamp(r.e0, lo, hi);\n e1 = clamp(r.e1, lo, hi);\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 ivec4 t = e0; e0 = e1; e1 = t;\n for (int k = 0; k < 16; k++) { gIdx[k] = wmax - gIdx[k]; }\n }\n\n if (opaque) {\n // CEM 8: 3-bit weights, stream bit q = 3k.\n uint s0 = 0u; uint s1 = 0u;\n for (int k = 0; k < 16; k++) {\n uint w = gIdx[k];\n uint off = 3u * uint(k);\n if (off < 30u) { s0 |= (w << off); }\n else if (off == 30u) { s0 |= (w << 30u); s1 |= (w >> 2u); }\n else { s1 |= (w << (off - 32u)); }\n }\n // Mode 0x053, CEM 8 @13, endpoints R0 R1 G0 G1 B0 B1 from bit 17.\n w0 = 0x053u | (8u << 13u) | (uint(e0.x) << 17u) | (uint(e1.x) << 25u);\n w1 = (uint(e1.x) >> 7u) | (uint(e0.y) << 1u) | (uint(e1.y) << 9u)\n | (uint(e0.z) << 17u) | (uint(e1.z) << 25u);\n w2 = (uint(e1.z) >> 7u) | rev32(s1);\n w3 = rev32(s0);\n } else {\n // CEM 12: 2-bit weights, stream bit q = 2k (single stream word).\n uint s0 = 0u;\n for (int k = 0; k < 16; k++) {\n s0 |= (gIdx[k] << (2u * uint(k)));\n }\n // Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.\n w0 = 0x042u | (12u << 13u) | (uint(e0.x) << 17u) | (uint(e1.x) << 25u);\n w1 = (uint(e1.x) >> 7u) | (uint(e0.y) << 1u) | (uint(e1.y) << 9u)\n | (uint(e0.z) << 17u) | (uint(e1.z) << 25u);\n w2 = (uint(e1.z) >> 7u) | (uint(e0.w) << 1u) | (uint(e1.w) << 9u);\n w3 = rev32(s0);\n }\n }\n\n outColor = uvec4(w0, w1, w2, w3);\n}\n";
|
|
2227
|
+
var astc4x4_frag_default = "#version 300 es\n// ASTC 4\xD74 LDR fragment-shader encoder \u2014 WebGL2 port of astc4x4.wgsl.\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor.\n// Restricted subset: single partition, no dual-plane, with the block class\n// picked per block from the content (see astc4x4_ref.ts for layouts, block\n// modes, the QUANT_192 trit ISE and the weight-stream bit order):\n// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, mode 0x253\n// opaque \u2192 CEM 8 (RGB), 3-channel PCA extents, two bit budgets:\n// span > 12 \u2192 QUANT_192 endpoints (trit ISE) + 4-bit\n// weights, mode 0x242; span \u2264 12 \u2192 8-bit endpoints +\n// 3-bit weights, mode 0x053\n// translucent \u2192 CEM 12 (RGBA), 2-bit weights, mode 0x042 \u2014 PCA seed \u2192\n// fused projection + least-squares refit, the fit pass's\n// weights shipped\n// Same algorithm and arithmetic as astc4x4.wgsl (roundEven == WGSL round());\n// see astc4x4_fast_f16.wgsl for the design measurements.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\nivec4 gPixels[16];\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\n// GLSL ES 3.00 has no bitfieldReverse; classic 5-step swap. Weight-stream\n// bit q lives at block bit 127 \u2212 q, so a stream word assembled LSB-first\n// maps onto a block word with one reversal (see astc4x4.wgsl).\nuint rev32(uint x) {\n uint v = x;\n v = ((v & 0x55555555u) << 1) | ((v >> 1) & 0x55555555u);\n v = ((v & 0x33333333u) << 2) | ((v >> 2) & 0x33333333u);\n v = ((v & 0x0F0F0F0Fu) << 4) | ((v >> 4) & 0x0F0F0F0Fu);\n v = ((v & 0x00FF00FFu) << 8) | ((v >> 8) & 0x00FF00FFu);\n return (v << 16) | (v >> 16);\n}\n\n// Principal colour axis via covariance power-iteration (RGBA, 8-bit integer\n// pixel domain), seeded with the bbox diagonal. Returns a unit axis, or\n// vec4(0.0) for a degenerate (constant) block. The bbox diagonal alone is\n// sign-blind and points across anti-correlated data (normal maps, hue\n// edges) instead of along it.\nvec4 principalAxis4(vec4 mean, vec4 seed, int iters) {\n vec4 c0v = vec4(0.0);\n vec4 c1v = vec4(0.0);\n vec4 c2v = vec4(0.0);\n vec4 c3v = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n vec4 d = vec4(gPixels[k]) - mean;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n c3v += d.w * d;\n }\n vec4 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec4(0.0); }\n v = v / len;\n for (int it = 0; it < iters; it++) {\n vec4 nv = vec4(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// Principal RGB axis for opaque blocks (alpha is constant there, so the\n// 4th covariance lane is dead weight). Same iteration as principalAxis4.\nvec3 principalAxis3(vec3 mean, vec3 seed) {\n vec3 c0v = vec3(0.0);\n vec3 c1v = vec3(0.0);\n vec3 c2v = vec3(0.0);\n for (int k = 0; k < 16; k++) {\n vec3 d = vec3(gPixels[k].xyz) - mean;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n }\n vec3 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec3(0.0); }\n v = v / len;\n for (int it = 0; it < 8; it++) {\n vec3 nv = vec3(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// One pass over the block: project every pixel onto the e0\u2192e1 line (4\n// colinear levels, so the nearest entry is the rounded projection), pack\n// the 2-bit weights, and solve the least-squares refit from the\n// normal-equation sums accumulated in the same pass.\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; uint wstream; };\nFit projFit(ivec4 e0, ivec4 e1) {\n Fit r;\n r.e0 = ivec4(0);\n r.e1 = ivec4(0);\n r.valid = false;\n r.wstream = 0u;\n vec4 dir = vec4(e1 - e0);\n float dd = dot(dir, dir);\n if (dd == 0.0) { return r; }\n vec4 e0f = vec4(e0);\n float inv = 3.0 / dd;\n float sAA = 0.0;\n float sBB = 0.0;\n float sAB = 0.0;\n vec4 sAV = vec4(0.0);\n vec4 sBV = vec4(0.0);\n float sMin = 3.0;\n float sMax = 0.0;\n for (int k = 0; k < 16; k++) {\n vec4 v = vec4(gPixels[k]);\n float s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 3.0);\n r.wstream |= uint(s) << (2u * uint(k));\n sMin = min(sMin, s);\n sMax = max(sMax, s);\n float b = s * (1.0 / 3.0);\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b;\n sAV += a * v; sBV += b * v;\n }\n // Rank-1 guard: one level \u21D2 singular system (the solve would be noise).\n if (sMin == sMax) { return r; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { return r; }\n r.e0 = ivec4(clamp(roundEven((sBB * sAV - sAB * sBV) / det), vec4(0.0), vec4(255.0)));\n r.e1 = ivec4(clamp(roundEven((sAA * sBV - sAB * sAV) / det), vec4(0.0), vec4(255.0)));\n r.valid = true;\n return r;\n}\n\n// ISE trit-block encoder: 5 trits \u2192 the 8-bit T field (the inverse of the\n// spec's trit-block decode; see astc4x4_ref.ts).\nuint tritEnc(uint t0, uint t1, uint t2, uint t3, uint t4) {\n uint c = (t2 == 2u && t1 == 2u) ? (12u | t0)\n : (t2 == 2u ? ((t1 << 4u) | (t0 << 2u) | 3u) : ((t2 << 4u) | (t1 << 2u) | t0));\n return (t3 == 2u && t4 == 2u) ? (((c >> 2u) << 5u) | 28u | (c & 3u))\n : (t4 == 2u ? ((t3 << 7u) | 96u | c) : ((t4 << 7u) | (t3 << 5u) | c));\n}\n\n// Nearest QUANT_192 endpoint to x \u2208 [0,255]; returns (ISE value =\n// trit\xB764 + bits, unquantised level). See astc4x4_fast_f16.wgsl.\nuvec2 q192(float x) {\n uint v = uint(clamp(floor(x + 0.5), 0.0, 255.0));\n bool up = v > 127u;\n uint u = up ? 255u - v : v;\n if ((u & 3u) == 3u) {\n float xu = up ? 255.0 - x : x;\n u = (xu > float(u) && u < 127u) ? u + 1u : u - 1u;\n }\n return uvec2(((u & 3u) << 6u) | ((u >> 2u) << 1u) | (up ? 1u : 0u), up ? 255u - u : u);\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n ivec4 isum = ivec4(0);\n int gd = 0; // max |R\u2212G|, |R\u2212B| over the block; 0 \u21D4 exactly grayscale\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n isum += px;\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n bool opaque = lo.w == 255;\n\n uint w0 = 0u; uint w1 = 0u; uint w2 = 0u; uint w3 = 0u;\n\n if (opaque && gd == 0) {\n // ---------------- Luminance path: CEM 0, 5-bit weights ----------------\n // Endpoints at the exact extremes; 32 palette levels make an LSQ refit\n // unnecessary.\n uint L0 = uint(lo.x);\n uint L1 = uint(hi.x);\n uint s0 = 0u; uint s1 = 0u; uint s2 = 0u;\n if (L1 > L0) {\n float sc = 64.0 / float(hi.x - lo.x);\n // Exact nearest entry of the QUANT_32 grid: unq = 2w for w \u2264 15,\n // 2w + 2 for w \u2265 16 (4-wide gap at the middle, so uniform rounding\n // is wrong there). Best candidate of each half, keep the closer.\n for (int k = 0; k < 16; k++) {\n float u = clamp(float(gPixels[k].x - lo.x) * sc, 0.0, 64.0);\n float wlo = clamp(floor(u * 0.5 + 0.5), 0.0, 15.0);\n float whi = clamp(floor((u - 2.0) * 0.5 + 0.5), 16.0, 31.0);\n bool pick = abs(u - wlo * 2.0) <= abs(u - (whi * 2.0 + 2.0));\n uint w = uint(pick ? wlo : whi);\n // Stream bit q = 5k + j; straddles handled with constant shifts.\n uint off = 5u * uint(k);\n if (off < 28u) { s0 |= (w << off); }\n else if (off == 30u) { s0 |= (w << 30u); s1 |= (w >> 2u); }\n else if (off < 60u) { s1 |= (w << (off - 32u)); }\n else if (off == 60u) { s1 |= (w << 28u); s2 |= (w >> 4u); }\n else { s2 |= (w << (off - 64u)); }\n }\n }\n // Mode 0x253, partitions\u22121 = 0, CEM 0, L0 @17, L1 @25 (top bit spills\n // into word 1 bit 0); stream words map onto block words via rev32.\n w0 = 0x253u | (L0 << 17u) | (L1 << 25u);\n w1 = (L1 >> 7u) | rev32(s2);\n w2 = rev32(s1);\n w3 = rev32(s0);\n } else if (opaque) {\n // ------------- Opaque colour: CEM 8, two bit budgets -------------------\n // span > 12 \u2192 QUANT_192 endpoints + 4-bit weights (mode 0x242), else\n // exact 8-bit endpoints + 3-bit weights (mode 0x053); endpoints are the\n // bbox-clamped PCA extents.\n vec3 mean3 = vec3(isum.xyz) * (1.0 / 16.0);\n vec3 lo3 = vec3(lo.xyz);\n vec3 hi3 = vec3(hi.xyz);\n vec3 x0 = lo3;\n vec3 x1 = hi3;\n vec3 axis = principalAxis3(mean3, hi3 - lo3);\n if (dot(axis, axis) > 0.0) {\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec3(gPixels[k].xyz) - mean3, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n x0 = clamp(mean3 + tMin * axis, lo3, hi3);\n x1 = clamp(mean3 + tMax * axis, lo3, hi3);\n }\n ivec3 span3 = hi.xyz - lo.xyz;\n bool small = max(max(span3.x, span3.y), span3.z) <= 12;\n uvec2 r0; uvec2 g0; uvec2 b0;\n uvec2 r1; uvec2 g1; uvec2 b1;\n if (small) {\n uvec3 q0 = uvec3(clamp(floor(x0 + 0.5), vec3(0.0), vec3(255.0)));\n uvec3 q1 = uvec3(clamp(floor(x1 + 0.5), vec3(0.0), vec3(255.0)));\n r0 = uvec2(q0.x); g0 = uvec2(q0.y); b0 = uvec2(q0.z);\n r1 = uvec2(q1.x); g1 = uvec2(q1.y); b1 = uvec2(q1.z);\n } else {\n r0 = q192(x0.x); g0 = q192(x0.y); b0 = q192(x0.z);\n r1 = q192(x1.x); g1 = q192(x1.y); b1 = q192(x1.z);\n }\n // Blue-contraction ordering on the unquantised levels.\n if (r0.y + g0.y + b0.y > r1.y + g1.y + b1.y) {\n uvec2 tr = r0; r0 = r1; r1 = tr;\n uvec2 tg = g0; g0 = g1; g1 = tg;\n uvec2 tb = b0; b0 = b1; b1 = tb;\n }\n vec3 d0 = vec3(uvec3(r0.y, g0.y, b0.y));\n vec3 d1 = vec3(uvec3(r1.y, g1.y, b1.y));\n // One weight loop for both budgets; weights as 4-bit nibbles.\n float lmax = small ? 7.0 : 15.0;\n vec3 dir = d1 - d0;\n float dd = dot(dir, dir);\n uint s0 = 0u;\n uint s1 = 0u;\n if (dd > 0.0) {\n float inv = lmax / dd;\n for (int k = 0; k < 8; k++) {\n uint w = uint(clamp(floor(dot(vec3(gPixels[k].xyz) - d0, dir) * inv + 0.5), 0.0, lmax));\n s0 |= w << (4u * uint(k));\n }\n for (int k = 8; k < 16; k++) {\n uint w = uint(clamp(floor(dot(vec3(gPixels[k].xyz) - d0, dir) * inv + 0.5), 0.0, lmax));\n s1 |= w << (4u * uint(k - 8));\n }\n }\n if (small) {\n // Mode 0x053: plain 8-bit endpoints; the 3-bit weight stream is the\n // nibbles compacted (8 nibbles \u2192 24 bits).\n uint c0 = (s0 & 0x07070707u) | ((s0 & 0x70707070u) >> 1u);\n c0 = (c0 & 0x003F003Fu) | ((c0 & 0x3F003F00u) >> 2u);\n c0 = (c0 & 0x00000FFFu) | ((c0 & 0x0FFF0000u) >> 4u);\n uint c1 = (s1 & 0x07070707u) | ((s1 & 0x70707070u) >> 1u);\n c1 = (c1 & 0x003F003Fu) | ((c1 & 0x3F003F00u) >> 2u);\n c1 = (c1 & 0x00000FFFu) | ((c1 & 0x0FFF0000u) >> 4u);\n w0 = 0x053u | (8u << 13u) | (r0.x << 17u) | (r1.x << 25u);\n w1 = (r1.x >> 7u) | (g0.x << 1u) | (g1.x << 9u) | (b0.x << 17u) | (b1.x << 25u);\n w2 = (b1.x >> 7u) | rev32(c1 >> 8u);\n w3 = rev32(c0 | (c1 << 24u));\n } else {\n // Mode 0x242: trit-ISE QUANT_192 endpoints (v0..v5 = R0 R1 G0 G1 B0\n // B1, 46 bits from bit 17: group 1 = v0..v4 with trit field T, group\n // 2 = v5 with its lone trit as 2 bits), 4-bit weights.\n uint tg = tritEnc(r0.x >> 6u, r1.x >> 6u, g0.x >> 6u, g1.x >> 6u, b0.x >> 6u);\n w0 = 0x242u | (8u << 13u) | ((r0.x & 63u) << 17u) | ((tg & 3u) << 23u) | ((r1.x & 63u) << 25u) | (((tg >> 2u) & 1u) << 31u);\n w1 = ((tg >> 3u) & 1u) | ((g0.x & 63u) << 1u) | (((tg >> 4u) & 1u) << 7u) | ((g1.x & 63u) << 8u)\n | (((tg >> 5u) & 3u) << 14u) | ((b0.x & 63u) << 16u) | ((tg >> 7u) << 22u) | ((b1.x & 63u) << 23u)\n | ((b1.x >> 6u) << 29u);\n w2 = rev32(s1);\n w3 = rev32(s0);\n }\n } else {\n // ------------- Translucent: CEM 12, 2-bit weights, PCA seed + refit ----\n vec4 mean = vec4(isum) * (1.0 / 16.0);\n // Fused LSQ fit seeded from the block's principal RGBA axis (4 power\n // iterations \u2014 the refit absorbs residual axis error) at the exact\n // projection extents, clamped to the block bbox (the unconstrained solve\n // extrapolates outside multi-cluster blocks and would bend the hue).\n ivec4 seed0 = lo;\n ivec4 seed1 = hi;\n vec4 axis = principalAxis4(mean, vec4(hi - lo), 4);\n if (dot(axis, axis) > 0.0) {\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec4(gPixels[k]) - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n seed0 = ivec4(clamp(roundEven(mean + tMin * axis), vec4(0.0), vec4(255.0)));\n seed1 = ivec4(clamp(roundEven(mean + tMax * axis), vec4(0.0), vec4(255.0)));\n }\n ivec4 e0 = lo;\n ivec4 e1 = hi;\n uint fitStream = 0u;\n bool haveFitWeights = false;\n Fit r = projFit(seed0, seed1);\n if (r.valid) {\n e0 = clamp(r.e0, lo, hi);\n e1 = clamp(r.e1, lo, hi);\n fitStream = r.wstream;\n haveFitWeights = true;\n }\n bool swapped = false;\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n ivec4 t = e0; e0 = e1; e1 = t;\n swapped = true;\n }\n uvec4 E0 = uvec4(e0);\n uvec4 E1 = uvec4(e1);\n // Valid fits ship the fit-pass weights; the blue-contraction swap is a\n // full reflection w \u2192 3\u2212w = bitwise NOT of the packed stream.\n uint s0 = 0u;\n if (haveFitWeights) {\n s0 = swapped ? ~fitStream : fitStream;\n } else {\n vec4 dir = vec4(e1 - e0);\n float dd = dot(dir, dir);\n vec4 e0f = vec4(e0);\n if (dd > 0.0) {\n float inv = 3.0 / dd;\n for (int k = 0; k < 16; k++) {\n uint w = uint(clamp(floor(dot(vec4(gPixels[k]) - e0f, dir) * inv + 0.5), 0.0, 3.0));\n s0 |= w << (2u * uint(k));\n }\n }\n }\n // Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.\n w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);\n w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);\n w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);\n w3 = rev32(s0);\n }\n\n outColor = uvec4(w0, w1, w2, w3);\n}\n";
|
|
2153
2228
|
|
|
2154
2229
|
// src/webgl/ASTC4x4WebGLEncoder.ts
|
|
2155
2230
|
var ASTC4x4WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -2167,6 +2242,25 @@ var ASTC4x4WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
2167
2242
|
}
|
|
2168
2243
|
};
|
|
2169
2244
|
|
|
2245
|
+
// src/webgl/glsl/etc2.frag.glsl
|
|
2246
|
+
var etc2_frag_default = "#version 300 es\n// ETC2 RGB8 fragment-shader encoder \u2014 WebGL2 port of etc2.wgsl.\n//\n// One fragment per 4\xD74 block. Output is the 8-byte ETC2 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused), each word byte-swapped because ETC2 is\n// big-endian on the wire; the encoder reads back RGBA32UI and keeps the low\n// two words per block. Same algorithm and arithmetic as etc2.wgsl (see its\n// header for the design and measurements): scalar-luma selection, O(1) flip\n// preselect with both flips scored for exactly-gray blocks, a two-candidate\n// table search per subblock (lower neighbour skipped when both covers are\n// table 0), and a closed-form planar contest with quantised corners. WebGL2\n// has no textureGather, so each 2\xD72 quad is four texelFetch()es arranged in\n// gather order (w=(0,0) z=(1,0) x=(0,1) y=(1,1)).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize; // original (unpadded) width, height\nuniform int uFlipY; // 1 = sample bottom-up (matches Three.js flipY)\n\nlayout(location = 0) out uvec4 outColor;\n\nconst float THR[8] = float[8](15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n// Per-table score constants: DK = b3\xB2 \u2212 a3\xB2, DM = \u22122(b3 \u2212 a3), A8 = 8\xB7a3\xB2,\n// AM = \u22122\xB7a3, with (a3, b3) = 3 \xD7 the modifier magnitudes.\nconst float DK[8] = float[8](540.0, 2376.0, 6840.0, 14355.0, 29484.0, 52416.0, 91323.0, 281520.0);\nconst float DM[8] = float[8](-36.0, -72.0, -120.0, -174.0, -252.0, -336.0, -438.0, -816.0);\nconst float A8[8] = float[8](288.0, 1800.0, 5832.0, 12168.0, 23328.0, 41472.0, 78408.0, 159048.0);\nconst float AM[8] = float[8](-12.0, -30.0, -54.0, -78.0, -108.0, -144.0, -198.0, -282.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst float PLANAR_FUDGE = 8.0;\n// Fraction of the luma variance the flip preselect treats as absorbed.\nconst float KAPPA = 0.9;\nconst vec3 ONE3 = vec3(1.0);\nconst vec4 ONE4 = vec4(1.0);\n\nint signed3(uint bits) {\n return bits > 3u ? int(bits) - 8 : int(bits);\n}\n\nuint bswap(uint x) {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nfloat max4(vec4 v) {\n return max(max(v.x, v.y), max(v.z, v.w));\n}\n\n// Base colours from subblock SUMS (8 texels each): codes (as floats) and\n// their 8-bit expansions. Differential mode when the 5-bit codes are within\n// the 3-bit delta range, else individual 4-bit.\nstruct Bases { vec3 c0; vec3 c1; vec3 b0; vec3 b1; bool diff; };\nBases quantiseBases(vec3 sum0, vec3 sum1) {\n vec3 q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n vec3 q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n vec3 d = q1 - q0;\n Bases o;\n o.diff = all(greaterThanEqual(d, vec3(-4.0))) && all(lessThanEqual(d, vec3(3.0)));\n vec3 i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n vec3 i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.c0 = o.diff ? q0 : i0;\n o.c1 = o.diff ? q1 : i1;\n o.b0 = o.diff ? floor(q0 * 8.25) : i0 * 17.0;\n o.b1 = o.diff ? floor(q1 * 8.25) : i1 * 17.0;\n return o;\n}\n\n// Subblock error (\xD73) of table t under the threshold rule, in min form.\nfloat tableScore(vec4 au, vec4 av, float sad, uint t) {\n int i = int(t);\n float dk = DK[i];\n float dm = DM[i];\n vec4 eu = min(vec4(0.0), au * dm + dk);\n vec4 ev = min(vec4(0.0), av * dm + dk);\n return A8[i] + AM[i] * sad + dot(eu + ev, ONE4);\n}\n\n// First table whose large modifier reaches mx \u2014 a binary search over the 7\n// thresholds.\nuint coverTable(float mx) {\n bool s1 = mx > 126.0;\n bool s2 = mx > (s1 ? 240.0 : 51.0);\n bool s3 = mx > (s1 ? (s2 ? 318.0 : 180.0) : (s2 ? 87.0 : 24.0));\n return (s1 ? 4u : 0u) + (s2 ? 2u : 0u) + (s3 ? 1u : 0u);\n}\n\n// Both subblocks of one flip (lumas u, v against base luma lb): cover\n// tables and their scores, then the lower neighbours behind ONE branch \u2014\n// skipped when both covers are table 0 (the lower neighbour IS the cover).\nstruct PairOut { uint t0; uint t1; float acc; };\nPairOut sbPair(vec4 u0, vec4 v0, float lb0, vec4 u1, vec4 v1, float lb1) {\n vec4 au0 = abs(u0 - lb0);\n vec4 av0 = abs(v0 - lb0);\n vec4 au1 = abs(u1 - lb1);\n vec4 av1 = abs(v1 - lb1);\n float sad0 = dot(au0 + av0, ONE4);\n float sad1 = dot(au1 + av1, ONE4);\n uint c0 = coverTable(max(max4(au0), max4(av0)));\n uint c1 = coverTable(max(max4(au1), max4(av1)));\n float hi0 = tableScore(au0, av0, sad0, c0);\n float hi1 = tableScore(au1, av1, sad1, c1);\n PairOut o = PairOut(c0, c1, hi0 + hi1);\n if (c0 != 0u || c1 != 0u) {\n uint l0 = max(c0, 1u) - 1u;\n uint l1 = max(c1, 1u) - 1u;\n float lo0 = tableScore(au0, av0, sad0, l0);\n float lo1 = tableScore(au1, av1, sad1, l1);\n bool w0 = lo0 <= hi0;\n bool w1 = lo1 <= hi1;\n o.t0 = w0 ? l0 : c0;\n o.t1 = w1 ? l1 : c1;\n o.acc = (w0 ? lo0 : hi0) + (w1 ? lo1 : hi1);\n }\n return o;\n}\n\n// One flip's fit: base quantisation + table search, and its estimate\n// (\u03A3||p||\xB2 omitted).\nstruct FlipFit { float est; Bases bases; float lb0; float lb1; uint t0; uint t1; };\nFlipFit fitFlip(vec4 s0u, vec4 s0v, vec4 s1u, vec4 s1v, vec3 sum0, vec3 sum1) {\n FlipFit o;\n o.bases = quantiseBases(sum0, sum1);\n vec3 b0 = o.bases.b0;\n vec3 b1 = o.bases.b1;\n o.lb0 = b0.r + b0.g + b0.b;\n o.lb1 = b1.r + b1.g + b1.b;\n PairOut pp = sbPair(s0u, s0v, o.lb0, s1u, s1v, o.lb1);\n o.t0 = pp.t0;\n o.t1 = pp.t1;\n o.est = dot(b0, 8.0 * b0 - 2.0 * sum0) + dot(b1, 8.0 * b1 - 2.0 * sum1) + pp.acc * (1.0 / 3.0);\n return o;\n}\n\n// fitFlip for exactly-gray blocks (r = g = b): the same arithmetic on one\n// channel; sum0/sum1 are one channel's subblock sums.\nFlipFit fitGray(vec4 s0u, vec4 s0v, vec4 s1u, vec4 s1v, float sum0, float sum1) {\n FlipFit o;\n float q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n float q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n float d = q1 - q0;\n bool diff = d >= -4.0 && d <= 3.0;\n float i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n float i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.bases.diff = diff;\n o.bases.c0 = vec3(diff ? q0 : i0);\n o.bases.c1 = vec3(diff ? q1 : i1);\n float b0 = diff ? floor(q0 * 8.25) : i0 * 17.0;\n float b1 = diff ? floor(q1 * 8.25) : i1 * 17.0;\n o.bases.b0 = vec3(b0);\n o.bases.b1 = vec3(b1);\n o.lb0 = 3.0 * b0;\n o.lb1 = 3.0 * b1;\n PairOut pp = sbPair(s0u, s0v, o.lb0, s1u, s1v, o.lb1);\n o.t0 = pp.t0;\n o.t1 = pp.t1;\n o.est = 3.0 * (b0 * (8.0 * b0 - 2.0 * sum0) + b1 * (8.0 * b1 - 2.0 * sum1)) + pp.acc * (1.0 / 3.0);\n return o;\n}\n\nvec3 fetchRGB(ivec2 p) {\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n return texelFetch(uSrc, ivec2(p.x, sy), 0).rgb;\n}\n\n// One 2\xD72 quad at top-left p: per-texel luma (gather order), channel sums,\n// and the sums of its right column and bottom row (the planar moments'\n// local parts).\nstruct Quad { vec4 l; vec3 s; vec3 right; vec3 bottom; };\nQuad fetchQuad(ivec2 p) {\n vec3 cw = fetchRGB(p) * 255.0;\n vec3 cz = fetchRGB(p + ivec2(1, 0)) * 255.0;\n vec3 cx = fetchRGB(p + ivec2(0, 1)) * 255.0;\n vec3 cy = fetchRGB(p + ivec2(1, 1)) * 255.0;\n vec4 r = vec4(cx.r, cy.r, cz.r, cw.r);\n vec4 g = vec4(cx.g, cy.g, cz.g, cw.g);\n vec4 b = vec4(cx.b, cy.b, cz.b, cw.b);\n Quad o;\n o.l = r + g + b;\n o.right = vec3(r.z + r.y, g.z + g.y, b.z + b.y);\n o.s = o.right + vec3(r.w + r.x, g.w + g.x, b.w + b.x);\n o.bottom = vec3(r.x + r.y, g.x + g.y, b.x + b.y);\n return o;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n\n // Luma by column: col[x][y]. Quadrant q = (x >= 2) | (y >= 2) << 1.\n vec4 col[4];\n vec3 qsum[4];\n // Planar right-hand sides: \u03A3 x\xB7p and \u03A3 y\xB7p.\n vec3 sxp = vec3(0.0);\n vec3 syp = vec3(0.0);\n if (base.x + 4 <= uSrcSize.x && base.y + 4 <= uSrcSize.y) {\n Quad q0 = fetchQuad(base);\n Quad q1 = fetchQuad(base + ivec2(2, 0));\n Quad q2 = fetchQuad(base + ivec2(0, 2));\n Quad q3 = fetchQuad(base + ivec2(2, 2));\n qsum[0] = q0.s;\n qsum[1] = q1.s;\n qsum[2] = q2.s;\n qsum[3] = q3.s;\n sxp = q0.right + q2.right + 2.0 * (q1.s + q3.s) + q1.right + q3.right;\n syp = q0.bottom + q1.bottom + 2.0 * (q2.s + q3.s) + q2.bottom + q3.bottom;\n col[0] = vec4(q0.l.w, q0.l.x, q2.l.w, q2.l.x);\n col[1] = vec4(q0.l.z, q0.l.y, q2.l.z, q2.l.y);\n col[2] = vec4(q1.l.w, q1.l.x, q3.l.w, q3.l.x);\n col[3] = vec4(q1.l.z, q1.l.y, q3.l.z, q3.l.y);\n } else {\n // Blocks straddling the edge of a non-multiple-of-4 image: per-texel\n // loads clamped to the last real texel.\n ivec2 maxXY = uSrcSize - ivec2(1);\n for (int q = 0; q < 4; q++) qsum[q] = vec3(0.0);\n for (int i = 0; i < 16; i++) {\n int lx = i & 3;\n int ly = i >> 2;\n vec3 c = roundEven(fetchRGB(clamp(base + ivec2(lx, ly), ivec2(0), maxXY)) * 255.0);\n col[lx][ly] = c.r + c.g + c.b;\n int q = (lx >= 2 ? 1 : 0) | (ly >= 2 ? 2 : 0);\n qsum[q] += c;\n sxp += float(lx) * c;\n syp += float(ly) * c;\n }\n }\n\n vec3 total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n // Exactly gray: every quadrant sum AND both planar moments equal across\n // R, G, B \u2014 then R and B planar corners coincide (same 6-bit code).\n bool gray = all(equal(qsum[0].rg, qsum[0].gb)) && all(equal(qsum[1].rg, qsum[1].gb)) &&\n all(equal(qsum[2].rg, qsum[2].gb)) && all(equal(qsum[3].rg, qsum[3].gb)) &&\n all(equal(sxp.rg, sxp.gb)) && all(equal(syp.rg, syp.gb));\n\n float planarEst;\n vec3 qo;\n vec3 qh;\n vec3 qv;\n uint bflip = 0u;\n FlipFit sel;\n if (gray) {\n // Planar on two channels: R and B share the 6-bit solve.\n float rB = sxp.r * 0.25;\n float rC = syp.r * 0.25;\n float rA = total.r - rB - rC;\n float po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n float ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n float pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n vec2 pmax = vec2(63.0, 127.0);\n vec2 qo2 = clamp(floor(po * (pmax / 255.0) + 0.5), vec2(0.0), pmax);\n vec2 qh2 = clamp(floor(ph * (pmax / 255.0) + 0.5), vec2(0.0), pmax);\n vec2 qv2 = clamp(floor(pv * (pmax / 255.0) + 0.5), vec2(0.0), pmax);\n vec2 xk = vec2(4.0625, 2.015625);\n vec2 eo = floor(qo2 * xk);\n vec2 eh = floor(qh2 * xk);\n vec2 ev = floor(qv2 * xk);\n vec2 gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n vec2 pe = gram - 2.0 * (eo * rA + eh * rB + ev * rC);\n planarEst = 2.0 * pe.x + pe.y + PLANAR_FUDGE;\n qo = qo2.xyx;\n qh = qh2.xyx;\n qv = qv2.xyx;\n\n float sum0a = qsum[0].r + qsum[2].r;\n float sum1a = qsum[1].r + qsum[3].r;\n float sum0b = qsum[0].r + qsum[1].r;\n float sum1b = qsum[2].r + qsum[3].r;\n sel = fitGray(col[0], col[1], col[2], col[3], sum0a, sum1a);\n FlipFit alt = fitGray(\n vec4(col[0].xy, col[1].xy),\n vec4(col[2].xy, col[3].xy),\n vec4(col[0].zw, col[1].zw),\n vec4(col[2].zw, col[3].zw),\n sum0b,\n sum1b\n );\n if (alt.est < sel.est) {\n sel = alt;\n bflip = 1u;\n }\n } else {\n // LSQ plane in closed form (constant inverse Gram matrix), estimated with\n // the quantised, clamped corners: \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8.\n vec3 rB = sxp * 0.25;\n vec3 rC = syp * 0.25;\n vec3 rA = total - rB - rC;\n vec3 po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n vec3 ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n vec3 pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n vec3 pmax = vec3(63.0, 127.0, 63.0);\n qo = clamp(floor(po * (pmax / 255.0) + 0.5), vec3(0.0), pmax);\n qh = clamp(floor(ph * (pmax / 255.0) + 0.5), vec3(0.0), pmax);\n qv = clamp(floor(pv * (pmax / 255.0) + 0.5), vec3(0.0), pmax);\n // 6-bit expand (q<<2)|(q>>4) = floor(4.0625\xB7q); 7-bit (q<<1)|(q>>6) = floor(2.015625\xB7q).\n vec3 xk = vec3(4.0625, 2.015625, 4.0625);\n vec3 eo = floor(qo * xk);\n vec3 eh = floor(qh * xk);\n vec3 ev = floor(qv * xk);\n vec3 gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n planarEst = dot(gram - 2.0 * (eo * rA + eh * rB + ev * rC), ONE3) + PLANAR_FUDGE;\n\n // Flip 0 splits columns, flip 1 splits rows. Per flip, the preselect\n // residual minus the flip-independent terms: \u2212\u03A3||s||\xB2/8 + \u03BA\xB7(\u03A3\u2113)\xB2/24.\n vec3 sum0a = qsum[0] + qsum[2];\n vec3 sum1a = qsum[1] + qsum[3];\n vec3 sum0b = qsum[0] + qsum[1];\n vec3 sum1b = qsum[2] + qsum[3];\n float l0a = dot(sum0a, ONE3);\n float l1a = dot(sum1a, ONE3);\n float l0b = dot(sum0b, ONE3);\n float l1b = dot(sum1b, ONE3);\n float resA = KAPPA / 24.0 * (l0a * l0a + l1a * l1a) - 0.125 * (dot(sum0a, sum0a) + dot(sum1a, sum1a));\n float resB = KAPPA / 24.0 * (l0b * l0b + l1b * l1b) - 0.125 * (dot(sum0b, sum0b) + dot(sum1b, sum1b));\n bool fb = resB < resA;\n bflip = fb ? 1u : 0u;\n sel = fitFlip(\n fb ? vec4(col[0].xy, col[1].xy) : col[0],\n fb ? vec4(col[2].xy, col[3].xy) : col[1],\n fb ? vec4(col[0].zw, col[1].zw) : col[2],\n fb ? vec4(col[2].zw, col[3].zw) : col[3],\n fb ? sum0b : sum0a,\n fb ? sum1b : sum1a\n );\n }\n\n // ------------------------------------------------------------ packing --\n uint hi;\n uint lo;\n if (sel.est <= planarEst) {\n uvec3 codes0 = uvec3(sel.bases.c0);\n uvec3 codes1 = uvec3(sel.bases.c1);\n uint t0 = sel.t0;\n uint t1 = sel.t1;\n if (sel.bases.diff) {\n uvec3 d = uvec3(ivec3(codes1) - ivec3(codes0)) & uvec3(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n // Wire indices, column by column (bit x\xB74 + y): flip 0 gives columns\n // 0,1 subblock 0; flip 1 gives rows 0,1 (lanes x, y) subblock 0.\n // LSB = large modifier, MSB = negative.\n bool fb = bflip == 1u;\n float lb0 = sel.lb0;\n float lb1 = sel.lb1;\n float th0 = THR[int(t0)];\n float th1 = THR[int(t1)];\n vec4 lbRows = vec4(lb0, lb0, lb1, lb1);\n vec4 thRows = vec4(th0, th0, th1, th1);\n vec4 lbL = fb ? lbRows : vec4(lb0);\n vec4 lbR = fb ? lbRows : vec4(lb1);\n vec4 thL = fb ? thRows : vec4(th0);\n vec4 thR = fb ? thRows : vec4(th1);\n uint lsb = 0u;\n uint msb = 0u;\n for (int c = 0; c < 4; c++) {\n vec4 d = col[c] - (c >= 2 ? lbR : lbL);\n bvec4 large = greaterThan(abs(d), c >= 2 ? thR : thL);\n bvec4 neg = lessThan(d, vec4(0.0));\n uint nl = (large.x ? 1u : 0u) | (large.y ? 2u : 0u) | (large.z ? 4u : 0u) | (large.w ? 8u : 0u);\n uint nn = (neg.x ? 1u : 0u) | (neg.y ? 2u : 0u) | (neg.z ? 4u : 0u) | (neg.w ? 8u : 0u);\n lsb |= nl << uint(c * 4);\n msb |= nn << uint(c * 4);\n }\n lo = lsb | (msb << 16u);\n } else {\n uint ro = uint(qo.r); uint go = uint(qo.g); uint bo = uint(qo.b);\n uint rh = uint(qh.r); uint gh = uint(qh.g); uint bh = uint(qh.b);\n uint rv = uint(qv.r); uint gv = uint(qv.g); uint bv = uint(qv.b);\n int rSum = int(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n uint rFix = rSum < 0 ? 1u : 0u;\n int gSum = int((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n uint gFix = gSum < 0 ? 1u : 0u;\n uint p = (bo >> 3u) & 3u;\n uint q = (bo >> 1u) & 3u;\n uint bFix3 = p + q >= 4u ? 7u : 0u;\n uint bFix1 = p + q >= 4u ? 0u : 1u;\n hi = (rFix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (gFix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (bFix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (bFix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n outColor = uvec4(bswap(hi), bswap(lo), 0u, 0u);\n}\n";
|
|
2247
|
+
|
|
2248
|
+
// src/webgl/ETC2WebGLEncoder.ts
|
|
2249
|
+
var ETC2WebGLEncoder = class extends WebGLBlockEncoder {
|
|
2250
|
+
get label() {
|
|
2251
|
+
return "etc2";
|
|
2252
|
+
}
|
|
2253
|
+
get bytesPerBlock() {
|
|
2254
|
+
return 8;
|
|
2255
|
+
}
|
|
2256
|
+
get supportsSrgb() {
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
2259
|
+
fragSource() {
|
|
2260
|
+
return etc2_frag_default;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2170
2264
|
// src/webgl/webglCapabilities.ts
|
|
2171
2265
|
function detectWebGLCapabilities(gl) {
|
|
2172
2266
|
if (!gl || typeof gl.getExtension !== "function") {
|
|
@@ -2178,7 +2272,8 @@ function detectWebGLCapabilities(gl) {
|
|
|
2178
2272
|
rgtc: has("EXT_texture_compression_rgtc"),
|
|
2179
2273
|
s3tc: has("WEBGL_compressed_texture_s3tc"),
|
|
2180
2274
|
s3tcSrgb: has("WEBGL_compressed_texture_s3tc_srgb"),
|
|
2181
|
-
astc: has("WEBGL_compressed_texture_astc")
|
|
2275
|
+
astc: has("WEBGL_compressed_texture_astc"),
|
|
2276
|
+
etc: has("WEBGL_compressed_texture_etc")
|
|
2182
2277
|
};
|
|
2183
2278
|
}
|
|
2184
2279
|
|
|
@@ -2197,17 +2292,24 @@ function selectWebGLFormat(caps, hint, options = {}) {
|
|
|
2197
2292
|
encoderClass: BC1WebGLEncoder,
|
|
2198
2293
|
astcNormalRemap: false
|
|
2199
2294
|
});
|
|
2295
|
+
const etc2 = () => ({
|
|
2296
|
+
format: srgb ? TextureFormat.ETC2_RGB8_SRGB : TextureFormat.ETC2_RGB8,
|
|
2297
|
+
encoderClass: ETC2WebGLEncoder,
|
|
2298
|
+
astcNormalRemap: false
|
|
2299
|
+
});
|
|
2300
|
+
const hasBc1 = srgb ? caps.s3tcSrgb : caps.s3tc;
|
|
2200
2301
|
if (preferredFormat === "bc1") {
|
|
2201
2302
|
if (hint !== "color") {
|
|
2202
2303
|
console.warn(
|
|
2203
2304
|
`[gputex] preferredFormat 'bc1' ignored for hint '${hint}' \u2014 BC1 has no real alpha channel and is unsuitable for normal maps.`
|
|
2204
2305
|
);
|
|
2205
|
-
} else if (
|
|
2306
|
+
} else if (hasBc1) {
|
|
2206
2307
|
return bc1();
|
|
2207
2308
|
}
|
|
2208
2309
|
}
|
|
2209
|
-
if (quality === "low" && hint === "color"
|
|
2210
|
-
return bc1();
|
|
2310
|
+
if (quality === "low" && hint === "color") {
|
|
2311
|
+
if (hasBc1) return bc1();
|
|
2312
|
+
if (caps.etc) return etc2();
|
|
2211
2313
|
}
|
|
2212
2314
|
if (hint === "normal") {
|
|
2213
2315
|
if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
|
|
@@ -2222,9 +2324,8 @@ function selectWebGLFormat(caps, hint, options = {}) {
|
|
|
2222
2324
|
};
|
|
2223
2325
|
}
|
|
2224
2326
|
if (caps.astc) return astc(false);
|
|
2225
|
-
if (hint === "color" &&
|
|
2226
|
-
|
|
2227
|
-
}
|
|
2327
|
+
if (hint === "color" && hasBc1) return bc1();
|
|
2328
|
+
if (hint === "color" && caps.etc) return etc2();
|
|
2228
2329
|
return NONE;
|
|
2229
2330
|
}
|
|
2230
2331
|
|
|
@@ -2399,6 +2500,9 @@ function getDownsamplePipeline(device) {
|
|
|
2399
2500
|
}
|
|
2400
2501
|
return pipeline;
|
|
2401
2502
|
}
|
|
2503
|
+
async function warmGpuMipgen(device) {
|
|
2504
|
+
await getDownsamplePipeline(device);
|
|
2505
|
+
}
|
|
2402
2506
|
async function generateGpuMipChain(device, source, { flipY = false } = {}) {
|
|
2403
2507
|
const width = source.width;
|
|
2404
2508
|
const height = source.height;
|
|
@@ -2606,6 +2710,7 @@ async function buildTranscodeKey(source, cacheKey, fp) {
|
|
|
2606
2710
|
if (!id) return null;
|
|
2607
2711
|
const fingerprint = [
|
|
2608
2712
|
fp.format,
|
|
2713
|
+
fp.backend,
|
|
2609
2714
|
fp.colorSpace,
|
|
2610
2715
|
fp.flipY ? "flip" : "noflip",
|
|
2611
2716
|
fp.mipmaps ? "mips" : "nomips",
|
|
@@ -2681,7 +2786,36 @@ function getSharedGpu() {
|
|
|
2681
2786
|
}
|
|
2682
2787
|
return sharedGpuPromise;
|
|
2683
2788
|
}
|
|
2789
|
+
function sharedWebGPUEncoder(shared, cls) {
|
|
2790
|
+
let enc = shared.encoders.get(cls);
|
|
2791
|
+
if (!enc) {
|
|
2792
|
+
enc = new cls({ device: shared.device, adapter: shared.adapter, ownsDevice: false });
|
|
2793
|
+
shared.encoders.set(cls, enc);
|
|
2794
|
+
}
|
|
2795
|
+
return enc;
|
|
2796
|
+
}
|
|
2797
|
+
var sharedGl = null;
|
|
2798
|
+
function sharedWebGLEncoder(gl, cls) {
|
|
2799
|
+
if (!sharedGl || sharedGl.gl !== gl) {
|
|
2800
|
+
sharedGl?.encoders.forEach((e) => e.destroy());
|
|
2801
|
+
sharedGl = { gl, encoders: /* @__PURE__ */ new Map() };
|
|
2802
|
+
}
|
|
2803
|
+
let enc = sharedGl.encoders.get(cls);
|
|
2804
|
+
if (!enc) {
|
|
2805
|
+
enc = cls.create(gl);
|
|
2806
|
+
sharedGl.encoders.set(cls, enc);
|
|
2807
|
+
}
|
|
2808
|
+
return enc;
|
|
2809
|
+
}
|
|
2810
|
+
function dropSharedWebGLEncoder(cls) {
|
|
2811
|
+
const enc = sharedGl?.encoders.get(cls);
|
|
2812
|
+
if (!enc) return;
|
|
2813
|
+
enc.destroy();
|
|
2814
|
+
sharedGl.encoders.delete(cls);
|
|
2815
|
+
}
|
|
2684
2816
|
function releaseSharedGpuResources() {
|
|
2817
|
+
sharedGl?.encoders.forEach((e) => e.destroy());
|
|
2818
|
+
sharedGl = null;
|
|
2685
2819
|
const p = sharedGpuPromise;
|
|
2686
2820
|
sharedGpuPromise = null;
|
|
2687
2821
|
void p?.then((shared) => {
|
|
@@ -2692,6 +2826,46 @@ function releaseSharedGpuResources() {
|
|
|
2692
2826
|
}).catch(() => {
|
|
2693
2827
|
});
|
|
2694
2828
|
}
|
|
2829
|
+
async function prewarmCompressTexture(targets = {}) {
|
|
2830
|
+
const t0 = performance.now();
|
|
2831
|
+
const list = Array.isArray(targets) ? targets : [targets];
|
|
2832
|
+
const hasWebGPU = typeof navigator !== "undefined" && "gpu" in navigator;
|
|
2833
|
+
const shared = hasWebGPU && list.some((t) => !t.forceWebGL) ? await getSharedGpu().catch(() => null) : null;
|
|
2834
|
+
const waits = [];
|
|
2835
|
+
let mipgenWarmed = false;
|
|
2836
|
+
const out = [];
|
|
2837
|
+
for (const t of list) {
|
|
2838
|
+
const { hint = "color", quality = "high", preferredFormat, colorSpace = "srgb", mipmaps = false } = t;
|
|
2839
|
+
const opts = { colorSpace, preferredFormat, quality };
|
|
2840
|
+
if (!t.forceWebGL && shared) {
|
|
2841
|
+
const sel = selectFormat(shared.adapter, hint, opts);
|
|
2842
|
+
if (sel.format && sel.encoderClass) {
|
|
2843
|
+
waits.push(sharedWebGPUEncoder(shared, sel.encoderClass).ready());
|
|
2844
|
+
if (mipmaps && !mipgenWarmed && !needsWriteTextureWorkaround(shared.adapter)) {
|
|
2845
|
+
mipgenWarmed = true;
|
|
2846
|
+
waits.push(warmGpuMipgen(shared.device));
|
|
2847
|
+
}
|
|
2848
|
+
out.push({ backend: "webgpu", format: sel.format });
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
const gl = getSharedWebGLContext();
|
|
2853
|
+
if (gl) {
|
|
2854
|
+
const sel = selectWebGLFormat(detectWebGLCapabilities(gl), hint, opts);
|
|
2855
|
+
if (sel.format && sel.encoderClass) {
|
|
2856
|
+
try {
|
|
2857
|
+
waits.push(sharedWebGLEncoder(gl, sel.encoderClass).ready());
|
|
2858
|
+
} catch {
|
|
2859
|
+
}
|
|
2860
|
+
out.push({ backend: "webgl", format: sel.format });
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
out.push({ backend: "none", format: null });
|
|
2865
|
+
}
|
|
2866
|
+
await Promise.allSettled(waits);
|
|
2867
|
+
return { targets: out, ms: performance.now() - t0 };
|
|
2868
|
+
}
|
|
2695
2869
|
async function sourceToBitmap(source, svgSize) {
|
|
2696
2870
|
const opts = {
|
|
2697
2871
|
colorSpaceConversion: "none",
|
|
@@ -2793,16 +2967,18 @@ async function compressTextureToBytes(source, options = {}) {
|
|
|
2793
2967
|
cache = false,
|
|
2794
2968
|
cacheKey,
|
|
2795
2969
|
device: providedDevice,
|
|
2796
|
-
adapter: providedAdapter
|
|
2970
|
+
adapter: providedAdapter,
|
|
2971
|
+
forceWebGL = false
|
|
2797
2972
|
} = options;
|
|
2798
2973
|
const t0 = performance.now();
|
|
2799
|
-
const gpu = await resolveWebGPU();
|
|
2974
|
+
const gpu = forceWebGL ? null : await resolveWebGPU();
|
|
2800
2975
|
const gl = gpu ? null : resolveWebGL();
|
|
2801
2976
|
const activeFormat = gpu?.selection.format ?? gl?.selection.format ?? null;
|
|
2802
2977
|
let transcodeKey = null;
|
|
2803
2978
|
if (cache && activeFormat) {
|
|
2804
2979
|
transcodeKey = await buildTranscodeKey(source, cacheKey, {
|
|
2805
2980
|
format: activeFormat,
|
|
2981
|
+
backend: gpu ? "webgpu" : "webgl",
|
|
2806
2982
|
colorSpace,
|
|
2807
2983
|
flipY,
|
|
2808
2984
|
mipmaps,
|
|
@@ -2829,11 +3005,33 @@ async function compressTextureToBytes(source, options = {}) {
|
|
|
2829
3005
|
}
|
|
2830
3006
|
}
|
|
2831
3007
|
}
|
|
3008
|
+
const gpuEncoder = gpu ? acquireWebGPUEncoder(gpu) : null;
|
|
3009
|
+
gpuEncoder?.catch(() => {
|
|
3010
|
+
});
|
|
3011
|
+
let glEncoder = null;
|
|
3012
|
+
let glEncoderError = null;
|
|
3013
|
+
if (gl) {
|
|
3014
|
+
try {
|
|
3015
|
+
glEncoder = sharedWebGLEncoder(gl.gl, gl.selection.encoderClass);
|
|
3016
|
+
} catch (e) {
|
|
3017
|
+
glEncoderError = e;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
2832
3020
|
const tDecode = performance.now();
|
|
2833
|
-
|
|
3021
|
+
let bitmap;
|
|
3022
|
+
try {
|
|
3023
|
+
bitmap = await sourceToBitmap(source, svgSize);
|
|
3024
|
+
} catch (e) {
|
|
3025
|
+
void gpuEncoder?.then(
|
|
3026
|
+
(a) => a.shared || a.encoder.destroy(),
|
|
3027
|
+
() => {
|
|
3028
|
+
}
|
|
3029
|
+
);
|
|
3030
|
+
throw e;
|
|
3031
|
+
}
|
|
2834
3032
|
const decodeMs = performance.now() - tDecode;
|
|
2835
|
-
if (gpu) return encodeViaWebGPU(gpu);
|
|
2836
|
-
const viaWebGL = gl ? encodeViaWebGL(gl) : null;
|
|
3033
|
+
if (gpu && gpuEncoder) return encodeViaWebGPU(gpu, await gpuEncoder);
|
|
3034
|
+
const viaWebGL = gl ? encodeViaWebGL(gl, glEncoder, glEncoderError) : null;
|
|
2837
3035
|
if (viaWebGL) return viaWebGL;
|
|
2838
3036
|
console.warn(
|
|
2839
3037
|
"[compressTextureToBytes] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
|
|
@@ -2874,23 +3072,23 @@ async function compressTextureToBytes(source, options = {}) {
|
|
|
2874
3072
|
selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass }
|
|
2875
3073
|
};
|
|
2876
3074
|
}
|
|
2877
|
-
async function
|
|
3075
|
+
async function acquireWebGPUEncoder({ adapter, shared, selection }) {
|
|
2878
3076
|
const EncoderCtor = selection.encoderClass;
|
|
2879
|
-
let
|
|
2880
|
-
let sharedEncoder = false;
|
|
3077
|
+
let acquired;
|
|
2881
3078
|
if (providedDevice) {
|
|
2882
|
-
|
|
3079
|
+
acquired = { encoder: new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false }), shared: false };
|
|
2883
3080
|
} else if (shared) {
|
|
2884
|
-
|
|
2885
|
-
let cached = shared.encoders.get(EncoderCtor);
|
|
2886
|
-
if (!cached) {
|
|
2887
|
-
cached = new EncoderCtor({ device: shared.device, adapter: shared.adapter, ownsDevice: false });
|
|
2888
|
-
shared.encoders.set(EncoderCtor, cached);
|
|
2889
|
-
}
|
|
2890
|
-
encoder = cached;
|
|
3081
|
+
acquired = { encoder: sharedWebGPUEncoder(shared, EncoderCtor), shared: true };
|
|
2891
3082
|
} else {
|
|
2892
|
-
|
|
3083
|
+
acquired = { encoder: await EncoderCtor.create(), shared: false };
|
|
2893
3084
|
}
|
|
3085
|
+
if (mipmaps && !needsWriteTextureWorkaround(adapter)) {
|
|
3086
|
+
warmGpuMipgen(acquired.encoder.device).catch(() => {
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
return acquired;
|
|
3090
|
+
}
|
|
3091
|
+
async function encodeViaWebGPU({ adapter, selection }, { encoder, shared: sharedEncoder }) {
|
|
2894
3092
|
const destroyEncoder = sharedEncoder ? () => {
|
|
2895
3093
|
} : () => encoder.destroy();
|
|
2896
3094
|
try {
|
|
@@ -2979,9 +3177,9 @@ async function compressTextureToBytes(source, options = {}) {
|
|
|
2979
3177
|
if (!selection.format || !selection.encoderClass) return null;
|
|
2980
3178
|
return { gl: gl2, selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass } };
|
|
2981
3179
|
}
|
|
2982
|
-
function encodeViaWebGL({
|
|
2983
|
-
const encoder = selection.encoderClass.create(gl2);
|
|
3180
|
+
function encodeViaWebGL({ selection }, encoder, acquireError) {
|
|
2984
3181
|
try {
|
|
3182
|
+
if (!encoder) throw acquireError;
|
|
2985
3183
|
if (!mipmaps) {
|
|
2986
3184
|
const bytes = encoder.encodeToBytes(bitmap, { flipY });
|
|
2987
3185
|
if (transcodeKey) {
|
|
@@ -2992,7 +3190,6 @@ async function compressTextureToBytes(source, options = {}) {
|
|
|
2992
3190
|
levels: [bytes]
|
|
2993
3191
|
});
|
|
2994
3192
|
}
|
|
2995
|
-
encoder.destroy();
|
|
2996
3193
|
return {
|
|
2997
3194
|
levels: [bytes],
|
|
2998
3195
|
fallbackBitmap: null,
|
|
@@ -3027,7 +3224,6 @@ async function compressTextureToBytes(source, options = {}) {
|
|
|
3027
3224
|
levels: encodedLevels
|
|
3028
3225
|
});
|
|
3029
3226
|
}
|
|
3030
|
-
encoder.destroy();
|
|
3031
3227
|
return {
|
|
3032
3228
|
levels: encodedLevels,
|
|
3033
3229
|
fallbackBitmap: null,
|
|
@@ -3044,7 +3240,7 @@ async function compressTextureToBytes(source, options = {}) {
|
|
|
3044
3240
|
cacheHit: false
|
|
3045
3241
|
};
|
|
3046
3242
|
} catch (e) {
|
|
3047
|
-
encoder.
|
|
3243
|
+
if (encoder) dropSharedWebGLEncoder(selection.encoderClass);
|
|
3048
3244
|
console.warn("[compressTextureToBytes] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
|
|
3049
3245
|
return null;
|
|
3050
3246
|
}
|
|
@@ -3188,6 +3384,9 @@ var GputexLoader = class extends Loader {
|
|
|
3188
3384
|
*/
|
|
3189
3385
|
device;
|
|
3190
3386
|
adapter;
|
|
3387
|
+
/** Encode on the WebGL2 fallback even when WebGPU is available (testing).
|
|
3388
|
+
* See `CompressOptions.forceWebGL`. Default false. */
|
|
3389
|
+
forceWebGL = false;
|
|
3191
3390
|
/**
|
|
3192
3391
|
* Most recent full encode result. Useful when the caller wants format
|
|
3193
3392
|
* / mipLevels / astcNormalRemap metadata without threading a separate
|
|
@@ -3213,7 +3412,8 @@ var GputexLoader = class extends Loader {
|
|
|
3213
3412
|
mipmaps: this.mipmaps,
|
|
3214
3413
|
cache: this.cache,
|
|
3215
3414
|
device: this.device,
|
|
3216
|
-
adapter: this.adapter
|
|
3415
|
+
adapter: this.adapter,
|
|
3416
|
+
forceWebGL: this.forceWebGL
|
|
3217
3417
|
}).then(
|
|
3218
3418
|
(result) => {
|
|
3219
3419
|
this.lastResult = result;
|
|
@@ -3253,6 +3453,7 @@ export {
|
|
|
3253
3453
|
BC7Encoder,
|
|
3254
3454
|
BC7WebGLEncoder,
|
|
3255
3455
|
ETC2Encoder,
|
|
3456
|
+
ETC2WebGLEncoder,
|
|
3256
3457
|
Encoder,
|
|
3257
3458
|
GputexLoader,
|
|
3258
3459
|
TextureFormat,
|
|
@@ -3272,6 +3473,7 @@ export {
|
|
|
3272
3473
|
gpuMipLevelCount,
|
|
3273
3474
|
isWebGLAvailable,
|
|
3274
3475
|
padToBlockMultiple,
|
|
3476
|
+
prewarmCompressTexture,
|
|
3275
3477
|
rasterizeSvg,
|
|
3276
3478
|
releaseSharedGpuResources,
|
|
3277
3479
|
selectFormat,
|