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

450 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`](../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](#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](#needs) |
| 0x20 | 4 | channel map | meaning of channels 0–3, see [Channel meanings](#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.