mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 04:54:26 +00:00
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
This commit is contained in:
parent
5642b8c85d
commit
a226b06368
7 changed files with 5317 additions and 237 deletions
138
README.md
138
README.md
|
|
@ -14,10 +14,17 @@ encoder does the hard work, so the decoder only follows instructions.
|
|||
and natural textures — the price of having no entropy coder.
|
||||
- **Made for games and tools too**: mipmaps, GPU format hint, sRGB /
|
||||
premultiplied flags, and random access to 64×64 tiles.
|
||||
- **A real texture format (YAPF 2)**: GPU-ready BC1–BC7 / BC6H / ETC2 /
|
||||
ASTC blocks, 16-bit and float pixels, cubemaps, arrays and volumes,
|
||||
channel meanings (normal X/Y, roughness, AO, …), coverage-preserving
|
||||
mips, small-mips-first streaming, per-tile checksums, notes and
|
||||
animation — see [Textures](#textures-yapf-2).
|
||||
- **Tiny and portable**: two C99 files, no dependencies. Plus a CLI, Python,
|
||||
JavaScript and plugins for the tools you already use.
|
||||
|
||||
This is **YAPF version 1**, the first stable format.
|
||||
Two format versions: **version 1** (an 8-bit image, the first stable
|
||||
format) and **version 2** (the texture container). Plain 8-bit images are
|
||||
still written as version 1, so every existing YAPF reader keeps working.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -67,7 +74,64 @@ yapf compare photo.png ui.png # size and decode speed vs the original fo
|
|||
```
|
||||
|
||||
Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
|
||||
`--premult` (alpha is premultiplied).
|
||||
`--premult` (alpha is premultiplied), and the texture options below.
|
||||
|
||||
---
|
||||
|
||||
## Textures (YAPF 2)
|
||||
|
||||
Version 2 turns YAPF into a texture container for engines, designed our own
|
||||
way rather than copied from KTX2 or DDS (the design and the full
|
||||
specification are in [docs/FORMAT.md](docs/FORMAT.md)):
|
||||
|
||||
| Need | YAPF 2 |
|
||||
| :--- | :----- |
|
||||
| **GPU-compressed payloads** | BC1, BC2, BC3, BC4, BC5, BC6H, BC7, ETC2 / EAC and ASTC 4×4–12×12 blocks, stored as the GPU takes them. Built-in encoders for BC1, BC3, BC4, BC5, BC7 and BC6H; or bring blocks from your own encoder. |
|
||||
| **More than 8 bits** | lossless 16-bit, half and float pixels, coded as byte planes with the same fast machinery |
|
||||
| **Cubes, arrays, volumes** | cubemaps and cube arrays (reflection probes, skies), texture arrays (terrain layers), 3D textures (colour-grading LUTs) |
|
||||
| **Channel meaning** | per channel: normal X/Y/Z, roughness, metalness, AO, height, opacity, …; OpenGL or DirectX normal convention; linear channels inside sRGB images |
|
||||
| **Streaming** | small mips are stored first: a file prefix is a complete low-resolution texture (`yapf info` prints the prefix sizes); single 64 × 64 tiles decode on their own for virtual texturing |
|
||||
| **Alpha-tested mips** | `--coverage REF` keeps fences and foliage from thinning out with distance; the filter used is recorded |
|
||||
| **Entropy coding (optional)** | `--entropy`: shared per-surface Huffman code books with a neighbourhood context — about 10 % smaller photos, about 3× smaller GPU blocks on disk; tiles stay independent |
|
||||
| **Metadata** | notes (author, licence, source, tool, ICC profile, orientation, DPI, HDR light levels, your own tags), colour primaries and transfer (sRGB, Display P3, BT.2020, PQ, HLG, …) |
|
||||
| **Integrity** | yapf-sum checksums on the header, tables and every tile; *needs* bits make an older decoder refuse cleanly what it cannot read |
|
||||
| **Animation** | frames with per-frame durations and a loop count |
|
||||
|
||||
```sh
|
||||
yapf albedo.png albedo.yapf --format bc7 --mips # GPU-ready colour
|
||||
yapf normal.png normal.yapf --format bc5 --mips --normal dx # normal map, DirectX Y
|
||||
yapf orm.png orm.yapf --map ao,rough,metal --linear --mips # packed masks
|
||||
yapf foliage.png foliage.yapf --format bc7 --coverage 128 # alpha-tested mips
|
||||
yapf sky.hdr sky.yapf --format bc6h --mips # HDR, no RGBM tricks
|
||||
yapf sky.hdr sky_lossless.yapf # lossless half float
|
||||
yapf pack cube px.png nx.png py.png ny.png pz.png nz.png probe.yapf --mips
|
||||
yapf pack array grass.png rock.png sand.png terrain.yapf --format bc7 --mips
|
||||
yapf pack volume lut_00.png lut_01.png ... grading.yapf --linear
|
||||
yapf pack anim f0.png f1.png f2.png fx.yapf --frame-ms 50
|
||||
yapf albedo.png albedo.yapf --entropy --author "Keel" --license CC-BY-4.0
|
||||
yapf info albedo.yapf # everything in the header, notes, stream prefix sizes
|
||||
yapf verify albedo.yapf # check every checksum
|
||||
yapf probe.yapf face2.png --face 2 --mip 1 # export one surface
|
||||
```
|
||||
|
||||
In C:
|
||||
|
||||
```c
|
||||
int err;
|
||||
yapf_texture_t *t = yapf_tex_load("albedo.yapf", 0, 0, &err);
|
||||
for (uint32_t m = 0; m < t->mip_levels; m++) {
|
||||
uint32_t s = yapf_tex_surface(t, 0, 0, 0, m), w, h;
|
||||
yapf_tex_mip_size(t, m, &w, &h, NULL);
|
||||
if (t->payload == YAPF_PAYLOAD_BLOCKS) /* e.g. YAPF_GPU_BC7 */
|
||||
glCompressedTexImage2D(GL_TEXTURE_2D, m, gl_format(t->format, t->flags),
|
||||
w, h, 0, (GLsizei)yapf_tex_surface_bytes(t, m), t->data[s]);
|
||||
}
|
||||
yapf_tex_free(t);
|
||||
```
|
||||
|
||||
The language bindings and the JavaScript port speak version 1; the C
|
||||
library's v1 functions also read 8-bit version 2 files (2D, cube face 0,
|
||||
array layer 0), so plugins built on them open those too.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -198,6 +262,47 @@ void yapf_free(yapf_image_t *img);
|
|||
```
|
||||
Releases memory returned by the loaders. Passing `NULL` is safe.
|
||||
|
||||
### Textures (version 2)
|
||||
|
||||
```c
|
||||
yapf_texture_t *yapf_tex_load(const char *file, int flags, int threads, int *err);
|
||||
yapf_texture_t *yapf_tex_load_memory(const void *buf, size_t size, int flags, int threads, int *err);
|
||||
int yapf_tex_save(const char *file, const yapf_texture_t *t, int flags);
|
||||
int yapf_tex_encode(const yapf_texture_t *t, int flags, void **out, size_t *size);
|
||||
yapf_texture_t *yapf_tex_create(const yapf_texture_t *desc); /* zeroed storage */
|
||||
void yapf_tex_free(yapf_texture_t *t);
|
||||
```
|
||||
A texture describes its shape (width, height, depth, layers, frames,
|
||||
cube / volume, mips), storage (pixels with u8 / u16 / half / float samples,
|
||||
or GPU blocks), meaning (channel map, linear mask, normal convention,
|
||||
primaries, transfer, mip filter, alpha reference) and holds `data[s]` per
|
||||
surface `s = yapf_tex_surface(t, frame, layer, face, mip)`. Load flag
|
||||
`YAPF_LOAD_TRUST` skips checksum checks; encode flag `YAPF_ENC_ENTROPY`
|
||||
allows the entropy stage.
|
||||
|
||||
```c
|
||||
yapf_texture_t *yapf_tex_open(const void *buf, size_t avail, int flags, int *err);
|
||||
uint64_t yapf_tex_stream_bytes(const yapf_texture_t *t, uint32_t first_mip);
|
||||
int yapf_tex_decode_surface(yapf_texture_t *t, const void *buf, size_t avail, uint32_t s, int threads);
|
||||
int yapf_tex_decode_tile(const yapf_texture_t *t, const void *buf, size_t avail, uint32_t s,
|
||||
uint32_t tx, uint32_t ty, uint32_t slice, void *out, size_t stride,
|
||||
uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h);
|
||||
```
|
||||
Streaming and virtual texturing: open a file from its first bytes, ask how
|
||||
many bytes hold mip *m* and smaller, decode surfaces as their bytes arrive
|
||||
(`YAPF_ERR_TRUNCATED` until then), or decode single tiles.
|
||||
|
||||
```c
|
||||
int yapf_tex_build_mips(yapf_texture_t *t, uint32_t levels, uint8_t alpha_ref);
|
||||
int yapf_tex_compress(const yapf_texture_t *src, uint8_t format, yapf_texture_t **out);
|
||||
int yapf_blocks_decode(uint8_t format, const void *blocks, uint32_t w, uint32_t h, void *out);
|
||||
int yapf_tex_add_note(yapf_texture_t *t, const char tag[4], const void *data, uint32_t size);
|
||||
const yapf_note_t *yapf_tex_find_note(const yapf_texture_t *t, const char tag[4]);
|
||||
```
|
||||
Mip chains (linear-light sRGB, optional coverage preservation), the
|
||||
built-in BCn encoders, BC1–BC7 decoding for previews, and notes. The
|
||||
complete list, with sizes and helpers, is in [`yapf.h`](yapf.h).
|
||||
|
||||
### Return codes
|
||||
|
||||
| Constant | Value | Meaning |
|
||||
|
|
@ -206,6 +311,10 @@ Releases memory returned by the loaders. Passing `NULL` is safe.
|
|||
| `YAPF_ERR_INVALID` | –1 | NULL or logically invalid argument |
|
||||
| `YAPF_ERR_IO` | –2 | File could not be opened or written |
|
||||
| `YAPF_ERR_OOM` | –3 | Out of memory |
|
||||
| `YAPF_ERR_CORRUPT` | –4 | Corrupt or not a YAPF file |
|
||||
| `YAPF_ERR_UNSUPPORTED` | –5 | Needs a feature this decoder lacks |
|
||||
| `YAPF_ERR_CHECKSUM` | –6 | A checksum does not match (v2) |
|
||||
| `YAPF_ERR_TRUNCATED` | –7 | The data is not (yet) all there |
|
||||
|
||||
### Channel constants
|
||||
|
||||
|
|
@ -256,7 +365,8 @@ domain [stb](https://github.com/nothings/stb) headers in `third_party/`.
|
|||
7. **Independent tiles** — any tile can be decoded on its own, in any order,
|
||||
on any thread.
|
||||
|
||||
The complete specification is the comment at the top of [`yapf.h`](yapf.h):
|
||||
The version 1 specification is the comment at the top of [`yapf.h`](yapf.h);
|
||||
version 2 (textures) is specified in [docs/FORMAT.md](docs/FORMAT.md). Version 1:
|
||||
|
||||
```
|
||||
HEADER (20 bytes) "YAPF", version 1, channels, GPU hint, mip count,
|
||||
|
|
@ -273,7 +383,7 @@ TILE (64×64) header byte: stored raw, or constant-plane mask + colour tr
|
|||
## Testing
|
||||
|
||||
```sh
|
||||
make test # C suite: 786 checks, plus JavaScript if node is installed
|
||||
make test # C suite: 2631 checks, plus JavaScript if node is installed
|
||||
node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input
|
||||
```
|
||||
|
||||
|
|
@ -286,6 +396,16 @@ What has been verified:
|
|||
C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
|
||||
ThreadSanitizer;
|
||||
- thousands of corrupted and truncated files are rejected without crashing;
|
||||
- version 2: lossless round-trips of u8 / u16 / half / float pixels with 1–4
|
||||
channels, with and without entropy coding; cubes, cube arrays, arrays,
|
||||
volumes and animations with notes and every description field; GPU block
|
||||
payloads; checksum, needs-bit and truncation handling; streaming from file
|
||||
prefixes and single-tile decoding; BCn encoder quality; linear-light and
|
||||
coverage-preserving mips; fuzzing of entropy-coded and packed-block files;
|
||||
- the BC1–BC7 decoders were checked against an independent reference decoder
|
||||
(bcdec) on 300,000 random blocks: BC6H and BC7 bit-exact, BC1–BC5 within
|
||||
the ±1 rounding the formats allow (a development check, not part of
|
||||
`make test`);
|
||||
- every language binding runs its example; JavaScript, Python, C++, C#,
|
||||
Rust, Go, Java and Lua re-encode the sample to byte-identical output;
|
||||
- the GIMP plugin in GIMP 3.0.8 and the Blender extension in Blender 5.1;
|
||||
|
|
@ -293,14 +413,20 @@ What has been verified:
|
|||
|
||||
**Not yet verified:** running on a Mac (macOS builds are cross-compiled
|
||||
only), Windows on ARM, and running inside Photoshop, VS Code, Figma or a
|
||||
live Dolphin / GNOME file manager.
|
||||
live Dolphin / GNOME file manager. The version 2 code has been run on
|
||||
Linux x64 (with and without SIMD, as C and C++, under ASan, UBSan and
|
||||
TSan); its NEON path is compiled for ARM64 but has not been run yet, and
|
||||
the Windows builds have not been rerun. ETC2 / EAC / ASTC blocks are
|
||||
stored and loaded, but there is no built-in encoder or preview decoder
|
||||
for them.
|
||||
|
||||
---
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
yapf.h, yapf.c the library (format spec in yapf.h)
|
||||
yapf.h, yapf.c the library (v1 spec in yapf.h)
|
||||
docs/FORMAT.md the full format specification, version 2
|
||||
tools/yapf_cli.c the `yapf` command-line tool
|
||||
tools/build_release.sh builds every download for all platforms
|
||||
tests/test_yapf.c C test suite
|
||||
|
|
|
|||
450
docs/FORMAT.md
Normal file
450
docs/FORMAT.md
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
# 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.
|
||||
|
|
@ -22,6 +22,14 @@ the same native code, so they are all about equally fast; the JavaScript
|
|||
port needs no native library at all. Every example also re-encodes the
|
||||
image and checks that the result is byte-identical to the original file.
|
||||
|
||||
**YAPF 2 textures.** The bindings wrap the version 1 API (`yapf_load`,
|
||||
`yapf_save`, …). Through it they also read 8-bit version 2 files (2D, or
|
||||
face 0 / layer 0 of cubes and arrays). GPU blocks, 16-bit / float pixels,
|
||||
volumes and the other version 2 features are available from C and C++
|
||||
through the `yapf_tex_*` functions in [`yapf.h`](../yapf.h) (see
|
||||
[FORMAT.md](FORMAT.md)); any language with a C FFI can call them the same
|
||||
way. The JavaScript port reads and writes version 1 only.
|
||||
|
||||
## Getting the native library
|
||||
|
||||
Bindings other than C, C++, Rust, Go, Zig and JavaScript load the shared
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
* Round-trips many sizes, channel counts and image kinds, checks mip
|
||||
* chains, single- vs multi-threaded decode, the bundled sample file, and
|
||||
* that corrupted or truncated files are rejected without crashing.
|
||||
* Version 2: wide samples, shapes, GPU blocks, notes, checksums, needs,
|
||||
* streaming, tile decode, block encoders and mip generation.
|
||||
* Exit code 0 means every test passed.
|
||||
*/
|
||||
#include "../yapf.h"
|
||||
|
|
@ -14,6 +16,7 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
/* FNV-1a of the decoded pixels of other/YAPF.YAPF (the YAPF logo). */
|
||||
#define SAMPLE_HASH 0x8891c2b4u
|
||||
|
|
@ -171,6 +174,494 @@ static uint32_t fnv(const uint8_t *p, size_t n) {
|
|||
return h;
|
||||
}
|
||||
|
||||
|
||||
/* ======================================================================
|
||||
* Version 2
|
||||
* ====================================================================== */
|
||||
|
||||
static yapf_texture_t desc(uint32_t w, uint32_t h, int ch, int sample) {
|
||||
yapf_texture_t d;
|
||||
memset(&d, 0, sizeof(d));
|
||||
d.width = w; d.height = h; d.depth = 1; d.layers = 1; d.frames = 1;
|
||||
d.mip_levels = 1; d.channels = (uint8_t)ch; d.sample = (uint8_t)sample;
|
||||
return d;
|
||||
}
|
||||
|
||||
/* Fills every surface: noise, gradient or noisy curves, in the sample
|
||||
* type; block payloads get bytes with some structure. */
|
||||
static void tex_fill(yapf_texture_t *t, int kind) {
|
||||
uint32_t S = yapf_tex_surface_count(t);
|
||||
for (uint32_t s = 0; s < S; s++) {
|
||||
uint32_t m = s % t->mip_levels, w, h, d;
|
||||
yapf_tex_mip_size(t, m, &w, &h, &d);
|
||||
uint8_t *p = (uint8_t *)t->data[s];
|
||||
if (t->payload == YAPF_PAYLOAD_BLOCKS) {
|
||||
size_t n = yapf_tex_surface_bytes(t, m);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
p[i] = kind ? (uint8_t)(i % 16 < 8 ? (i / 64) & 0xFF : rnd()) : (uint8_t)rnd();
|
||||
continue;
|
||||
}
|
||||
size_t cnt = (size_t)w * h * d * t->channels;
|
||||
for (size_t i = 0; i < cnt; i++) {
|
||||
size_t px = i / t->channels;
|
||||
uint32_t x = (uint32_t)(px % w), y = (uint32_t)((px / w) % h);
|
||||
int c = (int)(i % t->channels);
|
||||
double v = kind == 0 ? (rnd() % 1000) / 1000.0
|
||||
: kind == 1 ? (x * 3 + y * 5 + c * 40) % 600 / 600.0
|
||||
: ((x * x + y * y) / 50 % 700 + rnd() % 5) / 705.0;
|
||||
switch (t->sample) {
|
||||
case YAPF_SAMPLE_U8: p[i] = (uint8_t)(v * 255); break;
|
||||
case YAPF_SAMPLE_U16: ((uint16_t *)p)[i] = (uint16_t)(v * 65535); break;
|
||||
case YAPF_SAMPLE_F16: ((uint16_t *)p)[i] = yapf_float_to_half((float)(v * 20 - 2)); break;
|
||||
default: ((float *)p)[i] = (float)(v * 100 - 10) + (kind == 0 ? (float)rnd() : 0.0f); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int tex_equal(const yapf_texture_t *a, const yapf_texture_t *b) {
|
||||
uint32_t S = yapf_tex_surface_count(a);
|
||||
if (S != yapf_tex_surface_count(b)) return 0;
|
||||
for (uint32_t s = 0; s < S; s++)
|
||||
if (!a->data[s] || !b->data[s] ||
|
||||
memcmp(a->data[s], b->data[s], yapf_tex_surface_bytes(a, s % a->mip_levels)) != 0)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Encode, decode, compare; returns the encoded file (caller frees). */
|
||||
static uint8_t *tex_roundtrip(yapf_texture_t *t, int flags, size_t *len, const char *what) {
|
||||
void *buf = NULL;
|
||||
*len = 0;
|
||||
int e = yapf_tex_encode(t, flags, &buf, len);
|
||||
CHECK(e == YAPF_OK, "v2 encode %s (%d)", what, e);
|
||||
if (e != YAPF_OK) return NULL;
|
||||
int err = 0;
|
||||
yapf_texture_t *a = yapf_tex_load_memory(buf, *len, 0, 1, &err);
|
||||
yapf_texture_t *b = yapf_tex_load_memory(buf, *len, 0, 0, &err);
|
||||
CHECK(a && tex_equal(a, t), "v2 roundtrip %s", what);
|
||||
CHECK(b && tex_equal(b, t), "v2 roundtrip (threads) %s", what);
|
||||
yapf_tex_free(a);
|
||||
yapf_tex_free(b);
|
||||
uint8_t *copy = (uint8_t *)malloc(*len);
|
||||
memcpy(copy, buf, *len);
|
||||
yapf_free_buffer(buf);
|
||||
return copy;
|
||||
}
|
||||
|
||||
static void v2_pixels(void) {
|
||||
static const int sizes[][2] = { {1, 1}, {7, 3}, {64, 64}, {65, 63}, {130, 70}, {200, 201} };
|
||||
static const char *sn[] = { "u8", "u16", "f16", "f32" };
|
||||
for (size_t si = 0; si < sizeof(sizes) / sizeof(sizes[0]); si++)
|
||||
for (int ch = 1; ch <= 4; ch++)
|
||||
for (int sample = 0; sample < 4; sample++)
|
||||
for (int kind = 0; kind < 3; kind++)
|
||||
for (int fl = 0; fl < 2; fl++) {
|
||||
yapf_texture_t d = desc((uint32_t)sizes[si][0], (uint32_t)sizes[si][1], ch, sample);
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
char what[96];
|
||||
sprintf(what, "%dx%d ch%d %s kind%d%s", sizes[si][0], sizes[si][1], ch,
|
||||
sn[sample], kind, fl ? " entropy" : "");
|
||||
tex_fill(t, kind);
|
||||
size_t len;
|
||||
free(tex_roundtrip(t, fl ? YAPF_ENC_ENTROPY : 0, &len, what));
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
}
|
||||
|
||||
/* Shapes, frames, notes and every description field survive. */
|
||||
static void v2_layout(void) {
|
||||
struct { int w, h, ch, sample, shape, layers, frames, mips, depth; } c[] = {
|
||||
{ 64, 64, 4, 0, YAPF_SHAPE_CUBE, 1, 1, 7, 1 },
|
||||
{ 32, 32, 3, 2, YAPF_SHAPE_CUBE, 2, 1, 6, 1 },
|
||||
{ 40, 30, 4, 0, YAPF_SHAPE_2D, 5, 1, 3, 1 },
|
||||
{ 33, 17, 1, 1, YAPF_SHAPE_VOLUME, 1, 1, 4, 9 },
|
||||
{ 50, 50, 4, 0, YAPF_SHAPE_2D, 1, 6, 1, 1 },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(c) / sizeof(c[0]); i++) {
|
||||
yapf_texture_t d = desc((uint32_t)c[i].w, (uint32_t)c[i].h, c[i].ch, c[i].sample);
|
||||
d.shape = (uint8_t)c[i].shape; d.layers = (uint16_t)c[i].layers; d.frames = (uint16_t)c[i].frames;
|
||||
d.mip_levels = (uint8_t)c[i].mips; d.depth = (uint32_t)c[i].depth;
|
||||
d.channel_map[0] = YAPF_CH_NORMAL_X; d.channel_map[1] = YAPF_CH_ROUGHNESS;
|
||||
d.linear_mask = 0x08; d.normal = YAPF_NORMAL_DX | YAPF_NORMAL_XY;
|
||||
d.primaries = YAPF_PRIMARIES_DISPLAY_P3; d.transfer = YAPF_TRANSFER_PQ;
|
||||
d.mip_filter = YAPF_MIP_KAISER; d.alpha_ref = 77; d.loop_count = 3;
|
||||
d.flags = YAPF_FLAG_SRGB | YAPF_FLAG_COVERAGE;
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
tex_fill(t, 2);
|
||||
if (t->frames > 1) for (int f = 0; f < t->frames; f++) t->frame_us[f] = 16667u * (uint32_t)(f + 1);
|
||||
yapf_tex_add_note(t, "AUTH", "Keel", 4);
|
||||
yapf_tex_add_note(t, "ICCP", "\0\1\2", 3);
|
||||
size_t len;
|
||||
char what[32];
|
||||
sprintf(what, "layout case %d", (int)i);
|
||||
uint8_t *file = tex_roundtrip(t, (int)(i & 1), &len, what);
|
||||
yapf_texture_t *b = file ? yapf_tex_load_memory(file, len, 0, 1, NULL) : NULL;
|
||||
int ok = b && b->shape == d.shape && b->layers == d.layers && b->frames == d.frames &&
|
||||
b->depth == d.depth && b->mip_levels == d.mip_levels &&
|
||||
memcmp(b->channel_map, d.channel_map, 4) == 0 && b->linear_mask == 0x08 &&
|
||||
b->normal == d.normal && b->primaries == d.primaries && b->transfer == d.transfer &&
|
||||
b->mip_filter == d.mip_filter && b->alpha_ref == 77 && b->loop_count == 3 &&
|
||||
b->flags == d.flags && b->note_count == 2;
|
||||
const yapf_note_t *n = b ? yapf_tex_find_note(b, "ICCP") : NULL;
|
||||
ok = ok && n && n->size == 3 && memcmp(n->data, "\0\1\2", 3) == 0;
|
||||
if (ok && b->frames > 1) ok = b->frame_us[b->frames - 1] == 16667u * b->frames;
|
||||
CHECK(ok, "v2 fields, notes and frame times survive (%s)", what);
|
||||
yapf_tex_free(b);
|
||||
free(file);
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
}
|
||||
|
||||
static void v2_blocks(void) {
|
||||
static const uint8_t fmts[] = { YAPF_GPU_BC1, YAPF_GPU_BC7, YAPF_GPU_BC4, YAPF_GPU_ASTC_6x6,
|
||||
YAPF_GPU_ASTC_12x12, YAPF_GPU_ETC2_RGBA8 };
|
||||
for (size_t f = 0; f < sizeof(fmts); f++)
|
||||
for (int fl = 0; fl < 2; fl++) {
|
||||
yapf_texture_t d = desc(300, 200, 4, 0);
|
||||
d.payload = YAPF_PAYLOAD_BLOCKS; d.format = fmts[f]; d.mip_levels = 3;
|
||||
if (f == 3) { d.shape = YAPF_SHAPE_VOLUME; d.depth = 5; }
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
tex_fill(t, 1);
|
||||
char what[48];
|
||||
sprintf(what, "blocks 0x%02x%s", fmts[f], fl ? " packed" : "");
|
||||
size_t len;
|
||||
free(tex_roundtrip(t, fl, &len, what));
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
}
|
||||
|
||||
/* Recomputes both header checks after editing a file. */
|
||||
static void reseal(uint8_t *f, size_t front) {
|
||||
uint32_t a = yapf_checksum(f + 64, front - 64), b;
|
||||
f[0x38] = (uint8_t)a; f[0x39] = (uint8_t)(a >> 8); f[0x3A] = (uint8_t)(a >> 16); f[0x3B] = (uint8_t)(a >> 24);
|
||||
b = yapf_checksum(f, 0x3C);
|
||||
f[0x3C] = (uint8_t)b; f[0x3D] = (uint8_t)(b >> 8); f[0x3E] = (uint8_t)(b >> 16); f[0x3F] = (uint8_t)(b >> 24);
|
||||
}
|
||||
|
||||
/* Flips, truncation, scribbles: never crash; truncation always fails. */
|
||||
static void v2_fuzz(const uint8_t *file, size_t len, const char *what) {
|
||||
uint8_t *bad = (uint8_t *)malloc(len);
|
||||
int truncated_ok = 1, err;
|
||||
for (int it = 0; it < 2000; it++) {
|
||||
memcpy(bad, file, len);
|
||||
size_t n = len;
|
||||
switch (it % 3) {
|
||||
case 0: for (int k = 0; k < 1 + it % 7; k++) bad[rnd() % len] ^= (uint8_t)(1u << (rnd() % 8)); break;
|
||||
case 1: n = rnd() % len; break;
|
||||
default: bad[rnd() % len] = (uint8_t)rnd(); break;
|
||||
}
|
||||
int fl = (it & 4) ? YAPF_LOAD_TRUST : 0;
|
||||
yapf_texture_t *x = yapf_tex_load_memory(bad, n, fl, (it & 1) ? 2 : 1, &err);
|
||||
if (it % 3 == 1 && x) truncated_ok = 0;
|
||||
yapf_tex_free(x);
|
||||
if (it % 50 == 0) yapf_free(yapf_load_memory(bad, n));
|
||||
}
|
||||
CHECK(truncated_ok, "truncated v2 files are rejected (%s)", what);
|
||||
free(bad);
|
||||
}
|
||||
|
||||
static void v2_integrity(void) {
|
||||
yapf_texture_t d = desc(150, 90, 4, YAPF_SAMPLE_F16);
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
tex_fill(t, 2);
|
||||
size_t len;
|
||||
uint8_t *file = tex_roundtrip(t, YAPF_ENC_ENTROPY, &len, "integrity setup");
|
||||
if (!file) { yapf_tex_free(t); return; }
|
||||
uint8_t *bad = (uint8_t *)malloc(len);
|
||||
size_t front = 64 + 16; /* header + one surface entry */
|
||||
int err;
|
||||
|
||||
/* a flipped bit in tile data is caught by its yapf-sum ... */
|
||||
memcpy(bad, file, len);
|
||||
bad[len - 10] ^= 0x10;
|
||||
CHECK(!yapf_tex_load_memory(bad, len, 0, 1, &err) && err == YAPF_ERR_CHECKSUM,
|
||||
"corrupt tile data reports YAPF_ERR_CHECKSUM (%d)", err);
|
||||
yapf_texture_t *trusted = yapf_tex_load_memory(bad, len, YAPF_LOAD_TRUST, 1, &err);
|
||||
yapf_tex_free(trusted); /* may or may not decode; must not crash */
|
||||
/* ... and in the header by the header check */
|
||||
memcpy(bad, file, len);
|
||||
bad[0x0C] ^= 1;
|
||||
CHECK(!yapf_tex_load_memory(bad, len, 0, 1, &err) && err == YAPF_ERR_CHECKSUM,
|
||||
"corrupt header reports YAPF_ERR_CHECKSUM");
|
||||
/* an unknown needs bit: refused as unsupported */
|
||||
memcpy(bad, file, len);
|
||||
bad[0x1F] |= 0x80;
|
||||
reseal(bad, front);
|
||||
CHECK(!yapf_tex_load_memory(bad, len, 0, 1, &err) && err == YAPF_ERR_UNSUPPORTED,
|
||||
"unknown needs bit is refused (%d)", err);
|
||||
/* a feature in use but not announced: corrupt */
|
||||
memcpy(bad, file, len);
|
||||
bad[0x1C] &= (uint8_t)~YAPF_NEED_WIDE;
|
||||
reseal(bad, front);
|
||||
CHECK(!yapf_tex_load_memory(bad, len, 0, 1, &err) && err == YAPF_ERR_CORRUPT,
|
||||
"unannounced feature is refused (%d)", err);
|
||||
/* the old API refuses what it cannot represent */
|
||||
CHECK(yapf_load_memory(file, len) == NULL, "yapf_load refuses half-float textures");
|
||||
|
||||
free(bad);
|
||||
v2_fuzz(file, len, "half float + entropy");
|
||||
free(file);
|
||||
yapf_tex_free(t);
|
||||
|
||||
/* 8-bit RGBA with entropy planes (decoded several streams at once) */
|
||||
d = desc(140, 75, 4, 0);
|
||||
t = yapf_tex_create(&d);
|
||||
tex_fill(t, 2);
|
||||
file = tex_roundtrip(t, YAPF_ENC_ENTROPY, &len, "fuzz setup rgba8");
|
||||
if (file) v2_fuzz(file, len, "rgba8 + entropy");
|
||||
free(file);
|
||||
/* BC7 blocks with packed tiles */
|
||||
yapf_texture_t *c = NULL;
|
||||
if (yapf_tex_compress(t, YAPF_GPU_BC7, &c) == YAPF_OK) {
|
||||
file = tex_roundtrip(c, YAPF_ENC_ENTROPY, &len, "fuzz setup bc7");
|
||||
if (file) v2_fuzz(file, len, "packed BC7");
|
||||
free(file);
|
||||
}
|
||||
yapf_tex_free(c);
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
|
||||
/* Streaming: a file prefix holds the small mips; tiles decode alone. */
|
||||
static void v2_streaming(void) {
|
||||
yapf_texture_t d = desc(300, 260, 4, 0);
|
||||
d.shape = YAPF_SHAPE_CUBE; d.width = d.height = 260; d.mip_levels = 5;
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
tex_fill(t, 2);
|
||||
void *buf = NULL;
|
||||
size_t len = 0;
|
||||
yapf_tex_encode(t, 0, &buf, &len);
|
||||
uint8_t *file = (uint8_t *)buf;
|
||||
int ok = 1, err = 0;
|
||||
for (uint32_t m = 0; m < 5 && ok; m++) {
|
||||
yapf_texture_t *probe = yapf_tex_open(file, len, 0, &err);
|
||||
uint64_t need = probe ? yapf_tex_stream_bytes(probe, m) : 0;
|
||||
yapf_tex_free(probe);
|
||||
yapf_texture_t *s = yapf_tex_open(file, (size_t)need, 0, &err);
|
||||
ok = s != NULL && need <= len && (m > 0 || need == len);
|
||||
for (uint32_t f = 0; ok && f < 6; f++)
|
||||
for (uint32_t mm = 0; ok && mm < 5; mm++) {
|
||||
uint32_t sf = yapf_tex_surface(s, 0, 0, f, mm);
|
||||
int rc = yapf_tex_decode_surface(s, file, (size_t)need, sf, 1);
|
||||
if (mm >= m) ok = rc == YAPF_OK &&
|
||||
memcmp(s->data[sf], t->data[sf], yapf_tex_surface_bytes(t, mm)) == 0;
|
||||
else ok = rc == YAPF_ERR_TRUNCATED;
|
||||
}
|
||||
yapf_tex_free(s);
|
||||
}
|
||||
CHECK(ok, "stream prefixes decode exactly their small mips");
|
||||
|
||||
/* tile decode equals the matching part of the full surface */
|
||||
yapf_texture_t *s = yapf_tex_open(file, len, 0, &err);
|
||||
uint32_t sf = yapf_tex_surface(s, 0, 0, 3, 0), tx, ty;
|
||||
yapf_tex_tile_grid(s, 0, &tx, &ty);
|
||||
uint8_t tile[64 * 64 * 4];
|
||||
ok = s && tx == 5 && ty == 5;
|
||||
for (uint32_t y = 0; ok && y < ty; y++)
|
||||
for (uint32_t x = 0; ok && x < tx; x++) {
|
||||
uint32_t px, py, pw, ph;
|
||||
ok = yapf_tex_decode_tile(s, file, len, sf, x, y, 0, tile, 64 * 4, &px, &py, &pw, &ph) == YAPF_OK;
|
||||
for (uint32_t r = 0; ok && r < ph; r++)
|
||||
ok = memcmp(tile + r * 64 * 4, (uint8_t *)t->data[sf] + ((size_t)(py + r) * 260 + px) * 4,
|
||||
(size_t)pw * 4) == 0;
|
||||
}
|
||||
CHECK(ok, "single tiles decode to the same pixels");
|
||||
yapf_tex_free(s);
|
||||
yapf_free_buffer(buf);
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
|
||||
/* v1 and v2 work through either API where the data allows it. */
|
||||
static void v2_compat(const char *sample) {
|
||||
yapf_texture_t *t = yapf_tex_load(sample, 0, 0, NULL);
|
||||
if (t) {
|
||||
uint32_t h = fnv((const uint8_t *)t->data[0], yapf_tex_surface_bytes(t, 0));
|
||||
CHECK(h == SAMPLE_HASH, "yapf_tex_load reads the v1 sample");
|
||||
size_t len;
|
||||
uint8_t *file = tex_roundtrip(t, YAPF_ENC_ENTROPY, &len, "sample as v2");
|
||||
yapf_image_t *img = file ? yapf_load_memory(file, len) : NULL;
|
||||
CHECK(img && img->width == 512 && img->channels == 4 &&
|
||||
fnv(img->pixels, 512u * 512u * 4u) == SAMPLE_HASH, "yapf_load reads an 8-bit v2 file");
|
||||
yapf_free(img);
|
||||
free(file);
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
/* cube: the v1 API sees face 0 with its mips */
|
||||
yapf_texture_t d = desc(40, 40, 3, 0);
|
||||
d.shape = YAPF_SHAPE_CUBE; d.mip_levels = 3;
|
||||
yapf_texture_t *c = yapf_tex_create(&d);
|
||||
tex_fill(c, 1);
|
||||
size_t len;
|
||||
uint8_t *file = tex_roundtrip(c, 0, &len, "cube for v1 API");
|
||||
yapf_image_t *img = file ? yapf_load_memory_mt(file, len, 2) : NULL;
|
||||
CHECK(img && img->mip_levels == 3 && memcmp(img->mips[2], c->data[2], 10 * 10 * 3) == 0 &&
|
||||
memcmp(img->pixels, c->data[0], 40 * 40 * 3) == 0, "yapf_load reads face 0 of a cube");
|
||||
yapf_free(img);
|
||||
free(file);
|
||||
yapf_tex_free(c);
|
||||
}
|
||||
|
||||
static double psnr8(const uint8_t *a, int sa, const uint8_t *b, int sb, size_t n, int ch) {
|
||||
double se = 0;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
for (int c = 0; c < ch; c++) {
|
||||
double v = (double)a[i * sa + c] - b[i * sb + c];
|
||||
se += v * v;
|
||||
}
|
||||
if (se == 0) return 99.0;
|
||||
return 10.0 * log10(255.0 * 255.0 * (double)(n * ch) / se);
|
||||
}
|
||||
|
||||
static void v2_gpu(void) {
|
||||
const uint32_t w = 61, h = 47; /* not multiples of 4 */
|
||||
yapf_texture_t d = desc(w, h, 4, 0);
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
uint8_t *p = (uint8_t *)t->data[0];
|
||||
for (uint32_t y = 0; y < h; y++)
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
uint8_t *q = p + ((size_t)y * w + x) * 4;
|
||||
q[0] = (uint8_t)(x * 4); q[1] = (uint8_t)(y * 5); q[2] = (uint8_t)(255 - x * 2);
|
||||
q[3] = (uint8_t)(((x / 8 + y / 8) & 1) ? 255 : 40 + x);
|
||||
}
|
||||
static const struct { uint8_t f; int ch; double min; } F[] = {
|
||||
{ YAPF_GPU_BC1, 3, 30 }, { YAPF_GPU_BC3, 4, 30 }, { YAPF_GPU_BC7, 4, 34 },
|
||||
{ YAPF_GPU_BC4, 1, 38 }, { YAPF_GPU_BC5, 2, 38 }
|
||||
};
|
||||
uint8_t *out = (uint8_t *)malloc((size_t)w * h * 4);
|
||||
for (size_t i = 0; i < sizeof(F) / sizeof(F[0]); i++) {
|
||||
yapf_texture_t *c = NULL;
|
||||
int rc = yapf_tex_compress(t, F[i].f, &c);
|
||||
int ok = rc == YAPF_OK && c->payload == YAPF_PAYLOAD_BLOCKS &&
|
||||
yapf_blocks_decode(F[i].f, c->data[0], w, h, out) == YAPF_OK;
|
||||
double q = 0;
|
||||
if (ok && F[i].f == YAPF_GPU_BC1) {
|
||||
/* BC1 keeps alpha only as cut-out: compare opaque texels */
|
||||
for (size_t k = 0; k < (size_t)w * h; k++) if (p[k * 4 + 3] < 128) memcpy(out + k * 4, p + k * 4, 3);
|
||||
}
|
||||
if (ok) q = psnr8(p, 4, out, 4, (size_t)w * h, F[i].ch);
|
||||
CHECK(ok && q >= F[i].min, "GPU format 0x%02x encodes (PSNR %.1f dB)", F[i].f, q);
|
||||
if (c) {
|
||||
size_t len;
|
||||
char what[32];
|
||||
sprintf(what, "compressed 0x%02x", F[i].f);
|
||||
free(tex_roundtrip(c, YAPF_ENC_ENTROPY, &len, what));
|
||||
}
|
||||
yapf_tex_free(c);
|
||||
}
|
||||
CHECK(yapf_blocks_decode(YAPF_GPU_ASTC_4x4, out, 4, 4, out) == YAPF_ERR_UNSUPPORTED,
|
||||
"ASTC blocks are carried, not decoded");
|
||||
free(out);
|
||||
yapf_tex_free(t);
|
||||
|
||||
/* BC6H: relative error of a smooth HDR sky gradient */
|
||||
yapf_texture_t dh = desc(32, 32, 3, YAPF_SAMPLE_F16);
|
||||
yapf_texture_t *th = yapf_tex_create(&dh), *ch = NULL;
|
||||
uint16_t *hp = (uint16_t *)th->data[0];
|
||||
for (int i = 0; i < 32 * 32; i++)
|
||||
for (int k = 0; k < 3; k++) hp[i * 3 + k] = yapf_float_to_half((0.3f + 0.4f * (float)k) *
|
||||
(1.0f + (float)(i % 32) * 0.02f + (float)(i / 32) * 0.15f));
|
||||
int ok = yapf_tex_compress(th, YAPF_GPU_BC6H, &ch) == YAPF_OK;
|
||||
uint16_t back[32 * 32 * 3];
|
||||
double worst = 0;
|
||||
if (ok) ok = yapf_blocks_decode(YAPF_GPU_BC6H, ch->data[0], 32, 32, back) == YAPF_OK;
|
||||
for (int i = 0; ok && i < 32 * 32 * 3; i++) {
|
||||
float a = yapf_half_to_float(hp[i]), b = yapf_half_to_float(back[i]);
|
||||
double r = (a > b ? a - b : b - a) / a;
|
||||
if (r > worst) worst = r;
|
||||
}
|
||||
CHECK(ok && worst < 0.05, "BC6H encodes HDR within 5%% (worst %.4f)", worst);
|
||||
yapf_tex_free(ch);
|
||||
yapf_tex_free(th);
|
||||
}
|
||||
|
||||
static void v2_mips(void) {
|
||||
/* sRGB: a black / white checker averages to sRGB 188, not 128 */
|
||||
yapf_texture_t d = desc(64, 64, 4, 0);
|
||||
d.flags = YAPF_FLAG_SRGB;
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
uint8_t *p = (uint8_t *)t->data[0];
|
||||
for (int i = 0; i < 64 * 64; i++) {
|
||||
uint8_t v = ((i % 64 + i / 64) & 1) ? 255 : 0;
|
||||
p[i * 4] = p[i * 4 + 1] = p[i * 4 + 2] = v;
|
||||
p[i * 4 + 3] = v; /* alpha stays linear */
|
||||
}
|
||||
int ok = yapf_tex_build_mips(t, 0, 0) == YAPF_OK && t->mip_levels == 7;
|
||||
const uint8_t *m1 = ok ? (const uint8_t *)t->data[1] : NULL;
|
||||
CHECK(ok && m1[0] == 188 && m1[3] == 128 && (t->mip_filter & YAPF_MIP_LINEAR_LIGHT),
|
||||
"sRGB mips average in linear light (got %d, alpha %d)", m1 ? m1[0] : -1, m1 ? m1[3] : -1);
|
||||
yapf_tex_free(t);
|
||||
|
||||
/* alpha test: speckled foliage (30 % of texels opaque). A plain box
|
||||
* filter thins it out with distance; with alpha_ref = 128 every mip
|
||||
* keeps about the same share of texels above 128. */
|
||||
double plain[5] = {0}, kept[5] = {0};
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
d = desc(128, 128, 4, 0);
|
||||
t = yapf_tex_create(&d);
|
||||
p = (uint8_t *)t->data[0];
|
||||
rng = 99;
|
||||
for (int i = 0; i < 128 * 128; i++) p[i * 4 + 3] = rnd() % 100 < 30 ? 255 : 0;
|
||||
ok = yapf_tex_build_mips(t, 5, pass ? 128 : 0) == YAPF_OK && t->mip_levels == 5;
|
||||
if (pass) ok = ok && (t->flags & YAPF_FLAG_COVERAGE) && t->alpha_ref == 128;
|
||||
for (uint32_t m = 1; ok && m < 5; m++) {
|
||||
uint32_t w, h, n = 0;
|
||||
yapf_tex_mip_size(t, m, &w, &h, NULL);
|
||||
const uint8_t *q = (const uint8_t *)t->data[m];
|
||||
for (uint32_t i = 0; i < w * h; i++) n += q[i * 4 + 3] > 128;
|
||||
(pass ? kept : plain)[m] = (double)n / (w * h);
|
||||
}
|
||||
if (pass) {
|
||||
size_t len;
|
||||
free(tex_roundtrip(t, 0, &len, "mipped texture"));
|
||||
}
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
for (uint32_t m = 1; ok && m < 5; m++) ok = kept[m] > 0.2 && kept[m] < 0.4;
|
||||
CHECK(ok && plain[2] < 0.15, "coverage-preserving mips keep alpha-test coverage "
|
||||
"(mip 2: %.2f, plain box %.2f)", kept[2], plain[2]);
|
||||
|
||||
/* float volume: 2×2×2 box */
|
||||
d = desc(4, 4, 1, YAPF_SAMPLE_F32);
|
||||
d.shape = YAPF_SHAPE_VOLUME; d.depth = 4;
|
||||
t = yapf_tex_create(&d);
|
||||
float *f = (float *)t->data[0];
|
||||
for (int i = 0; i < 64; i++) f[i] = (float)i;
|
||||
ok = yapf_tex_build_mips(t, 0, 0) == YAPF_OK && t->mip_levels == 3;
|
||||
CHECK(ok && ((float *)t->data[1])[0] == (0 + 1 + 4 + 5 + 16 + 17 + 20 + 21) / 8.0f &&
|
||||
((float *)t->data[2])[0] == 31.5f, "volume mips filter in 3D");
|
||||
yapf_tex_free(t);
|
||||
}
|
||||
|
||||
static void v2_misc(void) {
|
||||
int ok = 1;
|
||||
for (uint32_t h = 0; h < 0x10000 && ok; h++) {
|
||||
if ((h & 0x7C00) == 0x7C00 && (h & 0x3FF)) continue; /* NaN payloads */
|
||||
ok = yapf_float_to_half(yapf_half_to_float((uint16_t)h)) == h;
|
||||
}
|
||||
CHECK(ok, "every half survives half -> float -> half");
|
||||
CHECK(yapf_float_to_half(1.0f) == 0x3C00 && yapf_float_to_half(65520.0f) == 0x7C00 &&
|
||||
yapf_float_to_half(1e-8f) == 0 && yapf_float_to_half(-2.0f) == 0xC000, "float -> half rounding");
|
||||
/* fixed vectors pin yapf-sum to the specification */
|
||||
CHECK(yapf_checksum("", 0) == 0xd7550ed0u && yapf_checksum("YAPF", 4) == 0xc8cc41d4u &&
|
||||
yapf_checksum("The quick brown fox jumps over the lazy dog", 43) == 0xbce4f410u,
|
||||
"yapf-sum test vectors");
|
||||
/* encoding is deterministic */
|
||||
yapf_texture_t d = desc(90, 70, 3, YAPF_SAMPLE_U16);
|
||||
yapf_texture_t *t = yapf_tex_create(&d);
|
||||
tex_fill(t, 2);
|
||||
void *a = NULL, *b = NULL;
|
||||
size_t la = 0, lb = 0;
|
||||
yapf_tex_encode(t, YAPF_ENC_ENTROPY, &a, &la);
|
||||
yapf_tex_encode(t, YAPF_ENC_ENTROPY, &b, &lb);
|
||||
CHECK(a && la == lb && memcmp(a, b, la) == 0, "v2 encoding is deterministic");
|
||||
yapf_free_buffer(a);
|
||||
yapf_free_buffer(b);
|
||||
yapf_tex_free(t);
|
||||
d = desc(10, 12, 4, 0);
|
||||
d.shape = YAPF_SHAPE_CUBE;
|
||||
CHECK(yapf_tex_create(&d) == NULL, "a cube must be square");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
static const int sizes[][2] = {
|
||||
{1, 1}, {1, 7}, {7, 1}, {63, 63}, {64, 64}, {65, 65}, {127, 1},
|
||||
|
|
@ -197,6 +688,16 @@ int main(int argc, char **argv) {
|
|||
printf("note: sample %s not found, skipped\n", sample);
|
||||
}
|
||||
|
||||
v2_pixels();
|
||||
v2_layout();
|
||||
v2_blocks();
|
||||
v2_integrity();
|
||||
v2_streaming();
|
||||
v2_compat(sample);
|
||||
v2_gpu();
|
||||
v2_mips();
|
||||
v2_misc();
|
||||
|
||||
printf("%d tests, %d failures\n", tests, fails);
|
||||
return fails != 0;
|
||||
}
|
||||
|
|
|
|||
862
tools/yapf_cli.c
862
tools/yapf_cli.c
|
|
@ -18,20 +18,17 @@
|
|||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* yapf <input> <output> [options] convert; formats chosen by extension
|
||||
* yapf info <file.yapf> print header information
|
||||
* yapf pack <cube|array|volume|anim> <inputs…> <out.yapf> [options]
|
||||
* yapf info <file.yapf> print header, tables and notes
|
||||
* yapf verify <file.yapf> check every checksum and decode
|
||||
* yapf bench <file.yapf> time decoding
|
||||
* yapf thumbnail <in.yapf> <out.png> [size] small preview (default 256)
|
||||
* yapf compare <image.png> [more…] size and decode speed: original vs YAPF
|
||||
*
|
||||
* Reads PNG, JPEG, BMP, TGA, GIF (first frame), PSD (composite), PNM, HDR
|
||||
* (tone-clamped) and YAPF. Writes YAPF, PNG, BMP, TGA and JPEG.
|
||||
*
|
||||
* Options for writing YAPF:
|
||||
* --mips also store a full mip chain (2×2 box filter)
|
||||
* --linear mark pixels as linear instead of sRGB
|
||||
* --premult mark alpha as premultiplied
|
||||
* Options for reading YAPF:
|
||||
* --mip N export mip level N instead of the base image
|
||||
* Reads PNG (8 / 16-bit), JPEG, BMP, TGA, GIF (first frame), PSD, PNM,
|
||||
* HDR (float) and YAPF. Writes YAPF, PNG, BMP, TGA, JPEG and HDR.
|
||||
* Plain 8-bit 2D images are written as YAPF version 1; anything that
|
||||
* needs version 2 (see `yapf` without arguments) is written as version 2.
|
||||
*
|
||||
* Build: cc -O2 -std=c99 -Ithird_party tools/yapf_cli.c yapf.c -o yapf -lm -pthread
|
||||
*/
|
||||
|
|
@ -84,124 +81,532 @@ static double now_s(void) {
|
|||
#endif
|
||||
}
|
||||
|
||||
static uint8_t gpu_hint(int ch, int srgb) {
|
||||
switch (ch) {
|
||||
case 1: return YAPF_GPU_R8;
|
||||
case 2: return YAPF_GPU_RG8;
|
||||
case 3: return srgb ? YAPF_GPU_SRGB8 : YAPF_GPU_RGB8;
|
||||
default: return srgb ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_RGBA8;
|
||||
}
|
||||
|
||||
/* ── Options ─────────────────────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
int mips, srgb, premult, v2, entropy, eight_bit, float32;
|
||||
int coverage; /* alpha_ref, 0 = off */
|
||||
int format; /* block format, -1 = none */
|
||||
int normal; /* YAPF_NORMAL_* */
|
||||
int linear_alpha;
|
||||
int primaries, transfer;
|
||||
int have_map;
|
||||
uint8_t map[4];
|
||||
double frame_ms;
|
||||
int loops;
|
||||
/* export selection */
|
||||
int mip, layer, face, frame, slice;
|
||||
const char *author, *license, *source;
|
||||
const char *notes[16];
|
||||
int nnotes;
|
||||
} opts_t;
|
||||
|
||||
static const struct { const char *name; uint8_t v; } ch_names[] = {
|
||||
{ "r", YAPF_CH_RED }, { "g", YAPF_CH_GREEN }, { "b", YAPF_CH_BLUE }, { "a", YAPF_CH_ALPHA },
|
||||
{ "luma", YAPF_CH_LUMA }, { "nx", YAPF_CH_NORMAL_X }, { "ny", YAPF_CH_NORMAL_Y },
|
||||
{ "nz", YAPF_CH_NORMAL_Z }, { "rough", YAPF_CH_ROUGHNESS }, { "gloss", YAPF_CH_GLOSS },
|
||||
{ "metal", YAPF_CH_METALNESS }, { "ao", YAPF_CH_OCCLUSION }, { "height", YAPF_CH_HEIGHT },
|
||||
{ "spec", YAPF_CH_SPECULAR }, { "emissive", YAPF_CH_EMISSIVE }, { "opacity", YAPF_CH_OPACITY },
|
||||
{ "mask", YAPF_CH_MASK }, { "cavity", YAPF_CH_CAVITY }, { "thickness", YAPF_CH_THICKNESS },
|
||||
{ "id", YAPF_CH_ID }, { "-", YAPF_CH_UNUSED }, { "default", YAPF_CH_DEFAULT }
|
||||
};
|
||||
|
||||
static const char *ch_name(uint8_t v) {
|
||||
for (size_t i = 0; i < sizeof(ch_names) / sizeof(ch_names[0]); i++)
|
||||
if (ch_names[i].v == v) return ch_names[i].name;
|
||||
return "?";
|
||||
}
|
||||
|
||||
/* Full mip chain with a 2×2 box filter (edges clamp for odd sizes). */
|
||||
static int build_mips(yapf_image_t *img) {
|
||||
int levels = 1;
|
||||
while (levels < (int)YAPF_MAX_MIPS &&
|
||||
((img->width >> levels) > 0 || (img->height >> levels) > 0))
|
||||
levels++;
|
||||
img->mips = (uint8_t **)calloc((size_t)levels, sizeof(uint8_t *));
|
||||
if (!img->mips) return -1;
|
||||
img->mips[0] = img->pixels;
|
||||
img->mip_levels = (uint8_t)levels;
|
||||
|
||||
int ch = img->channels;
|
||||
for (int m = 1; m < levels; m++) {
|
||||
uint32_t pw = img->width >> (m - 1), ph = img->height >> (m - 1);
|
||||
uint32_t w = img->width >> m, h = img->height >> m;
|
||||
if (pw < 1) pw = 1;
|
||||
if (ph < 1) ph = 1;
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
uint8_t *src = img->mips[m - 1];
|
||||
uint8_t *dst = (uint8_t *)malloc((size_t)w * h * ch);
|
||||
if (!dst) return -1;
|
||||
for (uint32_t y = 0; y < h; y++)
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
for (int c = 0; c < ch; c++) {
|
||||
uint32_t x0 = 2 * x < pw ? 2 * x : pw - 1, x1 = 2 * x + 1 < pw ? 2 * x + 1 : pw - 1;
|
||||
uint32_t y0 = 2 * y < ph ? 2 * y : ph - 1, y1 = 2 * y + 1 < ph ? 2 * y + 1 : ph - 1;
|
||||
unsigned s = src[((size_t)y0 * pw + x0) * ch + c] + src[((size_t)y0 * pw + x1) * ch + c]
|
||||
+ src[((size_t)y1 * pw + x0) * ch + c] + src[((size_t)y1 * pw + x1) * ch + c];
|
||||
dst[((size_t)y * w + x) * ch + c] = (uint8_t)((s + 2) / 4);
|
||||
}
|
||||
img->mips[m] = dst;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_image(const char *path, const uint8_t *px, int w, int h, int ch) {
|
||||
if (ends_with(path, ".png")) return stbi_write_png(path, w, h, ch, px, w * ch);
|
||||
if (ends_with(path, ".bmp")) return stbi_write_bmp(path, w, h, ch, px);
|
||||
if (ends_with(path, ".tga")) return stbi_write_tga(path, w, h, ch, px);
|
||||
if (ends_with(path, ".jpg") || ends_with(path, ".jpeg"))
|
||||
return stbi_write_jpg(path, w, h, ch, px, 95);
|
||||
fprintf(stderr, "yapf: unsupported output format: %s (use .yapf .png .bmp .tga .jpg)\n", path);
|
||||
return 0;
|
||||
}
|
||||
static const struct { const char *name; uint8_t v; } fmt_names[] = {
|
||||
{ "bc1", YAPF_GPU_BC1 }, { "bc3", YAPF_GPU_BC3 }, { "bc4", YAPF_GPU_BC4 },
|
||||
{ "bc5", YAPF_GPU_BC5 }, { "bc6h", YAPF_GPU_BC6H }, { "bc7", YAPF_GPU_BC7 }
|
||||
};
|
||||
|
||||
static const char *gpu_name(int g) {
|
||||
static const char *n[] = { "RGBA8", "RGB8", "RG8", "R8", "SRGB8_A8", "SRGB8" };
|
||||
return g >= 0 && g < 6 ? n[g] : "unknown";
|
||||
static const char *px[] = { "RGBA8", "RGB8", "RG8", "R8", "SRGB8_A8", "SRGB8",
|
||||
"RGBA16", "RGB16", "RG16", "R16", "RGBA16F", "RGB16F", "RG16F", "R16F",
|
||||
"RGBA32F", "RGB32F", "RG32F", "R32F" };
|
||||
static const char *bc[] = { "BC1", "BC2", "BC3", "BC4", "BC4_SNORM", "BC5", "BC5_SNORM",
|
||||
"BC6H", "BC6H_SF", "BC7" };
|
||||
static const char *etc[] = { "ETC2_RGB8", "ETC2_RGB8A1", "ETC2_RGBA8", "EAC_R11",
|
||||
"EAC_R11_SNORM", "EAC_RG11", "EAC_RG11_SNORM" };
|
||||
static const char *astc[] = { "ASTC_4x4", "ASTC_5x4", "ASTC_5x5", "ASTC_6x5", "ASTC_6x6",
|
||||
"ASTC_8x5", "ASTC_8x6", "ASTC_8x8", "ASTC_10x5", "ASTC_10x6", "ASTC_10x8",
|
||||
"ASTC_10x10", "ASTC_12x10", "ASTC_12x12" };
|
||||
if (g >= 0 && g < 18) return px[g];
|
||||
if (g >= 0x40 && g < 0x4A) return bc[g - 0x40];
|
||||
if (g >= 0x50 && g < 0x57) return etc[g - 0x50];
|
||||
if (g >= 0x60 && g < 0x6E) return astc[g - 0x60];
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static uint8_t gpu_hint(int ch, int srgb, int sample) {
|
||||
static const uint8_t u8[2][4] = { { YAPF_GPU_R8, YAPF_GPU_RG8, YAPF_GPU_RGB8, YAPF_GPU_RGBA8 },
|
||||
{ YAPF_GPU_R8, YAPF_GPU_RG8, YAPF_GPU_SRGB8, YAPF_GPU_SRGB8_A8 } };
|
||||
static const uint8_t wide[3][4] = {
|
||||
{ YAPF_GPU_R16, YAPF_GPU_RG16, YAPF_GPU_RGB16, YAPF_GPU_RGBA16 },
|
||||
{ YAPF_GPU_R16F, YAPF_GPU_RG16F, YAPF_GPU_RGB16F, YAPF_GPU_RGBA16F },
|
||||
{ YAPF_GPU_R32F, YAPF_GPU_RG32F, YAPF_GPU_RGB32F, YAPF_GPU_RGBA32F } };
|
||||
if (sample == YAPF_SAMPLE_U8) return u8[srgb ? 1 : 0][ch - 1];
|
||||
return wide[sample - 1][ch - 1];
|
||||
}
|
||||
|
||||
static int parse_map(const char *s, uint8_t *map) {
|
||||
memset(map, 0, 4);
|
||||
for (int c = 0; c < 4 && *s; c++) {
|
||||
const char *e = strchr(s, ',');
|
||||
size_t n = e ? (size_t)(e - s) : strlen(s);
|
||||
size_t i;
|
||||
for (i = 0; i < sizeof(ch_names) / sizeof(ch_names[0]); i++)
|
||||
if (strlen(ch_names[i].name) == n && strncmp(ch_names[i].name, s, n) == 0) break;
|
||||
if (i == sizeof(ch_names) / sizeof(ch_names[0])) return 0;
|
||||
map[c] = ch_names[i].v;
|
||||
s += n + (e ? 1 : 0);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ── Reading any input into a texture ────────────────────────────────── */
|
||||
|
||||
static yapf_texture_t *new_tex(uint32_t w, uint32_t h, int ch, int sample) {
|
||||
yapf_texture_t d;
|
||||
memset(&d, 0, sizeof(d));
|
||||
d.width = w; d.height = h; d.depth = 1; d.layers = 1; d.frames = 1; d.mip_levels = 1;
|
||||
d.channels = (uint8_t)ch; d.sample = (uint8_t)sample;
|
||||
return yapf_tex_create(&d);
|
||||
}
|
||||
|
||||
static yapf_texture_t *read_input(const char *path, const opts_t *o, int *from_yapf) {
|
||||
*from_yapf = 0;
|
||||
if (is_yapf(path)) {
|
||||
int err = 0;
|
||||
yapf_texture_t *t = yapf_tex_load(path, 0, 0, &err);
|
||||
if (!t) fprintf(stderr, "yapf: cannot read %s (error %d)\n", path, err);
|
||||
*from_yapf = 1;
|
||||
return t;
|
||||
}
|
||||
int w, h, ch;
|
||||
yapf_texture_t *t = NULL;
|
||||
if (stbi_is_hdr(path)) {
|
||||
float *f = stbi_loadf(path, &w, &h, &ch, 0);
|
||||
if (!f) { fprintf(stderr, "yapf: cannot read %s: %s\n", path, stbi_failure_reason()); return NULL; }
|
||||
int sample = o->float32 ? YAPF_SAMPLE_F32 : YAPF_SAMPLE_F16;
|
||||
t = new_tex((uint32_t)w, (uint32_t)h, ch, sample);
|
||||
if (t) {
|
||||
size_t n = (size_t)w * h * ch;
|
||||
if (sample == YAPF_SAMPLE_F32) memcpy(t->data[0], f, n * 4);
|
||||
else for (size_t i = 0; i < n; i++) ((uint16_t *)t->data[0])[i] = yapf_float_to_half(f[i]);
|
||||
t->transfer = YAPF_TRANSFER_LINEAR;
|
||||
}
|
||||
stbi_image_free(f);
|
||||
} else if (stbi_is_16_bit(path) && !o->eight_bit) {
|
||||
stbi_us *p = stbi_load_16(path, &w, &h, &ch, 0);
|
||||
if (!p) { fprintf(stderr, "yapf: cannot read %s: %s\n", path, stbi_failure_reason()); return NULL; }
|
||||
t = new_tex((uint32_t)w, (uint32_t)h, ch, YAPF_SAMPLE_U16);
|
||||
if (t) memcpy(t->data[0], p, (size_t)w * h * ch * 2);
|
||||
stbi_image_free(p);
|
||||
} else {
|
||||
stbi_uc *p = stbi_load(path, &w, &h, &ch, 0);
|
||||
if (!p) { fprintf(stderr, "yapf: cannot read %s: %s\n", path, stbi_failure_reason()); return NULL; }
|
||||
t = new_tex((uint32_t)w, (uint32_t)h, ch, YAPF_SAMPLE_U8);
|
||||
if (t) memcpy(t->data[0], p, (size_t)w * h * ch);
|
||||
stbi_image_free(p);
|
||||
}
|
||||
if (!t) fprintf(stderr, "yapf: %s: unsupported size or out of memory\n", path);
|
||||
return t;
|
||||
}
|
||||
|
||||
/* Several same-sized inputs into one cube / array / volume / animation. */
|
||||
static yapf_texture_t *assemble(const char *kind, int n, char **paths, const opts_t *o) {
|
||||
yapf_texture_t *first = NULL, *t = NULL;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int y;
|
||||
yapf_texture_t *in = read_input(paths[i], o, &y);
|
||||
if (!in) { yapf_tex_free(t); yapf_tex_free(first); return NULL; }
|
||||
if (!t) {
|
||||
yapf_texture_t d = *in;
|
||||
d.mip_levels = 1;
|
||||
if (strcmp(kind, "cube") == 0) {
|
||||
if (n % 6) { fprintf(stderr, "yapf: a cube needs 6 faces per layer (+X -X +Y -Y +Z -Z)\n"); yapf_tex_free(in); return NULL; }
|
||||
d.shape = YAPF_SHAPE_CUBE; d.layers = (uint16_t)(n / 6);
|
||||
} else if (strcmp(kind, "array") == 0) {
|
||||
d.layers = (uint16_t)n;
|
||||
} else if (strcmp(kind, "volume") == 0) {
|
||||
d.shape = YAPF_SHAPE_VOLUME; d.depth = (uint32_t)n;
|
||||
} else if (strcmp(kind, "anim") == 0) {
|
||||
d.frames = (uint16_t)n;
|
||||
} else {
|
||||
fprintf(stderr, "yapf: pack kind must be cube, array, volume or anim\n");
|
||||
yapf_tex_free(in);
|
||||
return NULL;
|
||||
}
|
||||
t = yapf_tex_create(&d);
|
||||
if (!t) { fprintf(stderr, "yapf: invalid %s (cube faces must be square)\n", kind); yapf_tex_free(in); return NULL; }
|
||||
first = in;
|
||||
} else if (in->width != t->width || in->height != t->height ||
|
||||
in->channels != t->channels || in->sample != t->sample) {
|
||||
fprintf(stderr, "yapf: %s does not match the size and channels of %s\n", paths[i], paths[0]);
|
||||
yapf_tex_free(in); yapf_tex_free(t); yapf_tex_free(first);
|
||||
return NULL;
|
||||
}
|
||||
size_t bytes = yapf_tex_surface_bytes(in, 0);
|
||||
if (t->shape == YAPF_SHAPE_VOLUME) memcpy((uint8_t *)t->data[0] + bytes * i, in->data[0], bytes);
|
||||
else if (t->frames > 1) memcpy(t->data[yapf_tex_surface(t, (uint32_t)i, 0, 0, 0)], in->data[0], bytes);
|
||||
else if (t->shape == YAPF_SHAPE_CUBE) memcpy(t->data[yapf_tex_surface(t, 0, (uint32_t)i / 6, (uint32_t)i % 6, 0)], in->data[0], bytes);
|
||||
else memcpy(t->data[yapf_tex_surface(t, 0, (uint32_t)i, 0, 0)], in->data[0], bytes);
|
||||
if (in != first) yapf_tex_free(in);
|
||||
}
|
||||
if (t && t->frames > 1)
|
||||
for (int f = 0; f < t->frames; f++) t->frame_us[f] = (uint32_t)(o->frame_ms * 1000.0 + 0.5);
|
||||
yapf_tex_free(first);
|
||||
return t;
|
||||
}
|
||||
|
||||
/* ── Writing .yapf ───────────────────────────────────────────────────── */
|
||||
|
||||
static int needs_v2(const yapf_texture_t *t, const opts_t *o) {
|
||||
return o->v2 || o->entropy || o->format >= 0 || o->coverage || o->normal || o->have_map ||
|
||||
o->linear_alpha || o->primaries || o->transfer || o->author || o->license ||
|
||||
o->source || o->nnotes || t->sample != YAPF_SAMPLE_U8 || t->shape != YAPF_SHAPE_2D ||
|
||||
t->layers > 1 || t->frames > 1 || t->payload != YAPF_PAYLOAD_PIXELS || t->note_count;
|
||||
}
|
||||
|
||||
static int write_yapf(yapf_texture_t *t, const char *out, const opts_t *o, int from_yapf) {
|
||||
if (!from_yapf) {
|
||||
t->flags = (uint8_t)((o->srgb && t->sample == YAPF_SAMPLE_U8 && !o->normal ? YAPF_FLAG_SRGB : 0) |
|
||||
(o->premult ? YAPF_FLAG_PREMULT_ALPHA : 0));
|
||||
t->format = gpu_hint(t->channels, t->flags & YAPF_FLAG_SRGB, t->sample);
|
||||
}
|
||||
if (o->normal) {
|
||||
t->normal = (uint8_t)o->normal;
|
||||
t->channel_map[0] = YAPF_CH_NORMAL_X;
|
||||
t->channel_map[1] = YAPF_CH_NORMAL_Y;
|
||||
if (t->channels >= 3) t->channel_map[2] = YAPF_CH_NORMAL_Z;
|
||||
else t->normal |= YAPF_NORMAL_XY;
|
||||
t->flags &= (uint8_t)~YAPF_FLAG_SRGB;
|
||||
}
|
||||
if (o->have_map) memcpy(t->channel_map, o->map, 4);
|
||||
if (o->linear_alpha && (t->channels == 2 || t->channels == 4))
|
||||
t->linear_mask |= (uint8_t)(1u << (t->channels - 1));
|
||||
if (o->primaries) t->primaries = (uint8_t)o->primaries;
|
||||
if (o->transfer) t->transfer = (uint8_t)o->transfer;
|
||||
if (o->frame_ms > 0 && t->frames > 1)
|
||||
for (int f = 0; f < t->frames; f++) t->frame_us[f] = (uint32_t)(o->frame_ms * 1000.0 + 0.5);
|
||||
if (o->loops >= 0) t->loop_count = (uint16_t)o->loops;
|
||||
|
||||
if (o->mips && t->payload == YAPF_PAYLOAD_PIXELS) {
|
||||
int rc = yapf_tex_build_mips(t, 0, (uint8_t)o->coverage);
|
||||
if (rc != YAPF_OK) {
|
||||
fprintf(stderr, "yapf: cannot build mips%s\n", o->coverage ? " (--coverage needs an alpha channel)" : "");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
int v2 = needs_v2(t, o);
|
||||
if (o->format >= 0) {
|
||||
yapf_texture_t *c = NULL;
|
||||
int rc = yapf_tex_compress(t, (uint8_t)o->format, &c);
|
||||
if (rc != YAPF_OK) {
|
||||
fprintf(stderr, "yapf: cannot compress to %s: %s\n", gpu_name(o->format),
|
||||
o->format == YAPF_GPU_BC6H ? "BC6H needs a 16-bit or HDR (float) image"
|
||||
: "LDR block formats need an 8-bit image");
|
||||
return 1;
|
||||
}
|
||||
opts_t rest = *o; /* the blocks are made: write them */
|
||||
rest.format = -1;
|
||||
rest.mips = 0;
|
||||
rest.v2 = 1;
|
||||
int r2 = write_yapf(c, out, &rest, 1);
|
||||
yapf_tex_free(c);
|
||||
return r2;
|
||||
}
|
||||
if (!v2) {
|
||||
/* 8-bit 2D: version 1, readable by every YAPF decoder */
|
||||
yapf_image_t img;
|
||||
memset(&img, 0, sizeof(img));
|
||||
img.width = t->width; img.height = t->height; img.channels = t->channels;
|
||||
img.gpu_format = t->format; img.flags = t->flags; img.mip_levels = t->mip_levels;
|
||||
img.mips = (uint8_t **)t->data;
|
||||
img.pixels = (uint8_t *)t->data[0];
|
||||
if (yapf_save(out, &img) != YAPF_OK) { fprintf(stderr, "yapf: cannot write %s\n", out); return 1; }
|
||||
return 0;
|
||||
}
|
||||
if (!yapf_tex_find_note(t, "TOOL")) yapf_tex_add_note(t, "TOOL", "yapf command-line tool", 22);
|
||||
if (o->author) yapf_tex_add_note(t, "AUTH", o->author, (uint32_t)strlen(o->author));
|
||||
if (o->license) yapf_tex_add_note(t, "LICN", o->license, (uint32_t)strlen(o->license));
|
||||
if (o->source) yapf_tex_add_note(t, "SRCE", o->source, (uint32_t)strlen(o->source));
|
||||
for (int i = 0; i < o->nnotes; i++) {
|
||||
const char *eq = strchr(o->notes[i], '=');
|
||||
if (!eq || eq - o->notes[i] != 4) { fprintf(stderr, "yapf: --note wants TAG=text with a 4-letter tag\n"); return 1; }
|
||||
yapf_tex_add_note(t, o->notes[i], eq + 1, (uint32_t)strlen(eq + 1));
|
||||
}
|
||||
if (yapf_tex_save(out, t, o->entropy ? YAPF_ENC_ENTROPY : 0) != YAPF_OK) {
|
||||
fprintf(stderr, "yapf: cannot write %s\n", out);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Exporting a surface to a common format ──────────────────────────── */
|
||||
|
||||
/* One surface slice as 8-bit pixels (*hdr = 0) or float RGB(A) (*hdr = 1). */
|
||||
static void *surface_view(const yapf_texture_t *t, uint32_t s, uint32_t slice,
|
||||
uint32_t *w, uint32_t *h, int *ch, int *hdr)
|
||||
{
|
||||
uint32_t m = s % t->mip_levels, d;
|
||||
yapf_tex_mip_size(t, m, w, h, &d);
|
||||
if (slice >= d) return NULL;
|
||||
size_t n = (size_t)*w * *h, slice_bytes = yapf_tex_surface_bytes(t, m) / d;
|
||||
const uint8_t *src = (const uint8_t *)t->data[s] + slice_bytes * slice;
|
||||
*hdr = 0;
|
||||
if (t->payload == YAPF_PAYLOAD_BLOCKS) {
|
||||
int is6 = t->format == YAPF_GPU_BC6H || t->format == YAPF_GPU_BC6H_SF;
|
||||
void *out = malloc(n * (is6 ? 6 : 4));
|
||||
if (!out) return NULL;
|
||||
if (yapf_blocks_decode(t->format, src, *w, *h, out) != YAPF_OK) { free(out); return NULL; }
|
||||
if (!is6) { *ch = 4; return out; }
|
||||
float *f = (float *)malloc(n * 3 * sizeof(float));
|
||||
for (size_t i = 0; f && i < n * 3; i++) f[i] = yapf_half_to_float(((uint16_t *)out)[i]);
|
||||
free(out);
|
||||
*ch = 3; *hdr = 1;
|
||||
return f;
|
||||
}
|
||||
*ch = t->channels;
|
||||
if (t->sample == YAPF_SAMPLE_U8) {
|
||||
uint8_t *o = (uint8_t *)malloc(n * *ch);
|
||||
if (o) memcpy(o, src, n * *ch);
|
||||
return o;
|
||||
}
|
||||
if (t->sample == YAPF_SAMPLE_U16) {
|
||||
uint8_t *o = (uint8_t *)malloc(n * *ch);
|
||||
for (size_t i = 0; o && i < n * *ch; i++) o[i] = (uint8_t)((((const uint16_t *)src)[i] + 128) / 257);
|
||||
return o;
|
||||
}
|
||||
float *f = (float *)malloc(n * *ch * sizeof(float));
|
||||
for (size_t i = 0; f && i < n * *ch; i++)
|
||||
f[i] = t->sample == YAPF_SAMPLE_F16 ? yapf_half_to_float(((const uint16_t *)src)[i])
|
||||
: ((const float *)src)[i];
|
||||
*hdr = 1;
|
||||
return f;
|
||||
}
|
||||
|
||||
static uint8_t *to8(const float *f, size_t n) {
|
||||
uint8_t *o = (uint8_t *)malloc(n);
|
||||
for (size_t i = 0; o && i < n; i++) {
|
||||
float v = f[i] < 0 ? 0 : f[i] > 1 ? 1 : f[i];
|
||||
o[i] = (uint8_t)(v * 255.0f + 0.5f);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
static int write_image(const char *path, const void *px, int w, int h, int ch, int hdr) {
|
||||
if (ends_with(path, ".hdr")) {
|
||||
if (!hdr) {
|
||||
float *f = (float *)malloc((size_t)w * h * ch * sizeof(float));
|
||||
for (size_t i = 0; f && i < (size_t)w * h * ch; i++) f[i] = ((const uint8_t *)px)[i] / 255.0f;
|
||||
int ok = f && stbi_write_hdr(path, w, h, ch, f);
|
||||
free(f);
|
||||
return ok;
|
||||
}
|
||||
return stbi_write_hdr(path, w, h, ch, (const float *)px);
|
||||
}
|
||||
uint8_t *tmp = NULL;
|
||||
if (hdr) {
|
||||
tmp = to8((const float *)px, (size_t)w * h * ch);
|
||||
if (!tmp) return 0;
|
||||
fprintf(stderr, "yapf: note: float pixels clamped to 0..1 for %s (use .hdr to keep them)\n", path);
|
||||
px = tmp;
|
||||
}
|
||||
int ok = 0;
|
||||
if (ends_with(path, ".png")) ok = stbi_write_png(path, w, h, ch, px, w * ch);
|
||||
else if (ends_with(path, ".bmp")) ok = stbi_write_bmp(path, w, h, ch, px);
|
||||
else if (ends_with(path, ".tga")) ok = stbi_write_tga(path, w, h, ch, px);
|
||||
else if (ends_with(path, ".jpg") || ends_with(path, ".jpeg")) ok = stbi_write_jpg(path, w, h, ch, px, 95);
|
||||
else fprintf(stderr, "yapf: unsupported output format: %s (use .yapf .png .bmp .tga .jpg .hdr)\n", path);
|
||||
free(tmp);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int export_image(const yapf_texture_t *t, const char *in, const char *out, const opts_t *o) {
|
||||
uint32_t faces = yapf_tex_faces(t);
|
||||
if ((uint32_t)o->mip >= t->mip_levels || (uint32_t)o->layer >= t->layers ||
|
||||
(uint32_t)o->face >= faces || (uint32_t)o->frame >= t->frames) {
|
||||
fprintf(stderr, "yapf: %s has %u mip(s), %u layer(s), %u face(s), %u frame(s)\n",
|
||||
in, t->mip_levels, t->layers, faces, t->frames);
|
||||
return 1;
|
||||
}
|
||||
uint32_t s = yapf_tex_surface(t, (uint32_t)o->frame, (uint32_t)o->layer, (uint32_t)o->face, (uint32_t)o->mip);
|
||||
uint32_t w, h;
|
||||
int ch, hdr;
|
||||
void *px = surface_view(t, s, (uint32_t)o->slice, &w, &h, &ch, &hdr);
|
||||
if (!px) {
|
||||
fprintf(stderr, "yapf: cannot export %s: %s\n", in,
|
||||
t->payload == YAPF_PAYLOAD_BLOCKS ? "no built-in decoder for this block format (ETC2 / ASTC)"
|
||||
: "no such slice, or out of memory");
|
||||
return 1;
|
||||
}
|
||||
if (t->sample == YAPF_SAMPLE_U16 && !ends_with(out, ".hdr"))
|
||||
fprintf(stderr, "yapf: note: 16-bit samples written as 8-bit\n");
|
||||
int ok = write_image(out, px, (int)w, (int)h, ch, hdr);
|
||||
free(px);
|
||||
if (!ok) { fprintf(stderr, "yapf: cannot write %s\n", out); return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── info / verify / bench / thumbnail ───────────────────────────────── */
|
||||
|
||||
static uint8_t *read_all(const char *path, size_t *n) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
rewind(f);
|
||||
uint8_t *buf = sz >= 0 ? (uint8_t *)malloc((size_t)sz + 1) : NULL;
|
||||
if (buf && fread(buf, 1, (size_t)sz, f) != (size_t)sz) { free(buf); buf = NULL; }
|
||||
fclose(f);
|
||||
*n = buf ? (size_t)sz : 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
static const char *err_name(int e) {
|
||||
switch (e) {
|
||||
case YAPF_ERR_INVALID: return "invalid argument";
|
||||
case YAPF_ERR_IO: return "cannot read the file";
|
||||
case YAPF_ERR_OOM: return "out of memory";
|
||||
case YAPF_ERR_CORRUPT: return "corrupt or not a YAPF file";
|
||||
case YAPF_ERR_UNSUPPORTED: return "needs a feature this decoder does not have";
|
||||
case YAPF_ERR_CHECKSUM: return "checksum mismatch (damaged file)";
|
||||
case YAPF_ERR_TRUNCATED: return "file is truncated";
|
||||
default: return "error";
|
||||
}
|
||||
}
|
||||
|
||||
static int cmd_info(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
uint8_t hdr[20];
|
||||
if (!f || fread(hdr, 1, 20, f) != 20 || memcmp(hdr, "YAPF", 4) != 0) {
|
||||
fprintf(stderr, "yapf: %s is not a YAPF file\n", path);
|
||||
if (f) fclose(f);
|
||||
size_t n;
|
||||
uint8_t *buf = read_all(path, &n);
|
||||
int err = 0;
|
||||
yapf_texture_t *t = buf ? yapf_tex_open(buf, n, 0, &err) : NULL;
|
||||
if (!t) {
|
||||
fprintf(stderr, "yapf: %s: %s\n", path, buf ? err_name(err) : "cannot open");
|
||||
free(buf);
|
||||
return 1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fclose(f);
|
||||
unsigned w = hdr[12] | hdr[13] << 8 | (unsigned)hdr[14] << 16 | (unsigned)hdr[15] << 24;
|
||||
unsigned h = hdr[16] | hdr[17] << 8 | (unsigned)hdr[18] << 16 | (unsigned)hdr[19] << 24;
|
||||
static const char *shape[] = { "2D", "cube", "volume" };
|
||||
static const char *sample[] = { "8-bit", "16-bit", "half float", "float" };
|
||||
static const char *prim[] = { "unspecified", "BT.709 / sRGB", "Display P3", "BT.2020",
|
||||
"ACES AP0", "ACEScg", "Adobe RGB" };
|
||||
static const char *tf[] = { "unspecified", "sRGB", "linear", "gamma 2.2", "PQ", "HLG", "BT.1886" };
|
||||
static const char *mf[] = { "unknown", "box", "tent", "Kaiser", "Lanczos", "Mitchell", "authored" };
|
||||
uint32_t w, h, d;
|
||||
yapf_tex_mip_size(t, 0, &w, &h, &d);
|
||||
printf("%s\n", path);
|
||||
printf(" format version : %u\n", hdr[4]);
|
||||
printf(" size : %u x %u, %u channel(s)\n", w, h, hdr[5]);
|
||||
printf(" mip levels : %u\n", hdr[7]);
|
||||
printf(" gpu format : %s\n", gpu_name(hdr[6]));
|
||||
printf(" flags : %s%s\n", hdr[8] & YAPF_FLAG_SRGB ? "sRGB " : "linear ",
|
||||
hdr[8] & YAPF_FLAG_PREMULT_ALPHA ? "premultiplied-alpha" : "");
|
||||
printf(" file size : %ld bytes (%.1f%% of raw)\n", size,
|
||||
100.0 * (double)size / ((double)w * h * (hdr[5] ? hdr[5] : 1)));
|
||||
yapf_image_t *img = yapf_load(path);
|
||||
printf(" decodes : %s\n", img ? "ok" : "FAILED (corrupt or unsupported)");
|
||||
yapf_free(img);
|
||||
return img ? 0 : 1;
|
||||
printf(" format version : %u\n", buf[4]);
|
||||
printf(" size : %u x %u%s", w, h, t->shape == YAPF_SHAPE_VOLUME ? "" : "\n");
|
||||
if (t->shape == YAPF_SHAPE_VOLUME) printf(" x %u\n", d);
|
||||
printf(" shape : %s, %u layer(s), %u frame(s)\n", shape[t->shape], t->layers, t->frames);
|
||||
printf(" mip levels : %u\n", t->mip_levels);
|
||||
if (t->payload == YAPF_PAYLOAD_BLOCKS)
|
||||
printf(" payload : GPU blocks, %s (%u channel(s))\n", gpu_name(t->format), t->channels);
|
||||
else
|
||||
printf(" payload : lossless pixels, %u channel(s), %s, GPU hint %s\n",
|
||||
t->channels, sample[t->sample], gpu_name(t->format));
|
||||
printf(" flags : %s%s%s\n", t->flags & YAPF_FLAG_SRGB ? "sRGB" : "linear",
|
||||
t->flags & YAPF_FLAG_PREMULT_ALPHA ? ", premultiplied alpha" : "",
|
||||
t->flags & YAPF_FLAG_COVERAGE ? ", coverage-preserving mips" : "");
|
||||
if (buf[4] == 2) {
|
||||
uint32_t needs = (uint32_t)buf[0x1C] | (uint32_t)buf[0x1D] << 8;
|
||||
printf(" needs : %s%s%s%s%s%s\n", needs ? "" : "nothing beyond 8-bit 2D",
|
||||
needs & YAPF_NEED_WIDE ? "wide " : "", needs & YAPF_NEED_BLOCKS ? "blocks " : "",
|
||||
needs & YAPF_NEED_ENTROPY ? "entropy " : "", needs & YAPF_NEED_LAYOUT ? "layout " : "",
|
||||
needs & YAPF_NEED_FRAMES ? "frames" : "");
|
||||
printf(" channels :");
|
||||
for (int c = 0; c < t->channels; c++)
|
||||
printf(" %s%s", ch_name(t->channel_map[c]), t->linear_mask & (1u << c) ? "(linear)" : "");
|
||||
printf("\n");
|
||||
if (t->normal)
|
||||
printf(" normal map : %s, %s\n",
|
||||
(t->normal & 3) == YAPF_NORMAL_GL ? "OpenGL (+Y)" : (t->normal & 3) == YAPF_NORMAL_DX ? "DirectX (-Y)" : "no convention",
|
||||
t->normal & YAPF_NORMAL_XY ? "XY (Z reconstructed)" : t->normal & YAPF_NORMAL_OCT ? "octahedral" : "XYZ");
|
||||
printf(" colour : primaries %s, transfer %s\n",
|
||||
t->primaries < 7 ? prim[t->primaries] : "?", t->transfer < 7 ? tf[t->transfer] : "?");
|
||||
if (t->mip_levels > 1)
|
||||
printf(" mip filter : %s%s%s\n", (t->mip_filter & 0x7F) < 7 ? mf[t->mip_filter & 0x7F] : "?",
|
||||
t->mip_filter & YAPF_MIP_LINEAR_LIGHT ? ", linear light" : "",
|
||||
t->flags & YAPF_FLAG_COVERAGE ? "" : "");
|
||||
if (t->flags & YAPF_FLAG_COVERAGE) printf(" alpha ref : %u\n", t->alpha_ref);
|
||||
if (t->frames > 1)
|
||||
printf(" animation : %u frames, first %.1f ms, loops %s\n", t->frames,
|
||||
t->frame_us ? t->frame_us[0] / 1000.0 : 0.0, t->loop_count ? "" : "forever");
|
||||
for (uint32_t i = 0; i < t->note_count; i++) {
|
||||
const yapf_note_t *nt = &t->notes[i];
|
||||
int text = 1;
|
||||
for (uint32_t k = 0; k < nt->size; k++) if (nt->data[k] < 9) text = 0;
|
||||
if (text && nt->size < 200) printf(" note %.4s : %.*s\n", nt->tag, (int)nt->size, nt->data);
|
||||
else printf(" note %.4s : %u bytes\n", nt->tag, nt->size);
|
||||
}
|
||||
printf(" stream prefix :");
|
||||
for (uint32_t m = t->mip_levels; m-- > 0; ) {
|
||||
uint32_t mw;
|
||||
yapf_tex_mip_size(t, m, &mw, NULL, NULL);
|
||||
printf(" %u px: %llu B%s", mw, (unsigned long long)yapf_tex_stream_bytes(t, m), m ? "," : "\n");
|
||||
}
|
||||
}
|
||||
size_t raw = 0;
|
||||
for (uint32_t s = 0; s < yapf_tex_surface_count(t); s++) {
|
||||
uint32_t mw, mh, md;
|
||||
yapf_tex_mip_size(t, s % t->mip_levels, &mw, &mh, &md);
|
||||
raw += (size_t)mw * mh * md * t->channels * (t->payload ? 1 : yapf_sample_bytes(t->sample));
|
||||
}
|
||||
printf(" file size : %zu bytes (%.1f%% of raw)\n", n, 100.0 * (double)n / (double)raw);
|
||||
yapf_tex_free(t);
|
||||
t = yapf_tex_load_memory(buf, n, 0, 0, &err);
|
||||
printf(" decodes : %s\n", t ? "ok (checksums verified)" : err_name(err));
|
||||
yapf_tex_free(t);
|
||||
free(buf);
|
||||
return t ? 0 : 1;
|
||||
}
|
||||
|
||||
static int cmd_verify(const char *path) {
|
||||
size_t n;
|
||||
uint8_t *buf = read_all(path, &n);
|
||||
int err = YAPF_ERR_IO;
|
||||
yapf_texture_t *t = buf ? yapf_tex_load_memory(buf, n, 0, 0, &err) : NULL;
|
||||
if (t) printf("%s: ok%s\n", path, buf[4] == 2 ? " (header, tables and every tile checksum match)"
|
||||
: " (version 1: decodes; no checksums in v1)");
|
||||
else printf("%s: FAILED — %s\n", path, err_name(err));
|
||||
yapf_tex_free(t);
|
||||
free(buf);
|
||||
return t ? 0 : 1;
|
||||
}
|
||||
|
||||
static int cmd_bench(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) { fprintf(stderr, "yapf: cannot open %s\n", path); return 1; }
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
rewind(f);
|
||||
uint8_t *buf = (uint8_t *)malloc((size_t)n);
|
||||
if (!buf || fread(buf, 1, (size_t)n, f) != (size_t)n) { fclose(f); free(buf); return 1; }
|
||||
fclose(f);
|
||||
yapf_image_t *img = yapf_load_memory(buf, (size_t)n);
|
||||
if (!img) { fprintf(stderr, "yapf: %s does not decode\n", path); free(buf); return 1; }
|
||||
double raw = (double)img->width * img->height * img->channels;
|
||||
yapf_free(img);
|
||||
for (int mt = 0; mt < 2; mt++) {
|
||||
size_t n;
|
||||
uint8_t *buf = read_all(path, &n);
|
||||
if (!buf) { fprintf(stderr, "yapf: cannot open %s\n", path); return 1; }
|
||||
int err;
|
||||
yapf_texture_t *t = yapf_tex_load_memory(buf, n, 0, 1, &err);
|
||||
if (!t) { fprintf(stderr, "yapf: %s: %s\n", path, err_name(err)); free(buf); return 1; }
|
||||
double raw = 0;
|
||||
for (uint32_t s = 0; s < yapf_tex_surface_count(t); s++) raw += (double)yapf_tex_surface_bytes(t, s % t->mip_levels);
|
||||
yapf_tex_free(t);
|
||||
for (int mode = 0; mode < 3; mode++) {
|
||||
int threads = mode == 1 ? 0 : 1, flags = mode == 2 ? YAPF_LOAD_TRUST : 0;
|
||||
if (mode == 2 && buf[4] != 2) break;
|
||||
/* Best of 15 rounds; each round repeats enough decodes to last
|
||||
* ~20 ms, so short images are measured warm and not timer-bound. */
|
||||
double one = now_s();
|
||||
yapf_free(mt ? yapf_load_memory_mt(buf, (size_t)n, 0) : yapf_load_memory(buf, (size_t)n));
|
||||
yapf_tex_free(yapf_tex_load_memory(buf, n, flags, threads, NULL));
|
||||
one = now_s() - one;
|
||||
int reps = (int)(0.02 / (one > 1e-6 ? one : 1e-6)) + 1;
|
||||
double best = 1e9;
|
||||
for (int r = 0; r < 15; r++) {
|
||||
double t = now_s();
|
||||
for (int k = 0; k < reps; k++)
|
||||
yapf_free(mt ? yapf_load_memory_mt(buf, (size_t)n, 0)
|
||||
: yapf_load_memory(buf, (size_t)n));
|
||||
t = (now_s() - t) / reps;
|
||||
if (t < best) best = t;
|
||||
double tt = now_s();
|
||||
for (int k = 0; k < reps; k++) yapf_tex_free(yapf_tex_load_memory(buf, n, flags, threads, NULL));
|
||||
tt = (now_s() - tt) / reps;
|
||||
if (tt < best) best = tt;
|
||||
}
|
||||
printf(" %-12s %8.3f ms %6.2f GB/s\n", mt ? "all cores" : "1 thread", best * 1e3, raw / best / 1e9);
|
||||
static const char *nm[] = { "1 thread", "all cores", "no checks" };
|
||||
printf(" %-12s %8.3f ms %6.2f GB/s\n", nm[mode], best * 1e3, raw / best / 1e9);
|
||||
}
|
||||
free(buf);
|
||||
return 0;
|
||||
|
|
@ -210,23 +615,31 @@ static int cmd_bench(const char *path) {
|
|||
/* Preview no larger than size×size: start from the smallest mip that is
|
||||
* still big enough, then average boxes of source pixels. */
|
||||
static int cmd_thumbnail(const char *in, const char *out, int size) {
|
||||
yapf_image_t *img = yapf_load_mt(in, 0);
|
||||
if (!img) { fprintf(stderr, "yapf: cannot read %s\n", in); return 1; }
|
||||
int err;
|
||||
yapf_texture_t *t = yapf_tex_load(in, 0, 0, &err);
|
||||
if (!t) { fprintf(stderr, "yapf: cannot read %s: %s\n", in, err_name(err)); return 1; }
|
||||
if (size < 1) size = 256;
|
||||
int m = 0;
|
||||
while (m + 1 < img->mip_levels &&
|
||||
(int)(img->width >> (m + 1)) >= size && (int)(img->height >> (m + 1)) >= size)
|
||||
while (m + 1 < t->mip_levels &&
|
||||
(int)(t->width >> (m + 1)) >= size && (int)(t->height >> (m + 1)) >= size)
|
||||
m++;
|
||||
uint32_t sw = img->width >> m ? img->width >> m : 1, sh = img->height >> m ? img->height >> m : 1;
|
||||
const uint8_t *src = img->mips[m];
|
||||
uint32_t sw, sh;
|
||||
int ch, hdr;
|
||||
uint8_t *src = (uint8_t *)surface_view(t, (uint32_t)m, 0, &sw, &sh, &ch, &hdr);
|
||||
yapf_tex_free(t);
|
||||
if (!src) { fprintf(stderr, "yapf: cannot preview %s\n", in); return 1; }
|
||||
if (hdr) {
|
||||
uint8_t *b = to8((const float *)src, (size_t)sw * sh * ch);
|
||||
free(src);
|
||||
if (!(src = b)) return 1;
|
||||
}
|
||||
double k = (double)size / (double)(sw > sh ? sw : sh);
|
||||
if (k > 1.0) k = 1.0;
|
||||
uint32_t dw = (uint32_t)(sw * k + 0.5), dh = (uint32_t)(sh * k + 0.5);
|
||||
if (dw < 1) dw = 1;
|
||||
if (dh < 1) dh = 1;
|
||||
int ch = img->channels;
|
||||
uint8_t *dst = (uint8_t *)malloc((size_t)dw * dh * ch);
|
||||
if (!dst) { yapf_free(img); return 1; }
|
||||
if (!dst) { free(src); return 1; }
|
||||
for (uint32_t y = 0; y < dh; y++) {
|
||||
uint32_t y0 = (uint32_t)((uint64_t)y * sh / dh), y1 = (uint32_t)((uint64_t)(y + 1) * sh / dh);
|
||||
if (y1 <= y0) y1 = y0 + 1;
|
||||
|
|
@ -238,14 +651,14 @@ static int cmd_thumbnail(const char *in, const char *out, int size) {
|
|||
for (uint32_t yy = y0; yy < y1; yy++)
|
||||
for (uint32_t xx = x0; xx < x1; xx++)
|
||||
sum += src[((size_t)yy * sw + xx) * ch + c];
|
||||
uint64_t n = (uint64_t)(y1 - y0) * (x1 - x0);
|
||||
dst[((size_t)y * dw + x) * ch + c] = (uint8_t)((sum + n / 2) / n);
|
||||
uint64_t nn = (uint64_t)(y1 - y0) * (x1 - x0);
|
||||
dst[((size_t)y * dw + x) * ch + c] = (uint8_t)((sum + nn / 2) / nn);
|
||||
}
|
||||
}
|
||||
}
|
||||
int ok = stbi_write_png(out, (int)dw, (int)dh, ch, dst, (int)dw * ch);
|
||||
free(dst);
|
||||
yapf_free(img);
|
||||
free(src);
|
||||
if (!ok) fprintf(stderr, "yapf: cannot write %s\n", out);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
|
@ -285,7 +698,7 @@ static const char *ext_name(const char *path) {
|
|||
* and a lossless check. */
|
||||
static int cmd_compare(int n, char **paths) {
|
||||
int rc = 0;
|
||||
double tot_src = 0, tot_yapf = 0, tot_src_t = 0, tot_yapf_t = 0;
|
||||
double tot_src = 0, tot_yapf = 0, tot_src_t = 0, tot_yapf_t = 0, tot_ent = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char *path = paths[i];
|
||||
if (is_yapf(path)) {
|
||||
|
|
@ -310,7 +723,7 @@ static int cmd_compare(int n, char **paths) {
|
|||
memset(&img, 0, sizeof(img));
|
||||
img.width = (uint32_t)w; img.height = (uint32_t)h; img.channels = (uint8_t)ch;
|
||||
img.mip_levels = 1; img.pixels = px; img.flags = YAPF_FLAG_SRGB;
|
||||
img.gpu_format = gpu_hint(ch, 1);
|
||||
img.gpu_format = gpu_hint(ch, 1, YAPF_SAMPLE_U8);
|
||||
|
||||
void *enc = NULL;
|
||||
size_t esz = 0;
|
||||
|
|
@ -339,6 +752,28 @@ static int cmd_compare(int n, char **paths) {
|
|||
printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms %.1fx faster, %.0f%% of the %s size\n",
|
||||
"YAPF", fmt_kb((double)esz, b), 100.0 * esz / raw, t_one * 1e3,
|
||||
t_src / t_one, 100.0 * esz / fsz, ext_name(path));
|
||||
/* version 2 with the optional Huffman planes */
|
||||
{
|
||||
yapf_texture_t v;
|
||||
void *data[1] = { px };
|
||||
size_t esz2 = 0;
|
||||
void *enc2 = NULL;
|
||||
double t_e;
|
||||
memset(&v, 0, sizeof(v));
|
||||
v.width = (uint32_t)w; v.height = (uint32_t)h; v.depth = 1; v.layers = 1; v.frames = 1;
|
||||
v.mip_levels = 1; v.channels = (uint8_t)ch; v.flags = YAPF_FLAG_SRGB; v.data = data;
|
||||
if (yapf_tex_encode(&v, YAPF_ENC_ENTROPY, &enc2, &esz2) == YAPF_OK) {
|
||||
yapf_texture_t *c2 = yapf_tex_load_memory(enc2, esz2, 0, 1, NULL);
|
||||
if (!c2 || memcmp(c2->data[0], px, (size_t)w * h * ch) != 0) lossless = 0;
|
||||
yapf_tex_free(c2);
|
||||
YAPF_TIME_BEST(t_e, yapf_tex_free(yapf_tex_load_memory(enc2, esz2, 0, 1, NULL)));
|
||||
printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms %.1fx faster, %.0f%% of the %s size\n",
|
||||
"YAPF+E", fmt_kb((double)esz2, c), 100.0 * esz2 / raw, t_e * 1e3,
|
||||
t_src / t_e, 100.0 * esz2 / fsz, ext_name(path));
|
||||
tot_ent += (double)esz2;
|
||||
yapf_free_buffer(enc2);
|
||||
}
|
||||
}
|
||||
printf(" YAPF on all cores: decode %.3f ms (%.1f GB/s), encode %.0f ms, %s\n\n",
|
||||
t_all * 1e3, raw / t_all / 1e9, t_enc * 1e3,
|
||||
lossless ? "lossless: yes" : "LOSSLESS CHECK FAILED");
|
||||
|
|
@ -350,8 +785,10 @@ static int cmd_compare(int n, char **paths) {
|
|||
}
|
||||
if (n > 1 && tot_yapf_t > 0) {
|
||||
char a[32], b[32];
|
||||
printf("Total: originals %s, YAPF %s (%.0f%%); decoding %.1fx faster on one core\n",
|
||||
fmt_kb(tot_src, a), fmt_kb(tot_yapf, b), 100.0 * tot_yapf / tot_src, tot_src_t / tot_yapf_t);
|
||||
char e[32];
|
||||
printf("Total: originals %s, YAPF %s (%.0f%%), YAPF+E %s (%.0f%%); decoding %.1fx faster on one core\n",
|
||||
fmt_kb(tot_src, a), fmt_kb(tot_yapf, b), 100.0 * tot_yapf / tot_src,
|
||||
fmt_kb(tot_ent, e), 100.0 * tot_ent / tot_src, tot_src_t / tot_yapf_t);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
|
@ -359,96 +796,131 @@ static int cmd_compare(int n, char **paths) {
|
|||
static void usage(void) {
|
||||
fprintf(stderr,
|
||||
"usage: yapf <input> <output> [options] convert (formats by extension)\n"
|
||||
" yapf info <file.yapf> show header information\n"
|
||||
" yapf pack <cube|array|volume|anim> <inputs...> <out.yapf> [options]\n"
|
||||
" yapf info <file.yapf> header, tables and notes\n"
|
||||
" yapf verify <file.yapf> check every checksum\n"
|
||||
" yapf bench <file.yapf> time decoding\n"
|
||||
" yapf thumbnail <in.yapf> <out.png> [size] preview, default 256 px\n"
|
||||
" yapf compare <image.png> [more...] size and speed vs the original format\n"
|
||||
"\n"
|
||||
"reads: .yapf .png .jpg .bmp .tga .gif .psd .pnm .hdr\n"
|
||||
"writes: .yapf .png .jpg .bmp .tga\n"
|
||||
"reads: .yapf .png (8/16-bit) .jpg .bmp .tga .gif .psd .pnm .hdr\n"
|
||||
"writes: .yapf .png .jpg .bmp .tga .hdr\n"
|
||||
"\n"
|
||||
"options when writing .yapf:\n"
|
||||
" --mips store a full mip chain\n"
|
||||
" --mips store a full mip chain (sRGB averaged in linear light)\n"
|
||||
" --coverage REF mips keep alpha-test coverage at alpha REF (0-255)\n"
|
||||
" --format F GPU blocks: bc1 bc3 bc4 bc5 bc6h bc7\n"
|
||||
" --entropy Huffman-coded planes: smaller, slower to decode\n"
|
||||
" --linear pixels are linear, not sRGB\n"
|
||||
" --premult alpha is premultiplied\n"
|
||||
" --linear-alpha alpha is linear even in an sRGB image\n"
|
||||
" --normal gl|dx normal map (OpenGL +Y / DirectX -Y); implies linear\n"
|
||||
" --map a,b,c,d channel meanings: r g b a luma nx ny nz rough gloss metal\n"
|
||||
" ao height spec emissive opacity mask cavity thickness id -\n"
|
||||
" --primaries P bt709 p3 bt2020 aces acescg adobe\n"
|
||||
" --transfer T srgb linear gamma22 pq hlg bt1886\n"
|
||||
" --8bit read 16-bit images as 8-bit\n"
|
||||
" --float32 keep .hdr input as 32-bit float (default half)\n"
|
||||
" --frame-ms N animation frame time (pack anim), --loops N\n"
|
||||
" --author S --license S --source S --note TAG=text\n"
|
||||
" --v2 write version 2 even for a plain 8-bit image\n"
|
||||
"options when reading .yapf:\n"
|
||||
" --mip N export mip level N\n");
|
||||
" --mip N --layer N --face N --frame N --slice N which image to export\n");
|
||||
}
|
||||
|
||||
static int parse_opts(int argc, char **argv, int start, opts_t *o) {
|
||||
static const char *prim[] = { "", "bt709", "p3", "bt2020", "aces", "acescg", "adobe" };
|
||||
static const char *tf[] = { "", "srgb", "linear", "gamma22", "pq", "hlg", "bt1886" };
|
||||
memset(o, 0, sizeof(*o));
|
||||
o->srgb = 1;
|
||||
o->format = -1;
|
||||
o->loops = -1;
|
||||
for (int i = start; i < argc; i++) {
|
||||
const char *a = argv[i], *v = i + 1 < argc ? argv[i + 1] : NULL;
|
||||
if (strcmp(a, "--mips") == 0) o->mips = 1;
|
||||
else if (strcmp(a, "--linear") == 0) o->srgb = 0;
|
||||
else if (strcmp(a, "--premult") == 0) o->premult = 1;
|
||||
else if (strcmp(a, "--entropy") == 0) o->entropy = 1;
|
||||
else if (strcmp(a, "--v2") == 0) o->v2 = 1;
|
||||
else if (strcmp(a, "--8bit") == 0) o->eight_bit = 1;
|
||||
else if (strcmp(a, "--float32") == 0) o->float32 = 1;
|
||||
else if (strcmp(a, "--linear-alpha") == 0) o->linear_alpha = 1;
|
||||
else if (!v) return 0;
|
||||
else if (strcmp(a, "--coverage") == 0) { o->coverage = atoi(v); o->mips = 1; i++;
|
||||
if (o->coverage < 1 || o->coverage > 255) return 0; }
|
||||
else if (strcmp(a, "--format") == 0) {
|
||||
size_t k;
|
||||
for (k = 0; k < sizeof(fmt_names) / sizeof(fmt_names[0]); k++)
|
||||
if (strcmp(v, fmt_names[k].name) == 0) o->format = fmt_names[k].v;
|
||||
if (o->format < 0) return 0;
|
||||
i++;
|
||||
}
|
||||
else if (strcmp(a, "--normal") == 0) {
|
||||
o->normal = strcmp(v, "gl") == 0 ? YAPF_NORMAL_GL : strcmp(v, "dx") == 0 ? YAPF_NORMAL_DX : 0;
|
||||
if (!o->normal) return 0;
|
||||
i++;
|
||||
}
|
||||
else if (strcmp(a, "--map") == 0) { if (!parse_map(v, o->map)) return 0; o->have_map = 1; i++; }
|
||||
else if (strcmp(a, "--primaries") == 0 || strcmp(a, "--transfer") == 0) {
|
||||
const char **names = a[2] == 'p' ? prim : tf;
|
||||
int found = 0;
|
||||
for (int k = 1; k < 7; k++) if (strcmp(v, names[k]) == 0) found = k;
|
||||
if (!found) return 0;
|
||||
if (a[2] == 'p') o->primaries = found; else o->transfer = found;
|
||||
i++;
|
||||
}
|
||||
else if (strcmp(a, "--frame-ms") == 0) { o->frame_ms = atof(v); i++; }
|
||||
else if (strcmp(a, "--loops") == 0) { o->loops = atoi(v); i++; }
|
||||
else if (strcmp(a, "--author") == 0) { o->author = v; i++; }
|
||||
else if (strcmp(a, "--license") == 0) { o->license = v; i++; }
|
||||
else if (strcmp(a, "--source") == 0) { o->source = v; i++; }
|
||||
else if (strcmp(a, "--note") == 0) { if (o->nnotes == 16) return 0; o->notes[o->nnotes++] = v; i++; }
|
||||
else if (strcmp(a, "--mip") == 0) { o->mip = atoi(v); i++; }
|
||||
else if (strcmp(a, "--layer") == 0) { o->layer = atoi(v); i++; }
|
||||
else if (strcmp(a, "--face") == 0) { o->face = atoi(v); i++; }
|
||||
else if (strcmp(a, "--frame") == 0) { o->frame = atoi(v); i++; }
|
||||
else if (strcmp(a, "--slice") == 0) { o->slice = atoi(v); i++; }
|
||||
else return 0;
|
||||
}
|
||||
if (o->mip < 0 || o->layer < 0 || o->face < 0 || o->frame < 0 || o->slice < 0) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc == 3 && strcmp(argv[1], "info") == 0) return cmd_info(argv[2]);
|
||||
if (argc == 3 && strcmp(argv[1], "verify") == 0) return cmd_verify(argv[2]);
|
||||
if (argc == 3 && strcmp(argv[1], "bench") == 0) return cmd_bench(argv[2]);
|
||||
if (argc >= 3 && strcmp(argv[1], "compare") == 0) return cmd_compare(argc - 2, argv + 2);
|
||||
if ((argc == 4 || argc == 5) && strcmp(argv[1], "thumbnail") == 0)
|
||||
return cmd_thumbnail(argv[2], argv[3], argc == 5 ? atoi(argv[4]) : 256);
|
||||
if (argc < 3) { usage(); return 2; }
|
||||
|
||||
const char *in = argv[1], *out = argv[2];
|
||||
int mips = 0, srgb = 1, premult = 0, mip_out = 0;
|
||||
for (int i = 3; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--mips") == 0) mips = 1;
|
||||
else if (strcmp(argv[i], "--linear") == 0) srgb = 0;
|
||||
else if (strcmp(argv[i], "--premult") == 0) premult = 1;
|
||||
else if (strcmp(argv[i], "--mip") == 0 && i + 1 < argc) mip_out = atoi(argv[++i]);
|
||||
else { usage(); return 2; }
|
||||
}
|
||||
|
||||
/* Read */
|
||||
yapf_image_t src;
|
||||
yapf_image_t *loaded = NULL;
|
||||
uint8_t *stb_px = NULL;
|
||||
memset(&src, 0, sizeof(src));
|
||||
if (is_yapf(in)) {
|
||||
loaded = yapf_load_mt(in, 0);
|
||||
if (!loaded) { fprintf(stderr, "yapf: cannot read %s (missing, corrupt or not YAPF v1)\n", in); return 1; }
|
||||
src = *loaded;
|
||||
} else {
|
||||
int w, h, ch;
|
||||
if (stbi_is_16_bit(in))
|
||||
fprintf(stderr, "yapf: note: %s has 16-bit channels; YAPF stores 8 bits per channel\n", in);
|
||||
stb_px = stbi_load(in, &w, &h, &ch, 0);
|
||||
if (!stb_px) { fprintf(stderr, "yapf: cannot read %s: %s\n", in, stbi_failure_reason()); return 1; }
|
||||
src.width = (uint32_t)w; src.height = (uint32_t)h; src.channels = (uint8_t)ch;
|
||||
src.pixels = stb_px; src.mip_levels = 1;
|
||||
src.flags = srgb ? YAPF_FLAG_SRGB : 0;
|
||||
src.gpu_format = gpu_hint(ch, srgb);
|
||||
}
|
||||
|
||||
/* Write */
|
||||
int rc = 0;
|
||||
if (is_yapf(out)) {
|
||||
yapf_image_t dst = src;
|
||||
if (!loaded) {
|
||||
if (premult) dst.flags |= YAPF_FLAG_PREMULT_ALPHA;
|
||||
}
|
||||
dst.mips = NULL;
|
||||
dst.mip_levels = 1;
|
||||
if (mips && build_mips(&dst) != 0) { fprintf(stderr, "yapf: out of memory\n"); rc = 1; }
|
||||
if (!rc && yapf_save(out, &dst) != YAPF_OK) { fprintf(stderr, "yapf: cannot write %s\n", out); rc = 1; }
|
||||
if (dst.mips) {
|
||||
for (int m = 1; m < dst.mip_levels; m++) free(dst.mips[m]);
|
||||
free(dst.mips);
|
||||
}
|
||||
} else {
|
||||
const uint8_t *px = src.pixels;
|
||||
uint32_t w = src.width, h = src.height;
|
||||
if (mip_out > 0) {
|
||||
if (!loaded || mip_out >= src.mip_levels) {
|
||||
fprintf(stderr, "yapf: %s has no mip level %d\n", in, mip_out);
|
||||
rc = 1;
|
||||
} else {
|
||||
px = src.mips[mip_out];
|
||||
w = src.width >> mip_out ? src.width >> mip_out : 1;
|
||||
h = src.height >> mip_out ? src.height >> mip_out : 1;
|
||||
}
|
||||
}
|
||||
if (!rc && !write_image(out, px, (int)w, (int)h, src.channels)) {
|
||||
fprintf(stderr, "yapf: cannot write %s\n", out);
|
||||
rc = 1;
|
||||
}
|
||||
}
|
||||
|
||||
yapf_free(loaded);
|
||||
stbi_image_free(stb_px);
|
||||
opts_t o;
|
||||
if (argc >= 5 && strcmp(argv[1], "pack") == 0) {
|
||||
int first_opt = 3;
|
||||
while (first_opt < argc && strncmp(argv[first_opt], "--", 2) != 0) first_opt++;
|
||||
int n = first_opt - 3 - 1; /* inputs, then the output */
|
||||
if (n < 1 || !parse_opts(argc, argv, first_opt, &o)) { usage(); return 2; }
|
||||
const char *out = argv[first_opt - 1];
|
||||
if (!is_yapf(out)) { fprintf(stderr, "yapf: pack writes a .yapf file\n"); return 2; }
|
||||
if (o.frame_ms <= 0) o.frame_ms = 100;
|
||||
yapf_texture_t *t = assemble(argv[2], n, argv + 3, &o);
|
||||
if (!t) return 1;
|
||||
int rc = write_yapf(t, out, &o, 0);
|
||||
yapf_tex_free(t);
|
||||
return rc;
|
||||
}
|
||||
if (argc < 3 || !parse_opts(argc, argv, 3, &o)) { usage(); return 2; }
|
||||
|
||||
const char *in = argv[1], *out = argv[2];
|
||||
int from_yapf;
|
||||
yapf_texture_t *t = read_input(in, &o, &from_yapf);
|
||||
if (!t) return 1;
|
||||
int rc;
|
||||
if (is_yapf(out)) {
|
||||
rc = write_yapf(t, out, &o, from_yapf);
|
||||
} else {
|
||||
rc = export_image(t, in, out, &o);
|
||||
}
|
||||
yapf_tex_free(t);
|
||||
return rc;
|
||||
}
|
||||
|
|
|
|||
359
yapf.h
359
yapf.h
|
|
@ -38,6 +38,13 @@
|
|||
* loader can upload the decoded buffer straight to a texture without any
|
||||
* additional transcoding step.
|
||||
*
|
||||
* Two versions exist. Version 1 (below) is a single 8-bit 2D image with
|
||||
* optional mips. Version 2 is the texture container: GPU block payloads
|
||||
* (BCn / ETC2 / ASTC), 16-bit and float samples, cubemaps, arrays and
|
||||
* volumes, channel meanings, per-tile checksums, optional entropy-coded
|
||||
* planes, notes (metadata) and animation frames. Its complete
|
||||
* specification is docs/FORMAT.md; a summary follows the v1 spec.
|
||||
*
|
||||
* =========================================================================
|
||||
* FILE FORMAT (version 1)
|
||||
* =========================================================================
|
||||
|
|
@ -140,6 +147,37 @@
|
|||
* ─────────────────────────────────────────────────
|
||||
* bit 0 PREMULT_ALPHA alpha is premultiplied into RGB
|
||||
* bit 1 SRGB pixels are in sRGB gamma (informational only)
|
||||
* bit 2 COVERAGE (v2) mips keep alpha-test coverage at alpha_ref
|
||||
*
|
||||
* =========================================================================
|
||||
* FILE FORMAT (version 2) — summary; the full text is docs/FORMAT.md
|
||||
* =========================================================================
|
||||
*
|
||||
* HEADER (64 bytes)
|
||||
* 0x00 "YAPF" 0x04 version 2 0x05 channels 0x06 format
|
||||
* 0x07 mips 0x08 flags 0x09 payload 0x0A sample 0x0B shape
|
||||
* 0x0C width u32 0x10 height u32 0x14 depth u32
|
||||
* 0x18 layers u16 0x1A frames u16 0x1C needs u32
|
||||
* 0x20 channel map[4] 0x24 linear mask 0x25 normal 0x26 primaries
|
||||
* 0x27 transfer 0x28 mip filter 0x29 alpha ref 0x2A loop count u16
|
||||
* 0x2C notes size u32 0x30 file size u64
|
||||
* 0x38 tables check u32 (yapf-sum of bytes [0x40, data start))
|
||||
* 0x3C header check u32 (yapf-sum of bytes [0, 0x3C))
|
||||
* SURFACE TABLE 16 bytes per surface: offset u64, size u32,
|
||||
* tile-table check u32. Surface s of (frame, layer,
|
||||
* face, mip) is ((frame·layers + layer)·faces + face)·mips + mip
|
||||
* FRAME TABLE u32 duration in µs per frame (only when frames > 1)
|
||||
* NOTES tag[4], length u32, bytes — repeated
|
||||
* SURFACE DATA smallest mips first, so a file prefix is a complete
|
||||
* low-resolution texture. Per surface: u32 book size,
|
||||
* code book (Huffman tables by slot and context, only
|
||||
* with entropy coding), tile table (size u32 with bit 31 =
|
||||
* packed, yapf-sum u32 per tile), then the tiles. Pixel
|
||||
* tiles are the v1 tiles plus byte planes for wide samples
|
||||
* and entropy-coded planes; block tiles hold 16 × 16 GPU
|
||||
* blocks, raw or packed lane by lane.
|
||||
* NEEDS a decoder must refuse a file with a needs bit it does
|
||||
* not implement; everything else may be ignored safely.
|
||||
*/
|
||||
|
||||
#ifndef YAPF_H
|
||||
|
|
@ -156,11 +194,58 @@
|
|||
#define YAPF_GPU_R8 0x03
|
||||
#define YAPF_GPU_SRGB8_A8 0x04
|
||||
#define YAPF_GPU_SRGB8 0x05
|
||||
/* v2 pixel hints for wide samples */
|
||||
#define YAPF_GPU_RGBA16 0x06
|
||||
#define YAPF_GPU_RGB16 0x07
|
||||
#define YAPF_GPU_RG16 0x08
|
||||
#define YAPF_GPU_R16 0x09
|
||||
#define YAPF_GPU_RGBA16F 0x0A
|
||||
#define YAPF_GPU_RGB16F 0x0B
|
||||
#define YAPF_GPU_RG16F 0x0C
|
||||
#define YAPF_GPU_R16F 0x0D
|
||||
#define YAPF_GPU_RGBA32F 0x0E
|
||||
#define YAPF_GPU_RGB32F 0x0F
|
||||
#define YAPF_GPU_RG32F 0x10
|
||||
#define YAPF_GPU_R32F 0x11
|
||||
/* v2 GPU block formats (payload = YAPF_PAYLOAD_BLOCKS). The sRGB flag
|
||||
* selects the _SRGB variant where the API has one. */
|
||||
#define YAPF_GPU_BC1 0x40 /* RGB(A1), 8 bytes per 4×4 */
|
||||
#define YAPF_GPU_BC2 0x41 /* RGBA, explicit 4-bit alpha */
|
||||
#define YAPF_GPU_BC3 0x42 /* RGBA */
|
||||
#define YAPF_GPU_BC4 0x43 /* R */
|
||||
#define YAPF_GPU_BC4_SNORM 0x44
|
||||
#define YAPF_GPU_BC5 0x45 /* RG (normal maps) */
|
||||
#define YAPF_GPU_BC5_SNORM 0x46
|
||||
#define YAPF_GPU_BC6H 0x47 /* RGB half float, unsigned */
|
||||
#define YAPF_GPU_BC6H_SF 0x48 /* RGB half float, signed */
|
||||
#define YAPF_GPU_BC7 0x49 /* RGB(A) */
|
||||
#define YAPF_GPU_ETC2_RGB8 0x50
|
||||
#define YAPF_GPU_ETC2_RGB8A1 0x51
|
||||
#define YAPF_GPU_ETC2_RGBA8 0x52
|
||||
#define YAPF_GPU_EAC_R11 0x53
|
||||
#define YAPF_GPU_EAC_R11_SNORM 0x54
|
||||
#define YAPF_GPU_EAC_RG11 0x55
|
||||
#define YAPF_GPU_EAC_RG11_SNORM 0x56
|
||||
#define YAPF_GPU_ASTC_4x4 0x60 /* ASTC: 0x60 + footprint index */
|
||||
#define YAPF_GPU_ASTC_5x4 0x61
|
||||
#define YAPF_GPU_ASTC_5x5 0x62
|
||||
#define YAPF_GPU_ASTC_6x5 0x63
|
||||
#define YAPF_GPU_ASTC_6x6 0x64
|
||||
#define YAPF_GPU_ASTC_8x5 0x65
|
||||
#define YAPF_GPU_ASTC_8x6 0x66
|
||||
#define YAPF_GPU_ASTC_8x8 0x67
|
||||
#define YAPF_GPU_ASTC_10x5 0x68
|
||||
#define YAPF_GPU_ASTC_10x6 0x69
|
||||
#define YAPF_GPU_ASTC_10x8 0x6A
|
||||
#define YAPF_GPU_ASTC_10x10 0x6B
|
||||
#define YAPF_GPU_ASTC_12x10 0x6C
|
||||
#define YAPF_GPU_ASTC_12x12 0x6D
|
||||
|
||||
/* ── Flags ───────────────────────────────────────────────────────────── */
|
||||
|
||||
#define YAPF_FLAG_PREMULT_ALPHA 0x01
|
||||
#define YAPF_FLAG_SRGB 0x02
|
||||
#define YAPF_FLAG_COVERAGE 0x04 /* v2: mips preserve alpha-test coverage */
|
||||
|
||||
/* ── Channel constants ───────────────────────────────────────────────── */
|
||||
|
||||
|
|
@ -176,12 +261,16 @@
|
|||
#define YAPF_ERR_IO -2
|
||||
#define YAPF_ERR_OOM -3
|
||||
#define YAPF_ERR_CORRUPT -4
|
||||
#define YAPF_ERR_UNSUPPORTED -5 /* valid file, feature this decoder lacks */
|
||||
#define YAPF_ERR_CHECKSUM -6 /* a yapf-sum does not match */
|
||||
#define YAPF_ERR_TRUNCATED -7 /* data lies beyond the bytes available yet */
|
||||
|
||||
/* ── Limits ──────────────────────────────────────────────────────────── */
|
||||
|
||||
#define YAPF_MAX_DIM 65535u
|
||||
#define YAPF_MAX_MIPS 16u
|
||||
#define YAPF_TILE_SIZE 64u
|
||||
#define YAPF_BLOCK_TILE 16u /* v2 block tiles: 16 × 16 GPU blocks */
|
||||
|
||||
/* ── Image descriptor ────────────────────────────────────────────────── */
|
||||
|
||||
|
|
@ -196,6 +285,150 @@ typedef struct {
|
|||
uint8_t **mips; /* mips[0]=base alias, mips[1..mip_levels-1] owned */
|
||||
} yapf_image_t;
|
||||
|
||||
/* ── Version 2: texture description ──────────────────────────────────── */
|
||||
|
||||
/* payload */
|
||||
#define YAPF_PAYLOAD_PIXELS 0 /* lossless YAPF tiles */
|
||||
#define YAPF_PAYLOAD_BLOCKS 1 /* GPU blocks, uploaded as they are */
|
||||
|
||||
/* sample: how one channel value is stored (pixel payload); for block
|
||||
* payloads, the type of the decoded texels (U8, or F16 for BC6H) */
|
||||
#define YAPF_SAMPLE_U8 0
|
||||
#define YAPF_SAMPLE_U16 1
|
||||
#define YAPF_SAMPLE_F16 2 /* IEEE half, as uint16_t bits */
|
||||
#define YAPF_SAMPLE_F32 3
|
||||
|
||||
/* shape */
|
||||
#define YAPF_SHAPE_2D 0
|
||||
#define YAPF_SHAPE_CUBE 1 /* 6 faces: +X −X +Y −Y +Z −Z; with layers
|
||||
> 1 a cube array */
|
||||
#define YAPF_SHAPE_VOLUME 2 /* depth slices; mips halve depth too */
|
||||
|
||||
/* needs: features a decoder must implement to read the file */
|
||||
#define YAPF_NEED_WIDE 0x01 /* sample other than U8 (pixel payload) */
|
||||
#define YAPF_NEED_BLOCKS 0x02 /* GPU block payload */
|
||||
#define YAPF_NEED_ENTROPY 0x04 /* code books: entropy planes, packed blocks */
|
||||
#define YAPF_NEED_LAYOUT 0x08 /* cube, volume or layers > 1 */
|
||||
#define YAPF_NEED_FRAMES 0x10 /* frames > 1 */
|
||||
#define YAPF_NEED_ALL 0x1F /* everything this library implements */
|
||||
|
||||
/* channel meanings (channel_map[c]); 0 = default for the channel count */
|
||||
#define YAPF_CH_DEFAULT 0
|
||||
#define YAPF_CH_RED 1
|
||||
#define YAPF_CH_GREEN 2
|
||||
#define YAPF_CH_BLUE 3
|
||||
#define YAPF_CH_ALPHA 4
|
||||
#define YAPF_CH_LUMA 5
|
||||
#define YAPF_CH_NORMAL_X 6
|
||||
#define YAPF_CH_NORMAL_Y 7
|
||||
#define YAPF_CH_NORMAL_Z 8
|
||||
#define YAPF_CH_ROUGHNESS 9
|
||||
#define YAPF_CH_GLOSS 10 /* smoothness = 1 − roughness */
|
||||
#define YAPF_CH_METALNESS 11
|
||||
#define YAPF_CH_OCCLUSION 12 /* ambient occlusion */
|
||||
#define YAPF_CH_HEIGHT 13
|
||||
#define YAPF_CH_SPECULAR 14
|
||||
#define YAPF_CH_EMISSIVE 15
|
||||
#define YAPF_CH_OPACITY 16 /* alpha-test / cut-out mask */
|
||||
#define YAPF_CH_MASK 17 /* generic mask */
|
||||
#define YAPF_CH_CAVITY 18
|
||||
#define YAPF_CH_THICKNESS 19
|
||||
#define YAPF_CH_ID 20 /* material or object id */
|
||||
#define YAPF_CH_UNUSED 255
|
||||
|
||||
/* normal: bits 0–1 convention, bits 2–3 encoding */
|
||||
#define YAPF_NORMAL_NONE 0x00
|
||||
#define YAPF_NORMAL_GL 0x01 /* +Y up (OpenGL, Blender, Unity) */
|
||||
#define YAPF_NORMAL_DX 0x02 /* −Y (DirectX, Unreal) */
|
||||
#define YAPF_NORMAL_XYZ 0x00
|
||||
#define YAPF_NORMAL_XY 0x04 /* Z = sqrt(1 − x² − y²) is reconstructed */
|
||||
#define YAPF_NORMAL_OCT 0x08 /* octahedral */
|
||||
|
||||
/* primaries / transfer (0 = unspecified: the sRGB flag decides) */
|
||||
#define YAPF_PRIMARIES_UNSPECIFIED 0
|
||||
#define YAPF_PRIMARIES_BT709 1 /* = sRGB */
|
||||
#define YAPF_PRIMARIES_DISPLAY_P3 2
|
||||
#define YAPF_PRIMARIES_BT2020 3
|
||||
#define YAPF_PRIMARIES_ACES_AP0 4
|
||||
#define YAPF_PRIMARIES_ACES_AP1 5 /* ACEScg */
|
||||
#define YAPF_PRIMARIES_ADOBE_RGB 6
|
||||
|
||||
#define YAPF_TRANSFER_UNSPECIFIED 0
|
||||
#define YAPF_TRANSFER_SRGB 1
|
||||
#define YAPF_TRANSFER_LINEAR 2
|
||||
#define YAPF_TRANSFER_GAMMA22 3
|
||||
#define YAPF_TRANSFER_PQ 4 /* SMPTE ST 2084 */
|
||||
#define YAPF_TRANSFER_HLG 5
|
||||
#define YAPF_TRANSFER_BT1886 6 /* gamma 2.4 */
|
||||
|
||||
/* mip filter (bits 0–6) | YAPF_MIP_LINEAR_LIGHT */
|
||||
#define YAPF_MIP_UNKNOWN 0
|
||||
#define YAPF_MIP_BOX 1
|
||||
#define YAPF_MIP_TENT 2
|
||||
#define YAPF_MIP_KAISER 3
|
||||
#define YAPF_MIP_LANCZOS 4
|
||||
#define YAPF_MIP_MITCHELL 5
|
||||
#define YAPF_MIP_AUTHORED 6 /* made by hand / by an external tool */
|
||||
#define YAPF_MIP_LINEAR_LIGHT 0x80 /* sRGB channels were filtered in linear light */
|
||||
|
||||
/* load flags */
|
||||
#define YAPF_LOAD_TRUST 0x01 /* skip checksum verification */
|
||||
|
||||
/* encode flags */
|
||||
#define YAPF_ENC_ENTROPY 0x01 /* allow entropy-coded planes and packed
|
||||
blocks where they are smaller: ~10 %
|
||||
smaller photos, ~3× smaller GPU blocks
|
||||
on disk; slower to decode */
|
||||
|
||||
/* A note (metadata entry). Well-known tags: "AUTH" author, "LICN"
|
||||
* licence, "SRCE" source, "TOOL" creating tool, "TITL" title, "DESC"
|
||||
* description, "DATE" ISO 8601 date (all UTF-8 text); "ICCP" ICC profile;
|
||||
* "ORNT" orientation (1 byte); "DPI " 2 × u32 milli-dots per inch;
|
||||
* "CLL " 2 × u16 max content / max frame-average light level (cd/m²).
|
||||
* Tags that start with a lowercase letter belong to applications. */
|
||||
typedef struct {
|
||||
char tag[4];
|
||||
uint32_t size;
|
||||
uint8_t *data;
|
||||
} yapf_note_t;
|
||||
|
||||
/*
|
||||
* A version 2 texture. Surfaces are numbered
|
||||
* s = ((frame × layers + layer) × faces + face) × mip_levels + mip
|
||||
* (faces = 6 for cubes, else 1) and data[s] holds the whole surface:
|
||||
* pixels: width_m × height_m × depth_m × channels samples, rows top to
|
||||
* bottom, slices in order; samples in the native byte order
|
||||
* (uint8_t, uint16_t, half bits as uint16_t, float);
|
||||
* blocks: ceil(width_m / bw) × ceil(height_m / bh) blocks per slice,
|
||||
* row-major — exactly what glCompressedTexImage / Vulkan take.
|
||||
* width_m = max(1, width >> m), likewise height and (volumes) depth.
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t width, height, depth; /* depth: volumes only, else 1 */
|
||||
uint16_t layers; /* array layers, >= 1 */
|
||||
uint16_t frames; /* animation frames, >= 1 */
|
||||
uint8_t shape; /* YAPF_SHAPE_* */
|
||||
uint8_t mip_levels; /* 1–16 */
|
||||
uint8_t payload; /* YAPF_PAYLOAD_* */
|
||||
uint8_t format; /* YAPF_GPU_*: block format, or hint */
|
||||
uint8_t sample; /* YAPF_SAMPLE_* */
|
||||
uint8_t channels; /* 1–4 */
|
||||
uint8_t flags; /* YAPF_FLAG_* */
|
||||
uint8_t channel_map[4]; /* YAPF_CH_* per channel */
|
||||
uint8_t linear_mask; /* bit c: channel c is linear even in
|
||||
an sRGB image (e.g. alpha, masks) */
|
||||
uint8_t normal; /* YAPF_NORMAL_* */
|
||||
uint8_t primaries, transfer; /* YAPF_PRIMARIES_*, YAPF_TRANSFER_* */
|
||||
uint8_t mip_filter; /* YAPF_MIP_* */
|
||||
uint8_t alpha_ref; /* coverage reference (with COVERAGE) */
|
||||
uint16_t loop_count; /* animation loops, 0 = forever */
|
||||
uint32_t *frame_us; /* frames durations in µs, or NULL */
|
||||
uint32_t note_count;
|
||||
yapf_note_t *notes;
|
||||
void **data; /* one pointer per surface */
|
||||
void *internal_; /* library use: file tables, ownership */
|
||||
} yapf_texture_t;
|
||||
|
||||
/* ── Public API ──────────────────────────────────────────────────────── */
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
@ -249,6 +482,132 @@ void yapf_free_buffer(void *data);
|
|||
*/
|
||||
void yapf_free(yapf_image_t *img);
|
||||
|
||||
/*
|
||||
* The v1 functions above also read version 2 files that hold 8-bit pixels
|
||||
* of a 2D, cube or array texture: they return frame 0, layer 0, face 0
|
||||
* with all its mips. yapf_save / yapf_encode always write version 1.
|
||||
*/
|
||||
|
||||
/* ── Version 2: textures ─────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* yapf_tex_create — a library-owned texture with the description of
|
||||
* `desc` (every field up to loop_count; notes, frame times and data are
|
||||
* not copied) and zeroed storage for every surface. frame_us is
|
||||
* allocated (zeroed) when frames > 1. NULL on invalid description / OOM.
|
||||
*/
|
||||
yapf_texture_t *yapf_tex_create(const yapf_texture_t *desc);
|
||||
|
||||
/* yapf_tex_free — release a texture made by the library. NULL-safe. */
|
||||
void yapf_tex_free(yapf_texture_t *tex);
|
||||
|
||||
/* Surface helpers. */
|
||||
uint32_t yapf_tex_faces(const yapf_texture_t *tex); /* 6 or 1 */
|
||||
uint32_t yapf_tex_surface_count(const yapf_texture_t *tex);
|
||||
uint32_t yapf_tex_surface(const yapf_texture_t *tex, uint32_t frame,
|
||||
uint32_t layer, uint32_t face, uint32_t mip);
|
||||
void yapf_tex_mip_size(const yapf_texture_t *tex, uint32_t mip,
|
||||
uint32_t *w, uint32_t *h, uint32_t *d);
|
||||
size_t yapf_tex_surface_bytes(const yapf_texture_t *tex, uint32_t mip);
|
||||
size_t yapf_sample_bytes(uint8_t sample); /* 1, 2 or 4 */
|
||||
|
||||
/* Notes: add (library-owned textures only; data is copied) and find. */
|
||||
int yapf_tex_add_note(yapf_texture_t *tex, const char tag[4],
|
||||
const void *data, uint32_t size);
|
||||
const yapf_note_t *yapf_tex_find_note(const yapf_texture_t *tex, const char tag[4]);
|
||||
|
||||
/*
|
||||
* yapf_tex_encode / yapf_tex_save — write a version 2 file. `flags` is
|
||||
* YAPF_ENC_*. Works on library-owned and caller-built textures alike
|
||||
* (nothing is taken over). Returns YAPF_OK or a YAPF_ERR_* code.
|
||||
*/
|
||||
int yapf_tex_encode(const yapf_texture_t *tex, int flags,
|
||||
void **out_data, size_t *out_size);
|
||||
int yapf_tex_save(const char *filename, const yapf_texture_t *tex, int flags);
|
||||
|
||||
/*
|
||||
* yapf_tex_load / yapf_tex_load_memory — read a version 1 or 2 file and
|
||||
* decode every surface on up to `threads` threads (0 = one per core).
|
||||
* `flags` is YAPF_LOAD_*. On failure NULL is returned and *err (if not
|
||||
* NULL) receives the YAPF_ERR_* reason.
|
||||
*/
|
||||
yapf_texture_t *yapf_tex_load(const char *filename, int flags, int threads, int *err);
|
||||
yapf_texture_t *yapf_tex_load_memory(const void *buffer, size_t size,
|
||||
int flags, int threads, int *err);
|
||||
|
||||
/*
|
||||
* Streaming and partial decoding.
|
||||
*
|
||||
* yapf_tex_open — read only the header, tables and notes. `avail` may be
|
||||
* less than the whole file (a download in progress): it must cover the
|
||||
* front matter, else YAPF_ERR_TRUNCATED. All data[] pointers are NULL.
|
||||
*
|
||||
* yapf_tex_stream_bytes — how many leading bytes of the file hold every
|
||||
* surface of mip `first_mip` and smaller (the file stores small mips
|
||||
* first, so these are a usable texture on their own).
|
||||
*
|
||||
* yapf_tex_decode_surface — decode surface s into tex->data[s]
|
||||
* (allocated). `buffer`/`avail` is the file or a prefix of it; returns
|
||||
* YAPF_ERR_TRUNCATED when the surface is not in it yet.
|
||||
*
|
||||
* yapf_tex_tile_grid / yapf_tex_decode_tile — virtual texturing: the
|
||||
* surfaces of mip m are cut into tiles (64 × 64 pixels, or 16 × 16
|
||||
* blocks), tx across, ty down, per slice. decode_tile writes one tile
|
||||
* into `out`, rows `stride` bytes apart, and reports its pixel rectangle.
|
||||
*/
|
||||
yapf_texture_t *yapf_tex_open(const void *buffer, size_t avail, int flags, int *err);
|
||||
uint64_t yapf_tex_stream_bytes(const yapf_texture_t *tex, uint32_t first_mip);
|
||||
int yapf_tex_decode_surface(yapf_texture_t *tex, const void *buffer, size_t avail,
|
||||
uint32_t surface, int threads);
|
||||
void yapf_tex_tile_grid(const yapf_texture_t *tex, uint32_t mip,
|
||||
uint32_t *tiles_x, uint32_t *tiles_y);
|
||||
int yapf_tex_decode_tile(const yapf_texture_t *tex, const void *buffer, size_t avail,
|
||||
uint32_t surface, uint32_t tx, uint32_t ty, uint32_t slice,
|
||||
void *out, size_t stride,
|
||||
uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h);
|
||||
|
||||
/*
|
||||
* yapf_tex_build_mips — replace the mips of a library-owned pixel texture
|
||||
* with a chain made from mip 0: `levels` levels (0 = down to 1 × 1).
|
||||
* 2 × 2 (× 2 for volumes) box filter; sRGB channels are averaged in
|
||||
* linear light. alpha_ref > 0 rescales alpha on every mip so the share
|
||||
* of texels above alpha_ref stays that of mip 0 (fences and foliage do
|
||||
* not thin out); it sets YAPF_FLAG_COVERAGE. Records the filter used.
|
||||
*/
|
||||
int yapf_tex_build_mips(yapf_texture_t *tex, uint32_t levels, uint8_t alpha_ref);
|
||||
|
||||
/* ── GPU blocks ──────────────────────────────────────────────────────── */
|
||||
|
||||
/* Block footprint and size of a YAPF_GPU_* block format; 0 if not one. */
|
||||
int yapf_block_info(uint8_t format, uint32_t *block_w, uint32_t *block_h,
|
||||
uint32_t *block_bytes);
|
||||
|
||||
/*
|
||||
* yapf_tex_compress — a new library-owned texture holding `src` (pixel
|
||||
* payload) as GPU blocks. Built-in encoders: BC1, BC3, BC4, BC5, BC7 and
|
||||
* BC6H (unsigned). LDR formats take 8-bit pixels; BC6H takes 16-bit,
|
||||
* half or float pixels. For other formats bring blocks from your own
|
||||
* encoder: a payload = BLOCKS texture with data[] filled.
|
||||
*/
|
||||
int yapf_tex_compress(const yapf_texture_t *src, uint8_t format,
|
||||
yapf_texture_t **out);
|
||||
|
||||
/*
|
||||
* yapf_blocks_decode — decode a w × h image of BC1–BC7 blocks (previews,
|
||||
* tools). LDR formats write RGBA8 (BC4 / BC5: R / RG, then 0, 255;
|
||||
* snorm values shifted by +128); BC6H writes RGB half floats. Returns
|
||||
* YAPF_ERR_UNSUPPORTED for ETC2 / EAC / ASTC.
|
||||
*/
|
||||
int yapf_blocks_decode(uint8_t format, const void *blocks, uint32_t w, uint32_t h,
|
||||
void *out);
|
||||
|
||||
/* yapf-sum, the checksum used by version 2 files. */
|
||||
uint32_t yapf_checksum(const void *data, size_t size);
|
||||
|
||||
/* Half float conversion helpers. */
|
||||
uint16_t yapf_float_to_half(float f);
|
||||
float yapf_half_to_float(uint16_t h);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Reference in a new issue