Skip to content

perf: BC5 −22%, BC7 lossless gray tail, ETC2 −5%; WebGL parity + ETC2 WebGL encoder - #12

Merged
verekia merged 4 commits into
mainfrom
perf/bc5-etc2-bc7
Sep 24, 2026
Merged

verekia merged 4 commits into
mainfrom
perf/bc5-etc2-bc7

Conversation

@verekia

@verekia verekia commented Sep 24, 2026

Copy link
Copy Markdown
Owner

WebGPU encoders

  • BC5 −22% GPU (f16; f32 −20%, rgba8 source −17%), quality unchanged. Pass-1 moments without clamp, branch-free vec2 refit, levels packed as float fields with a SWAR level→index map. 1K encodes were ALU-bound; 2K/4K normal maps now sit at the read floor.
  • BC7 gray maps: RGBA +0.8 to +19 dB (e.g. rock displacement 4K 57.1 → 76.4 dB; RGB alone +0.2..0.65), GPU neutral. Gray + opaque blocks take an integer tail: lossless for spans ≤ 15 with odd endpoints so alpha decodes to exactly 255 (it was 254 on up to half the texels), alpha-aware LSQ refit above. Moving the covariance out of the load loop also made the f32 fallback ~30% faster on colour content.
  • ETC2 −5% geomean (−15..17% on smooth maps at 2K/4K), byte-identical except 47 blocks from a stricter gray test (CPU mirror updated). Gray-only 2-channel planar, subblock-pair search that skips the redundant lower-neighbour score on smooth content, precomputed table constants. The kernel is ALU-bound everywhere; larger gains need quality trades (not taken).

WebGL path

  • All GLSL encoders (BC1, BC5, BC7, ASTC) are now line-for-line ports of the WGSL f32 modules and produce byte-identical blocks — they had drifted (BC1 solid-colour/moments, ASTC QUANT_192 bit budgets, BC7 packing were never ported). WebGL quality gains up to +8.5 dB (BC7 gray), +1.3 dB (BC1 normal), +1.2 dB (ASTC wood); 4K wall time −8..34% on BC5/BC7/ASTC.
  • New ETC2WebGLEncoder (byte-identical to WebGPU), etc capability (WEBGL_compressed_texture_etc), and WebGL format selection mirroring WebGPU (ETC2 as the quality: 'low' pick without s3tc and the final opaque fallback).
  • WebGL host: cached source/target textures + ImageBitmap upload memo (fresh −24..30%, repeat −67% at 4K); compressTexture() keeps one WebGL encoder per format across calls instead of recompiling each time.

API / tooling

  • compressTexture({ forceWebGL }) / loader.forceWebGL to test the fallback on WebGPU browsers; transcode-cache keys now include the backend.
  • Example app: ?forcewebgl=1 on any page encodes and renders through WebGL2 (nav toggle included).
  • /eval: src=rgba8 option and worst-easy-block details.

Validation

  • GPU suite /test: 0 failures (BC7 gray pins raised to the new baselines).
  • bun test: 222 pass. Format, lint, typecheck, warden clean.
  • WebGL vs WebGPU f32 byte identity checked on the texture corpus, odd sizes and flipY for all five formats.

🤖 Generated with Claude Code

verekia and others added 4 commits September 24, 2026 02:00
BC5 (both modules): texels as quad-major vec4s, pass-1 moments ΣL, ΣL²,
Σd, ΣL·d with no clamp (the seed covers the data), branch-free vec2
refit, and pass-2 levels packed as float Σ L·8^k with a SWAR level →
index map. GPU −22% f16 / −20% f32 (rg8 source), −17% on rgba8; quality
unchanged.

BC7 (both modules): gray + opaque blocks take an integer 1-D tail —
lossless for spans ≤ 15 with odd endpoints so alpha decodes to exactly
255, closed-form scalar LSQ refit with alpha-aware p-bit pricing above.
RGBA PSNR on gray maps +0.8..+19 dB (RGB alone +0.2..0.65), GPU neutral:
covariance moved out of the load loop onto the colour path (f32 module
−30% on colour content), float-nibble index packing.

ETC2 (both modules): gray-only two-channel planar solve, subblock pairs
searched together with the lower-neighbour scores skipped when both
covers are table 0, precomputed table-score constants. GPU −5% f16 / −7%
f32 geomean, −15..17% on smooth maps at 2K/4K. Gray test now also
requires equal planar moments (CPU mirror updated); otherwise
byte-identical.

/eval: src=rgba8 option and worst easy-block details. Suite gray BC7
pins raised.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GLSL fragment encoders had drifted behind the WGSL f32 modules. Each
is now a line-for-line port and produces byte-identical blocks to the
WebGPU f32 shader (verified on the texture corpus, all four formats):

- BC1: solid-colour path for near-flat blocks, moment-form projection
  passes, looped accept-if-better refits (normal card +1.3 dB,
  displacement +0.4 dB)
- BC5: moment-form kernel with reprojection, float-packed levels + SWAR
  index map (+0.08..0.12 dB)
- BC7: lossless gray tail with alpha-exact odd endpoints and alpha-aware
  refit (gray maps up to +8.5 dB), covariance off the load loop,
  straight-line packing, roundEven seeds
- ASTC: QUANT_192 bit budgets and 3-channel opaque path, fit-pass
  weights for translucent blocks (colour +0.06..1.2 dB)

The WebGL host now caches its source texture and RGBA32UI target across
encodes and skips re-uploading an ImageBitmap it already holds (the
upload is ~65% of a 4096² WebGL encode). Output bytes unchanged across
memo hits, flipY and size changes.

WebGL wall time vs before: 4096² ASTC −17..34%, BC5 −11%, BC7/BC1 par
to −8% (shaders); with host caching, fresh encodes −24..30% and
repeat-bitmap encodes −67% at 4096². An rg8 source for BC5 was measured
and not ported: WebGL's RG8 upload is slower than RGBA8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
compressTexture({ forceWebGL: true }) / loader.forceWebGL skips the WebGPU
tier and encodes on the WebGL2 fallback, so that path can be tested on
WebGPU-capable browsers. The transcode-cache fingerprint now includes the
backend, so forced-WebGL calls are never served WebGPU bytes.

compressTexture()'s WebGL tier now keeps one encoder per format on the
shared context across calls (it recompiled the fragment program on every
call), matching the WebGPU tier's shared encoders; released by
releaseSharedGpuResources().

Example app: ?forcewebgl=1 on any page encodes through WebGL2 (the demo via
compressTexture, the per-format pages via each *WebGLEncoder twin — ETC2
has none and says so) and renders through three's WebGL2 backend
(WebGPURenderer forceWebGL). Nav links keep the flag; a WebGL2 toggle in
the nav reloads with or without it; the panels show the backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ETC2 was WebGPU-only, so the WebGL tier could only offer BC1 at
quality: 'low' and nothing on ETC-only devices. ETC2WebGLEncoder is a
line-for-line GLSL port of etc2.wgsl (texelFetch quads in gather order,
since WebGL2 has no textureGather) and produces byte-identical blocks to
the WebGPU encoder — verified on the colour card, packed atlas, rock/wood
colour, roughness, displacement and normal maps, an odd 133×61 image and
flipY.

WebGL capabilities gain `etc` (WEBGL_compressed_texture_etc) and
selectWebGLFormat now mirrors the WebGPU rules: ETC2 RGB8 is the 'low'
pick when s3tc is absent and the final opaque-colour fallback after
BPTC, ASTC and BC1. The example's /etc2?forcewebgl=1 uses it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@verekia
verekia merged commit 0e67ad1 into main Sep 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant