A standalone, portable C++ library (with a flat C API and WebAssembly build) for decoding and encoding GPU compressed texture formats, plus platform unswizzling and reswizzling for console texture layouts.
No external dependencies. One header, one static/shared library.
Includes Web Assembly for .wasm, .js and .ts output.
| Family | Formats | Decode | Encode |
|---|---|---|---|
| BC / DXT | BC1 (DXT1), BC2 (DXT3), BC3 (DXT5), BC4 ± SNORM (ATI1/RGTC1), BC5 ± SNORM (ATI2/3Dc), BC6H UF16/SF16, BC7 | ✅ all modes | ✅ (BC6H: mode 11, BC7: mode 6) |
| ETC / EAC | ETC1 (with alpha†), ETC2 RGB / RGBA1 (punchthrough) / RGBA8, EAC R11 / RG11 ± signed | ✅ | ✅ |
| PVRTC | PVRTC1 (with alpha†) 2bpp & 4bpp RGB/RGBA, PVRTC2 2bpp & 4bpp | ✅ | ✅ (PVRTC2: hard-mode) |
| ASTC | LDR + HDR blocks, 2D block sizes 4x4 ... 12x12 | ✅ | ✅ (valid-bitstream baseline) |
| ATC | ATC RGB, RGBA explicit, RGBA interpolated | ✅ | ✅ |
| PICA200 (3DS) | PICA_ETC1_RGB8 (GPU_ETC1), PICA_ETC1_RGB8A4 (GPU_ETC1A4) |
✅ | ✅ |
| GameCube / Wii (GX, TPL) | WII_I4, WII_I8, WII_IA4, WII_IA8, WII_RGB565, WII_RGB5A3, WII_RGBA8, WII_CMPR; paletted WII_C4, WII_C8, WII_C14X2 |
✅ (paletted via texc_decode_paletted) |
✅ (not paletted) |
| Raw | RGBA8 passthrough | ✅ | ✅ |
| Raw elements | RAW8, RAW16, RAW32, RAW64, RAW128 (1-16 bytes a texel, swizzling only) |
- | - |
Decode target is 8-bit RGBA (texc_decode) or float RGBA (texc_decode_f32,
full range for BC6H / ASTC HDR). Encode source is 8-bit RGBA (texc_encode)
or float RGBA (texc_encode_f32). Non-block-aligned dimensions are handled
(decoders clip, encoders edge-replicate). Encoders favour portability and
determinism over ultimate quality; they produce valid bitstreams everywhere.
† See Alpha-atlas formats below.
Some alpha-less codecs can hold an alpha by storing the image twice in one
double-height texture: RGB on top, the alpha channel as a grayscale map
below. The *_A_ATLAS formats reproduce that end to end -
you always pass the final (half-height) dimensions:
texc_decodedecodes the double-height base image and folds the atlas back exactly like the engine (A = Rof the bottom-half texel).texc_encodesplits your RGBA into the two planes and encodes them as one base-codec image.texc_swizzle/texc_unswizzle/texc_swizzled_sizeoperate on the double-height layout automatically (doublesHEIGHTbefore untiling).
PICA_ETC1_RGB8 and PICA_ETC1_RGB8A4 are ordinary ETC1 colour data in
the 3DS's own container, handled transparently:
- the image is stored as 8x8 pixel tiles in row-major order, each tile holding four 4x4 ETC1 blocks in the order (0,0), (4,0), (0,4), (4,4);
- each 64-bit ETC1 block is byte-reversed relative to the standard big-endian layout;
RGB8A4additionally prefixes every colour block with 8 bytes of 4-bit alpha, nibble indexx*4 + y(ETC1's own pixel order), expanded as(a << 4) | a.
Block geometry is reported as the 8x8 tile (32 bytes = 4bpp for RGB8,
64 bytes = 8bpp for RGB8A4), so texc_encoded_size rounds up to whole
tiles exactly as the hardware stores them. The layout was verified against
devkitPro's tex3ds (which encodes via rg-etc1) and gdkchan/SPICA; note that
SPICA also flips its output vertically as its own convention - that is not
part of the format, so this library keeps its usual top-left origin (use
texc_flip_y for the other orientation).
texc decode -i tex.bin -f PICA_ETC1_RGB8A4 -w 128 -h 128 -o out.pngPorted from Kerilk's tpl.h (noesis_bayonetta_pc), which is the decoder
Project-G1M's PLATFORM::RVL path runs. "Wii swizzling" is really the GX
tile layout plus per-format encoding, so it is modelled as a set of
formats rather than a swizzle mode - decode/encode handle the tiling:
- the image is a row-major grid of tiles, every tile 32 bytes (RGBA8: 64);
tile size per format: I4 8x8, I8/IA4 8x4, IA8/RGB565/RGB5A3/RGBA8 4x4,
CMPR 8x8, C4 8x8, C8 8x4, C14X2 4x4 - this is what
texc_block_dimsreports, so sizes round up to whole tiles as the hardware stores them; - all multi-byte values are big-endian; 4bpp formats put the even column in
the high nibble; expansion is
v*255/max(integer), as the reference; RGB5A3: bit 15 set = RGB555 opaque, clear = A3+RGB444;RGBA8: each 64-byte tile is a 32-byte AR plane then a 32-byte GB plane;CMPR: an 8x8 tile is four DXT1-style 4x4 sub-blocks (TL, TR, BL, BR) with big-endian colour words and MSB-first selectors;c0 <= c1selects 3-colour mode with a transparent 4th entry. Encoding reuses the BC1 encoder, soalpha_thresholdcontrols punchthrough here too.
Paletted formats (WII_C4, WII_C8, WII_C14X2) need a palette of
big-endian 16-bit IA8 / RGB565 / RGB5A3 entries - for G1T content,
an entry from the matching G1TL file (its WiiPALETTE_TYPE maps directly
onto texc_palette_format). texc_decode returns TEXC_ERR_NEEDS_PALETTE
for them; use texc_decode_paletted, sizing the palette with
texc_palette_size (32 / 512 / 32768 bytes). Encoding paletted formats is
not supported (no palette generation).
Project-G1M's getNWiiFormat() maps G1T pixel-format IDs like so:
| Wii format | G1T pixel-format IDs |
|---|---|
WII_I4 |
0x2B, 0x2F |
WII_I8 |
0x18, 0x28, 0x29, 0x2A, 0x2C, 0x30 |
WII_IA4 |
0x2D |
WII_IA8 |
0x26, 0x27, 0x2E |
WII_RGB565 |
0x1C |
WII_RGB5A3 |
0x25 |
WII_RGBA8 |
0x0A, 0x13, 0x22 |
WII_CMPR |
0x10 |
WII_C4 / WII_C8 / WII_C14X2 |
0x31 / 0x32 / 0x15, 0x33 |
Cube maps and volume textures are simply consecutive 2D faces / slices; decode each with its own dimensions, as the reference does.
texc decode -i tex.bin -f WII_CMPR -w 256 -h 256 -o out.png
texc decode -i tex.bin -f WII_C8 -w 128 -h 128 --palette pal.bin --palette-format rgb5a3 -o out.pngtexc_encode_ex takes an optional texc_encode_options
(zero-init + texc_encode_options_init for defaults; the struct is
versioned by struct_size and may grow):
texc_encode_options opts;
texc_encode_options_init(&opts);
opts.alpha_threshold = 200; /* punchthrough cutoff for BC1 / ETC2_RGBA1 */
texc_encode_ex(TEXC_FORMAT_BC1, rgba, size, w, h, dst, dst_size, &opts);Two clearly named directions (swizzle logic credited to Piken / DwayneR):
texc_unswizzle- platform-tiled → linear row-major: the direction you need before decoding console data.texc_reswizzle- linear → platform-tiled: the exact inverse, for putting the platform tiling back when repacking data for the target hardware (e.g. into a G1T). (texc_swizzleis the same function under its legacy name.)
Every mode's reswizzle→unswizzle roundtrip is verified byte-identical in the test suite. Supported layouts:
| Mode | Layout |
|---|---|
TEXC_SWIZZLE_PS4 |
Orbis 8x8 micro-tile Morton |
TEXC_SWIZZLE_PS5 |
Prospero / AMD GFX10 block tiling (arg = texc_ps5_tile_mode, 0 = standard 4 KB blocks); whole mip chains via the surface-layout API below |
TEXC_SWIZZLE_SWITCH |
Tegra X1 block-linear GOBs (arg = block height log2, 0xFFFFFFFF = auto) |
TEXC_SWIZZLE_SWITCH2 |
Switch 2 / NVIDIA Orin block-linear GOBs, same arg and sizes as SWITCH |
TEXC_SWIZZLE_PSVITA |
GXM: 32x32 pixel tiles for raw formats (arg = bytes per pixel, 0 = the format's own; G1T ships 8/16/24/32bpp raw Vita textures), Morton / Z-order for block formats |
TEXC_SWIZZLE_X360 |
Xenos macro tiling (arg = pitch override, byte order and stored-rows flag, see below) |
TEXC_SWIZZLE_PSP |
16-byte x 8-row tiles |
TEXC_SWIZZLE_3DS |
8x8 Z-order tiles |
TEXC_SWIZZLE_WIIU |
GX2 tiled (arg = GX2 swizzle value) |
TEXC_SWIZZLE_DX12_64KB |
D3D12 64KB tiles as KTGL stores them: always the linear size, see below |
texc_decode_swizzled does unswizzle + decode in one call. Container
parsing is intentionally left to the adopting project - this library
is the conversion core.
Switch 2 (Orin) uses the same block-linear layout as the original
Switch: the same 512-byte GOBs (64 bytes x 8 rows) stacked into blocks of
2^arg GOBs, the same block-height rules and the same tiled sizes. The
only difference is the byte order inside each GOB, where two address bits
trade places:
Tegra X1: ((x%64)/32)*256 + ((y%8)/2)*64 + ((x%32)/16)*32 + (y%2)*16 + x%16
Orin: ((x%64)/32)*256 + ((y%8)/4)*128 + ((x%32)/16)*64 + (y%4)*16 + x%16
(x = byte within the row, y = row within the GOB.) So any code that
already handles a Switch G1T handles a Switch 2 one by passing
TEXC_SWIZZLE_SWITCH2 instead. Both patterns come from ScanMountGoat's
tegra_swizzle (its isOrin path), and tests/switch2_kat.h holds
known answers produced by that library for both GPUs.
When you pass a non-zero arg, size the linear side with
texc_unswizzled_size(mode, format, w, h, arg) rather than
texc_encoded_size() - the format alone cannot describe the PS Vita raw
bytes-per-pixel override.
PS Vita raw reproduces DeswizzlePSVitaRaw exactly: the tiled buffer
is width * height * bytesPerPixel (the raw width is used, not rounded up
to whole 32x32 tiles), and texels whose mapped offset falls outside the
image are dropped, as the reference does. That only happens when a
dimension is not a multiple of 32, where the mapping is consequently not
invertible. A test compares the port against the reference formula across
sizes and 8/16/24/32bpp.
Uncompressed data other than RGBA8 (R8, RG8, RGBA16F, RGBA32F, ...) goes
through the swizzle modes, the mip-chain API and PS5 tile-mode detection
as TEXC_FORMAT_RAW8 / RAW16 / RAW32 / RAW64 / RAW128, named by
bytes per texel alone. Each mode applies its own element-size rules:
| Mode | What the element size changes |
|---|---|
| PS4 | 8x8 Morton micro-tiles for every size, but the pitch grows by 8 until a slice is a multiple of 256 bytes (1- and 2-byte texels); matched byte for byte against Sony's GpuAddress (tools/ps4_oracle) |
| PS5 | AMD GFX10 standard swizzle per element size, as verified against Sony's AgcGpuAddress |
| Wii U | addrlib's micro-tile order and macro tiling for the element size (a GX2 format of that size) |
| X360 | element size in the tile address and byte order (8in16 for 2/8 bytes, 8in32 for 4/16); the level is aligned to 4 KB, which 1- and 2-byte texels need |
| DX12 64KB | the tile shape (256x256 down to 64x64 texels) |
| Switch / Switch 2 / PSP | byte based, so nothing |
They carry no channel layout, so texc_decode / texc_encode reject them
(texc_can_decode / texc_can_encode report 0); decode the texels
yourself after unswizzling. RAW32 tiles exactly like RGBA8.
KTGL streams large PC textures into D3D12 reserved resources in 64 KB tiles, and the G1T stores the data in that tile order (the "swizzle" field of the texture's extended header). Taken from Rise of the Ronin's own CPU upload path:
- each mip is cut into tiles of the D3D12 standard 64 KB shape for its element size: 256x256 elements at 1 byte, 256x128 at 2, 128x128 at 4, 128x64 at 8 and 64x64 at 16. In pixels, BC1/BC4 tiles are 512x256 and BC2/3/5/6H/7 tiles 256x256;
- tiles run left to right, then top to bottom;
- each tile is stored in one piece, row by row (block rows for BCn), and tiles on the right and bottom edges are only as wide or tall as what is left.
So the stored size is always the linear size, and a mip at most one tile wide is plain linear. In a whole texture KTGL orders the data by mip, then by array slice (mip 0 of every slice first). Up to 1.7 this mode used padded edge tiles, a "64 KB or less is linear" rule and 128x128 raw tiles for every element size; all three were wrong.
The X360 arg packs three fields: the texel pitch override in the low
byte, a texc_x360_endian in bits 8-10, and the TEXC_X360_LOADER_ROWS
flag. With TEXC_X360_ENDIAN_AUTO (0) the byte order follows the
D3DFORMATs Koei Tecmo's X360 loader declares (Dynasty Warriors 8):
8IN16 for DXT and 2- or 8-byte texels, 8IN32 for 4- and 16-byte texels,
none for 1-byte texels. Up to 1.7 every format was swapped 8IN16, which
scrambles 32-bit texels such as RGBA8; pass TEXC_X360_ENDIAN_8IN16 to get
that back.
The loader does not untile anything: it reads each mip's ceil(h/bh) rows
of tightly packed elements straight into the locked tiled surface, one row
per surface pitch (the width in elements rounded up to 32), and never
writes the rest of the surface. TEXC_X360_LOADER_ROWS makes the tiled
side exactly that stored data: texc_swizzled_size returns the stored size
(the linear size), texc_unswizzle places the rows at the pitch, accepts a
shorter src and reads the missing tail as zeros (missing bytes =
texc_swizzled_size - src_size), and texc_reswizzle writes the stored
rows. Elements the stored rows never reach come out as zeros, as on the
console.
KTGL's Switch loader (Hyrule Warriors: Age of Calamity) creates the NVN
texture and reads a tiled G1T payload straight into its storage, exactly
nvnTextureGetStorageSize bytes, so the file is NVN's own block-linear
surface. texc_get_surface_layout / texc_unswizzle_mip /
texc_reswizzle_mip with TEXC_SWIZZLE_SWITCH or SWITCH2 and
arg = 0xFFFFFFFF (NVN's own block height) describe it:
- each slice (array layer, cube face) holds its mips back to back;
- each mip is in GOB blocks of its own height: mip 0's, halved while the mip still fits in half of it;
- with several slices, every slice is padded to a whole block of GOBs, from mip 0's block height halved while half of it still covers the image height in pixels.
This matches ScanMountGoat's tegra_swizzle byte for byte
(tests/switch_surface_kat.h, generated by tools/switch_oracle) and the
payload of every tiled Switch sample, arrays of 4 to 600 layers included; a
1024x1024 BC1 with 9 mips is 704,512 bytes a slice.
Whether a Switch texture is tiled at all depends only on its G1T format,
as on Wii U, PS4 and X360: KTGL tiles 0x09-0x0D, 0x11-0x13, 0x15,
0x17, 0x18, 0x1C-0x1E, 0x20, 0x22, 0x24-0x33,
0x3B-0x3F, 0x42-0x48, 0x4B, 0x4D, 0x4F, 0x51, 0x53,
0x60-0x66, 0x6D, 0x74-0x79 and 0x7E, and uploads every other
format linearly through the driver: the plain BC formats 0x59-0x5F and
the linear ASTC 0x7D, whose payloads are the tight linear sizes. A tiled
format's payload is always the full block-linear surface.
texc_get_volume_layout, texc_unswizzle_volume_mip and
texc_reswizzle_volume_mip (JS: the depth option of surfaceLayout /
unswizzleMip / reswizzleMip; CLI: --mips N --depth N) handle a
width x height x depth texture, a mip at a time with all of its slices,
slice-major on the linear side. texc_mip_layout.depth gives each mip's
depth.
TEXC_SWIZZLE_SWITCH/SWITCH2: NVN's 3D block-linear layout. A block is one GOB wide, one GOB tall (whatever the 2D rule would pick) andbdGOBs deep, the GOBs of successive slices back to back;bdis 16, 8, 4, 2 or 1 from the mip's own depth, blocks run across x, then down y, then deeper in z, and each mip is padded to whole blocks in depth. Matchestegra_swizzlebyte for byte (7 volume cases intests/switch_surface_kat.h, mip chains and odd depths included), the payload sizes of Pokopia's Switch 2 volumes (512x512x64 BC1, 256x256x16 RGBA16F) and Age of Calamity's 32x32x32 ASTC, and all three read smooth along every axis.TEXC_SWIZZLE_PS4: KTGL's volume setup asks GNM forThick_1dThick. Raw texels of 1 to 8 bytes are thick-tiled: 8x8x4-element micro tiles, row by row, a 4-slice slab at a time, depth padded to 4, with the element order inside a tile depending on the texel size. Block-compressed formats and 16-byte texels (RGBA32F) come outThin_1dThina slice at a time. Checked byte for byte against Sony's GpuAddress (tests/ps4_volume_kat.h,tools/ps4_oracle), and all 779 volumes in the Nioh 2 samples (colour-grading LUTs and BC mip chains) match KTGL's sizes exactly and decode smooth along every axis.TEXC_SWIZZLE_NONE: mips back to back, each mip's slices back to back.- Other modes return
TEXC_ERR_UNSUPPORTED. PS5, X360 and Wii U have thick tile modes too, but a sweep of the known corpus found no G1T volume on those platforms, so they are deliberately not implemented.
KTGL's Wii U loader (Hyrule Warriors) hands GX2 the G1T bytes as the
surface, with tile mode DEFAULT, which GX2 turns into 2D_TILED_THIN1.
Mip 0 keeps that mode at any size, but a smaller mip that is narrower than
one macro tile (32 x 16 elements for 64- and 128-bit elements) falls to
1D_TILED_THIN1: 8x8-element micro tiles, row by row, padded to whole
tiles. The mips follow mip 0 back to back, each at its addrlib base
alignment. texc_get_surface_layout / texc_unswizzle_mip /
texc_reswizzle_mip with TEXC_SWIZZLE_WIIU give that layout; the payload
sizes of real Hyrule Warriors textures match it exactly (128x128 BC3 with
3 mips = 21504 bytes: 16384 + 4096 + 1024), and each 1D mip decodes to its
parent mip scaled down. Single-mip arrays and cube maps work too (faces
use addrlib's per-slice bank rotation); GX2 keeps all slices of a level
together, which the slice-major layout struct cannot describe, so several
mips of several slices return TEXC_ERR_UNSUPPORTED.
The PS5 GPU is an AMD GFX10-class part, and a texture is a row-major raster
of fixed-size blocks (256 B, 4 KB or 64 KB) whose interior uses AMD's
"standard" address swizzle - the layout AMD's open-source addrlib computes
for SW_4KB_S and friends, and what Sony's AgcGpuAddress library calls
TileMode::kStandard4KB etc. Every known G1T PS5 texture uses the 4 KB
mode (Sony's own texture tool defaults sampled textures to it), which is
arg = TEXC_PS5_TILE_DEFAULT (0). TEXC_PS5_TILE_STANDARD_256B (1),
_4KB (5) and _64KB (9) match Sony's enum so a header value can be
passed straight through; TEXC_PS5_TILE_LEGACY (0x100) keeps the pre-1.6
layout reachable (it equals the 64 KB mode for 16-byte blocks and 32bpp raw
data, but is not a hardware layout for anything else).
Koei Tecmo content is not all 4 KB: small textures (mip 0 up to 64 KB
in every sample seen) use STANDARD_4KB, large ones (1024x128 and
64x2048 BC7 samples, mip 0 = 128 KB) use STANDARD_64KB, and G1T
carries no tile-mode field. The two layouts have different whole-surface
sizes, so the mode can be recovered from the texture's data size:
texc_ps5_tile_mode tm;
texc_ps5_detect_tile_mode(TEXC_FORMAT_BC7, 1024, 128, 6, 1,
/*data bytes in the G1T*/ 524288, &tm);
/* tm == TEXC_PS5_TILE_STANDARD_64KB (a 4 KB surface would be 196608) */For the write path, texc_ps5_default_tile_mode(format, w, h) returns the
choice observed in Koei Tecmo content (64 KB blocks once mip 0 exceeds
64 KB); it is a heuristic that matches every sample so far. The CLI's
--arg auto uses the detector when unswizzling and the heuristic when
reswizzling.
A PS5 texture stores its whole mip chain as one surface: the smallest mips that fit in half a block share a single "mip tail" block stored first, then the remaining mips follow in descending order, so mip 0 is last; array slices and cube faces repeat the chain. Because the padding, tail packing and ordering are platform-defined, the library exposes the layout instead of leaving callers to guess offsets:
texc_surface_layout lay;
texc_get_surface_layout(TEXC_SWIZZLE_PS5, TEXC_FORMAT_BC7, 256, 256,
/*mips*/ 7, /*slices*/ 1, /*arg*/ 0, &lay);
/* lay.total_size == 90112: 64K (mip 0) + 16K + 4K + one 4K tail block;
lay.mips[0].offset == 24576, lay.mips[3].in_tail == 1 ... */
/* pull any mip straight out of the surface, ready to decode */
texc_unswizzle_mip(TEXC_SWIZZLE_PS5, TEXC_FORMAT_BC7, 256, 256, 7, 1, 0,
/*mip*/ 0, /*slice*/ 0, surface, lay.total_size,
linear, texc_encoded_size(TEXC_FORMAT_BC7, 256, 256));
/* and back: zero-fill total_size bytes, then texc_reswizzle_mip per mip */texc_unswizzle / texc_reswizzle with TEXC_SWIZZLE_PS5 handle a
single mip (its block-padded raster, texc_swizzled_size bytes) and are
exactly texc_unswizzle_mip on a one-level surface, so mip 0 of a chain
can also be converted by pointing them at lay.mips[0].offset.
texc_get_surface_layout also works for TEXC_SWIZZLE_NONE (mips back to
back, mip 0 first), TEXC_SWIZZLE_PS4, TEXC_SWIZZLE_SWITCH / SWITCH2
and TEXC_SWIZZLE_WIIU (above); other modes return
TEXC_ERR_UNSUPPORTED.
PS4 mip chains are stored mip 0 first, back to back, each tiled at its
own pitch. A chain of several mips is padded by GNM, at any size: each
mip's width is mip 0's width rounded up to a power of two and halved per
level, its height is its own rounded up to a power of two, then the usual
8-element alignment and 256-byte pitch rule apply. This was fitted against
KTGL's own layout code on 1,000 random chains and is checked on every run
of tools/ps4_oracle. A chain with several slices is not supported yet
(the slice order is unknown).
The implementation is derived from AMD addrlib (MIT). It is verified
bit-for-bit against Sony's AgcGpuAddress host library across tile modes,
element sizes, odd sizes, mip counts and slices by tools/ps5_oracle
(needs the PS5 SDK, never committed); the SDK-generated known-answer table
it produces (tests/ps5_kat.h) is checked by the normal test suite. 3D
(volume) textures, render-target/depth tile modes and MSAA are not
implemented.
Ported from tex-decoder's
profiler.ts and flipper.ts:
- Colour profile converter -
texc_convert_profileconverts raw pixels between 72 profiles (texc_pixel_profile: RGBA8/BGRA8 and every channel permutation, RGB565, RGBA4, RGBA51, RGB10_A2, signed*I, half-float*16F, float*32F, 16/32-bit integer). Semantics match tex-decoder: proportional range scaling, a missing source alpha becomes opaque, missing colour channels become 0, float 1.0 ≙ integer max.texc_profile_bytes_per_pixel/texc_profile_namegive metadata. - Flipper -
texc_flip_y(row order, tex-decoderflipImage) andtexc_flip_x(horizontal mirror), any bytes-per-pixel, in-place capable. - Cropper -
texc_crop(tex-decodercropImage): copy a rectangle out of a raw image with bounds validation.
tools/make_release.py packages a release zip from the built artifacts:
tex_codec_v<version>.zip
wasm/ tex_codec.js, tex_codec.wasm, tex_codec_api.mjs, .d.ts
cli/ texc.exe, tex_codec.lib
include/ tex_codec.h
README.md, CHANGELOG.md
Build everything first (the zip needs both the native and the Emscripten output), then package:
cmake --build build --config Release
cd wasm && ./build_wasm.sh ../dist && cd ..
cmake --build build --config Release --target release # or: python tools/make_release.pyThe version comes from include/tex_codec.h, and the script refuses to
package if texc.exe or dist/tex_codec_api.mjs report a different one -
so a zip can never ship stale artifacts under a new version number.
To also create the GitHub release, using that version's CHANGELOG.md
section verbatim as the release notes:
python tools/make_release.py --publish # add --draft to stage it firstPublishing needs a clean working tree (it refuses otherwise) and either the
gh CLI authenticated via gh auth login, or a
GITHUB_TOKEN environment variable with repo scope - the token is read
from the environment and never printed. It tags v<version> and attaches
the zip.
The version is declared once, in include/tex_codec.h:
#define TEXC_VERSION_MAJOR 1
#define TEXC_VERSION_MINOR 5
#define TEXC_VERSION_PATCH 0Everything else derives from it, so nothing can drift: CMake parses those
macros for the project/package version, stamps them into the Windows
file-version resource of tex_codec.dll and texc.exe (visible under
Properties → Details) and into SOVERSION on ELF builds, and the test
suite fails if the compiled library, the header or the JS wrapper's
VERSION disagree. Bumping a release means editing those three lines and
adding a CHANGELOG.md entry.
New formats, profiles and swizzle modes are only ever appended to their enums, so numeric values stay stable across minor versions - which is what lets the JS wrapper and other FFI bindings mirror them by number.
Query it at runtime:
texc_version() |
packed (major << 16) | (minor << 8) | patch |
texc_version_string() |
"1.5.0" |
texc_build_info() |
tex_codec 1.5.0 (git 3f2a1b8, built Sep 2 2026, MSVC 1944, x64, Release) |
texc version |
the same build line (texc version --short prints just 1.5.0) |
await tex.version() / tex.buildInfo() |
from JavaScript, plus tex.checkVersion() to catch a stale .wasm beside a newer wrapper |
Compile-time checks are available too:
#if TEXC_VERSION_NUMBER < TEXC_VERSION_ENCODE(1, 1, 0)
# error "tex_codec 1.1.0 or newer is required"
#endifCMake consumers can require a version with
find_package(tex_codec 1.1 REQUIRED). The git hash in texc_build_info()
is captured at configure time - re-run cmake to refresh it after
committing.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest --test-dir build -C ReleaseOptions: -DTEXC_BUILD_SHARED=ON (DLL/.so with exported symbols),
-DTEXC_BUILD_TESTS=OFF, -DTEXC_BUILD_CLI=OFF.
The native build also produces texc (build/Release/texc.exe), a full
front end for the library. --offset / --size open a window into any
file, so you can point it straight at texture data inside a container
(e.g. a G1T) without extracting it first:
# decode BC7 data at an offset inside a container straight to PNG
texc decode -i file.g1t --offset 0x1234 --size 0x8000 -f BC7 -w 256 -h 256 -o out.png
# Switch 2 (G1T platform 0x18): same flags, mode switch2
texc decode -i tex.bin -f BC7 -w 512 -h 512 --swizzle switch2 --arg auto -o out.png
# Switch-swizzled ASTC: unswizzle + decode in one step ('auto' block height)
texc decode -i tex.bin -f ASTC_8x8 -w 256 -h 256 --swizzle switch --arg auto -o out.tga
# G1T alpha atlas (final half-height dimensions, alpha folded automatically)
texc decode -i tex.bin -f ETC1_RGB_A_ATLAS -w 128 -h 128 -o out.png
# encode a TGA (dimensions come from the file) and reswizzle for PS4
texc encode -i art.tga -f BC1 --alpha-threshold 200 --swizzle ps4 -o tiled.bc1
# pure layout conversions, clearly named per direction
texc unswizzle -i tiled.bin -f BC7 -w 256 -h 256 -m switch --arg auto -o linear.bin
texc reswizzle -i linear.bin -f BC7 -w 256 -h 256 -m switch --arg auto -o tiled.bin
# PS5: the whole mip chain is one surface - describe it, then pull a mip out
# (--arg auto recovers the 4KB / 64KB tile mode from the input size)
texc info -f BC7 -w 256 -h 256 -m ps5 --mips 7
texc unswizzle -i file.g1t --offset 0x38 -f BC7 -w 256 -h 256 -m ps5 --arg auto --mips 7 --mip 0 -o mip0.bin
texc reswizzle -i mip0.bin -f BC7 -w 256 -h 256 -m ps5 --mips 7 --mip 0 -o surface.bin
# image utilities
texc convert -i in.raw --src-profile RGBA8 --dst-profile BGRA8 -o out.raw
texc flip -i in.raw -w 64 -h 64 --dir y -o out.raw
texc crop -i in.raw -w 64 -h 64 --rect 16,16,32,32 -o out.raw
# sizes without touching data
texc info -f ASTC_8x8 -w 256 -h 256 -m switch --arg autoOutput format follows the extension: .png / .tga for decoded images,
anything else raw bytes (--f32 writes float RGBA). Encode input is an
uncompressed TGA or raw RGBA8 (with -w/-h). Discovery commands:
texc formats, texc modes, texc profiles, texc version,
texc help <command>.
With an activated emsdk:
cd wasm && ./build_wasm.sh ../distor via CMake: emcmake cmake -S . -B build-wasm && cmake --build build-wasm.
Produces tex_codec.js (MODULARIZE loader, export name TexCodec) and
tex_codec.wasm, plus copies of the ergonomic wrapper
(tex_codec_api.mjs + tex_codec_api.d.ts), usable from web, workers, and
Node.
Verify the module with node wasm/smoke_test.mjs dist (wrapper roundtrips,
alpha-atlas fold, encoder options, swizzle identity, named exports).
#include "tex_codec.h"
size_t out_size = texc_decoded_size(width, height);
uint8_t *rgba = texc_alloc(out_size);
int rc = texc_decode(TEXC_FORMAT_BC7, data, data_size,
width, height, rgba, out_size);
if (rc != TEXC_OK) fprintf(stderr, "%s\n", texc_result_str(rc));
/* Swizzled console data in one step: */
rc = texc_decode_swizzled(TEXC_SWIZZLE_SWITCH, TEXC_FORMAT_ASTC_8x8,
width, height, data, data_size,
rgba, out_size, 0xFFFFFFFF /* auto block height */);
/* Encoding: */
size_t enc_size = texc_encoded_size(TEXC_FORMAT_ETC2_RGBA8, width, height);
uint8_t *enc = texc_alloc(enc_size);
rc = texc_encode(TEXC_FORMAT_ETC2_RGBA8, rgba, out_size,
width, height, enc, enc_size);
texc_free(rgba); texc_free(enc);The recommended entry point is the wrapper in wasm/tex_codec_api.mjs
(TypeScript declarations in wasm/tex_codec_api.d.ts; both are static,
hand-maintained files that build_wasm.sh copies next to the module). It
gives you real enums, async self-initialising methods, automatic WASM
memory management, and one class per concern:
import { TexCodec, TexFormat, SwizzleMode, PixelProfile,
SwitchBlockHeightAuto } from "./dist/tex_codec_api.mjs";
const tex = await TexCodec.load(); // finds ./tex_codec.js next to the file
// --- decoding -----------------------------------------------------------
const rgba = await tex.decoder.decode(TexFormat.BC7, data, w, h);
const hdr = await tex.decoder.decodeF32(TexFormat.BC6H_UF16, data, w, h);
const fromSwitch = await tex.decoder.decodeSwizzled(
SwizzleMode.SWITCH, TexFormat.ASTC_8x8, tiled, w, h,
SwitchBlockHeightAuto);
// G1T alpha atlas - pass the FINAL (half-height) dimensions:
const folded = await tex.decoder.decode(TexFormat.ETC1_RGB_A_ATLAS,
g1tData, w, h);
// --- encoding -----------------------------------------------------------
const bc1 = await tex.encoder.encode(TexFormat.BC1, rgba, w, h,
{ alphaThreshold: 200 });
const atlas = await tex.encoder.encode(TexFormat.ETC1_RGB_A_ATLAS,
rgba, w, h);
// --- unswizzling / reswizzling ------------------------------------------
const linear = await tex.swizzler.unswizzle(SwizzleMode.PS4, // tiled -> linear
TexFormat.BC7, tiled, w, h);
const tiledAgain = await tex.swizzler.reswizzle(SwizzleMode.PS4, // linear -> tiled
TexFormat.BC7, linear, w, h);
const bytes = await tex.swizzler.swizzledSize(SwizzleMode.SWITCH,
TexFormat.BC1, w, h, 4);
// --- image utilities (tex.image) ----------------------------------------
const bgra = await tex.image.convertProfile(PixelProfile.RGBA8,
PixelProfile.BGRA8, rgba);
const flipped = await tex.image.flipY(rgba, w, h); // 4 bpp default
const region = await tex.image.crop(rgba, w, h, 4, 16, 16, 64, 64);Every class also carries generated named helpers, so call sites don't need
the enums - one per format and one per swizzle mode (fully typed in the
.d.ts):
await tex.decoder.decodeBC7(data, w, h);
await tex.decoder.decodeETC1_RGB_A_ATLAS(g1tData, w, h);
await tex.decoder.decodePICA_ETC1_RGB8A4(tex3dsData, w, h); // 3DS
await tex.decoder.decodeSwizzledSwitch(TexFormat.ASTC_8x8, tiled, w, h,
SwitchBlockHeightAuto);
await tex.encoder.encodeETC2_RGBA8(rgba, w, h);
await tex.encoder.encodeBC1(rgba, w, h, { alphaThreshold: 200 });
await tex.encoder.encodedSizeASTC_4x4(w, h);
await tex.swizzler.unswizzlePS4(TexFormat.BC7, tiled, w, h); // -> linear
await tex.swizzler.reswizzlePS4(TexFormat.BC7, linear, w, h); // -> tiled
// PS5: a whole mip chain is one surface (mip tail first, mip 0 last)
const lay = await tex.swizzler.surfaceLayout(SwizzleMode.PS5, TexFormat.BC7,
256, 256, /*mips*/ 7);
// lay.totalSize, lay.mips[0].offset, lay.mips[3].inTail, ...
const mip0 = await tex.swizzler.unswizzleMip(SwizzleMode.PS5, TexFormat.BC7,
surface, 256, 256, 7, /*mip*/ 0);
// G1T has no tile-mode field: recover 4KB vs 64KB blocks from the data size
const tm = await tex.swizzler.detectPS5TileMode(TexFormat.BC7, 1024, 128, 6,
surface.byteLength);Mode names: PS4, PS5, Switch, Switch2, PSVita, X360, PSP, N3DS, WiiU, DX12_64KB.
surfaceLayout / unswizzleMip / reswizzleMip take { sliceCount, slice, arg } options (arg = a PS5TileMode for PS5).
Every method throws TexCodecError (with a .code from TexResult) on
failure. In a bundler, pass the module factory yourself:
TexCodec.load({ factory: (await import("./tex_codec.js")).default }).
For callers that manage their own buffers, the full C API is exported on the
module (tex.module) - see TexCodecModule in tex_codec_api.d.ts for
typed signatures:
- Generic:
_texc_decode,_texc_encode,_texc_encode_ex,_texc_encode_with_options(..., alphaThreshold),_texc_decode_f32,_texc_unswizzle,_texc_swizzle,_texc_swizzled_size,_texc_decode_swizzled,_texc_get_surface_layout,_texc_unswizzle_mip,_texc_reswizzle_mip, sizes/metadata,_malloc/_free. - Per format (all 45):
_texc_decode_bc7(src, srcSize, w, h, dst, dstSize),_texc_encode_etc2_rgba8(..., alphaThreshold),_texc_encoded_size_astc_12x12(w, h), … - Per swizzle mode (ps4, ps5, switch, switch2, psvita, x360, psp, n3ds,
wiiu, dx12_64kb):
_texc_unswizzle_ps4(fmt, w, h, src, srcSize, dst, dstSize, arg)(tiled→linear),_texc_reswizzle_switch(...)(linear→tiled;_texc_swizzle_<mode>is its legacy alias), and_texc_swizzled_size_wiiu(...). - Image utilities:
_texc_convert_profile,_texc_profile_name,_texc_profile_bytes_per_pixel,_texc_flip_y,_texc_flip_x,_texc_crop. - Allocation helpers:
_texc_decode_alloc,_texc_decode_f32_alloc,_texc_decode_swizzled_alloc,_texc_encode_alloc,_texc_swizzle_alloc,_texc_unswizzle_mip_alloc, with_texc_last_error()for the failure reason.
The entire API is extern "C", uses only scalar types and raw pointers, and
never throws or allocates on the caller's behalf (except the documented
*_alloc WASM helpers). texc_format / texc_swizzle_mode are plain ints.
Build with -DTEXC_BUILD_SHARED=ON and bind with P/Invoke, bindgen, or
ctypes directly against include/tex_codec.h.
include/tex_codec.h public C API
src/tex_codec.cpp dispatch, validation, buffer management
src/codecs/ bcn.cpp, etc.cpp, astc.cpp, pvrtc.cpp, atc.cpp
src/unswizzle/ platform tiling conversions
wasm/ Emscripten exports + build script
tests/ roundtrip + swizzle-identity test suite
- swizzle machinery credit: Piken / DwayneR, github.com/fdwr
- Switch 2 (Orin) GOB layout: ScanMountGoat's tegra_swizzle
- PS5 tiling: derived from AMD addrlib (MIT, GPUOpen-Drivers/pal, gfx10addrlib.cpp / gfx10SwizzlePattern.h); the pre-1.6 legacy layout is a port of id-daemon's RawTex Cooker
- Format specifications: Khronos Data Format Spec (ASTC, ETC2/EAC), Microsoft D3D BC1-7 specs, AMD ATC extension, PowerVR PVRTC documentation
- Reference codebases studied: tex-decoder, texture2ddecoder, Basis Universal, bimg, ctt