Repository navigation
perf: hidden shader costs — ETC2 −8%, BC1 −9%, BC5 −5%, ASTC f32 −10% - #17
Merged
Merged
Conversation
…te-identical The kernel is ALU-bound at 1K: replacing the gathers with synthetic data takes the same time as the real kernel, so every op counts. The biggest wins were costs WGSL→Metal adds that the source doesn't show: - u32(f32) compiles to a saturating conversion (compares + selects). Packing's 15 now use bitcast(x + 2^23) on exact integers (~−2.5%). - The edge-block loop wrote col[x][y]/qsum[q] with runtime indices, costing ~2% on every block even where it never ran. Edge blocks now build the same quads from clamped loads. - Runtime-indexed const-array lookups each pay a bounds clamp: one vec4 table per score instead of four scalar arrays (~−1%). - Table score: min(0,x) = (x − |x|)/2 with |·| as a free source modifier; exact integers, identical results (−2%, gray −3%). - Smaller: expand codes once, branch-free ETC1 header, streamlined planar fix bits, 8-select flip arrangement, gather constant offsets, hand-unrolled index loop, cheaper bswap + vec2<u32> store. /eval (M3, default corpus): f16 ×0.918, f32 ×0.917; 100% byte-identical to main on all 14 images; f16 ≡ f32; WebGL ≡ main. Only edge blocks of non-multiple-of-4 images may move (they now round like interior blocks): 6 blocks of a 250² colour-card crop, +0.02% SSE; /test odd-size check Δ 0.00 dB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvMZvPkPRPM8oZMVs44qB7
Applies the ETC2 findings (saturating u32(f32) conversions, runtime- indexed loops) to the other encoders: - BC1: projection index and to565 via the 2^23 trick, min not clamp in the load, projection loop unrolled via a project() helper. f16 ×0.907, f32 ×0.923. The unroll lets fast-math reorder the sums: 0.1–0.7% of blocks move, ΔPSNR +0.0003 dB mean / −0.0005 worst, no easy-block regressions. The GLSL port got the same unroll: WebGL still matches the f32 WGSL module byte for byte. - BC5: endpoint conversions via the trick, unrolled quad loads. f16 ×0.951, f32 ×0.982, byte-identical. - ASTC: per-texel weight and endpoint conversions via the trick; the f32 module's to8() did four per texel in the load. f16 ×0.978, f32 ×0.905 (gray maps −15%), byte-identical. - BC7 f32: gray-path per-texel conversions only (gray maps −5%, colour neutral), byte-identical. BC7 f16 unchanged: nothing measured > 0.6% without a quality cost. /test 0 failures, bun test 225 pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvMZvPkPRPM8oZMVs44qB7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every encoder except BC7 f16 is faster on the GPU. ETC2, BC5, BC7 and ASTC output is byte-identical to main. BC1 moves 0.1–0.7% of blocks with no measurable quality change.
Why
Replacing the ETC2 texture reads with synthetic data took the same time as the real kernel. The encoders are limited by arithmetic, not memory reads, so every instruction counts. Most of the gain came from costs that WGSL-to-Metal compilation adds without showing them in the source:
u32(f32)andi32(f32)compile to a clamped conversion with extra compares and selects. Where values are exact whole numbers in [0, 2²³),bitcast(x + 2²³)reads them out in one or two ops. The worst cases were ASTC f32'sto8(), with four conversions per texel in the load loop, and one per texel in the BC1, ASTC and BC7-gray index loops.col[x][y]andqsum[q]with runtime indices. That cost about 2% on every block, even on 1K images where it never runs.Results
Measured on /eval (M3, hardware-decoded quality, interleaved timing, default corpus per format):
Per format
min(0,x) = (x − |x|)/2, which is exact with a free|·|modifier (−2%, gray −3%). Smaller cuts: codes expanded once, branch-free ETC1 header, simpler planar fix bits, 8-select flip arrangement, constant gather offsets, index loop written out by hand, cheaper byte swap and a vec2 store.to565via the bitcast trick,mininstead ofclampin the load, and the projection loop unrolled through aproject()helper (−3.5%). The unroll lets fast-math reorder the sums, which is where the moved blocks come from.to8()in the f32 module.Edge blocks
In ETC2, blocks on the edge of images that aren't a multiple of 4 now round their sums the same way interior blocks do. In a 250×250 colour-card crop, 6 blocks moved and total error rose by 0.02%. The /test odd-size check still matches the CPU reference (Δ 0.00 dB).
Tried and rejected
🤖 Generated with Claude Code
https://claude.ai/code/session_01CvMZvPkPRPM8oZMVs44qB7