1
0
Fork
You've already forked cl-uastc
0
Common Lisp UASTC texture decoder
  • Common Lisp 90.7%
  • Python 5.7%
  • HTML 2.8%
  • Gnuplot 0.8%
Find a file
Glenn Thompson e4ba07df0e Merge perf/scalar-and-simd: word-wise ASTC packer + sequential bit-readers
Incremental single-threaded gains over master (min-of-5, bit-exact, 100 tests):
 ASTC ~1.3x, RGBA8 ~12%, BC7 ~6%.
Also: all compiler style-warnings silenced, an evaluated-and-rejected sb-simd
AVX2 path documented in the README, and a full benchmark/chart/README refresh.
2026年06月02日 01:41:22 +03:00
bench bench: refresh benchmarks, charts, and README for perf/scalar-and-simd 2026年06月02日 01:38:36 +03:00
src cleanup: silence all compiler style-warnings; document scalar/single-thread rationale 2026年06月02日 01:14:34 +03:00
tests
.gitignore bench: add procedural test-image generator + content-aware scaling benchmark 2026年06月01日 20:13:19 +03:00
cl-uastc-bench.asd
cl-uastc-tests.asd
cl-uastc.asd
README.md bench: refresh benchmarks, charts, and README for perf/scalar-and-simd 2026年06月02日 01:38:36 +03:00

cl-uastc

A Common Lisp library for decoding and transcoding UASTC GPU texture blocks.

UASTC is a 19-mode, 4x4 pixel, LDR-only subset of ASTC designed for universal transcoding to GPU-native compressed formats. This library operates on raw 16-byte blocks and includes optional container parsing for KTX2 and .basis files.

Status

All transcode targets complete.

Feature Status
Bit utilities + mode detection Done
UASTC → RGBA8 (all modes) Done
UASTC → ASTC ×ばつ4 transcoding Done (lossless, pixel-exact)
UASTC → BC7 transcoding Done (byte-exact match vs basisu ref)
KTX2 container parsing Done (uncompressed)
.basis container parsing Done (UASTC mode)

The RGBA8 decode path produces pixels directly. The ASTC transcoding path produces hardware-native ASTC ×ばつ4 blocks that decode to identical pixels, with no quality loss, suitable for GPU upload on devices with ASTC hardware support.

The BC7 transcoding path produces hardware-native BC7 blocks suitable for GPU upload on all desktop GPUs (and most mobile GPUs via Vulkan). Output is byte-exact against the Basis Universal reference transcoder.

Supported Modes

All 19 UASTC modes are decoded and transcoded:

  • Modes 0, 1, 2, 5, 18 - 1-subset RGB (trit/quint/bit endpoints)
  • Modes 3, 7 - Multi-subset RGB (2 or 3 subsets, partition patterns)
  • Mode 4 - 2-subset RGB with precomputed partition patterns
  • Mode 6 - 1-subset RGB, dual-plane (component select)
  • Modes 9, 10, 11, 12, 13, 14 - RGBA modes (single/dual-plane, various quantizations)
  • Modes 15, 16, 17 - Luminance-Alpha (single/dual-plane)
  • Mode 8 - Solid color (void-extent)

Usage

(ql:quickload "cl-uastc")
;; Decode a single 16-byte UASTC block to 16 RGBA8 pixels (64 bytes)
(let ((block (make-array 16 :element-type '(unsigned-byte 8))))
 ;; ... fill block from your container parser ...
 (cl-uastc:decode-uastc-block-rgba8 block))
;; Transcode a single 16-byte UASTC block to a 16-byte ASTC 4x4 block
(cl-uastc:transcode-uastc-block-astc block)
;; Transcode a single 16-byte UASTC block to a 16-byte BC7 block
(cl-uastc:transcode-uastc-block-bc7 block)
;; Transcode/decode a whole image (raw block data, no container)
(cl-uastc:transcode-uastc-image block-data width height :target :astc) ; or :bc7, :rgba8

The input is a flat octet vector of 16-byte UASTC blocks in raster order. For RGBA8 output, the result is a flat octet vector of pixels (4 bytes per pixel) in raster order. For ASTC/BC7 output, the result is a flat octet vector of 16-byte compressed blocks in raster order.

Loading from Container Files

KTX2 and .basis containers are supported directly:

;; Load and transcode a KTX2 or .basis file (auto-detected)
(let ((file (cl-uastc:load-uastc-file #p"/path/to/texture.ktx2")))
 (format t "~Dx~D, ~D mip levels~%"
 (cl-uastc:uastc-file-width file)
 (cl-uastc:uastc-file-height file)
 (length (cl-uastc:uastc-file-levels file)))
 ;; Transcode mip 0 to BC7
 (cl-uastc:transcode-uastc-file file :target :bc7))
;; Access raw block data for a specific mip level
(multiple-value-bind (data width height)
 (cl-uastc:uastc-file-mip-data file 0)
 (cl-uastc:transcode-uastc-image data width height :target :astc))

Supported containers:

  • KTX2 - Khronos standard (.ktx2); supercompression must be 0 (uncompressed)
  • .basis - Basis Universal format; UASTC mode only (not ETC1S)

Testing

Decode correctness is verified against the Basis Universal reference transcoder using multiple test images covering 17 of 19 modes (modes 14 and 16 are extremely rare and use the same generic code path).

ASTC Transcoding

ASTC transcoding is verified by round-tripping: transcode UASTC → ASTC, decode with ARM's astcenc, then compare against our RGBA8 decode.

=== GLOBAL SUMMARY (4 test images, 61440 blocks) ===
 Mode 0: 827968 ch, mean=0.1260 max=1
 Mode 1: 20672 ch, mean=0.0440 max=1
 Mode 2: 2816 ch, mean=0.0969 max=1
 Mode 3: 3264 ch, mean=0.0643 max=1
 Mode 4: 72832 ch, mean=0.0862 max=1
 Mode 5: 200192 ch, mean=0.0901 max=1
 Mode 6: 1057856 ch, mean=0.0718 max=1
 Mode 7: 1728 ch, mean=0.0839 max=1
 Mode 8: 1920 ch, mean=0.0000 max=0
 Mode 9: 20352 ch, mean=0.0114 max=1
 Mode 10: 1004928 ch, mean=0.1273 max=1
 Mode 11: 156096 ch, mean=0.0408 max=1
 Mode 12: 83968 ch, mean=0.1760 max=1
 Mode 13: 139136 ch, mean=0.0000 max=0
 Mode 15: 2880 ch, mean=0.1240 max=1
 Mode 17: 64 ch, mean=0.1406 max=1
 Mode 18: 335488 ch, mean=0.0964 max=1

Max error ≤ 1 for all modes. The ±1 difference is integer rounding in endpoint unquantization; the transcoded ASTC blocks are bit-exact matches to basisu's reference ASTC output.

BC7 Transcoding

BC7 transcoding is verified by byte-exact comparison against Basis Universal's reference BC7 output across 36,864 blocks from 3 test images, covering all UASTC modes that appear in practice:

test_rgba.basis (256x256): 4096 blocks, 0 mismatches
test_complex_rgb.basis (512x512): 16384 blocks, 0 mismatches
test_rgba2.basis (512x512): 16384 blocks, 0 mismatches

To reproduce:

sbcl --load tests/transcode-astc-test.lisp # kodim23 pixel-exact test
sbcl --load tests/transcode-all-modes-test.lisp # multi-image all-modes test
sbcl --load tests/bc7-smoke-test.lisp # BC7 vs basisu byte-exact test

Performance

Benchmarks run on real test images covering all UASTC modes present in practice.

All numbers below were measured single-threaded on the perf/scalar-and-simd branch. It builds on the dependency-free optimizations already in master (per-file block compilation, a byte-gather extract-bits, comprehensive type declarations across the lookup tables and hot paths, and a table-driven byte reversal in the ASTC weight packer) with three further bit-exact scalar improvements: a single-pass word-wise bit-buffer in the ASTC weight packer and sequential forward bit-readers in the weight extractors. Net effect vs master (min-of-5, single-thread): ASTC ×ばつ faster, RGBA8 ~12%, BC7 ~6% (output byte-identical, all 100 tests pass). An AVX2 sb-simd decode path was also evaluated and rejected (see Design Notes).

Throughput

Throughput

  • RGBA8 (full decode + interpolation): ~19-29 Mpix/s (single-thread)
  • ASTC (lossless repack): ~23-26 Mpix/s (single-thread)
  • BC7 (near-lossless, endpoint requantization): ~12-13 Mpix/s (single-thread)

Per-Mode Transcode Time

Per-mode transcode time

Most modes transcode in 1-3 μs/block. Multi-subset modes (3, 4, 7) are slightly slower due to partition pattern handling.

Allocation Pressure

Allocation per block

All three paths use dynamic-extent stack allocation for temporary buffers (weight arrays, endpoint arrays, quantization intermediates). Per-block allocation is minimal for common modes: ASTC ~0.03 KB, BC7 ~0.03-0.3 KB, RGBA8 ~0.07-0.3 KB. Multi-subset modes allocate more due to partition handling. The RGBA8 path uses pre-allocated buffers instead of struct allocation for the interpolated-block decode.

Full Image Transcode

Full image transcode

Results Table

Benchmark μs/call Bytes/call
extract-bits 0.01 0
decode-rgba8/solid 0.1 82
decode-rgba8/solid+target 0.0 0
transcode-astc/solid 0.1 33
transcode-bc7/solid 0.1 31
block-astc/complex-rgb/mode0 1.0 16
block-bc7/complex-rgb/mode0 2.0 42
block-rgba8/complex-rgb/mode0 0.0 84
block-astc/complex-rgb/mode6 2.0 32
block-bc7/complex-rgb/mode6 1.0 87
block-rgba8/complex-rgb/mode6 1.0 80
block-astc/complex-rgb/mode12 1.0 7
block-bc7/complex-rgb/mode12 2.0 30
block-rgba8/complex-rgb/mode12 1.0 107
image-astc/complex-rgb (×ばつ512) 10,150 6,095,168
image-bc7/complex-rgb (×ばつ512) 20,900 17,301,824
image-rgba8/complex-rgb (×ばつ512) 9,050 26,447,552

Size Scaling

Size scaling

cl-uastc RGBA8 decode scales linearly with pixel count and is consistently faster than pngload: roughly ×ばつ faster at small/medium sizes, narrowing ×ばつ at multi-megapixel sizes but staying ahead across the full range (single-threaded). The values below are the mean across all five content patterns at each size; pngload decodes the matching PNG of identical content. UASTC's per-block cost is bounded by its mode mix, whereas PNG decode time swings widely with content (see the per-pattern chart below).

Size Blocks ASTC BC7 RGBA8 PNG (pngload) RGBA8 vs PNG
×ばつ256 4,096 2.1ms 4.1ms 1.5ms 2.2ms ×ばつ
×ばつ512 16,384 8.5ms 16.0ms 6.0ms 8.6ms ×ばつ
×ばつ1024 65,536 34.4ms 63.2ms 24.6ms 35.7ms ×ばつ
×ばつ2048 262,144 136ms 250ms 99ms 136ms ×ばつ
×ばつ4096 1,048,576 545ms 1017ms 396ms 510ms ×ばつ
×ばつ8192 4,194,304 2165ms 4027ms 1613ms 1952ms ×ばつ

(All times single-threaded, mean of the five content patterns per size. RGBA8-vs-PNG = mean pngload time / mean RGBA8 time. pngload decodes a PNG of the same content; its time is highly content-dependent (e.g. at 81922 it ranges from ~1.1 s on noise to ~3.9 s on smooth-worley).)

Comparison: cl-uastc vs PNG Loading

Operation (×ばつ1024, mean of 5 patterns) Time Notes
PNG decode (pngload) 35.7ms Pure CL, content-dependent (22-64ms)
cl-uastc → RGBA8 (single-thread) 24.6ms Full pixel decode, ×ばつ faster
cl-uastc → BC7 (single-thread) 63.2ms GPU-native, near-lossless
cl-uastc → ASTC (single-thread) 34.4ms GPU-native, lossless

cl-uastc RGBA8 decode is consistently faster than pngload (×ばつ) across the full size range tested (single-threaded). The more durable advantages are that UASTC decode is content-stable (cost bounded by mode mix, not compressibility) and that it produces GPU-native compressed formats (ASTC/BC7) that PNG cannot provide.

Size & Content Scaling

A second benchmark measures how single-threaded transcode time scales with image size (322 ... 81922) across five procedurally-generated content patterns (black, gradient, noise, perlin, smooth-worley). The generator (bench/gen-test-data.lisp) is adapted from a script by Nicolas Hafner (shinmera); it renders each pattern to SF3, converts to PNG via ImageMagick, then encodes UASTC KTX2 with the Khronos ktx CLI.

Transcode time vs image size

Time scales linearly with pixel count. Against pngload, RGBA8 decode is faster across sizes, with the largest margin on cheaper/smaller content, while the compressed targets (ASTC/BC7) trade some decode speed for GPU-native output that PNG cannot produce.

Transcode time by content pattern

Content matters as much as size: at 81922, uniform black transcodes in ~0.6 s (ASTC) while high-entropy noise takes ~2.8 s, because the UASTC mode mix (and thus per-block work) differs sharply between flat and detailed regions. This variety is what makes the set useful for exercising every mode path.

Running Benchmarks

sbcl --eval '(push #p"./" asdf:*central-registry*)' \
 --eval '(asdf:load-system "cl-uastc-bench")' \
 --eval '(cl-uastc-bench:run-benchmarks :output-dir #p"bench/results/")'
# Generate charts (requires matplotlib + seaborn)
python3 bench/plot-charts.py bench/results/bench-results.csv
# Or with gnuplot
cd bench && gnuplot plot-gnuplot.gp

Size & content scaling benchmark (requires the ktx CLI, ImageMagick with SF3 support, and the random-noise, nibbles, and pngload Quicklisp systems):

# 1. Generate the test images into bench/images/ (~2.2 GB for the full 322..81922 set)
sbcl --dynamic-space-size 8192 --non-interactive \
 --load bench/gen-test-data.lisp --eval '(run)'
# 2. Benchmark every pattern x size -> bench/results/bench-scaling.csv
sbcl --dynamic-space-size 10240 --non-interactive --load bench/bench-scaling.lisp
# 3. Generate the scaling + content charts
python3 bench/plot-scaling.py

Design Notes: Pure Scalar, Single-Threaded

cl-uastc is deliberately dependency-free, single-threaded, and free of SIMD intrinsics. Two avenues were evaluated and rejected:

  • SIMD (sb-simd/AVX2). An AVX2 RGBA8 interpolation path was prototyped (all 16 texels of a channel in one 16-lane s16.16 vector, bit-exact, behind a feature flag). It ran ×ばつ slower than the scalar loop. UASTC's ×ばつ4 blocks are too small: the per-block cost of lane construction (make-s16.16), value extraction, and simd-pack boxing dwarfs the arithmetic it saves. It also coupled the build to an SBCL-specific contrib. Removed.
  • Multithreading. Intentionally not used. The transcoder is a pure, embeddable per-block function; threading is left to the caller, who knows their own workload and scheduler.

The performance gains here come entirely from portable scalar techniques: per-file block compilation, byte-gather bit extraction, sequential forward bit readers, a table-driven byte-reversal weight packer, and comprehensive type declarations on the hot integer paths.

Project Structure

cl-uastc/
 cl-uastc.asd ; system definition
 cl-uastc-tests.asd ; test system definition
 cl-uastc-bench.asd ; benchmark system definition
 src/
 package.lisp ; package + exports
 bits.lisp ; bit extraction, replication, reversal
 tables.lisp ; spec tables, partition patterns, anchors
 uastc-block.lisp ; block parsing, mode decode, endpoints
 transcode-astc.lisp ; UASTC -> ASTC 4x4 lossless transcoding
 transcode-bc7.lisp ; UASTC -> BC7 near-lossless transcoding
 decode-rgba.lisp ; UASTC -> RGBA8 interpolation
 image.lisp ; whole-image transcode/decode convenience
 container.lisp ; KTX2 and .basis file parsing
 bench/
 package.lisp ; benchmark package
 bench.lisp ; benchmark suite (all modes, real images)
 gen-test-data.lisp ; procedural test-image generator (SF3->PNG->KTX)
 bench-scaling.lisp ; size + content scaling benchmark suite
 profile-sprof.lisp ; sb-sprof statistical profiler script
 plot-charts.py ; matplotlib/seaborn chart generator
 plot-scaling.py ; chart generator for size + content scaling
 plot-gnuplot.gp ; gnuplot chart script
 plot-interactive.html ; interactive Chart.js dashboard
 images/ ; generated test images (gitignored)
 charts/ ; committed chart PNGs for README
 tests/
 package.lisp ; test package definition
 bits-test.lisp ; bit utility unit tests
 block-test.lisp ; block parsing unit tests
 container-test.lisp ; KTX2 and .basis parsing unit tests
 census.lisp ; mode distribution analysis
 decode-test.lisp ; RGBA8 decode vs reference PNG
 transcode-astc-test.lisp ; pixel-exact ASTC roundtrip (kodim23)
 transcode-all-modes-test.lisp ; full ASTC roundtrip across all modes
 bc7-smoke-test.lisp ; BC7 byte-exact test vs basisu reference
 kodim23.basis ; test image (UASTC+KTX2)
 kodim23_raw.basis ; test image (raw UASTC+KTX2)
 kodim23_reference_rgba.png ; reference RGBA8 decode output
 kodim23_transcoded.astc ; ASTC transcode output (roundtrip test)
 kodim23_astc_decoded.png ; astcenc decode of transcoded ASTC
 test_rgba.basis ; 256x256 RGBA test image
 test_rgba.png ; reference PNG for test_rgba
 test_rgba2.basis ; 512x512 RGBA test image
 test_rgba2.png ; reference PNG for test_rgba2
 test_complex_rgb.basis ; 512x512 complex RGB test image
 test_complex_rgb.png ; reference PNG for test_complex_rgb

Scope

The primary transcode targets (ASTC and BC7) are implemented and verified. Together these cover virtually all modern GPU hardware: ASTC for mobile/embedded (ARM, Apple, Qualcomm) and BC7 for desktop (all DX11+ and Vulkan GPUs). Container parsing supports KTX2 and .basis files directly. The only transcode formats not implemented would be lower-priority ones like ETC1/2, BC1-5, or PVRTC, which are less commonly needed when you have BC7 + ASTC coverage.

References

License

MIT