basisu
A pure-Rust transcoder for Basis Universal
supercompressed GPU textures. It reads a .ktx2 or .basis Basis texture and transcodes
it, at load time, into whatever block-compressed format the target GPU supports:
BC7 and the other BCn formats on desktop, ASTC and ETC on mobile, plus PVRTC,
ATC, FXT1, and uncompressed RGBA fallbacks.
Basis Universal addresses GPU texture-format fragmentation with a compress-once,
transcode-at-load model. You encode an image once into a compact intermediate,
ship that single file everywhere, and at load time transcode it (without
re-encoding) into the GPU-native format the device actually wants. This crate is
the transcoder half of that pipeline, the runtime decode side. The encoder is the
upstream basisu toolchain.
The implementation is a port of the reference C++ transcoder, verified byte-for-byte identical to it across a corpus of real textures.
Quick start
[]
= "0.1"
The library name is basisu, so you write use basisu::...:
use ;
let bytes = read?;
let tex = new?;
// transcode mip level 0 to ASTC 4x4
let astc = tex.transcode?;
Pick the target at runtime from what the GPU reports, so one shipped file serves every device:
let target = if gpu.supports_astc else if gpu.supports_bc7 else ;
for level in 0..tex.level_count
How it works
A Basis texture stores its image data in a source codec chosen when the file is encoded:
- ETC1S is a supercompressed subset of ETC1. It is small (roughly 0.3 to 3 bits per pixel after the BasisLZ entropy stage) at low to medium quality, and transcodes very fast. A good default when size dominates.
- UASTC LDR is a high-quality 4x4 codec (8 bits per pixel) built on the ASTC bit layout. It is larger, but transcodes to high-quality targets like BC7 and ASTC with little quality loss. A good choice for normal maps and detail-heavy content.
- UASTC HDR is the high-dynamic-range variant: every block is a restricted ASTC HDR 4x4 block, transcoding to BC6H, ASTC HDR, or half-float/RGB-9E5 pixels.
Transcoding turns that source codec into a specific GPU format without a full decode and recompress. Each source block maps onto the target block format directly, which is fast enough to run while a texture loads. The same file becomes BC7 on a desktop, ASTC on a phone, and uncompressed RGBA where neither is available, with no per-platform asset builds.
You produce .ktx2 or .basis Basis files with the upstream encoder (basisu, or toktx
from KTX-Software). This crate consumes them.
Supported formats
Every source codec the reference transcoder accepts is implemented: ETC1S, UASTC LDR, UASTC HDR 4x4, raw ASTC LDR (all 14 block sizes, 4x4 through 12x12), raw ASTC HDR 6x6, UASTC HDR 6x6 (the bitwise-compressed intermediate format), and XUASTC LDR (all 14 block sizes; the arithmetic- or zstd-coded intermediate format). All decode bit-exactly.
Every transcoder target for these codecs is implemented, 500 source/target combinations verified byte-identical to the reference:
| Family | Targets |
|---|---|
| Desktop (S3TC, RGTC, BPTC) | BC1 (RGB), BC3 (RGBA), BC4 (R), BC5 (RG), BC7 (RGBA) |
| Mobile (ETC) | ETC1 (RGB), ETC2 (RGBA), EAC R11, EAC RG11 |
| ASTC | ASTC 4x4 (RGBA) |
| PowerVR | PVRTC1 4bpp (RGB, RGBA), PVRTC2 4bpp (RGB, RGBA) |
| Other | ATC (RGB, RGBA), FXT1 (RGB) |
| Uncompressed | RGBA32, RGB565, BGR565, RGBA4444 |
| HDR (from the HDR sources) | BC6H, ASTC 4x4/6x6 HDR, RGB/RGBA half-float, RGB 9E5 |
Not every target is valid for every source. UASTC LDR does not transcode to
PVRTC2, ATC, or FXT1; ETC1S covers all of them; raw ASTC LDR and XUASTC LDR
reach the 15 LDR targets plus only their own block size's ASTC pass-through.
The HDR sources transcode only to HDR targets (each reaching only its own
block size's ASTC pass-through), and the LDR sources never do.
Transcoder::supports(target) reports what a given texture can produce.
Container features: multi-mip textures, cubemaps, texture arrays,
Zstandard-supercompressed levels (with the optional zstd feature), and shared
(global) ETC1S codebooks across .basis files.
API
Transcoder::new(bytes) parses and prepares a texture. The rest queries it and
transcodes:
transcode(level, target, flags) -> Vec<u8>transcodes one mip level.transcode_image(level, layer, face, target, flags)selects a cubemap face or array layer.transcode_into(level, target, flags, out)writes into a caller-provided buffer without allocating the output (codecs that inherently need scratch, such as a Zstandard-compressed level or an intermediate stream, still allocate that internally, as the reference transcoder does).transcode_video_frame(state, level, frame, target, flags)decodes one ETC1S video frame; frames decode in order against a caller-ownedVideoState.etc1s_codebook()lifts a self-contained ETC1S file's shared codebook out, andTranscoder::new_with_codebook(bytes, &codebook)opens a global-codebook.basis(one that references a codebook stored in a separate file) with it.output_size(level, target)returns the exact byte length a transcode produces.image_level_info(level),base_dimensions(),level_count(),layer_count(),face_count()describe the contents.source_format(),has_alpha(),supercompression(),is_video(),supports(target)report capabilities.
TargetFormat and DecodeFlags carry the same numeric values as the C++
transcoder_texture_format and basisd_decode_flags. The flags cover the
reference decode options, such as HIGH_QUALITY for higher-quality UASTC to BCn
transcodes and TRANSCODE_ALPHA_TO_OPAQUE.
Features
std(default): the standard library. On std the crate has no required dependencies and no C or C++.zstd(default): decode Zstandard-supercompressed KTX2 levels via the pure-Rustruzstd. Without it, such levels returnError::ZstdRequired(most ETC1S files use BasisLZ and need nothing here).
The crate also builds #![no_std] (with alloc) for embedded use: disable
std and enable the libm feature. That is a niche path most consumers can
ignore.
Relationship to the upstream project
This is a Rust port of the Basis Universal C++ transcoder, tracking upstream v2_1_0. It is not a binding: the published crate contains no C++. It is a derivative work: the port was written against the reference source, and the lookup tables are generated from the upstream table sources rather than transcribed by hand.
Status
- LDR transcoding is complete: all 21 target formats, both source codecs, every mip level and decode-flag variant, byte-identical to the reference.
- Container support: both
.ktx2and.basis, cubemaps, texture arrays, Zstandard levels, and ETC1S video are done. Video frames decode in order throughtranscode_video_framewith a caller-ownedVideoState(P-frames replenish from the previous frame); the stateless entry points detect video (Transcoder::is_video) and reject it rather than decode P-frames as garbage. - Shared (global) ETC1S codebooks are done: a
.basisfile that references a codebook stored in a separate file decodes throughTranscoder::new_with_codebook(the codebook is lifted from the donor file withetc1s_codebook), byte-identical to the reference'sset_global_codebooks. KTX2 has no equivalent (its codebook is always embedded). The stateless entry points still reject a global-codebook file, so a caller that has no codebook never decodes garbage. - UASTC HDR 4x4 is complete: BC6H, ASTC 4x4 HDR, half-float, and RGB-9e5 output, byte-identical to the reference.
- Raw ASTC LDR sources (all 14 block sizes) are complete: the matching-block-size ASTC pass-through, all four uncompressed targets (with the deblocking filter), BC1/BC3/BC4/BC5, EAC R11/RG11, ETC1/ETC2, PVRTC1, and BC7. Every target is byte-identical to the reference over the full corpus.
- The 6x6 HDR sources are complete. Raw ASTC HDR 6x6: the 6x6 ASTC
pass-through, BC6H (via the reference's real-time
fast_encode_bc6hre-encoder, with the HIGH_QUALITY 2-subset search), half-float, and RGB-9E5 output. UASTC HDR 6x6 (the intermediate format, KTX2 supercompression scheme 4 or raw.basisslices): the run/solid/reuse/ block stream decompresses to standard ASTC 6x6 blocks, then reaches the same five targets. - XUASTC LDR (all 14 block sizes) is complete: the three stream syntaxes
(adaptive arithmetic, zstd side channels, and the hybrid) decompress to
standard ASTC blocks, which reach the same-block-size ASTC pass-through
and every LDR target, including the dedicated BC7 fast paths for the 4x4,
8x6, and 6x6 sources and the fast-BC7-disable decode flag (the reference's
cDecodeFlagXUASTCLDRDisableFastBC7Transcoding, value 1024). - All 500 (source, target) combinations in the conformance matrix are verified byte-identical over the corpus.
One documented divergence exists on malformed input only: the pure-Rust
zstd decoder (ruzstd) rejects some corrupted zstd frames that libzstd
tolerates. Valid files are unaffected.
Development
The repository is a workspace:
basisuis the published crate (package name and library name are the same). Its tests and benches depend on repo-level files (corpus/,goldens/), so the published tarball excludes them; they run from the repository checkout.conformanceis a dev-only crate. It compiles the vendored reference C++ as a golden oracle and runs the Rust transcoder against it, asserting zero differing bytes for every implemented format over the corpus. A committedgoldens/manifest.tsvof SHA-256 hashes backs a pure-Rust replay test that needs no C++ toolchain.xtaskdrives the checks.
Start with cargo xtask doctor: it reports every capability the gates use
(C++ compiler, system zstd, corpus, QEMU, wasm tooling) with the install
command for anything missing, on Linux and macOS alike. cargo xtask all
deliberately fails, rather than skips, when tooling is missing, so a green
all means the same thing on every machine.
The benchmarks in verify compare against a machine-local criterion baseline
named pre-opt when one exists (create it with cargo bench -p basisu --bench transcode -- --save-baseline pre-opt); on a fresh checkout there is none, so
they run and record timings without a regression comparison.
CI
This project uses no cloud CI: there are no workflow files, and nothing runs
on third-party infrastructure. cargo xtask ci runs every gate (check,
verify, wasm, qemu, MSRV, no_std, cargo package dry-run) on the machine it
is invoked on, keeps going after a failure so one report shows the full
picture, and writes a timestamped report to target/ci/. Run it after a
work session and before publishing. cargo xtask install-hooks wires the
fast check gate to git's pre-push hook, so every push is checked before it
leaves the machine (git push --no-verify bypasses it in an emergency).
Benchmarks are excluded from ci by design: timings are only meaningful
against the machine-local baseline described above.
The corpus lives under corpus/: a small committed smoke/ set that every
test falls back to, plus the full set fetched by cargo xtask corpus (three
pinned, license-clean upstream sources). Running cargo test -p conformance --release after that fetch reproduces the full byte-equality result (the
500/500 conformance matrix).
Two environment notes, both surfaced by doctor:
- The oracle decodes Zstandard-supercompressed files only when a system zstd
is found at build time (Debian/Ubuntu
libzstd-dev, Fedoralibzstd-devel, macOSbrew install zstd, orZSTD_INCLUDE_DIR/ZSTD_LIB_DIR). Without it, the build prints a warning and the conformance run counts those files aszstd-no-oracleskips; the committed goldens still gate them in pure Rust. - The wasm tests (
cargo xtask wasm) needcargo install wasm-bindgen-cli --version 0.2.118(paired with the pinnedwasm-bindgen-test 0.3.68) and node; the QEMU harnesses (cargo xtask qemu) needqemu-system-armplus the two thumb targets (rustup target add thumbv7m-none-eabi thumbv6m-none-eabi).
Staying in sync with upstream
The vendored C++ oracle is the reference every format is proven against, so moving to a newer upstream release goes like this:
cargo xtask vendor <ref>re-vendors the transcoder-only source set at the given git ref, leaving a reviewable diff underconformance/csrc/basisu_transcoder/.- Pin the new ref in that directory's
UPSTREAM_VERSION. cargo xtask verifyruns the Rust transcoder against the new oracle; any behavioral change surfaces as a failure localized to the exact (format, file) rows that diverged.- Port each divergence in the Rust code, repeating step 3 until it is green.
cargo xtask bake-goldensrefreshes the committedgoldens/manifest.tsv, so the toolchain-freecargo xtask checkgate tracks the new reference.
License
Apache-2.0. The vendored C++ under conformance/csrc/basisu_transcoder/ is Basis
Universal (Apache-2.0, (c) Binomial LLC) and is used only as the test oracle. It
is not part of the published crate.