gputex 0.7.0 → 0.8.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 +24 -19
- package/dist/index.js +2 -2
- package/dist/testing.js +4 -2
- package/dist/three.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,17 +103,19 @@ algebra of that scalar shift — table and index selection depend only on each
|
|
|
103
103
|
texel's luma-sum difference from the base, exactly (modulo decode clamping) —
|
|
104
104
|
so the whole 8-table × 4-modifier search collapses to a handful of scalar
|
|
105
105
|
threshold tests against a two-candidate table shortlist, with subblock error
|
|
106
|
-
constants and the flip preselect computed O(1) from quadrant sums
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
constants and the flip preselect computed O(1) from quadrant sums (exactly
|
|
107
|
+
gray blocks, which give the preselect nothing to go on, score both flips on
|
|
108
|
+
a one-channel path). A closed-form least-squares fit of ETC2's planar mode
|
|
109
|
+
(which rescues the smooth gradients ETC1-style blocks band on) completes
|
|
110
|
+
the block, driven by the same estimates. There is no base-colour refit
|
|
111
|
+
(~0.2 dB on photographic content for ≥13% GPU). The kernel reads the source
|
|
112
|
+
through `textureGather` and keeps every per-texel quantity in registers
|
|
113
|
+
with constant indexing (numbers below). Its f16 module is EXACT-VALUE: lumas,
|
|
114
|
+
D values and thresholds are integers (or halves) f16 represents exactly,
|
|
115
|
+
while the sums and estimates stay f32 (they overflow f16), so the two
|
|
116
|
+
modules produce byte-identical output wherever the sampler's unorm
|
|
117
|
+
conversion is exact (verified on Apple) — f16 buys register space, not
|
|
118
|
+
different results.
|
|
117
119
|
|
|
118
120
|
On the repo's test textures this lands within a few tenths of a dB of the
|
|
119
121
|
per-block CPU reference encoders (`gputex/testing`) and above them on
|
|
@@ -353,7 +355,8 @@ shader alone.
|
|
|
353
355
|
| BC7 | f32 | 0.52 ms |
|
|
354
356
|
| ASTC 4×4 | f16 (default) | **0.17 ms** |
|
|
355
357
|
| ASTC 4×4 | f32 | 0.28 ms |
|
|
356
|
-
| ETC2 | f16
|
|
358
|
+
| ETC2 | f16 (default) | **0.14 ms** |
|
|
359
|
+
| ETC2 | f32 | 0.15 ms |
|
|
357
360
|
|
|
358
361
|
End-to-end `encodeToBytes()` wall time adds the upload and the readback.
|
|
359
362
|
Each encoder caches its GPU resources (source texture, output/staging
|
|
@@ -366,13 +369,15 @@ readback this cuts wall time by 23–33% at 4096² and 5–25% at 2048²
|
|
|
366
369
|
(bytes identical); a fresh 4096² encode is then dominated by the ~8 ms
|
|
367
370
|
`copyExternalImageToTexture` upload.
|
|
368
371
|
|
|
369
|
-
On a 100 GB/s part just reading the 2048² RGBA8 source costs ~0.
|
|
370
|
-
BC5/BC7/ASTC
|
|
371
|
-
refit rounds keep it ALU-bound.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
372
|
+
On a 100 GB/s part just reading the 2048² RGBA8 source costs ~0.14 ms, so
|
|
373
|
+
BC5/BC7/ASTC sit within ~1.3× of simply touching the bytes and ETC2 at it;
|
|
374
|
+
BC1's refit rounds keep it ALU-bound. On real textures (which Apple's
|
|
375
|
+
lossless framebuffer compression makes cheaper to read) and at 1024², where
|
|
376
|
+
the source stays cached, the ETC2 kernel is ALU-exposed again: 0.035–0.04 ms
|
|
377
|
+
at 1024², 0.13–0.15 ms at 2048² and 0.49–0.57 ms at 4096² across the corpus.
|
|
378
|
+
A two-pass 2 B/px prepared-source ETC2 variant lives in git history and was
|
|
379
|
+
not shipped: its prep pass is also bandwidth-bound and cannot overlap, so
|
|
380
|
+
the per-texture total regressed.
|
|
376
381
|
|
|
377
382
|
Single-dispatch timestamps are coarse and Apple GPU clock states swing
|
|
378
383
|
timings by up to ~2× across page loads, so compare variants only within a
|
package/dist/index.js
CHANGED
|
@@ -1852,10 +1852,10 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
1852
1852
|
};
|
|
1853
1853
|
|
|
1854
1854
|
// 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. The f16 module (etc2_fast_f16.wgsl) is an\n// EXACT-VALUE port \u2014 byte-identical output; see its header.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\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: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 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 ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\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\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\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\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\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\nfn sb_table_score(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let thr = THR[t];\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = select(a3, b3, ad > thr);\n acc = acc + m3 * (m3 - 2.0 * ad);\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32) -> SearchOut {\n var mx = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let cover = min(\n u32(mx > 24.0) + u32(mx > 51.0) + u32(mx > 87.0) + u32(mx > 126.0) +\n u32(mx > 180.0) + u32(mx > 240.0) + u32(mx > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\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 base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f32, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, out.lb0);\n let s1 = sb_search(luma, flip, 1u, out.lb1);\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> u32 {\n let thr = THR[t];\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\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 let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var luma: array<f32, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = l;\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, sel.lb0, t0);\n let fit1 = sb_indices(&luma, bflip, 1u, sel.lb1, t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[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 = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\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 lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = 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";
|
|
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";
|
|
1856
1856
|
|
|
1857
1857
|
// 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 that f16 module.\n//\n// EXACT-VALUE f16: unlike the other formats' f16 fast paths (which accept\n// float rounding in a [0,1] domain), every f16 value in this shader is an\n// integer that f16 represents exactly \u2014 lumas and bases (<= 765), D values\n// (|D| <= 765) and thresholds (<= 549) all sit below f16's 2048 integer-\n// exactness limit. Sums of squares, scores and estimates stay f32 (they\n// reach +-5e5..9e6, far past f16's 65504 max). The output is therefore\n// BYTE-IDENTICAL to the f32 module \u2014 verified per-block on the suite\n// textures \u2014 and the two modules share every pin and every test gate.\n//\n// What f16 buys here is register pressure (the luma array halves), not\n// arithmetic rate: on Apple/metal-3 the two modules measure identical\n// (the shader is DRAM-read-bound), but on the mobile GPUs where ETC2 is\n// actually the target format, occupancy from smaller registers is the\n// cheapest speed there is. The COLOUR accumulators deliberately stay f32\n// even though quadrant/pair sums (<= 2040) would be exact in f16: porting\n// them measured 15% SLOWER on Apple (conversion traffic outweighs the\n// register saving). Luma + the table search are the f16 surface.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\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: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 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 ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\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\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\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\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\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// D-domain values (|D| <= 765, thresholds <= 549) are exact in f16; the\n// score PRODUCTS reach +-5e5 and must be f32.\nfn sb_table_score(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> f32 {\n let a3 = f16(A3[t]);\n let b3 = f16(B3[t]);\n let thr = f16(THR[t]);\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = f32(select(a3, b3, ad > thr));\n acc = acc + m3 * (m3 - 2.0 * f32(ad));\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16) -> SearchOut {\n var mx: f16 = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let mxf = f32(mx);\n let cover = min(\n u32(mxf > 24.0) + u32(mxf > 51.0) + u32(mxf > 87.0) + u32(mxf > 126.0) +\n u32(mxf > 180.0) + u32(mxf > 240.0) + u32(mxf > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\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 base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f16, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, f16(out.lb0));\n let s1 = sb_search(luma, flip, 1u, f16(out.lb1));\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> u32 {\n let thr = f16(THR[t]);\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\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 let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Luma lives in f16: every value is an integer <= 765, exact in f16.\n var luma: array<f16, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = f16(l);\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, f16(sel.lb0), t0);\n let fit1 = sb_indices(&luma, bflip, 1u, f16(sel.lb1), t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[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 = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\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 lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = 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";
|
|
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";
|
|
1859
1859
|
|
|
1860
1860
|
// src/ETC2Encoder.ts
|
|
1861
1861
|
var ETC2Encoder = class extends Encoder {
|
package/dist/testing.js
CHANGED
|
@@ -1610,6 +1610,7 @@ var A3 = MODIFIERS.map((m) => 3 * m[0]);
|
|
|
1610
1610
|
var B3 = MODIFIERS.map((m) => 3 * m[1]);
|
|
1611
1611
|
var THR3 = MODIFIERS.map((m) => 1.5 * (m[0] + m[1]));
|
|
1612
1612
|
var PLANAR_FUDGE = 8;
|
|
1613
|
+
var KAPPA = 0.9;
|
|
1613
1614
|
function fastTableScore(D, t) {
|
|
1614
1615
|
let acc = 0;
|
|
1615
1616
|
for (let i = 0; i < D.length; i++) {
|
|
@@ -1699,13 +1700,14 @@ function encodeFastBlock(px) {
|
|
|
1699
1700
|
const residual = (s) => {
|
|
1700
1701
|
const dotSum = s.sum[0] * s.sum[0] + s.sum[1] * s.sum[1] + s.sum[2] * s.sum[2];
|
|
1701
1702
|
const lsum = s.sum[0] + s.sum[1] + s.sum[2];
|
|
1702
|
-
return s.sq - dotSum / 8 - (s.lsq - lsum * lsum / 8) / 3;
|
|
1703
|
+
return s.sq - dotSum / 8 - KAPPA * (s.lsq - lsum * lsum / 8) / 3;
|
|
1703
1704
|
};
|
|
1704
1705
|
const resA = residual(pair(0, 2)) + residual(pair(1, 3));
|
|
1705
1706
|
const resB = residual(pair(0, 1)) + residual(pair(2, 3));
|
|
1707
|
+
const gray = qsum.every((q) => q[0] === q[1] && q[1] === q[2]);
|
|
1706
1708
|
let flip;
|
|
1707
1709
|
let cur;
|
|
1708
|
-
if (
|
|
1710
|
+
if (gray) {
|
|
1709
1711
|
const fa = evalFlip(0, pair(0, 2), pair(1, 3));
|
|
1710
1712
|
const fb = evalFlip(1, pair(0, 1), pair(2, 3));
|
|
1711
1713
|
flip = fb.est < fa.est ? 1 : 0;
|
package/dist/three.js
CHANGED
|
@@ -1852,10 +1852,10 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
1852
1852
|
};
|
|
1853
1853
|
|
|
1854
1854
|
// 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. The f16 module (etc2_fast_f16.wgsl) is an\n// EXACT-VALUE port \u2014 byte-identical output; see its header.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\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: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 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 ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\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\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\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\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\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\nfn sb_table_score(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let thr = THR[t];\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = select(a3, b3, ad > thr);\n acc = acc + m3 * (m3 - 2.0 * ad);\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32) -> SearchOut {\n var mx = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let cover = min(\n u32(mx > 24.0) + u32(mx > 51.0) + u32(mx > 87.0) + u32(mx > 126.0) +\n u32(mx > 180.0) + u32(mx > 240.0) + u32(mx > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\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 base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f32, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, out.lb0);\n let s1 = sb_search(luma, flip, 1u, out.lb1);\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> u32 {\n let thr = THR[t];\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\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 let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var luma: array<f32, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = l;\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, sel.lb0, t0);\n let fit1 = sb_indices(&luma, bflip, 1u, sel.lb1, t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[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 = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\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 lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = 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";
|
|
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";
|
|
1856
1856
|
|
|
1857
1857
|
// 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 that f16 module.\n//\n// EXACT-VALUE f16: unlike the other formats' f16 fast paths (which accept\n// float rounding in a [0,1] domain), every f16 value in this shader is an\n// integer that f16 represents exactly \u2014 lumas and bases (<= 765), D values\n// (|D| <= 765) and thresholds (<= 549) all sit below f16's 2048 integer-\n// exactness limit. Sums of squares, scores and estimates stay f32 (they\n// reach +-5e5..9e6, far past f16's 65504 max). The output is therefore\n// BYTE-IDENTICAL to the f32 module \u2014 verified per-block on the suite\n// textures \u2014 and the two modules share every pin and every test gate.\n//\n// What f16 buys here is register pressure (the luma array halves), not\n// arithmetic rate: on Apple/metal-3 the two modules measure identical\n// (the shader is DRAM-read-bound), but on the mobile GPUs where ETC2 is\n// actually the target format, occupancy from smaller registers is the\n// cheapest speed there is. The COLOUR accumulators deliberately stay f32\n// even though quadrant/pair sums (<= 2040) would be exact in f16: porting\n// them measured 15% SLOWER on Apple (conversion traffic outweighs the\n// register saving). Luma + the table search are the f16 surface.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\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: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 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 ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\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\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\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\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\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// D-domain values (|D| <= 765, thresholds <= 549) are exact in f16; the\n// score PRODUCTS reach +-5e5 and must be f32.\nfn sb_table_score(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> f32 {\n let a3 = f16(A3[t]);\n let b3 = f16(B3[t]);\n let thr = f16(THR[t]);\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = f32(select(a3, b3, ad > thr));\n acc = acc + m3 * (m3 - 2.0 * f32(ad));\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16) -> SearchOut {\n var mx: f16 = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let mxf = f32(mx);\n let cover = min(\n u32(mxf > 24.0) + u32(mxf > 51.0) + u32(mxf > 87.0) + u32(mxf > 126.0) +\n u32(mxf > 180.0) + u32(mxf > 240.0) + u32(mxf > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\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 base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f16, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, f16(out.lb0));\n let s1 = sb_search(luma, flip, 1u, f16(out.lb1));\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> u32 {\n let thr = f16(THR[t]);\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\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 let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Luma lives in f16: every value is an integer <= 765, exact in f16.\n var luma: array<f16, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = f16(l);\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, f16(sel.lb0), t0);\n let fit1 = sb_indices(&luma, bflip, 1u, f16(sel.lb1), t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[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 = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\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 lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = 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";
|
|
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";
|
|
1859
1859
|
|
|
1860
1860
|
// src/ETC2Encoder.ts
|
|
1861
1861
|
var ETC2Encoder = class extends Encoder {
|