gputex 0.9.0 → 0.10.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 +7 -2
- package/dist/index.js +94 -81
- package/dist/three.js +94 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,8 +90,13 @@ moments and accepted per block only when they lower the error; its
|
|
|
90
90
|
near-flat blocks skip the line fit and take the endpoint pair whose ⅔/⅓
|
|
91
91
|
interpolant lands nearest the block colour (direct 565 rounding is up to 4
|
|
92
92
|
levels off — worth up to +3.9 dB on maps with flat regions). BC5 refits
|
|
93
|
-
once
|
|
94
|
-
|
|
93
|
+
once from levels assigned against a slightly inset range (so the extreme
|
|
94
|
+
levels gather every pixel near the extremes, not just the extreme pixel),
|
|
95
|
+
then shifts both endpoints by the mean residual of the final levels, and
|
|
96
|
+
encodes blocks spanning ≤ 7 values losslessly — roughly 4–9% lower error than a
|
|
97
|
+
plain min/max seed on real textures, up to 60% on displacement maps. BC7's
|
|
98
|
+
16-level mode-6 palette makes the refit redundant on a principal-axis seed
|
|
99
|
+
(≤0.05 dB). ASTC spends every one of its 128 bits: a
|
|
95
100
|
wide-span opaque block gets 16 weight levels with 192-level (trit-coded)
|
|
96
101
|
endpoints, a small-span one exact 8-bit endpoints with 8 levels,
|
|
97
102
|
exactly-grayscale blocks a luminance-only mode with 32 levels. On GPUs that
|
package/dist/index.js
CHANGED
|
@@ -1098,7 +1098,7 @@ var BC1Encoder = class extends Encoder {
|
|
|
1098
1098
|
};
|
|
1099
1099
|
|
|
1100
1100
|
// src/bc5.wgsl
|
|
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
|
|
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 seed at the per-channel extremes (spans \u2264 7: a lossless 7-wide\n// window); pass-1 levels come from that range inset by ~5.5/256 of the\n// span and accumulate MOMENTS (\u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d with d = v \u2212 r0) \u2014 the\n// seed covers the data, so pass 1 needs no clamp;\n// \u2022 refit = the least-squares line through those levels straight off the\n// moments (den = 16\u03A3L\xB2 \u2212 (\u03A3L)\xB2 = 0 keeps the seed), both channels as\n// branch-free vec2 lanes;\n// \u2022 pass 2 derives the levels ONCE, against the refit endpoints \u2014 full\n// reprojection quality \u2014 then an offset round shifts both endpoints by\n// the rounded mean residual of those levels (never worse on them);\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 per-channel extremes (round-to-nearest, the same\n // rule the CPU reference uses); spans \u2264 7 (incl. flat blocks) seed a\n // 7-wide window instead, whose levels land on every integer the block\n // holds \u2014 lossless, and the refit keeps it.\n let vhi = clamp(floor(vmax + 0.5), vec2<f32>(0.0), vec2<f32>(255.0));\n let vlo = clamp(floor(vmin + 0.5), vec2<f32>(0.0), vec2<f32>(255.0));\n let small = vhi - vlo <= vec2<f32>(7.0);\n let r1f = select(vlo, min(vlo, vec2<f32>(248.0)), small);\n let r0f = select(vhi, r1f + 7.0, small);\n // Pass-1 levels come from the seed range INSET by ~5.5/256 of the span\n // on both ends: scale \xD77.3125/7, offset \xBD \u2212 0.15625 (exact dyadic\n // constants, so the WebGL port folds them identically).\n let scale = vec2<f32>(7.3125) / (r1f - r0f);\n\n // Pass 1, both channels \u2014 MOMENTS only. t = d\xB7scale \u2208 [0,7.3125] (the seed\n // covers the data), L = floor(t + 11/32) \u2208 [0,7], no clamp.\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.34375);\n let Lg = floor(dg * scale.y + 0.34375);\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 // Refit: least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through the pass-1 levels,\n // straight off the moments; den = 0 \u27FA every pixel on one level \u2014 keep\n // the seed then. Endpoints clamp to [0,255], NOT the block's value\n // range: for a scalar channel, endpoints beyond the data range are often\n // genuinely optimal and there is no colour axis to bend.\n let den = 16.0 * sLL - sL * sL;\n let beta = (16.0 * sLd - sL * sd) / den;\n let e0 = r0f + (sd - beta * sL) * (1.0 / 16.0);\n let q0f = floor(clamp(e0, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);\n let q1f = floor(clamp(e0 + 7.0 * beta, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);\n let acc = (den > vec2<f32>(0.0)) & (q0f > q1f);\n let n0f = select(r0f, q0f, acc);\n let n1f = select(r1f, q1f, acc);\n\n // Pass 2, both channels \u2014 levels against the refit endpoints, packed as\n // \u03A3 L\xB78^k (A = pixels 0..7, B = pixels 8..15), plus \u03A3L for the offset\n // round.\n let sc2 = vec2<f32>(7.0) / (n1f - n0f);\n var pk = vec4<f32>(0.0); // (Ax, Bx, Ay, By)\n var sL2 = vec2<f32>(0.0);\n for (var q: u32 = 0u; q < 4u; q = q + 1u) {\n let Lr = clamp(floor((vr[q] - n0f.x) * sc2.x + 0.5), vec4<f32>(0.0), vec4<f32>(7.0));\n let Lg = clamp(floor((vg[q] - n0f.y) * sc2.y + 0.5), vec4<f32>(0.0), vec4<f32>(7.0));\n let w = select(W0, W1, (q & 1u) == 1u);\n let hi = q >= 2u;\n let pr = dot(Lr, w);\n let pg = dot(Lg, w);\n pk = pk + vec4<f32>(select(pr, 0.0, hi), select(0.0, pr, hi), select(pg, 0.0, hi), select(0.0, pg, hi));\n sL2 = sL2 + vec2<f32>(dot(Lr, vec4<f32>(1.0)), dot(Lg, vec4<f32>(1.0)));\n }\n\n // Offset round: shift both endpoints by the rounded mean residual of the\n // shipped levels (a whole-level shift moves every palette entry equally,\n // so the error on these indices can only drop, under any decoder).\n let res = sd + 16.0 * (r0f - n0f) - (n1f - n0f) * sL2 * (1.0 / 7.0);\n let sh = clamp(floor(res * (1.0 / 16.0) + 0.5), -n1f, vec2<f32>(255.0) - n0f);\n let m0 = vec2<u32>(n0f + sh);\n let m1 = vec2<u32>(n1f + sh);\n let iAx = lvl_to_idx(u32(pk.x));\n let iBx = lvl_to_idx(u32(pk.y));\n let iAy = lvl_to_idx(u32(pk.z));\n let iBy = lvl_to_idx(u32(pk.w));\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n let o = bi * 4u;\n dst[o] = m0.x | (m1.x << 8u) | (iAx << 16u);\n dst[o + 1u] = (iAx >> 16u) | (iBx << 8u);\n dst[o + 2u] = m0.y | (m1.y << 8u) | (iAy << 16u);\n dst[o + 3u] = (iAy >> 16u) | (iBy << 8u);\n}\n";
|
|
1102
1102
|
|
|
1103
1103
|
// src/bc5_fast_f16.wgsl
|
|
1104
1104
|
var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
@@ -1122,43 +1122,58 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
1122
1122
|
// upload pads the texture with ZEROS, so a normalised-coordinate
|
|
1123
1123
|
// gather there would read padding (or mis-scale against the padded
|
|
1124
1124
|
// size) instead of replicating the last real texel.
|
|
1125
|
-
// \u2022
|
|
1126
|
-
//
|
|
1127
|
-
// level
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1125
|
+
// \u2022 SEED: endpoints at the per-channel extremes, but pass-1 levels are
|
|
1126
|
+
// assigned against that range INSET by ~5.5/256 of the span on both
|
|
1127
|
+
// ends (level scale \xD77.3125/7, offset \xBD \u2212 0.15625 = 11/32 \u2014 exact dyadic
|
|
1128
|
+
// constants so every backend folds them identically), so each extreme level
|
|
1129
|
+
// gathers the pixels NEAR the extremes instead of only the extreme
|
|
1130
|
+
// pixel itself \u2014 the refit then lands much closer to the optimum
|
|
1131
|
+
// (an exhaustive search over all endpoint pairs showed the plain
|
|
1132
|
+
// bbox seed leaving 0.7\u20136 dB on the table). Swept 0..20/256; 5.5 wins
|
|
1133
|
+
// under both the /7 spec decode and Apple's hardware decode (below),
|
|
1134
|
+
// and per-block adaptive insets (variance, extreme gaps) all lost.
|
|
1135
|
+
// Spans \u2264 7 (incl. flat blocks) seed a 7-wide window instead, whose
|
|
1136
|
+
// levels land on every integer the block holds: lossless.
|
|
1137
|
+
// \u2022 Pass 1 accumulates MOMENTS \u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d per channel (d = v \u2212 r0,
|
|
1138
|
+
// exact integers). \u03A3L \u2264 112 and \u03A3L\xB2 \u2264 784 are exact f16 integers; \u03A3L\xB7d
|
|
1139
|
+
// (\u2264 28560) and \u03A3d accumulate in f32 so they stay exact too. The seed
|
|
1140
|
+
// covers the data, so t \u2208 [0,7.3125] and L = floor(t + 11/32) \u2208 [0,7] needs
|
|
1141
|
+
// no clamp.
|
|
1142
|
+
// \u2022 REFIT = the least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through those levels,
|
|
1143
|
+
// straight off the moments: \u03B2 = (16\u03A3Ld \u2212 \u03A3L\xB7\u03A3d)/(16\u03A3L\xB2 \u2212 (\u03A3L)\xB2), \u03B1 =
|
|
1144
|
+
// (\u03A3d \u2212 \u03B2\u03A3L)/16 \u2014 ~12 ops per channel. den = 0 \u27FA every pixel on one
|
|
1145
|
+
// level (exact integer test) keeps the seed. Accepted whenever it stays
|
|
1146
|
+
// in 6-interp mode: on the inset partition, pricing it against the seed
|
|
1147
|
+
// (the previous E(\u03B4) closed form) changed nothing, and dropping that
|
|
1148
|
+
// pricing is what pays for the offset round below.
|
|
1149
|
+
// \u2022 Pass 2 derives the shipped levels ONCE, against the refit endpoints \u2014
|
|
1150
|
+
// full reprojection quality \u2014 as a LOOP over quads (the unrolled form
|
|
1151
|
+
// with all eight level vectors live measured ~6% slower once \u03A3L was
|
|
1152
|
+
// added).
|
|
1153
|
+
// \u2022 OFFSET ROUND: both endpoints shift by round(mean residual) of the
|
|
1154
|
+
// shipped levels (\u03A3v is exact from pass 1, so only \u03A3L is new). A
|
|
1155
|
+
// whole-level shift moves every palette entry equally, so the error on
|
|
1156
|
+
// these indices can only drop, under ANY decoder's weights. Buys half
|
|
1157
|
+
// of a full second refit round (+0.05 dB) for ~1/4 of its cost; the
|
|
1158
|
+
// full round (\u03A3L\xB2, \u03A3L\xB7v in pass 2 + a second solve) measured +0.1 dB
|
|
1159
|
+
// more but +18% GPU at 1K \u2014 rejected.
|
|
1160
|
+
// \u2022 Apple GPUs (M3 measured) decode BC4/BC5 with BC7-style 6-bit weights
|
|
1161
|
+
// (0,9,18,27,37,46,55,64)/64, not exact sevenths \u2014 up to \xB10.0067\xB7span
|
|
1162
|
+
// off the spec palette. The encoder targets the spec (/7) palette;
|
|
1163
|
+
// /eval's hardware-decoded PSNR sits ~0.07 dB under the CPU-decoded one.
|
|
1136
1164
|
// \u2022 Texels live as quad-major vec4<f16> per channel (the gather layout),
|
|
1137
1165
|
// so min/max reduce as vectors and both passes run 4-wide; edge blocks
|
|
1138
1166
|
// load into the same layout.
|
|
1139
|
-
// \u2022 The rank guard is EXACT: all pixels on one level \u27FA 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2
|
|
1140
|
-
// (integers, so the comparison is precise in f32) \u2014 no lmin/lmax
|
|
1141
|
-
// tracking in the loop.
|
|
1142
|
-
// \u2022 The refit is accepted or rejected CLOSED-FORM, with no trial
|
|
1143
|
-
// projection pass: the solve is e = seed + M\u207B\xB9(sAR,sBR), and the error
|
|
1144
|
-
// of re-quantised endpoints ON THE CURRENT INDICES is
|
|
1145
|
-
// E(\u03B4) = err \u2212 2(\u03B40\xB7sAR + \u03B41\xB7sBR) + \u03B40\xB2sAA + 2\u03B40\u03B41\xB7sAB + \u03B41\xB2sBB
|
|
1146
|
-
// with \u03B4 = quantised endpoint \u2212 base endpoint, compared as the delta
|
|
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
1167
|
// \u2022 3-bit indices are packed as FLOAT: each group of 8 pixels'
|
|
1156
1168
|
// levels accumulates as \u03A3 L\xB78^k in f32 (\u2264 2^24 \u2212 1, exact) \u2014 one fma
|
|
1157
1169
|
// per texel \u2014 and the level \u2192 BC4 index map (0\u21920, 7\u21921, L\u2192L+1) is
|
|
1158
1170
|
// applied to the whole 24-bit word with SWAR bit tricks.
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1171
|
+
// The moment/float-packing structure measured \u221222% GPU vs the kernel
|
|
1172
|
+
// before it (rg8 source; 2K/4K normal maps sit at the read floor on rgba8
|
|
1173
|
+
// either way) \u2014 the 1K case is ALU-bound, not read-bound (/eval 2026-09).
|
|
1174
|
+
// The inset seed + regression refit + offset round then lowered MSE by
|
|
1175
|
+
// 3.8\u20139% on every real texture (normals 3.8\u20136.4%, displacement 9\u201360%,
|
|
1176
|
+
// hardware decode) at equal GPU time (/eval 2026-09-24).
|
|
1162
1177
|
//
|
|
1163
1178
|
// The host selects this module only when the device reports shader-f16,
|
|
1164
1179
|
// falling back to bc5.wgsl otherwise.
|
|
@@ -1229,26 +1244,28 @@ fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
|
1229
1244
|
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
1245
|
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)));
|
|
1231
1246
|
|
|
1232
|
-
// Seed endpoints at the exact per-channel extremes
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1247
|
+
// Seed endpoints at the exact per-channel extremes; spans \u2264 7 (incl.
|
|
1248
|
+
// flat blocks) seed a 7-wide window instead, whose levels land on every
|
|
1249
|
+
// integer the block holds \u2014 lossless, and the refit keeps it.
|
|
1250
|
+
let small = vmax - vmin <= vec2<h>(7.0);
|
|
1251
|
+
let r1 = vec2<u32>(select(vmin, min(vmin, vec2<h>(248.0)), small));
|
|
1252
|
+
let r0 = select(vec2<u32>(vmax), r1 + 7u, small);
|
|
1238
1253
|
|
|
1239
1254
|
let r0h = vec2<h>(vec2<f32>(r0));
|
|
1240
1255
|
let dirf = vec2<f32>(r1) - vec2<f32>(r0);
|
|
1241
|
-
|
|
1256
|
+
// Pass-1 levels come from the seed range INSET by ~5.5/256 of the span
|
|
1257
|
+
// on both ends (see header).
|
|
1258
|
+
let scale = vec2<h>(vec2<f32>(7.3125) / dirf);
|
|
1242
1259
|
|
|
1243
|
-
// Pass 1 \u2014 t = d\xB7scale \u2208 [0,7] by construction (seed covers the
|
|
1244
|
-
// so L = floor(t +
|
|
1260
|
+
// Pass 1 \u2014 t = d\xB7scale \u2208 [0,7.3125] by construction (seed covers the
|
|
1261
|
+
// data), so L = floor(t + 11/32) \u2208 [0,7] needs no clamp.
|
|
1245
1262
|
var sLr = h(0.0); var sLLr = h(0.0); var sdr = 0.0; var sLdr = 0.0;
|
|
1246
1263
|
var sLg = h(0.0); var sLLg = h(0.0); var sdg = 0.0; var sLdg = 0.0;
|
|
1247
1264
|
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1248
1265
|
let dr = vr[q] - r0h.x;
|
|
1249
1266
|
let dg = vg[q] - r0h.y;
|
|
1250
|
-
let Lr = floor(dr * scale.x + h(0.
|
|
1251
|
-
let Lg = floor(dg * scale.y + h(0.
|
|
1267
|
+
let Lr = floor(dr * scale.x + h(0.34375));
|
|
1268
|
+
let Lg = floor(dg * scale.y + h(0.34375));
|
|
1252
1269
|
sLr = sLr + dot(Lr, h4(1.0)); sLLr = sLLr + dot(Lr, Lr);
|
|
1253
1270
|
sLg = sLg + dot(Lg, h4(1.0)); sLLg = sLLg + dot(Lg, Lg);
|
|
1254
1271
|
sdr = sdr + f32(dot(dr, h4(1.0)));
|
|
@@ -1257,32 +1274,18 @@ fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
|
1257
1274
|
sLdg = sLdg + dot(vec4<f32>(Lg), vec4<f32>(dg));
|
|
1258
1275
|
}
|
|
1259
1276
|
|
|
1260
|
-
// Per-block refit in f32
|
|
1277
|
+
// Per-block refit in f32: least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through
|
|
1278
|
+
// the pass-1 levels, straight off the (exact-integer) moments. den = 0
|
|
1279
|
+
// \u27FA every pixel on one level (rank-deficient) \u2014 keep the seed then.
|
|
1261
1280
|
let sLf = vec2<f32>(f32(sLr), f32(sLg));
|
|
1262
1281
|
let sLLf = vec2<f32>(f32(sLLr), f32(sLLg));
|
|
1263
|
-
let
|
|
1264
|
-
let
|
|
1265
|
-
let
|
|
1266
|
-
let
|
|
1267
|
-
let
|
|
1268
|
-
let
|
|
1269
|
-
let
|
|
1270
|
-
let sAR = (pR - pLR * (1.0 / 7.0)) * dirf * (1.0 / 7.0);
|
|
1271
|
-
let spread = 16.0 * sLLf != sLf * sLf;
|
|
1272
|
-
|
|
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));
|
|
1282
|
+
let sdf = vec2<f32>(sdr, sdg);
|
|
1283
|
+
let den = 16.0 * sLLf - sLf * sLf;
|
|
1284
|
+
let beta = (16.0 * vec2<f32>(sLdr, sLdg) - sLf * sdf) / den;
|
|
1285
|
+
let e0 = vec2<f32>(r0) + (sdf - beta * sLf) * (1.0 / 16.0);
|
|
1286
|
+
let q0f = floor(clamp(e0, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);
|
|
1287
|
+
let q1f = floor(clamp(e0 + 7.0 * beta, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);
|
|
1288
|
+
let acc = (den > vec2<f32>(0.0)) & (q0f > q1f);
|
|
1286
1289
|
let n0 = select(r0, vec2<u32>(q0f), acc);
|
|
1287
1290
|
let n1 = select(r1, vec2<u32>(q1f), acc);
|
|
1288
1291
|
|
|
@@ -1291,24 +1294,34 @@ fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
|
1291
1294
|
// (\u03A3 L\xB78^k \u2264 2^24 \u2212 1, exact): iA = pixels 0..7, iB = pixels 8..15.
|
|
1292
1295
|
let n0h = vec2<h>(vec2<f32>(n0));
|
|
1293
1296
|
let sc2 = vec2<h>(vec2<f32>(7.0) / (vec2<f32>(n1) - vec2<f32>(n0)));
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1297
|
+
var pk = vec4<f32>(0.0); // (Ax, Bx, Ay, By) level words
|
|
1298
|
+
var sLq = vec2<h>(0.0);
|
|
1299
|
+
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1300
|
+
let Lr = clamp(floor((vr[q] - n0h.x) * sc2.x + h(0.5)), h4(0.0), h4(7.0));
|
|
1301
|
+
let Lg = clamp(floor((vg[q] - n0h.y) * sc2.y + h(0.5)), h4(0.0), h4(7.0));
|
|
1302
|
+
let w = select(W0, W1, (q & 1u) == 1u);
|
|
1303
|
+
let hi = q >= 2u;
|
|
1304
|
+
let pr = dot(vec4<f32>(Lr), w);
|
|
1305
|
+
let pg = dot(vec4<f32>(Lg), w);
|
|
1306
|
+
pk = pk + vec4<f32>(select(pr, 0.0, hi), select(0.0, pr, hi), select(pg, 0.0, hi), select(0.0, pg, hi));
|
|
1307
|
+
sLq = sLq + vec2<h>(dot(Lr, h4(1.0)), dot(Lg, h4(1.0)));
|
|
1308
|
+
}
|
|
1309
|
+
let n0f = vec2<f32>(n0);
|
|
1310
|
+
let sL2 = vec2<f32>(sLq);
|
|
1311
|
+
let res = sdf + 16.0 * (vec2<f32>(r0) - n0f) - (vec2<f32>(n1) - n0f) * sL2 * (1.0 / 7.0);
|
|
1312
|
+
let sh = clamp(floor(res * (1.0 / 16.0) + 0.5), -vec2<f32>(n1), vec2<f32>(255.0) - n0f);
|
|
1313
|
+
let m0 = vec2<u32>(n0f + sh);
|
|
1314
|
+
let m1 = vec2<u32>(vec2<f32>(n1) + sh);
|
|
1315
|
+
let iAx = lvl_to_idx(u32(pk.x));
|
|
1316
|
+
let iBx = lvl_to_idx(u32(pk.y));
|
|
1317
|
+
let iAy = lvl_to_idx(u32(pk.z));
|
|
1318
|
+
let iBy = lvl_to_idx(u32(pk.w));
|
|
1306
1319
|
|
|
1307
1320
|
// BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.
|
|
1308
1321
|
let o = bi * 4u;
|
|
1309
|
-
dst[o] =
|
|
1322
|
+
dst[o] = m0.x | (m1.x << 8u) | (iAx << 16u);
|
|
1310
1323
|
dst[o + 1u] = (iAx >> 16u) | (iBx << 8u);
|
|
1311
|
-
dst[o + 2u] =
|
|
1324
|
+
dst[o + 2u] = m0.y | (m1.y << 8u) | (iAy << 16u);
|
|
1312
1325
|
dst[o + 3u] = (iAy >> 16u) | (iBy << 8u);
|
|
1313
1326
|
}
|
|
1314
1327
|
`;
|
|
@@ -2186,7 +2199,7 @@ var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
2186
2199
|
};
|
|
2187
2200
|
|
|
2188
2201
|
// src/webgl/glsl/bc5.frag.glsl
|
|
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
|
|
2202
|
+
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 (spans \u2264 7: a 7-wide\n// window, lossless); pass-1 levels come from the seed range inset by\n// ~5.5/256 of the span (scale \xD77.3125/7, offset 11/32), accumulating\n// the MOMENTS \u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d (d = v \u2212 r0; the seed covers the\n// data, so no clamp);\n// \u2022 one least-squares line through the pass-1 levels straight off the\n// moments (den = 16\u03A3L\xB2 \u2212 (\u03A3L)\xB2 = 0 keeps the seed);\n// \u2022 pass 2 derives the shipped levels against the refit endpoints,\n// packed as float fields \u03A3 L\xB78^k (exact below 2^24), then one SWAR\n// level \u2192 BC4 index map (0\u21920, 7\u21921, L\u2192L+1) per 24-bit word;\n// \u2022 offset round: both endpoints shift by the rounded mean residual of\n// the shipped levels (never worse on those indices).\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\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 per-channel extremes (round-to-nearest); spans\n // \u2264 7 (incl. flat blocks) seed a 7-wide window instead \u2014 lossless.\n vec2 vhi = clamp(floor(vmax + 0.5), vec2(0.0), vec2(255.0));\n vec2 vlo = clamp(floor(vmin + 0.5), vec2(0.0), vec2(255.0));\n bvec2 small = lessThanEqual(vhi - vlo, vec2(7.0));\n vec2 r1f = vec2(small.x ? min(vlo.x, 248.0) : vlo.x, small.y ? min(vlo.y, 248.0) : vlo.y);\n vec2 r0f = vec2(small.x ? r1f.x + 7.0 : vhi.x, small.y ? r1f.y + 7.0 : vhi.y);\n // Pass-1 levels from the seed range inset by ~5.5/256 of the span.\n vec2 scale = vec2(7.3125) / (r1f - 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.34375);\n vec4 Lg = floor(dg * scale.y + 0.34375);\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 // Refit: least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through the pass-1 levels.\n // Endpoints clamp to [0,255], NOT the block's value range.\n vec2 den = 16.0 * sLL - sL * sL;\n vec2 beta = (16.0 * sLd - sL * sd) / den;\n vec2 e0 = r0f + (sd - beta * sL) * (1.0 / 16.0);\n vec2 q0f = floor(clamp(e0, vec2(0.0), vec2(255.0)) + 0.5);\n vec2 q1f = floor(clamp(e0 + 7.0 * beta, vec2(0.0), vec2(255.0)) + 0.5);\n bvec2 acc = bvec2(den.x > 0.0 && q0f.x > q1f.x, den.y > 0.0 && q0f.y > q1f.y);\n vec2 n0f = vec2(acc.x ? q0f.x : r0f.x, acc.y ? q0f.y : r0f.y);\n vec2 n1f = vec2(acc.x ? q1f.x : r1f.x, acc.y ? q1f.y : r1f.y);\n\n // Pass 2 \u2014 levels against the refit endpoints, packed as \u03A3 L\xB78^k\n // (A = pixels 0..7, B = pixels 8..15), plus \u03A3L for the offset round.\n vec2 sc2 = vec2(7.0) / (n1f - n0f);\n vec4 pk = vec4(0.0); // (Ax, Bx, Ay, By)\n vec2 sL2 = vec2(0.0);\n for (int q = 0; q < 4; q++) {\n vec4 Lr = clamp(floor((vr[q] - n0f.x) * sc2.x + 0.5), vec4(0.0), vec4(7.0));\n vec4 Lg = clamp(floor((vg[q] - n0f.y) * sc2.y + 0.5), vec4(0.0), vec4(7.0));\n vec4 w = (q & 1) == 1 ? W1 : W0;\n bool hi = q >= 2;\n float pr = dot(Lr, w);\n float pg = dot(Lg, w);\n pk += vec4(hi ? 0.0 : pr, hi ? pr : 0.0, hi ? 0.0 : pg, hi ? pg : 0.0);\n sL2 += vec2(dot(Lr, vec4(1.0)), dot(Lg, vec4(1.0)));\n }\n\n // Offset round: shift both endpoints by the rounded mean residual of the\n // shipped levels.\n vec2 res = sd + 16.0 * (r0f - n0f) - (n1f - n0f) * sL2 * (1.0 / 7.0);\n vec2 sh = clamp(floor(res * (1.0 / 16.0) + 0.5), -n1f, vec2(255.0) - n0f);\n uvec2 m0 = uvec2(n0f + sh);\n uvec2 m1 = uvec2(n1f + sh);\n uint iAx = lvlToIdx(uint(pk.x));\n uint iBx = lvlToIdx(uint(pk.y));\n uint iAy = lvlToIdx(uint(pk.z));\n uint iBy = lvlToIdx(uint(pk.w));\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n outColor = uvec4(\n m0.x | (m1.x << 8u) | (iAx << 16u),\n (iAx >> 16u) | (iBx << 8u),\n m0.y | (m1.y << 8u) | (iAy << 16u),\n (iAy >> 16u) | (iBy << 8u)\n );\n}\n";
|
|
2190
2203
|
|
|
2191
2204
|
// src/webgl/BC5WebGLEncoder.ts
|
|
2192
2205
|
var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|
package/dist/three.js
CHANGED
|
@@ -1098,7 +1098,7 @@ var BC1Encoder = class extends Encoder {
|
|
|
1098
1098
|
};
|
|
1099
1099
|
|
|
1100
1100
|
// src/bc5.wgsl
|
|
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
|
|
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 seed at the per-channel extremes (spans \u2264 7: a lossless 7-wide\n// window); pass-1 levels come from that range inset by ~5.5/256 of the\n// span and accumulate MOMENTS (\u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d with d = v \u2212 r0) \u2014 the\n// seed covers the data, so pass 1 needs no clamp;\n// \u2022 refit = the least-squares line through those levels straight off the\n// moments (den = 16\u03A3L\xB2 \u2212 (\u03A3L)\xB2 = 0 keeps the seed), both channels as\n// branch-free vec2 lanes;\n// \u2022 pass 2 derives the levels ONCE, against the refit endpoints \u2014 full\n// reprojection quality \u2014 then an offset round shifts both endpoints by\n// the rounded mean residual of those levels (never worse on them);\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 per-channel extremes (round-to-nearest, the same\n // rule the CPU reference uses); spans \u2264 7 (incl. flat blocks) seed a\n // 7-wide window instead, whose levels land on every integer the block\n // holds \u2014 lossless, and the refit keeps it.\n let vhi = clamp(floor(vmax + 0.5), vec2<f32>(0.0), vec2<f32>(255.0));\n let vlo = clamp(floor(vmin + 0.5), vec2<f32>(0.0), vec2<f32>(255.0));\n let small = vhi - vlo <= vec2<f32>(7.0);\n let r1f = select(vlo, min(vlo, vec2<f32>(248.0)), small);\n let r0f = select(vhi, r1f + 7.0, small);\n // Pass-1 levels come from the seed range INSET by ~5.5/256 of the span\n // on both ends: scale \xD77.3125/7, offset \xBD \u2212 0.15625 (exact dyadic\n // constants, so the WebGL port folds them identically).\n let scale = vec2<f32>(7.3125) / (r1f - r0f);\n\n // Pass 1, both channels \u2014 MOMENTS only. t = d\xB7scale \u2208 [0,7.3125] (the seed\n // covers the data), L = floor(t + 11/32) \u2208 [0,7], no clamp.\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.34375);\n let Lg = floor(dg * scale.y + 0.34375);\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 // Refit: least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through the pass-1 levels,\n // straight off the moments; den = 0 \u27FA every pixel on one level \u2014 keep\n // the seed then. Endpoints clamp to [0,255], NOT the block's value\n // range: for a scalar channel, endpoints beyond the data range are often\n // genuinely optimal and there is no colour axis to bend.\n let den = 16.0 * sLL - sL * sL;\n let beta = (16.0 * sLd - sL * sd) / den;\n let e0 = r0f + (sd - beta * sL) * (1.0 / 16.0);\n let q0f = floor(clamp(e0, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);\n let q1f = floor(clamp(e0 + 7.0 * beta, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);\n let acc = (den > vec2<f32>(0.0)) & (q0f > q1f);\n let n0f = select(r0f, q0f, acc);\n let n1f = select(r1f, q1f, acc);\n\n // Pass 2, both channels \u2014 levels against the refit endpoints, packed as\n // \u03A3 L\xB78^k (A = pixels 0..7, B = pixels 8..15), plus \u03A3L for the offset\n // round.\n let sc2 = vec2<f32>(7.0) / (n1f - n0f);\n var pk = vec4<f32>(0.0); // (Ax, Bx, Ay, By)\n var sL2 = vec2<f32>(0.0);\n for (var q: u32 = 0u; q < 4u; q = q + 1u) {\n let Lr = clamp(floor((vr[q] - n0f.x) * sc2.x + 0.5), vec4<f32>(0.0), vec4<f32>(7.0));\n let Lg = clamp(floor((vg[q] - n0f.y) * sc2.y + 0.5), vec4<f32>(0.0), vec4<f32>(7.0));\n let w = select(W0, W1, (q & 1u) == 1u);\n let hi = q >= 2u;\n let pr = dot(Lr, w);\n let pg = dot(Lg, w);\n pk = pk + vec4<f32>(select(pr, 0.0, hi), select(0.0, pr, hi), select(pg, 0.0, hi), select(0.0, pg, hi));\n sL2 = sL2 + vec2<f32>(dot(Lr, vec4<f32>(1.0)), dot(Lg, vec4<f32>(1.0)));\n }\n\n // Offset round: shift both endpoints by the rounded mean residual of the\n // shipped levels (a whole-level shift moves every palette entry equally,\n // so the error on these indices can only drop, under any decoder).\n let res = sd + 16.0 * (r0f - n0f) - (n1f - n0f) * sL2 * (1.0 / 7.0);\n let sh = clamp(floor(res * (1.0 / 16.0) + 0.5), -n1f, vec2<f32>(255.0) - n0f);\n let m0 = vec2<u32>(n0f + sh);\n let m1 = vec2<u32>(n1f + sh);\n let iAx = lvl_to_idx(u32(pk.x));\n let iBx = lvl_to_idx(u32(pk.y));\n let iAy = lvl_to_idx(u32(pk.z));\n let iBy = lvl_to_idx(u32(pk.w));\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n let o = bi * 4u;\n dst[o] = m0.x | (m1.x << 8u) | (iAx << 16u);\n dst[o + 1u] = (iAx >> 16u) | (iBx << 8u);\n dst[o + 2u] = m0.y | (m1.y << 8u) | (iAy << 16u);\n dst[o + 3u] = (iAy >> 16u) | (iBy << 8u);\n}\n";
|
|
1102
1102
|
|
|
1103
1103
|
// src/bc5_fast_f16.wgsl
|
|
1104
1104
|
var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
@@ -1122,43 +1122,58 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
1122
1122
|
// upload pads the texture with ZEROS, so a normalised-coordinate
|
|
1123
1123
|
// gather there would read padding (or mis-scale against the padded
|
|
1124
1124
|
// size) instead of replicating the last real texel.
|
|
1125
|
-
// \u2022
|
|
1126
|
-
//
|
|
1127
|
-
// level
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1125
|
+
// \u2022 SEED: endpoints at the per-channel extremes, but pass-1 levels are
|
|
1126
|
+
// assigned against that range INSET by ~5.5/256 of the span on both
|
|
1127
|
+
// ends (level scale \xD77.3125/7, offset \xBD \u2212 0.15625 = 11/32 \u2014 exact dyadic
|
|
1128
|
+
// constants so every backend folds them identically), so each extreme level
|
|
1129
|
+
// gathers the pixels NEAR the extremes instead of only the extreme
|
|
1130
|
+
// pixel itself \u2014 the refit then lands much closer to the optimum
|
|
1131
|
+
// (an exhaustive search over all endpoint pairs showed the plain
|
|
1132
|
+
// bbox seed leaving 0.7\u20136 dB on the table). Swept 0..20/256; 5.5 wins
|
|
1133
|
+
// under both the /7 spec decode and Apple's hardware decode (below),
|
|
1134
|
+
// and per-block adaptive insets (variance, extreme gaps) all lost.
|
|
1135
|
+
// Spans \u2264 7 (incl. flat blocks) seed a 7-wide window instead, whose
|
|
1136
|
+
// levels land on every integer the block holds: lossless.
|
|
1137
|
+
// \u2022 Pass 1 accumulates MOMENTS \u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d per channel (d = v \u2212 r0,
|
|
1138
|
+
// exact integers). \u03A3L \u2264 112 and \u03A3L\xB2 \u2264 784 are exact f16 integers; \u03A3L\xB7d
|
|
1139
|
+
// (\u2264 28560) and \u03A3d accumulate in f32 so they stay exact too. The seed
|
|
1140
|
+
// covers the data, so t \u2208 [0,7.3125] and L = floor(t + 11/32) \u2208 [0,7] needs
|
|
1141
|
+
// no clamp.
|
|
1142
|
+
// \u2022 REFIT = the least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through those levels,
|
|
1143
|
+
// straight off the moments: \u03B2 = (16\u03A3Ld \u2212 \u03A3L\xB7\u03A3d)/(16\u03A3L\xB2 \u2212 (\u03A3L)\xB2), \u03B1 =
|
|
1144
|
+
// (\u03A3d \u2212 \u03B2\u03A3L)/16 \u2014 ~12 ops per channel. den = 0 \u27FA every pixel on one
|
|
1145
|
+
// level (exact integer test) keeps the seed. Accepted whenever it stays
|
|
1146
|
+
// in 6-interp mode: on the inset partition, pricing it against the seed
|
|
1147
|
+
// (the previous E(\u03B4) closed form) changed nothing, and dropping that
|
|
1148
|
+
// pricing is what pays for the offset round below.
|
|
1149
|
+
// \u2022 Pass 2 derives the shipped levels ONCE, against the refit endpoints \u2014
|
|
1150
|
+
// full reprojection quality \u2014 as a LOOP over quads (the unrolled form
|
|
1151
|
+
// with all eight level vectors live measured ~6% slower once \u03A3L was
|
|
1152
|
+
// added).
|
|
1153
|
+
// \u2022 OFFSET ROUND: both endpoints shift by round(mean residual) of the
|
|
1154
|
+
// shipped levels (\u03A3v is exact from pass 1, so only \u03A3L is new). A
|
|
1155
|
+
// whole-level shift moves every palette entry equally, so the error on
|
|
1156
|
+
// these indices can only drop, under ANY decoder's weights. Buys half
|
|
1157
|
+
// of a full second refit round (+0.05 dB) for ~1/4 of its cost; the
|
|
1158
|
+
// full round (\u03A3L\xB2, \u03A3L\xB7v in pass 2 + a second solve) measured +0.1 dB
|
|
1159
|
+
// more but +18% GPU at 1K \u2014 rejected.
|
|
1160
|
+
// \u2022 Apple GPUs (M3 measured) decode BC4/BC5 with BC7-style 6-bit weights
|
|
1161
|
+
// (0,9,18,27,37,46,55,64)/64, not exact sevenths \u2014 up to \xB10.0067\xB7span
|
|
1162
|
+
// off the spec palette. The encoder targets the spec (/7) palette;
|
|
1163
|
+
// /eval's hardware-decoded PSNR sits ~0.07 dB under the CPU-decoded one.
|
|
1136
1164
|
// \u2022 Texels live as quad-major vec4<f16> per channel (the gather layout),
|
|
1137
1165
|
// so min/max reduce as vectors and both passes run 4-wide; edge blocks
|
|
1138
1166
|
// load into the same layout.
|
|
1139
|
-
// \u2022 The rank guard is EXACT: all pixels on one level \u27FA 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2
|
|
1140
|
-
// (integers, so the comparison is precise in f32) \u2014 no lmin/lmax
|
|
1141
|
-
// tracking in the loop.
|
|
1142
|
-
// \u2022 The refit is accepted or rejected CLOSED-FORM, with no trial
|
|
1143
|
-
// projection pass: the solve is e = seed + M\u207B\xB9(sAR,sBR), and the error
|
|
1144
|
-
// of re-quantised endpoints ON THE CURRENT INDICES is
|
|
1145
|
-
// E(\u03B4) = err \u2212 2(\u03B40\xB7sAR + \u03B41\xB7sBR) + \u03B40\xB2sAA + 2\u03B40\u03B41\xB7sAB + \u03B41\xB2sBB
|
|
1146
|
-
// with \u03B4 = quantised endpoint \u2212 base endpoint, compared as the delta
|
|
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
1167
|
// \u2022 3-bit indices are packed as FLOAT: each group of 8 pixels'
|
|
1156
1168
|
// levels accumulates as \u03A3 L\xB78^k in f32 (\u2264 2^24 \u2212 1, exact) \u2014 one fma
|
|
1157
1169
|
// per texel \u2014 and the level \u2192 BC4 index map (0\u21920, 7\u21921, L\u2192L+1) is
|
|
1158
1170
|
// applied to the whole 24-bit word with SWAR bit tricks.
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1171
|
+
// The moment/float-packing structure measured \u221222% GPU vs the kernel
|
|
1172
|
+
// before it (rg8 source; 2K/4K normal maps sit at the read floor on rgba8
|
|
1173
|
+
// either way) \u2014 the 1K case is ALU-bound, not read-bound (/eval 2026-09).
|
|
1174
|
+
// The inset seed + regression refit + offset round then lowered MSE by
|
|
1175
|
+
// 3.8\u20139% on every real texture (normals 3.8\u20136.4%, displacement 9\u201360%,
|
|
1176
|
+
// hardware decode) at equal GPU time (/eval 2026-09-24).
|
|
1162
1177
|
//
|
|
1163
1178
|
// The host selects this module only when the device reports shader-f16,
|
|
1164
1179
|
// falling back to bc5.wgsl otherwise.
|
|
@@ -1229,26 +1244,28 @@ fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
|
1229
1244
|
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
1245
|
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)));
|
|
1231
1246
|
|
|
1232
|
-
// Seed endpoints at the exact per-channel extremes
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1247
|
+
// Seed endpoints at the exact per-channel extremes; spans \u2264 7 (incl.
|
|
1248
|
+
// flat blocks) seed a 7-wide window instead, whose levels land on every
|
|
1249
|
+
// integer the block holds \u2014 lossless, and the refit keeps it.
|
|
1250
|
+
let small = vmax - vmin <= vec2<h>(7.0);
|
|
1251
|
+
let r1 = vec2<u32>(select(vmin, min(vmin, vec2<h>(248.0)), small));
|
|
1252
|
+
let r0 = select(vec2<u32>(vmax), r1 + 7u, small);
|
|
1238
1253
|
|
|
1239
1254
|
let r0h = vec2<h>(vec2<f32>(r0));
|
|
1240
1255
|
let dirf = vec2<f32>(r1) - vec2<f32>(r0);
|
|
1241
|
-
|
|
1256
|
+
// Pass-1 levels come from the seed range INSET by ~5.5/256 of the span
|
|
1257
|
+
// on both ends (see header).
|
|
1258
|
+
let scale = vec2<h>(vec2<f32>(7.3125) / dirf);
|
|
1242
1259
|
|
|
1243
|
-
// Pass 1 \u2014 t = d\xB7scale \u2208 [0,7] by construction (seed covers the
|
|
1244
|
-
// so L = floor(t +
|
|
1260
|
+
// Pass 1 \u2014 t = d\xB7scale \u2208 [0,7.3125] by construction (seed covers the
|
|
1261
|
+
// data), so L = floor(t + 11/32) \u2208 [0,7] needs no clamp.
|
|
1245
1262
|
var sLr = h(0.0); var sLLr = h(0.0); var sdr = 0.0; var sLdr = 0.0;
|
|
1246
1263
|
var sLg = h(0.0); var sLLg = h(0.0); var sdg = 0.0; var sLdg = 0.0;
|
|
1247
1264
|
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1248
1265
|
let dr = vr[q] - r0h.x;
|
|
1249
1266
|
let dg = vg[q] - r0h.y;
|
|
1250
|
-
let Lr = floor(dr * scale.x + h(0.
|
|
1251
|
-
let Lg = floor(dg * scale.y + h(0.
|
|
1267
|
+
let Lr = floor(dr * scale.x + h(0.34375));
|
|
1268
|
+
let Lg = floor(dg * scale.y + h(0.34375));
|
|
1252
1269
|
sLr = sLr + dot(Lr, h4(1.0)); sLLr = sLLr + dot(Lr, Lr);
|
|
1253
1270
|
sLg = sLg + dot(Lg, h4(1.0)); sLLg = sLLg + dot(Lg, Lg);
|
|
1254
1271
|
sdr = sdr + f32(dot(dr, h4(1.0)));
|
|
@@ -1257,32 +1274,18 @@ fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
|
1257
1274
|
sLdg = sLdg + dot(vec4<f32>(Lg), vec4<f32>(dg));
|
|
1258
1275
|
}
|
|
1259
1276
|
|
|
1260
|
-
// Per-block refit in f32
|
|
1277
|
+
// Per-block refit in f32: least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through
|
|
1278
|
+
// the pass-1 levels, straight off the (exact-integer) moments. den = 0
|
|
1279
|
+
// \u27FA every pixel on one level (rank-deficient) \u2014 keep the seed then.
|
|
1261
1280
|
let sLf = vec2<f32>(f32(sLr), f32(sLg));
|
|
1262
1281
|
let sLLf = vec2<f32>(f32(sLLr), f32(sLLg));
|
|
1263
|
-
let
|
|
1264
|
-
let
|
|
1265
|
-
let
|
|
1266
|
-
let
|
|
1267
|
-
let
|
|
1268
|
-
let
|
|
1269
|
-
let
|
|
1270
|
-
let sAR = (pR - pLR * (1.0 / 7.0)) * dirf * (1.0 / 7.0);
|
|
1271
|
-
let spread = 16.0 * sLLf != sLf * sLf;
|
|
1272
|
-
|
|
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));
|
|
1282
|
+
let sdf = vec2<f32>(sdr, sdg);
|
|
1283
|
+
let den = 16.0 * sLLf - sLf * sLf;
|
|
1284
|
+
let beta = (16.0 * vec2<f32>(sLdr, sLdg) - sLf * sdf) / den;
|
|
1285
|
+
let e0 = vec2<f32>(r0) + (sdf - beta * sLf) * (1.0 / 16.0);
|
|
1286
|
+
let q0f = floor(clamp(e0, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);
|
|
1287
|
+
let q1f = floor(clamp(e0 + 7.0 * beta, vec2<f32>(0.0), vec2<f32>(255.0)) + 0.5);
|
|
1288
|
+
let acc = (den > vec2<f32>(0.0)) & (q0f > q1f);
|
|
1286
1289
|
let n0 = select(r0, vec2<u32>(q0f), acc);
|
|
1287
1290
|
let n1 = select(r1, vec2<u32>(q1f), acc);
|
|
1288
1291
|
|
|
@@ -1291,24 +1294,34 @@ fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
|
1291
1294
|
// (\u03A3 L\xB78^k \u2264 2^24 \u2212 1, exact): iA = pixels 0..7, iB = pixels 8..15.
|
|
1292
1295
|
let n0h = vec2<h>(vec2<f32>(n0));
|
|
1293
1296
|
let sc2 = vec2<h>(vec2<f32>(7.0) / (vec2<f32>(n1) - vec2<f32>(n0)));
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1297
|
+
var pk = vec4<f32>(0.0); // (Ax, Bx, Ay, By) level words
|
|
1298
|
+
var sLq = vec2<h>(0.0);
|
|
1299
|
+
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1300
|
+
let Lr = clamp(floor((vr[q] - n0h.x) * sc2.x + h(0.5)), h4(0.0), h4(7.0));
|
|
1301
|
+
let Lg = clamp(floor((vg[q] - n0h.y) * sc2.y + h(0.5)), h4(0.0), h4(7.0));
|
|
1302
|
+
let w = select(W0, W1, (q & 1u) == 1u);
|
|
1303
|
+
let hi = q >= 2u;
|
|
1304
|
+
let pr = dot(vec4<f32>(Lr), w);
|
|
1305
|
+
let pg = dot(vec4<f32>(Lg), w);
|
|
1306
|
+
pk = pk + vec4<f32>(select(pr, 0.0, hi), select(0.0, pr, hi), select(pg, 0.0, hi), select(0.0, pg, hi));
|
|
1307
|
+
sLq = sLq + vec2<h>(dot(Lr, h4(1.0)), dot(Lg, h4(1.0)));
|
|
1308
|
+
}
|
|
1309
|
+
let n0f = vec2<f32>(n0);
|
|
1310
|
+
let sL2 = vec2<f32>(sLq);
|
|
1311
|
+
let res = sdf + 16.0 * (vec2<f32>(r0) - n0f) - (vec2<f32>(n1) - n0f) * sL2 * (1.0 / 7.0);
|
|
1312
|
+
let sh = clamp(floor(res * (1.0 / 16.0) + 0.5), -vec2<f32>(n1), vec2<f32>(255.0) - n0f);
|
|
1313
|
+
let m0 = vec2<u32>(n0f + sh);
|
|
1314
|
+
let m1 = vec2<u32>(vec2<f32>(n1) + sh);
|
|
1315
|
+
let iAx = lvl_to_idx(u32(pk.x));
|
|
1316
|
+
let iBx = lvl_to_idx(u32(pk.y));
|
|
1317
|
+
let iAy = lvl_to_idx(u32(pk.z));
|
|
1318
|
+
let iBy = lvl_to_idx(u32(pk.w));
|
|
1306
1319
|
|
|
1307
1320
|
// BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.
|
|
1308
1321
|
let o = bi * 4u;
|
|
1309
|
-
dst[o] =
|
|
1322
|
+
dst[o] = m0.x | (m1.x << 8u) | (iAx << 16u);
|
|
1310
1323
|
dst[o + 1u] = (iAx >> 16u) | (iBx << 8u);
|
|
1311
|
-
dst[o + 2u] =
|
|
1324
|
+
dst[o + 2u] = m0.y | (m1.y << 8u) | (iAy << 16u);
|
|
1312
1325
|
dst[o + 3u] = (iAy >> 16u) | (iBy << 8u);
|
|
1313
1326
|
}
|
|
1314
1327
|
`;
|
|
@@ -2186,7 +2199,7 @@ var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
2186
2199
|
};
|
|
2187
2200
|
|
|
2188
2201
|
// src/webgl/glsl/bc5.frag.glsl
|
|
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
|
|
2202
|
+
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 (spans \u2264 7: a 7-wide\n// window, lossless); pass-1 levels come from the seed range inset by\n// ~5.5/256 of the span (scale \xD77.3125/7, offset 11/32), accumulating\n// the MOMENTS \u03A3L, \u03A3L\xB2, \u03A3d, \u03A3L\xB7d (d = v \u2212 r0; the seed covers the\n// data, so no clamp);\n// \u2022 one least-squares line through the pass-1 levels straight off the\n// moments (den = 16\u03A3L\xB2 \u2212 (\u03A3L)\xB2 = 0 keeps the seed);\n// \u2022 pass 2 derives the shipped levels against the refit endpoints,\n// packed as float fields \u03A3 L\xB78^k (exact below 2^24), then one SWAR\n// level \u2192 BC4 index map (0\u21920, 7\u21921, L\u2192L+1) per 24-bit word;\n// \u2022 offset round: both endpoints shift by the rounded mean residual of\n// the shipped levels (never worse on those indices).\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\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 per-channel extremes (round-to-nearest); spans\n // \u2264 7 (incl. flat blocks) seed a 7-wide window instead \u2014 lossless.\n vec2 vhi = clamp(floor(vmax + 0.5), vec2(0.0), vec2(255.0));\n vec2 vlo = clamp(floor(vmin + 0.5), vec2(0.0), vec2(255.0));\n bvec2 small = lessThanEqual(vhi - vlo, vec2(7.0));\n vec2 r1f = vec2(small.x ? min(vlo.x, 248.0) : vlo.x, small.y ? min(vlo.y, 248.0) : vlo.y);\n vec2 r0f = vec2(small.x ? r1f.x + 7.0 : vhi.x, small.y ? r1f.y + 7.0 : vhi.y);\n // Pass-1 levels from the seed range inset by ~5.5/256 of the span.\n vec2 scale = vec2(7.3125) / (r1f - 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.34375);\n vec4 Lg = floor(dg * scale.y + 0.34375);\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 // Refit: least-squares line v \u2248 r0 + \u03B1 + \u03B2\xB7L through the pass-1 levels.\n // Endpoints clamp to [0,255], NOT the block's value range.\n vec2 den = 16.0 * sLL - sL * sL;\n vec2 beta = (16.0 * sLd - sL * sd) / den;\n vec2 e0 = r0f + (sd - beta * sL) * (1.0 / 16.0);\n vec2 q0f = floor(clamp(e0, vec2(0.0), vec2(255.0)) + 0.5);\n vec2 q1f = floor(clamp(e0 + 7.0 * beta, vec2(0.0), vec2(255.0)) + 0.5);\n bvec2 acc = bvec2(den.x > 0.0 && q0f.x > q1f.x, den.y > 0.0 && q0f.y > q1f.y);\n vec2 n0f = vec2(acc.x ? q0f.x : r0f.x, acc.y ? q0f.y : r0f.y);\n vec2 n1f = vec2(acc.x ? q1f.x : r1f.x, acc.y ? q1f.y : r1f.y);\n\n // Pass 2 \u2014 levels against the refit endpoints, packed as \u03A3 L\xB78^k\n // (A = pixels 0..7, B = pixels 8..15), plus \u03A3L for the offset round.\n vec2 sc2 = vec2(7.0) / (n1f - n0f);\n vec4 pk = vec4(0.0); // (Ax, Bx, Ay, By)\n vec2 sL2 = vec2(0.0);\n for (int q = 0; q < 4; q++) {\n vec4 Lr = clamp(floor((vr[q] - n0f.x) * sc2.x + 0.5), vec4(0.0), vec4(7.0));\n vec4 Lg = clamp(floor((vg[q] - n0f.y) * sc2.y + 0.5), vec4(0.0), vec4(7.0));\n vec4 w = (q & 1) == 1 ? W1 : W0;\n bool hi = q >= 2;\n float pr = dot(Lr, w);\n float pg = dot(Lg, w);\n pk += vec4(hi ? 0.0 : pr, hi ? pr : 0.0, hi ? 0.0 : pg, hi ? pg : 0.0);\n sL2 += vec2(dot(Lr, vec4(1.0)), dot(Lg, vec4(1.0)));\n }\n\n // Offset round: shift both endpoints by the rounded mean residual of the\n // shipped levels.\n vec2 res = sd + 16.0 * (r0f - n0f) - (n1f - n0f) * sL2 * (1.0 / 7.0);\n vec2 sh = clamp(floor(res * (1.0 / 16.0) + 0.5), -n1f, vec2(255.0) - n0f);\n uvec2 m0 = uvec2(n0f + sh);\n uvec2 m1 = uvec2(n1f + sh);\n uint iAx = lvlToIdx(uint(pk.x));\n uint iBx = lvlToIdx(uint(pk.y));\n uint iAy = lvlToIdx(uint(pk.z));\n uint iBy = lvlToIdx(uint(pk.w));\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n outColor = uvec4(\n m0.x | (m1.x << 8u) | (iAx << 16u),\n (iAx >> 16u) | (iBx << 8u),\n m0.y | (m1.y << 8u) | (iAy << 16u),\n (iAy >> 16u) | (iBy << 8u)\n );\n}\n";
|
|
2190
2203
|
|
|
2191
2204
|
// src/webgl/BC5WebGLEncoder.ts
|
|
2192
2205
|
var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|