YAPF/docs/FORMAT.md
Claude a226b06368
YAPF 2: texture container with GPU blocks, wide samples and streaming
Adds format version 2 alongside version 1 (still written for plain 8-bit
images, so existing readers keep working). Full specification in
docs/FORMAT.md.

- GPU block payloads: BC1-BC7, BC6H, ETC2/EAC, ASTC 4x4-12x12, in tiles
  of 16x16 blocks. Built-in BC1/BC3/BC4/BC5/BC7/BC6H encoders and
  BC1-BC7 decoders (BC6H/BC7 bit-exact with a reference decoder).
- Lossless 16-bit, half and float pixels as byte planes over the 8-bit
  coder; order-preserving float mapping, transforms at sample width.
- Cubemaps, cube arrays, texture arrays, volumes, animation frames.
- Channel meanings, normal convention, linear mask, primaries/transfer,
  mip filter and coverage-preserving mips (yapf_tex_build_mips).
- Small mips stored first, file size in the header, surface/tile tables:
  decode from a file prefix and single tiles (virtual texturing).
- yapf-sum checksums on header, tables and every tile; needs bits so
  decoders refuse cleanly what they cannot read.
- Optional entropy stage: per-surface Huffman code books with a
  neighbourhood context (~10% smaller photos) and per-lane packing of
  GPU blocks (~3x smaller on disk); planes decoded interleaved.
- Notes: author, licence, source, tool, ICC and application tags.
- CLI: --format, --entropy, --coverage, --normal, --map, colour options,
  16-bit/HDR input, pack cube/array/volume/anim, info, verify, export.
- Tests for all of the above, including fuzzing; v1 API reads 8-bit v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-10 21:57:14 +00:00

18 KiB
Raw Blame History

YAPF file format

This is the complete specification of YAPF files. yapf.c is the reference implementation; where this text and the code disagree, that is a bug in one of them.

YAPF has two versions:

Version What it holds Who writes it
1 one 8-bit image (1–4 channels) with optional mips yapf_save / yapf_encode, and the CLI for plain 8-bit images
2 a texture: GPU blocks or lossless pixels in 8/16-bit or float, cubes, arrays, volumes, animation, channel meanings, colour description, notes, checksums, optional entropy coding yapf_tex_save / yapf_tex_encode, and the CLI whenever a v2 feature is used

Version 1 is specified at the top of yapf.h and is not repeated here, except that version 2 reuses its tiles, filters and plane payloads (the "group form") byte for byte.


Design: how YAPF 2 does things its own way

YAPF 2 is not a wrapper around someone else's container. A few ideas carry the whole format:

  1. Everything is tiles. Pixels are cut into 64 × 64 tiles, GPU blocks into tiles of 16 × 16 blocks. Each tile has its own size and checksum in a table, so any tile can be found, checked and decoded on its own: for multithreaded loading, for partial updates, and for virtual texturing / "megatexture" streaming.
  2. Small mips first. Surface data is stored from the smallest mip to the largest. The first N bytes of a file are a complete, lower resolution texture, so an engine can show something after reading a few kilobytes and stream in the large mips later. The header carries the file size, so a loader knows how much is still to come.
  3. Needs, not versions. New capabilities are announced by needs bits. A decoder refuses a file with a needs bit it does not know, and can ignore everything else safely. No more version bumps that break every reader.
  4. Wide samples are byte planes. 16-bit and float samples are split into byte planes (high bytes, low bytes) and coded with the same fast 8-bit machinery as version 1: smooth high bytes compress very well, and the decoder stays branch-light and SIMD friendly.
  5. Shared code books. The optional entropy stage keeps its Huffman tables once per surface, keyed by a small neighbourhood context, so tiles stay independent and their own headers stay tiny. For GPU blocks the tables are per byte lane, so endpoint bytes and index bytes are coded separately.
  6. Meaning travels with the data. Channel meanings, the normal-map convention, linear channels inside sRGB images, colour primaries and transfer, and how the mips were made are header fields, not file-name suffixes.

Conventions

All integers are little-endian. u8, u16, u32 and u64 are unsigned integers of that many bits. Offsets are from the start of the file.

yapf-sum

The checksum of a byte string of length n. K = 0x9E3779B97F4A7C15; all arithmetic is modulo 2⁶⁴.

lane[i] = (i + 1)·K  XOR  n                       for i = 0, 1, 2, 3
for each 8-byte word w_j of the input (little-endian; the last word is
padded with zero bytes):
    a = lane[j mod 4]
    a = (a XOR w_j) · K
    a = a XOR (a >> 32)
    lane[j mod 4] = a
h = lane[0] XOR rotl(lane[1], 16) XOR rotl(lane[2], 32) XOR rotl(lane[3], 48)
h = h · K
h = h XOR (h >> 32)
result = low 32 bits of h

Test vectors: "" → 0xd7550ed0, "YAPF" → 0xc8cc41d4, "The quick brown fox jumps over the lazy dog" → 0xbce4f410.


Version 2 layout

HEADER            64 bytes
SURFACE TABLE     16 bytes per surface
FRAME TABLE       4 bytes per frame, only when frames > 1
NOTES             notes size bytes
SURFACE DATA      in stream order: smallest mip first

The front matter is everything before the surface data: 64 + 16·surfaces + (frames > 1 ? 4·frames : 0) + notes size bytes.

Header

Offset Size Field Notes
0x00 4 magic "YAPF"
0x04 1 version 2
0x05 1 channels 1–4
0x06 1 format block format (block payload) or GPU hint (pixel payload); see GPU formats
0x07 1 mip levels 1–16
0x08 1 flags bit 0 premultiplied alpha, bit 1 sRGB, bit 2 coverage-preserving mips; other bits 0
0x09 1 payload 0 pixels (lossless tiles), 1 GPU blocks
0x0A 1 sample pixel payload: 0 u8, 1 u16, 2 half float, 3 float. Block payload: type of the decoded texels (0, or 2 for BC6H)
0x0B 1 shape 0 2D, 1 cube, 2 volume
0x0C 4 width 1–65535
0x10 4 height 1–65535; cubes: width = height
0x14 4 depth volumes: 1–65535; otherwise 1
0x18 2 layers ≥ 1 (array layers; with a cube, a cube array); volumes: 1
0x1A 2 frames ≥ 1 (animation frames)
0x1C 4 needs see Needs
0x20 4 channel map meaning of channels 0–3, see Channel meanings
0x24 1 linear mask bit c: channel c is linear even when the sRGB flag is set
0x25 1 normal bits 0–1 convention (0 none, 1 OpenGL +Y, 2 DirectX −Y); bits 2–3 encoding (0 XYZ, 1 XY with Z reconstructed, 2 octahedral)
0x26 1 primaries 0 unspecified, 1 BT.709/sRGB, 2 Display P3, 3 BT.2020, 4 ACES AP0, 5 ACEScg (AP1), 6 Adobe RGB
0x27 1 transfer 0 unspecified (the sRGB flag decides), 1 sRGB, 2 linear, 3 gamma 2.2, 4 PQ (ST 2084), 5 HLG, 6 BT.1886
0x28 1 mip filter bits 0–6: 0 unknown, 1 box, 2 tent, 3 Kaiser, 4 Lanczos, 5 Mitchell, 6 authored; bit 7: sRGB channels were filtered in linear light
0x29 1 alpha ref with the coverage flag: alpha-test reference 0–255
0x2A 2 loop count animation repeats, 0 = forever
0x2C 4 notes size bytes of the notes area
0x30 8 file size total bytes of the file
0x38 4 tables check yapf-sum of bytes [0x40, front matter end)
0x3C 4 header check yapf-sum of bytes [0x00, 0x3C)

The header check covers the tables check, so the whole front matter is protected.

Surfaces

A surface is one mip level of one face of one layer of one frame. With faces = 6 for cubes (order +X, −X, +Y, −Y, +Z, −Z) and 1 otherwise, surface number

s = ((frame · layers + layer) · faces + face) · mip_levels + mip

Mip m is max(1, width >> m) × max(1, height >> m), and for volumes max(1, depth >> m) slices deep. A volume surface holds all its slices.

Surface table entry (16 bytes, one per surface, in surface order):

Offset Size Field
+0 8 offset of the surface data
+8 4 size of the surface data
+12 4 tile-table check: yapf-sum of the surface's book size, book and tile table

Stream order. Writers store surface data for mip mip_levels − 1 first (all frames, layers and faces in surface order), then the next larger mip, and so on, ending with mip 0. Readers must use the offsets and must not rely on the order. The number of leading bytes that hold every surface of mip m and smaller is the largest offset + size over those surfaces.

Frame table

When frames > 1: one u32 per frame, its display time in microseconds.

Notes

A sequence of entries that exactly fills the notes area:

tag      4 bytes
length   u32
data     length bytes
Tag Content
AUTH author (UTF-8)
LICN licence (UTF-8, e.g. an SPDX id)
SRCE source / origin (UTF-8, e.g. a URL or path)
TOOL the tool that wrote the file (UTF-8)
TITL title (UTF-8)
DESC description (UTF-8)
DATE date, ISO 8601 (UTF-8)
ICCP an ICC colour profile
ORNT orientation, 1 byte: bits 0–1 clockwise quarter turns, bit 2 mirror left–right after turning
DPI 2 × u32: horizontal and vertical dots per inch × 1000
CLL 2 × u16: maximum content light level, maximum frame-average light level (cd/m²)

Tags that begin with a lowercase letter belong to applications (for example keel). Readers ignore tags they do not know. Tags may repeat; order is kept.

Needs

Bit Name Set when
0 WIDE pixel payload with sample ≠ u8
1 BLOCKS block payload
2 ENTROPY any surface has a code book
3 LAYOUT shape ≠ 2D, or layers > 1
4 FRAMES frames > 1

A decoder must refuse a file that sets a needs bit it does not implement (this library: any bit above 4). A file that uses a feature without its bit is corrupt. Everything that is not a needs feature — channel map, colour fields, mip filter, notes, frame times — may be ignored.

A decoder that only implements 8-bit 2D pixels can therefore read every version 2 file whose needs are 0, and cleanly refuse the rest.


Surface data

book size   u32            0 when the surface has no code book
book        book size bytes
tile table  8 bytes per tile:  u32 size (bit 31: packed),  u32 yapf-sum of the tile bytes
tiles       back to back; their sizes must add up exactly to the rest of the surface

Tiles are numbered slice by slice, rows top to bottom, tiles left to right. For pixels the grid is ceil(w / 64) × ceil(h / 64) tiles per slice (edge tiles are smaller, no padding); for blocks it is ceil(bx / 16) × ceil(by / 16) tiles of 16 × 16 blocks, where bx = ceil(w / block width) and by = ceil(h / block height).

Bit 31 of a tile size (packed) is allowed only in block tiles of a surface with a code book.


Pixel tiles

Let B be the bytes per sample (1, 2 or 4), ch the channels, and the tile tw × th pixels.

Tile header

byte 0     0x80          stored: the raw samples follow, interleaved in
                         channel order, each B bytes little-endian;
                         exactly tw·th·ch·B bytes
           otherwise     coded:
             bits 0–3    B = 1: bit c set = plane c is constant;  B > 1: 0
             bit 4       colour transform subtract-green
             bit 5       an entropy mask follows
             bit 6       no colour transform
             bit 7       0
           bits 4 and 6 are exclusive and need ch >= 3; with neither
           set, an RGB(A) tile uses YCoCg-R
B > 1      u16 constant mask, bit p = plane p is constant
bit 5      entropy mask: u8 (B = 1) or u16 (B > 1); bit p = plane p is
           entropy coded.  It must not include constant planes.
then       one value byte per constant plane, in plane order;
           one payload per other plane, in plane order;
           the tile ends exactly where its last payload ends

With B = 1 and neither bit 5 nor bit 6 set, a coded tile is exactly a version 1 tile.

Planes

The colour transform turns R, G, B (+ A) into transformed channels; it is skipped for 1–2 channels and with bit 6.

  • YCoCg-R at the sample width n = 8·B bits, all arithmetic mod 2ⁿ, >> an arithmetic shift of the n-bit two's complement value, h = 2ⁿ⁻¹:

    forward:  o = R − B;  t = B + (o >> 1);  q = G − t;
              Y = t + (q >> 1);  Co = o + h;  Cg = q + h
    inverse:  o = Co − h;  q = Cg − h;  t = Y − (q >> 1);
              G = q + t;  B = t − (o >> 1);  R = B + o
    
  • subtract-green: G, R − G + h, B − G + h (+ A), mod 2ⁿ.

Before the transform, float samples (half or float) are mapped to integers that keep their order: if the sign bit is set every bit is flipped, otherwise the sign bit is set. The decoder undoes this after the inverse transform.

Transformed channel c gives B planes: plane c·B + k holds byte B − 1 − k of the value (the most significant byte first). For B = 1 the planes are the transformed channels.

Plane payloads

A plane payload has one of two forms, chosen per plane by the entropy mask.

Group form — exactly the version 1 plane payload: a filter byte per band of 8 rows, 4-bit control codes, and groups of 8 residuals at the group's bit width. See yapf.h.

Entropy form

filter[ceil(th / 8)]     as in the group form
u16 L                    bytes of the bit stream
L bytes                  the folded residuals of the plane in raster order

Each residual is coded with table (slot, context) of the surface's code book:

  • slot = t · 16 + p, where p is the plane and t is 1 with subtract-green, 2 with no transform, else 0;
  • context = min(6, floor(log2(s + 1))), s = left + above, the folded residuals of the left and upper neighbours in this plane (0 outside the tile) — so 0, 1–2, 3–6, 7–14, 15–30, 31–62, 63 and up.

The stream must end in its last byte. Folded residuals then go through the filters exactly as in the group form.


Block tiles

A block tile of tbw × tbh blocks of S bytes is either

  • raw: the blocks, rows top to bottom, exactly tbw·tbh·S bytes — as the GPU takes them; or
  • packed (bit 31 of its size): one bit stream holding byte 0 of every block (in raster order), then byte 1 of every block, …, byte S − 1. Byte lane k is coded with table (slot k, context 0) of the surface's code book. The stream must end in the last byte of the tile.

Decoded block surfaces are row-major blocks, slice after slice, ready for glCompressedTexImage* / vkCmdCopyBufferToImage.


Code book

u16 count                      number of tables, 1–336
count × {
    u8 slot                    0–47
    u8 context                 0–6
    code table
}

Each (slot, context) pair appears at most once. The book must fill its size exactly.

Code table: u8 n − 1, then n 4-bit code lengths (symbols 0 … n − 1, low nibble first, an odd count padded with a zero nibble). Length 0 = no code; otherwise 1–11. At least one code; Kraft sum ≤ 1. Codes are canonical — by length, then by symbol, the first code of each length one more than the last code of the previous length, shifted left — and stored least significant bit first (the first bit of a code is the lowest unread bit of the stream), so a decoder can look up 11 bits at a time.


GPU formats

Pixel payload hints (format field):

Value Name Vulkan
0x00 RGBA8 R8G8B8A8_UNORM
0x01 RGB8 R8G8B8_UNORM
0x02 RG8 R8G8_UNORM
0x03 R8 R8_UNORM
0x04 SRGB8_A8 R8G8B8A8_SRGB
0x05 SRGB8 R8G8B8_SRGB
0x06–0x09 RGBA16, RGB16, RG16, R16 R16G16B16A16_UNORM … R16_UNORM
0x0A–0x0D RGBA16F, RGB16F, RG16F, R16F R16G16B16A16_SFLOAT … R16_SFLOAT
0x0E–0x11 RGBA32F, RGB32F, RG32F, R32F R32G32B32A32_SFLOAT … R32_SFLOAT

Block formats (block payload). With the sRGB flag, use the _SRGB variant where one exists.

Value Name Block Bytes Vulkan / DXGI
0x40 BC1 4×4 8 BC1_RGB(A)_UNORM / BC1_UNORM
0x41 BC2 4×4 16 BC2_UNORM
0x42 BC3 4×4 16 BC3_UNORM
0x43 BC4 4×4 8 BC4_UNORM
0x44 BC4_SNORM 4×4 8 BC4_SNORM
0x45 BC5 4×4 16 BC5_UNORM
0x46 BC5_SNORM 4×4 16 BC5_SNORM
0x47 BC6H 4×4 16 BC6H_UFLOAT / BC6H_UF16
0x48 BC6H_SF 4×4 16 BC6H_SFLOAT / BC6H_SF16
0x49 BC7 4×4 16 BC7_UNORM
0x50 ETC2_RGB8 4×4 8 ETC2_R8G8B8_UNORM
0x51 ETC2_RGB8A1 4×4 8 ETC2_R8G8B8A1_UNORM
0x52 ETC2_RGBA8 4×4 16 ETC2_R8G8B8A8_UNORM
0x53 EAC_R11 4×4 8 EAC_R11_UNORM
0x54 EAC_R11_SNORM 4×4 8 EAC_R11_SNORM
0x55 EAC_RG11 4×4 16 EAC_R11G11_UNORM
0x56 EAC_RG11_SNORM 4×4 16 EAC_R11G11_SNORM
0x60–0x6D ASTC 4×4, 5×4, 5×5, 6×5, 6×6, 8×5, 8×6, 8×8, 10×5, 10×6, 10×8, 10×10, 12×10, 12×12 as named 16 ASTC_*_UNORM (HDR: transfer linear, sample half)

Channel meanings

Value Meaning Value Meaning
0 default for the channel count (Y / YA / RGB / RGBA) 11 metalness
1 red 12 ambient occlusion
2 green 13 height
3 blue 14 specular
4 alpha 15 emissive
5 luma 16 opacity (alpha-test mask)
6 normal X 17 mask
7 normal Y 18 cavity
8 normal Z 19 thickness
9 roughness 20 id
10 gloss (1 − roughness) 255 unused

Alpha (an explicit 4 or 16, or the last channel of a 2- or 4-channel image with default meanings) is always linear. The linear mask marks further channels that are linear inside an sRGB image, such as masks packed next to colour.


Mips

mip filter records how the mips were made. This library's yapf_tex_build_mips uses a 2 × 2 box (2 × 2 × 2 for volumes, edges repeat), averages sRGB 8-bit channels in linear light (bit 7), and with an alpha reference rescales alpha on every mip so that the share of texels with alpha above the reference stays that of mip 0 (coverage flag, alpha ref). Alpha-tested foliage and fences then keep their density at a distance instead of thinning out.


Validation summary

A decoder rejects a file when:

  • the magic, version, a size, shape or count is out of range, or a cube is not square;
  • the header or tables check does not match (unless the reader was told to trust the file);
  • a needs bit is unknown (unsupported), or a feature is used without its needs bit (corrupt);
  • a surface lies outside the file, its book is malformed or present without ENTROPY, or its tiles do not fill it exactly;
  • a tile's yapf-sum does not match, or the tile does not decode to exactly its size (reserved bits, invalid filters, codes, masks or lengths).

Because every structure is validated before use, corrupt or hostile input is rejected, never read out of bounds.